@feardread/fear 1.2.0 → 2.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.
@@ -1,77 +1,523 @@
1
1
  const nodemailer = require("nodemailer");
2
+ const FormData = require("form-data");
3
+ const Mailgun = require("mailgun");
2
4
 
3
- module.exports = class Worker {
4
- constructor(mailinfo) {
5
- this.mailinfo = mailinfo;
6
- this.email = mailinfo.smtp.auth.user;
7
- }
8
-
9
- /**
10
- * Send a message.
11
- *
12
- * @param options An object containing to, from, subject and text properties (matches the IContact interface,
13
- * but can't be used since the type comes from nodemailer, not app code).
14
- * @return A Promise that eventually resolves to a string (null for success, error message for an error).
15
- */
16
- sendMessage = async (options) => {
17
- return new Promise((res, req) => {
18
- const transport = nodemailer.createTransport(this.mailinfo.smtp);
19
-
20
- transport.sendMail( options, (error, info) => {
21
- if (error) return reject(error);
22
- return resolve();
5
+ module.exports = function (fear) {
6
+ const _this = {};
7
+ const logger = fear.getLogger();
8
+
9
+ _this.mailConfig = fear.mailinfo || {};
10
+ _this.mailService = fear.mailinfo?.service || 'smtp';
11
+ _this.transporter = null;
12
+ _this.mailgunClient = null;
13
+
14
+ // Initialize the appropriate mail service
15
+ _this.initializeMailService = () => {
16
+ if (_this.mailService === 'mailgun') {
17
+ if (!_this.mailConfig.mailgun) {
18
+ throw new Error('Missing Mailgun configuration. Please update mail configuration.');
19
+ }
20
+
21
+ const { apiKey, domain, region } = _this.mailConfig.mailgun;
22
+
23
+ if (!apiKey || !domain) {
24
+ throw new Error('Mailgun requires apiKey and domain in configuration.');
25
+ }
26
+
27
+ const mailgun = new Mailgun(FormData);
28
+ const clientOptions = {
29
+ username: 'api',
30
+ key: apiKey
31
+ };
32
+
33
+ // Add EU endpoint if region is specified
34
+ if (region === 'EU') {
35
+ clientOptions.url = 'https://api.eu.mailgun.net';
36
+ }
37
+
38
+ _this.mailgunClient = mailgun.client(clientOptions);
39
+ _this.mailgunDomain = domain;
40
+
41
+ logger.info('Mailgun client initialized successfully.');
42
+ } else {
43
+ // Default SMTP setup with nodemailer
44
+ if (!_this.mailConfig.smtp || !_this.mailConfig.smtp[_this.mailService]) {
45
+ throw new Error(`Missing mail configuration for service: ${_this.mailService}. Please update mail configuration.`);
46
+ }
47
+
48
+ _this.transporter = nodemailer.createTransport(_this.mailConfig.smtp[_this.mailService]);
49
+ _this.transporter.verify()
50
+ .then(() => logger.info('Mail transport setup complete.'))
51
+ .catch((error) => logger.error('Error loading mail transport :: ', error));
52
+ }
53
+ };
54
+
55
+ // Initialize on module load
56
+ _this.initializeMailService();
57
+
58
+ _this.isValidEmail = (email) => {
59
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
60
+ return emailRegex.test(email);
61
+ };
62
+
63
+ _this.validateEmailOptions = (options) => {
64
+ const errors = [];
65
+
66
+ if (!options.to) errors.push('Recipient email (to) is required');
67
+ if (!options.subject) errors.push('Subject is required');
68
+ if (!options.html && !options.text) errors.push('Email content (html or text) is required');
69
+
70
+ if (options.to && !_this.isValidEmail(options.to)) {
71
+ errors.push('Invalid recipient email address');
72
+ }
73
+
74
+ if (options.from && !_this.isValidEmail(options.from)) {
75
+ errors.push('Invalid sender email address');
76
+ }
77
+
78
+ return {
79
+ isValid: errors.length === 0,
80
+ errors
81
+ };
82
+ };
83
+
84
+ _this.sendViaMailgun = (options) => {
85
+ const messageData = {
86
+ from: options.from,
87
+ to: Array.isArray(options.to) ? options.to : [options.to],
88
+ subject: options.subject,
89
+ text: options.text,
90
+ html: options.html
91
+ };
92
+
93
+ // Add reply-to if specified
94
+ if (options.replyTo) {
95
+ messageData['h:Reply-To'] = options.replyTo;
96
+ }
97
+
98
+ // Add CC if specified
99
+ if (options.cc) {
100
+ messageData.cc = Array.isArray(options.cc) ? options.cc : [options.cc];
101
+ }
102
+
103
+ // Add BCC if specified
104
+ if (options.bcc) {
105
+ messageData.bcc = Array.isArray(options.bcc) ? options.bcc : [options.bcc];
106
+ }
107
+
108
+ return _this.mailgunClient.messages.create(_this.mailgunDomain, messageData)
109
+ .then((data) => {
110
+ logger.info(`Email sent successfully via Mailgun :: messageId: ${data.id}`);
111
+ return {
112
+ success: true,
113
+ message: 'Email sent successfully',
114
+ messageId: data.id,
115
+ response: data
116
+ };
117
+ })
118
+ .catch((error) => {
119
+ logger.error('Error sending email via Mailgun :: ', error);
120
+ return {
121
+ success: false,
122
+ message: error.message || 'Failed to send email',
123
+ error: error
124
+ };
125
+ });
126
+ };
127
+
128
+ _this.sendViaSMTP = (options) => {
129
+ return _this.transporter.sendMail(options)
130
+ .then((info) => {
131
+ if (!info.messageId) {
132
+ logger.warn('Email sent but messageId not found :: ', info);
133
+ }
134
+ logger.info(`Email sent successfully via SMTP :: messageId: ${info.messageId}`);
135
+ return {
136
+ success: true,
137
+ message: 'Email sent successfully',
138
+ messageId: info.messageId
139
+ };
140
+ })
141
+ .catch((error) => {
142
+ logger.error('Error sending email via SMTP :: ', error);
143
+ return {
144
+ success: false,
145
+ message: error.message || 'Failed to send email',
146
+ error: error
147
+ };
23
148
  });
149
+ };
150
+
151
+ _this.sendEmail = (options) => {
152
+ const validation = _this.validateEmailOptions(options);
153
+
154
+ if (!validation.isValid) {
155
+ return Promise.reject(new Error(`Validation failed: ${validation.errors.join(', ')}`));
156
+ }
157
+
158
+ // Set default from address based on service
159
+ let defaultFrom;
160
+ if (_this.mailService === 'mailgun') {
161
+ defaultFrom = _this.mailConfig.mailgun.from || `noreply@${_this.mailgunDomain}`;
162
+ } else {
163
+ defaultFrom = _this.mailConfig.smtp[_this.mailService].auth.user;
164
+ }
165
+
166
+ const emailOptions = {
167
+ from: defaultFrom,
168
+ ...options
169
+ };
170
+
171
+ // Route to appropriate sending method
172
+ if (_this.mailService === 'mailgun') {
173
+ return _this.sendViaMailgun(emailOptions);
174
+ } else {
175
+ return _this.sendViaSMTP(emailOptions);
176
+ }
177
+ };
178
+
179
+ _this.handleError = (res, statusCode, error) => {
180
+ logger.error(`E-Mailer Error :: `, error);
181
+ return res.status(statusCode).json({
182
+ success: false,
183
+ message: error.message || 'An error occurred',
184
+ error
24
185
  });
25
- }
26
-
27
- sendProjectEmail = async (data) => {
28
- return new Promise((resolve, reject) => {
29
- let transporter = nodemailer.createTransport(this.mailinfo.smtp);
30
-
31
- const { $subject, fullname, company, email, phone, budget, about } = data;
32
-
33
- const message = `Fullname: ${fullname}\n`
34
- + `Email: ${email}\n`
35
- + `Budget: ${budget}\n`
36
- + `Phone number: + ${phone}\n`
37
- + `Company: ${company}\n`
38
- + `About project: ${about}\n`
39
-
186
+ };
187
+
188
+ _this.getDefaultFromAddress = () => {
189
+ if (_this.mailService === 'mailgun') {
190
+ return _this.mailConfig.mailgun.from || `noreply@${_this.mailgunDomain}`;
191
+ }
192
+ return _this.mailConfig.smtp[_this.mailService].auth.user;
193
+ };
194
+
195
+ _this.templates = {
196
+ baseTemplate(content, title = "Email") {
197
+ return `
198
+ <!DOCTYPE html>
199
+ <html lang="en">
200
+ <head>
201
+ <meta charset="UTF-8">
202
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
203
+ <title>${title}</title>
204
+ <style>
205
+ body {
206
+ margin: 0;
207
+ padding: 0;
208
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
209
+ line-height: 1.6;
210
+ color: #242424;
211
+ background-color: #f4f4f4;
212
+ }
213
+ .container {
214
+ max-width: 600px;
215
+ margin: 40px auto;
216
+ background: #ffffff;
217
+ border-radius: 8px;
218
+ overflow: hidden;
219
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
220
+ }
221
+ .header {
222
+ background: linear-gradient(135deg, #ea6666 0%, #410182 100%);
223
+ color: white;
224
+ padding: 30px;
225
+ text-align: center;
226
+ }
227
+ .content {
228
+ padding: 40px 30px;
229
+ }
230
+ .field {
231
+ margin-bottom: 20px;
232
+ padding-bottom: 20px;
233
+ border-bottom: 1px solid #eee;
234
+ }
235
+ .field:last-child {
236
+ border-bottom: none;
237
+ }
238
+ .label {
239
+ font-weight: 600;
240
+ color: #667eea;
241
+ font-size: 12px;
242
+ text-transform: uppercase;
243
+ letter-spacing: 0.5px;
244
+ margin-bottom: 5px;
245
+ }
246
+ .value {
247
+ color: #333;
248
+ font-size: 16px;
249
+ }
250
+ .footer {
251
+ background: #f8f9fa;
252
+ padding: 20px 30px;
253
+ text-align: center;
254
+ color: #666;
255
+ font-size: 14px;
256
+ }
257
+ .message-box {
258
+ background: #f8f9fa;
259
+ padding: 20px;
260
+ border-radius: 6px;
261
+ margin-top: 10px;
262
+ }
263
+ </style>
264
+ </head>
265
+ <body>
266
+ <div class="container">
267
+ ${content}
268
+ </div>
269
+ </body>
270
+ </html>
271
+ `;
272
+ },
273
+
274
+ projectTemplate(data) {
275
+ const { fullname, company, email, phone, budget, about } = data;
276
+
277
+ const content = `
278
+ <div class="header">
279
+ <h1 style="margin: 0; font-size: 28px;">New Project Inquiry</h1>
280
+ <p style="margin: 10px 0 0 0; opacity: 0.9;">You have received a new project submission</p>
281
+ </div>
282
+ <div class="content">
283
+ <div class="field">
284
+ <div class="label">Full Name</div>
285
+ <div class="value">${fullname || 'Not provided'}</div>
286
+ </div>
287
+ <div class="field">
288
+ <div class="label">Email Address</div>
289
+ <div class="value"><a href="mailto:${email}" style="color: #667eea;">${email}</a></div>
290
+ </div>
291
+ ${company ? `
292
+ <div class="field">
293
+ <div class="label">Company</div>
294
+ <div class="value">${company}</div>
295
+ </div>
296
+ ` : ''}
297
+ ${phone ? `
298
+ <div class="field">
299
+ <div class="label">Phone Number</div>
300
+ <div class="value"><a href="tel:${phone}" style="color: #667eea;">${phone}</a></div>
301
+ </div>
302
+ ` : ''}
303
+ ${budget ? `
304
+ <div class="field">
305
+ <div class="label">Budget</div>
306
+ <div class="value">${budget}</div>
307
+ </div>
308
+ ` : ''}
309
+ ${about ? `
310
+ <div class="field">
311
+ <div class="label">Project Details</div>
312
+ <div class="message-box">${about.replace(/\n/g, '<br>')}</div>
313
+ </div>
314
+ ` : ''}
315
+ </div>
316
+ <div class="footer">
317
+ <p style="margin: 0;">Received on ${new Date().toLocaleString()}</p>
318
+ </div>
319
+ `;
320
+
321
+ return _this.templates.baseTemplate(content, "New Project Inquiry");
322
+ },
323
+
324
+ contactTemplate(data) {
325
+ const { $email, $message, $name } = data;
326
+
327
+ const content = `
328
+ <div class="header">
329
+ <h1 style="margin: 0; font-size: 28px;">New Contact Message</h1>
330
+ <p style="margin: 10px 0 0 0; opacity: 0.9;">You have received a new message</p>
331
+ </div>
332
+ <div class="content">
333
+ ${$name ? `
334
+ <div class="field">
335
+ <div class="label">Name</div>
336
+ <div class="value">${$name}</div>
337
+ </div>
338
+ ` : ''}
339
+ <div class="field">
340
+ <div class="label">Email Address</div>
341
+ <div class="value"><a href="mailto:${$email}" style="color: #667eea;">${$email}</a></div>
342
+ </div>
343
+ <div class="field">
344
+ <div class="label">Message</div>
345
+ <div class="message-box">${$message.replace(/\n/g, '<br>')}</div>
346
+ </div>
347
+ </div>
348
+ <div class="footer">
349
+ <p style="margin: 0;">Received on ${new Date().toLocaleString()}</p>
350
+ </div>
351
+ `;
352
+
353
+ return _this.templates.baseTemplate(content, "New Contact Message");
354
+ },
355
+
356
+ generatePlainText(data, type = 'contact') {
357
+ if (type === 'project') {
358
+ const { fullname, company, email, phone, budget, about } = data;
359
+ return `
360
+ New Project Inquiry
361
+ ===================
362
+
363
+ Full Name: ${fullname || 'Not provided'}
364
+ Email: ${email}
365
+ ${company ? `Company: ${company}\n` : ''}${phone ? `Phone: ${phone}\n` : ''}${budget ? `Budget: ${budget}\n` : ''}
366
+ Project Details:
367
+ ${about || 'No details provided'}
368
+
369
+ Received: ${new Date().toLocaleString()}
370
+ `.trim();
371
+ }
372
+
373
+ // Contact form
374
+ const { $email, $message, $name } = data;
375
+ return `
376
+ New Contact Message
377
+ ===================
378
+
379
+ ${$name ? `Name: ${$name}\n` : ''}Email: ${$email}
380
+
381
+ Message:
382
+ ${$message}
383
+
384
+ Received: ${new Date().toLocaleString()}
385
+ `.trim();
386
+ },
387
+ };
388
+
389
+ return {
390
+ templates: _this.templates,
391
+ sendEmail: _this.sendEmail,
392
+
393
+ sendProjectEmail(req, res) {
394
+ const data = req.body;
395
+ const { $subject, email } = data;
396
+
397
+ if (!email || !_this.isValidEmail(email)) {
398
+ return _this.handleError(res, 400, { message: 'Valid email required' });
399
+ }
400
+
401
+ const htmlContent = _this.templates.projectTemplate(data);
402
+ const textContent = _this.templates.generatePlainText(data, 'project');
40
403
  const options = {
41
- from: email,
42
- to: this.email,
43
- subject: $subject || `Gfolio Contact Form Submission`,
44
- text: message,
404
+ from: _this.getDefaultFromAddress(),
405
+ replyTo: email,
406
+ to: _this.getDefaultFromAddress(),
407
+ subject: $subject || 'New Project Inquiry',
408
+ html: htmlContent,
409
+ text: textContent
45
410
  };
46
-
47
- transporter.sendMail(options, function(error, info) {
48
- if (error) {
49
- return reject({ message: `An error has occured: ${error}`});
50
- }
51
- return resolve({ message: 'Email sent succesfully!'})
52
- })
53
- })
54
- }
55
411
 
56
- sendContactEmail = async (data) => {
57
- return new Promise((resolve, reject) => {
58
- let transport = nodemailer.createTransport(this.mailinfo.smtp);
59
- const { $subject, $email, $message, $source } = data;
412
+ return _this.sendEmail(options)
413
+ .then((resp) => {
414
+ if (!resp.success) {
415
+ return _this.handleError(res, 500, resp);
416
+ }
417
+ return res.status(200).json({
418
+ success: true,
419
+ message: 'Project email sent successfully',
420
+ result: resp
421
+ });
422
+ })
423
+ .catch((error) => _this.handleError(res, 500, error));
424
+ },
425
+
426
+ sendContactEmail(req, res) {
427
+ const { $email, $message, $subject } = req.body;
428
+
429
+ if (!$email || !_this.isValidEmail($email)) {
430
+ return _this.handleError(res, 400, { message: 'Valid email is required' });
431
+ }
432
+ if (!$message || $message.trim().length === 0) {
433
+ return _this.handleError(res, 400, { message: 'Message is required' });
434
+ }
435
+
436
+ const htmlContent = _this.templates.contactTemplate(req.body);
437
+ const textContent = _this.templates.generatePlainText(req.body, 'contact');
60
438
 
61
439
  const options = {
62
- from: $source,
63
- to: this.email,
64
- subject:"Contact Form :: " + $email,
65
- text: $message,
440
+ from: _this.getDefaultFromAddress(),
441
+ replyTo: $email,
442
+ to: _this.getDefaultFromAddress(),
443
+ subject: $subject || `Contact Form Message from ${$email}`,
444
+ html: htmlContent,
445
+ text: textContent
446
+ };
447
+
448
+ return _this.sendEmail(options)
449
+ .then((resp) => {
450
+ if (!resp.success) {
451
+ return res.status(500).json(resp);
452
+ }
453
+ return res.status(200).json({
454
+ success: true,
455
+ message: 'Contact email sent successfully',
456
+ result: resp
457
+ });
458
+ })
459
+ .catch((error) => _this.handleError(res, 500, error));
460
+ },
461
+
462
+ sendSubscriptionEmail(req, res) {
463
+ const { email, options = {} } = req.body;
464
+
465
+ if (!email || !_this.isValidEmail(email)) {
466
+ return _this.handleError(res, 400, { message: 'Valid email is required' });
66
467
  }
67
468
 
68
- transport.sendMail(options, (error, info) => {
69
- console.log('send contact email resp :: ', error);
70
-
71
- if (error) return reject({ message: `An error has occured: ${error}`});
72
-
73
- return resolve({ message: 'Email sent succesfully!'})
469
+ const { subject, customMessage } = options;
470
+ const htmlContent = `
471
+ <div class="header">
472
+ <h1 style="margin: 0; font-size: 28px;">Welcome! 🎉</h1>
473
+ <p style="margin: 10px 0 0 0; opacity: 0.9;">Thank you for subscribing</p>
474
+ </div>
475
+ <div class="content">
476
+ <p style="font-size: 16px;">
477
+ ${customMessage || "You've successfully subscribed to our newsletter. We're excited to have you on board!"}
478
+ </p>
479
+ <p style="font-size: 16px;">
480
+ You'll receive updates and news directly to your inbox.
481
+ </p>
482
+ </div>
483
+ <div class="footer">
484
+ <p style="margin: 0;">If you didn't subscribe, you can safely ignore this email.</p>
485
+ </div>
486
+ `;
487
+
488
+ const emailOptions = {
489
+ to: email,
490
+ subject: subject || 'Welcome to Our Newsletter!',
491
+ html: _this.templates.baseTemplate(htmlContent, 'Subscription Confirmation'),
492
+ text: customMessage || "Thank you for subscribing! You'll receive updates directly to your inbox."
493
+ };
494
+
495
+ return _this.sendEmail(emailOptions)
496
+ .then((resp) => {
497
+ if (!resp.success) {
498
+ return _this.handleError(res, 500, resp);
499
+ }
500
+ return res.status(200).json({
501
+ success: true,
502
+ message: 'Subscription email sent successfully',
503
+ result: resp
504
+ });
505
+ })
506
+ .catch((error) => _this.handleError(res, 500, error));
507
+ },
508
+ sendTestMessage(req, res) {
509
+ const mg = _this.mailgunClient
510
+
511
+ mg.messages.create("sandbox933b4315c0164f209bbf0bc7fb908598.mailgun.org", {
512
+ from: "Mailgun Sandbox <postmaster@sandbox933b4315c0164f209bbf0bc7fb908598.mailgun.org>",
513
+ to: ["Garrett Haptonstall <fear.dread@underworld.dog>"],
514
+ subject: "Hello Garrett Haptonstall",
515
+ text: "Congratulations Garrett Haptonstall, you just sent an email with Mailgun! You are truly awesome!",
516
+ })
517
+ .then((data) => {
518
+ logger.info(data)
74
519
  })
75
- })
76
- }
77
- }
520
+ .catch(error => logger.error(error));
521
+ }
522
+ };
523
+ };