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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
|
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>mailweb ({{account}}) — Spam melden</title>
<style>
:root {
color-scheme: light dark;
--bg: #f5f5f5;
--bg-card: #ffffff;
--border: #dddddd;
--text: #111111;
--text-meta: #666666;
--link: #3781b8;
--danger: #c0392b;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #1a1a1a;
--bg-card: #242424;
--border: #3a3a3a;
--text: #e8e8e8;
--text-meta: #999999;
--link: #7ab3e0;
--danger: #e05c4a;
}
}
body { font-family: sans-serif; margin: 0; padding: 1rem;
background: var(--bg); color: var(--text); }
nav { margin-bottom: 1rem; }
nav a { color: var(--link); text-decoration: none; font-size: 0.9rem; }
nav a:hover { text-decoration: underline; }
h1 { font-size: 1.2rem; margin-bottom: 0.3rem; }
.addr { color: var(--text-meta); font-size: 0.9rem; margin-bottom: 1.5rem; }
.prefill-note {
max-width: 600px; margin: -0.8rem 0 1.2rem;
padding: 0.6rem 0.8rem; font-size: 0.88rem; line-height: 1.4;
background: var(--bg-card); color: var(--text-meta);
border: 1px solid var(--border); border-left: 3px solid var(--danger);
border-radius: 3px;
}
.send-error {
margin: 0; padding: 0.5rem 0.6rem; overflow-x: auto;
font-size: 0.82rem; line-height: 1.35; white-space: pre-wrap;
word-break: break-word; color: var(--text);
background: var(--bg); border: 1px solid var(--border); border-radius: 3px;
}
form { background: var(--bg-card); border: 1px solid var(--border);
border-radius: 4px; padding: 1.2rem; max-width: 600px; }
label { display: block; font-size: 0.9rem; margin-bottom: 0.4rem;
color: var(--text-meta); }
select {
width: 100%; box-sizing: border-box; padding: 0.45rem 0.5rem;
border: 1px solid var(--border); border-radius: 3px;
background: var(--bg); color: var(--text);
font: inherit; margin-bottom: 1rem; cursor: pointer;
}
textarea {
width: 100%; box-sizing: border-box;
min-height: 120px; padding: 0.5rem;
border: 1px solid var(--border); border-radius: 3px;
background: var(--bg); color: var(--text);
font: inherit; resize: vertical;
margin-bottom: 1rem;
}
.checkbox-row {
display: flex; align-items: center; gap: 0.5rem;
margin-bottom: 1.2rem; font-size: 0.9rem;
}
.checkbox-row input { width: 1rem; height: 1rem; cursor: pointer; }
{{/* No max-height scroller here: the rows now contain message bodies, and a
220px scrolling box is not somewhere a body can be read. The list is as
long as it is; the page scrolls. */}}
.msg-list {
margin-bottom: 1rem;
border: 1px solid var(--border); border-radius: 3px;
}
.msg-list-item {
padding: 0.35rem 0.6rem;
border-bottom: 1px solid var(--border); font-size: 0.88rem;
}
.msg-list-item:last-child { border-bottom: none; }
.msg-list-row {
display: flex; align-items: baseline; gap: 0.6rem;
cursor: pointer;
}
.msg-list-row input { flex-shrink: 0; cursor: pointer; }
.msg-subject { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.msg-date { color: var(--text-meta); font-size: 0.8rem; flex-shrink: 0; }
.msg-from {
display: block; margin: 0.1rem 0 0 1.6rem;
color: var(--text-meta); font-size: 0.8rem;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
{{/* The body is collapsed by default, unlike contact_detail.html which
inlines text/plain. This page's job is the form, and a report against a
contact with forty messages would otherwise open as forty bodies. */}}
.msg-body { margin: 0.3rem 0 0.1rem 1.6rem; }
.msg-body summary {
cursor: pointer; color: var(--text-meta); font-size: 0.8rem;
}
.msg-body iframe {
width: 100%; border: 1px solid var(--border); border-radius: 3px;
display: block; margin-top: 0.3rem; background: var(--bg);
}
.send-info {
font-size: 0.85rem; color: var(--text-meta);
margin-bottom: 0.8rem;
}
.actions { display: flex; align-items: center; gap: 1rem; }
button[type=submit] {
background: var(--danger); color: #fff;
border: none; border-radius: 3px;
padding: 0.45rem 1rem; font-size: 0.9rem;
cursor: pointer;
}
button[type=submit]:hover { opacity: 0.85; }
.cancel { color: var(--text-meta); font-size: 0.9rem; text-decoration: none; }
.cancel:hover { text-decoration: underline; }
{{template "nameStyle"}}
</style>
<script>
// Sizes a message iframe to its content, as in contact_detail.html. This is the
// only reason the frames are granted allow-same-origin, and is safe solely
// because allow-scripts is absent: the two must never be granted together.
function resizeIframe(el) {
el.style.height = el.contentDocument.documentElement.scrollHeight + 'px';
}
var reasons = [
{
label: "— Grund auswählen —",
text: "",
illegal: false
},
{
label: "Unverlangte Werbung",
text: "Ich habe diese E-Mail unverlangt erhalten. Ich habe keine Geschäftsbeziehung mit dem Absender und habe keine Werbe-E-Mails angefordert.",
illegal: false
},
{
label: "Phishing",
text: "Diese E-Mail enthält einen Phishing-Versuch. Es wird versucht, persönliche Daten oder Zugangsdaten zu erschleichen.",
illegal: true
},
{
label: "Schadsoftware / Malware",
text: "Diese E-Mail enthält oder verlinkt auf Schadsoftware.",
illegal: true
},
{
label: "Betrug / Vorschussbetrug",
text: "Diese E-Mail enthält betrügerische Inhalte (z.\u202fB. Vorschussbetrug, gefälschte Gewinnbenachrichtigung).",
illegal: true
},
{
label: "Illegale Inhalte",
text: "Diese E-Mail enthält oder verweist auf illegale Inhalte.",
illegal: true
},
{
label: "Sonstiges",
text: "",
illegal: false
}
];
// When the form was opened with a prefilled description (see reportSpamPrefill
// in contacts.go), the canned text of the selected reason must not overwrite
// it: the caller wrote something specific about this sender, which is worth
// more than the generic sentence. Selecting a reason by hand still fills the
// textarea — that is an explicit request for the canned text.
window.onload = function() {
if (document.getElementById("description").value.trim() !== "") {
return;
}
var sel = document.getElementById("reason");
onReasonChange(sel);
};
function onReasonChange(sel) {
var idx = sel.selectedIndex;
var r = reasons[idx];
var ta = document.getElementById("description");
var cb = document.getElementById("illegal");
if (r.text !== "") {
ta.value = r.text;
}
if (idx !== 0) {
cb.checked = r.illegal;
}
ta.focus();
}
</script>
</head>
<body>
<nav><a href="/contact/{{.AddrEscaped}}">← {{template "name" .Name}}</a></nav>
<h1>Spam melden</h1>
<p class="addr">{{template "name" .Name}} <{{.Contact}}></p>
{{if .Prefilled}}
<p class="prefill-note">Dieses Formular wurde über die URL vorausgefüllt und ist
ein <strong>Entwurf</strong>: es wurde noch nichts gesendet. Bitte Beschreibung,
Auswahl und Empfänger prüfen, dann selbst absenden.</p>
{{end}}
{{/* Without SMTP nothing can be sent, so the form says so and offers no submit
button. The alternative — leaving the button and refusing the POST — was
what this replaced: the redirect looked identical to a report that went
out. Stating it here, before anything is typed, costs the reader less than
an error after. */}}
{{if not .SMTPEnabled}}
<p class="prefill-note">Für dieses Konto ist <strong>kein SMTP konfiguriert</strong>
(<code>--smtp-host</code> fehlt). Es kann keine Meldung versendet werden; dieses
Formular zeigt nur, was gemeldet würde. Das Absenden ist deaktiviert.</p>
{{end}}
{{/* .SendError is the remote server's own text. It is safe here only because
html/template escapes it in this context — it must never become
template.HTML, however tempting a <pre> of raw SMTP chatter looks. */}}
{{if .SendError}}
<div class="prefill-note">
<p style="margin:0 0 0.5rem"><strong>Die Meldung wurde nicht gesendet.</strong>
Es wurde <strong>nichts vermerkt</strong>; der Absender ist unverändert.
Der Postausgangsserver hat die Nachricht abgelehnt:</p>
<pre class="send-error">{{.SendError}}</pre>
<p style="margin:0.5rem 0 0">Bleibt der Versand blockiert, hilft nur, die
Nachricht von Hand an {{.ReportTo}} weiterzuleiten (mit Original-Header).
Für E-Mail-Beschwerden gibt es kein Webformular.</p>
</div>
{{end}}
<form method="POST" action="/contact/{{.AddrEscaped}}/report-spam">
<label for="reason">Grund</label>
{{/* The option order must match the `reasons` array in the script above:
onReasonChange indexes into it by selectedIndex. $r is the reason
requested via the query string, "" when the form was opened by hand, in
which case the first real reason stays selected as before. */}}
{{$r := .Reason}}
{{/* name= so a redisplay after a failed send can show the same reason back;
the report's substance is the description, which is submitted anyway. */}}
<select id="reason" name="reason" onchange="onReasonChange(this)">
<option{{if eq $r "— Grund auswählen —"}} selected{{end}}>— Grund auswählen —</option>
<option{{if or (eq $r "Unverlangte Werbung") (eq $r "")}} selected{{end}}>Unverlangte Werbung</option>
<option{{if eq $r "Phishing"}} selected{{end}}>Phishing</option>
<option{{if eq $r "Schadsoftware / Malware"}} selected{{end}}>Schadsoftware / Malware</option>
<option{{if eq $r "Betrug / Vorschussbetrug"}} selected{{end}}>Betrug / Vorschussbetrug</option>
<option{{if eq $r "Illegale Inhalte"}} selected{{end}}>Illegale Inhalte</option>
<option{{if eq $r "Sonstiges"}} selected{{end}}>Sonstiges</option>
</select>
{{if .Messages}}
<label>Nachrichten anfügen ({{len .Messages}} gefunden, {{.CheckedCount}} ausgewählt)</label>
<div class="msg-list">
{{range .Messages}}
<div class="msg-list-item">
<label class="msg-list-row">
<input type="checkbox" name="msg" value="{{.ID}}"{{if .Checked}} checked{{end}}>
<span class="msg-subject">{{if .Subject}}{{.Subject}}{{else}}(kein Betreff){{end}}</span>
<span class="msg-date">{{.Date.Format "02.01.2006"}}</span>
</label>
{{/* The sender of each message, because what gets reported and flagged is
the sender of the ticked messages, and the candidate list is selected
by substring so it can span more than one of them. */}}
<span class="msg-from">{{template "name" .From}} <{{.From.Address}}></span>
{{/* Deciding whether a mail is merely unsolicited or carries illegal
content — the checkbox that routes the report to besonderer-spam@ —
is a judgement about content, and this was the one page in mailweb
that showed less of the message than any other. loading="lazy" in a
closed <details> means no body is fetched until it is opened, so the
header-only mirror is not disturbed and a long list costs nothing.
Same endpoint as everywhere else, so the CSP and sandbox that make a
message body safe to display apply here unchanged. */}}
<details class="msg-body">
<summary>Nachricht anzeigen</summary>
<iframe src="/msg/{{.ID}}" sandbox="allow-same-origin allow-popups"
loading="lazy" onload="resizeIframe(this)"></iframe>
</details>
</div>
{{end}}
</div>
{{end}}
<label for="description">Beschreibung</label>
<textarea id="description" name="description" required
placeholder="Beschreiben Sie, warum diese E-Mail Spam ist …">{{.Description}}</textarea>
<div class="checkbox-row">
<input type="checkbox" id="illegal" name="illegal"{{if .Illegal}} checked{{end}}>
<label for="illegal" style="margin:0">Diese E-Mail enthält illegale Inhalte</label>
</div>
{{if .SMTPEnabled}}
<div class="send-info">Bericht wird gesendet an <strong>{{.ReportTo}}</strong></div>
{{else}}
<div class="send-info">Empfänger wäre <strong>{{.ReportTo}}</strong> — es wird nichts gesendet.</div>
{{end}}
<div class="actions">
{{if .SMTPEnabled}}<button type="submit">Bericht senden</button>{{end}}
<a class="cancel" href="/contact/{{.AddrEscaped}}">{{if .SMTPEnabled}}Abbrechen{{else}}Zurück{{end}}</a>
</div>
</form>
</body>
</html>
|