webspresso 0.0.99 → 0.0.101

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.
Files changed (108) hide show
  1. package/.agents/ADMIN.md +47 -4
  2. package/.agents/AGENTS.md +4 -2
  3. package/.agents/AUTH.md +309 -0
  4. package/.agents/CLI.md +128 -0
  5. package/.agents/EMAIL.md +84 -2
  6. package/.agents/ERRORS.md +59 -0
  7. package/.agents/EVENTS.md +179 -0
  8. package/.agents/ORM.md +149 -0
  9. package/.agents/PLUGINS.md +44 -7
  10. package/.agents/QUEUE.md +18 -10
  11. package/.agents/ROUTING.md +97 -1
  12. package/.agents/STREAMING.md +35 -0
  13. package/AGENTS.md +2 -0
  14. package/README.md +145 -93
  15. package/bin/commands/add-docker.js +467 -0
  16. package/bin/commands/db-makemigrations.js +83 -0
  17. package/bin/commands/deploy-check.js +339 -0
  18. package/bin/commands/list.js +96 -0
  19. package/bin/commands/make-command.js +93 -0
  20. package/bin/commands/make-event.js +80 -0
  21. package/bin/commands/make-factory.js +72 -0
  22. package/bin/commands/make-mail.js +93 -0
  23. package/bin/commands/make-policy.js +106 -0
  24. package/bin/commands/run.js +195 -0
  25. package/bin/webspresso.js +21 -0
  26. package/core/auth/bouncer.js +196 -0
  27. package/core/auth/index.js +73 -1
  28. package/core/auth/magic-link.js +170 -0
  29. package/core/auth/manager.js +343 -3
  30. package/core/auth/middleware.js +45 -6
  31. package/core/auth/session-manager.js +255 -0
  32. package/core/auth/sessions.js +351 -0
  33. package/core/auth/tokens.js +1 -0
  34. package/core/auth/totp.js +4 -1
  35. package/core/auth/user-agent.js +161 -0
  36. package/core/commands/define-command.js +48 -0
  37. package/core/commands/index.js +18 -0
  38. package/core/commands/registry.js +156 -0
  39. package/core/commands/runner.js +76 -0
  40. package/core/compression/compressible.js +1 -1
  41. package/core/compression/middleware.js +35 -251
  42. package/core/content/service.js +8 -2
  43. package/core/events/discovery.js +142 -0
  44. package/core/events/dispatcher.js +552 -0
  45. package/core/events/index.js +23 -0
  46. package/core/events/signal.js +92 -0
  47. package/core/forms/flash.js +234 -0
  48. package/core/forms/form-builder.js +214 -0
  49. package/core/forms/index.js +16 -0
  50. package/core/mail/index.js +23 -0
  51. package/core/mail/mailable.js +250 -0
  52. package/core/mail/mailer.js +184 -0
  53. package/core/mail/transports.js +238 -0
  54. package/core/orm/factory.js +250 -0
  55. package/core/orm/index.js +8 -1
  56. package/core/orm/migrations/diff.js +185 -0
  57. package/core/orm/migrations/index.js +22 -4
  58. package/core/orm/model.js +55 -1
  59. package/core/orm/repository.js +283 -13
  60. package/core/orm/scopes.js +3 -2
  61. package/core/orm/types.js +6 -1
  62. package/core/queue/cron.js +252 -39
  63. package/core/queue/manager.js +20 -2
  64. package/core/queue/scheduler.js +115 -59
  65. package/core/ssr/stream.js +62 -2
  66. package/index.d.ts +540 -14
  67. package/index.js +44 -0
  68. package/package.json +11 -5
  69. package/plugins/admin-panel/api.js +509 -3
  70. package/plugins/admin-panel/index.js +17 -0
  71. package/plugins/admin-panel/modules/admin-users.js +1 -1
  72. package/plugins/audit-log/admin-component.js +54 -54
  73. package/plugins/audit-log/index.js +2 -2
  74. package/plugins/content/client/inline-edit.js +2 -46
  75. package/plugins/content/models/content-entry.js +4 -3
  76. package/plugins/content/models/content-type.js +4 -3
  77. package/plugins/cors.js +15 -4
  78. package/plugins/email/auth-bridge.js +66 -0
  79. package/plugins/email/bundled-templates.js +15 -0
  80. package/plugins/email/index.js +48 -0
  81. package/plugins/email/preview-handler.js +555 -0
  82. package/plugins/email/service.js +23 -12
  83. package/plugins/email/templates/auth-magic-link.mjml +13 -0
  84. package/plugins/email/transport.js +30 -11
  85. package/plugins/file-manager/admin-component.js +49 -49
  86. package/plugins/file-manager/index.js +4 -4
  87. package/plugins/index.js +2 -0
  88. package/plugins/redirect/index.js +1 -1
  89. package/plugins/webhooks/admin-component.js +608 -0
  90. package/plugins/webhooks/api-handlers.js +237 -0
  91. package/plugins/webhooks/crypto.js +144 -0
  92. package/plugins/webhooks/dispatcher.js +708 -0
  93. package/plugins/webhooks/index.js +263 -0
  94. package/plugins/webhooks/matcher.js +113 -0
  95. package/plugins/webhooks/migration-template.js +75 -0
  96. package/plugins/webhooks/models.js +98 -0
  97. package/plugins/webhooks/sender.js +176 -0
  98. package/src/api/api-loader.js +1 -1
  99. package/src/app-context.js +48 -6
  100. package/src/bundle/index.js +2 -1
  101. package/src/dev-reload/index.js +9 -1
  102. package/src/discovery/scan-routes.js +10 -1
  103. package/src/file-router.js +173 -4
  104. package/src/helpers.js +150 -1
  105. package/src/njk-frontmatter.js +3 -0
  106. package/src/pages/page-loader.js +1 -1
  107. package/src/routing/cascading.js +120 -4
  108. package/src/server.js +180 -26
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Declarative Mailable Base Class (AdonisJS & Laravel-inspired)
3
+ * @module core/mail/mailable
4
+ */
5
+
6
+ /**
7
+ * Base class for declarative, reusable email templates
8
+ */
9
+ class Mailable {
10
+ constructor() {
11
+ this.message = {
12
+ to: [],
13
+ from: null,
14
+ replyTo: null,
15
+ cc: [],
16
+ bcc: [],
17
+ subject: '',
18
+ template: null,
19
+ html: null,
20
+ text: null,
21
+ mjml: null,
22
+ data: {},
23
+ attachments: [],
24
+ headers: {},
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Add recipient(s)
30
+ * @param {string|string[]|Object} recipient
31
+ * @param {string} [name]
32
+ * @returns {this}
33
+ */
34
+ to(recipient, name) {
35
+ if (Array.isArray(recipient)) {
36
+ this.message.to.push(...recipient);
37
+ } else if (name && typeof recipient === 'string') {
38
+ this.message.to.push({ address: recipient, name });
39
+ } else if (recipient) {
40
+ this.message.to.push(recipient);
41
+ }
42
+ return this;
43
+ }
44
+
45
+ /**
46
+ * Set sender address
47
+ * @param {string} address
48
+ * @param {string} [name]
49
+ * @returns {this}
50
+ */
51
+ from(address, name) {
52
+ this.message.from = name ? { address, name } : address;
53
+ return this;
54
+ }
55
+
56
+ /**
57
+ * Set reply-to address
58
+ * @param {string} address
59
+ * @returns {this}
60
+ */
61
+ replyTo(address) {
62
+ this.message.replyTo = address;
63
+ return this;
64
+ }
65
+
66
+ /**
67
+ * Add CC recipient(s)
68
+ * @param {string|string[]} recipient
69
+ * @returns {this}
70
+ */
71
+ cc(recipient) {
72
+ if (Array.isArray(recipient)) {
73
+ this.message.cc.push(...recipient);
74
+ } else if (recipient) {
75
+ this.message.cc.push(recipient);
76
+ }
77
+ return this;
78
+ }
79
+
80
+ /**
81
+ * Add BCC recipient(s)
82
+ * @param {string|string[]} recipient
83
+ * @returns {this}
84
+ */
85
+ bcc(recipient) {
86
+ if (Array.isArray(recipient)) {
87
+ this.message.bcc.push(...recipient);
88
+ } else if (recipient) {
89
+ this.message.bcc.push(recipient);
90
+ }
91
+ return this;
92
+ }
93
+
94
+ /**
95
+ * Set email subject line
96
+ * @param {string} subj
97
+ * @returns {this}
98
+ */
99
+ subject(subj) {
100
+ this.message.subject = subj;
101
+ return this;
102
+ }
103
+
104
+ /**
105
+ * Set template name / identifier
106
+ * @param {string} tplName
107
+ * @returns {this}
108
+ */
109
+ template(tplName) {
110
+ this.message.template = tplName;
111
+ return this;
112
+ }
113
+
114
+ /**
115
+ * Set raw HTML body
116
+ * @param {string} htmlContent
117
+ * @returns {this}
118
+ */
119
+ html(htmlContent) {
120
+ this.message.html = htmlContent;
121
+ return this;
122
+ }
123
+
124
+ /**
125
+ * Set plain text body
126
+ * @param {string} textContent
127
+ * @returns {this}
128
+ */
129
+ text(textContent) {
130
+ this.message.text = textContent;
131
+ return this;
132
+ }
133
+
134
+ /**
135
+ * Set MJML template string
136
+ * @param {string} mjmlContent
137
+ * @returns {this}
138
+ */
139
+ mjml(mjmlContent) {
140
+ this.message.mjml = mjmlContent;
141
+ return this;
142
+ }
143
+
144
+ /**
145
+ * Set template view and/or view data
146
+ * @param {string|Object} templateOrData
147
+ * @param {Object} [data]
148
+ * @returns {this}
149
+ */
150
+ view(templateOrData, data) {
151
+ if (typeof templateOrData === 'string') {
152
+ this.message.template = templateOrData;
153
+ if (data && typeof data === 'object') {
154
+ Object.assign(this.message.data, data);
155
+ }
156
+ } else if (templateOrData && typeof templateOrData === 'object') {
157
+ Object.assign(this.message.data, templateOrData);
158
+ }
159
+ return this;
160
+ }
161
+
162
+ /**
163
+ * Assign template data
164
+ * @param {string|Object} keyOrObj
165
+ * @param {*} [value]
166
+ * @returns {this}
167
+ */
168
+ data(keyOrObj, value) {
169
+ if (typeof keyOrObj === 'string') {
170
+ this.message.data[keyOrObj] = value;
171
+ } else if (keyOrObj && typeof keyOrObj === 'object') {
172
+ Object.assign(this.message.data, keyOrObj);
173
+ }
174
+ return this;
175
+ }
176
+
177
+ /**
178
+ * Add attachment
179
+ * @param {Object} attachment
180
+ * @returns {this}
181
+ */
182
+ attach(attachment) {
183
+ if (attachment) {
184
+ this.message.attachments.push(attachment);
185
+ }
186
+ return this;
187
+ }
188
+
189
+ /**
190
+ * Set custom header
191
+ * @param {string} key
192
+ * @param {string} value
193
+ * @returns {this}
194
+ */
195
+ header(key, value) {
196
+ this.message.headers[key] = value;
197
+ return this;
198
+ }
199
+
200
+ /**
201
+ * Subclass hook executed before sending
202
+ */
203
+ prepare() {}
204
+
205
+ /**
206
+ * Subclass hook executed to build email definition
207
+ */
208
+ build() {}
209
+
210
+ /**
211
+ * Convert to normalized message payload object
212
+ * @returns {Object}
213
+ */
214
+ toMessage() {
215
+ // Copy any class field declarations (e.g. `template = 'welcome'`, `subjectText = 'Hi'`)
216
+ if (!this.message.template && typeof this.template === 'string') {
217
+ this.message.template = this.template;
218
+ }
219
+ if (!this.message.subject && (this.subjectText || this._subject)) {
220
+ this.message.subject = this.subjectText || this._subject;
221
+ }
222
+ if (!this.message.from && this.fromAddress) {
223
+ this.message.from = this.fromAddress;
224
+ }
225
+
226
+ this.prepare();
227
+ this.build();
228
+
229
+ const msg = {
230
+ ...this.message,
231
+ data: { ...this.message.data },
232
+ attachments: [...this.message.attachments],
233
+ headers: { ...this.message.headers },
234
+ };
235
+
236
+ if (Array.isArray(msg.to)) {
237
+ if (msg.to.length === 1) {
238
+ msg.to = msg.to[0];
239
+ } else if (msg.to.length === 0) {
240
+ msg.to = null;
241
+ }
242
+ }
243
+
244
+ return msg;
245
+ }
246
+ }
247
+
248
+ module.exports = {
249
+ Mailable,
250
+ };
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Core Mailer Service (AdonisJS & Django-inspired)
3
+ * @module core/mail/mailer
4
+ */
5
+
6
+ const { Mailable } = require('./mailable');
7
+ const { resolveTransport, MemoryTransport } = require('./transports');
8
+
9
+ /**
10
+ * Mailer service for managing email dispatch, previewing, and outbox tracking
11
+ */
12
+ class Mailer {
13
+ /**
14
+ * @param {Object} [options={}]
15
+ * @param {Object} [options.transport] - Custom transport or options for resolveTransport
16
+ * @param {Object} [options.defaults] - Default from/replyTo
17
+ * @param {Function} [options.renderer] - Template renderer function (template, data) => Promise<{ html, text }>
18
+ * @param {Object} [options.events] - Optional event emitter / signal dispatcher
19
+ * @param {Function} [options.logger] - Optional logger
20
+ */
21
+ constructor(options = {}) {
22
+ this.options = options;
23
+ this.transport = resolveTransport(options);
24
+ this.defaults = options.defaults || {};
25
+ this.renderer = options.renderer || null;
26
+ this.events = options.events || null;
27
+ this.logger = options.logger || console;
28
+ }
29
+
30
+ /**
31
+ * Send an email using a Mailable instance or raw options object
32
+ * @param {Mailable|Object} mailableOrOptions
33
+ * @returns {Promise<Object>}
34
+ */
35
+ async send(mailableOrOptions) {
36
+ if (!mailableOrOptions) {
37
+ throw new Error('mailableOrOptions is required to send email');
38
+ }
39
+
40
+ const msg = mailableOrOptions instanceof Mailable
41
+ ? mailableOrOptions.toMessage()
42
+ : { ...mailableOrOptions };
43
+
44
+ if (!msg.to || (Array.isArray(msg.to) && msg.to.length === 0)) {
45
+ throw new Error('Email recipient "to" is required');
46
+ }
47
+ if (!msg.subject) {
48
+ throw new Error('Email "subject" is required');
49
+ }
50
+
51
+ // Default from
52
+ if (!msg.from && this.defaults.from) {
53
+ msg.from = this.defaults.from;
54
+ }
55
+ if (!msg.replyTo && this.defaults.replyTo) {
56
+ msg.replyTo = this.defaults.replyTo;
57
+ }
58
+
59
+ // Render template if template identifier provided and renderer configured
60
+ if (msg.template && this.renderer && typeof this.renderer === 'function') {
61
+ const rendered = await this.renderer(msg.template, msg.data || {});
62
+ if (rendered) {
63
+ msg.html = rendered.html || msg.html;
64
+ msg.text = rendered.text || msg.text;
65
+ }
66
+ }
67
+
68
+ // Fallback simple text from HTML if text not provided
69
+ if (!msg.text && typeof msg.html === 'string') {
70
+ msg.text = msg.html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
71
+ }
72
+
73
+ try {
74
+ const result = await this.transport.sendMail(msg);
75
+
76
+ if (this.events && typeof this.events.emit === 'function') {
77
+ this.events.emit('mail:sent', { message: msg, result });
78
+ }
79
+
80
+ return {
81
+ ok: true,
82
+ messageId: result.messageId,
83
+ response: result.response,
84
+ accepted: result.accepted,
85
+ rejected: result.rejected,
86
+ result,
87
+ };
88
+ } catch (err) {
89
+ if (this.events && typeof this.events.emit === 'function') {
90
+ this.events.emit('mail:failed', { message: msg, error: err });
91
+ }
92
+ throw err;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Preview a Mailable without sending
98
+ * @param {Mailable|Object} mailableOrOptions
99
+ * @returns {Promise<Object>}
100
+ */
101
+ async preview(mailableOrOptions) {
102
+ if (!mailableOrOptions) {
103
+ throw new Error('mailableOrOptions is required to preview email');
104
+ }
105
+
106
+ const msg = mailableOrOptions instanceof Mailable
107
+ ? mailableOrOptions.toMessage()
108
+ : { ...mailableOrOptions };
109
+
110
+ if (msg.template && this.renderer && typeof this.renderer === 'function') {
111
+ const rendered = await this.renderer(msg.template, msg.data || {});
112
+ if (rendered) {
113
+ msg.html = rendered.html || msg.html;
114
+ msg.text = rendered.text || msg.text;
115
+ }
116
+ }
117
+
118
+ return {
119
+ to: msg.to,
120
+ from: msg.from || this.defaults.from,
121
+ subject: msg.subject,
122
+ html: msg.html || '',
123
+ text: msg.text || '',
124
+ data: msg.data || {},
125
+ attachments: msg.attachments || [],
126
+ headers: msg.headers || {},
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Get messages from in-memory outbox (if MemoryTransport active)
132
+ * @returns {Array<Object>}
133
+ */
134
+ getOutbox() {
135
+ if (this.transport && typeof this.transport.getOutbox === 'function') {
136
+ return this.transport.getOutbox();
137
+ }
138
+ return [];
139
+ }
140
+
141
+ /**
142
+ * Clear in-memory outbox
143
+ */
144
+ clearOutbox() {
145
+ if (this.transport && typeof this.transport.clearOutbox === 'function') {
146
+ this.transport.clearOutbox();
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Get latest sent message from outbox
152
+ * @returns {Object|null}
153
+ */
154
+ lastMessage() {
155
+ if (this.transport && typeof this.transport.lastMessage === 'function') {
156
+ return this.transport.lastMessage();
157
+ }
158
+ return null;
159
+ }
160
+
161
+ /**
162
+ * Check connection/transport health
163
+ */
164
+ async verify() {
165
+ if (this.transport && typeof this.transport.verify === 'function') {
166
+ return this.transport.verify();
167
+ }
168
+ return true;
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Factory to create Mailer instance
174
+ * @param {Object} options
175
+ * @returns {Mailer}
176
+ */
177
+ function createMailer(options = {}) {
178
+ return new Mailer(options);
179
+ }
180
+
181
+ module.exports = {
182
+ Mailer,
183
+ createMailer,
184
+ };
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Mail Transports / Backends (Django Email Backends & AdonisJS-inspired)
3
+ * @module core/mail/transports
4
+ */
5
+
6
+ const crypto = require('crypto');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ let nodemailer;
11
+ try {
12
+ nodemailer = require('nodemailer');
13
+ } catch {
14
+ nodemailer = null;
15
+ }
16
+
17
+ /**
18
+ * In-Memory Transport for testing & local development dev inbox (Django MemoryEmailBackend)
19
+ */
20
+ class MemoryTransport {
21
+ constructor(options = {}) {
22
+ this.name = 'memory';
23
+ this.outbox = [];
24
+ this.options = options;
25
+ }
26
+
27
+ /**
28
+ * Send mail into in-memory outbox array
29
+ * @param {Object} mailOptions
30
+ * @returns {Promise<Object>}
31
+ */
32
+ async sendMail(mailOptions) {
33
+ const id = mailOptions.messageId || `msg_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
34
+ const sentEntry = {
35
+ id,
36
+ messageId: id,
37
+ ...mailOptions,
38
+ sentAt: new Date(),
39
+ };
40
+
41
+ this.outbox.push(sentEntry);
42
+
43
+ return {
44
+ messageId: id,
45
+ accepted: Array.isArray(mailOptions.to) ? mailOptions.to : [mailOptions.to],
46
+ rejected: [],
47
+ response: '250 OK: Message stored in MemoryTransport outbox',
48
+ };
49
+ }
50
+
51
+ getOutbox() {
52
+ return [...this.outbox];
53
+ }
54
+
55
+ clearOutbox() {
56
+ this.outbox = [];
57
+ }
58
+
59
+ lastMessage() {
60
+ return this.outbox.length > 0 ? this.outbox[this.outbox.length - 1] : null;
61
+ }
62
+
63
+ findById(id) {
64
+ return this.outbox.find((msg) => msg.id === id || msg.messageId === id) || null;
65
+ }
66
+
67
+ async verify() {
68
+ return true;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Console Transport for development inspection (Django ConsoleEmailBackend)
74
+ */
75
+ class ConsoleTransport {
76
+ constructor(options = {}) {
77
+ this.name = 'console';
78
+ this.logger = options.logger || console;
79
+ }
80
+
81
+ async sendMail(mailOptions) {
82
+ const id = `console_${Date.now()}_${crypto.randomBytes(3).toString('hex')}`;
83
+ const border = '='.repeat(60);
84
+
85
+ this.logger.log(`\n${border}`);
86
+ this.logger.log(`📧 [EMAIL SENT VIA CONSOLE TRANSPORT] (${id})`);
87
+ this.logger.log(`From: ${mailOptions.from || '(default)'}`);
88
+ this.logger.log(`To: ${Array.isArray(mailOptions.to) ? mailOptions.to.join(', ') : mailOptions.to}`);
89
+ if (mailOptions.cc) this.logger.log(`Cc: ${mailOptions.cc}`);
90
+ if (mailOptions.bcc) this.logger.log(`Bcc: ${mailOptions.bcc}`);
91
+ this.logger.log(`Subject: ${mailOptions.subject}`);
92
+ this.logger.log(`Date: ${new Date().toISOString()}`);
93
+ this.logger.log('-'.repeat(60));
94
+ if (mailOptions.text) {
95
+ this.logger.log('--- Plain Text ---');
96
+ this.logger.log(mailOptions.text);
97
+ }
98
+ if (mailOptions.html) {
99
+ this.logger.log('--- HTML Content ---');
100
+ this.logger.log(mailOptions.html.length > 300 ? `${mailOptions.html.slice(0, 300)}... (truncated)` : mailOptions.html);
101
+ }
102
+ this.logger.log(`${border}\n`);
103
+
104
+ return {
105
+ messageId: id,
106
+ accepted: Array.isArray(mailOptions.to) ? mailOptions.to : [mailOptions.to],
107
+ rejected: [],
108
+ response: '250 OK: Logged to console',
109
+ };
110
+ }
111
+
112
+ async verify() {
113
+ return true;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * File-based Transport for offline inspection (Django FileEmailBackend)
119
+ */
120
+ class FileTransport {
121
+ constructor(options = {}) {
122
+ this.name = 'file';
123
+ this.dir = options.dir || path.join(process.cwd(), '.cache', 'emails');
124
+ if (!fs.existsSync(this.dir)) {
125
+ try {
126
+ fs.mkdirSync(this.dir, { recursive: true });
127
+ } catch {
128
+ // ignore
129
+ }
130
+ }
131
+ }
132
+
133
+ async sendMail(mailOptions) {
134
+ const id = `mail_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
135
+ const filePath = path.join(this.dir, `${id}.json`);
136
+ const data = {
137
+ id,
138
+ ...mailOptions,
139
+ sentAt: new Date().toISOString(),
140
+ };
141
+
142
+ try {
143
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
144
+ } catch (err) {
145
+ console.warn('[FileTransport] Failed to write file:', err.message);
146
+ }
147
+
148
+ return {
149
+ messageId: id,
150
+ accepted: Array.isArray(mailOptions.to) ? mailOptions.to : [mailOptions.to],
151
+ rejected: [],
152
+ filePath,
153
+ response: `250 OK: Written to ${filePath}`,
154
+ };
155
+ }
156
+
157
+ async verify() {
158
+ return fs.existsSync(this.dir);
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Build SMTP config from env vars
164
+ * @returns {Object|null}
165
+ */
166
+ function smtpFromEnv() {
167
+ const host = process.env.SMTP_HOST;
168
+ if (!host) return null;
169
+
170
+ const port = parseInt(process.env.SMTP_PORT || '587', 10);
171
+ const secure = process.env.SMTP_SECURE === 'true' || port === 465;
172
+ const user = process.env.SMTP_USER;
173
+ const pass = process.env.SMTP_PASS;
174
+
175
+ const config = { host, port, secure };
176
+ if (user && pass) {
177
+ config.auth = { user, pass };
178
+ }
179
+ return config;
180
+ }
181
+
182
+ /**
183
+ * Factory to resolve or create appropriate email transport
184
+ * @param {Object} [options={}]
185
+ * @returns {Object} Transport instance with sendMail()
186
+ */
187
+ function resolveTransport(options = {}) {
188
+ // 1. Direct custom transport provided
189
+ if (options.transport) {
190
+ return options.transport;
191
+ }
192
+
193
+ const backend = options.backend || options.type;
194
+
195
+ // 2. Explicit transport types
196
+ if (backend === 'memory' || options.memory === true) {
197
+ return new MemoryTransport(options);
198
+ }
199
+
200
+ if (backend === 'console' || options.console === true) {
201
+ return new ConsoleTransport(options);
202
+ }
203
+
204
+ if (backend === 'file' || options.fileDir) {
205
+ return new FileTransport({ dir: options.fileDir || options.dir });
206
+ }
207
+
208
+ // 3. SMTP configuration
209
+ const smtp = options.smtp || smtpFromEnv();
210
+ if (smtp && nodemailer) {
211
+ return nodemailer.createTransport(smtp);
212
+ }
213
+
214
+ // 4. Fallback defaults by environment
215
+ const env = process.env.NODE_ENV || 'development';
216
+ if (env === 'test') {
217
+ return new MemoryTransport(options);
218
+ }
219
+
220
+ if (!smtp) {
221
+ // If running in development with no SMTP, gracefully use MemoryTransport
222
+ return new MemoryTransport(options);
223
+ }
224
+
225
+ if (!nodemailer) {
226
+ throw new Error('nodemailer is required for SMTP transport. Install it with: npm install nodemailer');
227
+ }
228
+
229
+ return nodemailer.createTransport(smtp);
230
+ }
231
+
232
+ module.exports = {
233
+ MemoryTransport,
234
+ ConsoleTransport,
235
+ FileTransport,
236
+ resolveTransport,
237
+ smtpFromEnv,
238
+ };