domma-cms 1.6.37 → 1.6.39
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/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/package.json +2 -2
- package/plugins/_lib/admin/mail/mail.css +1 -1
- package/plugins/_lib/admin/mail/oauth-panel.js +303 -0
- package/plugins/_lib/admin/mail/reader-view.js +126 -13
- package/plugins/_lib/mail/accounts.js +548 -225
- package/plugins/_lib/mail/imapPool.js +63 -13
- package/plugins/_lib/mail/oauth.js +453 -0
- package/plugins/_lib/mail/oauthRoutes.js +294 -0
- package/plugins/_lib/mail/readRoutes.js +23 -2
- package/plugins/_lib/mail/scheduler.js +30 -6
- package/plugins/blog/CLAUDE.md +11 -1
- package/plugins/blog/lib/cache.js +43 -0
- package/plugins/blog/lib/page.js +28 -9
- package/plugins/blog/plugin.js +14 -13
- package/plugins/blog/plugin.json +1 -1
- package/plugins/blog/plugin.public.js +1 -1
- package/plugins/blog/tests/cache.test.js +65 -0
- package/plugins/booking/CLAUDE.md +14 -2
- package/plugins/booking/admin/lib/diary-layout.js +18 -0
- package/plugins/booking/admin/views/diary.js +153 -152
- package/plugins/booking/admin/views/people.js +64 -66
- package/plugins/booking/admin/views/settings.js +31 -32
- package/plugins/booking/plugin.json +1 -1
- package/plugins/booking/tests/admin-diary.test.js +9 -1
- package/plugins/feedback/CLAUDE.md +62 -0
- package/plugins/feedback/admin/css/index.css +16 -1
- package/plugins/feedback/admin/lib/kit.js +106 -0
- package/plugins/feedback/admin/lib/settings.js +107 -0
- package/plugins/feedback/admin/views/feedback.js +4 -1
- package/plugins/feedback/admin/views/inbox.js +11 -2
- package/plugins/feedback/admin/views/send.js +74 -7
- package/plugins/feedback/docs/guide.md +9 -9
- package/plugins/feedback/lib/attachments.js +133 -0
- package/plugins/feedback/lib/cache.js +43 -0
- package/plugins/feedback/lib/receiver.js +36 -6
- package/plugins/feedback/lib/sender.js +89 -6
- package/plugins/feedback/lib/shape.js +111 -6
- package/plugins/feedback/plugin.js +173 -17
- package/plugins/feedback/plugin.json +2 -2
- package/plugins/feedback/tests/api.test.js +125 -3
- package/plugins/feedback/tests/settings.test.js +59 -0
- package/plugins/feedback/tests/shape.test.js +87 -1
- package/plugins/free-tier.lock.json +74 -62
- package/plugins/mail-reader/CLAUDE.md +15 -4
- package/plugins/mail-reader/plugin.json +1 -1
- package/plugins/shopping-cart/CLAUDE.md +35 -2
- package/plugins/shopping-cart/admin/css/index.css +3 -0
- package/plugins/shopping-cart/admin/views/orders.js +75 -23
- package/plugins/shopping-cart/admin/views/shop.js +1 -1
- package/plugins/shopping-cart/docs/guide.md +7 -3
- package/plugins/shopping-cart/lib/payments.js +15 -2
- package/plugins/shopping-cart/lib/refunds.js +51 -0
- package/plugins/shopping-cart/lib/sales.js +15 -8
- package/plugins/shopping-cart/lib/shop.js +38 -3
- package/plugins/shopping-cart/plugin.js +47 -19
- package/plugins/shopping-cart/plugin.json +2 -2
- package/plugins/shopping-cart/tests/api.test.js +52 -0
- package/plugins/shopping-cart/tests/commission.test.js +1 -1
- package/plugins/shopping-cart/tests/pure.test.js +16 -0
- package/plugins/shopping-cart/tests/refunds.test.js +42 -0
- package/plugins/support/CLAUDE.md +58 -0
- package/plugins/support/admin/css/index.css +16 -1
- package/plugins/support/admin/lib/kit.js +192 -8
- package/plugins/support/admin/lib/settings.js +123 -0
- package/plugins/support/admin/views/desk.js +55 -12
- package/plugins/support/admin/views/inbox.js +17 -4
- package/plugins/support/admin/views/support.js +4 -1
- package/plugins/support/docs/guide.md +7 -9
- package/plugins/support/lib/attachments.js +133 -0
- package/plugins/support/lib/cache.js +43 -0
- package/plugins/support/lib/receiver.js +45 -10
- package/plugins/support/lib/sender.js +96 -10
- package/plugins/support/lib/shape.js +153 -12
- package/plugins/support/plugin.js +159 -29
- package/plugins/support/plugin.json +2 -2
- package/plugins/support/tests/api.test.js +125 -3
- package/plugins/support/tests/settings.test.js +68 -0
- package/plugins/support/tests/shape.test.js +76 -2
- package/server/routes/outreach-public.js +2 -2
- package/server/services/request-log.js +3 -1
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
addTurn, buildMessagePayload, buildTicketPayload, cleanRequest, cleanUpdate, contactCard, createThrottle, lastDommaAt,
|
|
6
6
|
receivedTicket, senderView, telHref, toCustomerEmail, toDommaEmail
|
|
7
7
|
} from '../lib/shape.js';
|
|
8
|
-
import {applyShelf, createOutbox, flush, mergeTickets} from '../lib/sender.js';
|
|
8
|
+
import {applyShelf, createOutbox, flush, keepFiles, mergeTickets} from '../lib/sender.js';
|
|
9
9
|
import fs from 'node:fs';
|
|
10
10
|
import os from 'node:os';
|
|
11
11
|
import path from 'node:path';
|
|
@@ -18,7 +18,7 @@ test('a request needs a subject and a message; unknown choices fall back', () =>
|
|
|
18
18
|
assert.equal(cleanRequest({body: 'x'}).error, 'Give it a short subject.');
|
|
19
19
|
assert.equal(cleanRequest({subject: 'x'}).error, 'Tell us what you need.');
|
|
20
20
|
const {request} = cleanRequest({subject: ' Site\n down ', body: ' help\r\nplease ', category: 'nope', priority: 'panic', where: '/shop'});
|
|
21
|
-
assert.deepEqual(request, {category: 'help', priority: 'normal', subject: 'Site down', body: 'help\nplease', where: '/shop'});
|
|
21
|
+
assert.deepEqual(request, {category: 'help', priority: 'normal', subject: 'Site down', body: 'help\nplease', where: '/shop', attachments: []});
|
|
22
22
|
assert.equal(cleanRequest({subject: 's', body: 'b', priority: 'urgent'}).request.priority, 'urgent');
|
|
23
23
|
});
|
|
24
24
|
|
|
@@ -187,3 +187,77 @@ test('review: a request removed while a send is in flight is not half-deleted, a
|
|
|
187
187
|
assert.ok(!posted.includes('T3') && !posted.includes('M3'));
|
|
188
188
|
} finally { fs.rmSync(dir, {recursive: true, force: true}); }
|
|
189
189
|
});
|
|
190
|
+
|
|
191
|
+
// ---- Attachments (1.1.4) ----
|
|
192
|
+
|
|
193
|
+
import {ATTACHMENTS, attachmentsOf, cleanAttachment, cleanAttachments, cleanMessage} from '../lib/shape.js';
|
|
194
|
+
|
|
195
|
+
const FID = '0f8fad5b-d9cb-469f-a165-70867728950e';
|
|
196
|
+
const file = (n = 1, extra = {}) => ({id: `${FID.slice(0, -1)}${n}`, name: `shot${n}.png`, type: 'image/png', size: 10, ...extra});
|
|
197
|
+
|
|
198
|
+
test('an attachment: a known type, a server-made id, a size within the cap', () => {
|
|
199
|
+
assert.deepEqual(cleanAttachment(file(1, {junk: 1})), {id: file(1).id, name: 'shot1.png', type: 'image/png', size: 10});
|
|
200
|
+
assert.equal(cleanAttachment(file(1, {type: 'image/svg+xml'})), null, 'SVG can carry script');
|
|
201
|
+
assert.equal(cleanAttachment(file(1, {type: 'text/html'})), null);
|
|
202
|
+
assert.equal(cleanAttachment(file(1, {id: '../../x'})), null);
|
|
203
|
+
assert.equal(cleanAttachment(file(1, {size: ATTACHMENTS.maxBytes + 1})), null);
|
|
204
|
+
assert.equal(cleanAttachment(file(1, {name: '/etc/"passwd'})).name, '_passwd');
|
|
205
|
+
assert.equal(cleanAttachments([file(1), file(2), file(3), file(4), file(1)]).length, 3, 'three at most, each once');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('files ride on a message: announced not stored, carried by a follow-up, shown to the site', () => {
|
|
209
|
+
const {request} = cleanRequest({subject: 's', body: 'b', attachments: [file(1), {id: 'nope'}]});
|
|
210
|
+
assert.equal(request.attachments.length, 1);
|
|
211
|
+
const {data} = receivedTicket(buildTicketPayload({request, user: {name: 'Ann'}, ref: 'r1'}), 'acme', T0);
|
|
212
|
+
assert.equal(data.messages[0].attachments[0].stored, false);
|
|
213
|
+
assert.equal(cleanMessage({body: '', attachments: [file(2)]}).error, 'Write a message first.', 'files go with words');
|
|
214
|
+
const msg = buildMessagePayload({ticketRef: 'r1', ref: 'm1', body: 'and this', attachments: [file(2)]});
|
|
215
|
+
assert.equal(msg.attachments.length, 1);
|
|
216
|
+
assert.equal(buildMessagePayload({ticketRef: 'r1', ref: 'm2', close: true, attachments: [file(3)]}).attachments, undefined, 'a close carries none');
|
|
217
|
+
const next = addTurn(data, {from: 'customer', name: 'Ann', body: 'and this', id: 'm1', at: T1, attachments: msg.attachments}).data;
|
|
218
|
+
assert.deepEqual(attachmentsOf(next).map(a => [a.name, a.messageId, a.stored]), [['shot1.png', 'r1', false], ['shot2.png', 'm1', false]]);
|
|
219
|
+
const view = senderView({id: 'e1', data: next});
|
|
220
|
+
assert.deepEqual(view.messages[1].attachments, [{id: file(2).id, name: 'shot2.png', type: 'image/png', size: 10, stored: false}]);
|
|
221
|
+
assert.equal(view.messages[0].attachments[0].messageId, undefined);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('a sent message stays in the outbox until its files have gone', () => {
|
|
225
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sp-files-'));
|
|
226
|
+
try {
|
|
227
|
+
const ob = createOutbox(dir);
|
|
228
|
+
ob.add('ticket', {ref: 'r1', subject: 's', body: 'b', attachments: [file(1)]});
|
|
229
|
+
ob.mark('r1', {state: 'sent'});
|
|
230
|
+
const remote = [{ref: 'r1', subject: 's', status: 'open', messages: [{id: 'r1', from: 'customer', body: 'b', at: T0}]}];
|
|
231
|
+
ob.remember(remote);
|
|
232
|
+
assert.equal(ob.all().length, 1, 'kept: its file has not gone');
|
|
233
|
+
assert.ok(ob.fileIds().has(file(1).id));
|
|
234
|
+
ob.fileSent('r1', file(1).id);
|
|
235
|
+
ob.remember(remote);
|
|
236
|
+
assert.equal(ob.all().length, 0, 'now done with');
|
|
237
|
+
assert.ok(ob.fileIds().has(file(1).id), 'the file itself is still wanted: the thread shows it');
|
|
238
|
+
} finally { fs.rmSync(dir, {recursive: true, force: true}); }
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('files kept for the thread go when the manager no longer has their request', () => {
|
|
242
|
+
const remote = [{ref: 't1', messages: [{id: 'm1', attachments: [{id: 'f1'}]}, {id: 'm2'}]}];
|
|
243
|
+
const items = [{ref: 'm9', payload: {attachments: [{id: 'f3'}]}}];
|
|
244
|
+
assert.deepEqual(keepFiles(['f1', 'f2', 'f3'], remote, items), ['f1', 'f3'], 'f2 was on a request deleted at the manager');
|
|
245
|
+
assert.deepEqual(keepFiles(['f1'], [], []), []);
|
|
246
|
+
assert.deepEqual(keepFiles(undefined, remote), []);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('remember(): a deleted request takes its files out of the keep list', () => {
|
|
250
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sup-outbox-'));
|
|
251
|
+
try {
|
|
252
|
+
const box = createOutbox(dir);
|
|
253
|
+
box.add('ticket', {ref: 't1', attachments: [{id: 'f1'}]});
|
|
254
|
+
box.mark('t1', {state: 'sent'});
|
|
255
|
+
box.fileSent('t1', 'f1');
|
|
256
|
+
box.remember([{ref: 't1', messages: [{id: 't1', attachments: [{id: 'f1'}]}]}], null);
|
|
257
|
+
assert.ok(box.fileIds().has('f1'), 'still on the manager: kept');
|
|
258
|
+
box.remember([], null);
|
|
259
|
+
assert.equal(box.fileIds().has('f1'), false, 'deleted on the manager: no longer kept');
|
|
260
|
+
} finally {
|
|
261
|
+
fs.rmSync(dir, {recursive: true, force: true});
|
|
262
|
+
}
|
|
263
|
+
});
|
|
@@ -40,7 +40,7 @@ const badLink = () => page('Unsubscribe', '<h1>That link is not right.</h1><p>Us
|
|
|
40
40
|
|
|
41
41
|
/** The confirm page: the same e and k, posted back by one button. */
|
|
42
42
|
function confirmPage(email, e, k) {
|
|
43
|
-
return page('Unsubscribe', `<h1>Unsubscribe?</h1><p>${esc(email)} will get no more campaign or
|
|
43
|
+
return page('Unsubscribe', `<h1>Unsubscribe?</h1><p>${esc(email)} will get no more campaign, survey or onboarding emails from this site.</p>`
|
|
44
44
|
+ `<form method="post" action="${UNSUBSCRIBE_PATH}"><input type="hidden" name="e" value="${esc(e)}"><input type="hidden" name="k" value="${esc(k)}">`
|
|
45
45
|
+ `<button type="submit" class="btn btn-primary">Unsubscribe</button></form>`);
|
|
46
46
|
}
|
|
@@ -107,6 +107,6 @@ export async function outreachPublicRoutes(fastify, opts = {}) {
|
|
|
107
107
|
if (!opts.col) await ensureList();
|
|
108
108
|
await suppression.add(email, 'unsubscribe-link');
|
|
109
109
|
return html(reply).send(page('Unsubscribed',
|
|
110
|
-
`<h1>You are unsubscribed.</h1><p>${esc(email)} will get no more campaign or
|
|
110
|
+
`<h1>You are unsubscribed.</h1><p>${esc(email)} will get no more campaign, survey or onboarding emails from this site.</p>`));
|
|
111
111
|
});
|
|
112
112
|
}
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/** Parameter names redacted whatever their case. */
|
|
15
|
-
|
|
15
|
+
// `code` and `state`: an OAuth2 callback (the mail sign-in) carries a live
|
|
16
|
+
// authorisation code and its one-time ticket in the query string.
|
|
17
|
+
const SENSITIVE = new Set(['token', 't', 'key', 'hostkey', 'invitekey', 'code', 'state']);
|
|
16
18
|
|
|
17
19
|
/** Anything with secret or password in the name is redacted too. */
|
|
18
20
|
const SENSITIVE_PATTERN = /secret|password|passwd/i;
|