@nka212bg/backend-utils 0.1.49 → 0.1.51
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/mailSmsTools/index.js +183 -142
- package/package.json +1 -1
package/mailSmsTools/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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()} ${name || ""}</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());
|