millas 0.1.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.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -0
  3. package/bin/millas.js +6 -0
  4. package/package.json +56 -0
  5. package/src/admin/Admin.js +617 -0
  6. package/src/admin/index.js +13 -0
  7. package/src/admin/resources/AdminResource.js +317 -0
  8. package/src/auth/Auth.js +254 -0
  9. package/src/auth/AuthController.js +188 -0
  10. package/src/auth/AuthMiddleware.js +67 -0
  11. package/src/auth/Hasher.js +51 -0
  12. package/src/auth/JwtDriver.js +74 -0
  13. package/src/auth/RoleMiddleware.js +44 -0
  14. package/src/cache/Cache.js +231 -0
  15. package/src/cache/drivers/FileDriver.js +152 -0
  16. package/src/cache/drivers/MemoryDriver.js +158 -0
  17. package/src/cache/drivers/NullDriver.js +27 -0
  18. package/src/cache/index.js +8 -0
  19. package/src/cli.js +27 -0
  20. package/src/commands/make.js +61 -0
  21. package/src/commands/migrate.js +174 -0
  22. package/src/commands/new.js +50 -0
  23. package/src/commands/queue.js +92 -0
  24. package/src/commands/route.js +93 -0
  25. package/src/commands/serve.js +50 -0
  26. package/src/container/Application.js +177 -0
  27. package/src/container/Container.js +281 -0
  28. package/src/container/index.js +13 -0
  29. package/src/controller/Controller.js +367 -0
  30. package/src/errors/HttpError.js +29 -0
  31. package/src/events/Event.js +39 -0
  32. package/src/events/EventEmitter.js +151 -0
  33. package/src/events/Listener.js +46 -0
  34. package/src/events/index.js +15 -0
  35. package/src/index.js +93 -0
  36. package/src/mail/Mail.js +210 -0
  37. package/src/mail/MailMessage.js +196 -0
  38. package/src/mail/TemplateEngine.js +150 -0
  39. package/src/mail/drivers/LogDriver.js +36 -0
  40. package/src/mail/drivers/MailgunDriver.js +84 -0
  41. package/src/mail/drivers/SendGridDriver.js +97 -0
  42. package/src/mail/drivers/SmtpDriver.js +67 -0
  43. package/src/mail/index.js +19 -0
  44. package/src/middleware/AuthMiddleware.js +46 -0
  45. package/src/middleware/CorsMiddleware.js +59 -0
  46. package/src/middleware/LogMiddleware.js +61 -0
  47. package/src/middleware/Middleware.js +36 -0
  48. package/src/middleware/MiddlewarePipeline.js +94 -0
  49. package/src/middleware/ThrottleMiddleware.js +61 -0
  50. package/src/orm/drivers/DatabaseManager.js +135 -0
  51. package/src/orm/fields/index.js +132 -0
  52. package/src/orm/index.js +19 -0
  53. package/src/orm/migration/MigrationRunner.js +216 -0
  54. package/src/orm/migration/ModelInspector.js +338 -0
  55. package/src/orm/migration/SchemaBuilder.js +173 -0
  56. package/src/orm/model/Model.js +371 -0
  57. package/src/orm/query/QueryBuilder.js +197 -0
  58. package/src/providers/AdminServiceProvider.js +40 -0
  59. package/src/providers/AuthServiceProvider.js +53 -0
  60. package/src/providers/CacheStorageServiceProvider.js +71 -0
  61. package/src/providers/DatabaseServiceProvider.js +45 -0
  62. package/src/providers/EventServiceProvider.js +34 -0
  63. package/src/providers/MailServiceProvider.js +51 -0
  64. package/src/providers/ProviderRegistry.js +82 -0
  65. package/src/providers/QueueServiceProvider.js +52 -0
  66. package/src/providers/ServiceProvider.js +45 -0
  67. package/src/queue/Job.js +135 -0
  68. package/src/queue/Queue.js +147 -0
  69. package/src/queue/drivers/DatabaseDriver.js +194 -0
  70. package/src/queue/drivers/SyncDriver.js +72 -0
  71. package/src/queue/index.js +16 -0
  72. package/src/queue/workers/QueueWorker.js +140 -0
  73. package/src/router/MiddlewareRegistry.js +82 -0
  74. package/src/router/Route.js +255 -0
  75. package/src/router/RouteGroup.js +19 -0
  76. package/src/router/RouteRegistry.js +55 -0
  77. package/src/router/Router.js +138 -0
  78. package/src/router/index.js +15 -0
  79. package/src/scaffold/generator.js +34 -0
  80. package/src/scaffold/maker.js +272 -0
  81. package/src/scaffold/templates.js +350 -0
  82. package/src/storage/Storage.js +170 -0
  83. package/src/storage/drivers/LocalDriver.js +215 -0
  84. package/src/storage/index.js +6 -0
