@nka212bg/backend-utils 0.1.49 → 0.1.50

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 (2) hide show
  1. package/mailSmsTools/index.js +183 -142
  2. package/package.json +1 -1
@@ -10,150 +10,11 @@ const emailListFilePath = `${__basedir}/state/useremailList.json`;
10
10
 
11
11
  let EMAILS_LIST = existsSync(emailListFilePath) ? JSON.parse(readFileSync(emailListFilePath, "utf8") || "{}") : {};
12
12
 
13
- // set via require(".../mailSmsTools")({ ... }), empty values fall back to process.env
14
- const config = {
15
- name: "", // "From" display name
16
- logoUrl: "",
17
- baseUrl: "",
18
- smtp: {}, // nodemailer transport options, override the env defaults
19
- twilio: {}, // { accountSid, authToken, sender }
20
- };
21
- let mailTransport;
22
- let twilio;
23
-
24
- // callable with options, named exports still work:
25
- // const { sendEmail } = require(".../mailSmsTools")({ name, logoUrl, baseUrl, smtp, twilio });
26
- // const { sendEmail } = require(".../mailSmsTools");
27
- module.exports = Object.assign((options = {}) => {
28
- Object.assign(config, options);
29
- mailTransport = twilio = null; // recreate with the new options on next send
30
- return module.exports;
31
- }, exports);
32
-
33
- // created on first send, so the config is already applied
34
- const nodemailerTransport = () =>
35
- (mailTransport ??= nodemailer.createTransport({
36
- pool: true,
37
- maxMessages: Infinity,
38
- maxConnections: 20,
39
-
40
- host: process.env.SMTP_HOST,
41
- port: parseInt(process.env.SMTP_PORT),
42
- secure: true, // true for 465, false for other ports
43
- auth: {
44
- user: process.env.SMTP_USER,
45
- pass: process.env.SMTP_PASS,
46
- },
47
- ...config.smtp,
48
- }));
49
-
50
- /* -------------------- EMAIL -------------------- */
51
- exports.sendEmail = async ({ email, content, subject, attachments, unsubscribe, userIp, logoUrl } = {}) => {
52
- if (!content) return { error: "missing_content" };
53
- if (!subject || subject === "") subject = content.substring(0, 25) + "... ";
54
-
55
- const baseUrl = config.baseUrl || process.env.BASE_URL;
56
- const smtpUser = config.smtp.auth?.user || process.env.SMTP_USER;
57
- logoUrl ||= config.logoUrl;
58
-
59
- const emailArray = strToEmailsArray(email || smtpUser);
60
- if (!emailArray.length) {
61
- return { error: "missing_email" };
62
- //
63
- } else if (userIp && emailArray.length === 1) {
64
- let userlocation = `User IP: ${userIp}`;
65
-
66
- const { error: locationError, data: locationData } = location({ ip: userIp });
67
-
68
- if (!locationError) {
69
- userlocation += `<br>Location: ${locationData.city} ${locationData.country_name} <a href="https://www.google.com/maps/@${locationData.coordinates.latitude},${locationData.coordinates.longitude},15z" target="_blank">see in map</a>`;
70
- }
71
-
72
- content += `<br/><br/>${userlocation}<br/><br/>`;
73
- }
74
-
75
- return new Promise((resolve, reject) => {
76
- const emailListEmails = [];
77
- const successEmails = [];
78
- const errorEmails = [];
79
-
80
- emailArray.forEach((e, i) => {
81
- if (unsubscribe && e in EMAILS_LIST && EMAILS_LIST[e] == -1) {
82
- emailListEmails.push(e);
83
-
84
- if (i + 1 >= emailArray.length) resolve({ error: errorEmails.length ? errorEmails : null, emailList: emailListEmails.length ? emailListEmails : null, data: successEmails });
85
- return;
86
- }
87
-
88
- const html = `
89
- <div style="padding: 20px">
90
- <table border="0" cellpadding="0" cellspacing="0" width="100%" role="presentation" style="margin: 0; max-width: 500px; padding: 0; font-family: system-ui; font-size: 14px; background-color: transparent">
91
- <tbody>
92
- <tr>
93
- <td style="padding-bottom: 6px">
94
- <img src="${logoUrl}" style="height: 36px; background-size: contain; background-repeat: no-repeat" />
95
- </td>
96
- </tr>
97
- <tr><td style="padding-bottom: 20px">${content}</td></tr>
98
- <tr>
99
- <td>
100
- <a href="${baseUrl}" target="_blank" style="text-decoration: none; color: gray; font-size: 12px">${new Date().getFullYear()} ${baseUrl}</a>
101
- </td>
102
- </tr>
103
- </tbody>
104
- </table>
105
- </div>`;
106
-
107
- const mailOptions = {
108
- from: config.name ? { name: config.name, address: smtpUser } : smtpUser,
109
- // html: handlebars({ template: defaultTemplate, content, subject, logoUrl: `${process.env.BASE_URL}/logo_black.png` }),
110
- html,
111
- subject,
112
- to: e,
113
- attachments,
114
- };
115
-
116
- nodemailerTransport().sendMail(mailOptions, (error, info) => {
117
- error ? errorEmails.push(error) : successEmails.push(info);
118
-
119
- if (i + 1 >= emailArray.length) resolve({ error: errorEmails.length ? errorEmails : null, emailList: emailListEmails.length ? emailListEmails : null, data: successEmails });
120
- });
121
- });
122
- });
123
- };
124
-
125
- /* -------------------- SMS -------------------- */
126
- exports.sendSMS = async ({ phones, content, sender = config.twilio.sender || process.env.TWILIO_SENDER } = {}) => {
127
- if (!phones) return { error: "missing_phones" };
128
- if (!content) return { error: "missing_content" };
129
- if (!twilio) twilio = require("twilio")(config.twilio.accountSid || process.env.TWILIO_ACCOUNT_SID, config.twilio.authToken || process.env.TWILIO_AUTH_TOKEN);
130
-
131
- return new Promise((resolve, reject) => {
132
- const errorPhones = [];
133
- const successPhones = [];
134
-
135
- if (!Array.isArray(phones)) phones = [phones.toString()];
136
- phones.forEach((e, i) => {
137
- twilio.messages
138
- .create({
139
- body: content,
140
- from: sender,
141
- to: e,
142
- })
143
- .then((message) => {
144
- successPhones.push(message);
145
- if (i >= phones.length - 1) setTimeout(() => resolve({ error: errorPhones.length ? errorPhones : null, data: successPhones }), i * 50);
146
- })
147
- .catch((error) => {
148
- errorPhones.push(error);
149
- if (i >= phones.length - 1) setTimeout(() => resolve({ error: errorPhones, data: successPhones }), i * 50);
150
- });
151
- });
152
- });
153
- };
13
+ const val = (v) => (typeof v == "function" ? v() : v);
154
14
 
