@agenticmail/core 0.3.4 → 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 +154 -2
- package/dist/index.js +467 -0
- 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
|
}
|
|
@@ -1078,6 +1085,8 @@ declare class GatewayManager {
|
|
|
1078
1085
|
private tunnel;
|
|
1079
1086
|
private dnsConfigurator;
|
|
1080
1087
|
private domainPurchaser;
|
|
1088
|
+
private smsManager;
|
|
1089
|
+
private smsPollers;
|
|
1081
1090
|
constructor(options: GatewayManagerOptions);
|
|
1082
1091
|
/**
|
|
1083
1092
|
* Check if a message has already been delivered to an agent (deduplication).
|
|
@@ -1190,6 +1199,11 @@ declare class GatewayManager {
|
|
|
1190
1199
|
success: boolean;
|
|
1191
1200
|
error?: string;
|
|
1192
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;
|
|
1193
1207
|
shutdown(): Promise<void>;
|
|
1194
1208
|
/**
|
|
1195
1209
|
* Resume gateway from saved config (e.g., after server restart).
|
|
@@ -1237,6 +1251,144 @@ declare class RelayBridge {
|
|
|
1237
1251
|
}
|
|
1238
1252
|
declare function startRelayBridge(options: RelayBridgeOptions): RelayBridge;
|
|
1239
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
|
+
|
|
1240
1392
|
declare function debug(tag: string, message: string): void;
|
|
1241
1393
|
declare function debugWarn(tag: string, message: string): void;
|
|
1242
1394
|
|
|
@@ -1371,4 +1523,4 @@ declare class SetupManager {
|
|
|
1371
1523
|
isInitialized(): boolean;
|
|
1372
1524
|
}
|
|
1373
1525
|
|
|
1374
|
-
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
|
@@ -4433,6 +4433,406 @@ var TunnelManager = class {
|
|
|
4433
4433
|
|
|
4434
4434
|
// src/gateway/manager.ts
|
|
4435
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
|
|
4436
4836
|
var GatewayManager = class {
|
|
4437
4837
|
constructor(options) {
|
|
4438
4838
|
this.options = options;
|
|
@@ -4449,6 +4849,10 @@ var GatewayManager = class {
|
|
|
4449
4849
|
} catch {
|
|
4450
4850
|
this.config = { mode: "none" };
|
|
4451
4851
|
}
|
|
4852
|
+
try {
|
|
4853
|
+
this.smsManager = new SmsManager(options.db);
|
|
4854
|
+
} catch {
|
|
4855
|
+
}
|
|
4452
4856
|
}
|
|
4453
4857
|
db;
|
|
4454
4858
|
stalwart;
|
|
@@ -4459,6 +4863,8 @@ var GatewayManager = class {
|
|
|
4459
4863
|
tunnel = null;
|
|
4460
4864
|
dnsConfigurator = null;
|
|
4461
4865
|
domainPurchaser = null;
|
|
4866
|
+
smsManager = null;
|
|
4867
|
+
smsPollers = /* @__PURE__ */ new Map();
|
|
4462
4868
|
/**
|
|
4463
4869
|
* Check if a message has already been delivered to an agent (deduplication).
|
|
4464
4870
|
*/
|
|
@@ -4487,6 +4893,26 @@ var GatewayManager = class {
|
|
|
4487
4893
|
return;
|
|
4488
4894
|
}
|
|
4489
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
|
+
}
|
|
4490
4916
|
try {
|
|
4491
4917
|
await this.tryProcessApprovalReply(mail);
|
|
4492
4918
|
} catch (err) {
|
|
@@ -5139,10 +5565,38 @@ var GatewayManager = class {
|
|
|
5139
5565
|
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
5140
5566
|
}
|
|
5141
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
|
+
}
|
|
5142
5592
|
// --- Lifecycle ---
|
|
5143
5593
|
async shutdown() {
|
|
5144
5594
|
await this.relay.shutdown();
|
|
5145
5595
|
this.tunnel?.stop();
|
|
5596
|
+
for (const poller of this.smsPollers.values()) {
|
|
5597
|
+
poller.stopPolling();
|
|
5598
|
+
}
|
|
5599
|
+
this.smsPollers.clear();
|
|
5146
5600
|
}
|
|
5147
5601
|
/**
|
|
5148
5602
|
* Resume gateway from saved config (e.g., after server restart).
|
|
@@ -5162,6 +5616,13 @@ var GatewayManager = class {
|
|
|
5162
5616
|
console.error("[GatewayManager] Failed to resume relay:", err);
|
|
5163
5617
|
}
|
|
5164
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
|
+
}
|
|
5165
5626
|
if (this.config.mode === "domain" && this.config.domain) {
|
|
5166
5627
|
try {
|
|
5167
5628
|
this.cfClient = new CloudflareClient(
|
|
@@ -5859,6 +6320,8 @@ export {
|
|
|
5859
6320
|
RelayGateway,
|
|
5860
6321
|
SPAM_THRESHOLD,
|
|
5861
6322
|
SetupManager,
|
|
6323
|
+
SmsManager,
|
|
6324
|
+
SmsPoller,
|
|
5862
6325
|
StalwartAdmin,
|
|
5863
6326
|
TunnelManager,
|
|
5864
6327
|
WARNING_THRESHOLD,
|
|
@@ -5868,9 +6331,13 @@ export {
|
|
|
5868
6331
|
debug,
|
|
5869
6332
|
debugWarn,
|
|
5870
6333
|
ensureDataDir,
|
|
6334
|
+
extractVerificationCode,
|
|
5871
6335
|
getDatabase,
|
|
5872
6336
|
isInternalEmail,
|
|
6337
|
+
isValidPhoneNumber,
|
|
6338
|
+
normalizePhoneNumber,
|
|
5873
6339
|
parseEmail,
|
|
6340
|
+
parseGoogleVoiceSms,
|
|
5874
6341
|
resolveConfig,
|
|
5875
6342
|
sanitizeEmail,
|
|
5876
6343
|
saveConfig,
|