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.
- package/LICENSE +21 -0
- package/README.md +137 -0
- package/bin/millas.js +6 -0
- package/package.json +56 -0
- package/src/admin/Admin.js +617 -0
- package/src/admin/index.js +13 -0
- package/src/admin/resources/AdminResource.js +317 -0
- package/src/auth/Auth.js +254 -0
- package/src/auth/AuthController.js +188 -0
- package/src/auth/AuthMiddleware.js +67 -0
- package/src/auth/Hasher.js +51 -0
- package/src/auth/JwtDriver.js +74 -0
- package/src/auth/RoleMiddleware.js +44 -0
- package/src/cache/Cache.js +231 -0
- package/src/cache/drivers/FileDriver.js +152 -0
- package/src/cache/drivers/MemoryDriver.js +158 -0
- package/src/cache/drivers/NullDriver.js +27 -0
- package/src/cache/index.js +8 -0
- package/src/cli.js +27 -0
- package/src/commands/make.js +61 -0
- package/src/commands/migrate.js +174 -0
- package/src/commands/new.js +50 -0
- package/src/commands/queue.js +92 -0
- package/src/commands/route.js +93 -0
- package/src/commands/serve.js +50 -0
- package/src/container/Application.js +177 -0
- package/src/container/Container.js +281 -0
- package/src/container/index.js +13 -0
- package/src/controller/Controller.js +367 -0
- package/src/errors/HttpError.js +29 -0
- package/src/events/Event.js +39 -0
- package/src/events/EventEmitter.js +151 -0
- package/src/events/Listener.js +46 -0
- package/src/events/index.js +15 -0
- package/src/index.js +93 -0
- package/src/mail/Mail.js +210 -0
- package/src/mail/MailMessage.js +196 -0
- package/src/mail/TemplateEngine.js +150 -0
- package/src/mail/drivers/LogDriver.js +36 -0
- package/src/mail/drivers/MailgunDriver.js +84 -0
- package/src/mail/drivers/SendGridDriver.js +97 -0
- package/src/mail/drivers/SmtpDriver.js +67 -0
- package/src/mail/index.js +19 -0
- package/src/middleware/AuthMiddleware.js +46 -0
- package/src/middleware/CorsMiddleware.js +59 -0
- package/src/middleware/LogMiddleware.js +61 -0
- package/src/middleware/Middleware.js +36 -0
- package/src/middleware/MiddlewarePipeline.js +94 -0
- package/src/middleware/ThrottleMiddleware.js +61 -0
- package/src/orm/drivers/DatabaseManager.js +135 -0
- package/src/orm/fields/index.js +132 -0
- package/src/orm/index.js +19 -0
- package/src/orm/migration/MigrationRunner.js +216 -0
- package/src/orm/migration/ModelInspector.js +338 -0
- package/src/orm/migration/SchemaBuilder.js +173 -0
- package/src/orm/model/Model.js +371 -0
- package/src/orm/query/QueryBuilder.js +197 -0
- package/src/providers/AdminServiceProvider.js +40 -0
- package/src/providers/AuthServiceProvider.js +53 -0
- package/src/providers/CacheStorageServiceProvider.js +71 -0
- package/src/providers/DatabaseServiceProvider.js +45 -0
- package/src/providers/EventServiceProvider.js +34 -0
- package/src/providers/MailServiceProvider.js +51 -0
- package/src/providers/ProviderRegistry.js +82 -0
- package/src/providers/QueueServiceProvider.js +52 -0
- package/src/providers/ServiceProvider.js +45 -0
- package/src/queue/Job.js +135 -0
- package/src/queue/Queue.js +147 -0
- package/src/queue/drivers/DatabaseDriver.js +194 -0
- package/src/queue/drivers/SyncDriver.js +72 -0
- package/src/queue/index.js +16 -0
- package/src/queue/workers/QueueWorker.js +140 -0
- package/src/router/MiddlewareRegistry.js +82 -0
- package/src/router/Route.js +255 -0
- package/src/router/RouteGroup.js +19 -0
- package/src/router/RouteRegistry.js +55 -0
- package/src/router/Router.js +138 -0
- package/src/router/index.js +15 -0
- package/src/scaffold/generator.js +34 -0
- package/src/scaffold/maker.js +272 -0
- package/src/scaffold/templates.js +350 -0
- package/src/storage/Storage.js +170 -0
- package/src/storage/drivers/LocalDriver.js +215 -0
- 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
|
+
});
|
package/src/mail/Mail.js
ADDED
|
@@ -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(/&/g, '&')
|
|
140
|
+
.replace(/</g, '<')
|
|
141
|
+
.replace(/>/g, '>')
|
|
142
|
+
.replace(/"/g, '"')
|
|
143
|
+
.replace(/'/g, "'")
|
|
144
|
+
.replace(/ /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;
|