domma-cms 1.10.36 → 1.10.38
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 +10 -0
- package/admin/js/lib/mail-compose.js +1 -1
- package/admin/js/lib/sidebar-notify.js +1 -0
- package/admin/js/lib/sidebar-renderer.js +5 -5
- package/bin/lib/plugin-backup.js +40 -0
- package/bin/update.js +6 -2
- package/package.json +2 -1
- package/plugins/_lib/admin/mail/compose-window.js +138 -20
- package/plugins/_lib/admin/mail/mail.css +1 -1
- package/plugins/_lib/admin/mail/reader-view.js +38 -2
- package/plugins/_lib/admin/mail/security-note.js +68 -0
- package/plugins/_lib/admin/mail/settings-section.js +205 -18
- package/plugins/_lib/mail/accounts.js +59 -0
- package/plugins/_lib/mail/compose.js +15 -1
- package/plugins/_lib/mail/message-action.js +32 -0
- package/plugins/_lib/mail/messageSecurity.js +111 -0
- package/plugins/_lib/mail/readRoutes.js +36 -6
- package/plugins/_lib/mail/send.js +105 -23
- package/plugins/_lib/mail/store.js +23 -1
- package/plugins/_lib/mail/unreadBadge.js +24 -1
- package/plugins/_lib/mail/vacationRunner.js +4 -1
- package/server/routes/api/sidebar-counts.js +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
#view-container.mail-fullbleed{max-width:none}.mail-layout [hidden],.dm-slideover [hidden]{display:none!important}.mail-reader-summary{display:flex;align-items:baseline;gap:.5rem;flex-wrap:wrap}.mail-header-toggle,.mail-extras-toggle{padding:0!important;font-size:.78rem;opacity:.75;text-decoration:underline}.mail-header-toggle:hover,.mail-extras-toggle:hover{opacity:1}.mail-header-details{display:grid;grid-template-columns:max-content 1fr;gap:.15rem .75rem;margin:.5rem 0 0;font-size:.8rem}.mail-header-details dt{opacity:.65}.mail-header-details dd{margin:0;overflow-wrap:anywhere}.mail-layout{display:grid;grid-template-columns:var(--mail-col-1, 190px) auto var(--mail-col-2, 340px) auto 1fr;height:calc(100vh - 140px);min-height:320px;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md, 8px);overflow:hidden;background:var(--dm-surface)}.mail-folders-pane,.mail-list-pane{display:flex;flex-direction:column;border-right:1px solid var(--dm-border);min-width:0;min-height:0}.mail-reader-pane{display:flex;flex-direction:column;min-width:0;min-height:0}.mail-pane-head{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.5rem .75rem;font-size:.8rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--dm-text-muted);border-bottom:1px solid var(--dm-border);flex-shrink:0}.mail-folders-list,.mail-list{overflow-y:auto;flex:1;min-height:0}.mail-search{display:flex;align-items:center;gap:.4rem;padding:.4rem .6rem;border-bottom:1px solid var(--dm-border);color:var(--dm-text-muted);flex-shrink:0}.mail-search input{flex:1;min-width:0;font-size:.85rem;padding:.25rem .4rem}.mail-search-note{padding:.3rem .6rem;font-size:.74rem;color:var(--dm-text-muted);border-bottom:1px solid var(--dm-border);flex-shrink:0}.mail-slideover-resizer{padding:0;position:absolute;left:0;top:0;bottom:0;width:6px;cursor:col-resize;z-index:2;background:transparent}.mail-slideover-resizer:hover,.is-resizing>.mail-slideover-resizer{background:var(--dm-primary);opacity:.7}.is-resizing{user-select:none;transition:none!important}.mail-selection{display:flex;align-items:center;gap:.35rem;flex-wrap:wrap;padding:.35rem .6rem;font-size:.78rem;background:var(--dm-primary);color:var(--dm-primary-text);flex-shrink:0}.mail-selection span{flex:1;min-width:0}.mail-selection .btn{color:var(--dm-primary-text);border:1px solid currentColor;background:transparent}.mail-selection .btn:hover{background:color-mix(in srgb,var(--dm-primary-text) 15%,transparent)}.mail-item.is-selected{background:var(--dm-active-bg);box-shadow:inset 3px 0 0 var(--dm-primary)}.mail-undo{display:flex;align-items:center;gap:.5rem;padding:.4rem .6rem;font-size:.78rem;background:var(--dm-active-bg);border-bottom:1px solid var(--dm-border);flex-shrink:0}.mail-undo span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-splitter{width:5px;cursor:col-resize;background:var(--dm-border);flex-shrink:0;background-clip:content-box;border-left:2px solid transparent;border-right:2px solid transparent;box-sizing:content-box}.mail-splitter:hover,.mail-layout.is-resizing .mail-splitter{background-color:var(--dm-primary)}.mail-layout.is-resizing{user-select:none}.mail-layout.is-resizing .mail-folders-pane,.mail-layout.is-resizing .mail-list-pane,.mail-layout.is-resizing .mail-reader-pane,.mail-layout.is-resizing iframe{pointer-events:none}.mail-unread-filter{display:flex;align-items:center;gap:.3rem;font-size:.7rem;text-transform:none;letter-spacing:0;cursor:pointer;margin-left:auto;margin-right:.25rem}.mail-unread-pill{flex-shrink:0;min-width:1.15rem;padding:0 .3rem;border-radius:999px;background:var(--dm-primary);color:var(--dm-primary-text);font-size:.66rem;font-weight:700;line-height:1.35;text-align:center}.mail-folder.is-context .mail-folder-name{opacity:.6}.mail-account{display:flex;align-items:center;gap:.4rem;width:100%;padding:.45rem .6rem;border:none;background:var(--dm-surface);cursor:pointer;text-align:left;font-size:.78rem;font-weight:700;text-transform:uppercase;letter-spacing:.03em;color:var(--dm-text-muted)}.mail-account:hover{background:var(--dm-hover-bg)}.mail-account.is-active{color:var(--dm-text);position:sticky;top:0;z-index:1;background:var(--dm-surface)}.mail-account-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-folder{display:flex;align-items:center;gap:.5rem;padding:.45rem .75rem;font-size:.875rem;cursor:pointer;border:none;background:none;width:100%;text-align:left;color:var(--dm-text)}.mail-folder:hover{background:var(--dm-hover-bg)}.mail-folder.is-active{background:var(--dm-active-bg);color:var(--dm-primary);font-weight:600}.mail-folder.is-unselectable{opacity:.6;cursor:default}.mail-folder.is-unselectable:hover{background:none}.mail-folder-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-item{display:block;width:100%;text-align:left;padding:.55rem .75rem;border:none;border-bottom:1px solid var(--dm-border);background:none;cursor:pointer;color:var(--dm-text)}.mail-item:hover{background:var(--dm-hover-bg)}.mail-item.is-active{background:var(--dm-active-bg)}.mail-item-top{display:flex;justify-content:space-between;gap:.5rem;align-items:baseline}.mail-item-from{font-size:.85rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-item.is-unread .mail-item-from,.mail-item.is-unread .mail-item-subject{font-weight:700}.mail-item-date{font-size:.72rem;color:var(--dm-text-muted);flex-shrink:0}.mail-item-subject{font-size:.82rem;margin-top:.1rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-item-meta{display:flex;align-items:center;gap:.35rem;margin-top:.15rem;color:var(--dm-text-muted)}.mail-pager{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.4rem .6rem;border-top:1px solid var(--dm-border);font-size:.75rem;color:var(--dm-text-muted);flex-shrink:0}.mail-reader-head{padding:.75rem 1rem;border-bottom:1px solid var(--dm-border);flex-shrink:0}.mail-reader-head:empty{display:none}.mail-reader-subject{font-size:1.05rem;font-weight:600;margin:0 0 .3rem;color:var(--dm-text)}.mail-reader-actions{display:flex;gap:.25rem;margin:.1rem 0 .35rem;flex-wrap:wrap}.mail-reader-meta{font-size:.8rem;color:var(--dm-text-muted)}.mail-reader-body{flex:1;position:relative;overflow:hidden;display:flex;flex-direction:column;min-height:0;background:var(--dm-surface)}.mail-reader-body iframe{width:100%;flex:1;border:none;display:block;background:#fff}.mail-notice{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;padding:.4rem .75rem;font-size:.78rem;background:var(--dm-warning);color:var(--dm-warning-text);border-bottom:1px solid var(--dm-border)}.mail-notice .btn{color:var(--dm-warning-text);border:1px solid currentColor;background:transparent;opacity:.95}.mail-notice .btn:hover{background:color-mix(in srgb,var(--dm-warning-text) 15%,transparent);opacity:1}.mail-attachments{display:flex;flex-wrap:wrap;gap:.4rem;padding:.5rem .75rem;border-bottom:1px solid var(--dm-border);background:var(--dm-background-alt)}.mail-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;height:100%;color:var(--dm-text-muted);text-align:center;padding:1rem}.mail-empty p{margin:0;font-size:.875rem}@media(max-width:900px){.mail-layout{grid-template-columns:1fr;height:auto!important}.mail-splitter{display:none}.mail-folders-pane,.mail-list-pane{border-right:none;border-bottom:1px solid var(--dm-border)}.mail-folders-list,.mail-list{max-height:240px}.mail-reader-body{min-height:60vh}}.mail-box-row{display:flex;align-items:center;gap:.5rem;padding:.5rem .6rem;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md, 6px)}.mail-default-badge{margin-left:.4rem;font-size:.7rem;font-weight:500;vertical-align:middle}.mail-item.is-dragging{opacity:.45}.mail-folder.is-drop-target{background:var(--dm-primary);color:var(--dm-primary-text);outline:2px solid var(--dm-primary);outline-offset:-2px}.mail-folder.is-drop-target .mail-folder-name,.mail-folder.is-drop-target .mail-unread-pill{color:inherit}.mail-item.is-dragging *{pointer-events:none}.mail-item{position:relative}.mail-item-tick{position:absolute;left:.45rem;top:.55rem;display:inline-flex;opacity:0;color:var(--dm-text-muted);transition:opacity .12s ease;padding:2px;margin:-2px;border-radius:var(--dm-radius-sm, 3px)}.mail-item:hover .mail-item-tick,.mail-item:focus-visible .mail-item-tick,.mail-list.is-selecting .mail-item-tick,.mail-item-tick[aria-checked=true]{opacity:1}.mail-item-tick[aria-checked=true]{color:var(--dm-primary)}.mail-item-tick:hover{color:var(--dm-primary);background:var(--dm-hover-bg)}.mail-item-top,.mail-item-subject,.mail-item-meta{padding-left:1.5rem}.mail-select-all{display:inline-flex;align-items:center;margin-right:.1rem;cursor:pointer}.mail-select-all input{cursor:pointer;margin:0}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.mail-item-meta>span{display:inline-flex}.mail-item-meta .is-replied{color:var(--dm-success)}.mail-item-meta .is-forwarded{color:var(--dm-info, var(--dm-primary))}.mail-item-meta .is-high{color:var(--dm-danger)}.mail-item-meta .is-low{color:var(--dm-text-muted);opacity:.7}.mail-folder.is-hidden-folder .mail-folder-name{opacity:.5;font-style:italic}.mail-thread-chip{display:inline-flex;align-items:center;margin-left:.4rem;padding:0 .35rem;border-radius:999px;font-size:.68rem;font-weight:700;line-height:1.5;color:var(--dm-text-muted);background:var(--dm-active-bg);cursor:pointer}.mail-thread-chip:hover{color:var(--dm-primary)}.mail-thread-chip.is-unread{background:var(--dm-primary);color:var(--dm-primary-text)}.mail-item.is-in-thread{padding-left:1.6rem;box-shadow:inset 3px 0 0 var(--dm-border)}.mail-item.is-in-thread .mail-item-from{font-size:.8rem}.mail-pane-head .btn.is-on{color:var(--dm-primary);background:var(--dm-active-bg)}#mail-outbox-btn{font-size:.72rem;font-weight:700}#mail-outbox-btn.is-failed{color:var(--dm-danger)}.mail-attachment{display:inline-flex;align-items:center;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md, 6px);overflow:hidden}.mail-attachment .btn{border:none;border-radius:0}.mail-attachment-save{padding-left:.4rem!important;padding-right:.4rem!important;border-left:1px solid var(--dm-border)!important;opacity:.7}.mail-attachment-save:hover{opacity:1;color:var(--dm-primary)}.mail-section-heading{margin:1rem 0 .1rem;font-size:.8rem;text-transform:uppercase;letter-spacing:.04em;color:var(--dm-text-muted)}.mail-section-heading:first-child{margin-top:.25rem}.mail-section-form .domma-form{margin:0}.mail-section-form .domma-form-field:last-child{margin-bottom:0}.mail-item-card{margin:0}.mail-item-card .card-header{padding:.5rem .6rem}.mail-item-card .card-body{padding:.6rem}.mail-ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.mail-status{min-height:1.2em}.mail-empty-line{margin:.25rem 0}.mail-editor-fallback{min-height:8rem;font-family:inherit;resize:vertical}.mail-clause{display:flex;gap:.3rem;align-items:center;flex-wrap:wrap}.mail-clause .form-select,.mail-clause .form-input{width:auto;flex:0 1 auto}.mail-clause .mail-clause-value{flex:1 1 8rem;min-width:8rem}.mail-section-list{display:flex;flex-direction:column;gap:.5rem}.mail-scroll-sm{max-height:20vh;overflow-y:auto}.mail-scroll-md{max-height:46vh;overflow-y:auto}.mail-section-form .domma-form-field:has(select) .form-label .text-danger{display:none}.mail-compose-form{padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:.6rem;height:100%}.mail-compose-body{display:flex;flex-direction:column;flex:1;min-height:0}.mail-compose-editor{flex:1;min-height:0}.mail-compose-textarea{flex:1;min-height:34vh;font-family:inherit;resize:vertical}.mail-attach-list{display:flex;flex-direction:column;gap:.2rem}.mail-template-row{margin:-.15rem 0 .35rem}.mail-template-picker{max-width:16rem}.mail-suggest-row{display:flex;flex-direction:column;line-height:1.25}.mail-suggest-detail{opacity:.7}.mail-rule-off{opacity:.55}.mail-panel{padding:.25rem 0 .5rem}.mail-panel-stack{padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:1rem}.mail-form-stack{padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:.75rem}.mail-dialog-stack{display:flex;flex-direction:column;gap:.5rem;padding:.25rem 0}.mail-hint{margin-top:.2rem}.mail-box-sub{overflow:hidden;text-overflow:ellipsis}.mail-add-btn{align-self:flex-start;margin-top:.5rem}.mail-none{margin:0;padding:.5rem .6rem}.mail-kv{padding:.5rem 0;display:flex;flex-direction:column;gap:.35rem}.mail-kv-row{display:flex;gap:.75rem}.mail-kv-key{min-width:8.5rem;flex-shrink:0}.mail-toggle-row{display:flex;align-items:center;gap:.5rem;margin-bottom:.75rem}.mail-sender-list{display:flex;flex-direction:column;gap:.2rem;max-height:40vh;overflow-y:auto}.mail-folder-pick{display:flex;align-items:center;gap:.5rem;padding:.3rem .4rem;border-radius:var(--dm-radius-md, 6px)}.mail-diag-log{width:100%;height:30vh;margin-top:.5rem;font-family:monospace;font-size:.72rem}.mail-facts{display:flex;flex-direction:column;gap:.2rem;margin-bottom:.75rem}.mail-item-more{position:absolute;right:.4rem;bottom:.35rem;display:inline-flex;align-items:center;justify-content:center;min-width:1.4rem;height:1.4rem;border-radius:var(--dm-radius-sm, 3px);font-size:1rem;line-height:1;color:var(--dm-text-muted);background:inherit;opacity:0;transition:opacity .12s ease}.mail-item:hover .mail-item-more,.mail-item:focus-visible .mail-item-more,.mail-item.is-active .mail-item-more{opacity:1}.mail-item-more:hover{color:var(--dm-primary)}.mail-section-intro{display:flex;align-items:center;gap:.35rem;margin:0 0 .75rem;padding-bottom:.5rem;border-bottom:1px solid var(--dm-border)}.mail-pager-middle{display:flex;align-items:center;gap:.5rem;min-width:0}.mail-page-size{font:inherit;color:inherit;background:transparent;border:1px solid var(--dm-border);border-radius:4px;padding:.1rem .25rem}.mail-auth-choice{display:flex;flex-wrap:wrap;align-items:center;gap:.4rem;padding-bottom:.5rem;border-bottom:1px solid var(--dm-border)}.mail-oauth{display:flex;flex-direction:column;gap:.75rem}.mail-oauth-warn{margin:0;padding:.5rem .7rem;border:1px solid var(--dm-warning);border-radius:var(--dm-radius-md);color:var(--dm-warning-text)}.mail-oauth-state{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.mail-oauth-chip{margin-left:.4rem;border:1px solid var(--dm-border);background:var(--dm-surface);color:var(--dm-text-muted)}.mail-oauth-state .mail-oauth-chip{margin-left:0}.mail-oauth-chip--ok{border-color:var(--dm-success);color:var(--dm-success)}.mail-oauth-chip--bad{border-color:var(--dm-danger);color:var(--dm-danger)}.mail-oauth-guide{border:1px solid var(--dm-border);border-radius:var(--dm-radius-md);padding:.5rem .75rem;background:var(--dm-background-alt)}.mail-oauth-guide>summary{cursor:pointer;font-weight:600}.mail-oauth-guide-body ol{padding-left:1.2rem;margin:.5rem 0 0}.mail-oauth-guide-body li{margin-bottom:.45rem}.mail-oauth-code{word-break:break-all;user-select:all}.mail-loading-slot{display:block;min-height:3rem;padding:.5rem .6rem}.mail-row-more{flex-shrink:0;margin-left:auto;display:inline-flex;align-items:center;justify-content:center;min-width:1.3rem;height:1.3rem;border-radius:var(--dm-radius-sm);font-size:.95rem;line-height:1;color:var(--dm-text-muted);opacity:0;transition:opacity .12s ease}.mail-unread-pill+.mail-row-more{margin-left:0}.mail-folder:hover .mail-row-more,.mail-folder:focus-visible .mail-row-more,.mail-folder.is-active .mail-row-more,.mail-account:hover .mail-row-more,.mail-account:focus-visible .mail-row-more,.mail-account.is-active .mail-row-more{opacity:1}.mail-row-more:hover{color:var(--dm-primary)}.mail-box-row .mail-row-more{opacity:1;margin-left:0}.mail-box-row:focus-visible{outline:2px solid var(--dm-primary);outline-offset:-2px}.mail-restored-note{margin:0;padding:.4rem .6rem;border-left:3px solid var(--dm-warning);background:var(--dm-background-alt)}.mail-font-controls .mail-font-btn{width:auto;min-width:3.5rem;max-width:10rem;padding:0 .5rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.8125rem}.domma-dropdown-menu:has(.mail-font-item),.domma-dropdown-menu:has(.mail-size-item){max-height:min(60vh,26rem);overflow-y:auto;z-index:10050}.domma-dropdown-item:has(.mail-font-item){display:flex;align-items:center;justify-content:space-between;gap:.75rem}.mail-font-item{font-size:.9375rem}.mail-font-web{font-size:.6875rem;color:var(--dm-text-muted);text-transform:uppercase;letter-spacing:.04em}
|
|
1
|
+
#view-container.mail-fullbleed{max-width:none}.mail-layout [hidden],.dm-slideover [hidden]{display:none!important}.mail-reader-summary{display:flex;align-items:baseline;gap:.5rem;flex-wrap:wrap}.mail-header-toggle,.mail-extras-toggle{padding:0!important;font-size:.78rem;opacity:.75;text-decoration:underline}.mail-header-toggle:hover,.mail-extras-toggle:hover{opacity:1}.mail-header-details{display:grid;grid-template-columns:max-content 1fr;gap:.15rem .75rem;margin:.5rem 0 0;font-size:.8rem}.mail-header-details dt{opacity:.65}.mail-header-details dd{margin:0;overflow-wrap:anywhere}.mail-layout{display:grid;grid-template-columns:var(--mail-col-1, 190px) auto var(--mail-col-2, 340px) auto 1fr;height:calc(100vh - 140px);min-height:320px;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md, 8px);overflow:hidden;background:var(--dm-surface)}.mail-folders-pane,.mail-list-pane{display:flex;flex-direction:column;border-right:1px solid var(--dm-border);min-width:0;min-height:0}.mail-reader-pane{display:flex;flex-direction:column;min-width:0;min-height:0}.mail-pane-head{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.5rem .75rem;font-size:.8rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--dm-text-muted);border-bottom:1px solid var(--dm-border);flex-shrink:0}.mail-folders-list,.mail-list{overflow-y:auto;flex:1;min-height:0}.mail-search{display:flex;align-items:center;gap:.4rem;padding:.4rem .6rem;border-bottom:1px solid var(--dm-border);color:var(--dm-text-muted);flex-shrink:0}.mail-search input{flex:1;min-width:0;font-size:.85rem;padding:.25rem .4rem}.mail-search-note{padding:.3rem .6rem;font-size:.74rem;color:var(--dm-text-muted);border-bottom:1px solid var(--dm-border);flex-shrink:0}.mail-slideover-resizer{padding:0;position:absolute;left:0;top:0;bottom:0;width:6px;cursor:col-resize;z-index:2;background:transparent}.mail-slideover-resizer:hover,.is-resizing>.mail-slideover-resizer{background:var(--dm-primary);opacity:.7}.is-resizing{user-select:none;transition:none!important}.mail-selection{display:flex;align-items:center;gap:.35rem;flex-wrap:wrap;padding:.35rem .6rem;font-size:.78rem;background:var(--dm-primary);color:var(--dm-primary-text);flex-shrink:0}.mail-selection span{flex:1;min-width:0}.mail-selection .btn{color:var(--dm-primary-text);border:1px solid currentColor;background:transparent}.mail-selection .btn:hover{background:color-mix(in srgb,var(--dm-primary-text) 15%,transparent)}.mail-item.is-selected{background:var(--dm-active-bg);box-shadow:inset 3px 0 0 var(--dm-primary)}.mail-undo{display:flex;align-items:center;gap:.5rem;padding:.4rem .6rem;font-size:.78rem;background:var(--dm-active-bg);border-bottom:1px solid var(--dm-border);flex-shrink:0}.mail-undo span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-splitter{width:5px;cursor:col-resize;background:var(--dm-border);flex-shrink:0;background-clip:content-box;border-left:2px solid transparent;border-right:2px solid transparent;box-sizing:content-box}.mail-splitter:hover,.mail-layout.is-resizing .mail-splitter{background-color:var(--dm-primary)}.mail-layout.is-resizing{user-select:none}.mail-layout.is-resizing .mail-folders-pane,.mail-layout.is-resizing .mail-list-pane,.mail-layout.is-resizing .mail-reader-pane,.mail-layout.is-resizing iframe{pointer-events:none}.mail-unread-filter{display:flex;align-items:center;gap:.3rem;font-size:.7rem;text-transform:none;letter-spacing:0;cursor:pointer;margin-left:auto;margin-right:.25rem}.mail-unread-pill{flex-shrink:0;min-width:1.15rem;padding:0 .3rem;border-radius:999px;background:var(--dm-primary);color:var(--dm-primary-text);font-size:.66rem;font-weight:700;line-height:1.35;text-align:center}.mail-folder.is-context .mail-folder-name{opacity:.6}.mail-account{display:flex;align-items:center;gap:.4rem;width:100%;padding:.45rem .6rem;border:none;background:var(--dm-surface);cursor:pointer;text-align:left;font-size:.78rem;font-weight:700;text-transform:uppercase;letter-spacing:.03em;color:var(--dm-text-muted)}.mail-account:hover{background:var(--dm-hover-bg)}.mail-account.is-active{color:var(--dm-text);position:sticky;top:0;z-index:1;background:var(--dm-surface)}.mail-account-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-folder{display:flex;align-items:center;gap:.5rem;padding:.45rem .75rem;font-size:.875rem;cursor:pointer;border:none;background:none;width:100%;text-align:left;color:var(--dm-text)}.mail-folder:hover{background:var(--dm-hover-bg)}.mail-folder.is-active{background:var(--dm-active-bg);color:var(--dm-primary);font-weight:600}.mail-folder.is-unselectable{opacity:.6;cursor:default}.mail-folder.is-unselectable:hover{background:none}.mail-folder-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-item{display:block;width:100%;text-align:left;padding:.55rem .75rem;border:none;border-bottom:1px solid var(--dm-border);background:none;cursor:pointer;color:var(--dm-text)}.mail-item:hover{background:var(--dm-hover-bg)}.mail-item.is-active{background:var(--dm-active-bg)}.mail-item-top{display:flex;justify-content:space-between;gap:.5rem;align-items:baseline}.mail-item-from{font-size:.85rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-item.is-unread .mail-item-from,.mail-item.is-unread .mail-item-subject{font-weight:700}.mail-item-date{font-size:.72rem;color:var(--dm-text-muted);flex-shrink:0}.mail-item-subject{font-size:.82rem;margin-top:.1rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-item-meta{display:flex;align-items:center;gap:.35rem;margin-top:.15rem;color:var(--dm-text-muted)}.mail-pager{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.4rem .6rem;border-top:1px solid var(--dm-border);font-size:.75rem;color:var(--dm-text-muted);flex-shrink:0}.mail-reader-head{padding:.75rem 1rem;border-bottom:1px solid var(--dm-border);flex-shrink:0}.mail-reader-head:empty{display:none}.mail-reader-subject{font-size:1.05rem;font-weight:600;margin:0 0 .3rem;color:var(--dm-text)}.mail-reader-actions{display:flex;gap:.25rem;margin:.1rem 0 .35rem;flex-wrap:wrap}.mail-reader-meta{font-size:.8rem;color:var(--dm-text-muted)}.mail-reader-body{flex:1;position:relative;overflow:hidden;display:flex;flex-direction:column;min-height:0;background:var(--dm-surface)}.mail-reader-body iframe{width:100%;flex:1;border:none;display:block;background:#fff}.mail-notice{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;padding:.4rem .75rem;font-size:.78rem;background:var(--dm-warning);color:var(--dm-warning-text);border-bottom:1px solid var(--dm-border)}.mail-notice .btn{color:var(--dm-warning-text);border:1px solid currentColor;background:transparent;opacity:.95}.mail-notice .btn:hover{background:color-mix(in srgb,var(--dm-warning-text) 15%,transparent);opacity:1}.mail-security--ok{background:color-mix(in srgb,var(--dm-success) 14%,var(--dm-surface));color:var(--dm-text)}.mail-security--bad{background:var(--dm-danger);color:var(--dm-danger-text)}.mail-security-detail{flex-basis:100%;opacity:.85;overflow-wrap:anywhere}.mail-attachments{display:flex;flex-wrap:wrap;gap:.4rem;padding:.5rem .75rem;border-bottom:1px solid var(--dm-border);background:var(--dm-background-alt)}.mail-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;height:100%;color:var(--dm-text-muted);text-align:center;padding:1rem}.mail-empty p{margin:0;font-size:.875rem}@media(max-width:900px){.mail-layout{grid-template-columns:1fr;height:auto!important}.mail-splitter{display:none}.mail-folders-pane,.mail-list-pane{border-right:none;border-bottom:1px solid var(--dm-border)}.mail-folders-list,.mail-list{max-height:240px}.mail-reader-body{min-height:60vh}}.mail-box-row{display:flex;align-items:center;gap:.5rem;padding:.5rem .6rem;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md, 6px)}.mail-default-badge{margin-left:.4rem;font-size:.7rem;font-weight:500;vertical-align:middle}.mail-item.is-dragging{opacity:.45}.mail-folder.is-drop-target{background:var(--dm-primary);color:var(--dm-primary-text);outline:2px solid var(--dm-primary);outline-offset:-2px}.mail-folder.is-drop-target .mail-folder-name,.mail-folder.is-drop-target .mail-unread-pill{color:inherit}.mail-item.is-dragging *{pointer-events:none}.mail-item{position:relative}.mail-item-tick{position:absolute;left:.45rem;top:.55rem;display:inline-flex;opacity:0;color:var(--dm-text-muted);transition:opacity .12s ease;padding:2px;margin:-2px;border-radius:var(--dm-radius-sm, 3px)}.mail-item:hover .mail-item-tick,.mail-item:focus-visible .mail-item-tick,.mail-list.is-selecting .mail-item-tick,.mail-item-tick[aria-checked=true]{opacity:1}.mail-item-tick[aria-checked=true]{color:var(--dm-primary)}.mail-item-tick:hover{color:var(--dm-primary);background:var(--dm-hover-bg)}.mail-item-top,.mail-item-subject,.mail-item-meta{padding-left:1.5rem}.mail-select-all{display:inline-flex;align-items:center;margin-right:.1rem;cursor:pointer}.mail-select-all input{cursor:pointer;margin:0}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.mail-item-meta>span{display:inline-flex}.mail-item-meta .is-replied{color:var(--dm-success)}.mail-item-meta .is-forwarded{color:var(--dm-info, var(--dm-primary))}.mail-item-meta .is-high{color:var(--dm-danger)}.mail-item-meta .is-low{color:var(--dm-text-muted);opacity:.7}.mail-folder.is-hidden-folder .mail-folder-name{opacity:.5;font-style:italic}.mail-thread-chip{display:inline-flex;align-items:center;margin-left:.4rem;padding:0 .35rem;border-radius:999px;font-size:.68rem;font-weight:700;line-height:1.5;color:var(--dm-text-muted);background:var(--dm-active-bg);cursor:pointer}.mail-thread-chip:hover{color:var(--dm-primary)}.mail-thread-chip.is-unread{background:var(--dm-primary);color:var(--dm-primary-text)}.mail-item.is-in-thread{padding-left:1.6rem;box-shadow:inset 3px 0 0 var(--dm-border)}.mail-item.is-in-thread .mail-item-from{font-size:.8rem}.mail-pane-head .btn.is-on{color:var(--dm-primary);background:var(--dm-active-bg)}#mail-outbox-btn{font-size:.72rem;font-weight:700}#mail-outbox-btn.is-failed{color:var(--dm-danger)}.mail-attachment{display:inline-flex;align-items:center;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md, 6px);overflow:hidden}.mail-attachment .btn{border:none;border-radius:0}.mail-attachment-save{padding-left:.4rem!important;padding-right:.4rem!important;border-left:1px solid var(--dm-border)!important;opacity:.7}.mail-attachment-save:hover{opacity:1;color:var(--dm-primary)}.mail-section-heading{margin:1rem 0 .1rem;font-size:.8rem;text-transform:uppercase;letter-spacing:.04em;color:var(--dm-text-muted)}.mail-section-heading:first-child{margin-top:.25rem}.mail-section-form .domma-form{margin:0}.mail-section-form .domma-form-field:last-child{margin-bottom:0}.mail-item-card{margin:0}.mail-item-card .card-header{padding:.5rem .6rem}.mail-item-card .card-body{padding:.6rem}.mail-ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.mail-status{min-height:1.2em}.mail-empty-line{margin:.25rem 0}.mail-editor-fallback{min-height:8rem;font-family:inherit;resize:vertical}.mail-clause{display:flex;gap:.3rem;align-items:center;flex-wrap:wrap}.mail-clause .form-select,.mail-clause .form-input{width:auto;flex:0 1 auto}.mail-clause .mail-clause-value{flex:1 1 8rem;min-width:8rem}.mail-section-list{display:flex;flex-direction:column;gap:.5rem}.mail-scroll-sm{max-height:20vh;overflow-y:auto}.mail-scroll-md{max-height:46vh;overflow-y:auto}.mail-section-form .domma-form-field:has(select) .form-label .text-danger{display:none}.mail-compose-form{padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:.6rem;height:100%}.mail-compose-body{display:flex;flex-direction:column;flex:1;min-height:0}.mail-compose-editor{flex:1;min-height:0}.mail-compose-textarea{flex:1;min-height:34vh;font-family:inherit;resize:vertical}.mail-attach-list{display:flex;flex-direction:column;gap:.2rem}.mail-template-row{margin:-.15rem 0 .35rem}.mail-template-picker{max-width:16rem}.mail-suggest-row{display:flex;flex-direction:column;line-height:1.25}.mail-suggest-detail{opacity:.7}.mail-rule-off{opacity:.55}.mail-panel{padding:.25rem 0 .5rem}.mail-panel-stack{padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:1rem}.mail-form-stack{padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:.75rem}.mail-dialog-stack{display:flex;flex-direction:column;gap:.5rem;padding:.25rem 0}.mail-hint{margin-top:.2rem}.mail-box-sub{overflow:hidden;text-overflow:ellipsis}.mail-add-btn{align-self:flex-start;margin-top:.5rem}.mail-none{margin:0;padding:.5rem .6rem}.mail-kv{padding:.5rem 0;display:flex;flex-direction:column;gap:.35rem}.mail-kv-row{display:flex;gap:.75rem}.mail-kv-key{min-width:8.5rem;flex-shrink:0}.mail-toggle-row{display:flex;align-items:center;gap:.5rem;margin-bottom:.75rem}.mail-sender-list{display:flex;flex-direction:column;gap:.2rem;max-height:40vh;overflow-y:auto}.mail-folder-pick{display:flex;align-items:center;gap:.5rem;padding:.3rem .4rem;border-radius:var(--dm-radius-md, 6px)}.mail-diag-log{width:100%;height:30vh;margin-top:.5rem;font-family:monospace;font-size:.72rem}.mail-facts{display:flex;flex-direction:column;gap:.2rem;margin-bottom:.75rem}.mail-item-more{position:absolute;right:.4rem;bottom:.35rem;display:inline-flex;align-items:center;justify-content:center;min-width:1.4rem;height:1.4rem;border-radius:var(--dm-radius-sm, 3px);font-size:1rem;line-height:1;color:var(--dm-text-muted);background:inherit;opacity:0;transition:opacity .12s ease}.mail-item:hover .mail-item-more,.mail-item:focus-visible .mail-item-more,.mail-item.is-active .mail-item-more{opacity:1}.mail-item-more:hover{color:var(--dm-primary)}.mail-section-intro{display:flex;align-items:center;gap:.35rem;margin:0 0 .75rem;padding-bottom:.5rem;border-bottom:1px solid var(--dm-border)}.mail-pager-middle{display:flex;align-items:center;gap:.5rem;min-width:0}.mail-page-size{font:inherit;color:inherit;background:transparent;border:1px solid var(--dm-border);border-radius:4px;padding:.1rem .25rem}.mail-auth-choice{display:flex;flex-wrap:wrap;align-items:center;gap:.4rem;padding-bottom:.5rem;border-bottom:1px solid var(--dm-border)}.mail-oauth{display:flex;flex-direction:column;gap:.75rem}.mail-oauth-warn{margin:0;padding:.5rem .7rem;border:1px solid var(--dm-warning);border-radius:var(--dm-radius-md);color:var(--dm-warning-text)}.mail-oauth-state{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.mail-oauth-chip{margin-left:.4rem;border:1px solid var(--dm-border);background:var(--dm-surface);color:var(--dm-text-muted)}.mail-oauth-state .mail-oauth-chip{margin-left:0}.mail-oauth-chip--ok{border-color:var(--dm-success);color:var(--dm-success)}.mail-oauth-chip--bad{border-color:var(--dm-danger);color:var(--dm-danger)}.mail-oauth-guide{border:1px solid var(--dm-border);border-radius:var(--dm-radius-md);padding:.5rem .75rem;background:var(--dm-background-alt)}.mail-oauth-guide>summary{cursor:pointer;font-weight:600}.mail-oauth-guide-body ol{padding-left:1.2rem;margin:.5rem 0 0}.mail-oauth-guide-body li{margin-bottom:.45rem}.mail-oauth-code{word-break:break-all;user-select:all}.mail-loading-slot{display:block;min-height:3rem;padding:.5rem .6rem}.mail-row-more{flex-shrink:0;margin-left:auto;display:inline-flex;align-items:center;justify-content:center;min-width:1.3rem;height:1.3rem;border-radius:var(--dm-radius-sm);font-size:.95rem;line-height:1;color:var(--dm-text-muted);opacity:0;transition:opacity .12s ease}.mail-unread-pill+.mail-row-more{margin-left:0}.mail-folder:hover .mail-row-more,.mail-folder:focus-visible .mail-row-more,.mail-folder.is-active .mail-row-more,.mail-account:hover .mail-row-more,.mail-account:focus-visible .mail-row-more,.mail-account.is-active .mail-row-more{opacity:1}.mail-row-more:hover{color:var(--dm-primary)}.mail-box-row .mail-row-more{opacity:1;margin-left:0}.mail-box-row:focus-visible{outline:2px solid var(--dm-primary);outline-offset:-2px}.mail-restored-note{margin:0;padding:.4rem .6rem;border-left:3px solid var(--dm-warning);background:var(--dm-background-alt)}.mail-font-controls .mail-font-btn{width:auto;min-width:3.5rem;max-width:10rem;padding:0 .5rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.8125rem}.domma-dropdown-menu:has(.mail-font-item),.domma-dropdown-menu:has(.mail-size-item){max-height:min(60vh,26rem);overflow-y:auto;z-index:10050}.domma-dropdown-item:has(.mail-font-item){display:flex;align-items:center;justify-content:space-between;gap:.75rem}.mail-font-item{font-size:.9375rem}.mail-font-web{font-size:.6875rem;color:var(--dm-text-muted);text-transform:uppercase;letter-spacing:.04em}
|
|
@@ -35,7 +35,7 @@ const {
|
|
|
35
35
|
} = await import(versioned('folder-tree.js'));
|
|
36
36
|
const {imageSendersSection} = await import(versioned('image-senders-section.js'));
|
|
37
37
|
const {
|
|
38
|
-
createSettingsSection, READ_DELAY_KEY, readDelaySeconds, scheduleMarkRead,
|
|
38
|
+
createSettingsSection, READ_DELAY_KEY, readDelaySeconds, scheduleMarkRead, readComposePrefs,
|
|
39
39
|
PAGE_SIZE_KEY, PAGE_SIZES, listPageSize, pageAfterResize
|
|
40
40
|
} = await import(versioned('settings-section.js'));
|
|
41
41
|
const {createSplitters, fitToViewport} = await import(versioned('panes.js'));
|
|
@@ -50,6 +50,8 @@ const {fillMessageAction, loadMessageActions, messageFacts} = await import(versi
|
|
|
50
50
|
const {isPlainKey, keyBelongsElsewhere, resolveKey, SHORTCUTS} = await import(versioned('keyboard.js'));
|
|
51
51
|
const {menuPoint, quoteSelection, readBridgeMessage, selectionItems} = await import(versioned('body-menu.js'));
|
|
52
52
|
const {describeWhen, quickTimes} = await import(versioned('scheduling.js'));
|
|
53
|
+
// Optional for the same reason as oauth-panel.js below.
|
|
54
|
+
const {securityNote} = await import(versioned('security-note.js')).catch(() => ({securityNote: () => null}));
|
|
53
55
|
// Sign in with Microsoft / Google (Email Pro). Optional: an older copy of this
|
|
54
56
|
// directory without it must not take the reader down.
|
|
55
57
|
const {describeSignIn, mountSignIn} = await import(versioned('oauth-panel.js')).catch(() => ({}));
|
|
@@ -958,6 +960,8 @@ return {
|
|
|
958
960
|
const sections = [
|
|
959
961
|
createSettingsSection({
|
|
960
962
|
canWrite: features.write,
|
|
963
|
+
canSend: features.send === true,
|
|
964
|
+
sidebarUrl: sidebarUrl(pluginName),
|
|
961
965
|
load: () => readDelaySeconds(S.get(READ_DELAY_KEY)),
|
|
962
966
|
save: seconds => {
|
|
963
967
|
S.set(READ_DELAY_KEY, seconds);
|
|
@@ -3192,6 +3196,29 @@ return {
|
|
|
3192
3196
|
|
|
3193
3197
|
readerBodyEl.textContent = '';
|
|
3194
3198
|
|
|
3199
|
+
// Signed or encrypted: said first, above the images notice, since
|
|
3200
|
+
// it is about whether to believe the rest of the pane at all.
|
|
3201
|
+
const security = securityNote(message.security);
|
|
3202
|
+
if (security) {
|
|
3203
|
+
const strip = document.createElement('div');
|
|
3204
|
+
strip.className = `mail-notice mail-security mail-security--${security.tone}`;
|
|
3205
|
+
const icon = document.createElement('span');
|
|
3206
|
+
icon.dataset.icon = security.icon;
|
|
3207
|
+
icon.dataset.iconSize = '14';
|
|
3208
|
+
icon.setAttribute('aria-hidden', 'true');
|
|
3209
|
+
const text = document.createElement('span');
|
|
3210
|
+
text.textContent = security.text;
|
|
3211
|
+
strip.append(icon, text);
|
|
3212
|
+
if (security.detail) {
|
|
3213
|
+
const more = document.createElement('span');
|
|
3214
|
+
more.className = 'mail-security-detail';
|
|
3215
|
+
more.textContent = security.detail;
|
|
3216
|
+
strip.appendChild(more);
|
|
3217
|
+
}
|
|
3218
|
+
readerBodyEl.appendChild(strip);
|
|
3219
|
+
Domma.icons.scan(strip);
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3195
3222
|
if (message.blockedImages > 0 && !message.remoteImagesLoaded) {
|
|
3196
3223
|
const notice = document.createElement('div');
|
|
3197
3224
|
notice.className = 'mail-notice';
|
|
@@ -3923,7 +3950,11 @@ return {
|
|
|
3923
3950
|
identities: identities?.identities ?? [],
|
|
3924
3951
|
mailboxAddress: identities?.mailboxAddress ?? '',
|
|
3925
3952
|
templates: templates?.templates ?? [],
|
|
3926
|
-
undoSendSeconds: sending?.undoSendSeconds ?? 0
|
|
3953
|
+
undoSendSeconds: sending?.undoSendSeconds ?? 0,
|
|
3954
|
+
// Where this mailbox's ticks start, and whether it can
|
|
3955
|
+
// sign or encrypt at all (a certificate is set up).
|
|
3956
|
+
compose: sending?.compose ?? null,
|
|
3957
|
+
security: sending?.security ?? null
|
|
3927
3958
|
})).catch(() => EMPTY_PROFILE);
|
|
3928
3959
|
}
|
|
3929
3960
|
return profileRequest;
|
|
@@ -3983,6 +4014,11 @@ return {
|
|
|
3983
4014
|
mailboxAddress: profile?.mailboxAddress ?? '',
|
|
3984
4015
|
templates: profile?.templates ?? [],
|
|
3985
4016
|
undoSeconds: profile?.undoSendSeconds ?? 0,
|
|
4017
|
+
defaults: profile?.compose ?? null,
|
|
4018
|
+
security: profile?.security ?? null,
|
|
4019
|
+
// Read at each opening, so a change in Settings applies to
|
|
4020
|
+
// the next message without a reload.
|
|
4021
|
+
prefs: readComposePrefs(S),
|
|
3986
4022
|
onSent: () => loadMessages({quiet: true}),
|
|
3987
4023
|
onQueued: (result) => offerRecall(result),
|
|
3988
4024
|
...extra
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the reader says about a signed or encrypted (S/MIME) message.
|
|
3
|
+
*
|
|
4
|
+
* Pure: `security` is the `/messages/:uid` answer's field (readRoutes.js
|
|
5
|
+
* `publicSecurity`), and the result is plain text for the strip above the
|
|
6
|
+
* message. The wording separates the three things a signature can mean -
|
|
7
|
+
* the message is unchanged, the signer is who a trusted authority says, and
|
|
8
|
+
* the signer is the sender shown - because "signed" alone promises all three
|
|
9
|
+
* and a self-made certificate gives only the first.
|
|
10
|
+
*
|
|
11
|
+
* @module _lib/admin/mail/security-note
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {object|null} security
|
|
16
|
+
* @returns {{tone: 'ok'|'warn'|'bad', icon: string, text: string, detail: string}|null}
|
|
17
|
+
*/
|
|
18
|
+
export function securityNote(security) {
|
|
19
|
+
if (!security || (!security.signed && !security.encrypted)) return null;
|
|
20
|
+
|
|
21
|
+
if (security.encrypted && !security.decrypted) {
|
|
22
|
+
return {
|
|
23
|
+
tone: 'bad', icon: 'lock',
|
|
24
|
+
text: 'Encrypted - this message could not be opened.',
|
|
25
|
+
detail: security.problem || 'None of your certificates can open it. Add the certificate it was encrypted to in Settings > Security.'
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const parts = [];
|
|
30
|
+
if (security.encrypted) parts.push('Encrypted');
|
|
31
|
+
let tone = 'ok';
|
|
32
|
+
let detail = '';
|
|
33
|
+
|
|
34
|
+
if (security.signed) {
|
|
35
|
+
const who = security.signer?.emails?.[0] || security.signer?.subject || 'an unknown signer';
|
|
36
|
+
if (!security.verified) {
|
|
37
|
+
tone = 'bad';
|
|
38
|
+
parts.push('the signature is NOT valid');
|
|
39
|
+
detail = security.problem || 'The message may have been changed after it was signed.';
|
|
40
|
+
} else if (!security.trusted) {
|
|
41
|
+
tone = 'warn';
|
|
42
|
+
parts.push(`signed by ${who}, whose certificate is not from an authority this server trusts`);
|
|
43
|
+
detail = 'The message is unchanged since it was signed, but anyone can make a certificate with any address in it. '
|
|
44
|
+
+ 'Check the fingerprint with the sender if it matters.';
|
|
45
|
+
} else if (!security.fromMatches) {
|
|
46
|
+
tone = 'warn';
|
|
47
|
+
parts.push(`signed by ${who}, which is not the sender shown`);
|
|
48
|
+
detail = 'The signature is valid, but it belongs to a different address than the From line.';
|
|
49
|
+
} else {
|
|
50
|
+
parts.push(`signed by ${who}`);
|
|
51
|
+
detail = 'The signature is valid and the message is unchanged since it was signed.';
|
|
52
|
+
}
|
|
53
|
+
if (security.verified && security.problem && tone === 'ok') detail = security.problem;
|
|
54
|
+
if (security.signer?.fingerprint) {
|
|
55
|
+
detail += ` Certificate fingerprint (SHA-256): ${security.signer.fingerprint}.`;
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
detail = 'Only you and the other recipients can read it.';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const text = parts.join(', ');
|
|
62
|
+
return {
|
|
63
|
+
tone,
|
|
64
|
+
icon: tone !== 'ok' ? 'shield-alert' : (security.encrypted ? 'lock' : 'shield-check'),
|
|
65
|
+
text: `${text.charAt(0).toUpperCase()}${text.slice(1)}.`,
|
|
66
|
+
detail: detail.trim()
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/** Browser preferences for the shared mail reader. */
|
|
2
2
|
export const READ_DELAY_KEY = 'domma-mail.mark-read-seconds';
|
|
3
|
+
/** Compose preferences - this browser's, like the rest of this tab. */
|
|
4
|
+
export const EDITOR_KEY = 'domma-mail.compose-editor';
|
|
5
|
+
export const AUTOSAVE_KEY = 'domma-mail.autosave-minutes';
|
|
6
|
+
export const SPELLCHECK_KEY = 'domma-mail.spellcheck';
|
|
7
|
+
/** The choices for saving a draft automatically, in minutes; 0 is never. */
|
|
8
|
+
export const AUTOSAVE_CHOICES = [0, 1, 2, 5, 10];
|
|
3
9
|
export const DEFAULT_READ_DELAY_SECONDS = 1;
|
|
4
10
|
export const PAGE_SIZE_KEY = 'domma-mail.page-size';
|
|
5
11
|
/** The choices offered under the list. 200 is the server's maxListLimit. */
|
|
@@ -50,39 +56,220 @@ export function scheduleMarkRead(markRead, seconds, schedule = setTimeout) {
|
|
|
50
56
|
return schedule(markRead, delay * 1000);
|
|
51
57
|
}
|
|
52
58
|
|
|
53
|
-
|
|
59
|
+
/**
|
|
60
|
+
* The editor a new message opens in: 'rich' (formatting, sent as HTML with a
|
|
61
|
+
* plain-text part) unless this browser chose 'plain'.
|
|
62
|
+
*
|
|
63
|
+
* @param {*} value
|
|
64
|
+
* @returns {'rich'|'plain'}
|
|
65
|
+
*/
|
|
66
|
+
export function composeEditor(value) {
|
|
67
|
+
return value === 'plain' ? 'plain' : 'rich';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Minutes between automatic draft saves; 0 (never) for anything not offered.
|
|
72
|
+
*
|
|
73
|
+
* @param {*} value
|
|
74
|
+
* @returns {number}
|
|
75
|
+
*/
|
|
76
|
+
export function autosaveMinutes(value) {
|
|
77
|
+
const minutes = Number(value);
|
|
78
|
+
return AUTOSAVE_CHOICES.includes(minutes) ? minutes : 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Whether the browser checks spelling as you type. On unless switched off,
|
|
83
|
+
* which is the browser's own default for a text field.
|
|
84
|
+
*
|
|
85
|
+
* @param {*} value
|
|
86
|
+
* @returns {boolean}
|
|
87
|
+
*/
|
|
88
|
+
export function spellcheckOn(value) {
|
|
89
|
+
return value !== false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* This browser's compose preferences, every key present.
|
|
94
|
+
*
|
|
95
|
+
* @param {{get: Function}} store - Domma's S
|
|
96
|
+
* @returns {{editor: 'rich'|'plain', autosaveMinutes: number, spellcheck: boolean}}
|
|
97
|
+
*/
|
|
98
|
+
export function readComposePrefs(store) {
|
|
99
|
+
const read = key => { try { return store?.get(key); } catch { return undefined; } };
|
|
100
|
+
return {
|
|
101
|
+
editor: composeEditor(read(EDITOR_KEY)),
|
|
102
|
+
autosaveMinutes: autosaveMinutes(read(AUTOSAVE_KEY)),
|
|
103
|
+
spellcheck: spellcheckOn(read(SPELLCHECK_KEY))
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The browser's own answer to "may this site show notifications?", in words.
|
|
109
|
+
*
|
|
110
|
+
* @param {string|undefined} permission - Notification.permission
|
|
111
|
+
* @returns {string}
|
|
112
|
+
*/
|
|
113
|
+
export function permissionWords(permission) {
|
|
114
|
+
if (permission === 'granted') return 'This browser allows notifications from this site.';
|
|
115
|
+
if (permission === 'denied') return 'This browser blocks notifications from this site. Allow them in the browser\u2019s site settings (the padlock by the address).';
|
|
116
|
+
if (permission === 'default') return 'This browser has not been asked yet - turning popups on asks it.';
|
|
117
|
+
return 'This browser cannot show notifications.';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The Settings tab: preferences kept in this browser.
|
|
122
|
+
*
|
|
123
|
+
* @param {{canWrite: boolean, canSend?: boolean, load: Function, save: Function,
|
|
124
|
+
* sidebarUrl?: string|null, userId?: string|null}} options
|
|
125
|
+
* `sidebarUrl` is this edition's sidebar entry, which is what new-mail
|
|
126
|
+
* notifications hang off (admin/js/lib/sidebar-notify.js).
|
|
127
|
+
* @returns {object}
|
|
128
|
+
*/
|
|
129
|
+
export function createSettingsSection({canWrite, canSend = false, load, save, sidebarUrl = null, userId = null}) {
|
|
54
130
|
return {
|
|
55
131
|
id: 'settings',
|
|
56
132
|
label: 'Settings',
|
|
57
|
-
help: 'Preferences for reading email in this browser.',
|
|
133
|
+
help: 'Preferences for reading and writing email in this browser.',
|
|
58
134
|
async render(container, ctx) {
|
|
59
135
|
const kit = await import(new URL(`./section-kit.js?v=${ctx.assetVersion ?? '1'}`, import.meta.url).href);
|
|
136
|
+
// Optional: a core older than the notifications leaves that part out.
|
|
137
|
+
const notify = sidebarUrl
|
|
138
|
+
? await import('/admin/js/lib/sidebar-notify.js').catch(() => null)
|
|
139
|
+
: null;
|
|
140
|
+
// The same user id the sidebar keys its notifications by.
|
|
141
|
+
const who = userId ?? (await import('/admin/js/api.js').catch(() => null))?.getUser?.()?.id ?? null;
|
|
60
142
|
kit.note(container, 'Settings are saved in this browser and apply to all your mailboxes.');
|
|
143
|
+
|
|
144
|
+
const saveSteps = [];
|
|
145
|
+
|
|
146
|
+
// ---- Reading -------------------------------------------------
|
|
147
|
+
kit.heading(container, 'Reading');
|
|
61
148
|
if (!canWrite) {
|
|
62
149
|
kit.note(container, 'This reader leaves messages unread. Automatic marking as read requires an edition that can update messages.');
|
|
63
|
-
|
|
150
|
+
} else {
|
|
151
|
+
const {state} = kit.formIn(container, {
|
|
152
|
+
markReadSeconds: {
|
|
153
|
+
type: 'number',
|
|
154
|
+
label: 'How long before a message is marked as read? (seconds)',
|
|
155
|
+
required: true,
|
|
156
|
+
min: 0,
|
|
157
|
+
max: 2147483,
|
|
158
|
+
formConfig: {hint: '0 marks a message as read immediately. Otherwise, it must stay open for this long.'}
|
|
159
|
+
}
|
|
160
|
+
}, {markReadSeconds: load()});
|
|
161
|
+
saveSteps.push(() => {
|
|
162
|
+
const value = state.markReadSeconds;
|
|
163
|
+
const seconds = Number(value);
|
|
164
|
+
if (value === '' || value === null || value === undefined || !Number.isFinite(seconds) || seconds < 0 || seconds > 2147483) {
|
|
165
|
+
throw new Error('Enter a number of seconds between 0 and 2147483.');
|
|
166
|
+
}
|
|
167
|
+
return () => save(seconds);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ---- Composing -----------------------------------------------
|
|
172
|
+
if (canSend) {
|
|
173
|
+
kit.heading(container, 'Composing');
|
|
174
|
+
const prefs = readComposePrefs(S);
|
|
175
|
+
const {state} = kit.formIn(container, {
|
|
176
|
+
editor: {
|
|
177
|
+
type: 'select', label: 'Default text editor', required: true,
|
|
178
|
+
options: [
|
|
179
|
+
{value: 'rich', label: 'Rich text (formatting, pictures, links)'},
|
|
180
|
+
{value: 'plain', label: 'Plain text'}
|
|
181
|
+
],
|
|
182
|
+
formConfig: {hint: 'What a new message, reply or forward opens in. Plain text sends text only - what every mail program shows the same way.'}
|
|
183
|
+
},
|
|
184
|
+
autosave: {
|
|
185
|
+
type: 'select', label: 'Automatically save draft', required: true,
|
|
186
|
+
// Strings: Forma reads a 0 as "no value" and shows its
|
|
187
|
+
// "-- Select --" placeholder instead of Never.
|
|
188
|
+
options: AUTOSAVE_CHOICES.map(n => ({
|
|
189
|
+
value: String(n), label: n === 0 ? 'Never' : `Every ${n} minute${n === 1 ? '' : 's'}`
|
|
190
|
+
})),
|
|
191
|
+
formConfig: {hint: 'Saved to the Drafts folder while you write, so a closed tab or a crash loses little. A message you then discard takes its automatic draft with it.'}
|
|
192
|
+
},
|
|
193
|
+
spellcheck: {type: 'boolean', label: 'Check spelling as I type'}
|
|
194
|
+
}, {editor: prefs.editor, autosave: String(prefs.autosaveMinutes), spellcheck: prefs.spellcheck});
|
|
195
|
+
saveSteps.push(() => () => {
|
|
196
|
+
S.set(EDITOR_KEY, composeEditor(state.editor));
|
|
197
|
+
S.set(AUTOSAVE_KEY, autosaveMinutes(state.autosave));
|
|
198
|
+
S.set(SPELLCHECK_KEY, state.spellcheck !== false);
|
|
199
|
+
});
|
|
200
|
+
kit.note(container, 'Receipts, signing, encryption and where replies are filed belong to each mailbox: see the Composing and Security tabs.');
|
|
64
201
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
202
|
+
|
|
203
|
+
// ---- Notifications -------------------------------------------
|
|
204
|
+
if (notify) {
|
|
205
|
+
kit.heading(container, 'Notifications');
|
|
206
|
+
kit.note(container, 'While the admin is open in any tab, new mail in the inbox can pop up a notification and play a sound - checked about once a minute.');
|
|
207
|
+
const current = notify.readNotifyPrefs(who, sidebarUrl);
|
|
208
|
+
const supported = typeof Notification === 'function';
|
|
209
|
+
const {state} = kit.formIn(container, {
|
|
210
|
+
popup: {type: 'boolean', label: 'Show new messages notification popups', formConfig: {disabled: !supported}},
|
|
211
|
+
sound: {type: 'boolean', label: 'Sound notification'}
|
|
212
|
+
}, {popup: current.popup && supported, sound: current.sound});
|
|
213
|
+
const status = kit.note(container, permissionWords(supported ? Notification.permission : undefined));
|
|
214
|
+
const tools = kit.box(container, 'flex gap-2 items-center mb-2');
|
|
215
|
+
kit.button(tools, 'Play the sound', () => notify.playChime(), {icon: 'play'});
|
|
216
|
+
if (supported) {
|
|
217
|
+
kit.button(tools, 'Show a test notification', async () => {
|
|
218
|
+
if (Notification.permission === 'default') await Notification.requestPermission();
|
|
219
|
+
status.textContent = permissionWords(Notification.permission);
|
|
220
|
+
if (Notification.permission === 'granted') {
|
|
221
|
+
new Notification('New mail', {body: 'This is what a new message looks like.', tag: `dm-notify:${sidebarUrl}`});
|
|
222
|
+
}
|
|
223
|
+
}, {icon: 'bell'});
|
|
73
224
|
}
|
|
74
|
-
|
|
225
|
+
saveSteps.push(() => async () => {
|
|
226
|
+
let popup = state.popup === true && supported;
|
|
227
|
+
// Asked here, on the Save click: browsers only let a page
|
|
228
|
+
// ask in answer to something the person did.
|
|
229
|
+
if (popup && Notification.permission === 'default') {
|
|
230
|
+
await Notification.requestPermission();
|
|
231
|
+
status.textContent = permissionWords(Notification.permission);
|
|
232
|
+
}
|
|
233
|
+
if (popup && Notification.permission === 'denied') popup = false;
|
|
234
|
+
notify.writeNotifyPrefs(who, sidebarUrl, {popup, sound: state.sound === true});
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ---- Email links ---------------------------------------------
|
|
239
|
+
if (canSend && typeof navigator?.registerProtocolHandler === 'function') {
|
|
240
|
+
kit.heading(container, 'Email links');
|
|
241
|
+
kit.note(container, 'Make this browser open mailto: links - on any website - in a new message here, instead of in a mail program on this computer. The browser asks you to confirm. To undo it, use the browser\u2019s settings (in Chrome: Settings > Privacy and security > Site settings > Protocol handlers).');
|
|
242
|
+
const linkRow = kit.box(container, 'flex gap-2 items-center mb-2');
|
|
243
|
+
const linkStatus = kit.box(linkRow, 'flex-1 text-sm mail-status text-muted', 'span');
|
|
244
|
+
kit.button(linkRow, 'Register as the mailto: links handler', async () => {
|
|
245
|
+
try {
|
|
246
|
+
const mail = await import('/admin/js/lib/mail-compose.js');
|
|
247
|
+
navigator.registerProtocolHandler('mailto', mail.mailtoHandlerUrl(`${location.origin}${location.pathname}`));
|
|
248
|
+
linkStatus.textContent = 'Asked. If the browser showed a question, answer it to finish.';
|
|
249
|
+
} catch (err) {
|
|
250
|
+
linkStatus.textContent = /secure|https/i.test(err?.message ?? '')
|
|
251
|
+
? 'Only a site served over https can handle mailto: links.'
|
|
252
|
+
: (err?.message || 'This browser would not register it.');
|
|
253
|
+
}
|
|
254
|
+
}, {icon: 'mail'});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (!saveSteps.length) return;
|
|
75
258
|
const bar = kit.actionBar(container);
|
|
76
|
-
kit.button(bar.row, 'Save', () => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
259
|
+
kit.button(bar.row, 'Save', async () => {
|
|
260
|
+
// Every value checked before anything is stored: a bad number
|
|
261
|
+
// must not leave half the tab saved.
|
|
262
|
+
let apply;
|
|
263
|
+
try {
|
|
264
|
+
apply = saveSteps.map(step => step());
|
|
265
|
+
} catch (err) {
|
|
266
|
+
bar.say(err.message, 'bad');
|
|
81
267
|
return;
|
|
82
268
|
}
|
|
83
269
|
try {
|
|
84
|
-
|
|
270
|
+
for (const run of apply) await run();
|
|
85
271
|
bar.say('Saved.', 'ok');
|
|
272
|
+
await ctx.changed?.();
|
|
86
273
|
} catch (err) {
|
|
87
274
|
bar.say(err.message || 'Settings could not be saved.', 'bad');
|
|
88
275
|
}
|
|
@@ -725,6 +725,65 @@ export async function setSendingOptions(userId, accountId, options) {
|
|
|
725
725
|
});
|
|
726
726
|
}
|
|
727
727
|
|
|
728
|
+
/**
|
|
729
|
+
* How a new message from this mailbox starts out, and two rules about where
|
|
730
|
+
* and how it leaves.
|
|
731
|
+
*
|
|
732
|
+
* All off by default, which is what every message sent before these existed
|
|
733
|
+
* did. The receipts, signing and encryption are only where the compose
|
|
734
|
+
* window's ticks START - each message can still change them. The other two
|
|
735
|
+
* are the mailbox's rules and apply to every message:
|
|
736
|
+
* `repliesInSourceFolder` files the copy of a reply beside the message it
|
|
737
|
+
* answers instead of in Sent, and `requireSecureDelivery` refuses to send
|
|
738
|
+
* unless the outgoing server promises TLS all the way (REQUIRETLS, RFC 8689).
|
|
739
|
+
*/
|
|
740
|
+
export const COMPOSE_DEFAULTS = Object.freeze({
|
|
741
|
+
readReceipt: false,
|
|
742
|
+
deliveryReceipt: false,
|
|
743
|
+
sign: false,
|
|
744
|
+
encrypt: false,
|
|
745
|
+
repliesInSourceFolder: false,
|
|
746
|
+
requireSecureDelivery: false
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* This mailbox's compose options, every key present.
|
|
751
|
+
*
|
|
752
|
+
* @param {object|null} account
|
|
753
|
+
* @returns {typeof COMPOSE_DEFAULTS}
|
|
754
|
+
*/
|
|
755
|
+
export function composeOptions(account) {
|
|
756
|
+
const stored = account?.compose && typeof account.compose === 'object' ? account.compose : {};
|
|
757
|
+
return Object.fromEntries(Object.keys(COMPOSE_DEFAULTS)
|
|
758
|
+
.map(key => [key, stored[key] === true]));
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Store the compose options. Only the known switches, only as booleans; a key
|
|
763
|
+
* left out keeps its stored value, so a screen that knows half of them cannot
|
|
764
|
+
* switch the other half off.
|
|
765
|
+
*
|
|
766
|
+
* @param {string} userId
|
|
767
|
+
* @param {string} accountId
|
|
768
|
+
* @param {object} options
|
|
769
|
+
* @returns {Promise<object|null>}
|
|
770
|
+
*/
|
|
771
|
+
export async function setComposeOptions(userId, accountId, options) {
|
|
772
|
+
return withStoreLock(async () => {
|
|
773
|
+
const all = await readAll();
|
|
774
|
+
const index = all.findIndex(a => a.id === accountId && a.userId === userId);
|
|
775
|
+
if (index === -1) return null;
|
|
776
|
+
|
|
777
|
+
const next = composeOptions(all[index]);
|
|
778
|
+
for (const key of Object.keys(COMPOSE_DEFAULTS)) {
|
|
779
|
+
if (typeof options?.[key] === 'boolean') next[key] = options[key];
|
|
780
|
+
}
|
|
781
|
+
all[index] = {...all[index], compose: next, updatedAt: new Date().toISOString()};
|
|
782
|
+
await writeAll(all);
|
|
783
|
+
return all[index];
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
|
|
728
787
|
/**
|
|
729
788
|
* Every address this mailbox sends as.
|
|
730
789
|
*
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* @module _lib/mail/compose
|
|
11
11
|
*/
|
|
12
12
|
import {readPriority} from './priority.js';
|
|
13
|
-
import {normaliseMessageSource, readMessageSource} from './message-action.js';
|
|
13
|
+
import {normaliseMessageSource, readComposeFlags, readMessageSource} from './message-action.js';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Addresses to reply to.
|
|
@@ -237,6 +237,10 @@ export function editableDraft(message, uid = null) {
|
|
|
237
237
|
// Kept, or a draft marked urgent would quietly become ordinary the
|
|
238
238
|
// moment it was reopened.
|
|
239
239
|
priority: readPriority(message.headers),
|
|
240
|
+
// Both kept for the same reason: a draft that asked for a receipt, or
|
|
241
|
+
// to be signed or encrypted, must not quietly stop asking on reopen.
|
|
242
|
+
readReceipt: hasHeader(message.headers, 'disposition-notification-to'),
|
|
243
|
+
...readComposeFlags(message.headers),
|
|
240
244
|
attachments: (message.attachments ?? []).map(file => ({
|
|
241
245
|
filename: file.filename ?? 'attachment',
|
|
242
246
|
contentType: file.contentType ?? 'application/octet-stream',
|
|
@@ -248,6 +252,16 @@ export function editableDraft(message, uid = null) {
|
|
|
248
252
|
};
|
|
249
253
|
}
|
|
250
254
|
|
|
255
|
+
/**
|
|
256
|
+
* @param {Map|object|undefined} headers - mailparser's headers
|
|
257
|
+
* @param {string} name - lower case
|
|
258
|
+
* @returns {boolean}
|
|
259
|
+
*/
|
|
260
|
+
function hasHeader(headers, name) {
|
|
261
|
+
const value = typeof headers?.get === 'function' ? headers.get(name) : headers?.[name];
|
|
262
|
+
return value !== undefined && value !== null && value !== '';
|
|
263
|
+
}
|
|
264
|
+
|
|
251
265
|
/**
|
|
252
266
|
* References arrive as a string on one server and an array on the next.
|
|
253
267
|
*
|
|
@@ -22,6 +22,38 @@ export function readMessageSource(headers) {
|
|
|
22
22
|
} catch { return null; }
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* The compose ticks a saved draft carries - delivery receipt, sign, encrypt -
|
|
27
|
+
* none of which leaves a trace a reopened draft could read back otherwise
|
|
28
|
+
* (the read receipt does: its own header). Private, like SOURCE_HEADER: only
|
|
29
|
+
* ever written into a draft, never onto a message that is sent.
|
|
30
|
+
*/
|
|
31
|
+
export const COMPOSE_HEADER = 'X-Domma-Compose';
|
|
32
|
+
const COMPOSE_FLAGS = ['deliveryReceipt', 'sign', 'encrypt'];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {object} draft
|
|
36
|
+
* @returns {string|null} the header value, or null when nothing is ticked
|
|
37
|
+
*/
|
|
38
|
+
export function composeFlagsHeader(draft) {
|
|
39
|
+
const on = COMPOSE_FLAGS.filter(key => draft?.[key] === true);
|
|
40
|
+
return on.length ? Buffer.from(JSON.stringify(Object.fromEntries(on.map(k => [k, true])))).toString('base64url') : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {Map|object} headers - mailparser's headers
|
|
45
|
+
* @returns {{deliveryReceipt: boolean, sign: boolean, encrypt: boolean}}
|
|
46
|
+
*/
|
|
47
|
+
export function readComposeFlags(headers) {
|
|
48
|
+
const value = typeof headers?.get === 'function'
|
|
49
|
+
? headers.get(COMPOSE_HEADER.toLowerCase()) : headers?.[COMPOSE_HEADER.toLowerCase()];
|
|
50
|
+
let parsed = {};
|
|
51
|
+
if (typeof value === 'string' && value.length <= 512) {
|
|
52
|
+
try { parsed = JSON.parse(Buffer.from(value, 'base64url').toString('utf8')) ?? {}; } catch { parsed = {}; }
|
|
53
|
+
}
|
|
54
|
+
return Object.fromEntries(COMPOSE_FLAGS.map(key => [key, parsed[key] === true]));
|
|
55
|
+
}
|
|
56
|
+
|
|
25
57
|
/** Best effort: recording a delivered message must never turn it into a failed send. */
|
|
26
58
|
export async function recordMessageAction({source, account, pool, poolKey, connection, store = null}) {
|
|
27
59
|
source = normaliseMessageSource(source);
|