@timshel_npm/maildev 3.1.1
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 +16 -0
- package/README.md +287 -0
- package/bin/maildev +13 -0
- package/dist/app/components/angular/angular.js +20282 -0
- package/dist/app/components/angular/angular.min.js +201 -0
- package/dist/app/components/angular/angular.min.js.map +8 -0
- package/dist/app/components/angular-cookies/angular-cookies.js +202 -0
- package/dist/app/components/angular-cookies/angular-cookies.min.js +8 -0
- package/dist/app/components/angular-cookies/angular-cookies.min.js.map +8 -0
- package/dist/app/components/angular-resource/angular-resource.js +546 -0
- package/dist/app/components/angular-resource/angular-resource.min.js +12 -0
- package/dist/app/components/angular-resource/angular-resource.min.js.map +8 -0
- package/dist/app/components/angular-route/angular-route.js +891 -0
- package/dist/app/components/angular-route/angular-route.min.js +14 -0
- package/dist/app/components/angular-route/angular-route.min.js.map +8 -0
- package/dist/app/components/angular-sanitize/angular-sanitize.js +615 -0
- package/dist/app/components/angular-sanitize/angular-sanitize.min.js +14 -0
- package/dist/app/components/angular-sanitize/angular-sanitize.min.js.map +8 -0
- package/dist/app/components/socket.io/socket.io.min.js +7 -0
- package/dist/app/favicon.ico +0 -0
- package/dist/app/index.html +176 -0
- package/dist/app/scripts/app.js +68 -0
- package/dist/app/scripts/components/address.js +15 -0
- package/dist/app/scripts/controllers/item.js +191 -0
- package/dist/app/scripts/controllers/main.js +259 -0
- package/dist/app/scripts/services.js +84 -0
- package/dist/app/styles/style.css +37 -0
- package/dist/app/views/address.html +3 -0
- package/dist/app/views/item.html +310 -0
- package/dist/app/views/main.html +18 -0
- package/dist/app/webfonts/fa-brands-400.eot +0 -0
- package/dist/app/webfonts/fa-brands-400.svg +3717 -0
- package/dist/app/webfonts/fa-brands-400.ttf +0 -0
- package/dist/app/webfonts/fa-brands-400.woff +0 -0
- package/dist/app/webfonts/fa-brands-400.woff2 +0 -0
- package/dist/app/webfonts/fa-regular-400.eot +0 -0
- package/dist/app/webfonts/fa-regular-400.svg +801 -0
- package/dist/app/webfonts/fa-regular-400.ttf +0 -0
- package/dist/app/webfonts/fa-regular-400.woff +0 -0
- package/dist/app/webfonts/fa-regular-400.woff2 +0 -0
- package/dist/app/webfonts/fa-solid-900.eot +0 -0
- package/dist/app/webfonts/fa-solid-900.svg +5028 -0
- package/dist/app/webfonts/fa-solid-900.ttf +0 -0
- package/dist/app/webfonts/fa-solid-900.woff +0 -0
- package/dist/app/webfonts/fa-solid-900.woff2 +0 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +50 -0
- package/dist/lib/auth.d.ts +4 -0
- package/dist/lib/auth.js +26 -0
- package/dist/lib/helpers/bcc.d.ts +5 -0
- package/dist/lib/helpers/bcc.js +16 -0
- package/dist/lib/helpers/smtp.d.ts +4 -0
- package/dist/lib/helpers/smtp.js +16 -0
- package/dist/lib/helpers/strtotime.d.ts +2 -0
- package/dist/lib/helpers/strtotime.js +1301 -0
- package/dist/lib/logger.d.ts +2 -0
- package/dist/lib/logger.js +30 -0
- package/dist/lib/mailbuffer.d.ts +21 -0
- package/dist/lib/mailbuffer.js +102 -0
- package/dist/lib/mailparser.d.ts +2 -0
- package/dist/lib/mailparser.js +253 -0
- package/dist/lib/mailserver.d.ts +115 -0
- package/dist/lib/mailserver.js +497 -0
- package/dist/lib/options.d.ts +3 -0
- package/dist/lib/options.js +150 -0
- package/dist/lib/outgoing.d.ts +40 -0
- package/dist/lib/outgoing.js +162 -0
- package/dist/lib/routes.d.ts +2 -0
- package/dist/lib/routes.js +139 -0
- package/dist/lib/type.d.ts +228 -0
- package/dist/lib/type.js +2 -0
- package/dist/lib/utils.d.ts +4 -0
- package/dist/lib/utils.js +65 -0
- package/dist/lib/web.d.ts +32 -0
- package/dist/lib/web.js +90 -0
- package/package.json +109 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); }
|
|
12
|
+
var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) {
|
|
13
|
+
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
14
|
+
var g = generator.apply(thisArg, _arguments || []), i, q = [];
|
|
15
|
+
return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;
|
|
16
|
+
function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }
|
|
17
|
+
function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }
|
|
18
|
+
function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
|
|
19
|
+
function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
|
|
20
|
+
function fulfill(value) { resume("next", value); }
|
|
21
|
+
function reject(value) { resume("throw", value); }
|
|
22
|
+
function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
|
|
23
|
+
};
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.MailServer = void 0;
|
|
26
|
+
const bcc_1 = require("./helpers/bcc");
|
|
27
|
+
const smtp_1 = require("./helpers/smtp");
|
|
28
|
+
const mailbuffer_1 = require("./mailbuffer");
|
|
29
|
+
const mailparser_1 = require("./mailparser");
|
|
30
|
+
const outgoing_1 = require("./outgoing");
|
|
31
|
+
const smtp_server_1 = require("smtp-server");
|
|
32
|
+
const fs_1 = require("fs");
|
|
33
|
+
const events = require("events");
|
|
34
|
+
const fs = require("fs");
|
|
35
|
+
const os = require("os");
|
|
36
|
+
const path = require("path");
|
|
37
|
+
const utils = require("./utils");
|
|
38
|
+
const logger = require("./logger");
|
|
39
|
+
const createDOMPurify = require("dompurify");
|
|
40
|
+
const { JSDOM } = require("jsdom");
|
|
41
|
+
const defaultPort = 1025;
|
|
42
|
+
const defaultHost = "0.0.0.0";
|
|
43
|
+
const reservedEventName = ["close", "delete"];
|
|
44
|
+
class MailServer {
|
|
45
|
+
next(subject) {
|
|
46
|
+
if (reservedEventName.includes(subject)) {
|
|
47
|
+
throw new Error(`Invalid subject ${subject}; ${reservedEventName} are reserved for internal usage`);
|
|
48
|
+
}
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
this.once(subject, resolve);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Use an internal array to store received email even if not consummed
|
|
55
|
+
* Use `.return()` to close it
|
|
56
|
+
**/
|
|
57
|
+
iterator(subject) {
|
|
58
|
+
if (reservedEventName.includes(subject)) {
|
|
59
|
+
throw new Error(`Invalid subject ${subject}; ${reservedEventName} are reserved for internal usage`);
|
|
60
|
+
}
|
|
61
|
+
let closed = false;
|
|
62
|
+
const next = [];
|
|
63
|
+
const self = this;
|
|
64
|
+
const closing = () => {
|
|
65
|
+
closed = true;
|
|
66
|
+
};
|
|
67
|
+
let nextCallback;
|
|
68
|
+
function buildNext() {
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
nextCallback = (mail) => {
|
|
71
|
+
next.push(buildNext());
|
|
72
|
+
resolve(mail);
|
|
73
|
+
};
|
|
74
|
+
self.once(subject, nextCallback);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
self.once("close", closing);
|
|
78
|
+
next.push(buildNext());
|
|
79
|
+
// We use an internal generator otherwise the setup phase was not always run
|
|
80
|
+
function inner(subject) {
|
|
81
|
+
return __asyncGenerator(this, arguments, function* inner_1() {
|
|
82
|
+
try {
|
|
83
|
+
do {
|
|
84
|
+
const email = (yield __await(next.shift()));
|
|
85
|
+
yield yield __await(email);
|
|
86
|
+
} while (!closed);
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
self.removeListener("close", closing);
|
|
90
|
+
if (nextCallback) {
|
|
91
|
+
self.removeListener(subject, nextCallback);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return inner(subject);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Return a struct which store received emails.
|
|
100
|
+
* Then allow to obtain a `Promise<Mail>` dependant on a predicate `(Mail) => boolean`.
|
|
101
|
+
* Allow to wait for `Mail` independant of their order of arrival.
|
|
102
|
+
*/
|
|
103
|
+
buffer(subject, defaultTimeout = 10000) {
|
|
104
|
+
return new mailbuffer_1.MailBuffer(this, subject, defaultTimeout);
|
|
105
|
+
}
|
|
106
|
+
constructor(options, mailEventSubjectMapper = (m) => { var _a; return (_a = m.to[0]) === null || _a === void 0 ? void 0 : _a.address; }) {
|
|
107
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
108
|
+
this.store = [];
|
|
109
|
+
this.eventEmitter = new events.EventEmitter();
|
|
110
|
+
/**
|
|
111
|
+
* Extend Event Emitter methods
|
|
112
|
+
* events:
|
|
113
|
+
* 'new' - emitted when new email has arrived
|
|
114
|
+
*/
|
|
115
|
+
this.emit = this.eventEmitter.emit.bind(this.eventEmitter);
|
|
116
|
+
this.on = this.eventEmitter.on.bind(this.eventEmitter);
|
|
117
|
+
this.off = this.eventEmitter.off.bind(this.eventEmitter);
|
|
118
|
+
this.once = this.eventEmitter.once.bind(this.eventEmitter);
|
|
119
|
+
this.prependListener = this.eventEmitter.once.bind(this.eventEmitter);
|
|
120
|
+
this.prependOnceListener = this.eventEmitter.once.bind(this.eventEmitter);
|
|
121
|
+
this.removeListener = this.eventEmitter.removeListener.bind(this.eventEmitter);
|
|
122
|
+
this.removeAllListeners = this.eventEmitter.removeAllListeners.bind(this.eventEmitter);
|
|
123
|
+
const defaultMailDir = path.join(os.tmpdir(), `maildev-${process.pid.toString()}`);
|
|
124
|
+
const secure = (_a = options === null || options === void 0 ? void 0 : options.isSecure) !== null && _a !== void 0 ? _a : false;
|
|
125
|
+
const smtpServerConfig = {
|
|
126
|
+
secure,
|
|
127
|
+
cert: (options === null || options === void 0 ? void 0 : options.ssl) ? fs.readFileSync((_b = options === null || options === void 0 ? void 0 : options.ssl) === null || _b === void 0 ? void 0 : _b.certPath) : null,
|
|
128
|
+
key: (options === null || options === void 0 ? void 0 : options.ssl) ? fs.readFileSync((_c = options === null || options === void 0 ? void 0 : options.ssl) === null || _c === void 0 ? void 0 : _c.keyPath) : null,
|
|
129
|
+
onAuth: (0, smtp_1.createOnAuthCallback)((_d = options === null || options === void 0 ? void 0 : options.auth) === null || _d === void 0 ? void 0 : _d.user, (_e = options === null || options === void 0 ? void 0 : options.auth) === null || _e === void 0 ? void 0 : _e.pass),
|
|
130
|
+
onData: (stream, session, callback) => handleDataStream(this, stream, session, callback),
|
|
131
|
+
logger: false,
|
|
132
|
+
hideSTARTTLS: true,
|
|
133
|
+
hidePIPELINING: (_f = options === null || options === void 0 ? void 0 : options.hidePIPELINING) !== null && _f !== void 0 ? _f : false,
|
|
134
|
+
hide8BITMIME: (_g = options === null || options === void 0 ? void 0 : options.hide8BITMIME) !== null && _g !== void 0 ? _g : false,
|
|
135
|
+
hideSMTPUTF8: (_h = options === null || options === void 0 ? void 0 : options.hideSMTPUTF8) !== null && _h !== void 0 ? _h : false,
|
|
136
|
+
disabledCommands: (options === null || options === void 0 ? void 0 : options.auth) ? (secure ? [] : ["STARTTLS"]) : ["AUTH"],
|
|
137
|
+
};
|
|
138
|
+
this.port = (_j = options === null || options === void 0 ? void 0 : options.port) !== null && _j !== void 0 ? _j : defaultPort;
|
|
139
|
+
this.host = (_k = options === null || options === void 0 ? void 0 : options.host) !== null && _k !== void 0 ? _k : defaultHost;
|
|
140
|
+
this.mailDir = (_l = options === null || options === void 0 ? void 0 : options.mailDir) !== null && _l !== void 0 ? _l : defaultMailDir;
|
|
141
|
+
this.mailEventSubjectMapper = mailEventSubjectMapper;
|
|
142
|
+
this.smtp = new smtp_server_1.SMTPServer(smtpServerConfig);
|
|
143
|
+
this.smtp.on("error", onSmtpError);
|
|
144
|
+
createMailDir(this.mailDir);
|
|
145
|
+
if (options === null || options === void 0 ? void 0 : options.outgoing) {
|
|
146
|
+
this.outgoing = new outgoing_1.Outgoing(options === null || options === void 0 ? void 0 : options.outgoing);
|
|
147
|
+
}
|
|
148
|
+
if (options === null || options === void 0 ? void 0 : options.mailDir) {
|
|
149
|
+
this.loadMailsFromDirectory();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Start the mailServer
|
|
154
|
+
*/
|
|
155
|
+
listen() {
|
|
156
|
+
const self = this;
|
|
157
|
+
return new Promise((resolve, reject) => {
|
|
158
|
+
self.smtp.listen(self.port, self.host, (err) => {
|
|
159
|
+
if (err) {
|
|
160
|
+
reject(err);
|
|
161
|
+
}
|
|
162
|
+
logger.info("MailDev SMTP Server running at %s:%s", self.host, self.port);
|
|
163
|
+
resolve();
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Stop the mailserver
|
|
169
|
+
*/
|
|
170
|
+
close() {
|
|
171
|
+
var _a;
|
|
172
|
+
this.emit("close");
|
|
173
|
+
(_a = this.outgoing) === null || _a === void 0 ? void 0 : _a.close();
|
|
174
|
+
return new Promise((resolve) => {
|
|
175
|
+
this.smtp.close(resolve);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
isOutgoingEnabled() {
|
|
179
|
+
return this.outgoing !== undefined;
|
|
180
|
+
}
|
|
181
|
+
getOutgoingHost() {
|
|
182
|
+
var _a;
|
|
183
|
+
return (_a = this.outgoing) === null || _a === void 0 ? void 0 : _a.getOutgoingHost();
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Set Auto Relay Mode, automatic send email to recipient
|
|
187
|
+
*/
|
|
188
|
+
setAutoRelayMode(enabled, emailAddress, rules) {
|
|
189
|
+
if (this.outgoing) {
|
|
190
|
+
this.outgoing.setAutoRelayMode(enabled, emailAddress, rules);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
throw new Error("Outgoing not configured");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
relayMail(mail, isAutoRelay = true) {
|
|
197
|
+
const self = this;
|
|
198
|
+
const outgoing = this.outgoing;
|
|
199
|
+
return outgoing
|
|
200
|
+
? new Promise((resolve, reject) => {
|
|
201
|
+
self
|
|
202
|
+
.getRawEmail(mail.id)
|
|
203
|
+
.then((rawEmailStream) => {
|
|
204
|
+
outgoing.relayMail(mail, rawEmailStream, isAutoRelay, (err) => {
|
|
205
|
+
if (err) {
|
|
206
|
+
reject(err);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
resolve();
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
})
|
|
213
|
+
.catch((err) => {
|
|
214
|
+
logger.error("Mail Stream Error: ", err);
|
|
215
|
+
reject(err);
|
|
216
|
+
});
|
|
217
|
+
})
|
|
218
|
+
: Promise.reject(new Error("Outgoing not configured"));
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Get an email by id
|
|
222
|
+
*/
|
|
223
|
+
getEmail(id) {
|
|
224
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
225
|
+
const envelope = this.store.find(function (elt) {
|
|
226
|
+
return elt.id === id;
|
|
227
|
+
});
|
|
228
|
+
if (envelope) {
|
|
229
|
+
return getDiskEmail(this.mailDir, envelope.id, envelope).then((mail) => {
|
|
230
|
+
if (mail.html) {
|
|
231
|
+
// sanitize html
|
|
232
|
+
const window = new JSDOM("").window;
|
|
233
|
+
const DOMPurify = createDOMPurify(window);
|
|
234
|
+
mail.html = DOMPurify.sanitize(mail.html, {
|
|
235
|
+
WHOLE_DOCUMENT: true, // preserve html,head,body elements
|
|
236
|
+
SANITIZE_DOM: false, // ignore DOM cloberring to preserve form id/name attributes
|
|
237
|
+
ADD_TAGS: ["link"], // allow link element to preserve external style sheets
|
|
238
|
+
ADD_ATTR: ["target"], // Preserve explicit target attributes on links
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return mail;
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
throw new Error(`No email with id: ${id}`);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Returns a readable stream of the raw email
|
|
251
|
+
*/
|
|
252
|
+
getRawEmail(id) {
|
|
253
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
254
|
+
const emlPath = path.join(this.mailDir, id + ".eml");
|
|
255
|
+
if (fs.existsSync(emlPath)) {
|
|
256
|
+
return fs.createReadStream(emlPath);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
throw new Error(`No email with id: ${id}`);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Returns the html of a given email
|
|
265
|
+
*/
|
|
266
|
+
getEmailHTML(id_1) {
|
|
267
|
+
return __awaiter(this, arguments, void 0, function* (id, baseUrl = "") {
|
|
268
|
+
baseUrl = baseUrl ? "//" + baseUrl : "";
|
|
269
|
+
const mail = yield this.getEmail(id);
|
|
270
|
+
if (typeof mail.html === "string") {
|
|
271
|
+
var html = mail.html;
|
|
272
|
+
const getFileUrl = function (filename) {
|
|
273
|
+
return baseUrl + "/email/" + id + "/attachment/" + encodeURIComponent(filename);
|
|
274
|
+
};
|
|
275
|
+
for (const attachment of mail.attachments) {
|
|
276
|
+
if (attachment.contentId) {
|
|
277
|
+
const regex = new RegExp("src=(\"|')cid:" + attachment.contentId + "(\"|')", "g");
|
|
278
|
+
const replacement = 'src="' + getFileUrl(attachment.generatedFileName) + '"';
|
|
279
|
+
html = html.replace(regex, replacement);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return html;
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
throw new Error(`No html in email ${id}`);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Set all emails to read
|
|
291
|
+
*/
|
|
292
|
+
readAllEmail() {
|
|
293
|
+
return this.store.reduce(function (count, elt) {
|
|
294
|
+
if (!elt.isRead) {
|
|
295
|
+
count++;
|
|
296
|
+
}
|
|
297
|
+
return count;
|
|
298
|
+
}, 0);
|
|
299
|
+
}
|
|
300
|
+
getAllEnvelope() {
|
|
301
|
+
return this.store.slice();
|
|
302
|
+
}
|
|
303
|
+
getAllEmail() {
|
|
304
|
+
const emails = this.store.map((elt) => {
|
|
305
|
+
return this.getEmail(elt.id);
|
|
306
|
+
});
|
|
307
|
+
return Promise.all(emails);
|
|
308
|
+
}
|
|
309
|
+
deleteEmail(id) {
|
|
310
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
311
|
+
const self = this;
|
|
312
|
+
const emailIndex = this.store.findIndex((elt) => elt.id === id);
|
|
313
|
+
if (emailIndex < 0) {
|
|
314
|
+
throw new Error(`Email ${id} not found`);
|
|
315
|
+
}
|
|
316
|
+
const mail = this.store[emailIndex];
|
|
317
|
+
logger.warn("Deleting email - %s", mail.id);
|
|
318
|
+
this.store.splice(emailIndex, 1);
|
|
319
|
+
return Promise.all([
|
|
320
|
+
fs_1.promises.unlink(path.join(this.mailDir, id + ".eml")).catch((err) => {
|
|
321
|
+
throw new Error(`Error when deleteing ${id}`);
|
|
322
|
+
}),
|
|
323
|
+
fs_1.promises.rm(path.join(this.mailDir, id), { recursive: true, force: true }).catch((err) => {
|
|
324
|
+
throw new Error(`Error when deleteing ${id} attachments: ${err}`);
|
|
325
|
+
}),
|
|
326
|
+
]).then(() => {
|
|
327
|
+
self.eventEmitter.emit("delete", { id, index: emailIndex });
|
|
328
|
+
return true;
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
deleteAllEmail() {
|
|
333
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
334
|
+
logger.warn("Deleting all email");
|
|
335
|
+
this.clearMailDir();
|
|
336
|
+
this.store.length = 0;
|
|
337
|
+
this.eventEmitter.emit("delete", { id: "all" });
|
|
338
|
+
return true;
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Delete everything in the mail directory
|
|
343
|
+
*/
|
|
344
|
+
clearMailDir() {
|
|
345
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
346
|
+
const self = this;
|
|
347
|
+
const files = yield fs_1.promises.readdir(this.mailDir);
|
|
348
|
+
const rms = files.map(function (file) {
|
|
349
|
+
return fs_1.promises.rm(path.join(self.mailDir, file), { recursive: true });
|
|
350
|
+
});
|
|
351
|
+
return Promise.all(rms);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Returns the content type and a readable stream of the file
|
|
356
|
+
*/
|
|
357
|
+
getEmailAttachment(id, filename) {
|
|
358
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
359
|
+
const mail = yield this.getEmail(id);
|
|
360
|
+
if (mail.attachments.length === 0) {
|
|
361
|
+
throw new Error("Email has no attachments");
|
|
362
|
+
}
|
|
363
|
+
for (const attachment of mail.attachments) {
|
|
364
|
+
if (attachment.generatedFileName === filename) {
|
|
365
|
+
return attachment;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
throw new Error(`Attachment ${filename} not found`);
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Download a given email
|
|
373
|
+
*/
|
|
374
|
+
getEmailEml(id) {
|
|
375
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
376
|
+
const filename = id + ".eml";
|
|
377
|
+
const stream = yield this.getRawEmail(id);
|
|
378
|
+
return ["message/rfc822", filename, stream];
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
loadMailsFromDirectory() {
|
|
382
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
383
|
+
const self = this;
|
|
384
|
+
const persistencePath = yield fs_1.promises.realpath(this.mailDir);
|
|
385
|
+
const files = yield fs_1.promises.readdir(persistencePath).catch((err) => {
|
|
386
|
+
logger.error(`Error during reading of the mailDir ${persistencePath}`);
|
|
387
|
+
throw new Error(`Error during reading of the mailDir ${persistencePath}`);
|
|
388
|
+
});
|
|
389
|
+
this.store.length = 0;
|
|
390
|
+
const saved = files.map(function (file) {
|
|
391
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
392
|
+
const id = path.parse(file).name;
|
|
393
|
+
const email = yield getDiskEmail(self.mailDir, id, undefined);
|
|
394
|
+
return saveEmailToStore(self, email);
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
return Promise.all(saved);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
exports.MailServer = MailServer;
|
|
402
|
+
/**
|
|
403
|
+
* Handle mailServer error
|
|
404
|
+
*/
|
|
405
|
+
function onSmtpError(err) {
|
|
406
|
+
if (err.code === "ECONNRESET" && err.syscall === "read") {
|
|
407
|
+
logger.warn(`Ignoring "${err.message}" error thrown by SMTP server. Likely the client connection closed prematurely. Full error details below.`);
|
|
408
|
+
logger.error(err);
|
|
409
|
+
}
|
|
410
|
+
else
|
|
411
|
+
throw err;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Create mail directory
|
|
415
|
+
*/
|
|
416
|
+
function createMailDir(mailDir) {
|
|
417
|
+
fs.mkdirSync(mailDir, { recursive: true });
|
|
418
|
+
logger.info("MailDev using directory %s", mailDir);
|
|
419
|
+
}
|
|
420
|
+
function getDiskEmail(mailDir, id, envelope) {
|
|
421
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
422
|
+
const emlPath = path.join(mailDir, id + ".eml");
|
|
423
|
+
const data = yield fs_1.promises.readFile(emlPath, "utf8");
|
|
424
|
+
const parsedMail = yield (0, mailparser_1.parse)(data);
|
|
425
|
+
if (envelope === undefined) {
|
|
426
|
+
envelope = {
|
|
427
|
+
id: id,
|
|
428
|
+
from: parsedMail.from,
|
|
429
|
+
to: parsedMail.to,
|
|
430
|
+
date: parsedMail.date,
|
|
431
|
+
subject: parsedMail.subject,
|
|
432
|
+
hasAttachment: parsedMail.attachments.length > 0,
|
|
433
|
+
isRead: false,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
return buildMail(mailDir, envelope, parsedMail);
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
function buildMail(mailDir, envelope, parsedMail) {
|
|
440
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
441
|
+
const emlPath = path.join(mailDir, envelope.id + ".eml");
|
|
442
|
+
const stat = yield fs_1.promises.stat(emlPath);
|
|
443
|
+
return Object.assign({ id: envelope.id, envelope, calculatedBcc: (0, bcc_1.calculateBcc)(envelope.to, parsedMail.to, parsedMail.cc), size: stat.size, sizeHuman: utils.formatBytes(stat.size) }, parsedMail);
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* SMTP Server stream and helper functions
|
|
448
|
+
*/
|
|
449
|
+
// Save an email object on stream end
|
|
450
|
+
function saveEmailToStore(mailServer, mail) {
|
|
451
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
452
|
+
var _a;
|
|
453
|
+
logger.log("Saving email: %s, id: %s", mail.subject, mail.id);
|
|
454
|
+
mailServer.store.push(mail.envelope);
|
|
455
|
+
mailServer.eventEmitter.emit("new", mail);
|
|
456
|
+
const subject = mailServer === null || mailServer === void 0 ? void 0 : mailServer.mailEventSubjectMapper(mail);
|
|
457
|
+
if (subject) {
|
|
458
|
+
if (reservedEventName.includes(subject)) {
|
|
459
|
+
logger.error(`Invalid subject ${subject}; ${reservedEventName} are reserved for internal usage`);
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
mailServer.eventEmitter.emit(subject, mail);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if ((_a = mailServer === null || mailServer === void 0 ? void 0 : mailServer.outgoing) === null || _a === void 0 ? void 0 : _a.isAutoRelayEnabled()) {
|
|
466
|
+
yield mailServer.relayMail(mail).catch((err) => {
|
|
467
|
+
logger.error("Error when relaying email", err);
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Handle smtp-server onData stream
|
|
474
|
+
*/
|
|
475
|
+
function handleDataStream(mailServer, stream, session, callback) {
|
|
476
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
477
|
+
const id = utils.makeId();
|
|
478
|
+
const emlStream = fs.createWriteStream(path.join(mailServer.mailDir, id + ".eml"));
|
|
479
|
+
stream.on("end", function () {
|
|
480
|
+
emlStream.end();
|
|
481
|
+
callback(null, "Message queued as " + id);
|
|
482
|
+
});
|
|
483
|
+
stream.pipe(emlStream);
|
|
484
|
+
const parsed = yield (0, mailparser_1.parse)(stream);
|
|
485
|
+
const envelope = {
|
|
486
|
+
id: id,
|
|
487
|
+
from: session.envelope.mailFrom,
|
|
488
|
+
to: session.envelope.rcptTo,
|
|
489
|
+
date: parsed.date,
|
|
490
|
+
subject: parsed.subject,
|
|
491
|
+
hasAttachment: parsed.attachments.length > 0,
|
|
492
|
+
isRead: false,
|
|
493
|
+
};
|
|
494
|
+
const mail = yield buildMail(mailServer.mailDir, envelope, parsed);
|
|
495
|
+
return saveEmailToStore(mailServer, mail);
|
|
496
|
+
});
|
|
497
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.appendOptions = appendOptions;
|
|
4
|
+
exports.cliOptions = cliOptions;
|
|
5
|
+
const program = require("commander").program;
|
|
6
|
+
const pkg = require("../../package.json");
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const options = [
|
|
9
|
+
// General config
|
|
10
|
+
["-v, --verbose"],
|
|
11
|
+
["--silent"],
|
|
12
|
+
["--log-mail-contents", "Log a JSON representation of each incoming email"],
|
|
13
|
+
// SMTP server parameters
|
|
14
|
+
["-s, --smtp <port>", "MAILDEV_SMTP_PORT", "SMTP port to catch emails", 1025, (x) => parseInt(x)],
|
|
15
|
+
["--ip <ip address>", "MAILDEV_IP", "IP Address to bind SMTP service to", "0.0.0.0"],
|
|
16
|
+
["--mail-directory <path>", "MAILDEV_MAIL_DIRECTORY", "Directory for persisting mails"],
|
|
17
|
+
["--incoming-user <user>", "MAILDEV_INCOMING_USER", "SMTP user for incoming emails"],
|
|
18
|
+
["--incoming-pass <pass>", "MAILDEV_INCOMING_PASS", "SMTP password for incoming emails"],
|
|
19
|
+
["--incoming-secure", "MAILDEV_INCOMING_SECURE", "Use SMTP SSL for incoming emails", false],
|
|
20
|
+
["--incoming-cert <path>", "MAILDEV_INCOMING_CERT", "Cert file location for incoming SSL"],
|
|
21
|
+
["--incoming-key <path>", "MAILDEV_INCOMING_KEY", "Key file location for incoming SSL"],
|
|
22
|
+
[
|
|
23
|
+
"--hide-extensions <extensions>",
|
|
24
|
+
"MAILDEV_HIDE_EXTENSIONS",
|
|
25
|
+
"Comma separated list of SMTP extensions to NOT advertise (SMTPUTF8, PIPELINING, 8BITMIME)",
|
|
26
|
+
[],
|
|
27
|
+
(val) => val.split(","),
|
|
28
|
+
],
|
|
29
|
+
// Outgoing parameters
|
|
30
|
+
[
|
|
31
|
+
"--auto-relay [email]",
|
|
32
|
+
"MAILDEV_AUTO_RELAY",
|
|
33
|
+
"Use auto-relay mode. Optional relay email address",
|
|
34
|
+
],
|
|
35
|
+
["--outgoing-host <host>", "MAILDEV_OUTGOING_HOST", "SMTP host for outgoing emails"],
|
|
36
|
+
[
|
|
37
|
+
"--outgoing-port <port>",
|
|
38
|
+
"MAILDEV_OUTGOING_PORT",
|
|
39
|
+
"SMTP port for outgoing emails",
|
|
40
|
+
(x) => parseInt(x),
|
|
41
|
+
],
|
|
42
|
+
["--outgoing-user <user>", "MAILDEV_OUTGOING_USER", "SMTP user for outgoing emails"],
|
|
43
|
+
["--outgoing-pass <password>", "MAILDEV_OUTGOING_PASS", "SMTP password for outgoing emails"],
|
|
44
|
+
["--outgoing-secure", "MAILDEV_OUTGOING_SECURE", "Use SMTP SSL for outgoing emails", false],
|
|
45
|
+
["--auto-relay-rules <file>", "MAILDEV_AUTO_RELAY_RULES", "Filter rules for auto relay mode"],
|
|
46
|
+
// Web app config
|
|
47
|
+
[
|
|
48
|
+
"--disable-web",
|
|
49
|
+
"MAILDEV_DISABLE_WEB",
|
|
50
|
+
"Disable the use of the web interface. Useful for unit testing",
|
|
51
|
+
false,
|
|
52
|
+
],
|
|
53
|
+
["-w, --web <port>", "MAILDEV_WEB_PORT", "Port to run the Web GUI", 1080, (x) => parseInt(x)],
|
|
54
|
+
[
|
|
55
|
+
"--web-ip <ip address>",
|
|
56
|
+
"MAILDEV_WEB_IP",
|
|
57
|
+
"IP Address to bind HTTP service to, defaults to --ip",
|
|
58
|
+
],
|
|
59
|
+
["--web-user <user>", "MAILDEV_WEB_USER", "HTTP user for GUI"],
|
|
60
|
+
["--web-pass <password>", "MAILDEV_WEB_PASS", "HTTP password for GUI"],
|
|
61
|
+
["--base-pathname <path>", "MAILDEV_BASE_PATHNAME", "Base path for URLs"],
|
|
62
|
+
["--https", "MAILDEV_HTTPS", "Switch from http to https protocol", false],
|
|
63
|
+
["--https-key <file>", "MAILDEV_HTTPS_KEY", "The file path to the ssl private key"],
|
|
64
|
+
["--https-cert <file>", "MAILDEV_HTTPS_CERT", "The file path to the ssl cert file"],
|
|
65
|
+
];
|
|
66
|
+
function appendOptions(program, options) {
|
|
67
|
+
return options.reduce(function (chain, option) {
|
|
68
|
+
var _a;
|
|
69
|
+
const flag = option[0];
|
|
70
|
+
const envVariable = typeof option[1] === "string" ? option[1] : undefined;
|
|
71
|
+
const description = typeof option[2] === "string" ? option[2] : undefined;
|
|
72
|
+
const defaultValue = envVariable ? ((_a = process.env[envVariable]) !== null && _a !== void 0 ? _a : option[3]) : option[3];
|
|
73
|
+
const fn = option[4];
|
|
74
|
+
return fn
|
|
75
|
+
? chain.option(flag, description, fn, defaultValue)
|
|
76
|
+
: chain.option(flag, description, defaultValue);
|
|
77
|
+
}, program);
|
|
78
|
+
}
|
|
79
|
+
function cliOptions() {
|
|
80
|
+
var _a, _b, _c;
|
|
81
|
+
const config = appendOptions(program.version(pkg.version).allowUnknownOption(true), options)
|
|
82
|
+
.parse(process.argv)
|
|
83
|
+
.opts();
|
|
84
|
+
let web;
|
|
85
|
+
if (!(config === null || config === void 0 ? void 0 : config.disableWeb)) {
|
|
86
|
+
var secure;
|
|
87
|
+
if (config === null || config === void 0 ? void 0 : config.https) {
|
|
88
|
+
if (!(config === null || config === void 0 ? void 0 : config.httpsKey) || fs.existsSync(config === null || config === void 0 ? void 0 : config.httpsKey) === false) {
|
|
89
|
+
throw new Error("Unable to find https secure key. Please specify key file via -https-key argument");
|
|
90
|
+
}
|
|
91
|
+
if (!(config === null || config === void 0 ? void 0 : config.httpsCert) || fs.existsSync(config === null || config === void 0 ? void 0 : config.httpsCert) === false) {
|
|
92
|
+
throw new Error("Unable to find https secure cert. Please specify cert file via -https-cert argument");
|
|
93
|
+
}
|
|
94
|
+
secure = {
|
|
95
|
+
cert: config === null || config === void 0 ? void 0 : config.httpsCert,
|
|
96
|
+
key: config === null || config === void 0 ? void 0 : config.httpsKey,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
web = {
|
|
100
|
+
disabled: false,
|
|
101
|
+
port: config === null || config === void 0 ? void 0 : config.web,
|
|
102
|
+
host: config === null || config === void 0 ? void 0 : config.webIp,
|
|
103
|
+
basePathname: config === null || config === void 0 ? void 0 : config.basePathname,
|
|
104
|
+
auth: (config === null || config === void 0 ? void 0 : config.webUser) && (config === null || config === void 0 ? void 0 : config.webPass)
|
|
105
|
+
? { user: config === null || config === void 0 ? void 0 : config.webUser, pass: config === null || config === void 0 ? void 0 : config.webPass }
|
|
106
|
+
: undefined,
|
|
107
|
+
ssl: secure,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
web = { disabled: true };
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
verbose: config === null || config === void 0 ? void 0 : config.verbose,
|
|
115
|
+
silent: config === null || config === void 0 ? void 0 : config.silent,
|
|
116
|
+
logMailContents: config === null || config === void 0 ? void 0 : config.logMailContents,
|
|
117
|
+
port: config === null || config === void 0 ? void 0 : config.smtp,
|
|
118
|
+
host: config === null || config === void 0 ? void 0 : config.ip,
|
|
119
|
+
mailDir: config === null || config === void 0 ? void 0 : config.mailDirectory,
|
|
120
|
+
auth: (config === null || config === void 0 ? void 0 : config.incomingUser) && (config === null || config === void 0 ? void 0 : config.incomingPass)
|
|
121
|
+
? {
|
|
122
|
+
user: config === null || config === void 0 ? void 0 : config.incomingUser,
|
|
123
|
+
pass: config === null || config === void 0 ? void 0 : config.incomingPass,
|
|
124
|
+
}
|
|
125
|
+
: undefined,
|
|
126
|
+
isSecure: config === null || config === void 0 ? void 0 : config.incomingSecure,
|
|
127
|
+
ssl: (config === null || config === void 0 ? void 0 : config.incomingCert) && (config === null || config === void 0 ? void 0 : config.incomingKey)
|
|
128
|
+
? {
|
|
129
|
+
certPath: config === null || config === void 0 ? void 0 : config.incomingCert,
|
|
130
|
+
keyPath: config === null || config === void 0 ? void 0 : config.incomingKey,
|
|
131
|
+
}
|
|
132
|
+
: undefined,
|
|
133
|
+
hide8BITMIME: ((_a = config === null || config === void 0 ? void 0 : config.hideExtensions) !== null && _a !== void 0 ? _a : []).includes("8BITMIME"),
|
|
134
|
+
hidePIPELINING: ((_b = config === null || config === void 0 ? void 0 : config.hideExtensions) !== null && _b !== void 0 ? _b : []).includes("PIPELINING"),
|
|
135
|
+
hideSMTPUTF8: ((_c = config === null || config === void 0 ? void 0 : config.hideExtensions) !== null && _c !== void 0 ? _c : []).includes("SMTPUTF8"),
|
|
136
|
+
outgoing: (config === null || config === void 0 ? void 0 : config.autoRelay) || (config === null || config === void 0 ? void 0 : config.autoRelayRules)
|
|
137
|
+
? {
|
|
138
|
+
host: config === null || config === void 0 ? void 0 : config.outgoingHost,
|
|
139
|
+
port: config === null || config === void 0 ? void 0 : config.outgoingPort,
|
|
140
|
+
secure: config === null || config === void 0 ? void 0 : config.outgoingSecure,
|
|
141
|
+
auth: (config === null || config === void 0 ? void 0 : config.outgoingUser) && (config === null || config === void 0 ? void 0 : config.outgoingPass)
|
|
142
|
+
? { user: config === null || config === void 0 ? void 0 : config.outgoingUser, pass: config === null || config === void 0 ? void 0 : config.outgoingPass }
|
|
143
|
+
: undefined,
|
|
144
|
+
autoRelayAddress: config === null || config === void 0 ? void 0 : config.autoRelay,
|
|
145
|
+
autoRelayRules: config === null || config === void 0 ? void 0 : config.autoRelayRules,
|
|
146
|
+
}
|
|
147
|
+
: undefined,
|
|
148
|
+
web,
|
|
149
|
+
};
|
|
150
|
+
}
|