13 "github.com/mjl-/sconf"
15 "github.com/mjl-/mox/config"
16 "github.com/mjl-/mox/mox-"
17 "github.com/mjl-/mox/smtp"
18 "github.com/mjl-/mox/webhook"
21func cmdExample(c *cmd) {
23 c.help = `List available examples, or print a specific example.`
30 var match func() string
31 for _, ex := range examples {
34 } else if args[0] == ex.Name {
42 log.Fatalln("not found")
47func cmdConfigExample(c *cmd) {
49 c.help = `List available config examples, or print a specific example.`
56 var match func() string
57 for _, ex := range configExamples {
60 } else if args[0] == ex.Name {
68 log.Fatalln("not found")
73var configExamples = []struct {
80 const webhandlers = `# Snippet of domains.conf to configure WebDomainRedirects and WebHandlers.
82# Redirect all requests for mox.example to https://www.mox.example.
84 mox.example: www.mox.example
86# Each request is matched against these handlers until one matches and serves it.
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.
95 Domain: www.mox.example
97 # Could leave DontRedirectPlainHTTP at false if it wasn't for this being an
98 # example for doing this redirect.
99 DontRedirectPlainHTTP: true
101 BaseURL: https://www.mox.example
103 # The name of the handler, used in logging and metrics.
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/
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.
114 # With ListFiles true, if a directory does not contain an index.html, the contents are listed.
120 Domain: www.mox.example
121 PathRegexp: ^/redir/a/b/c
122 # Don't redirect from plain HTTP to HTTPS.
123 DontRedirectPlainHTTP: true
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.
133 Domain: www.mox.example
136 # Replace path, leaving rest of URL intact.
137 OrigPathRegexp: ^/old/(.*)
141 Domain: www.mox.example
144 # Strip the path matched by PathRegexp before forwarding the request. So original
145 # request /app/api become just /api.
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.
154 X-Frame-Options: deny
155 X-Content-Type-Options: nosniff
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.
160 WebDomainRedirects map[string]string
161 WebHandlers []config.WebHandler
163 err := sconf.Parse(strings.NewReader(webhandlers), &conf)
164 xcheckf(err, "parsing webhandlers example")
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).:
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
187 # Submission SMTP over a TLS connection to submit email to a remote queue.
190 # Host name to connect to and for verifying its TLS certificate.
191 Host: smtp.example.com
193 # If set, authentication credentials for the remote server. (optional)
195 Username: user@example.com
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)
206 const domainsconf = `# Snippet for domains.conf, specifying a route that sends through the transport:
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)
219 Transports map[string]config.Transport
222 Routes []config.Route
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
233var exampleTime = time.Date(2024, time.March, 27, 0, 0, 0, 0, time.UTC)
235var examples = []struct {
240 "webhook-outgoing-delivered",
242 v := webhook.Outgoing{
244 Event: webhook.EventDelivered,
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,
253 return "Example webhook HTTP POST JSON body for successful outgoing delivery:\n\n\t" + formatJSON(v)
257 "webhook-outgoing-dsn-failed",
259 v := webhook.Outgoing{
261 Event: webhook.EventFailed,
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,
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:
281 "webhook-incoming-basic",
283 v := webhook.Incoming{
285 From: []webhook.NameAddress{{Address: "mox@localhost"}},
286 To: []webhook.NameAddress{{Address: "mjl@localhost"}},
288 MessageID: "<QnxzgulZK51utga6agH_rg@mox.example>",
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{},
297 Meta: webhook.IncomingMeta{
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",
310 return "Example JSON body for webhooks for incoming delivery of basic message:\n\n\t" + formatJSON(v)
315func formatJSON(v any) string {
316 nv, _ := mox.FillNil(reflect.ValueOf(v))
319 enc := json.NewEncoder(&b)
320 enc.SetIndent("\t", "\t")
321 enc.SetEscapeHTML(false)
323 xcheckf(err, "encoding to json")