@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 +155 -2
- package/dist/index.js +523 -48
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
821
|
-
|
|
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
|
-
|
|
828
|
-
|
|
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 =
|
|
833
|
-
|
|
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 {
|
|
885
|
+
const { execFileSync: execFileSync3 } = await import("child_process");
|
|
879
886
|
const signatureId = `agenticmail-${domain.replace(/\./g, "-")}`;
|
|
880
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
[
|
|
908
|
-
[
|
|
909
|
-
[
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 {
|
|
950
|
+
const { execFileSync: execFileSync3 } = await import("child_process");
|
|
945
951
|
try {
|
|
946
|
-
|
|
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 {
|
|
3817
|
-
const whoisOutput =
|
|
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 {
|
|
4282
|
-
const sysPath =
|
|
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(/ /gi, " ").replace(/&/gi, "&").replace(/</gi, "<").replace(/>/gi, ">").replace(/"/gi, '"').replace(/'/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 {
|
|
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 =
|
|
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 =
|
|
5387
|
-
|
|
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 =
|
|
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 =
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5963
|
+
execFileSync2("open", ["-a", "Docker"], { timeout: 1e4, stdio: "ignore" });
|
|
5496
5964
|
} catch {
|
|
5497
5965
|
}
|
|
5498
5966
|
} else if (os === "linux") {
|
|
5499
5967
|
try {
|
|
5500
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
5555
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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,
|