@remit/ui 0.0.1
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/package.json +50 -0
- package/src/components/address-display.render.test.ts +93 -0
- package/src/components/address-display.stories.tsx +55 -0
- package/src/components/address-display.tsx +132 -0
- package/src/components/address-tag.render.test.ts +39 -0
- package/src/components/address-tag.stories.tsx +41 -0
- package/src/components/address-tag.tsx +38 -0
- package/src/components/app-password-hint.render.test.ts +23 -0
- package/src/components/app-password-hint.stories.tsx +19 -0
- package/src/components/app-password-hint.tsx +24 -0
- package/src/components/app-shell-slotted.render.test.ts +128 -0
- package/src/components/app-shell-slotted.tsx +267 -0
- package/src/components/app-shell-types.ts +328 -0
- package/src/components/app-shell.render.test.ts +415 -0
- package/src/components/app-shell.tsx +277 -0
- package/src/components/auth-card.render.test.ts +24 -0
- package/src/components/auth-card.stories.tsx +50 -0
- package/src/components/auth-card.tsx +32 -0
- package/src/components/auth-footer.render.test.ts +17 -0
- package/src/components/auth-footer.stories.tsx +23 -0
- package/src/components/auth-footer.tsx +14 -0
- package/src/components/auth-hero.render.test.ts +31 -0
- package/src/components/auth-hero.stories.tsx +19 -0
- package/src/components/auth-hero.tsx +41 -0
- package/src/components/auto-moved-badge.render.test.ts +42 -0
- package/src/components/auto-moved-badge.stories.tsx +48 -0
- package/src/components/auto-moved-badge.tsx +57 -0
- package/src/components/avatar.tsx +60 -0
- package/src/components/badge.tsx +49 -0
- package/src/components/banner.render.test.ts +58 -0
- package/src/components/banner.stories.tsx +92 -0
- package/src/components/banner.tsx +81 -0
- package/src/components/bottom-sheet.render.test.ts +48 -0
- package/src/components/bottom-sheet.stories.tsx +71 -0
- package/src/components/bottom-sheet.tsx +146 -0
- package/src/components/brief-section.render.test.ts +98 -0
- package/src/components/brief-section.stories.tsx +68 -0
- package/src/components/brief-section.tsx +102 -0
- package/src/components/brief-sections.render.test.ts +72 -0
- package/src/components/brief-sections.stories.tsx +178 -0
- package/src/components/brief-sections.tsx +201 -0
- package/src/components/button.tsx +48 -0
- package/src/components/card.tsx +48 -0
- package/src/components/category-badge.render.test.ts +40 -0
- package/src/components/category-badge.stories.tsx +37 -0
- package/src/components/category-badge.tsx +71 -0
- package/src/components/checkbox.render.test.ts +65 -0
- package/src/components/checkbox.stories.tsx +77 -0
- package/src/components/checkbox.tsx +78 -0
- package/src/components/compose-action-bar.render.test.ts +47 -0
- package/src/components/compose-action-bar.stories.tsx +35 -0
- package/src/components/compose-action-bar.tsx +95 -0
- package/src/components/compose-form-shell.render.test.ts +54 -0
- package/src/components/compose-form-shell.stories.tsx +96 -0
- package/src/components/compose-form-shell.tsx +49 -0
- package/src/components/danger-zone-section.tsx +36 -0
- package/src/components/dialog.render.test.ts +55 -0
- package/src/components/dialog.tsx +104 -0
- package/src/components/email-frame-css.ts +137 -0
- package/src/components/field-label.render.test.ts +16 -0
- package/src/components/field-label.tsx +19 -0
- package/src/components/filter-sheet.render.test.ts +76 -0
- package/src/components/filter-sheet.stories.tsx +146 -0
- package/src/components/filter-sheet.tsx +316 -0
- package/src/components/folder-role.tsx +66 -0
- package/src/components/input.tsx +44 -0
- package/src/components/intelligence-panel.stories.tsx +149 -0
- package/src/components/intelligence-panel.tsx +546 -0
- package/src/components/isolated-email-frame.render.test.ts +147 -0
- package/src/components/isolated-email-frame.stories.tsx +192 -0
- package/src/components/isolated-email-frame.tsx +265 -0
- package/src/components/kbd.tsx +17 -0
- package/src/components/keyboard-hint-bar.tsx +58 -0
- package/src/components/list-item.tsx +39 -0
- package/src/components/mail-action-toolbar.render.test.ts +43 -0
- package/src/components/mail-action-toolbar.tsx +180 -0
- package/src/components/mail-header.render.test.ts +69 -0
- package/src/components/mail-header.stories.tsx +233 -0
- package/src/components/mail-header.tsx +123 -0
- package/src/components/message-body-view.render.test.ts +59 -0
- package/src/components/message-body-view.stories.tsx +146 -0
- package/src/components/message-body-view.tsx +150 -0
- package/src/components/message-header.render.test.ts +58 -0
- package/src/components/message-header.stories.tsx +79 -0
- package/src/components/message-header.tsx +59 -0
- package/src/components/message-list-pane.render.test.ts +136 -0
- package/src/components/message-list-pane.stories.tsx +246 -0
- package/src/components/message-list-pane.tsx +248 -0
- package/src/components/message-list-state.render.test.ts +72 -0
- package/src/components/message-list-state.tsx +97 -0
- package/src/components/message-row.render.test.ts +48 -0
- package/src/components/message-row.stories.tsx +121 -0
- package/src/components/message-row.tsx +231 -0
- package/src/components/mobile-message-action-bar.render.test.ts +87 -0
- package/src/components/mobile-message-action-bar.stories.tsx +57 -0
- package/src/components/mobile-message-action-bar.tsx +175 -0
- package/src/components/mobile-reading-pane.render.test.ts +102 -0
- package/src/components/mobile-reading-pane.tsx +188 -0
- package/src/components/mobile-search-view.stories.tsx +247 -0
- package/src/components/mobile-search-view.tsx +100 -0
- package/src/components/move-mailbox-picker.render.test.ts +113 -0
- package/src/components/move-mailbox-picker.stories.tsx +76 -0
- package/src/components/move-mailbox-picker.tsx +324 -0
- package/src/components/nav-sidebar.render.test.ts +351 -0
- package/src/components/nav-sidebar.stories.tsx +216 -0
- package/src/components/nav-sidebar.tsx +625 -0
- package/src/components/outbox-row.render.test.ts +61 -0
- package/src/components/outbox-row.stories.tsx +53 -0
- package/src/components/outbox-row.tsx +120 -0
- package/src/components/outbox-status-badge.tsx +60 -0
- package/src/components/pane-layout.render.test.ts +105 -0
- package/src/components/popover-menu.render.test.ts +32 -0
- package/src/components/popover-menu.stories.tsx +57 -0
- package/src/components/popover-menu.tsx +110 -0
- package/src/components/primitives.stories.tsx +173 -0
- package/src/components/pull-to-refresh.render.test.ts +46 -0
- package/src/components/pull-to-refresh.stories.tsx +71 -0
- package/src/components/pull-to-refresh.tsx +54 -0
- package/src/components/quoted-text.render.test.ts +39 -0
- package/src/components/quoted-text.stories.tsx +67 -0
- package/src/components/quoted-text.tsx +67 -0
- package/src/components/reading-pane-empty.tsx +44 -0
- package/src/components/reading-pane.render.test.ts +148 -0
- package/src/components/reading-pane.stories.tsx +194 -0
- package/src/components/reading-pane.tsx +351 -0
- package/src/components/rescue-banner.render.test.ts +32 -0
- package/src/components/rescue-banner.stories.tsx +31 -0
- package/src/components/rescue-banner.tsx +50 -0
- package/src/components/rescue-candidate-row.render.test.ts +68 -0
- package/src/components/rescue-candidate-row.stories.tsx +108 -0
- package/src/components/rescue-candidate-row.tsx +94 -0
- package/src/components/rescue-from-spam-flow.render.test.ts +100 -0
- package/src/components/rescue-from-spam-flow.stories.tsx +92 -0
- package/src/components/rescue-from-spam-flow.tsx +342 -0
- package/src/components/resizable.tsx +55 -0
- package/src/components/role-appointment-list.render.test.ts +97 -0
- package/src/components/role-appointment-list.stories.tsx +138 -0
- package/src/components/role-appointment-list.tsx +227 -0
- package/src/components/row-actions.render.test.ts +83 -0
- package/src/components/row-actions.stories.tsx +87 -0
- package/src/components/row-actions.tsx +139 -0
- package/src/components/search-bar.render.test.ts +55 -0
- package/src/components/search-bar.stories.tsx +46 -0
- package/src/components/search-bar.tsx +118 -0
- package/src/components/search-result-row.tsx +139 -0
- package/src/components/search-results.render.test.ts +89 -0
- package/src/components/search-results.stories.tsx +126 -0
- package/src/components/search-results.tsx +215 -0
- package/src/components/search-token-chip.render.test.ts +42 -0
- package/src/components/search-token-chip.stories.tsx +42 -0
- package/src/components/search-token-chip.tsx +66 -0
- package/src/components/security-select.render.test.ts +33 -0
- package/src/components/security-select.tsx +53 -0
- package/src/components/segmented-control.tsx +70 -0
- package/src/components/select.tsx +33 -0
- package/src/components/selection-top-bar.render.test.ts +67 -0
- package/src/components/selection-top-bar.stories.tsx +39 -0
- package/src/components/selection-top-bar.tsx +95 -0
- package/src/components/sender-group-switch.tsx +63 -0
- package/src/components/sender-trust-indicator.render.test.ts +67 -0
- package/src/components/sender-trust-indicator.stories.tsx +58 -0
- package/src/components/sender-trust-indicator.tsx +87 -0
- package/src/components/settings-primitives.render.test.ts +160 -0
- package/src/components/settings-primitives.stories.tsx +80 -0
- package/src/components/settings-screen.tsx +354 -0
- package/src/components/swipeable-row.render.test.ts +106 -0
- package/src/components/swipeable-row.stories.tsx +117 -0
- package/src/components/swipeable-row.tsx +268 -0
- package/src/components/touch-list-body.render.test.ts +66 -0
- package/src/components/touch-list-body.stories.tsx +76 -0
- package/src/components/touch-list.tsx +87 -0
- package/src/components/wizard.render.test.ts +49 -0
- package/src/components/wizard.stories.tsx +113 -0
- package/src/components/wizard.tsx +321 -0
- package/src/filter-presets.test.ts +93 -0
- package/src/filter-presets.ts +101 -0
- package/src/index.ts +378 -0
- package/src/lib/cid-resolver.ts +35 -0
- package/src/lib/cn.ts +7 -0
- package/src/lib/email-layout-clamp.test.ts +109 -0
- package/src/lib/email-layout-clamp.ts +64 -0
- package/src/lib/email-render-treatment.test.ts +82 -0
- package/src/lib/email-render-treatment.ts +53 -0
- package/src/lib/email-sanitizer.test.ts +362 -0
- package/src/lib/email-sanitizer.ts +298 -0
- package/src/lib/row-keyboard.test.ts +41 -0
- package/src/lib/row-keyboard.ts +20 -0
- package/src/tokens.css +217 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render-treatment classification for an email body (#424).
|
|
3
|
+
*
|
|
4
|
+
* Two visual treatments exist in the reading pane:
|
|
5
|
+
*
|
|
6
|
+
* - `framed`: designed HTML mail (newsletter / marketing category, OR an
|
|
7
|
+
* author-specified background detected by the sanitizer). Rendered inside a
|
|
8
|
+
* left-anchored hairline frame pinned to light-mode so the author's own
|
|
9
|
+
* colors survive dark mode and are never inverted.
|
|
10
|
+
*
|
|
11
|
+
* - plain (`isPlain`): weakly-marked / personal mail with no author
|
|
12
|
+
* background. Receives the UI sans-serif + theme-aware base CSS so
|
|
13
|
+
* black-text-on-dark is readable.
|
|
14
|
+
*
|
|
15
|
+
* This is the single source of truth for the branch; `MessageBodyView` consumes
|
|
16
|
+
* it so the decision is unit-testable in isolation (no DOM, no iframe).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Subset of RemitImapMessageCategory used by the renderer. Kept local to avoid
|
|
21
|
+
* a hard dependency on the generated types (this file compiles before `make`
|
|
22
|
+
* in fresh checkouts).
|
|
23
|
+
*/
|
|
24
|
+
export type EmailRenderCategory =
|
|
25
|
+
| "personal"
|
|
26
|
+
| "newsletter"
|
|
27
|
+
| "marketing"
|
|
28
|
+
| "automated"
|
|
29
|
+
| "transactional"
|
|
30
|
+
| "social";
|
|
31
|
+
|
|
32
|
+
export interface EmailRenderTreatment {
|
|
33
|
+
/** Designed mail: render framed + light-mode, preserve author colors. */
|
|
34
|
+
framed: boolean;
|
|
35
|
+
/** Plain mail: inject UI font-stack + theme-aware colors. */
|
|
36
|
+
isPlain: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Classify an email into the framed (designed) vs plain treatment.
|
|
41
|
+
*
|
|
42
|
+
* `framed` when the category is `newsletter`/`marketing` OR the sanitizer
|
|
43
|
+
* detected an author background. `isPlain` is the complement.
|
|
44
|
+
*/
|
|
45
|
+
export const classifyEmailRenderTreatment = (
|
|
46
|
+
category: EmailRenderCategory | undefined,
|
|
47
|
+
hasAuthorBackground: boolean,
|
|
48
|
+
): EmailRenderTreatment => {
|
|
49
|
+
const isDesignedCategory =
|
|
50
|
+
category === "newsletter" || category === "marketing";
|
|
51
|
+
const framed = isDesignedCategory || hasAuthorBackground;
|
|
52
|
+
return { framed, isPlain: !framed };
|
|
53
|
+
};
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import { buildCidResolver } from "./cid-resolver.js";
|
|
4
|
+
import {
|
|
5
|
+
detectAuthorBackground,
|
|
6
|
+
sanitizeInlineStyle,
|
|
7
|
+
sanitizeStyleElementCss,
|
|
8
|
+
} from "./email-sanitizer.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Author CSS survives — the email body is rendered as a light-mode island
|
|
12
|
+
* (see `MessageBody.tsx`'s `color-scheme: light` wrapper), so the sanitizer
|
|
13
|
+
* must NOT rewrite color/background declarations or wrap author styles in a
|
|
14
|
+
* `prefers-color-scheme` media query. These tests pin that contract; if any
|
|
15
|
+
* dark-mode rewriting sneaks back in, newsletter designs go back to looking
|
|
16
|
+
* broken in dark mode (#375).
|
|
17
|
+
*
|
|
18
|
+
* DOMPurify needs a DOM at module-load, so the bgcolor/attribute side of the
|
|
19
|
+
* sanitizer is verified by the lack of any code that touches it (see
|
|
20
|
+
* `email-sanitizer.ts`). The pure string transforms below cover the
|
|
21
|
+
* inline-style and `<style>`-block paths.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
describe("sanitizeInlineStyle — author colors survive (#375)", () => {
|
|
25
|
+
test("white background passes through unchanged", () => {
|
|
26
|
+
const result = sanitizeInlineStyle("background:#fff;color:#000");
|
|
27
|
+
assert.ok(
|
|
28
|
+
result.includes("background:#fff"),
|
|
29
|
+
"author light background must survive — it is the whole point of #375",
|
|
30
|
+
);
|
|
31
|
+
assert.ok(
|
|
32
|
+
result.includes("color:#000"),
|
|
33
|
+
"author dark text must survive — no more `color: inherit` rewrite",
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("named-color background (white) is not stripped", () => {
|
|
38
|
+
const result = sanitizeInlineStyle("background: white; color: black");
|
|
39
|
+
assert.ok(result.includes("background: white"));
|
|
40
|
+
assert.ok(result.includes("color: black"));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("rgb() backgrounds are not transformed to transparent", () => {
|
|
44
|
+
const result = sanitizeInlineStyle(
|
|
45
|
+
"background-color: rgb(255, 255, 255); color: rgb(0, 0, 0)",
|
|
46
|
+
);
|
|
47
|
+
assert.ok(result.includes("rgb(255, 255, 255)"));
|
|
48
|
+
assert.ok(result.includes("rgb(0, 0, 0)"));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("border colors are not rewritten to currentColor", () => {
|
|
52
|
+
const result = sanitizeInlineStyle("border: 1px solid #eee");
|
|
53
|
+
assert.ok(result.includes("#eee"));
|
|
54
|
+
assert.ok(!result.includes("currentColor"));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("url() background images are neutered — privacy / read-tracker vector", () => {
|
|
58
|
+
const result = sanitizeInlineStyle(
|
|
59
|
+
"background: url(https://tracker.example/pixel.gif)",
|
|
60
|
+
);
|
|
61
|
+
assert.ok(!result.includes("tracker.example"));
|
|
62
|
+
assert.ok(result.includes("none"));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("expression() is stripped — legacy IE XSS vector", () => {
|
|
66
|
+
const result = sanitizeInlineStyle("width: expression(alert(1))");
|
|
67
|
+
assert.ok(!result.includes("expression"));
|
|
68
|
+
assert.ok(!result.includes("alert"));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("-moz-binding is stripped — legacy Firefox XSS vector", () => {
|
|
72
|
+
const result = sanitizeInlineStyle("-moz-binding: url(evil.xml)");
|
|
73
|
+
assert.ok(!result.includes("-moz-binding"));
|
|
74
|
+
assert.ok(!result.includes("evil.xml"));
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("sanitizeStyleElementCss — author <style> blocks survive (#375)", () => {
|
|
79
|
+
test("author body { background: white; color: black } is NOT wrapped in @media (prefers-color-scheme: light)", () => {
|
|
80
|
+
const css = "body { background: white; color: black; }";
|
|
81
|
+
const result = sanitizeStyleElementCss(css);
|
|
82
|
+
assert.ok(
|
|
83
|
+
!result.includes("@media"),
|
|
84
|
+
"author CSS must not be hidden behind a media query — that was the bug",
|
|
85
|
+
);
|
|
86
|
+
assert.ok(!result.includes("prefers-color-scheme"));
|
|
87
|
+
assert.ok(
|
|
88
|
+
result.includes("background: white"),
|
|
89
|
+
"author background survives in <style>",
|
|
90
|
+
);
|
|
91
|
+
assert.ok(
|
|
92
|
+
result.includes("color: black"),
|
|
93
|
+
"author color survives in <style>",
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("author CSS pass-through preserves the whole declaration block", () => {
|
|
98
|
+
const css = `
|
|
99
|
+
.brand { background:#0066cc; color:#fff; padding:12px; }
|
|
100
|
+
.muted { color:#666; }
|
|
101
|
+
`;
|
|
102
|
+
const result = sanitizeStyleElementCss(css);
|
|
103
|
+
assert.ok(result.includes(".brand"));
|
|
104
|
+
assert.ok(result.includes("#0066cc"));
|
|
105
|
+
assert.ok(result.includes(".muted"));
|
|
106
|
+
assert.ok(result.includes("#666"));
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("@import is neutered — remote stylesheet pulls leak the read event", () => {
|
|
110
|
+
const css = "@import url('https://tracker.example/style.css');";
|
|
111
|
+
const result = sanitizeStyleElementCss(css);
|
|
112
|
+
assert.ok(!result.includes("tracker.example"));
|
|
113
|
+
assert.ok(result.includes("@import blocked"));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("url() inside CSS is neutered — same read-tracker vector", () => {
|
|
117
|
+
const css =
|
|
118
|
+
".hero { background-image: url(https://tracker.example/pixel.gif); }";
|
|
119
|
+
const result = sanitizeStyleElementCss(css);
|
|
120
|
+
assert.ok(!result.includes("tracker.example"));
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("expression() and -moz-binding are stripped from <style> too", () => {
|
|
124
|
+
const css =
|
|
125
|
+
".x { width: expression(alert(1)); -moz-binding: url(evil.xml); }";
|
|
126
|
+
const result = sanitizeStyleElementCss(css);
|
|
127
|
+
assert.ok(!result.includes("expression"));
|
|
128
|
+
assert.ok(!result.includes("alert"));
|
|
129
|
+
assert.ok(!result.includes("-moz-binding"));
|
|
130
|
+
assert.ok(!result.includes("evil.xml"));
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("buildCidResolver (#224 PR 2)", () => {
|
|
135
|
+
const PARTS = [
|
|
136
|
+
{
|
|
137
|
+
contentId: "<inline-1@example.com>",
|
|
138
|
+
contentUrl:
|
|
139
|
+
"https://cdn.test/content/accounts/cfg/acc/messages/m/parts/1",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
contentId: "inline-2@example.com",
|
|
143
|
+
contentUrl:
|
|
144
|
+
"https://cdn.test/content/accounts/cfg/acc/messages/m/parts/2",
|
|
145
|
+
},
|
|
146
|
+
{ contentUrl: "https://cdn.test/no-cid/parts/3" },
|
|
147
|
+
{
|
|
148
|
+
contentId: "<inline-blank>",
|
|
149
|
+
contentUrl: "",
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
test("looks up the URL by Content-ID, stripping angle brackets on both sides", () => {
|
|
154
|
+
const resolve = buildCidResolver(PARTS);
|
|
155
|
+
assert.equal(
|
|
156
|
+
resolve("inline-1@example.com"),
|
|
157
|
+
"https://cdn.test/content/accounts/cfg/acc/messages/m/parts/1",
|
|
158
|
+
);
|
|
159
|
+
assert.equal(
|
|
160
|
+
resolve("<inline-1@example.com>"),
|
|
161
|
+
"https://cdn.test/content/accounts/cfg/acc/messages/m/parts/1",
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("matches Content-IDs that came in without angle brackets", () => {
|
|
166
|
+
const resolve = buildCidResolver(PARTS);
|
|
167
|
+
assert.equal(
|
|
168
|
+
resolve("inline-2@example.com"),
|
|
169
|
+
"https://cdn.test/content/accounts/cfg/acc/messages/m/parts/2",
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("returns undefined when no body part has a matching Content-ID — fail-loud, do not silently substitute", () => {
|
|
174
|
+
const resolve = buildCidResolver(PARTS);
|
|
175
|
+
assert.equal(resolve("missing@example.com"), undefined);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("skips parts without a contentId or with an empty contentUrl", () => {
|
|
179
|
+
const resolve = buildCidResolver(PARTS);
|
|
180
|
+
assert.equal(resolve("inline-blank"), undefined);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("empty body-part list returns a resolver that always returns undefined", () => {
|
|
184
|
+
const resolve = buildCidResolver([]);
|
|
185
|
+
assert.equal(resolve("anything"), undefined);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe("detectAuthorBackground — designed-vs-plain mail discriminator (#375)", () => {
|
|
190
|
+
test("inline style with background-color triggers (newsletter pattern)", () => {
|
|
191
|
+
assert.equal(
|
|
192
|
+
detectAuthorBackground(
|
|
193
|
+
'<body style="background-color:#ffffff;color:#000">x</body>',
|
|
194
|
+
),
|
|
195
|
+
true,
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("legacy bgcolor attribute triggers", () => {
|
|
200
|
+
assert.equal(
|
|
201
|
+
detectAuthorBackground(
|
|
202
|
+
'<table bgcolor="#ffffff"><tr><td>x</td></tr></table>',
|
|
203
|
+
),
|
|
204
|
+
true,
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("<style> block containing a background rule triggers", () => {
|
|
209
|
+
assert.equal(
|
|
210
|
+
detectAuthorBackground(
|
|
211
|
+
"<style>body { background: white; }</style><p>x</p>",
|
|
212
|
+
),
|
|
213
|
+
true,
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("plain mail with no author styling does NOT trigger — inherits app theme", () => {
|
|
218
|
+
assert.equal(detectAuthorBackground("<p>hello</p>"), false);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("author text colour alone does NOT trigger — only backgrounds do", () => {
|
|
222
|
+
assert.equal(
|
|
223
|
+
detectAuthorBackground(
|
|
224
|
+
'<p style="color:#666">just a text color, no bg</p>',
|
|
225
|
+
),
|
|
226
|
+
false,
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("inline style with shorthand `background:` (no -color) still triggers", () => {
|
|
231
|
+
assert.equal(
|
|
232
|
+
detectAuthorBackground('<div style="background:#eee">x</div>'),
|
|
233
|
+
true,
|
|
234
|
+
);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("case-insensitive on the attribute and on background keyword", () => {
|
|
238
|
+
assert.equal(
|
|
239
|
+
detectAuthorBackground('<div STYLE="BACKGROUND:#eee">x</div>'),
|
|
240
|
+
true,
|
|
241
|
+
);
|
|
242
|
+
assert.equal(detectAuthorBackground('<td BGCOLOR="#fff">x</td>'), true);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe("detectAuthorBackground — <style> block over-match hardening (#483)", () => {
|
|
247
|
+
test("(a) reset stylesheet with `background: none` does NOT trigger — no framing for resets", () => {
|
|
248
|
+
// Over-matched before #483: the bare substring `background` fired even on
|
|
249
|
+
// `background: none`, causing plain personal mail to be misclassified as
|
|
250
|
+
// framed and rendered unreadably in dark mode.
|
|
251
|
+
assert.equal(
|
|
252
|
+
detectAuthorBackground(
|
|
253
|
+
"<style>* { margin: 0; padding: 0; background: none; }</style><p>hi</p>",
|
|
254
|
+
),
|
|
255
|
+
false,
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("(a) `background-color: transparent` in a reset block does NOT trigger", () => {
|
|
260
|
+
assert.equal(
|
|
261
|
+
detectAuthorBackground(
|
|
262
|
+
"<style>body { background-color: transparent; color: #333; }</style><p>hi</p>",
|
|
263
|
+
),
|
|
264
|
+
false,
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("(a) `background: inherit` does NOT trigger", () => {
|
|
269
|
+
assert.equal(
|
|
270
|
+
detectAuthorBackground(
|
|
271
|
+
"<style>p { background: inherit; }</style><p>hi</p>",
|
|
272
|
+
),
|
|
273
|
+
false,
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("(a) `background: initial` does NOT trigger", () => {
|
|
278
|
+
assert.equal(
|
|
279
|
+
detectAuthorBackground(
|
|
280
|
+
"<style>div { background: initial; }</style><div>hi</div>",
|
|
281
|
+
),
|
|
282
|
+
false,
|
|
283
|
+
);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("(a) `background: unset` does NOT trigger", () => {
|
|
287
|
+
assert.equal(
|
|
288
|
+
detectAuthorBackground(
|
|
289
|
+
"<style>div { background: unset; }</style><div>hi</div>",
|
|
290
|
+
),
|
|
291
|
+
false,
|
|
292
|
+
);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("(b) unused class `.foo { background: red }` DOES trigger — conservative: real color value is treated as author background regardless of selector", () => {
|
|
296
|
+
// Determining whether a CSS class is actually applied to any element
|
|
297
|
+
// requires DOM access and a full cascade. A light pre-sanitization scan
|
|
298
|
+
// cannot make that distinction, so a class rule with a real color is
|
|
299
|
+
// treated as an author background on the conservative side. The main
|
|
300
|
+
// wins from #483 are reset/no-op values, not unused-class detection.
|
|
301
|
+
assert.equal(
|
|
302
|
+
detectAuthorBackground(
|
|
303
|
+
"<style>.foo { background: red; }</style><p>hi</p>",
|
|
304
|
+
),
|
|
305
|
+
true,
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("(c) genuine `body { background: #fff }` DOES trigger — framed treatment", () => {
|
|
310
|
+
assert.equal(
|
|
311
|
+
detectAuthorBackground(
|
|
312
|
+
"<style>body { background: #fff; color: #000; }</style><p>designed mail</p>",
|
|
313
|
+
),
|
|
314
|
+
true,
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("(c) `background-color: #ffffff` (explicit -color property) triggers", () => {
|
|
319
|
+
assert.equal(
|
|
320
|
+
detectAuthorBackground(
|
|
321
|
+
"<style>html { background-color: #ffffff; }</style><p>x</p>",
|
|
322
|
+
),
|
|
323
|
+
true,
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("(c) `background: rgb(255,255,255)` triggers — real value, not a keyword", () => {
|
|
328
|
+
assert.equal(
|
|
329
|
+
detectAuthorBackground(
|
|
330
|
+
"<style>body { background: rgb(255,255,255); }</style><p>x</p>",
|
|
331
|
+
),
|
|
332
|
+
true,
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("style block with only text-color rules does NOT trigger", () => {
|
|
337
|
+
assert.equal(
|
|
338
|
+
detectAuthorBackground(
|
|
339
|
+
"<style>.brand { color: #0066cc; font-weight: bold; }</style><p>hi</p>",
|
|
340
|
+
),
|
|
341
|
+
false,
|
|
342
|
+
);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("multiple <style> blocks: first has reset, second has real background — DOES trigger", () => {
|
|
346
|
+
const html = [
|
|
347
|
+
"<style>* { background: none; }</style>",
|
|
348
|
+
"<style>body { background: #f0f0f0; }</style>",
|
|
349
|
+
"<p>x</p>",
|
|
350
|
+
].join("");
|
|
351
|
+
assert.equal(detectAuthorBackground(html), true);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("multiple <style> blocks: both reset values — does NOT trigger", () => {
|
|
355
|
+
const html = [
|
|
356
|
+
"<style>* { background: none; }</style>",
|
|
357
|
+
"<style>p { background: transparent; }</style>",
|
|
358
|
+
"<p>x</p>",
|
|
359
|
+
].join("");
|
|
360
|
+
assert.equal(detectAuthorBackground(html), false);
|
|
361
|
+
});
|
|
362
|
+
});
|