domma-cms 0.32.4 → 0.33.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.
@@ -1,167 +1,167 @@
1
- /**
2
- * Core Email Utility
3
- * Nodemailer transport factory and generic form email sending utility.
4
- */
5
- import nodemailer from 'nodemailer';
6
-
7
- let lastSendResult = null;
8
-
9
- /**
10
- * Return the most recent email send result, or null if no send has occurred.
11
- *
12
- * @returns {{ ok: boolean, at: string, info: string|null } | null}
13
- */
14
- export function getLastSendResult() {
15
- return lastSendResult;
16
- }
17
-
18
- /**
19
- * Record the outcome of an email send for health reporting.
20
- *
21
- * @param {boolean} ok
22
- * @param {string|null} info
23
- * @returns {void}
24
- */
25
- function recordSendResult(ok, info) {
26
- lastSendResult = {
27
- ok,
28
- at: new Date().toISOString(),
29
- info: info || null
30
- };
31
- }
32
-
33
- /**
34
- * Escape HTML special characters for safe use in email bodies.
35
- *
36
- * @param {string} str
37
- * @returns {string}
38
- */
39
- export function escapeHtml(str) {
40
- return String(str)
41
- .replace(/&/g, '&')
42
- .replace(/</g, '&lt;')
43
- .replace(/>/g, '&gt;')
44
- .replace(/"/g, '&quot;')
45
- .replace(/'/g, '&#39;');
46
- }
47
-
48
- /**
49
- * Create a nodemailer transport.
50
- * Falls back to an Ethereal test account when no SMTP host is configured.
51
- *
52
- * @param {{ host: string, port: number, secure: boolean, user: string, pass: string }} smtp
53
- * @returns {Promise<import('nodemailer').Transporter>}
54
- * @throws {Error} If Ethereal test account creation fails.
55
- */
56
- export async function createTransport(smtp) {
57
- if (smtp?.host) {
58
- return nodemailer.createTransport({
59
- host: smtp.host,
60
- port: smtp.port || 587,
61
- secure: smtp.secure || false,
62
- auth: smtp.user ? { user: smtp.user, pass: smtp.pass } : undefined,
63
- tls: { rejectUnauthorized: false }
64
- });
65
- }
66
-
67
- // No SMTP configured — use Ethereal for dev/demo
68
- const testAccount = await nodemailer.createTestAccount();
69
- console.log('[email] No SMTP configured. Using Ethereal test account:', testAccount.user);
70
- return nodemailer.createTransport({
71
- host: 'smtp.ethereal.email',
72
- port: 587,
73
- secure: false,
74
- auth: { user: testAccount.user, pass: testAccount.pass }
75
- });
76
- }
77
-
78
- /**
79
- * Send a generic transactional email.
80
- *
81
- * @param {import('nodemailer').Transporter} transport
82
- * @param {{ from: string, fromName: string, to: string, subject: string, html: string, text: string }} opts
83
- * @returns {Promise<void>}
84
- * @throws {Error} If sending the email fails.
85
- */
86
- export async function sendEmail(transport, {from, fromName, to, subject, html, text}) {
87
- try {
88
- const info = await transport.sendMail({
89
- from: `"${fromName}" <${from}>`,
90
- to,
91
- subject,
92
- text,
93
- html
94
- });
95
- recordSendResult(true, info.messageId);
96
-
97
- const previewUrl = nodemailer.getTestMessageUrl(info);
98
- if (previewUrl) {
99
- console.log('[email] Preview URL:', previewUrl);
100
- }
101
- return info;
102
- } catch (err) {
103
- recordSendResult(false, err.message);
104
- throw err;
105
- }
106
- }
107
-
108
- /**
109
- * Send an HTML + plain-text form submission notification email.
110
- * Builds a generic table of field→value pairs from the submitted data.
111
- *
112
- * @param {import('nodemailer').Transporter} transport
113
- * @param {{ from: string, fromName: string, to: string, subject: string, formTitle: string, fields: Array<{name: string, label: string}>, data: Record<string, unknown> }} opts
114
- * @returns {Promise<void>}
115
- * @throws {Error} If sending the email fails.
116
- */
117
- export async function sendFormEmail(transport, { from, fromName, to, subject, formTitle, fields, data }) {
118
- const rows = fields.map(field => {
119
- const val = data[field.name] ?? '';
120
- const safe = escapeHtml(String(val)).replace(/\n/g, '<br>');
121
- const safeLabel = escapeHtml(field.label || field.name);
122
- return `
123
- <tr>
124
- <td style="padding:8px 12px;font-weight:600;background:#f9f9f9;border:1px solid #eee;white-space:nowrap;vertical-align:top;">${safeLabel}</td>
125
- <td style="padding:8px 12px;border:1px solid #eee;vertical-align:top;">${safe}</td>
126
- </tr>`.trim();
127
- }).join('\n');
128
-
129
- const plainRows = fields.map(field => {
130
- const val = data[field.name] ?? '';
131
- return `${field.label || field.name}: ${val}`;
132
- }).join('\n');
133
-
134
- const html = `
135
- <!DOCTYPE html>
136
- <html>
137
- <body style="font-family:sans-serif;max-width:640px;margin:0 auto;padding:20px;">
138
- <h2 style="color:#333;margin-bottom:4px;">New Form Submission</h2>
139
- <p style="color:#888;margin-top:0;font-size:.9rem;">${escapeHtml(formTitle)}</p>
140
- <table style="width:100%;border-collapse:collapse;margin-top:16px;">
141
- ${rows}
142
- </table>
143
- </body>
144
- </html>`.trim();
145
-
146
- const text = `New form submission: ${formTitle}\n\n${plainRows}`;
147
-
148
- try {
149
- const info = await transport.sendMail({
150
- from: `"${fromName}" <${from}>`,
151
- to,
152
- subject,
153
- text,
154
- html
155
- });
156
- recordSendResult(true, info.messageId);
157
-
158
- const previewUrl = nodemailer.getTestMessageUrl(info);
159
- if (previewUrl) {
160
- console.log('[email] Preview URL:', previewUrl);
161
- }
162
- return info;
163
- } catch (err) {
164
- recordSendResult(false, err.message);
165
- throw err;
166
- }
167
- }
1
+ /**
2
+ * Core Email Utility
3
+ * Nodemailer transport factory and generic form email sending utility.
4
+ */
5
+ import nodemailer from 'nodemailer';
6
+
7
+ let lastSendResult = null;
8
+
9
+ /**
10
+ * Return the most recent email send result, or null if no send has occurred.
11
+ *
12
+ * @returns {{ ok: boolean, at: string, info: string|null } | null}
13
+ */
14
+ export function getLastSendResult() {
15
+ return lastSendResult;
16
+ }
17
+
18
+ /**
19
+ * Record the outcome of an email send for health reporting.
20
+ *
21
+ * @param {boolean} ok
22
+ * @param {string|null} info
23
+ * @returns {void}
24
+ */
25
+ function recordSendResult(ok, info) {
26
+ lastSendResult = {
27
+ ok,
28
+ at: new Date().toISOString(),
29
+ info: info || null
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Escape HTML special characters for safe use in email bodies.
35
+ *
36
+ * @param {string} str
37
+ * @returns {string}
38
+ */
39
+ export function escapeHtml(str) {
40
+ return String(str)
41
+ .replace(/&/g, '&amp;')
42
+ .replace(/</g, '&lt;')
43
+ .replace(/>/g, '&gt;')
44
+ .replace(/"/g, '&quot;')
45
+ .replace(/'/g, '&#39;');
46
+ }
47
+
48
+ /**
49
+ * Create a nodemailer transport.
50
+ * Falls back to an Ethereal test account when no SMTP host is configured.
51
+ *
52
+ * @param {{ host: string, port: number, secure: boolean, user: string, pass: string }} smtp
53
+ * @returns {Promise<import('nodemailer').Transporter>}
54
+ * @throws {Error} If Ethereal test account creation fails.
55
+ */
56
+ export async function createTransport(smtp) {
57
+ if (smtp?.host) {
58
+ return nodemailer.createTransport({
59
+ host: smtp.host,
60
+ port: smtp.port || 587,
61
+ secure: smtp.secure || false,
62
+ auth: smtp.user ? { user: smtp.user, pass: smtp.pass } : undefined,
63
+ tls: { rejectUnauthorized: false }
64
+ });
65
+ }
66
+
67
+ // No SMTP configured — use Ethereal for dev/demo
68
+ const testAccount = await nodemailer.createTestAccount();
69
+ console.log('[email] No SMTP configured. Using Ethereal test account:', testAccount.user);
70
+ return nodemailer.createTransport({
71
+ host: 'smtp.ethereal.email',
72
+ port: 587,
73
+ secure: false,
74
+ auth: { user: testAccount.user, pass: testAccount.pass }
75
+ });
76
+ }
77
+
78
+ /**
79
+ * Send a generic transactional email.
80
+ *
81
+ * @param {import('nodemailer').Transporter} transport
82
+ * @param {{ from: string, fromName: string, to: string, subject: string, html: string, text: string }} opts
83
+ * @returns {Promise<void>}
84
+ * @throws {Error} If sending the email fails.
85
+ */
86
+ export async function sendEmail(transport, {from, fromName, to, subject, html, text}) {
87
+ try {
88
+ const info = await transport.sendMail({
89
+ from: `"${fromName}" <${from}>`,
90
+ to,
91
+ subject,
92
+ text,
93
+ html
94
+ });
95
+ recordSendResult(true, info.messageId);
96
+
97
+ const previewUrl = nodemailer.getTestMessageUrl(info);
98
+ if (previewUrl) {
99
+ console.log('[email] Preview URL:', previewUrl);
100
+ }
101
+ return info;
102
+ } catch (err) {
103
+ recordSendResult(false, err.message);
104
+ throw err;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Send an HTML + plain-text form submission notification email.
110
+ * Builds a generic table of field→value pairs from the submitted data.
111
+ *
112
+ * @param {import('nodemailer').Transporter} transport
113
+ * @param {{ from: string, fromName: string, to: string, subject: string, formTitle: string, fields: Array<{name: string, label: string}>, data: Record<string, unknown> }} opts
114
+ * @returns {Promise<void>}
115
+ * @throws {Error} If sending the email fails.
116
+ */
117
+ export async function sendFormEmail(transport, { from, fromName, to, subject, formTitle, fields, data }) {
118
+ const rows = fields.map(field => {
119
+ const val = data[field.name] ?? '';
120
+ const safe = escapeHtml(String(val)).replace(/\n/g, '<br>');
121
+ const safeLabel = escapeHtml(field.label || field.name);
122
+ return `
123
+ <tr>
124
+ <td style="padding:8px 12px;font-weight:600;background:#f9f9f9;border:1px solid #eee;white-space:nowrap;vertical-align:top;">${safeLabel}</td>
125
+ <td style="padding:8px 12px;border:1px solid #eee;vertical-align:top;">${safe}</td>
126
+ </tr>`.trim();
127
+ }).join('\n');
128
+
129
+ const plainRows = fields.map(field => {
130
+ const val = data[field.name] ?? '';
131
+ return `${field.label || field.name}: ${val}`;
132
+ }).join('\n');
133
+
134
+ const html = `
135
+ <!DOCTYPE html>
136
+ <html>
137
+ <body style="font-family:sans-serif;max-width:640px;margin:0 auto;padding:20px;">
138
+ <h2 style="color:#333;margin-bottom:4px;">New Form Submission</h2>
139
+ <p style="color:#888;margin-top:0;font-size:.9rem;">${escapeHtml(formTitle)}</p>
140
+ <table style="width:100%;border-collapse:collapse;margin-top:16px;">
141
+ ${rows}
142
+ </table>
143
+ </body>
144
+ </html>`.trim();
145
+
146
+ const text = `New form submission: ${formTitle}\n\n${plainRows}`;
147
+
148
+ try {
149
+ const info = await transport.sendMail({
150
+ from: `"${fromName}" <${from}>`,
151
+ to,
152
+ subject,
153
+ text,
154
+ html
155
+ });
156
+ recordSendResult(true, info.messageId);
157
+
158
+ const previewUrl = nodemailer.getTestMessageUrl(info);
159
+ if (previewUrl) {
160
+ console.log('[email] Preview URL:', previewUrl);
161
+ }
162
+ return info;
163
+ } catch (err) {
164
+ recordSendResult(false, err.message);
165
+ throw err;
166
+ }
167
+ }