155
15
  // -------- unsubscribe users --------
156
- exports.subscriptionState = {
16
+ // shared by all instances, it's one file on disk
17
+ const subscriptionState = {
157
18
  is(email) {
158
19
  return EMAILS_LIST[email];
159
20
  },
@@ -188,3 +49,183 @@ exports.subscriptionState = {
188
49
  if (change) writeFileSync(emailListFilePath, JSON.stringify(EMAILS_LIST));
189
50
  },
190
51
  };
52
+
53
+ // every call creates a separate instance with its own config and transports:
54
+ // const { sendEmail } = require(".../mailSmsTools")({ name, logoUrl, baseUrl, smtp, twilio });
55
+ // const { sendEmail } = require(".../mailSmsTools"); // default instance, env config only
56
+ //
57
+ // empty values fall back to process.env
58
+ // name, logoUrl, baseUrl, smtp, twilio can also be functions, resolved on each send (e.g. baseUrl: () => __baseUrl)
59
+ const createInstance = (options = {}) => {
60
+ const config = {
61
+ name: "", // "From" display name
62
+ logoUrl: "",
63
+ baseUrl: "",
64
+ smtp: {}, // nodemailer transport options, override the env defaults
65
+ twilio: {}, // { accountSid, authToken, sender }
66
+ ...options,
67
+ };
68
+ let mailTransport, mailTransportKey;
69
+ let twilioClient, twilioClientKey;
70
+
71
+ // created on first send, rebuilt only when the smtp options change
72
+ const nodemailerTransport = (smtp) => {
73
+ const key = JSON.stringify(smtp);
74
+ if (mailTransport && key == mailTransportKey) return mailTransport;
75
+
76
+ mailTransport?.close();
77
+ mailTransportKey = key;
78
+
79
+ return (mailTransport = nodemailer.createTransport({
80
+ pool: true,
81
+ maxMessages: Infinity,
82
+ maxConnections: 20,
83
+
84
+ host: process.env.SMTP_HOST,
85
+ port: parseInt(process.env.SMTP_PORT),
86
+ secure: true, // true for 465, false for other ports
87
+ auth: {
88
+ user: process.env.SMTP_USER,
89
+ pass: process.env.SMTP_PASS,
90
+ },
91
+ ...smtp,
92
+ }));
93
+ };
94
+
95
+ // created on first send, rebuilt only when the credentials change
96
+ const twilioTransport = (twilio) => {
97
+ const accountSid = twilio.accountSid || process.env.TWILIO_ACCOUNT_SID;
98
+ const authToken = twilio.authToken || process.env.TWILIO_AUTH_TOKEN;
99
+
100
+ const key = `${accountSid}:${authToken}`;
101
+ if (twilioClient && key == twilioClientKey) return twilioClient;
102
+
103
+ twilioClientKey = key;
104
+ return (twilioClient = require("twilio")(accountSid, authToken));
105
+ };
106
+
107
+ return {
108
+ subscriptionState,
109
+
110
+ /* -------------------- EMAIL -------------------- */
111
+ async sendEmail({ email, content, subject, attachments, unsubscribe, userIp, logoUrl } = {}) {
112
+ if (!content) return { error: "missing_content" };
113
+ if (!subject || subject === "") subject = content.substring(0, 25) + "... ";
114
+
115
+ const name = val(config.name);
116
+ const baseUrl = val(config.baseUrl) || process.env.BASE_URL;
117
+ const smtp = val(config.smtp) || {};
118
+ const smtpUser = smtp.auth?.user || process.env.SMTP_USER;
119
+ logoUrl ||= val(config.logoUrl);
120
+
121
+ const emailArray = strToEmailsArray(email || smtpUser);
122
+ if (!emailArray.length) {
123
+ return { error: "missing_email" };
124
+ //
125
+ } else if (userIp && emailArray.length === 1) {
126
+ let userlocation = `User IP: ${userIp}`;
127
+
128
+ const { error: locationError, data: locationData } = location({ ip: userIp });
129
+
130
+ if (!locationError) {
131
+ userlocation += `<br>Location: ${locationData.city} ${locationData.country_name} <a href="https://www.google.com/maps/@${locationData.coordinates.latitude},${locationData.coordinates.longitude},15z" target="_blank">see in map</a>`;
132
+ }
133
+
134
+ content += `<br/><br/>${userlocation}<br/><br/>`;
135
+ }
136
+
137
+ return new Promise((resolve, reject) => {
138
+ const emailListEmails = [];
139
+ const successEmails = [];
140
+ const errorEmails = [];
141
+
142
+ emailArray.forEach((e, i) => {
143
+ if (unsubscribe && e in EMAILS_LIST && EMAILS_LIST[e] == -1) {
144
+ emailListEmails.push(e);
145
+
146
+ if (i + 1 >= emailArray.length) resolve({ error: errorEmails.length ? errorEmails : null, emailList: emailListEmails.length ? emailListEmails : null, data: successEmails });
147
+ return;
148
+ }
149
+
150
+ const html = `
151
+ <div style="padding: 20px">
152
+ <table border="0" cellpadding="0" cellspacing="0" width="100%" role="presentation" style="margin: 0; max-width: 500px; padding: 0; font-family: system-ui; font-size: 14px; background-color: transparent">
153
+ <tbody>
154
+ ${
155
+ !logoUrl
156
+ ? ""
157
+ : `<tr>
158
+ <td style="padding-bottom: 6px">
159
+ <img src="${logoUrl}" style="height: 36px; background-size: contain; background-repeat: no-repeat" />
160
+ </td>
161
+ </tr>`
162
+ }
163
+ <tr><td style="padding-bottom: 20px">${content}</td></tr>
164
+ ${
165
+ !baseUrl
166
+ ? ""
167
+ : `<tr>
168
+ <td>
169
+ <a href="${baseUrl}" target="_blank" style="text-decoration: none; color: gray; font-size: 12px">${new Date().getFullYear()} ${baseUrl}</a>
170
+ </td>
171
+ </tr>`
172
+ }
173
+ </tbody>
174
+ </table>
175
+ </div>`;
176
+
177
+ const mailOptions = {
178
+ from: name ? { name, address: smtpUser } : smtpUser,
179
+ // html: handlebars({ template: defaultTemplate, content, subject, logoUrl: `${process.env.BASE_URL}/logo_black.png` }),
180
+ html,
181
+ subject,
182
+ to: e,
183
+ attachments,
184
+ };
185
+
186
+ nodemailerTransport(smtp).sendMail(mailOptions, (error, info) => {
187
+ error ? errorEmails.push(error) : successEmails.push(info);
188
+
189
+ if (i + 1 >= emailArray.length) resolve({ error: errorEmails.length ? errorEmails : null, emailList: emailListEmails.length ? emailListEmails : null, data: successEmails });
190
+ });
191
+ });
192
+ });
193
+ },
194
+
195
+ /* -------------------- SMS -------------------- */
196
+ async sendSMS({ phones, content, sender } = {}) {
197
+ if (!phones) return { error: "missing_phones" };
198
+ if (!content) return { error: "missing_content" };
199
+
200
+ const twilio = val(config.twilio) || {};
201
+ sender ||= twilio.sender || process.env.TWILIO_SENDER;
202
+ const client = twilioTransport(twilio);
203
+
204
+ return new Promise((resolve, reject) => {
205
+ const errorPhones = [];
206
+ const successPhones = [];
207
+
208
+ if (!Array.isArray(phones)) phones = [phones.toString()];
209
+ phones.forEach((e, i) => {
210
+ client.messages
211
+ .create({
212
+ body: content,
213
+ from: sender,
214
+ to: e,
215
+ })
216
+ .then((message) => {
217
+ successPhones.push(message);
218
+ if (i >= phones.length - 1) setTimeout(() => resolve({ error: errorPhones.length ? errorPhones : null, data: successPhones }), i * 50);
219
+ })
220
+ .catch((error) => {
221
+ errorPhones.push(error);
222
+ if (i >= phones.length - 1) setTimeout(() => resolve({ error: errorPhones, data: successPhones }), i * 50);
223
+ });
224
+ });
225
+ });
226
+ },
227
+ };
228
+ };
229
+
230
+ // callable for a configured instance, plain require gives the default one
231
+ module.exports = Object.assign(createInstance, createInstance());
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nka212bg/backend-utils",
3
3
  "author": "nka212bg",
4
- "version": "0.1.49",
4
+ "version": "0.1.50",
5
5
  "main": "index.js",
6
6
  "dependencies": {
7
7
  "@maxmind/geoip2-node": "^5.0.0",