domma-cms 1.8.7 → 1.8.8
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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.8.8] - 2026-10-16
|
|
4
|
+
|
|
5
|
+
- **Your emails with a contact, on their card.** With Email (Pro), the foot of a contact's card in Contacts lists your ten newest emails to or from any of the contact's addresses - Received or Sent, unread, and the mailbox when you have several - each opening the message in Email. Only your own mailboxes are searched. This release carries the engine side: a whole-address mail query (`listByAddresses`) and links that open the reader at one message (`#/plugins/<mail>?open=<uid>&account=<id>&folder=<path>`).
|
|
6
|
+
- The response cache's unfinished Redis option is gone. Setting `"driver": "redis"` in `config/cache.json` loaded a file that never existed and stopped the site starting; the in-process memory cache is the one driver (`memory`, or `none`).
|
|
7
|
+
|
|
3
8
|
## [1.8.7] - 2026-10-16
|
|
4
9
|
|
|
5
10
|
- **Import from WordPress brings the authors across.** The preview has a Users card listing everyone the export names as an author, with their email and what they wrote. An author whose email matches a user here (capitals do not matter) is credited with their posts and pages, and that user's account is not changed in any way. Anyone else who wrote something becomes a new account by default; anyone who wrote nothing does not, and each can be switched, or all at once. New accounts get the role you choose - only roles junior to your own are offered (and accepted), the site's least-privileged role by default - and no password: nobody can sign in to one until a password is set. A switch, off unless you turn it on, emails each new account an invitation once the import has finished - an account has been made for them, with a link to set their password that works once and lasts 7 days (ordinary password reset links keep their own, shorter, lifetime). An author with no usable email never gets an account, and two with the same email share one. Blog posts, pages and collection entries are credited to whoever their author became. Run it again and the same people are credited to the same accounts - nobody is made twice. The report and the import history keep names and ids, never the email addresses.
|
|
@@ -65,6 +65,11 @@
|
|
|
65
65
|
it and Admin is given it once. Groups made before 0.93.0 have no recorded creator, so only
|
|
66
66
|
holders of the permission can change them. In Manage groups, Rename and Delete are greyed out
|
|
67
67
|
where you may not, with a note saying why.</p>
|
|
68
|
+
<p><strong>The foot of a contact's card</strong> lists what other Tools hold about them, one
|
|
69
|
+
section per Tool: Bookings, Calls, Events, Quotes and, with <strong>Email</strong> (Pro), your
|
|
70
|
+
ten newest emails to or from any of the contact's addresses. Each entry opens where it lives -
|
|
71
|
+
an email opens in Email. Only your own mailboxes are searched. A Tool that is not installed
|
|
72
|
+
adds no section.</p>
|
|
68
73
|
|
|
69
74
|
<h3>Call someone now (Pro)</h3>
|
|
70
75
|
<p>With a Pro Tool that has calls - <strong>Booking Pro</strong>, <strong>Contacts Pro</strong> or
|
package/docs/DommaFeatures.md
CHANGED
|
@@ -41,7 +41,7 @@ Code was treated as the truth. Where docs disagree with it, the disagreement is
|
|
|
41
41
|
| `data-transfer` | paid | 1.3.3 | Move collections between file and MongoDB storage, back them up and restore them (all or some, merged or replaced) - keeping every entry's id - and clone them (cloned entries get new ids, so references stay unambiguous). Every restore and transfer takes a safety snapshot first. |
|
|
42
42
|
| `docs` | paid | 2.8.1 | Document editor: Home and editor screens, sharing with Viewers/Editors, public links, import/export, send to page/blog/email, edit lock, Trash, version history, templates. |
|
|
43
43
|
| `docs-pro` | paid | 0.8.1 | Domma Spaces (publish a Docs folder at `/spaces` with a Kindle-style reader), Cloud copies (Google Drive, OneDrive, Dropbox, S3), Signatures (emailed signing links, typed or drawn, signed copy with a certificate page) and protected public links (expiry date, password). Adds to Docs. |
|
|
44
|
-
| `email-pro` | paid | 0.
|
|
44
|
+
| `email-pro` | paid | 0.9.0 | Full mail: read, write, send and search, over a synchronised store. Search, threads, rules, scheduled sends, snooze and vacation replies need MongoDB; without it, reading, filing and sending still work. |
|
|
45
45
|
| `feedback` | free | 1.1.4 | Tell Domma what is broken, missing or good - straight from your admin - and see what became of it: seen, planned, in progress, done, with Domma's reply. |
|
|
46
46
|
| `garage` | paid | 1.4.3 | UK vehicle management with DVLA API lookup, save vehicles, and search history. |
|
|
47
47
|
| `invoice` | paid | 1.7.1 | Invoices and quotes. Issuer profiles, receivers importable from Contacts, line items, optional VAT, payments, archiving, CSV/Excel export, and a print-ready invoice you can email or download as a PDF. Quotes with a validity date and an emailed accept link - the client accepts or declines online, and one click turns an accepted quote into a draft invoice. Self-billing too: invoices raised on a supplier's behalf, marked and numbered their own way, with agreement review reminders. |
|
|
@@ -1144,7 +1144,7 @@ The four Tools below were plugins until 0.67.0 and are now part of the CMS. They
|
|
|
1144
1144
|
Since: 0.72.0.
|
|
1145
1145
|
- **Calendar sources and related items** - Two registries let plugins build on the core Tools without core depending on them:
|
|
1146
1146
|
- `registerCalendarSource` - Todo supplies a "To-dos" layer of your open to-dos on their due days.
|
|
1147
|
-
- `registerRelatedItems` - lets a plugin such as Calendar show "Events" beside a contact or note and add "Schedule with..." and "Add to calendar..." menu items.
|
|
1147
|
+
- `registerRelatedItems` - lets a plugin such as Calendar show "Events" beside a contact or note and add "Schedule with..." and "Add to calendar..." menu items. Email Pro 0.9 adds "Emails" to the contact card: the viewer's own newest messages with any of the contact's addresses, each opening in Email (engine `store.listByAddresses()`, reader links `#/plugins/<name>?open=<uid>&account=<id>&folder=<path>`; since 1.8.8).
|
|
1148
1148
|
|
|
1149
1149
|
Before any provider is asked, core checks that the viewer owns the record. A provider that fails or is slow is named and left out.
|
|
1150
1150
|
Where: `server/services/hooks.js`, `GET /api/related`, `GET /api/related/providers`, `admin/js/lib/related.js`; `contact:deleted` and `note:deleted` events - Since: 0.79.0.
|
|
@@ -1902,7 +1902,7 @@ Tier: paid (closed source) · Version: 0.8.0 · Requires: `docs` 2.6+ (2.8+ for
|
|
|
1902
1902
|
|
|
1903
1903
|
### Email (`email-pro`)
|
|
1904
1904
|
|
|
1905
|
-
Tier: paid (closed source) · Version: 0.
|
|
1905
|
+
Tier: paid (closed source) · Version: 0.9.0 · Requires: MongoDB for the message store's extras (without it reading, starring, moving, deleting, folders and sending still work; search, conversations, rules, undo send, send later, snooze and the away message need MongoDB and say so); npm `imapflow` ^2.0.5, `mailparser` ^3.9.28, `nodemailer` 8.0.5; no `minCmsVersion` declared (Contacts switch-off handling core 0.88) · Uses: `contacts` (recipient suggestions and group expansion in compose; Add sender to contacts) · Supersedes: `mail-reader`
|
|
1906
1906
|
|
|
1907
1907
|
A full email client in the admin: connect your own IMAP mailboxes, read, write, send, search and file mail. Chosen folders are mirrored (envelopes and flags, never bodies) into MongoDB so big mailboxes open instantly, and on top come identities, templates, rules, an away responder, undo send, send later, snooze, threads and one-click hand-off of messages into the CMS.
|
|
1908
1908
|
|
|
@@ -1921,6 +1921,7 @@ A full email client in the admin: connect your own IMAP mailboxes, read, write,
|
|
|
1921
1921
|
- Away (vacation) responder with a preview that runs the real decision over the mailbox and explains each message (skips lists, bounces, repeat senders).
|
|
1922
1922
|
- Unsubscribe: RFC 8058 one-click performed server-side; other methods handed back to open or compose.
|
|
1923
1923
|
- Hand-off to the CMS: save an attachment to Media, turn a message into a draft page, file a message as a collection entry; Add sender to contacts.
|
|
1924
|
+
- Emails on a contact's card (0.9.0, engine 1.8.8+): the contact slideover in Contacts (and Contacts Pro) lists your ten newest emails to or from the contact's main, billing and additional addresses - Received/Sent, unread, and the mailbox when you have several - each opening the message in Email. Your own mailboxes and mirrored folders only; needs MongoDB, and is absent without it.
|
|
1924
1925
|
- Keyboard shortcuts: j/k or arrows to move, Enter open, u read/unread, s star, r reply, Shift+R or a reply all, f forward, c compose, `/` search, x select, Shift+A select all, `!` spam, `#` or Delete delete, Esc, `?` help.
|
|
1925
1926
|
- Sidebar badge (`/unread`): unread in Inbox (counted from the store).
|
|
1926
1927
|
- Notifications to the mailbox owner only: `email-pro:mailbox` (warning after 5 failed checks in a row; resolved when a check works) and `email-pro:sending` (a scheduled/undo-send message that failed).
|
package/package.json
CHANGED
|
@@ -6126,6 +6126,38 @@ return {
|
|
|
6126
6126
|
await startNewMessage(draft);
|
|
6127
6127
|
}
|
|
6128
6128
|
|
|
6129
|
+
/**
|
|
6130
|
+
* Another screen linked to one message - a contact's Emails list
|
|
6131
|
+
* (Email Pro's related items): `#/plugins/<name>?open=<uid>&account=
|
|
6132
|
+
* <id>&folder=<path>`. Opens that mailbox, that folder and the
|
|
6133
|
+
* message, then takes the request off the address so a reload or Back
|
|
6134
|
+
* does not open it again. A mailbox that is not the viewer's is never
|
|
6135
|
+
* opened: the accounts list is theirs alone.
|
|
6136
|
+
*/
|
|
6137
|
+
async function takeOpenRequest() {
|
|
6138
|
+
const params = new URLSearchParams(location.hash.split('?')[1] ?? '');
|
|
6139
|
+
if (!params.has('open')) return;
|
|
6140
|
+
history.replaceState(history.state, '', `${location.pathname}${location.search}#/plugins/${pluginName}`);
|
|
6141
|
+
const uid = Number(params.get('open'));
|
|
6142
|
+
const path = params.get('folder');
|
|
6143
|
+
const account = params.get('account');
|
|
6144
|
+
if (!Number.isInteger(uid) || uid <= 0 || !path) return;
|
|
6145
|
+
if (account && account !== currentAccountId) {
|
|
6146
|
+
if (!mailboxes.some(b => b.id === account)) {
|
|
6147
|
+
E.toast('That message is in a mailbox that is not yours.', {type: 'warning'});
|
|
6148
|
+
return;
|
|
6149
|
+
}
|
|
6150
|
+
await selectAccount(account);
|
|
6151
|
+
if (!here()) return;
|
|
6152
|
+
}
|
|
6153
|
+
const folder = folders.find(f => f.path === path);
|
|
6154
|
+
if (folder && folder.path !== currentFolder) {
|
|
6155
|
+
await selectFolder(folder);
|
|
6156
|
+
if (!here()) return;
|
|
6157
|
+
}
|
|
6158
|
+
await openMessage(uid, path);
|
|
6159
|
+
}
|
|
6160
|
+
|
|
6129
6161
|
if (features.send) {
|
|
6130
6162
|
const compose = $container.find('#mail-compose-btn').get(0);
|
|
6131
6163
|
if (compose && bannerCompose) compose.remove();
|
|
@@ -6177,6 +6209,8 @@ return {
|
|
|
6177
6209
|
|
|
6178
6210
|
await start();
|
|
6179
6211
|
if (!here()) return;
|
|
6212
|
+
await takeOpenRequest();
|
|
6213
|
+
if (!here()) return;
|
|
6180
6214
|
await takeComposeRequest();
|
|
6181
6215
|
// The sidebar updates independently; keep an open inbox in step with
|
|
6182
6216
|
// the mirror without interrupting selection, search or older pages.
|
|
@@ -147,6 +147,32 @@ export function messageId(accountId, path, uid) {
|
|
|
147
147
|
return `${accountId}:${path}:${uid}`;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* The Mongo filter for "mail to or from any of these people".
|
|
152
|
+
*
|
|
153
|
+
* Whole-address and case-insensitive: Sam@ and sam@ are one person, but
|
|
154
|
+
* sam@example.com must not match pam.sam@example.com - so each address is an
|
|
155
|
+
* anchored, escaped pattern rather than the substring the search box uses.
|
|
156
|
+
* Blank and duplicate addresses are dropped; none left = null, and the
|
|
157
|
+
* caller asks nothing.
|
|
158
|
+
*
|
|
159
|
+
* @param {string[]} accountIds
|
|
160
|
+
* @param {string[]} addresses
|
|
161
|
+
* @returns {object|null}
|
|
162
|
+
*/
|
|
163
|
+
export function addressFilter(accountIds, addresses) {
|
|
164
|
+
const ids = [...new Set((accountIds ?? []).filter(id => typeof id === 'string' && id))];
|
|
165
|
+
const wanted = [...new Set((addresses ?? [])
|
|
166
|
+
.map(a => String(a ?? '').trim().toLowerCase())
|
|
167
|
+
.filter(a => a.includes('@')))];
|
|
168
|
+
if (!ids.length || !wanted.length) return null;
|
|
169
|
+
const patterns = wanted.map(a => new RegExp(`^${a.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'i'));
|
|
170
|
+
return {
|
|
171
|
+
accountId: {$in: ids},
|
|
172
|
+
$or: ['from', 'to', 'cc'].map(field => ({[`${field}.address`]: {$in: patterns}}))
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
150
176
|
/**
|
|
151
177
|
* Create a store bound to one connection.
|
|
152
178
|
*
|
|
@@ -800,6 +826,38 @@ export function createStore(connection) {
|
|
|
800
826
|
.toArray();
|
|
801
827
|
}
|
|
802
828
|
|
|
829
|
+
/**
|
|
830
|
+
* The newest mirrored messages to or from any of these addresses, across
|
|
831
|
+
* the given mailboxes - a contact's emails, for the card in Contacts.
|
|
832
|
+
* Headers only; never a body. The same message filed in two folders (or
|
|
833
|
+
* sent to yourself) appears once, by its Message-ID.
|
|
834
|
+
*
|
|
835
|
+
* @param {string[]} accountIds
|
|
836
|
+
* @param {string[]} addresses
|
|
837
|
+
* @param {{limit?: number}} [options]
|
|
838
|
+
* @returns {Promise<{accountId: string, folder: string, uid: number, subject: string, from: object[], to: object[], date: string|null, seen: boolean}[]>}
|
|
839
|
+
*/
|
|
840
|
+
async function listByAddresses(accountIds, addresses, {limit = 10} = {}) {
|
|
841
|
+
const filter = addressFilter(accountIds, addresses);
|
|
842
|
+
if (!filter) return [];
|
|
843
|
+
const database = await connect();
|
|
844
|
+
const rows = await database.collection(MESSAGES)
|
|
845
|
+
.find(filter, {projection: {_id: 0, accountId: 1, folder: 1, uid: 1, subject: 1, from: 1, to: 1, date: 1, seen: 1, messageId: 1}})
|
|
846
|
+
.sort({date: -1, uid: -1})
|
|
847
|
+
.limit(limit * 2)
|
|
848
|
+
.toArray();
|
|
849
|
+
const seen = new Set();
|
|
850
|
+
const out = [];
|
|
851
|
+
for (const row of rows) {
|
|
852
|
+
const key = row.messageId || `${row.accountId}:${row.folder}:${row.uid}`;
|
|
853
|
+
if (seen.has(key)) continue;
|
|
854
|
+
seen.add(key);
|
|
855
|
+
out.push(row);
|
|
856
|
+
if (out.length >= limit) break;
|
|
857
|
+
}
|
|
858
|
+
return out;
|
|
859
|
+
}
|
|
860
|
+
|
|
803
861
|
/** Folders already backfilled in this process, so it runs at most once each. */
|
|
804
862
|
const backfilled = new Set();
|
|
805
863
|
|
|
@@ -1023,7 +1081,7 @@ export function createStore(connection) {
|
|
|
1023
1081
|
connect,
|
|
1024
1082
|
getFolder, saveFolder,
|
|
1025
1083
|
putMessages, setFlags, setMessageAction, removeMessages, clearFolder,
|
|
1026
|
-
storedUids, listMessages, listThreads, searchMessages, countUnread, listUnread, forgetAccount,
|
|
1084
|
+
storedUids, listMessages, listThreads, searchMessages, countUnread, listUnread, listByAddresses, forgetAccount,
|
|
1027
1085
|
queueSend, listQueuedSends, claimDueSends, finishSend, cancelSend, getQueuedSend,
|
|
1028
1086
|
queueSnooze, listSnoozed, claimDueSnoozes, finishSnooze, wakeSnooze,
|
|
1029
1087
|
lastAutoReply, noteAutoReply, listAutoReplies, clearAutoReplies,
|