1package main
2
3import (
4 "bytes"
5 "encoding/base64"
6 "encoding/json"
7 "fmt"
8 "log"
9 "reflect"
10 "strings"
11 "time"
12
13 "github.com/mjl-/sconf"
14
15 "github.com/mjl-/mox/config"
16 "github.com/mjl-/mox/mox-"
17 "github.com/mjl-/mox/smtp"
18 "github.com/mjl-/mox/webhook"
19)
20
21func cmdExample(c *cmd) {
22 c.params = "[name]"
23 c.help = `List available examples, or print a specific example.`
24
25 args := c.Parse()
26 if len(args) > 1 {
27 c.Usage()
28 }
29
30 var match func() string
31 for _, ex := range examples {
32 if len(args) == 0 {
33 fmt.Println(ex.Name)
34 } else if args[0] == ex.Name {
35 match = ex.Get
36 }
37 }
38 if len(args) == 0 {
39 return
40 }
41 if match == nil {
42 log.Fatalln("not found")
43 }
44 fmt.Print(match())
45}
46
47func cmdConfigExample(c *cmd) {
48 c.params = "[name]"
49 c.help = `List available config examples, or print a specific example.`
50
51 args := c.Parse()
52 if len(args) > 1 {
53 c.Usage()
54 }
55
56 var match func() string
57 for _, ex := range configExamples {
58 if len(args) == 0 {
59 fmt.Println(ex.Name)
60 } else if args[0] == ex.Name {
61 match = ex.Get
62 }
63 }
64 if len(args) == 0 {
65 return
66 }
67 if match == nil {
68 log.Fatalln("not found")
69 }
70 fmt.Print(match())
71}
72
73var configExamples = []struct {
74 Name string
75 Get func() string
76}{
77 {
78 "webhandlers",
79 func() string {
80 const webhandlers = `# Snippet of domains.conf to configure WebDomainRedirects and WebHandlers.
81
82# Redirect all requests for mox.example to https://www.mox.example.
83WebDomainRedirects:
84 mox.example: www.mox.example
85
86# Each request is matched against these handlers until one matches and serves it.
87WebHandlers:
88 -
89 # Redirect all plain http requests to https, leaving path, query strings, etc
90 # intact. When the request is already to https, the destination URL would have the
91 # same scheme, host and path, causing this redirect handler to not match the
92 # request (and not cause a redirect loop) and the webserver to serve the request
93 # with a later handler.
94 LogName: redirhttps
95 Domain: www.mox.example
96 PathRegexp: ^/
97 # Could leave DontRedirectPlainHTTP at false if it wasn't for this being an
98 # example for doing this redirect.
99 DontRedirectPlainHTTP: true
100 WebRedirect:
101 BaseURL: https://www.mox.example
102 -
103 # The name of the handler, used in logging and metrics.
104 LogName: staticmjl
105 # With ACME configured, each configured domain will automatically get a TLS
106 # certificate on first request.
107 Domain: www.mox.example
108 PathRegexp: ^/who/mjl/
109 WebStatic:
110 StripPrefix: /who/mjl
111 # Requested path /who/mjl/inferno/ resolves to local web/mjl/inferno.
112 # If a directory contains an index.html, it is served when a directory is requested.
113 Root: web/mjl
114 # With ListFiles true, if a directory does not contain an index.html, the contents are listed.
115 ListFiles: true
116 ResponseHeaders:
117 X-Mox: hi
118 -
119 LogName: redir
120 Domain: www.mox.example
121 PathRegexp: ^/redir/a/b/c
122 # Don't redirect from plain HTTP to HTTPS.
123 DontRedirectPlainHTTP: true
124 WebRedirect:
125 # Just change the domain and add query string set fragment. No change to scheme.
126 # Path will start with /redir/a/b/c (and whathever came after) because no
127 # OrigPathRegexp+ReplacePath is set.
128 BaseURL: //moxest.example?q=1#frag
129 # Default redirection is 308 - Permanent Redirect.
130 StatusCode: 307
131 -
132 LogName: oldnew
133 Domain: www.mox.example
134 PathRegexp: ^/old/
135 WebRedirect:
136 # Replace path, leaving rest of URL intact.
137 OrigPathRegexp: ^/old/(.*)
138 ReplacePath: /new/$1
139 -
140 LogName: app
141 Domain: www.mox.example
142 PathRegexp: ^/app/
143 WebForward:
144 # Strip the path matched by PathRegexp before forwarding the request. So original
145 # request /app/api become just /api.
146 StripPath: true
147 # URL of backend, where requests are forwarded to. The path in the URL is kept,
148 # so for incoming request URL /app/api, the outgoing request URL has path /app-v2/api.
149 # Requests are made with Go's net/http DefaultTransporter, including using
150 # HTTP_PROXY and HTTPS_PROXY environment variables.
151 URL: http://127.0.0.1:8900/app-v2/
152 # Add headers to response.
153 ResponseHeaders:
154 X-Frame-Options: deny
155 X-Content-Type-Options: nosniff
156`
157 // Parse just so we know we have the syntax right.
158 // todo: ideally we would have a complete config file and parse it fully.
159 var conf struct {
160 WebDomainRedirects map[string]string
161 WebHandlers []config.WebHandler
162 }
163 err := sconf.Parse(strings.NewReader(webhandlers), &conf)
164 xcheckf(err, "parsing webhandlers example")
165 return webhandlers
166 },
167 },
168 {
169 "transport",
170 func() string {
171 const moxconf = `# Snippet for mox.conf, defining a transport called Example that connects on the
172# SMTP submission with TLS port 465 ("submissions"), authenticating with
173# SCRAM-SHA-256-PLUS (other providers may not support SCRAM-SHA-256-PLUS, but they
174# typically do support the older CRAM-MD5).:
175
176# Transport are mechanisms for delivering messages. Transports can be referenced
177# from Routes in accounts, domains and the global configuration. There is always
178# an implicit/fallback delivery transport doing direct delivery with SMTP from the
179# outgoing message queue. Transports are typically only configured when using
180# smarthosts, i.e. when delivering through another SMTP server. Zero or one
181# transport methods must be set in a transport, never multiple. When using an
182# external party to send email for a domain, keep in mind you may have to add
183# their IP address to your domain's SPF record, and possibly additional DKIM
184# records. (optional)
185Transports:
186 Example:
187 # Submission SMTP over a TLS connection to submit email to a remote queue.
188 # (optional)
189 Submissions:
190 # Host name to connect to and for verifying its TLS certificate.
191 Host: smtp.example.com
192
193 # If set, authentication credentials for the remote server. (optional)
194 Auth:
195 Username: user@example.com
196 Password: test1234
197 Mechanisms:
198 # Allowed authentication mechanisms. Defaults to SCRAM-SHA-256-PLUS,
199 # SCRAM-SHA-256, SCRAM-SHA-1-PLUS, SCRAM-SHA-1, CRAM-MD5. Not included by default:
200 # PLAIN. Specify the strongest mechanism known to be implemented by the server to
201 # prevent mechanism downgrade attacks. (optional)
202
203 - SCRAM-SHA-256-PLUS
204`
205
206 const domainsconf = `# Snippet for domains.conf, specifying a route that sends through the transport:
207
208# Routes for delivering outgoing messages through the queue. Each delivery attempt
209# evaluates account routes, domain routes and finally these global routes. The
210# transport of the first matching route is used in the delivery attempt. If no
211# routes match, which is the default with no configured routes, messages are
212# delivered directly from the queue. (optional)
213Routes:
214 -
215 Transport: Example
216`
217
218 var static struct {
219 Transports map[string]config.Transport
220 }
221 var dynamic struct {
222 Routes []config.Route
223 }
224 err := sconf.Parse(strings.NewReader(moxconf), &static)
225 xcheckf(err, "parsing moxconf example")
226 err = sconf.Parse(strings.NewReader(domainsconf), &dynamic)
227 xcheckf(err, "parsing domainsconf example")
228 return moxconf + "\n\n" + domainsconf
229 },
230 },
231}
232
233var exampleTime = time.Date(2024, time.March, 27, 0, 0, 0, 0, time.UTC)
234
235var examples = []struct {
236 Name string
237 Get func() string
238}{
239 {
240 "webhook-outgoing-delivered",
241 func() string {
242 v := webhook.Outgoing{
243 Version: 0,
244 Event: webhook.EventDelivered,
245 QueueMsgID: 101,
246 FromID: base64.RawURLEncoding.EncodeToString([]byte("0123456789abcdef")),
247 MessageID: "<QnxzgulZK51utga6agH_rg@mox.example>",
248 Subject: "subject of original message",
249 WebhookQueued: exampleTime,
250 Extra: map[string]string{},
251 SMTPCode: smtp.C250Completed,
252 }
253 return "Example webhook HTTP POST JSON body for successful outgoing delivery:\n\n\t" + formatJSON(v)
254 },
255 },
256 {
257 "webhook-outgoing-dsn-failed",
258 func() string {
259 v := webhook.Outgoing{
260 Version: 0,
261 Event: webhook.EventFailed,
262 DSN: true,
263 Suppressing: true,
264 QueueMsgID: 102,
265 FromID: base64.RawURLEncoding.EncodeToString([]byte("0123456789abcdef")),
266 MessageID: "<QnxzgulZK51utga6agH_rg@mox.example>",
267 Subject: "subject of original message",
268 WebhookQueued: exampleTime,
269 Extra: map[string]string{"userid": "456"},
270 Error: "timeout connecting to host",
271 SMTPCode: smtp.C554TransactionFailed,
272 SMTPEnhancedCode: "5." + smtp.SeNet4Other0,
273 }
274 return `Example webhook HTTP POST JSON body for failed delivery based on incoming DSN
275message, with custom extra data fields (from original submission), and adding address to the suppression list:
276
277 ` + formatJSON(v)
278 },
279 },
280 {
281 "webhook-incoming-basic",
282 func() string {
283 v := webhook.Incoming{
284 Version: 0,
285 From: []webhook.NameAddress{{Address: "mox@localhost"}},
286 To: []webhook.NameAddress{{Address: "mjl@localhost"}},
287 Subject: "hi",
288 MessageID: "<QnxzgulZK51utga6agH_rg@mox.example>",
289 Date: &exampleTime,
290 Text: "hello world ☺\n",
291 Structure: webhook.Structure{
292 ContentType: "text/plain",
293 ContentTypeParams: map[string]string{"charset": "utf-8"},
294 DecodedSize: int64(len("hello world ☺\r\n")),
295 Parts: []webhook.Structure{},
296 },
297 Meta: webhook.IncomingMeta{
298 MsgID: 201,
299 MailFrom: "mox@localhost",
300 MailFromValidated: false,
301 MsgFromValidated: true,
302 RcptTo: "mjl@localhost",
303 DKIMVerifiedDomains: []string{"localhost"},
304 RemoteIP: "127.0.0.1",
305 Received: exampleTime.Add(3 * time.Second),
306 MailboxName: "Inbox",
307 Automated: false,
308 },
309 }
310 return "Example JSON body for webhooks for incoming delivery of basic message:\n\n\t" + formatJSON(v)
311 },
312 },
313}
314
315func formatJSON(v any) string {
316 nv, _ := mox.FillNil(reflect.ValueOf(v))
317 v = nv.Interface()
318 var b bytes.Buffer
319 enc := json.NewEncoder(&b)
320 enc.SetIndent("\t", "\t")
321 enc.SetEscapeHTML(false)
322 err := enc.Encode(v)
323 xcheckf(err, "encoding to json")
324 return b.String()
325}
326