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
@@ -1,9 +1,9 @@
1
1
  /*!
2
- * Domma Tools CSS v0.51.11
2
+ * Domma Tools CSS v0.51.13
3
3
  * Dynamic Object Manipulation & Modeling API
4
4
  * (c) 2026 Darryl Waterhouse & DCBW-IT
5
- * Built: 2026-10-12T06:05:33.864Z
6
- * Commit: a53de2f
5
+ * Built: 2026-10-13T15:14:47.576Z
6
+ * Commit: 85d3945
7
7
  */
8
8
 
9
9
  /*!
@@ -1,9 +1,9 @@
1
1
  /*!
2
- * Domma Tools v0.51.11
2
+ * Domma Tools v0.51.13
3
3
  * Developer tools: Theme Roller & Page Roller
4
4
  * (c) 2026 Darryl Waterhouse & DCBW-IT
5
- * Built: 2026-10-12T06:05:20.912Z
6
- * Commit: a53de2f
5
+ * Built: 2026-10-13T15:14:31.868Z
6
+ * Commit: 85d3945
7
7
  *
8
8
  * Requires: domma.min.js
9
9
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "1.6.37",
3
+ "version": "1.6.39",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -84,7 +84,7 @@
84
84
  "@fastify/rate-limit": "^10.3.0",
85
85
  "@fastify/static": "9.1.1",
86
86
  "bcryptjs": "^3.0.3",
87
- "domma-js": "0.51.11",
87
+ "domma-js": "0.51.13",
88
88
  "dotenv": "^17.2.3",
89
89
  "fastify": "5.8.5",
90
90
  "gray-matter": "^4.0.3",
@@ -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, #e2e8f0);border-radius:var(--dm-radius-md, 8px);overflow:hidden;background:var(--dm-surface, #fff)}.mail-folders-pane,.mail-list-pane{display:flex;flex-direction:column;border-right:1px solid var(--dm-border, #e2e8f0);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, #64748b);border-bottom:1px solid var(--dm-border, #e2e8f0);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, #e2e8f0);color:var(--dm-text-muted, #64748b);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, #64748b);border-bottom:1px solid var(--dm-border, #e2e8f0);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, #2563eb);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, #2563eb);color:var(--dm-primary-text, #fff);flex-shrink:0}.mail-selection span{flex:1;min-width:0}.mail-selection .btn{color:var(--dm-primary-text, #fff);border:1px solid currentColor;background:transparent}.mail-selection .btn:hover{background:color-mix(in srgb,var(--dm-primary-text, #fff) 15%,transparent)}.mail-item.is-selected{background:var(--dm-active-bg, rgba(15, 23, 42, .07));box-shadow:inset 3px 0 0 var(--dm-primary, #2563eb)}.mail-undo{display:flex;align-items:center;gap:.5rem;padding:.4rem .6rem;font-size:.78rem;background:var(--dm-active-bg, rgba(15, 23, 42, .07));border-bottom:1px solid var(--dm-border, #e2e8f0);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, #e2e8f0);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, #2563eb)}.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, #2563eb);color:var(--dm-primary-text, #fff);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, #fff);cursor:pointer;text-align:left;font-size:.78rem;font-weight:700;text-transform:uppercase;letter-spacing:.03em;color:var(--dm-text-muted, #64748b)}.mail-account:hover{background:var(--dm-hover-bg, rgba(15, 23, 42, .04))}.mail-account.is-active{color:var(--dm-text, #1e293b);position:sticky;top:0;z-index:1;background:var(--dm-surface, #fff)}.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, #1e293b)}.mail-folder:hover{background:var(--dm-hover-bg, rgba(15, 23, 42, .04))}.mail-folder.is-active{background:var(--dm-active-bg, rgba(15, 23, 42, .07));color:var(--dm-primary, #2563eb);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, #e2e8f0);background:none;cursor:pointer;color:var(--dm-text, #1e293b)}.mail-item:hover{background:var(--dm-hover-bg, rgba(15, 23, 42, .04))}.mail-item.is-active{background:var(--dm-active-bg, rgba(15, 23, 42, .07))}.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, #64748b);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, #64748b)}.mail-pager{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.4rem .6rem;border-top:1px solid var(--dm-border, #e2e8f0);font-size:.75rem;color:var(--dm-text-muted, #64748b);flex-shrink:0}.mail-reader-head{padding:.75rem 1rem;border-bottom:1px solid var(--dm-border, #e2e8f0);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, #1e293b)}.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, #64748b)}.mail-reader-body{flex:1;position:relative;overflow:hidden;display:flex;flex-direction:column;min-height:0;background:var(--dm-surface, #fff)}.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, #8a6d1f);color:var(--dm-warning-text, #fff);border-bottom:1px solid var(--dm-border, #e2e8f0)}.mail-notice .btn{color:var(--dm-warning-text, #fff);border:1px solid currentColor;background:transparent;opacity:.95}.mail-notice .btn:hover{background:color-mix(in srgb,var(--dm-warning-text, #fff) 15%,transparent);opacity:1}.mail-attachments{display:flex;flex-wrap:wrap;gap:.4rem;padding:.5rem .75rem;border-bottom:1px solid var(--dm-border, #e2e8f0);background:var(--dm-background-alt, rgba(15, 23, 42, .03))}.mail-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;height:100%;color:var(--dm-text-muted, #94a3b8);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, #e2e8f0)}.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, #e2e8f0);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, #2563eb);color:var(--dm-primary-text, #fff);outline:2px solid var(--dm-primary, #2563eb);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, #64748b);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, #2563eb)}.mail-item-tick:hover{color:var(--dm-primary, #2563eb);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, #dc2626)}.mail-item-meta .is-low{color:var(--dm-text-muted, #64748b);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, #64748b);background:var(--dm-active-bg, rgba(15, 23, 42, .07));cursor:pointer}.mail-thread-chip:hover{color:var(--dm-primary, #2563eb)}.mail-thread-chip.is-unread{background:var(--dm-primary, #2563eb);color:var(--dm-primary-text, #fff)}.mail-item.is-in-thread{padding-left:1.6rem;box-shadow:inset 3px 0 0 var(--dm-border, #e2e8f0)}.mail-item.is-in-thread .mail-item-from{font-size:.8rem}.mail-pane-head .btn.is-on{color:var(--dm-primary, #2563eb);background:var(--dm-active-bg, rgba(15, 23, 42, .07))}#mail-outbox-btn{font-size:.72rem;font-weight:700}#mail-outbox-btn.is-failed{color:var(--dm-danger, #dc2626)}.mail-attachment{display:inline-flex;align-items:center;border:1px solid var(--dm-border, #e2e8f0);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, #e2e8f0)!important;opacity:.7}.mail-attachment-save:hover{opacity:1;color:var(--dm-primary, #2563eb)}.mail-section-heading{margin:1rem 0 .1rem;font-size:.8rem;text-transform:uppercase;letter-spacing:.04em;color:var(--dm-text-muted, #64748b)}.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, #64748b);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, #2563eb)}.mail-section-intro{display:flex;align-items:center;gap:.35rem;margin:0 0 .75rem;padding-bottom:.5rem;border-bottom:1px solid var(--dm-border, #e2e8f0)}.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, #e2e8f0);border-radius:4px;padding:.1rem .25rem}
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, #e2e8f0);border-radius:var(--dm-radius-md, 8px);overflow:hidden;background:var(--dm-surface, #fff)}.mail-folders-pane,.mail-list-pane{display:flex;flex-direction:column;border-right:1px solid var(--dm-border, #e2e8f0);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, #64748b);border-bottom:1px solid var(--dm-border, #e2e8f0);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, #e2e8f0);color:var(--dm-text-muted, #64748b);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, #64748b);border-bottom:1px solid var(--dm-border, #e2e8f0);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, #2563eb);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, #2563eb);color:var(--dm-primary-text, #fff);flex-shrink:0}.mail-selection span{flex:1;min-width:0}.mail-selection .btn{color:var(--dm-primary-text, #fff);border:1px solid currentColor;background:transparent}.mail-selection .btn:hover{background:color-mix(in srgb,var(--dm-primary-text, #fff) 15%,transparent)}.mail-item.is-selected{background:var(--dm-active-bg, rgba(15, 23, 42, .07));box-shadow:inset 3px 0 0 var(--dm-primary, #2563eb)}.mail-undo{display:flex;align-items:center;gap:.5rem;padding:.4rem .6rem;font-size:.78rem;background:var(--dm-active-bg, rgba(15, 23, 42, .07));border-bottom:1px solid var(--dm-border, #e2e8f0);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, #e2e8f0);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, #2563eb)}.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, #2563eb);color:var(--dm-primary-text, #fff);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, #fff);cursor:pointer;text-align:left;font-size:.78rem;font-weight:700;text-transform:uppercase;letter-spacing:.03em;color:var(--dm-text-muted, #64748b)}.mail-account:hover{background:var(--dm-hover-bg, rgba(15, 23, 42, .04))}.mail-account.is-active{color:var(--dm-text, #1e293b);position:sticky;top:0;z-index:1;background:var(--dm-surface, #fff)}.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, #1e293b)}.mail-folder:hover{background:var(--dm-hover-bg, rgba(15, 23, 42, .04))}.mail-folder.is-active{background:var(--dm-active-bg, rgba(15, 23, 42, .07));color:var(--dm-primary, #2563eb);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, #e2e8f0);background:none;cursor:pointer;color:var(--dm-text, #1e293b)}.mail-item:hover{background:var(--dm-hover-bg, rgba(15, 23, 42, .04))}.mail-item.is-active{background:var(--dm-active-bg, rgba(15, 23, 42, .07))}.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, #64748b);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, #64748b)}.mail-pager{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.4rem .6rem;border-top:1px solid var(--dm-border, #e2e8f0);font-size:.75rem;color:var(--dm-text-muted, #64748b);flex-shrink:0}.mail-reader-head{padding:.75rem 1rem;border-bottom:1px solid var(--dm-border, #e2e8f0);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, #1e293b)}.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, #64748b)}.mail-reader-body{flex:1;position:relative;overflow:hidden;display:flex;flex-direction:column;min-height:0;background:var(--dm-surface, #fff)}.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, #8a6d1f);color:var(--dm-warning-text, #fff);border-bottom:1px solid var(--dm-border, #e2e8f0)}.mail-notice .btn{color:var(--dm-warning-text, #fff);border:1px solid currentColor;background:transparent;opacity:.95}.mail-notice .btn:hover{background:color-mix(in srgb,var(--dm-warning-text, #fff) 15%,transparent);opacity:1}.mail-attachments{display:flex;flex-wrap:wrap;gap:.4rem;padding:.5rem .75rem;border-bottom:1px solid var(--dm-border, #e2e8f0);background:var(--dm-background-alt, rgba(15, 23, 42, .03))}.mail-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;height:100%;color:var(--dm-text-muted, #94a3b8);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, #e2e8f0)}.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, #e2e8f0);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, #2563eb);color:var(--dm-primary-text, #fff);outline:2px solid var(--dm-primary, #2563eb);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, #64748b);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, #2563eb)}.mail-item-tick:hover{color:var(--dm-primary, #2563eb);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, #dc2626)}.mail-item-meta .is-low{color:var(--dm-text-muted, #64748b);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, #64748b);background:var(--dm-active-bg, rgba(15, 23, 42, .07));cursor:pointer}.mail-thread-chip:hover{color:var(--dm-primary, #2563eb)}.mail-thread-chip.is-unread{background:var(--dm-primary, #2563eb);color:var(--dm-primary-text, #fff)}.mail-item.is-in-thread{padding-left:1.6rem;box-shadow:inset 3px 0 0 var(--dm-border, #e2e8f0)}.mail-item.is-in-thread .mail-item-from{font-size:.8rem}.mail-pane-head .btn.is-on{color:var(--dm-primary, #2563eb);background:var(--dm-active-bg, rgba(15, 23, 42, .07))}#mail-outbox-btn{font-size:.72rem;font-weight:700}#mail-outbox-btn.is-failed{color:var(--dm-danger, #dc2626)}.mail-attachment{display:inline-flex;align-items:center;border:1px solid var(--dm-border, #e2e8f0);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, #e2e8f0)!important;opacity:.7}.mail-attachment-save:hover{opacity:1;color:var(--dm-primary, #2563eb)}.mail-section-heading{margin:1rem 0 .1rem;font-size:.8rem;text-transform:uppercase;letter-spacing:.04em;color:var(--dm-text-muted, #64748b)}.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, #64748b);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, #2563eb)}.mail-section-intro{display:flex;align-items:center;gap:.35rem;margin:0 0 .75rem;padding-bottom:.5rem;border-bottom:1px solid var(--dm-border, #e2e8f0)}.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, #e2e8f0);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}
@@ -0,0 +1,303 @@
1
+ /**
2
+ * "Sign in with Microsoft / Google" for a mailbox - the part of the mailbox
3
+ * form that replaces the password with a provider sign-in (OAuth2/XOAUTH2).
4
+ *
5
+ * The customer registers their OWN app with the provider, so the panel
6
+ * carries the step-by-step guide for each, with this site's exact redirect
7
+ * URI and a Copy button (the Docs Pro cloud-copies design). Connect opens the
8
+ * provider in a popup; the server's callback page tells this window it is
9
+ * done, and when it cannot (an admin opened on another address than the
10
+ * site's) the closed popup is noticed instead.
11
+ *
12
+ * Shown only when the edition offers it (`features.oauth`) AND the server
13
+ * answers `GET /oauth/status` - an engine without the routes says 404 and the
14
+ * form stays password-only.
15
+ *
16
+ * @module _lib/admin/mail/oauth-panel
17
+ */
18
+
19
+ const esc = (s) => String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
20
+
21
+ /** What a stored sign-in's status says, and how it looks. Pure. */
22
+ export function describeSignIn(box) {
23
+ const o = box?.oauth;
24
+ if (box?.auth !== 'oauth' || !o) return null;
25
+ const who = o.provider === 'google' ? 'Google' : 'Microsoft';
26
+ if (o.status === 'ok') return {tone: 'ok', chip: 'Connected', text: `Signed in with ${who}.`};
27
+ if (o.status === 'disconnected') return {tone: 'quiet', chip: 'Disconnected', text: o.error || 'Disconnected. Reconnect to use this mailbox again.'};
28
+ return {tone: 'bad', chip: 'Needs reconnecting', text: o.error || `The ${who} sign-in has expired. Reconnect to sign in again.`};
29
+ }
30
+
31
+ /**
32
+ * The setup guide for each provider, written from the vendors' documentation
33
+ * (2026-10). `uri` is this site's exact return address.
34
+ *
35
+ * @param {'microsoft'|'google'} provider
36
+ * @param {string} uri
37
+ * @returns {string} HTML
38
+ */
39
+ export function providerGuide(provider, uri) {
40
+ const code = `<code class="mail-oauth-code">${esc(uri)}</code> <button type="button" class="btn btn-xs btn-ghost" data-act="copy-uri">Copy</button>`;
41
+ if (provider === 'microsoft') {
42
+ return `<p>You register your own small app with Microsoft, so your mail is read with your sign-in and nobody else's app is involved. Works for Microsoft 365 (work or school) and Outlook.com.</p><ol>
43
+ <li>Go to <a href="https://entra.microsoft.com/" target="_blank" rel="noopener">entra.microsoft.com</a> (or the Azure portal) &gt; <strong>App registrations &gt; New registration</strong>.</li>
44
+ <li>Name it (for example "Domma Mail"). Supported account types: <strong>Accounts in any organizational directory and personal Microsoft accounts</strong> - or <strong>this organizational directory only</strong> for one company's mailboxes (then put its Directory (tenant) ID in Tenant below). Redirect URI: platform <strong>Web</strong>, exactly:<br>${code}</li>
45
+ <li>Copy the <strong>Application (client) ID</strong> from the Overview.</li>
46
+ <li><strong>Certificates &amp; secrets &gt; New client secret</strong>. Copy its <strong>Value</strong> (not the Secret ID) straight away - it is shown once. Note when it expires: on that day the mailbox needs reconnecting with a new secret.</li>
47
+ <li><strong>API permissions &gt; Add a permission &gt; Microsoft Graph &gt; Delegated</strong>: tick <code>IMAP.AccessAsUser.All</code>, <code>SMTP.Send</code>, <code>offline_access</code>, <code>openid</code> and <code>email</code>.</li>
48
+ <li><strong>Microsoft 365:</strong> the mailbox needs <strong>IMAP</strong> and <strong>Authenticated SMTP</strong> switched on (Microsoft 365 admin center &gt; Users &gt; the user &gt; Mail &gt; Manage email apps). Your organisation may also have to consent to the app. <strong>Outlook.com:</strong> turn on IMAP under Settings &gt; Mail &gt; Forwarding and IMAP.</li>
49
+ <li>Fill in the fields here and press <strong>Connect</strong>, then sign in as the mailbox. For a Microsoft 365 <em>shared</em> mailbox, type its address in Email address and sign in as someone with Full Access to it.</li></ol>`;
50
+ }
51
+ return `<p>You make your own small Google app, so your mail is read with your sign-in and nobody else's app is involved. Works for Gmail and Google Workspace.</p><ol>
52
+ <li>Go to <a href="https://console.cloud.google.com/" target="_blank" rel="noopener">console.cloud.google.com</a> and create a project (or choose one). Under <strong>APIs &amp; Services &gt; Library</strong> enable the <strong>Gmail API</strong>.</li>
53
+ <li>Set up the <strong>OAuth consent screen</strong> (newer consoles: <strong>Google Auth Platform</strong>): an app name such as "Domma Mail" and your email. Audience: <strong>Internal</strong> on Google Workspace - no review and no time limit. For a personal Gmail account it has to be <strong>External</strong>.</li>
54
+ <li><strong>Data access</strong>: add the scope <code>https://mail.google.com/</code>. Google calls it <em>restricted</em>: an External app used by the public needs Google's verification and a security assessment. For your own mailboxes (under 100 users) you can skip that and click through Google's "unverified app" warning (Advanced &gt; Go to the app).</li>
55
+ <li><strong>Publish the app</strong> (Audience &gt; Publish app, <em>In production</em>). While an External app says <em>Testing</em>, Google ends the sign-in after <strong>7 days</strong> and the mailbox needs reconnecting every week.</li>
56
+ <li><strong>Clients &gt; Create client</strong> (or Credentials &gt; OAuth client ID): type <strong>Web application</strong>. Under <strong>Authorised redirect URIs</strong> add exactly:<br>${code}</li>
57
+ <li>Copy the <strong>Client ID</strong> and <strong>Client secret</strong> into the fields here and press <strong>Connect</strong>, then sign in as the mailbox. On Google Workspace an administrator may have to allow the app (Admin console &gt; Security &gt; API controls).</li></ol>`;
58
+ }
59
+
60
+ const TONE = {ok: 'text-success', bad: 'text-danger', quiet: 'text-muted'};
61
+
62
+ const TEMPLATE = `
63
+ <div class="mail-oauth">
64
+ <p class="mail-oauth-warn text-sm" data-bind-hidden="https.value">This site's address starts with <strong>http://</strong>. Microsoft and Google only return to <strong>https://</strong> addresses: set the Site URL (with https) in Settings &gt; General first.</p>
65
+ <div class="mail-oauth-state" data-bind-hidden="!hasState.value">
66
+ <span data-bind-class="'badge mail-oauth-chip mail-oauth-chip--' + stateTone.value" data-bind-text="stateChip.value"></span>
67
+ <span class="text-sm" data-bind-text="stateText.value"></span>
68
+ </div>
69
+ <details class="mail-oauth-guide">
70
+ <summary>How to set this up <span data-help-title="Your own app" data-help="Each site signs in with an app YOU register with Microsoft or Google - so nobody else's app stands between you and your mail, and you can withdraw it at any time from the provider's side."></span></summary>
71
+ <div class="mail-oauth-guide-body text-sm"></div>
72
+ </details>
73
+ <div class="mail-form-stack">
74
+ <div>
75
+ <label class="form-label" for="mail-oauth-label">Name</label>
76
+ <input id="mail-oauth-label" class="form-input" maxlength="80" data-model="label.value" placeholder="What to call this mailbox">
77
+ </div>
78
+ <div>
79
+ <label class="form-label" for="mail-oauth-user">Email address <span data-help="Leave blank to use the account you sign in as. Type an address only for a Microsoft 365 shared mailbox you have Full Access to."></span></label>
80
+ <input id="mail-oauth-user" class="form-input" type="email" maxlength="254" data-model="username.value" data-bind-disabled="locked.value" placeholder="Optional - the account you sign in as">
81
+ </div>
82
+ <div>
83
+ <label class="form-label" for="mail-oauth-client" data-bind-text="clientIdLabel.value">Client ID</label>
84
+ <input id="mail-oauth-client" class="form-input" maxlength="200" autocomplete="off" spellcheck="false" data-model="clientId.value">
85
+ </div>
86
+ <div>
87
+ <label class="form-label" for="mail-oauth-secret">Client secret <span data-help="Stored sealed on this server and never sent back to the browser. Microsoft secrets expire (you chose when): on that day the mailbox needs reconnecting with a new one."></span></label>
88
+ <input id="mail-oauth-secret" class="form-input" type="password" maxlength="2000" autocomplete="new-password" data-model="clientSecret.value" data-bind-placeholder="secretPlaceholder.value">
89
+ </div>
90
+ <div data-bind-hidden="!isMicrosoft.value">
91
+ <label class="form-label" for="mail-oauth-tenant">Tenant <span data-help="Leave as common unless the app is registered for your organisation only - then its Directory (tenant) ID."></span></label>
92
+ <input id="mail-oauth-tenant" class="form-input" maxlength="100" spellcheck="false" data-model="tenant.value" placeholder="common">
93
+ </div>
94
+ </div>
95
+ <div data-bind-class="'text-sm mail-status ' + statusClass.value" data-bind-text="status.value"></div>
96
+ <div class="flex justify-between gap-2 mt-2">
97
+ <div class="flex gap-2">
98
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="test" data-bind-hidden="!canTest.value" data-bind-disabled="busy.value">Test</button>
99
+ <button type="button" class="btn btn-sm btn-ghost text-danger" data-on-click="disconnect" data-bind-hidden="!canDisconnect.value" data-bind-disabled="busy.value">Disconnect</button>
100
+ </div>
101
+ <div class="flex gap-2">
102
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="cancel">Cancel</button>
103
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="saveName" data-bind-hidden="!isSaved.value" data-bind-disabled="busy.value">Save name</button>
104
+ <button type="button" class="btn btn-sm btn-primary" data-on-click="connect" data-bind-disabled="busy.value" data-bind-text="connectLabel.value">Connect</button>
105
+ </div>
106
+ </div>
107
+ </div>`;
108
+
109
+ /**
110
+ * Mount the sign-in panel.
111
+ *
112
+ * @param {HTMLElement} host
113
+ * @param {{provider: 'microsoft'|'google', box: object|null, base: string, api: Function,
114
+ * status: {redirectUri: string, https: boolean}, helpPopovers: Function,
115
+ * onConnected: (accountId: string|null) => void, onChanged: () => void, onCancel: () => void}} o
116
+ * @returns {{dispose: () => void, dirty: () => boolean, setProvider: (p: string) => void}}
117
+ */
118
+ export function mountSignIn(host, {provider: first, box, base, api, status, helpPopovers, onConnected, onChanged, onCancel}) {
119
+ const signedIn = box?.auth === 'oauth' ? box.oauth : null;
120
+ const provider = M.observable(signedIn?.provider ?? first);
121
+ const label = M.observable(box?.label ?? '');
122
+ const username = M.observable('');
123
+ const clientId = M.observable(signedIn?.clientId ?? '');
124
+ const clientSecret = M.observable('');
125
+ const tenant = M.observable(signedIn?.tenant ?? 'common');
126
+ const busy = M.observable(false);
127
+ const said = M.observable('');
128
+ const tone = M.observable('quiet');
129
+ const current = M.observable(box);
130
+ const say = (text, t = 'quiet') => { said.value = text ?? ''; tone.value = t; };
131
+
132
+ const opened = JSON.stringify({l: label.peek(), u: username.peek(), c: clientId.peek(), t: tenant.peek()});
133
+
134
+ const holder = document.createElement('div');
135
+ holder.innerHTML = TEMPLATE.trim();
136
+ const root = holder.firstElementChild;
137
+ const guideBody = root.querySelector('.mail-oauth-guide-body');
138
+ const guide = root.querySelector('.mail-oauth-guide');
139
+ // Open for a first setup, folded once there is a sign-in to look at.
140
+ if (!signedIn) guide.open = true;
141
+ host.appendChild(root);
142
+ // Help markers before binding (they stay where they are), then bind.
143
+ // (Computeds below read observables only - never another computed.)
144
+ const stopHelp = helpPopovers(root, {position: 'bottom'});
145
+
146
+ const stopGuide = M.effect(() => { guideBody.innerHTML = providerGuide(provider.value, status.redirectUri); });
147
+ root.addEventListener('click', (event) => {
148
+ if (!event.target.closest('[data-act="copy-uri"]')) return;
149
+ navigator.clipboard?.writeText(status.redirectUri)
150
+ .then(() => E.toast('Copied', {type: 'success'}))
151
+ .catch(() => E.toast('Copy it by hand - the browser refused.', {type: 'warning'}));
152
+ });
153
+
154
+ let popup = null;
155
+ let watch = null;
156
+ const onMessage = (event) => {
157
+ // Our window, our page, our origin: anything else posting this shape is ignored.
158
+ if (!popup || event.source !== popup || event.origin !== location.origin || event.data?.type !== 'domma-mail-oauth') return;
159
+ finish(event.data.ok, event.data.message, event.data.accountId ?? null);
160
+ };
161
+ window.addEventListener('message', onMessage);
162
+
163
+ function stopWatching() {
164
+ if (watch) clearInterval(watch);
165
+ watch = null;
166
+ }
167
+
168
+ function finish(ok, message, accountId) {
169
+ stopWatching();
170
+ popup = null;
171
+ busy.value = false;
172
+ if (ok) {
173
+ say(message || 'Connected.', 'ok');
174
+ clientSecret.value = '';
175
+ onConnected(accountId);
176
+ } else {
177
+ say(message || 'The sign-in was not completed.', 'bad');
178
+ }
179
+ }
180
+
181
+ async function connect() {
182
+ // The window must open inside the click itself, or the browser blocks it.
183
+ popup = window.open('about:blank', 'domma-mail-oauth', 'width=560,height=720');
184
+ if (!popup) {
185
+ say('The browser blocked the sign-in window. Allow pop-ups for this site and try again.', 'bad');
186
+ return;
187
+ }
188
+ busy.value = true;
189
+ say('Waiting for you to sign in…');
190
+ try {
191
+ const r = await api(`${base}/oauth/start`, 'POST', {
192
+ accountId: box?.id ?? null,
193
+ provider: provider.peek(),
194
+ clientId: clientId.peek().trim(),
195
+ clientSecret: clientSecret.peek(),
196
+ tenant: tenant.peek().trim(),
197
+ label: label.peek().trim(),
198
+ username: username.peek().trim()
199
+ });
200
+ popup.location.href = r.url;
201
+ // The callback page posts back when it can; if the admin is open on
202
+ // another address than the site's, the message cannot arrive - so a
203
+ // closed window also counts as "done, go and look".
204
+ watch = setInterval(() => {
205
+ if (popup && popup.closed) finish(null, null, null);
206
+ }, 1000);
207
+ } catch (err) {
208
+ try { popup.close(); } catch { /* gone */ }
209
+ popup = null;
210
+ busy.value = false;
211
+ say(err.message, 'bad');
212
+ }
213
+ }
214
+
215
+ const bindings = M.applyBindings({
216
+ label, username, clientId, clientSecret, tenant, busy,
217
+ https: M.computed(() => status.https !== false),
218
+ isMicrosoft: M.computed(() => provider.value === 'microsoft'),
219
+ clientIdLabel: M.computed(() => (provider.value === 'microsoft' ? 'Application (client) ID' : 'Client ID')),
220
+ secretPlaceholder: M.computed(() => (signedIn?.hasClientSecret && signedIn.provider === provider.value
221
+ ? 'Saved - leave blank to keep it' : (provider.value === 'microsoft' ? 'The secret\'s Value' : 'Client secret'))),
222
+ locked: M.computed(() => Boolean(signedIn)),
223
+ hasState: M.computed(() => Boolean(describeSignIn(current.value))),
224
+ stateChip: M.computed(() => describeSignIn(current.value)?.chip ?? ''),
225
+ stateText: M.computed(() => describeSignIn(current.value)?.text ?? ''),
226
+ stateTone: M.computed(() => describeSignIn(current.value)?.tone ?? 'quiet'),
227
+ isSaved: M.computed(() => Boolean(signedIn)),
228
+ canTest: M.computed(() => current.value?.auth === 'oauth' && current.value.oauth?.status === 'ok'),
229
+ canDisconnect: M.computed(() => current.value?.auth === 'oauth' && current.value.oauth?.status !== 'disconnected'),
230
+ connectLabel: M.computed(() => (signedIn ? 'Reconnect' : `Connect with ${provider.value === 'microsoft' ? 'Microsoft' : 'Google'}`)),
231
+ status: said,
232
+ statusClass: M.computed(() => TONE[tone.value] ?? TONE.quiet)
233
+ }, root, {
234
+ methods: {
235
+ connect: () => { connect(); },
236
+ cancel: () => onCancel(),
237
+ async test() {
238
+ busy.value = true;
239
+ say('Signing in to both servers…');
240
+ try {
241
+ const r = await api(`${base}/oauth/${encodeURIComponent(box.id)}/test`, 'POST', {});
242
+ say(r.ok ? 'Reading and sending both signed in.'
243
+ : [r.imap?.ok ? '' : `Reading: ${r.imap?.error}`, r.smtp?.ok ? '' : `Sending: ${r.smtp?.error}`].filter(Boolean).join(' '),
244
+ r.ok ? 'ok' : 'bad');
245
+ } catch (err) {
246
+ say(err.message, 'bad');
247
+ } finally {
248
+ busy.value = false;
249
+ }
250
+ },
251
+ async disconnect() {
252
+ const who = current.peek()?.oauth?.provider === 'google' ? 'Google' : 'Microsoft';
253
+ const note = who === 'Google'
254
+ ? 'The sign-in is handed back to Google and forgotten here.'
255
+ : 'The sign-in is forgotten here. Microsoft gives apps no way to hand one back: to cut it off at Microsoft too, delete the app\'s client secret in Entra.';
256
+ if (!await E.confirm(`Disconnect "${esc(current.peek()?.label ?? '')}"? ${note} The mailbox and its settings stay; Reconnect to use it again.`)) return;
257
+ busy.value = true;
258
+ try {
259
+ const r = await api(`${base}/oauth/${encodeURIComponent(box.id)}/disconnect`, 'POST', {});
260
+ current.value = r.account;
261
+ say(r.revoked ? 'Disconnected, and the sign-in was handed back.' : 'Disconnected.', 'quiet');
262
+ onChanged();
263
+ } catch (err) {
264
+ say(err.message, 'bad');
265
+ } finally {
266
+ busy.value = false;
267
+ }
268
+ },
269
+ async saveName() {
270
+ busy.value = true;
271
+ try {
272
+ const r = await api(`${base}/accounts/${encodeURIComponent(box.id)}`, 'PUT', {label: label.peek().trim()});
273
+ current.value = r.account;
274
+ say('Saved.', 'ok');
275
+ onChanged();
276
+ } catch (err) {
277
+ say(err.message, 'bad');
278
+ } finally {
279
+ busy.value = false;
280
+ }
281
+ }
282
+ }
283
+ });
284
+ Domma.icons?.scan?.(root);
285
+
286
+ return {
287
+ setProvider(p) {
288
+ if (signedIn) return;
289
+ provider.value = p;
290
+ say('');
291
+ },
292
+ dirty: () => clientSecret.peek() !== ''
293
+ || JSON.stringify({l: label.peek(), u: username.peek(), c: clientId.peek(), t: tenant.peek()}) !== opened,
294
+ dispose() {
295
+ stopWatching();
296
+ window.removeEventListener('message', onMessage);
297
+ try { if (popup && !popup.closed) popup.close(); } catch { /* gone */ }
298
+ try { stopGuide?.dispose?.(); } catch { /* gone */ }
299
+ try { bindings?.dispose?.(); } catch { /* gone */ }
300
+ try { stopHelp?.(); } catch { /* gone */ }
301
+ }
302
+ };
303
+ }
@@ -50,6 +50,9 @@ const {fillMessageAction, loadMessageActions, messageFacts} = await import(versi
50
50
  const {isPlainKey, isTyping, 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
+ // Sign in with Microsoft / Google (Email Pro). Optional: an older copy of this
54
+ // directory without it must not take the reader down.
55
+ const {describeSignIn, mountSignIn} = await import(versioned('oauth-panel.js')).catch(() => ({}));
53
56
 
54
57
  /**
55
58
  * Call a plugin route, returning parsed JSON.
@@ -975,7 +978,17 @@ return {
975
978
  }
976
979
  const sub = document.createElement('div');
977
980
  sub.className = 'text-xs text-muted mail-box-sub';
978
- sub.textContent = `${box.username} \u00b7 ${box.host}:${box.port}${box.secure ? '' : ' (no TLS)'}`;
981
+ const signIn = describeSignIn?.(box) ?? null;
982
+ sub.textContent = signIn
983
+ ? `${box.username} \u00b7 ${box.oauth.provider === 'google' ? 'Google' : 'Microsoft'} sign-in`
984
+ : `${box.username} \u00b7 ${box.host}:${box.port}${box.secure ? '' : ' (no TLS)'}`;
985
+ if (signIn && signIn.tone !== 'ok') {
986
+ const chip = document.createElement('span');
987
+ chip.className = `badge mail-oauth-chip mail-oauth-chip--${signIn.tone}`;
988
+ chip.textContent = signIn.chip;
989
+ chip.title = signIn.text;
990
+ name.appendChild(chip);
991
+ }
979
992
  info.appendChild(name);
980
993
  info.appendChild(sub);
981
994
  row.appendChild(info);
@@ -1060,13 +1073,19 @@ return {
1060
1073
 
1061
1074
  const form = document.createElement('div');
1062
1075
  form.className = 'mail-form-stack';
1063
-
1064
- const labelInput = field(form, 'label', 'Name', 'text', box?.label ?? '', 'What to call this mailbox.');
1065
- const hostInput = field(form, 'host', 'IMAP server', 'text', box?.host ?? '', 'e.g. mail.example.com');
1066
- const portInput = field(form, 'port', 'Port', 'number', String(box?.port ?? 993));
1067
- const userInput = field(form, 'username', 'Username', 'text', box?.username ?? '');
1076
+ // The password form lives in its own wrapper so "Sign in with
1077
+ // Microsoft / Google" can stand in for it (Email Pro).
1078
+ const pwWrap = document.createElement('div');
1079
+ pwWrap.className = 'mail-form-stack';
1080
+ form.appendChild(pwWrap);
1081
+ const signedIn = box?.auth === 'oauth';
1082
+
1083
+ const labelInput = field(pwWrap, 'label', 'Name', 'text', box?.label ?? '', 'What to call this mailbox.');
1084
+ const hostInput = field(pwWrap, 'host', 'IMAP server', 'text', box?.host ?? '', 'e.g. mail.example.com');
1085
+ const portInput = field(pwWrap, 'port', 'Port', 'number', String(box?.port ?? 993));
1086
+ const userInput = field(pwWrap, 'username', 'Username', 'text', box?.username ?? '');
1068
1087
  const passInput = field(
1069
- form, 'password', 'Password', 'password', '',
1088
+ pwWrap, 'password', 'Password', 'password', '',
1070
1089
  box?.hasPassword ? 'Leave blank to keep the saved password.' : ''
1071
1090
  );
1072
1091
 
@@ -1077,7 +1096,7 @@ return {
1077
1096
  secureInput.checked = box ? box.secure !== false : true;
1078
1097
  secureWrap.appendChild(secureInput);
1079
1098
  secureWrap.appendChild(document.createTextNode('Use TLS (port 993)'));
1080
- form.appendChild(secureWrap);
1099
+ pwWrap.appendChild(secureWrap);
1081
1100
 
1082
1101
  // Keep the port in step with the TLS choice until the user sets one
1083
1102
  // themselves - 993/143 is the only pair that matters here.
@@ -1109,7 +1128,7 @@ return {
1109
1128
  status.className = 'text-sm mail-status '
1110
1129
  + ({ok: 'text-success', bad: 'text-danger'}[tone] ?? 'text-muted');
1111
1130
  };
1112
- form.appendChild(status);
1131
+ pwWrap.appendChild(status);
1113
1132
 
1114
1133
  const actions = document.createElement('div');
1115
1134
  actions.className = 'flex justify-between gap-2 mt-2';
@@ -1131,17 +1150,111 @@ return {
1131
1150
 
1132
1151
  actions.appendChild(testBtn);
1133
1152
  actions.appendChild(right);
1134
- form.appendChild(actions);
1153
+ pwWrap.appendChild(actions);
1135
1154
 
1136
1155
  slideover.element.appendChild(form);
1137
1156
  // What it opened with; a typed password always counts as a change.
1138
1157
  const opened = JSON.stringify({...collect(), password: ''});
1158
+ let signInPanel = null;
1159
+ let stopChoiceHelp = null;
1160
+ let method = signedIn ? 'oauth' : 'password';
1139
1161
  guardPanel(slideover,
1140
- () => passInput.value !== '' || JSON.stringify({...collect(), password: ''}) !== opened,
1162
+ () => (method === 'password'
1163
+ ? passInput.value !== '' || JSON.stringify({...collect(), password: ''}) !== opened
1164
+ : Boolean(signInPanel?.dirty())),
1141
1165
  box ? 'Discard your changes to this mailbox?' : 'Discard this mailbox? It has not been saved.',
1142
- () => stopFormResize());
1166
+ () => { stopFormResize(); signInPanel?.dispose(); stopChoiceHelp?.(); });
1143
1167
  slideover.open();
1144
- setTimeout(() => hostInput.focus(), 50);
1168
+ if (signedIn) pwWrap.hidden = true;
1169
+ else setTimeout(() => hostInput.focus(), 50);
1170
+
1171
+ /**
1172
+ * Offer "Sign in with Microsoft / Google" when this edition has it
1173
+ * and the server answers for it. A signed-in mailbox opens on its
1174
+ * sign-in; a password one can be moved onto one.
1175
+ */
1176
+ (async () => {
1177
+ const oauth = features.oauth && mountSignIn
1178
+ ? await api(`${BASE}/oauth/status`).catch(() => null)
1179
+ : null;
1180
+ if (!oauth?.available) {
1181
+ if (signedIn) {
1182
+ const note = document.createElement('p');
1183
+ note.className = 'text-sm text-muted';
1184
+ note.textContent = `This mailbox signs in with ${box.oauth?.provider === 'google' ? 'Google' : 'Microsoft'} instead of a password. `
1185
+ + 'Its sign-in is managed in Email Pro.';
1186
+ form.prepend(note);
1187
+ }
1188
+ return;
1189
+ }
1190
+
1191
+ const signInHost = document.createElement('div');
1192
+ const choices = [
1193
+ ...(signedIn ? [] : [{id: 'password', text: 'Password'}]),
1194
+ {id: 'microsoft', text: 'Sign in with Microsoft'},
1195
+ {id: 'google', text: 'Sign in with Google'}
1196
+ ].filter(c => !signedIn || c.id === box.oauth.provider);
1197
+
1198
+ const mount = (provider) => {
1199
+ if (signInPanel) {
1200
+ signInPanel.setProvider(provider);
1201
+ return;
1202
+ }
1203
+ signInPanel = mountSignIn(signInHost, {
1204
+ provider, box, base: BASE, api, status: oauth, helpPopovers,
1205
+ onConnected: async (accountId) => {
1206
+ slideover.close(true);
1207
+ E.toast('Mailbox connected.', {type: 'success'});
1208
+ if (accountId) currentAccountId = accountId;
1209
+ await start();
1210
+ },
1211
+ onChanged: async () => {
1212
+ mailboxes = (await api(`${BASE}/accounts`)).accounts;
1213
+ renderFolders();
1214
+ },
1215
+ onCancel: () => slideover.close()
1216
+ });
1217
+ };
1218
+
1219
+ if (!signedIn) {
1220
+ const bar = document.createElement('div');
1221
+ bar.className = 'mail-auth-choice';
1222
+ bar.setAttribute('role', 'radiogroup');
1223
+ bar.setAttribute('aria-label', 'How this mailbox signs in');
1224
+ const buttons = choices.map(choice => {
1225
+ const b = document.createElement('button');
1226
+ b.type = 'button';
1227
+ b.className = 'btn btn-sm ' + (choice.id === 'password' ? 'btn-primary' : 'btn-ghost');
1228
+ b.setAttribute('role', 'radio');
1229
+ b.setAttribute('aria-checked', String(choice.id === 'password'));
1230
+ b.textContent = choice.text;
1231
+ b.addEventListener('click', () => {
1232
+ method = choice.id === 'password' ? 'password' : 'oauth';
1233
+ for (const other of buttons) {
1234
+ const on = other === b;
1235
+ other.className = 'btn btn-sm ' + (on ? 'btn-primary' : 'btn-ghost');
1236
+ other.setAttribute('aria-checked', String(on));
1237
+ }
1238
+ pwWrap.hidden = method !== 'password';
1239
+ signInHost.hidden = method === 'password';
1240
+ if (method === 'oauth') mount(choice.id);
1241
+ });
1242
+ return b;
1243
+ });
1244
+ bar.append(...buttons);
1245
+ const help = document.createElement('span');
1246
+ help.setAttribute('data-help-title', 'Password or sign-in');
1247
+ help.setAttribute('data-help', 'Microsoft 365, Outlook.com and Gmail can be connected by signing in with them instead of typing a password - no app password needed. You register a small app of your own with the provider first; the form shows how.');
1248
+ bar.appendChild(help);
1249
+ form.prepend(bar);
1250
+ stopChoiceHelp = helpPopovers(bar, {position: 'bottom'});
1251
+ signInHost.hidden = true;
1252
+ form.appendChild(signInHost);
1253
+ } else {
1254
+ form.appendChild(signInHost);
1255
+ mount(box.oauth.provider);
1256
+ }
1257
+ })();
1145
1258
 
1146
1259
  /**
1147
1260
  * Collect the form into the shape the API expects.