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
|
{{/*
The summary of an attached calendar, as a framed fragment.
This is a sibling of msgbody.html and carries the same protections for the
same reason: an event's description is free text written by whoever sent the
mail, frequently containing markup, and one of the descriptions in this
archive is a Deutsche Bahn check-in link. Rendering that usefully means
rendering sender HTML, which belongs in a document with no script and no
remote loads, framed without allow-scripts — exactly where a message body
goes.
Everything mailweb states about the event — the times, the zone label, the
cancellation, the recurrence warning — is above the description, and the
description is visibly a quotation of the sender. The distinction is the same
one drawn in attachments.html, but it cannot be drawn by placement here, since
the whole fragment is one document. So the description is fenced by a rule and
a caption instead.
Being a fragment is also what makes the summary affordable. An attachment is
never cached, so a summary costs an IMAP round-trip; the unpaged contact view
for one carsharing sender lists 59 of them, which inline would be a minute of
serial fetching before the page appeared. As lazily-loaded frames the browser
issues them in parallel, off the page's own render, and only for what is
scrolled into view — the argument report_spam.html already makes for message
bodies.
*/}}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="script-src 'none'; img-src 'self'">
<base target="_blank">
<style>
:root { color-scheme: light dark; }
body {
margin: 0; font: 13px -apple-system, system-ui, 'Segoe UI', Roboto, sans-serif;
color: #111;
}
@media (prefers-color-scheme: dark) {
body { color: #e8e8e8; }
a[href] { color: #7ab3e0; }
.ev { border-color: #3a3a3a !important; }
.desc { border-top-color: #3a3a3a !important; }
.cap, .meta, .sniffed { color: #999 !important; }
}
a[href] { color: #3781b8; text-decoration: none; }
a[href]:hover { text-decoration: underline; }
.ev { border: 1px solid #ddd; border-radius: 4px; padding: 0.5rem 0.7rem; margin-bottom: 0.5rem; }
.ev:last-child { margin-bottom: 0; }
.summary { font-weight: 600; }
.cancelled .summary { text-decoration: line-through; }
.badge {
font-size: 0.72em; border: 1px solid currentColor; border-radius: 3px;
padding: 0 0.35em; margin-left: 0.4em; vertical-align: middle;
color: #c0392b; white-space: nowrap;
}
@media (prefers-color-scheme: dark) { .badge { color: #e05c4a; } }
.when { margin-top: 0.15rem; }
/* How far off the event is, in words. Dimmer than the date it qualifies:
the date is what the sender wrote, this is mailweb's arithmetic. */
.rel { color: #666; font-size: 0.85em; }
.rel.past { font-style: italic; }
@media (prefers-color-scheme: dark) { .rel { color: #999; } }
.meta { color: #666; }
.warn {
margin-top: 0.3rem; font-size: 0.85em; color: #8a6d1f;
border-left: 3px solid currentColor; padding-left: 0.5em;
}
@media (prefers-color-scheme: dark) { .warn { color: #d0a94a; } }
.desc { margin-top: 0.5rem; border-top: 1px solid #eee; padding-top: 0.4rem; }
.cap { font-size: 0.75em; color: #666; margin-bottom: 0.25rem; }
.sniffed { font-size: 0.75em; color: #666; font-style: italic; }
pre { white-space: pre-wrap; word-break: break-word; margin: 0; font: inherit; }
img { max-width: 100%; }
</style>
</head>
<body>
{{range .Events}}
<div class="ev{{if .Cancelled}} cancelled{{end}}">
{{$ev := .}}
<div class="summary">
{{- if .Summary}}{{.Summary}}{{else}}(untitled event){{end -}}
{{- if .Cancelled}}<span class="badge" title="The sender withdrew this event: it is marked CANCELLED.">cancelled</span>{{end -}}
</div>
{{with .FormatWhen}}
{{/* Shown in the zone the sender named and labelled with it. mailweb does not
convert: doing so needs the event's zone, the reader's and a current zone
database all to be right, and a wrong hour displayed confidently is how a
calendar makes someone miss a meeting. */}}
<div class="when">{{.}}
{{- with $ev.Relative}} <span class="rel{{if $ev.IsPast}} past{{end}}">{{.}}</span>{{end -}}
</div>
{{end}}
{{if .Location}}
{{/* GEO coordinates map exactly; a URL-valued location is a video call and
links to itself; anything else is a map search, which is a guess at what
the string names. All three are links and none is embedded, so nothing is
requested until the reader clicks. */}}
<div class="meta">
{{with .LocationURL}}<a href="{{.}}" rel="noreferrer">{{end}}
{{- .Location -}}
{{if .LocationURL}}</a>{{end}}
{{- if .LocationIsSearch}} <span title="A map search for this text. The sender gave no coordinates, so this is a guess at what the string names.">(map search)</span>{{end}}
</div>
{{end}}
{{with .RecurrenceWarning}}
<div class="warn">⚠ {{.}}</div>
{{end}}
{{if .Description}}
<div class="desc">
<div class="cap">description, written by the sender:</div>
{{if .DescriptionIsHTML}}
{{/* Sender markup, rendered under this document's CSP and the sandbox of
the frame around it. Nothing here can execute and no remote image can
load, which is the same guarantee a message body gets. */}}
{{safeHTML .Description}}
{{if .DescriptionSniffed}}
<div class="sniffed">
The sender did not mark this as HTML; mailweb judged it to be markup.
</div>
{{end}}
{{else}}
<pre>{{.Description}}</pre>
{{end}}
</div>
{{end}}
</div>
{{else}}
<div class="meta">No events could be read from this calendar file.</div>
{{end}}
</body>
</html>
|