nolimit-x 1.0.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.
@@ -0,0 +1,222 @@
1
+ const QRCode = require('qrcode');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ class QRGenerator {
6
+ constructor() {
7
+ this.defaultOptions = {
8
+ errorCorrectionLevel: 'M',
9
+ type: 'image/png',
10
+ quality: 0.92,
11
+ margin: 1,
12
+ color: {
13
+ dark: '#000000',
14
+ light: '#FFFFFF'
15
+ },
16
+ width: 256
17
+ };
18
+ }
19
+
20
+ // Generate QR code as data URL
21
+ async generateQRDataURL(text, options = {}) {
22
+ try {
23
+ const qrOptions = { ...this.defaultOptions, ...options };
24
+
25
+ const dataURL = await QRCode.toDataURL(text, qrOptions);
26
+ return dataURL;
27
+ } catch (error) {
28
+ console.error('QR code generation failed:', error.message);
29
+ return null;
30
+ }
31
+ }
32
+
33
+ // Generate QR code as buffer
34
+ async generateQRBuffer(text, options = {}) {
35
+ try {
36
+ const qrOptions = { ...this.defaultOptions, ...options };
37
+
38
+ const buffer = await QRCode.toBuffer(text, qrOptions);
39
+ return buffer;
40
+ } catch (error) {
41
+ console.error('QR code generation failed:', error.message);
42
+ return null;
43
+ }
44
+ }
45
+
46
+ // Generate QR code and save to file
47
+ async generateQRFile(text, filePath, options = {}) {
48
+ try {
49
+ const qrOptions = { ...this.defaultOptions, ...options };
50
+
51
+ await QRCode.toFile(filePath, text, qrOptions);
52
+ return filePath;
53
+ } catch (error) {
54
+ console.error('QR code file generation failed:', error.message);
55
+ return null;
56
+ }
57
+ }
58
+
59
+ // Generate QR code with custom colors
60
+ async generateCustomQR(text, colors = {}, options = {}) {
61
+ try {
62
+ const customOptions = {
63
+ ...this.defaultOptions,
64
+ color: {
65
+ dark: colors.dark || this.defaultOptions.color.dark,
66
+ light: colors.light || this.defaultOptions.color.light
67
+ },
68
+ ...options
69
+ };
70
+
71
+ const dataURL = await QRCode.toDataURL(text, customOptions);
72
+ return dataURL;
73
+ } catch (error) {
74
+ console.error('Custom QR code generation failed:', error.message);
75
+ return null;
76
+ }
77
+ }
78
+
79
+ // Generate QR code for tracking
80
+ generateTrackingQR(email, campaignId, options = {}) {
81
+ const trackingData = {
82
+ email: email,
83
+ campaign: campaignId,
84
+ timestamp: new Date().toISOString(),
85
+ id: this.generateTrackingId()
86
+ };
87
+
88
+ const trackingURL = `https://track.example.com/open?data=${encodeURIComponent(JSON.stringify(trackingData))}`;
89
+ return this.generateQRDataURL(trackingURL, options);
90
+ }
91
+
92
+ // Generate QR code for link obfuscation
93
+ generateObfuscatedQR(originalUrl, options = {}) {
94
+ // Create an obfuscated URL that redirects to the original
95
+ const obfuscatedUrl = this.createObfuscatedUrl(originalUrl);
96
+ return this.generateQRDataURL(obfuscatedUrl, options);
97
+ }
98
+
99
+ // Create obfuscated URL
100
+ createObfuscatedUrl(originalUrl) {
101
+ const obfuscationId = this.generateTrackingId();
102
+ return `https://redirect.example.com/${obfuscationId}?target=${encodeURIComponent(originalUrl)}`;
103
+ }
104
+
105
+ // Generate tracking ID
106
+ generateTrackingId() {
107
+ return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
108
+ }
109
+
110
+ // Embed QR code in HTML message
111
+ embedQRInMessage(messageHtml, qrDataURL, position = 'bottom', size = 'medium') {
112
+ const sizeMap = {
113
+ small: '100px',
114
+ medium: '150px',
115
+ large: '200px'
116
+ };
117
+
118
+ const qrHtml = `
119
+ <div style="text-align: center; margin: 20px 0;">
120
+ <img src="${qrDataURL}" alt="QR Code" style="width: ${sizeMap[size] || '150px'}; height: ${sizeMap[size] || '150px'};" />
121
+ <p style="font-size: 12px; color: #666; margin-top: 5px;">Scan to access content</p>
122
+ </div>`;
123
+
124
+ if (position === 'top') {
125
+ return qrHtml + messageHtml;
126
+ } else if (position === 'middle') {
127
+ // Insert in middle of message
128
+ const middleIndex = Math.floor(messageHtml.length / 2);
129
+ return messageHtml.substring(0, middleIndex) + qrHtml + messageHtml.substring(middleIndex);
130
+ } else {
131
+ // Default: bottom
132
+ return messageHtml + qrHtml;
133
+ }
134
+ }
135
+
136
+ // Generate QR code for multiple purposes
137
+ async generateMultiPurposeQR(data, purpose = 'general', options = {}) {
138
+ let qrText = '';
139
+
140
+ switch (purpose) {
141
+ case 'url':
142
+ qrText = data;
143
+ break;
144
+ case 'email':
145
+ qrText = `mailto:${data}`;
146
+ break;
147
+ case 'phone':
148
+ qrText = `tel:${data}`;
149
+ break;
150
+ case 'wifi':
151
+ qrText = `WIFI:S:${data.ssid};T:${data.type || 'WPA'};P:${data.password};;`;
152
+ break;
153
+ case 'vcard':
154
+ qrText = this.generateVCardQR(data);
155
+ break;
156
+ case 'tracking':
157
+ qrText = this.generateTrackingURL(data);
158
+ break;
159
+ default:
160
+ qrText = data;
161
+ }
162
+
163
+ return await this.generateQRDataURL(qrText, options);
164
+ }
165
+
166
+ // Generate vCard QR code
167
+ generateVCardQR(contactData) {
168
+ const vcard = `BEGIN:VCARD
169
+ VERSION:3.0
170
+ FN:${contactData.name || ''}
171
+ ORG:${contactData.organization || ''}
172
+ TEL:${contactData.phone || ''}
173
+ EMAIL:${contactData.email || ''}
174
+ URL:${contactData.url || ''}
175
+ ADR:${contactData.address || ''}
176
+ END:VCARD`;
177
+
178
+ return vcard;
179
+ }
180
+
181
+ // Generate tracking URL
182
+ generateTrackingURL(trackingData) {
183
+ const params = new URLSearchParams(trackingData);
184
+ return `https://track.example.com/click?${params.toString()}`;
185
+ }
186
+
187
+ // Validate QR code
188
+ async validateQR(qrDataURL) {
189
+ try {
190
+ // Basic validation - check if it's a valid data URL
191
+ if (!qrDataURL.startsWith('data:image/')) {
192
+ return false;
193
+ }
194
+
195
+ // Try to decode the QR code
196
+ const buffer = Buffer.from(qrDataURL.split(',')[1], 'base64');
197
+ return buffer.length > 0;
198
+ } catch (error) {
199
+ return false;
200
+ }
201
+ }
202
+
203
+ // Get QR code statistics
204
+ getQRStats(qrDataURL) {
205
+ try {
206
+ const buffer = Buffer.from(qrDataURL.split(',')[1], 'base64');
207
+ return {
208
+ size: buffer.length,
209
+ format: qrDataURL.split(';')[0].split(':')[1],
210
+ valid: true
211
+ };
212
+ } catch (error) {
213
+ return {
214
+ size: 0,
215
+ format: 'unknown',
216
+ valid: false
217
+ };
218
+ }
219
+ }
220
+ }
221
+
222
+ module.exports = QRGenerator;
package/src/sender.js ADDED
@@ -0,0 +1,41 @@
1
+ const path = require('path');
2
+ const { ConfigManager, CampaignProcessor } = require('./processor');
3
+ const DocumentGenerator = require('./document-generator');
4
+
5
+ // Example: Simulated email sender (replace with real SMTP/Rust backend)
6
+ async function sendEmailFn({ email, senderEmail, subject, body, attachments, fromName, replyTo, priority }) {
7
+ // Simulate PDF generation for each email (if needed)
8
+ // You can add logic to generate a PDF from the body or an attachment
9
+ if (attachments && attachments.length > 0) {
10
+ for (const att of attachments) {
11
+ if (att.filename.endsWith('.pdf')) {
12
+ // Generate PDF from HTML content
13
+ const docGen = new DocumentGenerator();
14
+ const pdfBuffer = await docGen.htmlToPdf(att.content);
15
+ // In a real sender, attach pdfBuffer as the file content
16
+ }
17
+ }
18
+ }
19
+ // Simulate sending (replace with real logic)
20
+ console.log(`[SEND] To: ${email} | From: ${senderEmail} | Subject: ${subject}`);
21
+ // Return true for success, false for failure
22
+ return true;
23
+ }
24
+
25
+ async function send(options) {
26
+ try {
27
+ const configPath = options.config || './config.json';
28
+ const configManager = new ConfigManager(configPath);
29
+ const campaignProcessor = new CampaignProcessor(configManager);
30
+ const initialized = await campaignProcessor.initialize();
31
+ if (!initialized) {
32
+ console.error('Failed to initialize campaign.');
33
+ return;
34
+ }
35
+ await campaignProcessor.sendCampaign(sendEmailFn);
36
+ } catch (err) {
37
+ console.error('Fatal error in send:', err.message);
38
+ }
39
+ }
40
+
41
+ module.exports = { send };
package/src/utils.js ADDED
@@ -0,0 +1,374 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const natural = require('natural');
4
+ const { Buffer } = require('buffer');
5
+
6
+ // Date and Time Functions
7
+ function formatDate(date, format) {
8
+ const options = { year: 'numeric', month: '2-digit', day: '2-digit' };
9
+ return date.toLocaleDateString('en-GB', options).split('/').reverse().join('-');
10
+ }
11
+
12
+ function getCurrentDate() {
13
+ return new Date();
14
+ }
15
+
16
+ function getFutureDate(days) {
17
+ const date = new Date();
18
+ date.setDate(date.getDate() + days);
19
+ return date;
20
+ }
21
+
22
+ function getPastDate(weeks) {
23
+ const date = new Date();
24
+ date.setDate(date.getDate() - (weeks * 7));
25
+ return date;
26
+ }
27
+
28
+ // Randomization Functions
29
+ function randomInt(min, max) {
30
+ return Math.floor(Math.random() * (max - min + 1)) + min;
31
+ }
32
+
33
+ function randomString(length, type = '') {
34
+ let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
35
+
36
+ if (type === 'alpha') {
37
+ characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
38
+ } else if (type === 'numeric') {
39
+ characters = '0123456789';
40
+ } else if (type === 'uppercase') {
41
+ characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
42
+ } else if (type === 'lowercase') {
43
+ characters = 'abcdefghijklmnopqrstuvwxyz';
44
+ }
45
+
46
+ let result = '';
47
+ const charactersLength = characters.length;
48
+ for (let i = 0; i < length; i++) {
49
+ result += characters.charAt(Math.floor(Math.random() * charactersLength));
50
+ }
51
+ return result;
52
+ }
53
+
54
+ function randomMixedString(length, type = '') {
55
+ return randomString(length, type);
56
+ }
57
+
58
+ // Encoding Functions
59
+ function base64Encode(text) {
60
+ if (typeof text !== 'string') return '';
61
+ return Buffer.from(text).toString('base64');
62
+ }
63
+
64
+ function base64Decode(text) {
65
+ return Buffer.from(text, 'base64').toString('utf8');
66
+ }
67
+
68
+ function hexEncode(text) {
69
+ return Buffer.from(text).toString('hex');
70
+ }
71
+
72
+ function hexDecode(text) {
73
+ return Buffer.from(text, 'hex').toString('utf8');
74
+ }
75
+
76
+ // Text Formatting Functions
77
+ function uppercase(text) {
78
+ return text.toUpperCase();
79
+ }
80
+
81
+ function lowercase(text) {
82
+ return text.toLowerCase();
83
+ }
84
+
85
+ function capitalize(text) {
86
+ if (typeof text !== 'string') return '';
87
+ return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
88
+ }
89
+
90
+ function nameCase(text) {
91
+ return text.replace(/\b\w+/g, (txt) => txt.charAt(0).toUpperCase() + txt.slice(1).toLowerCase());
92
+ }
93
+
94
+ function sentenceCase(text) {
95
+ return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
96
+ }
97
+
98
+ function detectAndFormatNames(emailName) {
99
+ const tokenizer = new natural.WordTokenizer();
100
+ const tokens = tokenizer.tokenize(emailName.replace(/\d+/g, ''));
101
+ const formattedName = tokens.map(token => token.charAt(0).toUpperCase() + token.slice(1).toLowerCase()).join(' ');
102
+ return formattedName;
103
+ }
104
+
105
+ // Email Functions
106
+ function getDomainFromEmail(email) {
107
+ return email.split('@')[1];
108
+ }
109
+
110
+ function getEmailName(email) {
111
+ return email.split('@')[0];
112
+ }
113
+
114
+ function getCompanyName(email) {
115
+ return email.split('@')[1].split('.')[0];
116
+ }
117
+
118
+ function maskEmail(email) {
119
+ const [username, domain] = email.split('@');
120
+ return `${username.substr(0, 3)}***@${domain.substr(0, 2)}***.${domain.split('.').pop()}`;
121
+ }
122
+
123
+ // File Functions
124
+ async function readContentFromFile(filePath) {
125
+ return new Promise((resolve, reject) => {
126
+ fs.readFile(filePath, 'utf8', (err, data) => {
127
+ if (err) {
128
+ console.error(`Error reading file ${filePath}: ${err.message}`);
129
+ reject(err);
130
+ } else {
131
+ resolve(data);
132
+ }
133
+ });
134
+ });
135
+ }
136
+
137
+ function copyTemplate(src, dest) {
138
+ try {
139
+ const srcPath = path.join(__dirname, '../templates', src);
140
+ const destPath = path.join(dest, src);
141
+ fs.copyFileSync(srcPath, destPath);
142
+ return true;
143
+ } catch (err) {
144
+ console.error(`Failed to copy template ${src}:`, err);
145
+ return false;
146
+ }
147
+ }
148
+
149
+ // Validation Functions
150
+ function isValidEmail(email) {
151
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
152
+ return emailRegex.test(email);
153
+ }
154
+
155
+ function isValidDomain(domain) {
156
+ const domainRegex = /^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\.[a-zA-Z]{2,}$/;
157
+ return domainRegex.test(domain);
158
+ }
159
+
160
+ // Network Functions
161
+ const faviconCache = {};
162
+ const CACHE_FILE = 'favicons-cache.json';
163
+ const FAVICON_TIMEOUT = 3000; // 3 seconds
164
+
165
+ // Load persistent favicon cache from disk
166
+ function loadFaviconCache() {
167
+ try {
168
+ if (fs.existsSync(CACHE_FILE)) {
169
+ const cacheData = fs.readFileSync(CACHE_FILE, 'utf8');
170
+ const cache = JSON.parse(cacheData);
171
+ Object.assign(faviconCache, cache);
172
+ console.log(`Loaded ${Object.keys(cache).length} cached favicons`);
173
+ }
174
+ } catch (error) {
175
+ console.warn('Failed to load favicon cache:', error.message);
176
+ }
177
+ }
178
+
179
+ // Save favicon cache to disk
180
+ function saveFaviconCache() {
181
+ try {
182
+ fs.writeFileSync(CACHE_FILE, JSON.stringify(faviconCache, null, 2));
183
+ console.log(`Saved ${Object.keys(faviconCache).length} favicons to cache`);
184
+ } catch (error) {
185
+ console.warn('Failed to save favicon cache:', error.message);
186
+ }
187
+ }
188
+
189
+ // Fetch favicon with timeout and fallback
190
+ async function fetchFaviconWithTimeout(domain, timeout = FAVICON_TIMEOUT) {
191
+ const controller = new AbortController();
192
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
193
+
194
+ try {
195
+ const response = await fetch(`http://localhost:8080/favicon?domain=${encodeURIComponent(domain)}`, {
196
+ signal: controller.signal
197
+ });
198
+ clearTimeout(timeoutId);
199
+
200
+ if (!response.ok) {
201
+ throw new Error(`HTTP ${response.status}`);
202
+ }
203
+
204
+ const data = await response.json();
205
+ return data.faviconUrl || '';
206
+ } catch (error) {
207
+ clearTimeout(timeoutId);
208
+ throw error;
209
+ }
210
+ }
211
+
212
+ // Fallback favicon sources
213
+ async function fetchFaviconFallback(domain) {
214
+ const fallbackSources = [
215
+ `https://www.google.com/s2/favicons?domain=${domain}&sz=32`,
216
+ `https://t1.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=https://${domain}&size=32`,
217
+ `https://favicon.ico/${domain}`,
218
+ `https://${domain}/favicon.ico`
219
+ ];
220
+
221
+ for (const source of fallbackSources) {
222
+ try {
223
+ const controller = new AbortController();
224
+ const timeoutId = setTimeout(() => controller.abort(), 2000);
225
+
226
+ const response = await fetch(source, {
227
+ signal: controller.signal,
228
+ headers: { 'User-Agent': 'Mozilla/5.0 (compatible; NoLimit/1.0)' }
229
+ });
230
+ clearTimeout(timeoutId);
231
+
232
+ if (response.ok && response.url) {
233
+ return response.url;
234
+ }
235
+ } catch (error) {
236
+ continue; // Try next source
237
+ }
238
+ }
239
+
240
+ return ''; // No favicon found
241
+ }
242
+
243
+ // Main favicon fetching function with caching
244
+ async function fetchFavicon(domain) {
245
+ if (!domain) return '';
246
+
247
+ // Check cache first
248
+ if (faviconCache[domain]) {
249
+ return faviconCache[domain];
250
+ }
251
+
252
+ let faviconUrl = '';
253
+
254
+ // Try primary source first
255
+ try {
256
+ faviconUrl = await fetchFaviconWithTimeout(domain);
257
+ } catch (error) {
258
+ console.warn(`Primary favicon fetch failed for ${domain}:`, error.message);
259
+ }
260
+
261
+ // If primary failed, try fallback sources
262
+ if (!faviconUrl) {
263
+ try {
264
+ faviconUrl = await fetchFaviconFallback(domain);
265
+ } catch (error) {
266
+ console.warn(`Fallback favicon fetch failed for ${domain}:`, error.message);
267
+ }
268
+ }
269
+
270
+ // Cache the result (even if empty)
271
+ faviconCache[domain] = faviconUrl;
272
+
273
+ return faviconUrl;
274
+ }
275
+
276
+ // Batch fetch favicons for multiple domains
277
+ async function batchFetchFavicons(domains) {
278
+ const uniqueDomains = [...new Set(domains.filter(domain => domain))];
279
+ const missingDomains = uniqueDomains.filter(domain => !faviconCache[domain]);
280
+
281
+ if (missingDomains.length === 0) {
282
+ console.log('All favicons already cached');
283
+ return;
284
+ }
285
+
286
+ console.log(`Fetching favicons for ${missingDomains.length} domains...`);
287
+
288
+ // Fetch in parallel with concurrency limit
289
+ const concurrency = 10;
290
+ const results = [];
291
+
292
+ for (let i = 0; i < missingDomains.length; i += concurrency) {
293
+ const batch = missingDomains.slice(i, i + concurrency);
294
+ const batchPromises = batch.map(async (domain) => {
295
+ try {
296
+ const faviconUrl = await fetchFavicon(domain);
297
+ return { domain, faviconUrl, success: true };
298
+ } catch (error) {
299
+ console.warn(`Failed to fetch favicon for ${domain}:`, error.message);
300
+ return { domain, faviconUrl: '', success: false };
301
+ }
302
+ });
303
+
304
+ const batchResults = await Promise.all(batchPromises);
305
+ results.push(...batchResults);
306
+
307
+ // Small delay between batches to be respectful
308
+ if (i + concurrency < missingDomains.length) {
309
+ await new Promise(resolve => setTimeout(resolve, 100));
310
+ }
311
+ }
312
+
313
+ // Update cache with results
314
+ results.forEach(({ domain, faviconUrl }) => {
315
+ faviconCache[domain] = faviconUrl;
316
+ });
317
+
318
+ const successCount = results.filter(r => r.success && r.faviconUrl).length;
319
+ console.log(`Favicon batch complete: ${successCount}/${missingDomains.length} successful`);
320
+
321
+ // Save cache to disk
322
+ saveFaviconCache();
323
+ }
324
+
325
+ // Initialize cache on module load
326
+ loadFaviconCache();
327
+
328
+ // Export all functions
329
+ module.exports = {
330
+ // Date and Time
331
+ formatDate,
332
+ getCurrentDate,
333
+ getFutureDate,
334
+ getPastDate,
335
+
336
+ // Randomization
337
+ randomInt,
338
+ randomString,
339
+ randomMixedString,
340
+
341
+ // Encoding
342
+ base64Encode,
343
+ base64Decode,
344
+ hexEncode,
345
+ hexDecode,
346
+
347
+ // Text Formatting
348
+ uppercase,
349
+ lowercase,
350
+ capitalize,
351
+ nameCase,
352
+ sentenceCase,
353
+ detectAndFormatNames,
354
+
355
+ // Email
356
+ getDomainFromEmail,
357
+ getEmailName,
358
+ getCompanyName,
359
+ maskEmail,
360
+
361
+ // File
362
+ readContentFromFile,
363
+ copyTemplate,
364
+
365
+ // Validation
366
+ isValidEmail,
367
+ isValidDomain,
368
+
369
+ // Network
370
+ fetchFavicon,
371
+ batchFetchFavicons,
372
+ loadFaviconCache,
373
+ saveFaviconCache
374
+ };
@@ -0,0 +1,54 @@
1
+ {
2
+ "smtp": [
3
+ {
4
+ "host": "smtp.gmail.com",
5
+ "port": 587,
6
+ "secure": false,
7
+ "user": "your-email@gmail.com",
8
+ "pass": "your-app-password"
9
+ }
10
+ ],
11
+ "emails_list": {
12
+ "path": "emails.txt"
13
+ },
14
+ "senders_list": {
15
+ "path": "senders.txt"
16
+ },
17
+ "messages_body": {
18
+ "path": "messages.html"
19
+ },
20
+ "configurations": {
21
+ "from_name": "John Doe",
22
+ "from_email": "john.doe@company.com",
23
+ "mail_subject": "Important Update",
24
+ "reply_to": "support@company.com",
25
+ "mail_priority": "3",
26
+ "use_attachment": false,
27
+ "raw_smtp": false,
28
+ "raw_headers": {
29
+ "X-Custom-Header": "Custom Value",
30
+ "X-Mailer": "Custom Mailer v1.0",
31
+ "X-Priority": "1",
32
+ "X-MSMail-Priority": "High",
33
+ "Importance": "high"
34
+ },
35
+ "agent": {
36
+ "is_multi_thread": false,
37
+ "how_many_thread": 1
38
+ },
39
+ "system": {
40
+ "delay_sending": false,
41
+ "delay_sending_seconds": 1
42
+ }
43
+ },
44
+ "attachments": [
45
+ {
46
+ "filename": "document.pdf",
47
+ "path": "attachments/document.pdf",
48
+ "active": false,
49
+ "obfuscate": false,
50
+ "encrypted": false,
51
+ "scripter": false
52
+ }
53
+ ]
54
+ }
@@ -0,0 +1,3 @@
1
+ # List of recipient emails
2
+ recipient1@example.com
3
+ recipient2@example.com
@@ -0,0 +1,5 @@
1
+ <html>
2
+ <body>
3
+ <p>Hello, this is a test message.</p>
4
+ </body>
5
+ </html>
@@ -0,0 +1,3 @@
1
+ # List of sender emails
2
+ sender1@example.com
3
+ sender2@example.com