nolimit-x 1.0.68 → 1.0.70
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/package.json +1 -1
- package/src/advanced-name-extractor.js +6 -2
- package/src/cli.js +6 -1
- package/src/ics-generator.js +161 -0
- package/src/init.js +7 -0
- package/src/organization-extractor.js +6 -2
- package/src/processor.js +90 -24
- package/src/sender.js +34 -2
- package/templates/calendar/invite.ics +13 -0
- package/templates/config.json +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
const fetch = require('node-fetch');
|
|
2
2
|
const crypto = require('crypto');
|
|
3
3
|
const fs = require('fs').promises;
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const NAME_CACHE_PATH = path.join(os.tmpdir(), 'nolimit-name-cache.json');
|
|
4
8
|
|
|
5
9
|
class AdvancedNameExtractor {
|
|
6
10
|
constructor() {
|
|
@@ -118,7 +122,7 @@ class AdvancedNameExtractor {
|
|
|
118
122
|
// Load persistent cache from JSON file
|
|
119
123
|
async loadCache() {
|
|
120
124
|
try {
|
|
121
|
-
const data = await fs.readFile(
|
|
125
|
+
const data = await fs.readFile(NAME_CACHE_PATH, 'utf8');
|
|
122
126
|
const cacheData = JSON.parse(data);
|
|
123
127
|
for (const [email, result] of Object.entries(cacheData)) {
|
|
124
128
|
this.cache.set(email, result);
|
|
@@ -132,7 +136,7 @@ class AdvancedNameExtractor {
|
|
|
132
136
|
async saveCache() {
|
|
133
137
|
try {
|
|
134
138
|
const cacheData = Object.fromEntries(this.cache);
|
|
135
|
-
await fs.writeFile(
|
|
139
|
+
await fs.writeFile(NAME_CACHE_PATH, JSON.stringify(cacheData, null, 2));
|
|
136
140
|
} catch (error) {
|
|
137
141
|
console.warn('Failed to save name cache:', error.message);
|
|
138
142
|
}
|
package/src/cli.js
CHANGED
|
@@ -493,7 +493,12 @@ program
|
|
|
493
493
|
}
|
|
494
494
|
console.log(`${CYAN}Validate${RESET}\n`);
|
|
495
495
|
|
|
496
|
-
|
|
496
|
+
const _expiresMs2 = new Date(licenseResult.info.expires).getTime() - Date.now();
|
|
497
|
+
const _d2 = Math.floor(_expiresMs2 / 86400000);
|
|
498
|
+
const _h2 = Math.floor((_expiresMs2 % 86400000) / 3600000);
|
|
499
|
+
const _m2 = Math.floor((_expiresMs2 % 3600000) / 60000);
|
|
500
|
+
const _s2 = Math.floor((_expiresMs2 % 60000) / 1000);
|
|
501
|
+
console.log(`${GREEN}✓${RESET} License Validated · Remaining: ${GREEN}${_d2}d ${_h2}h ${_m2}m ${_s2}s${RESET}\n`);
|
|
497
502
|
|
|
498
503
|
// Read email list
|
|
499
504
|
const inputPath = path.resolve(workdir, options.input);
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const crypto = require('crypto');
|
|
3
|
+
const { resolvePlaceholders } = require('./simple-placeholders');
|
|
4
|
+
|
|
5
|
+
class ICSGenerator {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.template = null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Load and validate an .ics template file.
|
|
12
|
+
* Returns the raw template string.
|
|
13
|
+
*/
|
|
14
|
+
loadTemplate(icsPath) {
|
|
15
|
+
if (!fs.existsSync(icsPath)) {
|
|
16
|
+
throw new Error(`Calendar invite template not found: ${icsPath}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const raw = fs.readFileSync(icsPath, 'utf8');
|
|
20
|
+
|
|
21
|
+
// Basic validation — must have VCALENDAR and VEVENT
|
|
22
|
+
if (!raw.includes('BEGIN:VCALENDAR')) {
|
|
23
|
+
throw new Error('Invalid .ics template: missing BEGIN:VCALENDAR');
|
|
24
|
+
}
|
|
25
|
+
if (!raw.includes('BEGIN:VEVENT')) {
|
|
26
|
+
throw new Error('Invalid .ics template: missing BEGIN:VEVENT');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.template = raw;
|
|
30
|
+
return raw;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Generate a per-recipient .ics string from the loaded template.
|
|
35
|
+
* Resolves {{PLACEHOLDER}} values and auto-generates required fields if missing.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} template - Raw .ics template content
|
|
38
|
+
* @param {object} context - Placeholder context (EMAIL, NAME, COMPANY, LINK, etc.)
|
|
39
|
+
* @returns {string} Fully resolved .ics content
|
|
40
|
+
*/
|
|
41
|
+
generateICS(template, context) {
|
|
42
|
+
let ics = template;
|
|
43
|
+
|
|
44
|
+
// --- Auto-date placeholders ---
|
|
45
|
+
// Users can write {{DTSTART}}, {{DTEND}}, {{DTSTART+2}} etc.
|
|
46
|
+
// instead of hardcoding 20260402T100000Z format.
|
|
47
|
+
//
|
|
48
|
+
// {{DTSTART}} = tomorrow at 10:00 AM UTC
|
|
49
|
+
// {{DTEND}} = tomorrow at 11:00 AM UTC (1hr event)
|
|
50
|
+
// {{DTSTART+N}} = N days from now at 10:00 AM UTC
|
|
51
|
+
// {{DTEND+N}} = N days from now at 11:00 AM UTC
|
|
52
|
+
const autoDateContext = this._buildAutoDateContext();
|
|
53
|
+
|
|
54
|
+
// Merge auto-dates into context (context wins if user manually sets them)
|
|
55
|
+
const fullContext = Object.assign({}, autoDateContext, context);
|
|
56
|
+
|
|
57
|
+
// Resolve all {{PLACEHOLDER}} values using the shared engine
|
|
58
|
+
ics = resolvePlaceholders(ics, fullContext);
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
// Auto-generate UID if not present — must be unique per recipient + event
|
|
62
|
+
if (!ics.includes('UID:')) {
|
|
63
|
+
const uid = this._generateUID(context.EMAIL || 'unknown');
|
|
64
|
+
ics = ics.replace('BEGIN:VEVENT', `BEGIN:VEVENT\r\nUID:${uid}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Auto-generate DTSTAMP if not present (required by RFC 5545)
|
|
68
|
+
if (!ics.includes('DTSTAMP:')) {
|
|
69
|
+
const dtstamp = this._formatICSDate(new Date());
|
|
70
|
+
ics = ics.replace('BEGIN:VEVENT', `BEGIN:VEVENT\r\nDTSTAMP:${dtstamp}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Auto-inject STATUS:CONFIRMED if not present
|
|
74
|
+
if (!ics.includes('STATUS:')) {
|
|
75
|
+
ics = ics.replace('END:VEVENT', 'STATUS:CONFIRMED\r\nEND:VEVENT');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Auto-inject SEQUENCE:0 if not present
|
|
79
|
+
if (!ics.includes('SEQUENCE:')) {
|
|
80
|
+
ics = ics.replace('END:VEVENT', 'SEQUENCE:0\r\nEND:VEVENT');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Auto-inject PRODID if not present (value doesn't matter, required by RFC)
|
|
84
|
+
if (!ics.includes('PRODID:')) {
|
|
85
|
+
ics = ics.replace('VERSION:2.0', 'VERSION:2.0\r\nPRODID:-//Nolimit//Calendar//EN');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Ensure METHOD:REQUEST is present at the calendar level
|
|
89
|
+
if (!ics.includes('METHOD:')) {
|
|
90
|
+
ics = ics.replace('BEGIN:VCALENDAR', 'BEGIN:VCALENDAR\r\nMETHOD:REQUEST');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Ensure VERSION:2.0 is present
|
|
94
|
+
if (!ics.includes('VERSION:')) {
|
|
95
|
+
ics = ics.replace('BEGIN:VCALENDAR', 'BEGIN:VCALENDAR\r\nVERSION:2.0');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Normalize line endings to CRLF (RFC 5545 requires it)
|
|
99
|
+
ics = ics.replace(/\r?\n/g, '\r\n');
|
|
100
|
+
|
|
101
|
+
return ics;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Build auto-date context so users never have to write raw UTC timestamps.
|
|
106
|
+
*
|
|
107
|
+
* Generates:
|
|
108
|
+
* DTSTART = tomorrow 10:00 UTC
|
|
109
|
+
* DTEND = tomorrow 11:00 UTC
|
|
110
|
+
* DTSTART+1 through DTSTART+30 (days from now)
|
|
111
|
+
* DTEND+1 through DTEND+30
|
|
112
|
+
*/
|
|
113
|
+
_buildAutoDateContext() {
|
|
114
|
+
const ctx = {};
|
|
115
|
+
const now = new Date();
|
|
116
|
+
|
|
117
|
+
for (let i = 1; i <= 30; i++) {
|
|
118
|
+
const start = new Date(now);
|
|
119
|
+
start.setUTCDate(start.getUTCDate() + i);
|
|
120
|
+
start.setUTCHours(10, 0, 0, 0); // 10:00 AM UTC
|
|
121
|
+
|
|
122
|
+
const end = new Date(start);
|
|
123
|
+
end.setUTCHours(11, 0, 0, 0); // 11:00 AM UTC (1hr)
|
|
124
|
+
|
|
125
|
+
const startStr = this._formatICSDate(start);
|
|
126
|
+
const endStr = this._formatICSDate(end);
|
|
127
|
+
|
|
128
|
+
if (i === 1) {
|
|
129
|
+
// Default {{DTSTART}} = tomorrow
|
|
130
|
+
ctx['DTSTART'] = startStr;
|
|
131
|
+
ctx['DTEND'] = endStr;
|
|
132
|
+
}
|
|
133
|
+
ctx[`DTSTART+${i}`] = startStr;
|
|
134
|
+
ctx[`DTEND+${i}`] = endStr;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return ctx;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Generate a unique UID for the calendar event.
|
|
142
|
+
* Based on recipient email + timestamp + random bytes.
|
|
143
|
+
*/
|
|
144
|
+
_generateUID(email) {
|
|
145
|
+
const hash = crypto.createHash('sha256')
|
|
146
|
+
.update(`${email}-${Date.now()}-${crypto.randomBytes(8).toString('hex')}`)
|
|
147
|
+
.digest('hex')
|
|
148
|
+
.substring(0, 24);
|
|
149
|
+
const domain = email.includes('@') ? email.split('@')[1] : 'nolimit.local';
|
|
150
|
+
return `${hash}@${domain}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Format a Date into iCalendar DTSTAMP format: YYYYMMDDTHHmmssZ
|
|
155
|
+
*/
|
|
156
|
+
_formatICSDate(date) {
|
|
157
|
+
return date.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = ICSGenerator;
|
package/src/init.js
CHANGED
|
@@ -13,6 +13,7 @@ function init(projectName) {
|
|
|
13
13
|
// Create project directory and subdirectories
|
|
14
14
|
fs.mkdirSync(projectPath);
|
|
15
15
|
fs.mkdirSync(path.join(projectPath, 'attachments'));
|
|
16
|
+
fs.mkdirSync(path.join(projectPath, 'calendar'));
|
|
16
17
|
|
|
17
18
|
// Copy template files
|
|
18
19
|
const templates = [
|
|
@@ -33,6 +34,12 @@ function init(projectName) {
|
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
// Copy calendar invite template
|
|
38
|
+
const calendarSrc = path.join(templatesDir, 'calendar', 'invite.ics');
|
|
39
|
+
if (fs.existsSync(calendarSrc)) {
|
|
40
|
+
fs.copyFileSync(calendarSrc, path.join(projectPath, 'calendar', 'invite.ics'));
|
|
41
|
+
}
|
|
42
|
+
|
|
36
43
|
// Create empty nolimit.key placeholder
|
|
37
44
|
fs.writeFileSync(path.join(projectPath, 'nolimit.key'), '');
|
|
38
45
|
|
|
@@ -3,6 +3,10 @@ const cheerio = require('cheerio');
|
|
|
3
3
|
const tls = require('tls');
|
|
4
4
|
const dns = require('dns').promises;
|
|
5
5
|
const fs = require('fs').promises;
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const COMPANY_CACHE_PATH = path.join(os.tmpdir(), 'nolimit-company-cache.json');
|
|
6
10
|
|
|
7
11
|
class OrganizationExtractor {
|
|
8
12
|
constructor() {
|
|
@@ -27,7 +31,7 @@ class OrganizationExtractor {
|
|
|
27
31
|
|
|
28
32
|
async loadCache() {
|
|
29
33
|
try {
|
|
30
|
-
const data = await fs.readFile(
|
|
34
|
+
const data = await fs.readFile(COMPANY_CACHE_PATH, 'utf8');
|
|
31
35
|
const cacheData = JSON.parse(data);
|
|
32
36
|
for (const [domain, entry] of Object.entries(cacheData)) {
|
|
33
37
|
this.cache.set(domain, entry);
|
|
@@ -44,7 +48,7 @@ class OrganizationExtractor {
|
|
|
44
48
|
for (const [domain, entry] of this.cache.entries()) {
|
|
45
49
|
cacheData[domain] = entry;
|
|
46
50
|
}
|
|
47
|
-
await fs.writeFile(
|
|
51
|
+
await fs.writeFile(COMPANY_CACHE_PATH, JSON.stringify(cacheData, null, 2));
|
|
48
52
|
} catch (error) {
|
|
49
53
|
console.warn('Failed to save company cache:', error.message);
|
|
50
54
|
}
|
package/src/processor.js
CHANGED
|
@@ -14,6 +14,7 @@ const EngineConfig = require('./engine-config.js');
|
|
|
14
14
|
const QRGenerator = require('./qr-generator');
|
|
15
15
|
const AttachmentHandler = require('./attachment-handler');
|
|
16
16
|
const StructuralJitter = require('./structural-jitter');
|
|
17
|
+
const ICSGenerator = require('./ics-generator');
|
|
17
18
|
|
|
18
19
|
// Configuration management
|
|
19
20
|
class ConfigManager {
|
|
@@ -26,6 +27,7 @@ class ConfigManager {
|
|
|
26
27
|
this.messageBody = '';
|
|
27
28
|
this.attachments = [];
|
|
28
29
|
this.faviconCache = {};
|
|
30
|
+
this.calendarTemplate = null;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
// Load configuration from file
|
|
@@ -193,6 +195,35 @@ class ConfigManager {
|
|
|
193
195
|
}
|
|
194
196
|
}
|
|
195
197
|
|
|
198
|
+
// Load calendar invite template
|
|
199
|
+
loadCalendarInvite() {
|
|
200
|
+
try {
|
|
201
|
+
const conf = this.config.configurations || {};
|
|
202
|
+
const enabled = conf.calendar_invite === true
|
|
203
|
+
|| (conf.agent && conf.agent.calendar_invite === true);
|
|
204
|
+
|
|
205
|
+
if (!enabled) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Always look in calendar/invite.ics relative to the workspace (config.json dir).
|
|
210
|
+
// No path config needed — the folder makes it obvious where to edit.
|
|
211
|
+
const icsPath = path.join(path.dirname(this.configPath), 'calendar', 'invite.ics');
|
|
212
|
+
|
|
213
|
+
if (!fs.existsSync(icsPath)) {
|
|
214
|
+
console.warn(`Calendar folder not found. Create calendar/invite.ics in your workspace to use calendar invites.`);
|
|
215
|
+
return true; // Non-fatal — just skip
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const icsGen = new ICSGenerator();
|
|
219
|
+
this.calendarTemplate = icsGen.loadTemplate(icsPath);
|
|
220
|
+
return true;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.error(`Failed to load calendar invite: ${error.message}`);
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
196
227
|
// Get random sender
|
|
197
228
|
getRandomSender() {
|
|
198
229
|
if (this.sendersList.length === 0) {
|
|
@@ -247,6 +278,11 @@ class CampaignProcessor {
|
|
|
247
278
|
// Subject rotation
|
|
248
279
|
this.subjects = [];
|
|
249
280
|
this._subjectIndex = 0;
|
|
281
|
+
// ICSGenerator singleton — instantiated once, reused per email
|
|
282
|
+
this.icsGen = new ICSGenerator();
|
|
283
|
+
// Hoisted per-campaign flags (set in initialize, not per-email)
|
|
284
|
+
this.calendarEnabled = false;
|
|
285
|
+
this.cachedFromName = null; // non-null = static, null = resolve per-email
|
|
250
286
|
// Note: SMTP optimization is handled by Rust backend
|
|
251
287
|
}
|
|
252
288
|
|
|
@@ -259,6 +295,7 @@ class CampaignProcessor {
|
|
|
259
295
|
if (!this.configManager.loadSendersList()) return false;
|
|
260
296
|
if (!this.configManager.loadMessageBody()) return false;
|
|
261
297
|
if (!this.configManager.loadAttachments()) return false;
|
|
298
|
+
if (!this.configManager.loadCalendarInvite()) return false;
|
|
262
299
|
|
|
263
300
|
const summary = this.configManager.getSummary();
|
|
264
301
|
const engineSummary = this.engineConfig.getConfigSummary();
|
|
@@ -279,6 +316,41 @@ class CampaignProcessor {
|
|
|
279
316
|
} catch (e) { /* subjects.txt not found — use mail_subject */ }
|
|
280
317
|
}
|
|
281
318
|
|
|
319
|
+
// --- Hoist per-campaign flags (computed once, not per email) ---
|
|
320
|
+
const _conf = this.configManager.config.configurations;
|
|
321
|
+
|
|
322
|
+
// Flaw 2 fix: calendarEnabled computed once
|
|
323
|
+
this.calendarEnabled = (_conf.calendar_invite === true)
|
|
324
|
+
|| (_conf.agent && _conf.agent.calendar_invite === true);
|
|
325
|
+
|
|
326
|
+
// Flaw 3 fix: if from_name has no {{placeholders}}, resolve it once
|
|
327
|
+
const _rawFromName = _conf.from_name || '';
|
|
328
|
+
if (!_rawFromName.includes('{{')) {
|
|
329
|
+
this.cachedFromName = _rawFromName; // pure static string
|
|
330
|
+
} else {
|
|
331
|
+
this.cachedFromName = null; // has placeholders → resolve per email
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Flaw 1 fix: pre-warm org/name caches for all target domains in parallel
|
|
335
|
+
// Fires during the startup window — by the time the loop starts, cache is hot
|
|
336
|
+
const _uniqueDomains = [...new Set(
|
|
337
|
+
this.configManager.emailList.map(e => {
|
|
338
|
+
const parts = (typeof e === 'string' ? e : e.email || '').split('@');
|
|
339
|
+
return parts[1] ? parts[1].toLowerCase() : null;
|
|
340
|
+
}).filter(Boolean)
|
|
341
|
+
)];
|
|
342
|
+
Promise.allSettled(
|
|
343
|
+
_uniqueDomains.flatMap(domain => [
|
|
344
|
+
this.orgExtractor.extractOrganization(domain).catch(() => {}),
|
|
345
|
+
// name extractor uses the full email — warm first email per domain
|
|
346
|
+
this.nameExtractor.extractAdvancedName(
|
|
347
|
+
this.configManager.emailList.find(e =>
|
|
348
|
+
(typeof e === 'string' ? e : e.email || '').endsWith('@' + domain)
|
|
349
|
+
) || ('user@' + domain)
|
|
350
|
+
).catch(() => {})
|
|
351
|
+
])
|
|
352
|
+
); // intentionally fire-and-forget — no await
|
|
353
|
+
|
|
282
354
|
return true;
|
|
283
355
|
}
|
|
284
356
|
|
|
@@ -440,6 +512,7 @@ class CampaignProcessor {
|
|
|
440
512
|
NAME: nameResult.name,
|
|
441
513
|
COMPANY: companyResult.name,
|
|
442
514
|
LINK: processedLink,
|
|
515
|
+
FROM_EMAIL: senderEmail,
|
|
443
516
|
// QRCODE always populated — attachments use same context
|
|
444
517
|
QRCODE: qrCodeHtml,
|
|
445
518
|
BASE_TAG: baseTagHtml,
|
|
@@ -497,29 +570,7 @@ class CampaignProcessor {
|
|
|
497
570
|
}
|
|
498
571
|
}
|
|
499
572
|
|
|
500
|
-
|
|
501
|
-
if (this.engineConfig.isFeatureEnabled('dkimSpoofing')) {
|
|
502
|
-
const dkimMethod = this.engineConfig.getDKIMMethod();
|
|
503
|
-
const emailData = { headers: processedRawHeaders || {}, body: processedBody };
|
|
504
|
-
|
|
505
|
-
switch (dkimMethod) {
|
|
506
|
-
case 'replay':
|
|
507
|
-
const replayResult = this.dkimSpoofer.applyReplaySignature(emailData, domain);
|
|
508
|
-
processedRawHeaders = { ...processedRawHeaders, ...replayResult.headers };
|
|
509
|
-
processedBody = replayResult.body;
|
|
510
|
-
break;
|
|
511
|
-
case 'hybrid':
|
|
512
|
-
const hybridResult = this.dkimSpoofer.applyHybridAttack(emailData, domain);
|
|
513
|
-
processedRawHeaders = { ...processedRawHeaders, ...hybridResult.headers };
|
|
514
|
-
processedBody = hybridResult.body;
|
|
515
|
-
break;
|
|
516
|
-
case 'direct':
|
|
517
|
-
const directResult = this.dkimSpoofer.generateDKIMSignature(emailData, domain);
|
|
518
|
-
processedRawHeaders = { ...processedRawHeaders, ...directResult.headers };
|
|
519
|
-
processedBody = directResult.body;
|
|
520
|
-
break;
|
|
521
|
-
}
|
|
522
|
-
}
|
|
573
|
+
|
|
523
574
|
|
|
524
575
|
// Apply structural jitter — make each email's byte pattern unique
|
|
525
576
|
processedBody = this.structuralJitter.apply(processedBody);
|
|
@@ -591,6 +642,17 @@ class CampaignProcessor {
|
|
|
591
642
|
try { await this.attachmentHandler.documentGenerator.closeBrowser(); } catch (e) { }
|
|
592
643
|
}
|
|
593
644
|
|
|
645
|
+
// Generate calendar invite if enabled
|
|
646
|
+
let calendarInvite = null;
|
|
647
|
+
// calendarEnabled is hoisted — computed once in initialize(), not per email
|
|
648
|
+
if (this.calendarEnabled && this.configManager.calendarTemplate) {
|
|
649
|
+
try {
|
|
650
|
+
calendarInvite = this.icsGen.generateICS(this.configManager.calendarTemplate, context);
|
|
651
|
+
} catch (icsErr) {
|
|
652
|
+
console.error(`Failed to generate calendar invite for ${email}: ${icsErr.message}`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
594
656
|
return {
|
|
595
657
|
email,
|
|
596
658
|
senderEmail, // visible From header
|
|
@@ -598,7 +660,11 @@ class CampaignProcessor {
|
|
|
598
660
|
subject: processedSubject,
|
|
599
661
|
body: processedBody,
|
|
600
662
|
attachments: processedAttachments,
|
|
601
|
-
|
|
663
|
+
calendarInvite: calendarInvite,
|
|
664
|
+
// cachedFromName is non-null when from_name has no per-recipient placeholders
|
|
665
|
+
fromName: this.cachedFromName !== null
|
|
666
|
+
? this.cachedFromName
|
|
667
|
+
: resolvePlaceholders(conf.from_name || '', context),
|
|
602
668
|
replyTo: this.configManager.config.configurations.reply_to,
|
|
603
669
|
priority: this.configManager.config.configurations.mail_priority,
|
|
604
670
|
rawHeaders: processedRawHeaders
|
package/src/sender.js
CHANGED
|
@@ -206,7 +206,14 @@ async function send(options) {
|
|
|
206
206
|
console.error(`\n${RED}✗ ${licenseResult.error}${RESET}`);
|
|
207
207
|
process.exit(1);
|
|
208
208
|
}
|
|
209
|
-
|
|
209
|
+
// Format license expiry as a countdown instead of a raw date
|
|
210
|
+
const _expiresMs = new Date(licenseResult.info.expires).getTime() - Date.now();
|
|
211
|
+
const _days = Math.floor(_expiresMs / 86400000);
|
|
212
|
+
const _hrs = Math.floor((_expiresMs % 86400000) / 3600000);
|
|
213
|
+
const _mins = Math.floor((_expiresMs % 3600000) / 60000);
|
|
214
|
+
const _secs = Math.floor((_expiresMs % 60000) / 1000);
|
|
215
|
+
const _countdown = `${_days}d ${_hrs}h ${_mins}m ${_secs}s`;
|
|
216
|
+
console.log(`${GREEN}✓${RESET} License Validated · Remaining: ${GREEN}${_countdown}${RESET}\n`);
|
|
210
217
|
|
|
211
218
|
// Background server check-in (non-blocking, every 7 days)
|
|
212
219
|
serverVerify(licenseResult.rawKey).catch(() => { });
|
|
@@ -245,7 +252,21 @@ async function send(options) {
|
|
|
245
252
|
rustStartPromise,
|
|
246
253
|
]);
|
|
247
254
|
|
|
248
|
-
|
|
255
|
+
const _conf = configManager.config?.configurations || {};
|
|
256
|
+
const _sendersDisplay = _conf.multiple_senders && configManager.sendersList.length > 0
|
|
257
|
+
? ` · Senders: ${configManager.sendersList.length}`
|
|
258
|
+
: '';
|
|
259
|
+
spinner.stop(`Campaign Initialized · Emails: ${configManager.emailList.length}${_sendersDisplay}`);
|
|
260
|
+
|
|
261
|
+
// Flaw 4 fix: pre-warm SMTP TCP connection via Rust ping
|
|
262
|
+
// Fire-and-forget — Rust opens a TCP connection to the SMTP server so the
|
|
263
|
+
// first real job doesn't pay the full handshake cost.
|
|
264
|
+
const _smtp = configManager.smtpConfig;
|
|
265
|
+
try {
|
|
266
|
+
if (rustClient.isRunning) {
|
|
267
|
+
rustClient.sendToRust({ command: 'ping', id: 0, data: { host: _smtp.host, port: _smtp.port } });
|
|
268
|
+
}
|
|
269
|
+
} catch (_) { /* non-critical */ }
|
|
249
270
|
|
|
250
271
|
// ═══════════════════════════════════════════════════════
|
|
251
272
|
// SMTP
|
|
@@ -274,6 +295,7 @@ async function send(options) {
|
|
|
274
295
|
if (conf.subject_rotation) activeFeatures.push('Subject Rotation');
|
|
275
296
|
if (conf.direct_mx) activeFeatures.push('Direct MX');
|
|
276
297
|
if (conf.use_nodemailer) activeFeatures.push('Nodemailer');
|
|
298
|
+
if (conf.calendar_invite || (conf.agent && conf.agent.calendar_invite)) activeFeatures.push('Calendar Invite');
|
|
277
299
|
console.log(`${GREEN}Active Features:${RESET} [${activeFeatures.join(', ')}]`);
|
|
278
300
|
|
|
279
301
|
// ═══════════════════════════════════════════════════════
|
|
@@ -432,6 +454,9 @@ async function send(options) {
|
|
|
432
454
|
if (processedEmail.attachments && processedEmail.attachments.length > 0) {
|
|
433
455
|
job.attachments = processedEmail.attachments;
|
|
434
456
|
}
|
|
457
|
+
if (processedEmail.calendarInvite) {
|
|
458
|
+
job.calendar_invite = processedEmail.calendarInvite;
|
|
459
|
+
}
|
|
435
460
|
if (processedEmail.fromName) {
|
|
436
461
|
job.from_name = processedEmail.fromName;
|
|
437
462
|
}
|
|
@@ -605,6 +630,12 @@ async function sendViaNodemailer(jobs) {
|
|
|
605
630
|
contentType: att.content_type || 'application/octet-stream'
|
|
606
631
|
}));
|
|
607
632
|
}
|
|
633
|
+
if (job.calendar_invite) {
|
|
634
|
+
mailOptions.icalEvent = {
|
|
635
|
+
method: 'REQUEST',
|
|
636
|
+
content: job.calendar_invite
|
|
637
|
+
};
|
|
638
|
+
}
|
|
608
639
|
const info = await transporter.sendMail(mailOptions);
|
|
609
640
|
counter++;
|
|
610
641
|
console.log(`${WHITE}${counter}. From:${_RESET} ${colors.from}${maskEmail(fromAddr)}${_RESET} ${WHITE}Recipient:${_RESET} ${colors.to}${maskEmail(toAddr)}${_RESET}`);
|
|
@@ -676,6 +707,7 @@ async function sendEmailViaRust(jobs, useRawSmtp) {
|
|
|
676
707
|
vulnerability_scoring: job.vulnerability_scoring || false,
|
|
677
708
|
advanced_headers: job.advanced_headers || false,
|
|
678
709
|
use_nodemailer: job.use_nodemailer || false,
|
|
710
|
+
calendar_invite: job.calendar_invite || null,
|
|
679
711
|
license_key: job.license_key || null
|
|
680
712
|
}));
|
|
681
713
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
BEGIN:VCALENDAR
|
|
2
|
+
VERSION:2.0
|
|
3
|
+
METHOD:REQUEST
|
|
4
|
+
BEGIN:VEVENT
|
|
5
|
+
DTSTART:{{DTSTART}}
|
|
6
|
+
DTEND:{{DTEND}}
|
|
7
|
+
SUMMARY:Your event title here
|
|
8
|
+
DESCRIPTION:Dear {{NAME}}\n\nYour event description here.\n\nClick here: {{LINK}}
|
|
9
|
+
LOCATION:New York
|
|
10
|
+
ORGANIZER;CN={{COMPANY}}:mailto:{{FROM_EMAIL}}
|
|
11
|
+
ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION;CN={{NAME}}:mailto:{{EMAIL}}
|
|
12
|
+
END:VEVENT
|
|
13
|
+
END:VCALENDAR
|