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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nolimit-x",
3
- "version": "1.0.68",
3
+ "version": "1.0.70",
4
4
  "description": "Advanced email sender ",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
@@ -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('name-cache.json', 'utf8');
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('name-cache.json', JSON.stringify(cacheData, null, 2));
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
- console.log(`${GREEN}✓${RESET} License valid (${licenseResult.info.plan} expires ${licenseResult.info.expires})\n`);
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('company-cache.json', 'utf8');
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('company-cache.json', JSON.stringify(cacheData, null, 2));
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
- // Apply DKIM spoofing if enabled
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
- fromName: resolvePlaceholders(this.configManager.config.configurations.from_name || '', context),
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
- console.log(`${GREEN}✓${RESET} License valid (${licenseResult.info.plan} expires ${licenseResult.info.expires})\n`);
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
- spinner.stop(`Campaign initialized (${configManager.emailList.length} targets, ${configManager.sendersList.length} senders)`);
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
@@ -31,6 +31,7 @@
31
31
  "qr_in_attachment": false,
32
32
  "direct_mx": false,
33
33
  "use_nodemailer": false,
34
+ "calendar_invite": false,
34
35
  "raw_smtp": false,
35
36
  "agent": {
36
37
  "is_multi_thread": false,