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.
Files changed (5) hide show
  1. package/-H +0 -0
  2. package/-d +1 -0
  3. package/a.js +19 -0
  4. package/index.js +364 -0
  5. 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
+ }