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
|
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>mailweb ({{account}}) — {{.Contact}}</title>
<link rel="alternate" type="text/llm" href="/contact/{{.AddrEscaped}}"
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-sent: #eef4fb;
--border: #dddddd;
--border-sent: #a8c8e8;
--text: #111111;
--text-meta: #666666;
--link: #3781b8;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #1a1a1a;
--bg-card: #242424;
--bg-sent: #1e2d3d;
--border: #3a3a3a;
--border-sent: #3a6080;
--text: #e8e8e8;
--text-meta: #999999;
--link: #7ab3e0;
}
}
body { font-family: sans-serif; margin: 0; padding: 1rem;
background: var(--bg); color: var(--text); }
h1 { font-size: 1.2rem; margin-bottom: 1rem; }
.msg { border-radius: 4px; margin-bottom: 0.6rem; padding: 0.6rem 0.9rem; }
.msg-received {
background: var(--bg-card);
border: 1px solid var(--border);
margin-right: 3rem;
}
.msg-sent {
background: var(--bg-sent);
border: 1px solid var(--border-sent);
margin-left: 3rem;
}
h2 { margin: 0 0 0.2rem 0; font-size: 1rem; }
.meta { color: var(--text-meta); font-size: 0.85rem; margin-bottom: 0.4rem; }
.dir { font-size: 0.8rem; font-weight: bold; margin-right: 0.4rem; }
.dir-recv { color: var(--text-meta); }
.dir-sent { color: var(--link); }
a { color: var(--link); text-decoration: none; }
a:hover { text-decoration: underline; }
nav { margin-bottom: 1rem; display: flex; gap: 1.5rem; }
iframe { width: 100%; border: none; display: block; }
details { margin-top: 0.3rem; }
details summary { cursor: pointer; color: var(--text-meta); font-size: 0.85rem; }
.star { color: #c8920a; }
@media (prefers-color-scheme: dark) { .star { color: #e0aa30; } }
button.star-btn {
background: none; border: none; cursor: pointer;
font-size: 1rem; padding: 0 0.3rem; vertical-align: middle;
color: var(--text-meta);
}
button.star-btn.starred { color: #c8920a; }
@media (prefers-color-scheme: dark) { button.star-btn.starred { color: #e0aa30; } }
a.spam-link {
font-size: 0.85rem; color: var(--text-meta); text-decoration: none;
vertical-align: middle; margin-left: 0.4rem;
}
a.spam-link:hover { color: #c0392b; text-decoration: underline; }
@media (prefers-color-scheme: dark) { a.spam-link:hover { color: #e05c4a; } }
.spam-badge {
font-size: 0.8rem; color: #c0392b; vertical-align: middle;
margin-left: 0.4rem;
}
@media (prefers-color-scheme: dark) { .spam-badge { color: #e05c4a; } }
.unsub-form { margin-top: 0.3rem; }
button.unsub-btn {
background: none; border: 1px solid var(--border); border-radius: 3px;
color: var(--text-meta); font-size: 0.8rem; padding: 0.2rem 0.5rem;
cursor: pointer;
}
button.unsub-btn:hover { color: var(--text); border-color: var(--text-meta); }
.scope {
background: var(--bg-card); border: 1px solid var(--border);
border-radius: 4px; padding: 0.5rem 0.9rem; margin-bottom: 1rem;
color: var(--text-meta); font-size: 0.85rem;
}
h2 a.subj { color: var(--text); text-decoration: none; }
h2 a.subj:hover { color: var(--link); text-decoration: underline; }
{{template "nameStyle"}}
{{template "attachmentStyle"}}
{{template "editorStyle"}}
</style>
<script>
function resizeIframe(el) {
el.style.height = el.contentDocument.documentElement.scrollHeight + 'px';
}
</script>
</head>
<body>
<nav><a href="/contacts">← contacts</a>
<a href="{{.LLMViewURL}}" title="Compact plain-text rendering of this page">text view →</a></nav>
<h1>
{{if .Important}}
<form method="POST" action="/contact/{{.AddrEscaped}}/unstar" style="display:inline">
<button class="star-btn starred" type="submit" title="unstar">★</button>
</form>
{{else}}
<form method="POST" action="/contact/{{.AddrEscaped}}/star" style="display:inline">
<button class="star-btn" type="submit" title="mark as important">☆</button>
</form>
{{end}}
{{template "name" .Name}} <{{.Contact}}> — {{len .Messages}} messages
{{/* Naming happens on the settings page, which is also where the flags shown
beside this heading are listed in full. */}}
{{template "penLink" .Pen}}
{{/* No spam link on your own address: there is nobody to report yourself to.
See the same guard on the settings page. */}}
{{if not .Self}}
{{if .Spam}}
<span class="spam-badge" title="reported as spam">⚑ spam</span>
{{else}}
<a class="spam-link" href="/contact/{{.AddrEscaped}}/report-spam" title="report as spam">⚑ report spam</a>
{{end}}
{{end}}
{{if .ListUnsubscribe}}
<form style="display:inline" method="POST" action="/unsubscribe/contact/{{.AddrEscaped}}">
<button class="unsub-btn" type="submit" title="Send unsubscribe email to {{.ListUnsubscribe}}">✉ unsubscribe</button>
</form>
{{else if .ListUnsubscribeURL}}
<form style="display:inline" method="POST" action="/unsubscribe/contact/{{.AddrEscaped}}">
<button class="unsub-btn" type="submit"
onclick="window.open('{{.ListUnsubscribeURL}}', '_blank')"
title="Open unsubscribe page: {{.ListUnsubscribeURL}}">✉ unsubscribe</button>
</form>
{{end}}
</h1>
{{/* Your own address is on a header of nearly every message in the mirror, so
the substring listing is the whole archive rather than a conversation. The
page says which of the two it is showing and links to the other, because a
restriction that does not announce itself reads as an absence. */}}
{{if .Self}}
<div class="scope">
{{if .SelfOnly}}
This is your own address. Showing only the {{len .Messages}} message{{if ne (len .Messages) 1}}s{{end}}
you sent to nobody but yourself — your notes to self.
Your address also appears in a header of {{.MatchedCount}} messages in all,
which is most of the archive rather than a correspondence:
<a href="{{.AllURL}}">show all {{.MatchedCount}}</a>.
{{else}}
This is your own address, and this listing is every message carrying it in any
header — {{.MatchedCount}} of them, which is most of the archive rather than a
correspondence. <a href="{{.SelfURL}}">Show only your notes to self</a>.
{{end}}
</div>
{{end}}
{{range .Messages}}
<div class="msg {{if eq .Direction "sent"}}msg-sent{{else}}msg-received{{end}}">
{{/* /view rather than /msg/{id}: the latter is the fragment framed below, and
following it lands on a body with no header block. */}}
<h2><a class="subj" href="/msg/{{.ID}}/view">{{if .IsUnsubscribe}}Unsubscribe request{{else if .Subject}}{{.Subject}}{{else}}(no subject){{end}}</a></h2>
{{if .ListUnsubscribe}}
<form class="unsub-form" method="POST" action="/unsubscribe/{{.ID}}">
<button class="unsub-btn" type="submit" title="Send unsubscribe email to {{.ListUnsubscribe}}">✉ unsubscribe</button>
</form>
{{else if .ListUnsubscribeURL}}
<form class="unsub-form" method="POST" action="/unsubscribe/{{.ID}}">
<button class="unsub-btn" type="submit"
onclick="window.open('{{.ListUnsubscribeURL}}', '_blank')"
title="Open unsubscribe page: {{.ListUnsubscribeURL}}">✉ unsubscribe</button>
</form>
{{end}}
<div class="meta">
{{if eq .Direction "sent"}}
<span class="dir dir-sent">→</span>To: {{if .ToAddrs}}{{.ToAddrs}}{{else}}—{{end}}
{{else}}
<span class="dir dir-recv">←</span>From: {{template "name" .From}} <{{.From.Address}}>
{{end}}
| {{.Date.Format "Mon, 02 Jan 2006 15:04"}}{{with .SizeHint}}{{.}}{{end}}
</div>
{{/* Above the body, and above the <details> that may hide it: in a
conversation that is mostly booking confirmations, the events are the
thread and the covering letters are noise. */}}
{{template "calendars" (index $.AttachmentsByMsg .ID)}}
{{if eq .MimeType "text/plain"}}
<iframe src="/msg/{{.ID}}" sandbox="allow-same-origin allow-popups"
loading="lazy" onload="resizeIframe(this)"></iframe>
{{else}}
<details>
<summary>show message</summary>
<iframe src="/msg/{{.ID}}" sandbox="allow-same-origin allow-popups"
loading="lazy" onload="resizeIframe(this)"></iframe>
</details>
{{end}}
{{/* Outside the frame, and outside the <details> that may hide the body: what
is attached is exactly what a reader wants without unfolding the message.
See templates/attachments.html. */}}
{{template "attachments" (index $.AttachmentsByMsg .ID)}}
{{/* Below the message rather than above it: a reply is what you do after
reading one. Nothing is sent — this composes a draft, and the draft's
own page is where the recipients are listed and the send buttons are. */}}
{{if not .IsUnsubscribe}}
{{$drafts := index $.DraftsByMsg .ID}}
{{range $drafts}}
<div data-draft-editor="{{.Token}}"></div>
{{else}}
{{template "replyButton" .ID}}
{{end}}
{{end}}
</div>
{{end}}
{{template "editorScript"}}
</body>
</html>
|