package/src/index.js ADDED
@@ -0,0 +1,93 @@
1
+ 'use strict';
2
+
3
+ // ── HTTP Layer ────────────────────────────────────────────────────
4
+ const Controller = require('./controller/Controller');
5
+ const Middleware = require('./middleware/Middleware');
6
+ const MiddlewarePipeline = require('./middleware/MiddlewarePipeline');
7
+ const CorsMiddleware = require('./middleware/CorsMiddleware');
8
+ const ThrottleMiddleware = require('./middleware/ThrottleMiddleware');
9
+ const LogMiddleware = require('./middleware/LogMiddleware');
10
+ const HttpError = require('./errors/HttpError');
11
+
12
+ // ── DI Container ─────────────────────────────────────────────────
13
+ const Container = require('./container/Container');
14
+ const Application = require('./container/Application');
15
+ const ServiceProvider = require('./providers/ServiceProvider');
16
+ const ProviderRegistry = require('./providers/ProviderRegistry');
17
+
18
+ // ── ORM ───────────────────────────────────────────────────────────
19
+ const { Model, fields, QueryBuilder, DatabaseManager,
20
+ SchemaBuilder, MigrationRunner, ModelInspector } = require('./orm');
21
+ const DatabaseServiceProvider = require('./providers/DatabaseServiceProvider');
22
+
23
+ // ── Auth ──────────────────────────────────────────────────────────
24
+ const Auth = require('./auth/Auth');
25
+ const Hasher = require('./auth/Hasher');
26
+ const JwtDriver = require('./auth/JwtDriver');
27
+ const AuthMiddleware = require('./auth/AuthMiddleware');
28
+ const RoleMiddleware = require('./auth/RoleMiddleware');
29
+ const AuthController = require('./auth/AuthController');
30
+ const AuthServiceProvider = require('./providers/AuthServiceProvider');
31
+
32
+ // ── Mail ──────────────────────────────────────────────────────────
33
+ const { Mail, MailMessage, TemplateEngine,
34
+ SmtpDriver, SendGridDriver, MailgunDriver, LogDriver } = require('./mail');
35
+ const MailServiceProvider = require('./providers/MailServiceProvider');
36
+
37
+ // ── Queue ─────────────────────────────────────────────────────────
38
+ const Queue = require('./queue/Queue');
39
+ const Job = require('./queue/Job');
40
+ const QueueWorker = require('./queue/workers/QueueWorker');
41
+ const { dispatch } = require('./queue/Queue');
42
+ const QueueServiceProvider = require('./providers/QueueServiceProvider');
43
+
44
+ // ── Events ────────────────────────────────────────────────────────
45
+ const EventEmitter = require('./events/EventEmitter');
46
+ const Event = require('./events/Event');
47
+ const Listener = require('./events/Listener');
48
+ const { emit } = require('./events/EventEmitter');
49
+ const EventServiceProvider = require('./providers/EventServiceProvider');
50
+
51
+ // ── Cache ─────────────────────────────────────────────────────────
52
+ const Cache = require('./cache/Cache');
53
+ const MemoryDriver = require('./cache/drivers/MemoryDriver');
54
+ const FileDriver = require('./cache/drivers/FileDriver');
55
+ const NullDriver = require('./cache/drivers/NullDriver');
56
+ const { CacheServiceProvider, StorageServiceProvider } = require('./providers/CacheStorageServiceProvider');
57
+
58
+ // ── Storage ───────────────────────────────────────────────────────
59
+ const Storage = require('./storage/Storage');
60
+ const LocalDriver = require('./storage/drivers/LocalDriver');
61
+
62
+ module.exports = {
63
+ // HTTP
64
+ Controller, Middleware, MiddlewarePipeline,
65
+ CorsMiddleware, ThrottleMiddleware, LogMiddleware, HttpError,
66
+ // DI
67
+ Container, Application, ServiceProvider, ProviderRegistry,
68
+ // ORM
69
+ Model, fields, QueryBuilder, DatabaseManager, SchemaBuilder,
70
+ MigrationRunner, ModelInspector, DatabaseServiceProvider,
71
+ // Auth
72
+ Auth, Hasher, JwtDriver, AuthMiddleware, RoleMiddleware,
73
+ AuthController, AuthServiceProvider,
74
+ // Mail
75
+ Mail, MailMessage, TemplateEngine,
76
+ SmtpDriver, SendGridDriver, MailgunDriver, LogDriver, MailServiceProvider,
77
+ // Queue
78
+ Queue, Job, QueueWorker, dispatch, QueueServiceProvider,
79
+ // Events
80
+ EventEmitter, Event, Listener, emit, EventServiceProvider,
81
+ // Cache
82
+ Cache, MemoryDriver, FileDriver, NullDriver, CacheServiceProvider,
83
+ // Storage
84
+ Storage, LocalDriver, StorageServiceProvider,
85
+ };
86
+
87
+ // ── Admin ─────────────────────────────────────────────────────────
88
+ const { Admin, AdminResource, AdminField, AdminFilter } = require('./admin');
89
+ const AdminServiceProvider = require('./providers/AdminServiceProvider');
90
+
91
+ Object.assign(module.exports, {
92
+ Admin, AdminResource, AdminField, AdminFilter, AdminServiceProvider,
93
+ });
@@ -0,0 +1,210 @@
1
+ 'use strict';
2
+
3
+ const MailMessage = require('./MailMessage');
4
+ const TemplateEngine = require('./TemplateEngine');
5
+
6
+ /**
7
+ * Mail
8
+ *
9
+ * The primary mail facade.
10
+ *
11
+ * Usage:
12
+ * const { Mail } = require('millas/src');
13
+ *
14
+ * // Simple HTML
15
+ * await Mail.send(
16
+ * new MailMessage()
17
+ * .to('alice@example.com', 'Alice')
18
+ * .subject('Welcome!')
19
+ * .html('<h1>Hello Alice!</h1>')
20
+ * );
21
+ *
22
+ * // Shorthand — object instead of MailMessage
23
+ * await Mail.send({
24
+ * to: 'alice@example.com',
25
+ * subject: 'Welcome!',
26
+ * template: 'welcome',
27
+ * data: { name: 'Alice' },
28
+ * });
29
+ *
30
+ * // Raw send with builder callback
31
+ * await Mail.to('alice@example.com')
32
+ * .subject('Hi!')
33
+ * .html('<p>Hi!</p>')
34
+ * .send();
35
+ */
36
+ class Mail {
37
+ constructor() {
38
+ this._driver = null;
39
+ this._config = null;
40
+ this._engine = null;
41
+ this._queue = null; // set by QueueServiceProvider in Phase 9
42
+ }
43
+
44
+ // ─── Configuration ─────────────────────────────────────────────────────────
45
+
46
+ /**
47
+ * Configure Mail. Called by MailServiceProvider.
48
+ */
49
+ configure(config) {
50
+ this._config = config;
51
+ this._engine = new TemplateEngine(config.templatesPath);
52
+ }
53
+
54
+ /**
55
+ * Set the queue instance for async mail (Phase 9).
56
+ */
57
+ setQueue(queue) {
58
+ this._queue = queue;
59
+ }
60
+
61
+ // ─── Primary API ───────────────────────────────────────────────────────────
62
+
63
+ /**
64
+ * Send an email immediately.
65
+ *
66
+ * @param {MailMessage|object} message
67
+ */
68
+ async send(message) {
69
+ const msg = await this._resolve(message);
70
+ const driver = this._getDriver();
71
+ return driver.send(msg);
72
+ }
73
+
74
+ /**
75
+ * Queue an email for background delivery (requires Phase 9).
76
+ * Falls back to immediate send if no queue configured.
77
+ *
78
+ * @param {MailMessage|object} message
79
+ */
80
+ async queue(message) {
81
+ if (!this._queue) {
82
+ // Graceful degradation — send immediately
83
+ return this.send(message);
84
+ }
85
+ const msg = await this._resolve(message);
86
+ return this._queue.push('mail', msg);
87
+ }
88
+
89
+ /**
90
+ * Send later (alias for queue).
91
+ */
92
+ async later(message) {
93
+ return this.queue(message);
94
+ }
95
+
96
+ /**
97
+ * Send to multiple recipients in a loop.
98
+ *
99
+ * await Mail.sendBulk([
100
+ * { to: 'a@b.com', subject: 'Hi', template: 'welcome', data: { name: 'A' } },
101
+ * { to: 'c@d.com', subject: 'Hi', template: 'welcome', data: { name: 'C' } },
102
+ * ]);
103
+ */
104
+ async sendBulk(messages) {
105
+ return Promise.all(messages.map(m => this.send(m)));
106
+ }
107
+
108
+ // ─── Fluent builder entry point ────────────────────────────────────────────
109
+
110
+ /**
111
+ * Start building a message fluently.
112
+ * Returns a MailMessage with a .send() shortcut bound to this Mail instance.
113
+ *
114
+ * await Mail.to('alice@test.com').subject('Hi').html('<p>Hi</p>').send();
115
+ */
116
+ to(address, name) {
117
+ const mail = this;
118
+ const msg = new MailMessage().to(address, name);
119
+ // Attach a .send() shortcut
120
+ msg.send = () => mail.send(msg);
121
+ return msg;
122
+ }
123
+
124
+ // ─── Internal ──────────────────────────────────────────────────────────────
125
+
126
+ /**
127
+ * Resolve a MailMessage or plain object into a built message payload.
128
+ */
129
+ async _resolve(message) {
130
+ // Plain object shorthand
131
+ if (!(message instanceof MailMessage)) {
132
+ const msg = new MailMessage();
133
+ if (message.to) msg.to(message.to, message.toName);
134
+ if (message.cc) msg.cc(message.cc);
135
+ if (message.bcc) msg.bcc(message.bcc);
136
+ if (message.from) msg.from(message.from);
137
+ if (message.replyTo) msg.replyTo(message.replyTo);
138
+ if (message.subject) msg.subject(message.subject);
139
+ if (message.html) msg.html(message.html);
140
+ if (message.text) msg.text(message.text);
141
+ if (message.template) msg.template(message.template, message.data || {});
142
+ message = msg;
143
+ }
144
+
145
+ const defaults = {
146
+ from: this._config?.from
147
+ ? `${this._config.from.name || 'Millas'} <${this._config.from.address}>`
148
+ : 'noreply@millas.dev',
149
+ };
150
+
151
+ const built = message.build(defaults);
152
+
153
+ // Render template if specified
154
+ if (built._template && this._engine) {
155
+ const rendered = await this._engine.render(built._template, built._data || {});
156
+ built.html = built.html || rendered.html;
157
+ built.text = built.text || rendered.text;
158
+ }
159
+
160
+ // Clean up internal properties
161
+ delete built._template;
162
+ delete built._data;
163
+
164
+ return built;
165
+ }
166
+
167
+ _getDriver() {
168
+ if (this._driver) return this._driver;
169
+
170
+ if (!this._config) {
171
+ // No config — use LogDriver so mail never throws in tests/dev
172
+ const LogDriver = require('./drivers/LogDriver');
173
+ return new LogDriver();
174
+ }
175
+
176
+ const driverName = this._config.default || 'log';
177
+ const driverConf = this._config.drivers?.[driverName] || {};
178
+
179
+ switch (driverName) {
180
+ case 'smtp': {
181
+ const SmtpDriver = require('./drivers/SmtpDriver');
182
+ this._driver = new SmtpDriver(driverConf);
183
+ break;
184
+ }
185
+ case 'sendgrid': {
186
+ const SendGridDriver = require('./drivers/SendGridDriver');
187
+ this._driver = new SendGridDriver(driverConf);
188
+ break;
189
+ }
190
+ case 'mailgun': {
191
+ const MailgunDriver = require('./drivers/MailgunDriver');
192
+ this._driver = new MailgunDriver(driverConf);
193
+ break;
194
+ }
195
+ case 'log':
196
+ default: {
197
+ const LogDriver = require('./drivers/LogDriver');
198
+ this._driver = new LogDriver(driverConf);
199
+ break;
200
+ }
201
+ }
202
+
203
+ return this._driver;
204
+ }
205
+ }
206
+
207
+ // Singleton
208
+ module.exports = new Mail();
209
+ module.exports.Mail = Mail;
210
+ module.exports.MailMessage = MailMessage;
@@ -0,0 +1,196 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * MailMessage
5
+ *
6
+ * Fluent builder for composing email messages.
7
+ *
8
+ * Usage:
9
+ * const msg = new MailMessage()
10
+ * .to('alice@example.com', 'Alice')
11
+ * .subject('Welcome!')
12
+ * .html('<h1>Hello Alice</h1>')
13
+ * .text('Hello Alice');
14
+ *
15
+ * Or with a template:
16
+ * const msg = new MailMessage()
17
+ * .to('alice@example.com')
18
+ * .subject('Welcome!')
19
+ * .template('welcome', { name: 'Alice' });
20
+ */
21
+ class MailMessage {
22
+ constructor() {
23
+ this._to = [];
24
+ this._cc = [];
25
+ this._bcc = [];
26
+ this._from = null;
27
+ this._replyTo = null;
28
+ this._subject = '';
29
+ this._html = null;
30
+ this._text = null;
31
+ this._template = null;
32
+ this._data = {};
33
+ this._attachments = [];
34
+ this._headers = {};
35
+ this._priority = 'normal';
36
+ }
37
+
38
+ // ─── Recipients ────────────────────────────────────────────────────────────
39
+
40
+ /**
41
+ * Set the To recipient(s).
42
+ * @param {string|Array} address
43
+ * @param {string} name
44
+ */
45
+ to(address, name) {
46
+ this._to = this._normalise(address, name);
47
+ return this;
48
+ }
49
+
50
+ /**
51
+ * Add CC recipient(s).
52
+ */
53
+ cc(address, name) {
54
+ this._cc = [...this._cc, ...this._normalise(address, name)];
55
+ return this;
56
+ }
57
+
58
+ /**
59
+ * Add BCC recipient(s).
60
+ */
61
+ bcc(address, name) {
62
+ this._bcc = [...this._bcc, ...this._normalise(address, name)];
63
+ return this;
64
+ }
65
+
66
+ /**
67
+ * Override the From address for this message.
68
+ */
69
+ from(address, name) {
70
+ this._from = name ? `${name} <${address}>` : address;
71
+ return this;
72
+ }
73
+
74
+ /**
75
+ * Set the Reply-To address.
76
+ */
77
+ replyTo(address, name) {
78
+ this._replyTo = name ? `${name} <${address}>` : address;
79
+ return this;
80
+ }
81
+
82
+ // ─── Content ───────────────────────────────────────────────────────────────
83
+
84
+ /**
85
+ * Set the email subject line.
86
+ */
87
+ subject(subject) {
88
+ this._subject = subject;
89
+ return this;
90
+ }
91
+
92
+ /**
93
+ * Set HTML body.
94
+ */
95
+ html(content) {
96
+ this._html = content;
97
+ return this;
98
+ }
99
+
100
+ /**
101
+ * Set plain-text body.
102
+ */
103
+ text(content) {
104
+ this._text = content;
105
+ return this;
106
+ }
107
+
108
+ /**
109
+ * Use a named template with data.
110
+ * Template is resolved by the TemplateEngine.
111
+ *
112
+ * .template('welcome', { name: 'Alice', verifyUrl: '...' })
113
+ */
114
+ template(name, data = {}) {
115
+ this._template = name;
116
+ this._data = data;
117
+ return this;
118
+ }
119
+
120
+ // ─── Attachments ───────────────────────────────────────────────────────────
121
+
122
+ /**
123
+ * Attach a file.
124
+ * @param {string} filePath — absolute path or Buffer
125
+ * @param {string} filename — name shown in email
126
+ * @param {string} contentType
127
+ */
128
+ attach(filePath, filename, contentType) {
129
+ this._attachments.push({ path: filePath, filename, contentType });
130
+ return this;
131
+ }
132
+
133
+ /**
134
+ * Attach raw content (string or Buffer).
135
+ */
136
+ attachRaw(content, filename, contentType = 'text/plain') {
137
+ this._attachments.push({ content, filename, contentType });
138
+ return this;
139
+ }
140
+
141
+ // ─── Meta ──────────────────────────────────────────────────────────────────
142
+
143
+ /**
144
+ * Set a custom header.
145
+ */
146
+ header(key, value) {
147
+ this._headers[key] = value;
148
+ return this;
149
+ }
150
+
151
+ /**
152
+ * Set message priority: 'high' | 'normal' | 'low'
153
+ */
154
+ priority(level) {
155
+ this._priority = level;
156
+ return this;
157
+ }
158
+
159
+ // ─── Build ─────────────────────────────────────────────────────────────────
160
+
161
+ /**
162
+ * Build the final message object for the transport driver.
163
+ * Applies defaults from a config object.
164
+ */
165
+ build(defaults = {}) {
166
+ return {
167
+ from: this._from || defaults.from || 'noreply@millas.dev',
168
+ to: this._to,
169
+ cc: this._cc.length ? this._cc : undefined,
170
+ bcc: this._bcc.length ? this._bcc : undefined,
171
+ replyTo: this._replyTo || undefined,
172
+ subject: this._subject,
173
+ html: this._html || undefined,
174
+ text: this._text || undefined,
175
+ attachments: this._attachments.length ? this._attachments : undefined,
176
+ headers: Object.keys(this._headers).length ? this._headers : undefined,
177
+ priority: this._priority !== 'normal' ? this._priority : undefined,
178
+ // Internal use
179
+ _template: this._template,
180
+ _data: this._data,
181
+ };
182
+ }
183
+
184
+ // ─── Internal ──────────────────────────────────────────────────────────────
185
+
186
+ _normalise(address, name) {
187
+ if (Array.isArray(address)) {
188
+ return address.map(a =>
189
+ typeof a === 'string' ? a : (a.name ? `${a.name} <${a.address}>` : a.address)
190
+ );
191
+ }
192
+ return [name ? `${name} <${address}>` : address];
193
+ }
194
+ }
195
+
196
+ module.exports = MailMessage;
@@ -0,0 +1,150 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs-extra');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * TemplateEngine
8
+ *
9
+ * Renders email templates from the resources/mail/ directory.
10
+ *
11
+ * Supports:
12
+ * - Plain HTML files (.html)
13
+ * - Simple template syntax: {{ variable }}, {{ if condition }}, {{ each items }}
14
+ *
15
+ * Usage:
16
+ * const engine = new TemplateEngine('./resources/mail');
17
+ * const html = await engine.render('welcome', { name: 'Alice' });
18
+ *
19
+ * Template file: resources/mail/welcome.html
20
+ * <h1>Welcome, {{ name }}!</h1>
21
+ */
22
+ class TemplateEngine {
23
+ constructor(templatesPath) {
24
+ this._path = templatesPath || path.join(process.cwd(), 'resources/mail');
25
+ }
26
+
27
+ /**
28
+ * Render a named template with data.
29
+ * @param {string} name — template name without extension
30
+ * @param {object} data — variables to interpolate
31
+ * @returns {Promise<{html: string, text: string}>}
32
+ */
33
+ async render(name, data = {}) {
34
+ const htmlPath = path.join(this._path, `${name}.html`);
35
+ const textPath = path.join(this._path, `${name}.txt`);
36
+
37
+ let html = null;
38
+ let text = null;
39
+
40
+ if (await fs.pathExists(htmlPath)) {
41
+ const raw = await fs.readFile(htmlPath, 'utf8');
42
+ html = this._interpolate(raw, data);
43
+ }
44
+
45
+ if (await fs.pathExists(textPath)) {
46
+ const raw = await fs.readFile(textPath, 'utf8');
47
+ text = this._interpolate(raw, data);
48
+ }
49
+
50
+ // Auto-generate plain text from HTML if no .txt file
51
+ if (html && !text) {
52
+ text = this._htmlToText(html);
53
+ }
54
+
55
+ if (!html && !text) {
56
+ throw new Error(
57
+ `Mail template "${name}" not found in ${this._path}. ` +
58
+ `Create ${name}.html or ${name}.txt.`
59
+ );
60
+ }
61
+
62
+ return { html, text };
63
+ }
64
+
65
+ /**
66
+ * Render an inline HTML string with data.
67
+ */
68
+ renderInline(htmlString, data = {}) {
69
+ return {
70
+ html: this._interpolate(htmlString, data),
71
+ text: this._htmlToText(this._interpolate(htmlString, data)),
72
+ };
73
+ }
74
+
75
+ // ─── Template syntax ───────────────────────────────────────────────────────
76
+
77
+ /**
78
+ * Simple template interpolation.
79
+ *
80
+ * Supports:
81
+ * {{ name }} — variable substitution
82
+ * {{ user.email }} — dot notation
83
+ * {{# if condition }}...{{/ if }} — conditionals
84
+ * {{# each items }}...{{/ each }} — loops (item available as {{ this }})
85
+ */
86
+ _interpolate(template, data) {
87
+ let result = template;
88
+
89
+ // {{# each array }}...{{/ each }}
90
+ result = result.replace(
91
+ /\{\{#\s*each\s+(\w+)\s*\}\}([\s\S]*?)\{\{\/\s*each\s*\}\}/g,
92
+ (_, key, body) => {
93
+ const list = this._resolve(key, data);
94
+ if (!Array.isArray(list)) return '';
95
+ return list.map(item => {
96
+ return body
97
+ .replace(/\{\{\s*this\s*\}\}/g, String(item))
98
+ .replace(/\{\{\s*this\.([\w.]+)\s*\}\}/g, (__, prop) =>
99
+ String(this._resolve(prop, item) ?? '')
100
+ );
101
+ }).join('');
102
+ }
103
+ );
104
+
105
+ // {{# if condition }}...{{/ if }}
106
+ result = result.replace(
107
+ /\{\{#\s*if\s+(\w+)\s*\}\}([\s\S]*?)\{\{\/\s*if\s*\}\}/g,
108
+ (_, key, body) => this._resolve(key, data) ? body : ''
109
+ );
110
+
111
+ // {{ variable }} and {{ dot.notation }}
112
+ result = result.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, key) => {
113
+ const val = this._resolve(key, data);
114
+ return val !== undefined && val !== null ? String(val) : '';
115
+ });
116
+
117
+ return result;
118
+ }
119
+
120
+ _resolve(key, data) {
121
+ return key.split('.').reduce((obj, k) =>
122
+ obj && typeof obj === 'object' ? obj[k] : undefined, data
123
+ );
124
+ }
125
+
126
+ /**
127
+ * Strip HTML tags to produce a basic plain-text version.
128
+ */
129
+ _htmlToText(html) {
130
+ return html
131
+ .replace(/<style[\s\S]*?<\/style>/gi, '')
132
+ .replace(/<script[\s\S]*?<\/script>/gi, '')
133
+ .replace(/<br\s*\/?>/gi, '\n')
134
+ .replace(/<\/p>/gi, '\n\n')
135
+ .replace(/<\/h[1-6]>/gi, '\n\n')
136
+ .replace(/<\/li>/gi, '\n')
137
+ .replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/gi, '$2 ($1)')
138
+ .replace(/<[^>]+>/g, '')
139
+ .replace(/&amp;/g, '&')
140
+ .replace(/&lt;/g, '<')
141
+ .replace(/&gt;/g, '>')
142
+ .replace(/&quot;/g, '"')
143
+ .replace(/&#039;/g, "'")
144
+ .replace(/&nbsp;/g, ' ')
145
+ .replace(/\n{3,}/g, '\n\n')
146
+ .trim();
147
+ }
148
+ }
149
+
150
+ module.exports = TemplateEngine;
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * LogDriver
5
+ *
6
+ * Development mail driver — logs messages to the console
7
+ * instead of actually sending them.
8
+ *
9
+ * Set MAIL_DRIVER=log in .env to activate.
10
+ * Also used as a fallback when no driver is configured.
11
+ */
12
+ class LogDriver {
13
+ constructor(config = {}) {
14
+ this._silent = config.silent || false;
15
+ }
16
+
17
+ async send(message) {
18
+ if (this._silent) return { logged: true, message };
19
+
20
+ const sep = '─'.repeat(60);
21
+ console.log(`\n 📧 Mail (LogDriver)\n ${sep}`);
22
+ console.log(` From: ${message.from}`);
23
+ console.log(` To: ${[].concat(message.to).join(', ')}`);
24
+ if (message.cc) console.log(` CC: ${[].concat(message.cc).join(', ')}`);
25
+ if (message.bcc) console.log(` BCC: ${[].concat(message.bcc).join(', ')}`);
26
+ console.log(` Subject: ${message.subject}`);
27
+ if (message.text) {
28
+ console.log(` \n ${message.text.slice(0, 200)}${message.text.length > 200 ? '...' : ''}`);
29
+ }
30
+ console.log(` ${sep}\n`);
31
+
32
+ return { logged: true, message };
33
+ }
34
+ }
35
+
36
+ module.exports = LogDriver;