1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
|
package main
// Where the editor's script is, and where it must never be.
//
// The manual's security claim is that script lives in the outer page and never
// in a message, and that a message body is inert under a script-src 'none'
// policy and a sandbox without allow-scripts. Adding an editor to two pages
// that frame attacker-controlled HTML is exactly the change that could quietly
// break that, so it is checked rather than asserted.
import (
"strings"
"testing"
"time"
"codeberg.org/Profpatsch/Profpatsch/users/Profpatsch/mailtext"
)
// renderTemplate executes a template into a string.
func renderTemplate(t *testing.T, name string, data any) string {
t.Helper()
var sb strings.Builder
var err error
switch name {
case "index":
err = indexTmpl.Execute(&sb, data)
case "contactdetail":
err = contactDetailTmpl.Execute(&sb, data)
case "contacts":
err = contactsTmpl.Execute(&sb, data)
case "contactsettings":
err = contactSettingsTmpl.Execute(&sb, data)
case "draft":
err = draftTmpl.Execute(&sb, data)
case "msgbody":
err = msgBodyTmpl.Execute(&sb, data)
case "msgview":
err = msgViewTmpl.Execute(&sb, data)
default:
t.Fatalf("unknown template %q", name)
}
if err != nil {
t.Fatalf("%s: %v", name, err)
}
return sb.String()
}
func testMsgRow() msgRow {
now := time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC)
return msgRow{
ID: 28755, Subject: "Re: the thing",
FromAddr: "Klara <klara@example.org>",
From: resolveAddress(nil, "klara@example.org", "Klara"),
Date: now, MimeType: "text/plain",
}
}
// TestMessageBodyCarriesNoScript is the one that matters.
//
// A message body is rendered into an iframe of its own, and the whole security
// model rests on that document being inert. It must carry the CSP and it must
// not carry the editor, however much of the editor the pages around it grew.
func TestMessageBodyCarriesNoScript(t *testing.T) {
out := renderTemplate(t, "msgbody", map[string]any{
"Body": "<p>hello</p>",
"MimeType": "text/html",
})
if strings.Contains(out, "draft-editor.js") {
t.Error("a message body must never load the editor")
}
if strings.Contains(out, "data-draft-editor") {
t.Error("a message body must never host an editor")
}
if !strings.Contains(out, "script-src 'none'") {
t.Error("a message body must carry the script-src 'none' policy")
}
}
// TestEditorPagesLoadTheScript checks the three pages that mount an editor do
// load it, and that the framed message keeps its sandbox.
func TestEditorPagesLoadTheScript(t *testing.T) {
accountName = "test"
myAddress = "me@example.org"
myAddresses = map[string]bool{myAddress: true}
msg := testMsgRow()
d := &draft{Token: "tok123", Subject: "Re: the thing", ParentID: msg.ID}
index := renderTemplate(t, "index", indexPageData{
Messages: []msgRow{msg},
Paging: mailtext.Paging{Total: 1},
})
contact := renderTemplate(t, "contactdetail", contactDetailData{
Contact: "klara@example.org", Name: msg.From,
AddrEscaped: "klara%40example.org", Messages: []msgRow{msg},
})
draftPage := renderTemplate(t, "draft", draftViewData{
Draft: d, From: "me@example.org",
Blocks: []describedBlock{{draftBlock: draftBlock{Kind: blockText, Content: "hi"}}},
})
for name, out := range map[string]string{
"index": index, "contact": contact, "draft": draftPage,
} {
if !strings.Contains(out, "draft-editor.js") {
t.Errorf("%s: should load the editor script", name)
}
}
// The draft page mounts an editor directly; the listings offer a reply
// button, since no draft exists against the message yet.
if !strings.Contains(draftPage, `data-draft-editor="tok123"`) {
t.Error("draft page should mount an editor for its own draft")
}
for name, out := range map[string]string{"index": index, "contact": contact} {
if !strings.Contains(out, "data-reply-form") {
t.Errorf("%s: should offer a reply button", name)
}
if strings.Contains(out, "data-draft-editor") {
t.Errorf("%s: should not mount an editor before a draft exists", name)
}
// The frames around it are untouched by any of this.
if !strings.Contains(out, "sandbox=") {
t.Errorf("%s: message frames must keep their sandbox", name)
}
if strings.Contains(out, "allow-scripts") {
t.Errorf("%s: a message frame must never be granted allow-scripts", name)
}
}
}
// TestListingsMountExistingDrafts checks that a draft already written against a
// message is shown beside it, in place of the reply button.
func TestListingsMountExistingDrafts(t *testing.T) {
msg := testMsgRow()
drafts := map[int64][]draft{msg.ID: {{Token: "existing1"}}}
for name, out := range map[string]string{
"index": renderTemplate(t, "index", indexPageData{
Messages: []msgRow{msg}, Paging: mailtext.Paging{Total: 1},
DraftsByMsg: drafts,
}),
"contact": renderTemplate(t, "contactdetail", contactDetailData{
Contact: "klara@example.org", Name: msg.From,
AddrEscaped: "klara%40example.org", Messages: []msgRow{msg},
DraftsByMsg: drafts,
}),
} {
if !strings.Contains(out, `data-draft-editor="existing1"`) {
t.Errorf("%s: should mount the draft already written", name)
}
// Otherwise replying again would silently make a second draft.
if strings.Contains(out, "data-reply-form") {
t.Errorf("%s: should not offer to compose a second draft", name)
}
}
}
// TestSentDraftPageOffersNoDiscard checks the sent draft is presented as a
// record rather than as something still being written.
func TestSentDraftPageOffersNoDiscard(t *testing.T) {
sent := time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC)
out := renderTemplate(t, "draft", draftViewData{
Draft: &draft{Token: "tok", Subject: "Re: x", SentAt: &sent},
From: "me@example.org",
})
if !strings.Contains(out, "has been sent") {
t.Error("a sent draft should say so")
}
if strings.Contains(out, "/discard") {
t.Error("a sent draft should not offer to be discarded")
}
}
|