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.
Files changed (82) hide show
  1. package/admin/dist/domma/domma-tools.css +3 -3
  2. package/admin/dist/domma/domma-tools.min.js +3 -3
  3. package/package.json +2 -2
  4. package/plugins/_lib/admin/mail/mail.css +1 -1
  5. package/plugins/_lib/admin/mail/oauth-panel.js +303 -0
  6. package/plugins/_lib/admin/mail/reader-view.js +126 -13
  7. package/plugins/_lib/mail/accounts.js +548 -225
  8. package/plugins/_lib/mail/imapPool.js +63 -13
  9. package/plugins/_lib/mail/oauth.js +453 -0
  10. package/plugins/_lib/mail/oauthRoutes.js +294 -0
  11. package/plugins/_lib/mail/readRoutes.js +23 -2
  12. package/plugins/_lib/mail/scheduler.js +30 -6
  13. package/plugins/blog/CLAUDE.md +11 -1
  14. package/plugins/blog/lib/cache.js +43 -0
  15. package/plugins/blog/lib/page.js +28 -9
  16. package/plugins/blog/plugin.js +14 -13
  17. package/plugins/blog/plugin.json +1 -1
  18. package/plugins/blog/plugin.public.js +1 -1
  19. package/plugins/blog/tests/cache.test.js +65 -0
  20. package/plugins/booking/CLAUDE.md +14 -2
  21. package/plugins/booking/admin/lib/diary-layout.js +18 -0
  22. package/plugins/booking/admin/views/diary.js +153 -152
  23. package/plugins/booking/admin/views/people.js +64 -66
  24. package/plugins/booking/admin/views/settings.js +31 -32
  25. package/plugins/booking/plugin.json +1 -1
  26. package/plugins/booking/tests/admin-diary.test.js +9 -1
  27. package/plugins/feedback/CLAUDE.md +62 -0
  28. package/plugins/feedback/admin/css/index.css +16 -1
  29. package/plugins/feedback/admin/lib/kit.js +106 -0
  30. package/plugins/feedback/admin/lib/settings.js +107 -0
  31. package/plugins/feedback/admin/views/feedback.js +4 -1
  32. package/plugins/feedback/admin/views/inbox.js +11 -2
  33. package/plugins/feedback/admin/views/send.js +74 -7
  34. package/plugins/feedback/docs/guide.md +9 -9
  35. package/plugins/feedback/lib/attachments.js +133 -0
  36. package/plugins/feedback/lib/cache.js +43 -0
  37. package/plugins/feedback/lib/receiver.js +36 -6
  38. package/plugins/feedback/lib/sender.js +89 -6
  39. package/plugins/feedback/lib/shape.js +111 -6
  40. package/plugins/feedback/plugin.js +173 -17
  41. package/plugins/feedback/plugin.json +2 -2
  42. package/plugins/feedback/tests/api.test.js +125 -3
  43. package/plugins/feedback/tests/settings.test.js +59 -0
  44. package/plugins/feedback/tests/shape.test.js +87 -1
  45. package/plugins/free-tier.lock.json +74 -62
  46. package/plugins/mail-reader/CLAUDE.md +15 -4
  47. package/plugins/mail-reader/plugin.json +1 -1
  48. package/plugins/shopping-cart/CLAUDE.md +35 -2
  49. package/plugins/shopping-cart/admin/css/index.css +3 -0
  50. package/plugins/shopping-cart/admin/views/orders.js +75 -23
  51. package/plugins/shopping-cart/admin/views/shop.js +1 -1
  52. package/plugins/shopping-cart/docs/guide.md +7 -3
  53. package/plugins/shopping-cart/lib/payments.js +15 -2
  54. package/plugins/shopping-cart/lib/refunds.js +51 -0
  55. package/plugins/shopping-cart/lib/sales.js +15 -8
  56. package/plugins/shopping-cart/lib/shop.js +38 -3
  57. package/plugins/shopping-cart/plugin.js +47 -19
  58. package/plugins/shopping-cart/plugin.json +2 -2
  59. package/plugins/shopping-cart/tests/api.test.js +52 -0
  60. package/plugins/shopping-cart/tests/commission.test.js +1 -1
  61. package/plugins/shopping-cart/tests/pure.test.js +16 -0
  62. package/plugins/shopping-cart/tests/refunds.test.js +42 -0
  63. package/plugins/support/CLAUDE.md +58 -0
  64. package/plugins/support/admin/css/index.css +16 -1
  65. package/plugins/support/admin/lib/kit.js +192 -8
  66. package/plugins/support/admin/lib/settings.js +123 -0
  67. package/plugins/support/admin/views/desk.js +55 -12
  68. package/plugins/support/admin/views/inbox.js +17 -4
  69. package/plugins/support/admin/views/support.js +4 -1
  70. package/plugins/support/docs/guide.md +7 -9
  71. package/plugins/support/lib/attachments.js +133 -0
  72. package/plugins/support/lib/cache.js +43 -0
  73. package/plugins/support/lib/receiver.js +45 -10
  74. package/plugins/support/lib/sender.js +96 -10
  75. package/plugins/support/lib/shape.js +153 -12
  76. package/plugins/support/plugin.js +159 -29
  77. package/plugins/support/plugin.json +2 -2
  78. package/plugins/support/tests/api.test.js +125 -3
  79. package/plugins/support/tests/settings.test.js +68 -0
  80. package/plugins/support/tests/shape.test.js +76 -2
  81. package/server/routes/outreach-public.js +2 -2
  82. 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 survey emails from this site.</p>`
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 survey emails from this site.</p>`));
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
- const SENSITIVE = new Set(['token', 't', 'key', 'hostkey', 'invitekey']);
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;