@agenticmail/core 0.3.3 → 0.4.0

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.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { EventEmitter } from 'node:events';
2
- import Database from 'better-sqlite3';
2
+ import Database, { Database as Database$1 } from 'better-sqlite3';
3
3
  import { ImapFlow } from 'imapflow';
4
4
 
5
5
  interface SendMailOptions {
@@ -181,6 +181,13 @@ interface AgenticMailConfig {
181
181
  mode?: 'relay' | 'domain' | 'none';
182
182
  autoResume?: boolean;
183
183
  };
184
+ sms?: {
185
+ enabled?: boolean;
186
+ phoneNumber?: string;
187
+ forwardingEmail?: string;
188
+ provider?: 'google_voice';
189
+ configuredAt?: string;
190
+ };
184
191
  masterKey: string;
185
192
  dataDir: string;
186
193
  }
@@ -230,6 +237,7 @@ declare class StalwartAdmin {
230
237
  * Note: stalwart-cli may return a 500 error even when the operation succeeds.
231
238
  * We verify by listing the config afterwards.
232
239
  */
240
+ private cliArgs;
233
241
  updateSetting(key: string, value: string): Promise<void>;
234
242
  /**
235
243
  * Set the server hostname used in SMTP EHLO greetings.
@@ -1077,6 +1085,8 @@ declare class GatewayManager {
1077
1085
  private tunnel;
1078
1086
  private dnsConfigurator;
1079
1087
  private domainPurchaser;
1088
+ private smsManager;
1089
+ private smsPollers;
1080
1090
  constructor(options: GatewayManagerOptions);
1081
1091
  /**
1082
1092
  * Check if a message has already been delivered to an agent (deduplication).
@@ -1189,6 +1199,11 @@ declare class GatewayManager {
1189
1199
  success: boolean;
1190
1200
  error?: string;
1191
1201
  }>;
1202
+ /**
1203
+ * Start SMS pollers for all agents that have separate GV Gmail credentials.
1204
+ * Agents with sameAsRelay=true are handled in deliverInboundLocally.
1205
+ */
1206
+ private startSmsPollers;
1192
1207
  shutdown(): Promise<void>;
1193
1208
  /**
1194
1209
  * Resume gateway from saved config (e.g., after server restart).
@@ -1236,6 +1251,144 @@ declare class RelayBridge {
1236
1251
  }
1237
1252
  declare function startRelayBridge(options: RelayBridgeOptions): RelayBridge;
1238
1253
 
1254
+ /**
1255
+ * SMS Manager - Google Voice SMS integration
1256
+ *
1257
+ * How it works:
1258
+ * 1. User sets up Google Voice with SMS-to-email forwarding
1259
+ * 2. Incoming SMS arrives at Google Voice -> forwarded to email -> lands in agent inbox
1260
+ * 3. Agent parses forwarded SMS from email body
1261
+ * 4. Outgoing SMS sent via Google Voice web interface (browser automation)
1262
+ *
1263
+ * SMS config is stored in agent metadata under the "sms" key.
1264
+ */
1265
+
1266
+ interface SmsConfig {
1267
+ /** Whether SMS is enabled for this agent */
1268
+ enabled: boolean;
1269
+ /** Google Voice phone number (e.g. +12125551234) */
1270
+ phoneNumber: string;
1271
+ /** The email address Google Voice forwards SMS to (the Gmail used for GV signup) */
1272
+ forwardingEmail: string;
1273
+ /** App password for forwarding email (only needed if different from relay email) */
1274
+ forwardingPassword?: string;
1275
+ /** Whether the GV Gmail is the same as the relay email */
1276
+ sameAsRelay?: boolean;
1277
+ /** Provider (currently only google_voice) */
1278
+ provider: 'google_voice';
1279
+ /** When SMS was configured */
1280
+ configuredAt: string;
1281
+ }
1282
+ interface ParsedSms {
1283
+ from: string;
1284
+ body: string;
1285
+ timestamp: string;
1286
+ raw?: string;
1287
+ }
1288
+ interface SmsMessage {
1289
+ id: string;
1290
+ agentId: string;
1291
+ direction: 'inbound' | 'outbound';
1292
+ phoneNumber: string;
1293
+ body: string;
1294
+ status: 'pending' | 'sent' | 'delivered' | 'failed' | 'received';
1295
+ createdAt: string;
1296
+ metadata?: Record<string, unknown>;
1297
+ }
1298
+ /** Normalize a phone number to E.164-ish format (+1XXXXXXXXXX) */
1299
+ declare function normalizePhoneNumber(raw: string): string | null;
1300
+ /** Validate a phone number (basic) */
1301
+ declare function isValidPhoneNumber(phone: string): boolean;
1302
+ /**
1303
+ * Parse an SMS forwarded from Google Voice via email.
1304
+ * Google Voice forwards SMS with a specific format.
1305
+ *
1306
+ * Known sender addresses:
1307
+ * - voice-noreply@google.com
1308
+ * - *@txt.voice.google.com
1309
+ * - Google Voice <voice-noreply@google.com>
1310
+ */
1311
+ declare function parseGoogleVoiceSms(emailBody: string, emailFrom: string): ParsedSms | null;
1312
+ /**
1313
+ * Extract verification codes from SMS body.
1314
+ * Supports common formats: 6-digit, 4-digit, alphanumeric codes.
1315
+ */
1316
+ declare function extractVerificationCode(smsBody: string): string | null;
1317
+ declare class SmsManager {
1318
+ private db;
1319
+ private initialized;
1320
+ constructor(db: Database$1);
1321
+ private ensureTable;
1322
+ /** Get SMS config from agent metadata */
1323
+ getSmsConfig(agentId: string): SmsConfig | null;
1324
+ /** Save SMS config to agent metadata */
1325
+ saveSmsConfig(agentId: string, config: SmsConfig): void;
1326
+ /** Remove SMS config from agent metadata */
1327
+ removeSmsConfig(agentId: string): void;
1328
+ /** Record an inbound SMS (parsed from email) */
1329
+ recordInbound(agentId: string, parsed: ParsedSms): SmsMessage;
1330
+ /** Record an outbound SMS attempt */
1331
+ recordOutbound(agentId: string, phoneNumber: string, body: string, status?: 'pending' | 'sent' | 'failed'): SmsMessage;
1332
+ /** Update SMS status */
1333
+ updateStatus(id: string, status: SmsMessage['status']): void;
1334
+ /** List SMS messages for an agent */
1335
+ listMessages(agentId: string, opts?: {
1336
+ direction?: 'inbound' | 'outbound';
1337
+ limit?: number;
1338
+ offset?: number;
1339
+ }): SmsMessage[];
1340
+ /** Check for recent verification codes in inbound SMS */
1341
+ checkForVerificationCode(agentId: string, minutesBack?: number): {
1342
+ code: string;
1343
+ from: string;
1344
+ body: string;
1345
+ receivedAt: string;
1346
+ } | null;
1347
+ }
1348
+ /**
1349
+ * SmsPoller — Polls for Google Voice SMS forwarded emails.
1350
+ *
1351
+ * Two modes:
1352
+ * 1. **Same email** (sameAsRelay=true): Hooks into the relay's onInboundMail callback.
1353
+ * The relay poll already fetches emails; SmsPoller filters for GV forwarded SMS.
1354
+ * 2. **Separate email** (sameAsRelay=false): Runs its own IMAP poll against the GV Gmail
1355
+ * using the separate credentials (forwardingEmail + forwardingPassword).
1356
+ *
1357
+ * Parsed SMS messages are stored in the sms_messages table.
1358
+ */
1359
+ declare class SmsPoller {
1360
+ private smsManager;
1361
+ private agentId;
1362
+ private config;
1363
+ private pollTimer;
1364
+ private polling;
1365
+ private lastSeenUid;
1366
+ private firstPollDone;
1367
+ private consecutiveFailures;
1368
+ private readonly POLL_INTERVAL_MS;
1369
+ private readonly MAX_BACKOFF_MS;
1370
+ private readonly CONNECT_TIMEOUT_MS;
1371
+ /** Callback for new inbound SMS */
1372
+ onSmsReceived: ((agentId: string, sms: ParsedSms) => void | Promise<void>) | null;
1373
+ constructor(smsManager: SmsManager, agentId: string, config: SmsConfig);
1374
+ /** Whether this poller needs its own IMAP connection (separate Gmail) */
1375
+ get needsSeparatePoll(): boolean;
1376
+ /**
1377
+ * Process an email from the relay poll (same-email mode).
1378
+ * Called by the relay gateway's onInboundMail when it detects a GV email.
1379
+ * Returns true if the email was an SMS and was processed.
1380
+ */
1381
+ processRelayEmail(from: string, subject: string, body: string): boolean;
1382
+ /**
1383
+ * Start polling the separate GV Gmail for SMS (separate-email mode).
1384
+ * Only call this if needsSeparatePoll is true.
1385
+ */
1386
+ startPolling(): Promise<void>;
1387
+ stopPolling(): void;
1388
+ private scheduleNext;
1389
+ private pollOnce;
1390
+ }
1391
+
1239
1392
  declare function debug(tag: string, message: string): void;
1240
1393
  declare function debugWarn(tag: string, message: string): void;
1241
1394
 
@@ -1370,4 +1523,4 @@ declare class SetupManager {
1370
1523
  isInitialized(): boolean;
1371
1524
  }
1372
1525
 
1373
- export { AGENT_ROLES, AccountManager, type AddressInfo, type Agent, AgentDeletionService, type AgentRole, AgenticMailClient, type AgenticMailClientOptions, type AgenticMailConfig, type ArchiveAndDeleteOptions, type ArchivedEmail, type Attachment, type AttachmentAdvisory, CloudflareClient, type CreateAgentOptions, DEFAULT_AGENT_NAME, DEFAULT_AGENT_ROLE, DNSConfigurator, type DeletionReport, type DeletionSummary, DependencyChecker, DependencyInstaller, type DependencyStatus, type DnsRecord, type DnsSetupResult, type DomainInfo, DomainManager, type DomainModeConfig, type DomainPurchaseResult, DomainPurchaser, type DomainSearchResult, type DomainSetupResult, type EmailEnvelope, EmailSearchIndex, type FolderInfo, type GatewayConfig, GatewayManager, type GatewayManagerOptions, type GatewayMode, type GatewayStatus, type InboundEmail, type InboxEvent, type InboxExpungeEvent, type InboxFlagsEvent, type InboxNewEvent, InboxWatcher, type InboxWatcherOptions, type InstallProgress, type LinkAdvisory, type LocalSmtpConfig, MailReceiver, type MailReceiverOptions, MailSender, type MailSenderOptions, type MailboxInfo, type OutboundCategory, type OutboundScanInput, type OutboundScanResult, type OutboundWarning, type ParsedAttachment, type ParsedEmail, type PurchasedDomain, RELAY_PRESETS, RelayBridge, type RelayBridgeOptions, type RelayConfig, RelayGateway, type RelayProvider, type RelaySearchResult, SPAM_THRESHOLD, type SanitizeDetection, type SanitizeResult, type SearchCriteria, type SearchableEmail, type SecurityAdvisory, type SendMailOptions, type SendResult, type SendResultWithRaw, type SetupConfig, SetupManager, type SetupResult, type Severity, type SpamCategory, type SpamResult, type SpamRuleMatch, StalwartAdmin, type StalwartAdminOptions, type StalwartPrincipal, type TunnelConfig, TunnelManager, WARNING_THRESHOLD, type WatcherOptions, buildInboundSecurityAdvisory, closeDatabase, createTestDatabase, debug, debugWarn, ensureDataDir, getDatabase, isInternalEmail, parseEmail, resolveConfig, sanitizeEmail, saveConfig, scanOutboundEmail, scoreEmail, startRelayBridge };
1526
+ export { AGENT_ROLES, AccountManager, type AddressInfo, type Agent, AgentDeletionService, type AgentRole, AgenticMailClient, type AgenticMailClientOptions, type AgenticMailConfig, type ArchiveAndDeleteOptions, type ArchivedEmail, type Attachment, type AttachmentAdvisory, CloudflareClient, type CreateAgentOptions, DEFAULT_AGENT_NAME, DEFAULT_AGENT_ROLE, DNSConfigurator, type DeletionReport, type DeletionSummary, DependencyChecker, DependencyInstaller, type DependencyStatus, type DnsRecord, type DnsSetupResult, type DomainInfo, DomainManager, type DomainModeConfig, type DomainPurchaseResult, DomainPurchaser, type DomainSearchResult, type DomainSetupResult, type EmailEnvelope, EmailSearchIndex, type FolderInfo, type GatewayConfig, GatewayManager, type GatewayManagerOptions, type GatewayMode, type GatewayStatus, type InboundEmail, type InboxEvent, type InboxExpungeEvent, type InboxFlagsEvent, type InboxNewEvent, InboxWatcher, type InboxWatcherOptions, type InstallProgress, type LinkAdvisory, type LocalSmtpConfig, MailReceiver, type MailReceiverOptions, MailSender, type MailSenderOptions, type MailboxInfo, type OutboundCategory, type OutboundScanInput, type OutboundScanResult, type OutboundWarning, type ParsedAttachment, type ParsedEmail, type ParsedSms, type PurchasedDomain, RELAY_PRESETS, RelayBridge, type RelayBridgeOptions, type RelayConfig, RelayGateway, type RelayProvider, type RelaySearchResult, SPAM_THRESHOLD, type SanitizeDetection, type SanitizeResult, type SearchCriteria, type SearchableEmail, type SecurityAdvisory, type SendMailOptions, type SendResult, type SendResultWithRaw, type SetupConfig, SetupManager, type SetupResult, type Severity, type SmsConfig, SmsManager, type SmsMessage, SmsPoller, type SpamCategory, type SpamResult, type SpamRuleMatch, StalwartAdmin, type StalwartAdminOptions, type StalwartPrincipal, type TunnelConfig, TunnelManager, WARNING_THRESHOLD, type WatcherOptions, buildInboundSecurityAdvisory, closeDatabase, createTestDatabase, debug, debugWarn, ensureDataDir, extractVerificationCode, getDatabase, isInternalEmail, isValidPhoneNumber, normalizePhoneNumber, parseEmail, parseGoogleVoiceSms, resolveConfig, sanitizeEmail, saveConfig, scanOutboundEmail, scoreEmail, startRelayBridge };
package/dist/index.js CHANGED
@@ -813,24 +813,31 @@ var StalwartAdmin = class {
813
813
  * Note: stalwart-cli may return a 500 error even when the operation succeeds.
814
814
  * We verify by listing the config afterwards.
815
815
  */
816
- async updateSetting(key, value) {
817
- const { execSync: execSync3 } = await import("child_process");
816
+ cliArgs() {
818
817
  const creds = `${this.options.adminUser}:${this.options.adminPassword}`;
818
+ return ["exec", "agenticmail-stalwart", "stalwart-cli", "-u", "http://localhost:8080", "-c", creds];
819
+ }
820
+ async updateSetting(key, value) {
821
+ const { execFileSync: execFileSync3 } = await import("child_process");
822
+ const cli = this.cliArgs();
819
823
  try {
820
- execSync3(
821
- `docker exec agenticmail-stalwart stalwart-cli -u http://localhost:8080 -c ${creds} server delete-config ${key}`,
824
+ execFileSync3(
825
+ "docker",
826
+ [...cli, "server", "delete-config", key],
822
827
  { timeout: 15e3, stdio: ["ignore", "pipe", "pipe"] }
823
828
  );
824
829
  } catch {
825
830
  }
826
831
  try {
827
- execSync3(
828
- `docker exec agenticmail-stalwart stalwart-cli -u http://localhost:8080 -c ${creds} server add-config ${key} ${value}`,
832
+ execFileSync3(
833
+ "docker",
834
+ [...cli, "server", "add-config", key, value],
829
835
  { timeout: 15e3, stdio: ["ignore", "pipe", "pipe"] }
830
836
  );
831
837
  } catch {
832
- const output = execSync3(
833
- `docker exec agenticmail-stalwart stalwart-cli -u http://localhost:8080 -c ${creds} server list-config ${key}`,
838
+ const output = execFileSync3(
839
+ "docker",
840
+ [...cli, "server", "list-config", key],
834
841
  { timeout: 15e3, stdio: ["ignore", "pipe", "pipe"] }
835
842
  ).toString();
836
843
  if (!output.includes(value)) {
@@ -875,16 +882,15 @@ var StalwartAdmin = class {
875
882
  * Returns the public key (base64, no headers) for DNS TXT record.
876
883
  */
877
884
  async createDkimSignature(domain, selector = "agenticmail") {
878
- const { execSync: execSync3 } = await import("child_process");
885
+ const { execFileSync: execFileSync3 } = await import("child_process");
879
886
  const signatureId = `agenticmail-${domain.replace(/\./g, "-")}`;
880
- const creds = `${this.options.adminUser}:${this.options.adminPassword}`;
881
- const cli = `docker exec agenticmail-stalwart stalwart-cli -u http://localhost:8080 -c ${creds}`;
887
+ const cli = this.cliArgs();
882
888
  const existing = await this.getSettings(`signature.${signatureId}`);
883
889
  if (existing["private-key"] && existing["domain"]) {
884
890
  console.log(`[DKIM] Reusing existing signature "${signatureId}" from Stalwart DB`);
885
891
  } else {
886
892
  try {
887
- execSync3(`${cli} server delete-config "signature.${signatureId}"`, {
893
+ execFileSync3("docker", [...cli, "server", "delete-config", `signature.${signatureId}`], {
888
894
  timeout: 1e4,
889
895
  stdio: ["ignore", "pipe", "pipe"]
890
896
  });
@@ -892,7 +898,7 @@ var StalwartAdmin = class {
892
898
  }
893
899
  console.log(`[DKIM] Creating RSA signature for ${domain} via stalwart-cli`);
894
900
  try {
895
- execSync3(`${cli} dkim create rsa ${domain} ${signatureId} ${selector}`, {
901
+ execFileSync3("docker", [...cli, "dkim", "create", "rsa", domain, signatureId, selector], {
896
902
  timeout: 15e3,
897
903
  stdio: ["ignore", "pipe", "pipe"]
898
904
  });
@@ -904,12 +910,12 @@ var StalwartAdmin = class {
904
910
  if (!Object.keys(signRule).length) {
905
911
  console.log(`[DKIM] Configuring DKIM signing rule`);
906
912
  const rules = [
907
- [`auth.dkim.sign.0000.if`, `listener != 'smtp'`],
908
- [`auth.dkim.sign.0000.then`, `['${signatureId}']`],
909
- [`auth.dkim.sign.0001.else`, `false`]
913
+ ["auth.dkim.sign.0000.if", `listener != 'smtp'`],
914
+ ["auth.dkim.sign.0000.then", `['${signatureId}']`],
915
+ ["auth.dkim.sign.0001.else", "false"]
910
916
  ];
911
917
  for (const [key, value] of rules) {
912
- execSync3(`${cli} server add-config "${key}" "${value}"`, {
918
+ execFileSync3("docker", [...cli, "server", "add-config", key, value], {
913
919
  timeout: 1e4,
914
920
  stdio: ["ignore", "pipe", "pipe"]
915
921
  });
@@ -917,7 +923,7 @@ var StalwartAdmin = class {
917
923
  }
918
924
  let publicKey;
919
925
  try {
920
- const output = execSync3(`${cli} dkim get-public-key ${signatureId}`, {
926
+ const output = execFileSync3("docker", [...cli, "dkim", "get-public-key", signatureId], {
921
927
  timeout: 1e4,
922
928
  stdio: ["ignore", "pipe", "pipe"]
923
929
  }).toString();
@@ -928,7 +934,7 @@ var StalwartAdmin = class {
928
934
  throw new Error(`Failed to get DKIM public key: ${err.message}`);
929
935
  }
930
936
  try {
931
- execSync3(`${cli} server reload-config`, {
937
+ execFileSync3("docker", [...cli, "server", "reload-config"], {
932
938
  timeout: 1e4,
933
939
  stdio: ["ignore", "pipe", "pipe"]
934
940
  });
@@ -941,9 +947,9 @@ var StalwartAdmin = class {
941
947
  * Restart the Stalwart Docker container and wait for it to be ready.
942
948
  */
943
949
  async restartContainer() {
944
- const { execSync: execSync3 } = await import("child_process");
950
+ const { execFileSync: execFileSync3 } = await import("child_process");
945
951
  try {
946
- execSync3("docker restart agenticmail-stalwart", { timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
952
+ execFileSync3("docker", ["restart", "agenticmail-stalwart"], { timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
947
953
  for (let i = 0; i < 15; i++) {
948
954
  try {
949
955
  const res = await fetch(`${this.baseUrl}/health`, { signal: AbortSignal.timeout(2e3) });
@@ -3813,8 +3819,8 @@ var CloudflareClient = class {
3813
3819
  let available = false;
3814
3820
  if (result.supported_tld && !hasRegistration) {
3815
3821
  try {
3816
- const { execSync: execSync3 } = await import("child_process");
3817
- const whoisOutput = execSync3(`whois ${domain}`, { timeout: 1e4, stdio: ["ignore", "pipe", "pipe"] }).toString().toLowerCase();
3822
+ const { execFileSync: execFileSync3 } = await import("child_process");
3823
+ const whoisOutput = execFileSync3("whois", [domain], { timeout: 1e4, stdio: ["ignore", "pipe", "pipe"] }).toString().toLowerCase();
3818
3824
  available = whoisOutput.includes("domain not found") || whoisOutput.includes("no match") || whoisOutput.includes("not found") || whoisOutput.includes("no data found") || whoisOutput.includes("status: free") || whoisOutput.includes("no entries found");
3819
3825
  } catch {
3820
3826
  available = false;
@@ -4278,8 +4284,8 @@ var TunnelManager = class {
4278
4284
  return this.binPath;
4279
4285
  }
4280
4286
  try {
4281
- const { execSync: execSync3 } = await import("child_process");
4282
- const sysPath = execSync3("which cloudflared", { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
4287
+ const { execFileSync: execFileSync3 } = await import("child_process");
4288
+ const sysPath = execFileSync3("which", ["cloudflared"], { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
4283
4289
  if (sysPath && existsSync2(sysPath)) {
4284
4290
  this.binPath = sysPath;
4285
4291
  return sysPath;
@@ -4427,6 +4433,406 @@ var TunnelManager = class {
4427
4433
 
4428
4434
  // src/gateway/manager.ts
4429
4435
  import MailComposer3 from "nodemailer/lib/mail-composer/index.js";
4436
+
4437
+ // src/sms/manager.ts
4438
+ function normalizePhoneNumber(raw) {
4439
+ const cleaned = raw.replace(/[^+\d]/g, "");
4440
+ if (!cleaned) return null;
4441
+ const digits = cleaned.replace(/\D/g, "");
4442
+ if (digits.length === 10) return `+1${digits}`;
4443
+ if (digits.length === 11 && digits.startsWith("1")) return `+${digits}`;
4444
+ if (cleaned.startsWith("+") && digits.length >= 10 && digits.length <= 15) return `+${digits}`;
4445
+ if (digits.length < 10) return null;
4446
+ if (digits.length <= 11) return `+1${digits.slice(-10)}`;
4447
+ return null;
4448
+ }
4449
+ function isValidPhoneNumber(phone) {
4450
+ const normalized = normalizePhoneNumber(phone);
4451
+ if (!normalized) return false;
4452
+ const digits = normalized.replace(/\D/g, "");
4453
+ return digits.length >= 10 && digits.length <= 15;
4454
+ }
4455
+ function parseGoogleVoiceSms(emailBody, emailFrom) {
4456
+ if (!emailBody || typeof emailBody !== "string") return null;
4457
+ if (!emailFrom || typeof emailFrom !== "string") return null;
4458
+ const fromLower = emailFrom.toLowerCase();
4459
+ const isGoogleVoice = fromLower.includes("voice-noreply@google.com") || fromLower.includes("@txt.voice.google.com") || fromLower.includes("voice.google.com") || fromLower.includes("google.com/voice") || fromLower.includes("google") && fromLower.includes("voice");
4460
+ if (!isGoogleVoice) return null;
4461
+ let text = emailBody.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<\/div>/gi, "\n").replace(/<[^>]+>/g, "").replace(/&nbsp;/gi, " ").replace(/&amp;/gi, "&").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/&quot;/gi, '"').replace(/&#39;/gi, "'").trim();
4462
+ let from = "";
4463
+ const newMsgMatch = text.match(/new\s+(?:text\s+)?message\s+from\s+(\+?[\d\s().-]+)/i);
4464
+ if (newMsgMatch) {
4465
+ from = newMsgMatch[1];
4466
+ }
4467
+ if (!from) {
4468
+ const colonMatch = text.match(/^(\+?[\d\s().-]{10,})\s*:\s*/m);
4469
+ if (colonMatch) {
4470
+ from = colonMatch[1];
4471
+ }
4472
+ }
4473
+ if (!from) {
4474
+ const firstLines = text.split("\n").slice(0, 5).join(" ");
4475
+ const phoneMatch = firstLines.match(/(\+?1?[-.\s]?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4})/);
4476
+ if (phoneMatch) from = phoneMatch[1];
4477
+ }
4478
+ if (from) {
4479
+ const normalized = normalizePhoneNumber(from);
4480
+ from = normalized || from.replace(/[^+\d]/g, "");
4481
+ }
4482
+ const lines = text.split("\n").map((l) => l.trim()).filter((l) => l);
4483
+ const boilerplatePatterns = [
4484
+ /^new\s+(text\s+)?message\s+from/i,
4485
+ /^\+?1?[-.\s]?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\s*$/,
4486
+ /^to\s+respond\s+to\s+this/i,
4487
+ /^your\s+account$/i,
4488
+ /^google\s+voice$/i,
4489
+ /^sent\s+via\s+google\s+voice/i,
4490
+ /^you\s+received\s+this/i,
4491
+ /^to\s+stop\s+receiving/i,
4492
+ /^reply\s+to\s+this\s+email/i,
4493
+ /^https?:\/\/voice\.google\.com/i,
4494
+ /^manage\s+your\s+settings/i,
4495
+ /^google\s+llc/i,
4496
+ /^1600\s+amphitheatre/i
4497
+ ];
4498
+ const messageLines = lines.filter((l) => {
4499
+ for (const p of boilerplatePatterns) {
4500
+ if (p.test(l)) return false;
4501
+ }
4502
+ return true;
4503
+ });
4504
+ let body = messageLines.join("\n").trim();
4505
+ const prefixMatch = body.match(/^(\+?[\d\s().-]{10,})\s*:\s*([\s\S]+)/);
4506
+ if (prefixMatch) {
4507
+ body = prefixMatch[2].trim();
4508
+ }
4509
+ if (!body) return null;
4510
+ return {
4511
+ from: from || "unknown",
4512
+ body,
4513
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4514
+ raw: emailBody
4515
+ };
4516
+ }
4517
+ function extractVerificationCode(smsBody) {
4518
+ if (!smsBody || typeof smsBody !== "string") return null;
4519
+ const patterns = [
4520
+ // "Your code is 123456" / "verification code: 123456" / "code is: 123456"
4521
+ /(?:code|pin|otp|token|password)\s*(?:is|:)\s*(\d{4,8})/i,
4522
+ // "123456 is your code"
4523
+ /(\d{4,8})\s+is\s+your\s+(?:code|pin|otp|verification)/i,
4524
+ // G-123456 (Google style)
4525
+ /[Gg]-(\d{4,8})/,
4526
+ // "Enter 123456 to verify" / "Use 123456 to"
4527
+ /(?:enter|use)\s+(\d{4,8})\s+(?:to|for|as)/i,
4528
+ // Standalone 6-digit on its own line (common pattern)
4529
+ /^\s*(\d{6})\s*$/m,
4530
+ // "Code: ABC-123" style alphanumeric
4531
+ /(?:code|pin)\s*(?:is|:)\s*([A-Z0-9]{3,6}[-][A-Z0-9]{3,6})/i,
4532
+ // Last resort: any 6-digit sequence not part of a longer number
4533
+ /(?<!\d)(\d{6})(?!\d)/
4534
+ ];
4535
+ for (const pattern of patterns) {
4536
+ const match = smsBody.match(pattern);
4537
+ if (match?.[1]) return match[1];
4538
+ }
4539
+ return null;
4540
+ }
4541
+ var SmsManager = class {
4542
+ constructor(db2) {
4543
+ this.db = db2;
4544
+ this.ensureTable();
4545
+ }
4546
+ initialized = false;
4547
+ ensureTable() {
4548
+ if (this.initialized) return;
4549
+ try {
4550
+ this.db.exec(`
4551
+ CREATE TABLE IF NOT EXISTS sms_messages (
4552
+ id TEXT PRIMARY KEY,
4553
+ agent_id TEXT NOT NULL,
4554
+ direction TEXT NOT NULL CHECK(direction IN ('inbound', 'outbound')),
4555
+ phone_number TEXT NOT NULL,
4556
+ body TEXT NOT NULL,
4557
+ status TEXT NOT NULL DEFAULT 'pending',
4558
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
4559
+ metadata TEXT DEFAULT '{}'
4560
+ )
4561
+ `);
4562
+ try {
4563
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_sms_agent ON sms_messages(agent_id)");
4564
+ } catch {
4565
+ }
4566
+ try {
4567
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_sms_direction ON sms_messages(direction)");
4568
+ } catch {
4569
+ }
4570
+ try {
4571
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_sms_created ON sms_messages(created_at)");
4572
+ } catch {
4573
+ }
4574
+ this.initialized = true;
4575
+ } catch (err) {
4576
+ this.initialized = true;
4577
+ }
4578
+ }
4579
+ /** Get SMS config from agent metadata */
4580
+ getSmsConfig(agentId) {
4581
+ const row = this.db.prepare("SELECT metadata FROM agents WHERE id = ?").get(agentId);
4582
+ if (!row) return null;
4583
+ try {
4584
+ const meta = JSON.parse(row.metadata || "{}");
4585
+ return meta.sms && meta.sms.enabled !== void 0 ? meta.sms : null;
4586
+ } catch {
4587
+ return null;
4588
+ }
4589
+ }
4590
+ /** Save SMS config to agent metadata */
4591
+ saveSmsConfig(agentId, config) {
4592
+ const row = this.db.prepare("SELECT metadata FROM agents WHERE id = ?").get(agentId);
4593
+ if (!row) throw new Error(`Agent ${agentId} not found`);
4594
+ let meta;
4595
+ try {
4596
+ meta = JSON.parse(row.metadata || "{}");
4597
+ } catch {
4598
+ meta = {};
4599
+ }
4600
+ meta.sms = config;
4601
+ this.db.prepare("UPDATE agents SET metadata = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(meta), agentId);
4602
+ }
4603
+ /** Remove SMS config from agent metadata */
4604
+ removeSmsConfig(agentId) {
4605
+ const row = this.db.prepare("SELECT metadata FROM agents WHERE id = ?").get(agentId);
4606
+ if (!row) return;
4607
+ let meta;
4608
+ try {
4609
+ meta = JSON.parse(row.metadata || "{}");
4610
+ } catch {
4611
+ meta = {};
4612
+ }
4613
+ delete meta.sms;
4614
+ this.db.prepare("UPDATE agents SET metadata = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(meta), agentId);
4615
+ }
4616
+ /** Record an inbound SMS (parsed from email) */
4617
+ recordInbound(agentId, parsed) {
4618
+ const id = `sms_in_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
4619
+ const createdAt = parsed.timestamp || (/* @__PURE__ */ new Date()).toISOString();
4620
+ this.db.prepare(
4621
+ "INSERT INTO sms_messages (id, agent_id, direction, phone_number, body, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
4622
+ ).run(id, agentId, "inbound", parsed.from, parsed.body, "received", createdAt);
4623
+ return { id, agentId, direction: "inbound", phoneNumber: parsed.from, body: parsed.body, status: "received", createdAt };
4624
+ }
4625
+ /** Record an outbound SMS attempt */
4626
+ recordOutbound(agentId, phoneNumber, body, status = "pending") {
4627
+ const id = `sms_out_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
4628
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4629
+ const normalized = normalizePhoneNumber(phoneNumber) || phoneNumber;
4630
+ this.db.prepare(
4631
+ "INSERT INTO sms_messages (id, agent_id, direction, phone_number, body, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
4632
+ ).run(id, agentId, "outbound", normalized, body, status, now);
4633
+ return { id, agentId, direction: "outbound", phoneNumber: normalized, body, status, createdAt: now };
4634
+ }
4635
+ /** Update SMS status */
4636
+ updateStatus(id, status) {
4637
+ this.db.prepare("UPDATE sms_messages SET status = ? WHERE id = ?").run(status, id);
4638
+ }
4639
+ /** List SMS messages for an agent */
4640
+ listMessages(agentId, opts) {
4641
+ const limit = Math.min(Math.max(opts?.limit ?? 20, 1), 100);
4642
+ const offset = Math.max(opts?.offset ?? 0, 0);
4643
+ let query = "SELECT * FROM sms_messages WHERE agent_id = ?";
4644
+ const params = [agentId];
4645
+ if (opts?.direction && (opts.direction === "inbound" || opts.direction === "outbound")) {
4646
+ query += " AND direction = ?";
4647
+ params.push(opts.direction);
4648
+ }
4649
+ query += " ORDER BY created_at DESC LIMIT ? OFFSET ?";
4650
+ params.push(limit, offset);
4651
+ return this.db.prepare(query).all(...params).map((row) => ({
4652
+ id: row.id,
4653
+ agentId: row.agent_id,
4654
+ direction: row.direction,
4655
+ phoneNumber: row.phone_number,
4656
+ body: row.body,
4657
+ status: row.status,
4658
+ createdAt: row.created_at,
4659
+ metadata: row.metadata ? JSON.parse(row.metadata) : void 0
4660
+ }));
4661
+ }
4662
+ /** Check for recent verification codes in inbound SMS */
4663
+ checkForVerificationCode(agentId, minutesBack = 10) {
4664
+ const safeMins = Math.min(Math.max(minutesBack, 1), 1440);
4665
+ const cutoff = new Date(Date.now() - safeMins * 60 * 1e3).toISOString();
4666
+ const messages = this.db.prepare(
4667
+ "SELECT * FROM sms_messages WHERE agent_id = ? AND direction = ? AND created_at > ? ORDER BY created_at DESC LIMIT 50"
4668
+ ).all(agentId, "inbound", cutoff);
4669
+ for (const msg of messages) {
4670
+ const code = extractVerificationCode(msg.body);
4671
+ if (code) {
4672
+ return { code, from: msg.phone_number, body: msg.body, receivedAt: msg.created_at };
4673
+ }
4674
+ }
4675
+ return null;
4676
+ }
4677
+ };
4678
+ var SmsPoller = class {
4679
+ constructor(smsManager, agentId, config) {
4680
+ this.smsManager = smsManager;
4681
+ this.agentId = agentId;
4682
+ this.config = config;
4683
+ }
4684
+ pollTimer = null;
4685
+ polling = false;
4686
+ lastSeenUid = 0;
4687
+ firstPollDone = false;
4688
+ consecutiveFailures = 0;
4689
+ POLL_INTERVAL_MS = 3e4;
4690
+ MAX_BACKOFF_MS = 5 * 6e4;
4691
+ CONNECT_TIMEOUT_MS = 3e4;
4692
+ /** Callback for new inbound SMS */
4693
+ onSmsReceived = null;
4694
+ /** Whether this poller needs its own IMAP connection (separate Gmail) */
4695
+ get needsSeparatePoll() {
4696
+ return !this.config.sameAsRelay && !!this.config.forwardingPassword;
4697
+ }
4698
+ /**
4699
+ * Process an email from the relay poll (same-email mode).
4700
+ * Called by the relay gateway's onInboundMail when it detects a GV email.
4701
+ * Returns true if the email was an SMS and was processed.
4702
+ */
4703
+ processRelayEmail(from, subject, body) {
4704
+ const parsed = parseGoogleVoiceSms(body, from);
4705
+ if (!parsed) return false;
4706
+ this.smsManager.recordInbound(this.agentId, parsed);
4707
+ this.onSmsReceived?.(this.agentId, parsed);
4708
+ return true;
4709
+ }
4710
+ /**
4711
+ * Start polling the separate GV Gmail for SMS (separate-email mode).
4712
+ * Only call this if needsSeparatePoll is true.
4713
+ */
4714
+ async startPolling() {
4715
+ if (!this.needsSeparatePoll) return;
4716
+ if (this.polling) return;
4717
+ this.polling = true;
4718
+ console.log(`[SmsPoller] Starting SMS poll for ${this.config.phoneNumber} via ${this.config.forwardingEmail}`);
4719
+ await this.pollOnce();
4720
+ this.scheduleNext();
4721
+ }
4722
+ stopPolling() {
4723
+ this.polling = false;
4724
+ if (this.pollTimer) {
4725
+ clearTimeout(this.pollTimer);
4726
+ this.pollTimer = null;
4727
+ }
4728
+ }
4729
+ scheduleNext() {
4730
+ if (!this.polling) return;
4731
+ const backoff = this.consecutiveFailures > 0 ? Math.min(this.POLL_INTERVAL_MS * Math.pow(2, this.consecutiveFailures - 1), this.MAX_BACKOFF_MS) : this.POLL_INTERVAL_MS;
4732
+ this.pollTimer = setTimeout(async () => {
4733
+ await this.pollOnce();
4734
+ this.scheduleNext();
4735
+ }, backoff);
4736
+ }
4737
+ async pollOnce() {
4738
+ if (!this.config.forwardingEmail || !this.config.forwardingPassword) return;
4739
+ let ImapFlow4;
4740
+ try {
4741
+ ImapFlow4 = (await import("imapflow")).ImapFlow;
4742
+ } catch {
4743
+ console.error("[SmsPoller] imapflow not available");
4744
+ return;
4745
+ }
4746
+ let simpleParser3;
4747
+ try {
4748
+ simpleParser3 = (await import("mailparser")).simpleParser;
4749
+ } catch {
4750
+ console.error("[SmsPoller] mailparser not available");
4751
+ return;
4752
+ }
4753
+ const imap = new ImapFlow4({
4754
+ host: "imap.gmail.com",
4755
+ port: 993,
4756
+ secure: true,
4757
+ auth: {
4758
+ user: this.config.forwardingEmail,
4759
+ pass: this.config.forwardingPassword
4760
+ },
4761
+ logger: false,
4762
+ tls: { rejectUnauthorized: true }
4763
+ });
4764
+ const timeout = setTimeout(() => {
4765
+ try {
4766
+ imap.close();
4767
+ } catch {
4768
+ }
4769
+ }, this.CONNECT_TIMEOUT_MS);
4770
+ try {
4771
+ await imap.connect();
4772
+ clearTimeout(timeout);
4773
+ const lock = await imap.getMailboxLock("INBOX");
4774
+ try {
4775
+ if (!this.firstPollDone) {
4776
+ const status = imap.mailbox;
4777
+ const uidNext = status && typeof status === "object" && "uidNext" in status ? status.uidNext : 1;
4778
+ this.lastSeenUid = Math.max(0, uidNext - 21);
4779
+ this.firstPollDone = true;
4780
+ }
4781
+ let searchResult;
4782
+ try {
4783
+ searchResult = await imap.search(
4784
+ { from: "voice-noreply@google.com", uid: `${this.lastSeenUid + 1}:*` },
4785
+ { uid: true }
4786
+ );
4787
+ } catch {
4788
+ try {
4789
+ searchResult = await imap.search({ all: true }, { uid: true });
4790
+ searchResult = searchResult.filter((uid) => uid > this.lastSeenUid);
4791
+ } catch {
4792
+ return;
4793
+ }
4794
+ }
4795
+ if (!searchResult?.length) return;
4796
+ const uids = searchResult.filter((uid) => uid > this.lastSeenUid);
4797
+ for (const uid of uids) {
4798
+ if (uid > this.lastSeenUid) this.lastSeenUid = uid;
4799
+ try {
4800
+ const msg = await imap.fetchOne(String(uid), { source: true }, { uid: true });
4801
+ const source = msg?.source;
4802
+ if (!source) continue;
4803
+ const parsed = await simpleParser3(source);
4804
+ const fromAddr = parsed.from?.value?.[0]?.address ?? "";
4805
+ const body = parsed.text || parsed.html || "";
4806
+ const sms = parseGoogleVoiceSms(body, fromAddr);
4807
+ if (sms) {
4808
+ this.smsManager.recordInbound(this.agentId, sms);
4809
+ this.onSmsReceived?.(this.agentId, sms);
4810
+ }
4811
+ } catch {
4812
+ }
4813
+ }
4814
+ } finally {
4815
+ lock.release();
4816
+ }
4817
+ if (this.consecutiveFailures > 0) {
4818
+ console.log(`[SmsPoller] Recovered after ${this.consecutiveFailures} failures`);
4819
+ }
4820
+ this.consecutiveFailures = 0;
4821
+ await imap.logout();
4822
+ } catch (err) {
4823
+ clearTimeout(timeout);
4824
+ this.consecutiveFailures++;
4825
+ const msg = err instanceof Error ? err.message : String(err);
4826
+ console.error(`[SmsPoller] Poll failed (${this.consecutiveFailures}): ${msg}`);
4827
+ try {
4828
+ await imap.logout();
4829
+ } catch {
4830
+ }
4831
+ }
4832
+ }
4833
+ };
4834
+
4835
+ // src/gateway/manager.ts
4430
4836
  var GatewayManager = class {
4431
4837
  constructor(options) {
4432
4838
  this.options = options;
@@ -4443,6 +4849,10 @@ var GatewayManager = class {
4443
4849
  } catch {
4444
4850
  this.config = { mode: "none" };
4445
4851
  }
4852
+ try {
4853
+ this.smsManager = new SmsManager(options.db);
4854
+ } catch {
4855
+ }
4446
4856
  }
4447
4857
  db;
4448
4858
  stalwart;
@@ -4453,6 +4863,8 @@ var GatewayManager = class {
4453
4863
  tunnel = null;
4454
4864
  dnsConfigurator = null;
4455
4865
  domainPurchaser = null;
4866
+ smsManager = null;
4867
+ smsPollers = /* @__PURE__ */ new Map();
4456
4868
  /**
4457
4869
  * Check if a message has already been delivered to an agent (deduplication).
4458
4870
  */
@@ -4481,6 +4893,26 @@ var GatewayManager = class {
4481
4893
  return;
4482
4894
  }
4483
4895
  if (mail.messageId && this.isAlreadyDelivered(mail.messageId, agentName)) return;
4896
+ if (this.smsManager) {
4897
+ try {
4898
+ const smsBody = mail.text || mail.html || "";
4899
+ const parsedSms = parseGoogleVoiceSms(smsBody, mail.from);
4900
+ if (parsedSms) {
4901
+ const agent2 = this.accountManager ? await this.accountManager.getByName(agentName) : null;
4902
+ const agentId = agent2?.id;
4903
+ if (agentId) {
4904
+ const smsConfig = this.smsManager.getSmsConfig(agentId);
4905
+ if (smsConfig?.enabled && smsConfig.sameAsRelay) {
4906
+ this.smsManager.recordInbound(agentId, parsedSms);
4907
+ console.log(`[GatewayManager] SMS received from ${parsedSms.from}: "${parsedSms.body.slice(0, 50)}..." \u2192 agent ${agentName}`);
4908
+ if (mail.messageId) this.recordDelivery(mail.messageId, agentName);
4909
+ }
4910
+ }
4911
+ }
4912
+ } catch (err) {
4913
+ debug("GatewayManager", `SMS detection error: ${err.message}`);
4914
+ }
4915
+ }
4484
4916
  try {
4485
4917
  await this.tryProcessApprovalReply(mail);
4486
4918
  } catch (err) {
@@ -5133,10 +5565,38 @@ var GatewayManager = class {
5133
5565
  return { success: false, error: err instanceof Error ? err.message : String(err) };
5134
5566
  }
5135
5567
  }
5568
+ // --- SMS Polling ---
5569
+ /**
5570
+ * Start SMS pollers for all agents that have separate GV Gmail credentials.
5571
+ * Agents with sameAsRelay=true are handled in deliverInboundLocally.
5572
+ */
5573
+ async startSmsPollers() {
5574
+ if (!this.smsManager || !this.accountManager) return;
5575
+ const agents = this.db.prepare("SELECT id, name, metadata FROM agents").all();
5576
+ for (const agent of agents) {
5577
+ try {
5578
+ const meta = JSON.parse(agent.metadata || "{}");
5579
+ const smsConfig = meta.sms;
5580
+ if (!smsConfig?.enabled || !smsConfig.forwardingPassword || smsConfig.sameAsRelay) continue;
5581
+ const poller = new SmsPoller(this.smsManager, agent.id, smsConfig);
5582
+ poller.onSmsReceived = (agentId, sms) => {
5583
+ console.log(`[SmsPoller] SMS received for agent ${agent.name}: from ${sms.from}, body="${sms.body.slice(0, 50)}..."`);
5584
+ };
5585
+ this.smsPollers.set(agent.id, poller);
5586
+ await poller.startPolling();
5587
+ console.log(`[GatewayManager] SMS poller started for agent "${agent.name}" (${smsConfig.forwardingEmail})`);
5588
+ } catch {
5589
+ }
5590
+ }
5591
+ }
5136
5592
  // --- Lifecycle ---
5137
5593
  async shutdown() {
5138
5594
  await this.relay.shutdown();
5139
5595
  this.tunnel?.stop();
5596
+ for (const poller of this.smsPollers.values()) {
5597
+ poller.stopPolling();
5598
+ }
5599
+ this.smsPollers.clear();
5140
5600
  }
5141
5601
  /**
5142
5602
  * Resume gateway from saved config (e.g., after server restart).
@@ -5156,6 +5616,13 @@ var GatewayManager = class {
5156
5616
  console.error("[GatewayManager] Failed to resume relay:", err);
5157
5617
  }
5158
5618
  }
5619
+ if (this.smsManager && this.accountManager) {
5620
+ try {
5621
+ await this.startSmsPollers();
5622
+ } catch (err) {
5623
+ console.error("[GatewayManager] Failed to start SMS pollers:", err);
5624
+ }
5625
+ }
5159
5626
  if (this.config.mode === "domain" && this.config.domain) {
5160
5627
  try {
5161
5628
  this.cfClient = new CloudflareClient(
@@ -5351,7 +5818,7 @@ import { join as join7 } from "path";
5351
5818
  import { homedir as homedir6 } from "os";
5352
5819
 
5353
5820
  // src/setup/deps.ts
5354
- import { execSync } from "child_process";
5821
+ import { execFileSync } from "child_process";
5355
5822
  import { existsSync as existsSync3 } from "fs";
5356
5823
  import { join as join5 } from "path";
5357
5824
  import { homedir as homedir4 } from "os";
@@ -5365,7 +5832,7 @@ var DependencyChecker = class {
5365
5832
  }
5366
5833
  async checkDocker() {
5367
5834
  try {
5368
- const output = execSync("docker --version", { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
5835
+ const output = execFileSync("docker", ["--version"], { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
5369
5836
  const match = output.match(/Docker version ([\d.]+)/);
5370
5837
  return {
5371
5838
  name: "docker",
@@ -5383,8 +5850,9 @@ var DependencyChecker = class {
5383
5850
  }
5384
5851
  async checkStalwart() {
5385
5852
  try {
5386
- const output = execSync(
5387
- 'docker ps --filter name=agenticmail-stalwart --format "{{.Status}}"',
5853
+ const output = execFileSync(
5854
+ "docker",
5855
+ ["ps", "--filter", "name=agenticmail-stalwart", "--format", "{{.Status}}"],
5388
5856
  { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }
5389
5857
  ).toString().trim();
5390
5858
  const running = output.length > 0 && output.toLowerCase().includes("up");
@@ -5407,7 +5875,7 @@ var DependencyChecker = class {
5407
5875
  if (existsSync3(binPath)) {
5408
5876
  let version;
5409
5877
  try {
5410
- const output = execSync(`"${binPath}" --version`, { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
5878
+ const output = execFileSync(binPath, ["--version"], { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
5411
5879
  const match = output.match(/cloudflared version ([\d.]+)/);
5412
5880
  version = match?.[1];
5413
5881
  } catch {
@@ -5415,7 +5883,7 @@ var DependencyChecker = class {
5415
5883
  return { name: "cloudflared", installed: true, version, description: "Cloudflare Tunnel for custom domain email" };
5416
5884
  }
5417
5885
  try {
5418
- const output = execSync("cloudflared --version", { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
5886
+ const output = execFileSync("cloudflared", ["--version"], { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
5419
5887
  const match = output.match(/cloudflared version ([\d.]+)/);
5420
5888
  return { name: "cloudflared", installed: true, version: match?.[1], description: "Cloudflare Tunnel for custom domain email" };
5421
5889
  } catch {
@@ -5425,9 +5893,9 @@ var DependencyChecker = class {
5425
5893
  };
5426
5894
 
5427
5895
  // src/setup/installer.ts
5428
- import { execSync as execSync2 } from "child_process";
5896
+ import { execFileSync as execFileSync2, execSync } from "child_process";
5429
5897
  import { existsSync as existsSync4 } from "fs";
5430
- import { writeFile, rename, chmod as chmod2, mkdir as mkdir2 } from "fs/promises";
5898
+ import { writeFile, rename, chmod as chmod2, mkdir as mkdir2, unlink } from "fs/promises";
5431
5899
  import { join as join6 } from "path";
5432
5900
  import { homedir as homedir5, platform as platform2, arch as arch2 } from "os";
5433
5901
  var DependencyInstaller = class {
@@ -5444,13 +5912,13 @@ var DependencyInstaller = class {
5444
5912
  async installDocker() {
5445
5913
  let cliInstalled = false;
5446
5914
  try {
5447
- execSync2("docker --version", { timeout: 5e3, stdio: "ignore" });
5915
+ execFileSync2("docker", ["--version"], { timeout: 5e3, stdio: "ignore" });
5448
5916
  cliInstalled = true;
5449
5917
  } catch {
5450
5918
  }
5451
5919
  if (cliInstalled) {
5452
5920
  try {
5453
- execSync2("docker info", { timeout: 1e4, stdio: "ignore" });
5921
+ execFileSync2("docker", ["info"], { timeout: 1e4, stdio: "ignore" });
5454
5922
  return;
5455
5923
  } catch {
5456
5924
  this.onProgress("Docker found but not running \u2014 starting it now...");
@@ -5463,18 +5931,18 @@ var DependencyInstaller = class {
5463
5931
  if (os === "darwin") {
5464
5932
  this.onProgress("Installing Docker via Homebrew...");
5465
5933
  try {
5466
- execSync2("brew --version", { timeout: 5e3, stdio: "ignore" });
5934
+ execFileSync2("brew", ["--version"], { timeout: 5e3, stdio: "ignore" });
5467
5935
  } catch {
5468
5936
  throw new Error("Homebrew is required to install Docker on macOS. Install it from https://brew.sh then try again.");
5469
5937
  }
5470
- execSync2("brew install --cask docker", { timeout: 3e5, stdio: "inherit" });
5938
+ execFileSync2("brew", ["install", "--cask", "docker"], { timeout: 3e5, stdio: "inherit" });
5471
5939
  this.onProgress("Docker installed. Starting Docker Desktop...");
5472
5940
  this.startDockerDaemon();
5473
5941
  await this.waitForDocker();
5474
5942
  } else if (os === "linux") {
5475
5943
  this.onProgress("Installing Docker...");
5476
5944
  try {
5477
- execSync2("curl -fsSL https://get.docker.com | sh", { timeout: 3e5, stdio: "inherit" });
5945
+ execSync("curl -fsSL https://get.docker.com | sh", { timeout: 3e5, stdio: "inherit" });
5478
5946
  } catch {
5479
5947
  throw new Error("Failed to install Docker. Install it manually: https://docs.docker.com/get-docker/");
5480
5948
  }
@@ -5492,12 +5960,12 @@ var DependencyInstaller = class {
5492
5960
  const os = platform2();
5493
5961
  if (os === "darwin") {
5494
5962
  try {
5495
- execSync2("open -a Docker", { timeout: 1e4, stdio: "ignore" });
5963
+ execFileSync2("open", ["-a", "Docker"], { timeout: 1e4, stdio: "ignore" });
5496
5964
  } catch {
5497
5965
  }
5498
5966
  } else if (os === "linux") {
5499
5967
  try {
5500
- execSync2("sudo systemctl start docker", { timeout: 15e3, stdio: "ignore" });
5968
+ execFileSync2("sudo", ["systemctl", "start", "docker"], { timeout: 15e3, stdio: "ignore" });
5501
5969
  } catch {
5502
5970
  }
5503
5971
  }
@@ -5513,7 +5981,7 @@ var DependencyInstaller = class {
5513
5981
  let attempts = 0;
5514
5982
  while (Date.now() - start < maxWait) {
5515
5983
  try {
5516
- execSync2("docker info", { timeout: 5e3, stdio: "ignore" });
5984
+ execFileSync2("docker", ["info"], { timeout: 5e3, stdio: "ignore" });
5517
5985
  return;
5518
5986
  } catch {
5519
5987
  }
@@ -5536,14 +6004,14 @@ var DependencyInstaller = class {
5536
6004
  throw new Error(`docker-compose.yml not found at: ${composePath}`);
5537
6005
  }
5538
6006
  try {
5539
- execSync2("docker info", { timeout: 1e4, stdio: "ignore" });
6007
+ execFileSync2("docker", ["info"], { timeout: 1e4, stdio: "ignore" });
5540
6008
  } catch {
5541
6009
  this.onProgress("Starting Docker...");
5542
6010
  this.startDockerDaemon();
5543
6011
  await this.waitForDocker();
5544
6012
  }
5545
6013
  this.onProgress("Starting Stalwart mail server...");
5546
- execSync2(`docker compose -f "${composePath}" up -d`, {
6014
+ execFileSync2("docker", ["compose", "-f", composePath, "up", "-d"], {
5547
6015
  timeout: 12e4,
5548
6016
  stdio: ["ignore", "pipe", "pipe"]
5549
6017
  });
@@ -5551,8 +6019,9 @@ var DependencyInstaller = class {
5551
6019
  const start = Date.now();
5552
6020
  while (Date.now() - start < maxWait) {
5553
6021
  try {
5554
- const output = execSync2(
5555
- 'docker ps --filter name=agenticmail-stalwart --format "{{.Status}}"',
6022
+ const output = execFileSync2(
6023
+ "docker",
6024
+ ["ps", "--filter", "name=agenticmail-stalwart", "--format", "{{.Status}}"],
5556
6025
  { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }
5557
6026
  ).toString().trim();
5558
6027
  if (output.toLowerCase().includes("up")) {
@@ -5575,7 +6044,7 @@ var DependencyInstaller = class {
5575
6044
  return binPath;
5576
6045
  }
5577
6046
  try {
5578
- const sysPath = execSync2("which cloudflared", { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
6047
+ const sysPath = execFileSync2("which", ["cloudflared"], { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
5579
6048
  if (sysPath && existsSync4(sysPath)) return sysPath;
5580
6049
  } catch {
5581
6050
  }
@@ -5599,11 +6068,11 @@ var DependencyInstaller = class {
5599
6068
  const tgzPath = join6(binDir, "cloudflared.tgz");
5600
6069
  await writeFile(tgzPath, buffer);
5601
6070
  try {
5602
- execSync2(`tar -xzf "${tgzPath}" -C "${binDir}" cloudflared`, { timeout: 15e3, stdio: "ignore" });
6071
+ execFileSync2("tar", ["-xzf", tgzPath, "-C", binDir, "cloudflared"], { timeout: 15e3, stdio: "ignore" });
5603
6072
  await chmod2(binPath, 493);
5604
6073
  } finally {
5605
6074
  try {
5606
- execSync2(`rm -f "${tgzPath}"`, { stdio: "ignore" });
6075
+ await unlink(tgzPath);
5607
6076
  } catch {
5608
6077
  }
5609
6078
  }
@@ -5851,6 +6320,8 @@ export {
5851
6320
  RelayGateway,
5852
6321
  SPAM_THRESHOLD,
5853
6322
  SetupManager,
6323
+ SmsManager,
6324
+ SmsPoller,
5854
6325
  StalwartAdmin,
5855
6326
  TunnelManager,
5856
6327
  WARNING_THRESHOLD,
@@ -5860,9 +6331,13 @@ export {
5860
6331
  debug,
5861
6332
  debugWarn,
5862
6333
  ensureDataDir,
6334
+ extractVerificationCode,
5863
6335
  getDatabase,
5864
6336
  isInternalEmail,
6337
+ isValidPhoneNumber,
6338
+ normalizePhoneNumber,
5865
6339
  parseEmail,
6340
+ parseGoogleVoiceSms,
5866
6341
  resolveConfig,
5867
6342
  sanitizeEmail,
5868
6343
  saveConfig,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/core",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "description": "Core SDK for AgenticMail — programmatic email for AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",