dx-mail 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/-H +0 -0
- package/-d +1 -0
- package/a.js +19 -0
- package/index.js +364 -0
- package/package.json +19 -0
package/-H
ADDED
|
File without changes
|
package/-d
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"ok":false,"error":"Unauthorized"}
|
package/a.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const mail = require("./index.js");
|
|
2
|
+
const app = mail();
|
|
3
|
+
|
|
4
|
+
app.setBaseUrl("https://mailsrv-1a31daad9cb8.herokuapp.com");
|
|
5
|
+
app.auth("dxm_46a9ac05fd8230b856f6d4fb1e1f4c5af63f6b045cda4a48");
|
|
6
|
+
|
|
7
|
+
app.createMail("anj", (res) => {
|
|
8
|
+
console.log(JSON.stringify(res,null,2));
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
app.listMail((res) => {
|
|
12
|
+
console.log("kosek mek");
|
|
13
|
+
//console.log(JSON.stringify(res,null,2))
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
app.watchMail("memek", (res) => { if (res.message?.subject) { console.log("ada"); console.log(res.message.subject); console.log(res.message.text); }})
|
|
18
|
+
|
|
19
|
+
|
package/index.js
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
const { io } = require("socket.io-client");
|
|
2
|
+
const EventEmitter = require("events");
|
|
3
|
+
|
|
4
|
+
function cleanBaseUrl(url) {
|
|
5
|
+
return String(url || "http://localhost:3000").replace(/\/+$/, "");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function cleanLocalPart(value) {
|
|
9
|
+
return String(value || "")
|
|
10
|
+
.trim()
|
|
11
|
+
.toLowerCase()
|
|
12
|
+
.replace(/^@+/, "")
|
|
13
|
+
.split("@")[0];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isFunction(value) {
|
|
17
|
+
return typeof value === "function";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function callbackify(promise, callback) {
|
|
21
|
+
if (isFunction(callback)) {
|
|
22
|
+
promise.then((res) => callback(res)).catch((err) => {
|
|
23
|
+
callback({
|
|
24
|
+
ok: false,
|
|
25
|
+
error: err.message || String(err)
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return promise;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class DxMailClient extends EventEmitter {
|
|
34
|
+
constructor(options = {}) {
|
|
35
|
+
super();
|
|
36
|
+
|
|
37
|
+
if (typeof options === "string") {
|
|
38
|
+
options = {
|
|
39
|
+
token: options
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.baseUrl = cleanBaseUrl(
|
|
44
|
+
options.baseUrl ||
|
|
45
|
+
process.env.DX_MAIL_BASE_URL ||
|
|
46
|
+
"http://localhost:3000"
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
this.token = options.token || process.env.DX_MAIL_TOKEN || null;
|
|
50
|
+
|
|
51
|
+
this.socket = null;
|
|
52
|
+
this.watchers = new Map();
|
|
53
|
+
this.seenMessageIds = new Set();
|
|
54
|
+
this.maxSeenMessages = 1000;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
auth(token) {
|
|
58
|
+
this.token = token;
|
|
59
|
+
|
|
60
|
+
if (this.socket) {
|
|
61
|
+
this.socket.auth = {
|
|
62
|
+
token: this.token
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
setBaseUrl(baseUrl) {
|
|
70
|
+
this.baseUrl = cleanBaseUrl(baseUrl);
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
ensureToken() {
|
|
75
|
+
if (!this.token) {
|
|
76
|
+
throw new Error("DX Mail token belum di-set. Pakai app.auth(token) atau dxMail({ token }).");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async request(path, options = {}) {
|
|
81
|
+
this.ensureToken();
|
|
82
|
+
|
|
83
|
+
const response = await fetch(this.baseUrl + path, {
|
|
84
|
+
method: options.method || "GET",
|
|
85
|
+
headers: {
|
|
86
|
+
"Content-Type": "application/json",
|
|
87
|
+
"x-api-key": this.token,
|
|
88
|
+
...(options.headers || {})
|
|
89
|
+
},
|
|
90
|
+
body: options.body ? JSON.stringify(options.body) : undefined
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const text = await response.text();
|
|
94
|
+
let data;
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
data = text ? JSON.parse(text) : {};
|
|
98
|
+
} catch {
|
|
99
|
+
data = {
|
|
100
|
+
ok: false,
|
|
101
|
+
error: text || "Invalid JSON response"
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!response.ok || data.ok === false) {
|
|
106
|
+
const error = new Error(data.error || `Request failed with status ${response.status}`);
|
|
107
|
+
error.status = response.status;
|
|
108
|
+
error.data = data;
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return data;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
createMail(nameOrOptions, callback) {
|
|
116
|
+
let body = {};
|
|
117
|
+
|
|
118
|
+
if (isFunction(nameOrOptions)) {
|
|
119
|
+
callback = nameOrOptions;
|
|
120
|
+
} else if (typeof nameOrOptions === "string") {
|
|
121
|
+
body.name = cleanLocalPart(nameOrOptions);
|
|
122
|
+
} else if (nameOrOptions && typeof nameOrOptions === "object") {
|
|
123
|
+
body = {
|
|
124
|
+
...nameOrOptions
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
if (body.name) {
|
|
128
|
+
body.name = cleanLocalPart(body.name);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return callbackify(
|
|
133
|
+
this.request("/api/mail", {
|
|
134
|
+
method: "POST",
|
|
135
|
+
body
|
|
136
|
+
}),
|
|
137
|
+
callback
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
createInbox(nameOrOptions, callback) {
|
|
142
|
+
return this.createMail(nameOrOptions, callback);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
listMail(callback) {
|
|
146
|
+
return callbackify(
|
|
147
|
+
this.request("/api/mail"),
|
|
148
|
+
callback
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
listInboxes(callback) {
|
|
153
|
+
return this.listMail(callback);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
getMessages(name, optionsOrCallback, callback) {
|
|
157
|
+
let options = {};
|
|
158
|
+
|
|
159
|
+
if (isFunction(optionsOrCallback)) {
|
|
160
|
+
callback = optionsOrCallback;
|
|
161
|
+
} else if (optionsOrCallback && typeof optionsOrCallback === "object") {
|
|
162
|
+
options = optionsOrCallback;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const localPart = cleanLocalPart(name);
|
|
166
|
+
const limit = options.limit ? `?limit=${encodeURIComponent(options.limit)}` : "";
|
|
167
|
+
|
|
168
|
+
return callbackify(
|
|
169
|
+
this.request(`/api/mail/${encodeURIComponent(localPart)}/messages${limit}`),
|
|
170
|
+
callback
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
getMail(name, optionsOrCallback, callback) {
|
|
175
|
+
return this.getMessages(name, optionsOrCallback, callback);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
connect() {
|
|
179
|
+
this.ensureToken();
|
|
180
|
+
|
|
181
|
+
if (this.socket) {
|
|
182
|
+
if (!this.socket.connected) {
|
|
183
|
+
this.socket.connect();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return this.socket;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
this.socket = io(this.baseUrl, {
|
|
190
|
+
auth: {
|
|
191
|
+
token: this.token
|
|
192
|
+
},
|
|
193
|
+
transports: ["websocket", "polling"],
|
|
194
|
+
autoConnect: true,
|
|
195
|
+
reconnection: true
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
this.socket.on("connect", () => {
|
|
199
|
+
this.emit("connect", {
|
|
200
|
+
id: this.socket.id
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
for (const localPart of this.watchers.keys()) {
|
|
204
|
+
this.socket.emit("mail:watch", {
|
|
205
|
+
name: localPart
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
this.socket.on("connect_error", (error) => {
|
|
211
|
+
this.emit("error", error);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
this.socket.on("disconnect", (reason) => {
|
|
215
|
+
this.emit("disconnect", reason);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
this.socket.on("mail.received", (event) => {
|
|
219
|
+
const messageId = event?.message?.id;
|
|
220
|
+
|
|
221
|
+
if (messageId) {
|
|
222
|
+
if (this.seenMessageIds.has(messageId)) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
this.seenMessageIds.add(messageId);
|
|
227
|
+
|
|
228
|
+
if (this.seenMessageIds.size > this.maxSeenMessages) {
|
|
229
|
+
const first = this.seenMessageIds.values().next().value;
|
|
230
|
+
this.seenMessageIds.delete(first);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const localPart =
|
|
235
|
+
cleanLocalPart(event?.inbox?.name) ||
|
|
236
|
+
cleanLocalPart(event?.message?.to);
|
|
237
|
+
|
|
238
|
+
this.emit("mail.received", event);
|
|
239
|
+
|
|
240
|
+
const callbacks = this.watchers.get(localPart);
|
|
241
|
+
|
|
242
|
+
if (!callbacks || callbacks.size === 0) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
for (const callback of callbacks) {
|
|
247
|
+
callback(event);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
return this.socket;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
watchMail(name, callback) {
|
|
255
|
+
if (!isFunction(callback)) {
|
|
256
|
+
throw new Error("watchMail(name, callback) butuh callback.");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const localPart = cleanLocalPart(name);
|
|
260
|
+
|
|
261
|
+
if (!localPart) {
|
|
262
|
+
throw new Error("Nama mail kosong.");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (!this.watchers.has(localPart)) {
|
|
266
|
+
this.watchers.set(localPart, new Set());
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
this.watchers.get(localPart).add(callback);
|
|
270
|
+
|
|
271
|
+
const socket = this.connect();
|
|
272
|
+
|
|
273
|
+
socket.emit("mail:watch", {
|
|
274
|
+
name: localPart
|
|
275
|
+
}, (ack) => {
|
|
276
|
+
if (!ack || ack.ok === false) {
|
|
277
|
+
callback({
|
|
278
|
+
ok: false,
|
|
279
|
+
type: "mail.watch.error",
|
|
280
|
+
error: ack?.error || "Failed to watch mail",
|
|
281
|
+
inbox: {
|
|
282
|
+
name: localPart
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const stop = () => {
|
|
289
|
+
const callbacks = this.watchers.get(localPart);
|
|
290
|
+
|
|
291
|
+
if (callbacks) {
|
|
292
|
+
callbacks.delete(callback);
|
|
293
|
+
|
|
294
|
+
if (callbacks.size === 0) {
|
|
295
|
+
this.watchers.delete(localPart);
|
|
296
|
+
|
|
297
|
+
if (this.socket) {
|
|
298
|
+
this.socket.emit("mail:unwatch", {
|
|
299
|
+
name: localPart
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
return stop;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
watchAll(callback) {
|
|
310
|
+
if (!isFunction(callback)) {
|
|
311
|
+
throw new Error("watchAll(callback) butuh callback.");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
this.connect();
|
|
315
|
+
this.on("mail.received", callback);
|
|
316
|
+
|
|
317
|
+
return () => {
|
|
318
|
+
this.off("mail.received", callback);
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
waitMail(name, options = {}) {
|
|
323
|
+
const timeoutMs = Number(options.timeoutMs || 60000);
|
|
324
|
+
|
|
325
|
+
return new Promise((resolve, reject) => {
|
|
326
|
+
const timer = setTimeout(() => {
|
|
327
|
+
stop();
|
|
328
|
+
reject(new Error(`Timeout menunggu email masuk ke ${name}`));
|
|
329
|
+
}, timeoutMs);
|
|
330
|
+
|
|
331
|
+
const stop = this.watchMail(name, (event) => {
|
|
332
|
+
if (event?.ok === false) {
|
|
333
|
+
clearTimeout(timer);
|
|
334
|
+
stop();
|
|
335
|
+
reject(new Error(event.error || "Failed to watch mail"));
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
clearTimeout(timer);
|
|
340
|
+
stop();
|
|
341
|
+
resolve(event);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
close() {
|
|
347
|
+
if (this.socket) {
|
|
348
|
+
this.socket.close();
|
|
349
|
+
this.socket = null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
this.watchers.clear();
|
|
353
|
+
this.seenMessageIds.clear();
|
|
354
|
+
return this;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function dxMail(options) {
|
|
359
|
+
return new DxMailClient(options);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
dxMail.Client = DxMailClient;
|
|
363
|
+
|
|
364
|
+
module.exports = dxMail;
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dx-mail",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "DX Mail client for temp mail API and realtime inbox watcher",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"temp-mail",
|
|
8
|
+
"email",
|
|
9
|
+
"websocket",
|
|
10
|
+
"cloudflare"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"socket.io-client": "^4.8.3"
|
|
18
|
+
}
|
|
19
|
+
}
|