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
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
|
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>mailweb ({{account}}) — {{if .Draft.Subject}}{{.Draft.Subject}}{{else}}draft{{end}}</title>
<link rel="alternate" type="text/llm" href="/draft/{{.Draft.Token}}"
title="Same information as a compact plain-text rendering for machine and LLM consumption: identical semantics and links, far fewer tokens. Request it with: Accept: text/llm">
<style>
:root {
color-scheme: light dark;
--bg: #f5f5f5;
--bg-card: #ffffff;
--bg-quote: #f0f0f0;
--border: #dddddd;
--text: #111111;
--text-meta: #666666;
--link: #3781b8;
--danger: #c0392b;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #1a1a1a;
--bg-card: #242424;
--bg-quote: #1e1e1e;
--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; display: flex; gap: 1.5rem; font-size: 0.9rem; }
a { color: var(--link); text-decoration: none; }
a:hover { text-decoration: underline; }
h1 { font-size: 1.2rem; margin: 0 0 0.3rem; }
.headers {
background: var(--bg-card); border: 1px solid var(--border);
border-radius: 4px; padding: 0.7rem 0.9rem; margin-bottom: 1rem;
font-size: 0.9rem; color: var(--text-meta);
}
.headers strong { color: var(--text); font-weight: 600; }
.blocks { margin-bottom: 1.5rem; }
.block {
background: var(--bg-card); border: 1px solid var(--border);
border-radius: 4px; padding: 0.6rem 0.8rem; margin-bottom: 0.6rem;
}
.block-head {
display: flex; align-items: baseline; gap: 0.6rem;
font-size: 0.78rem; color: var(--text-meta); margin-bottom: 0.4rem;
}
.kind { text-transform: uppercase; letter-spacing: 0.04em; }
.block pre {
margin: 0; white-space: pre-wrap; word-break: break-word;
font: inherit;
}
.block-quote { background: var(--bg-quote); }
.block-quote pre { color: var(--text-meta); }
.block-code pre { font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.85rem; white-space: pre; overflow-x: auto; }
.attribution { font-style: italic; color: var(--text-meta);
font-size: 0.82rem; margin-bottom: 0.3rem; }
.wire { display: block; font-style: normal; font-size: 0.95em; opacity: 0.8; }
.empty { color: var(--text-meta); font-style: italic; }
{{/* The send options. Each is a whole card rather than a row of buttons,
because the recipients have to be readable before the button is pressed
and a list of nine addresses does not fit on a button. */}}
h2 { font-size: 1rem; margin: 0 0 0.6rem; }
.send-note { font-size: 0.85rem; color: var(--text-meta); margin: 0 0 0.8rem; }
.option {
background: var(--bg-card); border: 1px solid var(--border);
border-radius: 4px; padding: 0.8rem 0.9rem; margin-bottom: 0.7rem;
}
.option-head { display: flex; align-items: baseline; gap: 0.5rem;
flex-wrap: wrap; margin-bottom: 0.3rem; }
.option-label { font-weight: 600; }
.count { color: var(--text-meta); font-size: 0.85rem; }
.explain { color: var(--text-meta); font-size: 0.85rem; margin-bottom: 0.5rem; }
.rcpts { font-size: 0.88rem; margin-bottom: 0.6rem; line-height: 1.5; }
.rcpts .field { color: var(--text-meta); }
.warn {
font-size: 0.85rem; color: var(--danger); border: 1px solid currentColor;
border-radius: 3px; padding: 0.4rem 0.6rem; margin-bottom: 0.6rem;
}
button[type=submit] {
background: var(--link); color: #fff; border: none; border-radius: 3px;
padding: 0.45rem 1rem; font-size: 0.9rem; cursor: pointer;
}
button[type=submit]:hover { opacity: 0.85; }
.discard { margin-top: 1.5rem; }
.discard button {
background: none; border: 1px solid var(--border); border-radius: 3px;
color: var(--text-meta); font-size: 0.85rem; padding: 0.3rem 0.7rem;
cursor: pointer;
}
.discard button:hover { color: var(--danger); border-color: currentColor; }
.notice {
background: var(--bg-card); border: 1px solid var(--border);
border-left: 3px solid var(--danger); border-radius: 3px;
padding: 0.6rem 0.8rem; margin-bottom: 1rem; font-size: 0.88rem;
line-height: 1.45;
}
{{/* Attachments and the recipient field. Both are plain forms: attaching a
file and naming who a mail goes to must not need script, for the same
reason composing a reply does not. */}}
.attachments { margin-bottom: 1.5rem; }
.attachment {
background: var(--bg-card); border: 1px solid var(--border);
border-radius: 4px; padding: 0.5rem 0.7rem; margin-bottom: 0.5rem;
display: flex; align-items: center; gap: 0.7rem; font-size: 0.88rem;
}
.attachment img {
max-width: 90px; max-height: 90px; border-radius: 3px; display: block;
}
.attachment .grow { flex: 1; min-width: 0; }
.attachment .name { word-break: break-word; }
.attachment .type { color: var(--text-meta); font-size: 0.85em; }
.upload, .add-rcpt {
background: var(--bg-card); border: 1px solid var(--border);
border-radius: 4px; padding: 0.6rem 0.8rem; margin-bottom: 0.7rem;
font-size: 0.88rem;
}
.upload input[type=file] { font-size: 0.85rem; }
.add-rcpt input[type=email], .add-rcpt input[type=text] {
padding: 0.3rem 0.4rem; border: 1px solid var(--border); border-radius: 3px;
background: var(--bg); color: var(--text); min-width: 16rem;
}
.add-rcpt select {
padding: 0.3rem; border: 1px solid var(--border); border-radius: 3px;
background: var(--bg); color: var(--text);
}
.hint { color: var(--text-meta); font-size: 0.85rem; margin: 0.4rem 0 0; }
.rm {
background: none; border: 1px solid var(--border); border-radius: 3px;
color: var(--text-meta); font-size: 0.8rem; padding: 0.15rem 0.4rem;
cursor: pointer;
}
.rm:hover { color: var(--danger); border-color: currentColor; }
.inline-form { display: inline; }
{{template "nameStyle"}}
{{template "editorStyle"}}
</style>
</head>
<body>
<nav>
<a href="/drafts">← drafts</a>
{{if .Draft.ParentID}}<a href="/msg/{{.Draft.ParentID}}">the message this answers →</a>{{end}}
<a href="{{.LLMViewURL}}" title="Compact plain-text rendering of this page">text view →</a>
</nav>
{{if .Draft.Sent}}
<div class="notice"><strong>This draft has been sent.</strong> It is kept as the
record of what went out; editing it now changes nothing anybody received.</div>
{{end}}
{{if .Draft.ParentGone}}
<div class="notice">The message this answers is <strong>no longer in the
mirror</strong> — it was expunged on the server. The reply itself is unaffected:
its recipients and subject were copied when it was created. Only the quoted text
came from the original.</div>
{{end}}
<h1>{{if .Draft.Subject}}{{.Draft.Subject}}{{else}}(no subject){{end}}</h1>
<div class="headers">
From: <strong>{{.From}}</strong><br>
{{/* Stated because it is derived from the contents rather than chosen: a
code listing is what makes the body MIME, and an attachment is what
encloses it in a mixed. The page says which before anything is sent. */}}
Will be sent as: <strong>{{.Draft.WireFormat}}</strong>
</div>
{{/* Where a petname exists a quote is attributed twice, and neither line is
the editor's to show: the reader sees the name they assigned, the
recipients see the name the sender gave themselves. A petname is local and
never travels, so the line that goes on the wire is stated here rather
than silently differing from what is on screen. */}}
{{range .Blocks}}
{{if and .HasQuotedFrom .QuotedFrom.Petname}}
<div class="headers">
Quoting {{template "name" .QuotedFrom}} — goes out as:
<span class="wire">{{.Meta.Attribution}}</span>
</div>
{{end}}
{{end}}
{{/* The editor. Without script this element stays empty and the draft is
still readable below: what it will be sent as, and to whom. */}}
<div data-draft-editor="{{.Draft.Token}}"></div>
<noscript>
<div class="blocks">
{{range .Blocks}}
<div class="block block-{{.Kind}}">
<div class="block-head">
<span class="kind">{{.Kind}}</span>
{{if .Meta.Language}}<span>{{.Meta.Language}}</span>{{end}}
</div>
{{if .Meta.Attribution}}<div class="attribution">{{.Meta.Attribution}}</div>{{end}}
{{if .Content}}<pre>{{.Content}}</pre>{{else}}<p class="empty">(empty)</p>{{end}}
</div>
{{end}}
</div>
<p class="empty">Editing a draft needs script. Everything else on this page —
what will be sent, to whom, attaching a file, adding a recipient, and the
buttons that send or discard it — does not.</p>
</noscript>
{{/* Attachments are listed apart from the blocks because that is what they
are on the wire: enclosed beside the body rather than part of it. The
type shown is the one derived from the bytes, never what the upload
claimed it was. */}}
<h2>Attached</h2>
<div class="attachments">
{{if .Attachments}}
{{range .Attachments}}
<div class="attachment">
{{if .IsImage}}<a href="{{.AssetURL}}" target="_blank"><img src="{{.AssetURL}}" alt="{{.Content}}"></a>{{end}}
<span class="grow">
<a class="name" href="{{.AssetURL}}" target="_blank">{{.Content}}</a><br>
<span class="type">{{.Asset.MimeType}} — {{.AssetSize}}</span>
</span>
{{if not $.Draft.Sent}}
<form class="inline-form" method="POST"
action="/draft/{{$.Draft.Token}}/assets/{{.Asset.ID}}/remove"
onsubmit="return confirm('Remove this attachment from the draft?')">
<button class="rm" type="submit">remove</button>
</form>
{{end}}
</div>
{{end}}
<p class="hint">{{.AttachmentSize}} of the {{.AttachmentLimit}} a draft may hold.</p>
{{else}}
<p class="empty">Nothing attached.</p>
{{end}}
{{if not .Draft.Sent}}
<form class="upload" method="POST" action="/draft/{{.Draft.Token}}/assets"
enctype="multipart/form-data">
<input type="file" name="file" multiple required>
<button type="submit">Attach</button>
<p class="hint">Several at once is fine. A file may be up to 25MB and a draft
may hold {{.AttachmentLimit}} in total. Attaching sends nothing.</p>
</form>
{{end}}
</div>
<h2>Send this mail to</h2>
{{if not .SMTPEnabled}}
<div class="notice">No SMTP is configured for this account
(<code>--smtp-host</code> is missing), so nothing can be sent. What follows is
who this reply <em>would</em> go to.</div>
{{end}}
{{if .Sets}}
<p class="send-note">Nothing is selected. Each option writes to a different set
of people, and the button says which.</p>
{{range .Sets}}
<div class="option">
<div class="option-head">
<span class="option-label">{{.Label}}</span>
<span class="count">{{.Count}} recipient{{if ne .Count 1}}s{{end}}</span>
</div>
<div class="explain">{{.Explain}}</div>
<div class="rcpts">
<span class="field">To:</span>
{{if .To}}
{{range $i, $n := .To}}{{if $i}}, {{end}}{{template "name" $n}} <{{$n.Address}}>{{end}}
{{else}}<span class="empty">nobody</span>{{end}}
{{if .Cc}}<br><span class="field">Cc:</span>
{{range $i, $n := .Cc}}{{if $i}}, {{end}}{{template "name" $n}} <{{$n.Address}}>{{end}}{{end}}
</div>
{{/* Only the entered set may be edited. The others record who the message
being answered named, and the send button's label is built from those
same rows — editing them would let the addresses drift away from the
words somebody read before pressing it. */}}
{{if and .Entered (not $.Draft.Sent)}}
<div class="rcpts">
{{range .To}}
<form class="inline-form" method="POST" action="/draft/{{$.Draft.Token}}/recipients/remove">
<input type="hidden" name="address" value="{{.Address}}">
<input type="hidden" name="kind" value="to">
<button class="rm" type="submit" title="Remove {{.Address}} from To">− To: {{.Address}}</button>
</form>
{{end}}
{{range .Cc}}
<form class="inline-form" method="POST" action="/draft/{{$.Draft.Token}}/recipients/remove">
<input type="hidden" name="address" value="{{.Address}}">
<input type="hidden" name="kind" value="cc">
<button class="rm" type="submit" title="Remove {{.Address}} from Cc">− Cc: {{.Address}}</button>
</form>
{{end}}
</div>
{{end}}
{{if .NoReply}}
<div class="warn">This address looks like one that does not accept replies.
Mail sent here is commonly discarded without anybody reading it.</div>
{{end}}
{{if .NeedsTo}}
<div class="warn">Everyone here is in Cc and nobody is in To, so this cannot
be sent: a message with an empty To is accepted by some servers and delivered
to nobody. Add an address to To below.</div>
{{else if $.SMTPEnabled}}
<form method="POST" action="/draft/{{$.Draft.Token}}/send">
<input type="hidden" name="set" value="{{.Name}}">
<button type="submit">Send to {{.Label}} ({{.Count}})</button>
</form>
{{end}}
</div>
{{end}}
{{else}}
<p class="send-note">{{if .Draft.ParentID}}This draft names nobody to send to:
the message it answers carried no address that could be replied to. Enter one
below.{{else}}This is a new mail and is addressed to nobody yet. Enter an
address below.{{end}}</p>
{{end}}
{{/* Entering an address is inert: it writes a row on this machine, and it is
shown in full above before anything can be sent. Sending still names a
set rather than an address, so no address reaches the wire that was not
stored and displayed first. */}}
{{if not .Draft.Sent}}
<form class="add-rcpt" method="POST" action="/draft/{{.Draft.Token}}/recipients">
<label>
Add a recipient
<input type="email" name="address" placeholder="name@example.org"
multiple required aria-label="Address to add">
</label>
<select name="kind" aria-label="To or Cc">
<option value="to">To</option>
<option value="cc">Cc</option>
</select>
<button type="submit">Add</button>
<p class="hint">Goes into “the addresses you entered”, which is a set of its
own so that what you typed is never confused with what mailweb worked out
from the message. Several at once may be separated by commas. Nothing is
sent.</p>
</form>
{{end}}
{{if not .Draft.Sent}}
<div class="discard">
<form method="POST" action="/draft/{{.Draft.Token}}/discard"
onsubmit="return confirm('Discard this draft? It cannot be recovered.')">
<button type="submit">Discard draft</button>
</form>
</div>
{{end}}
{{template "editorScript"}}
</body>
</html>
|