package main import ( "bytes" "encoding/base64" "encoding/json" "fmt" "log" "reflect" "strings" "time" "github.com/mjl-/sconf" "github.com/mjl-/mox/config" "github.com/mjl-/mox/mox-" "github.com/mjl-/mox/smtp" "github.com/mjl-/mox/webhook" ) func cmdExample(c *cmd) { c.params = "[name]" c.help = `List available examples, or print a specific example.` args := c.Parse() if len(args) > 1 { c.Usage() } var match func() string for _, ex := range examples { if len(args) == 0 { fmt.Println(ex.Name) } else if args[0] == ex.Name { match = ex.Get } } if len(args) == 0 { return } if match == nil { log.Fatalln("not found") } fmt.Print(match()) } func cmdConfigExample(c *cmd) { c.params = "[name]" c.help = `List available config examples, or print a specific example.` args := c.Parse() if len(args) > 1 { c.Usage() } var match func() string for _, ex := range configExamples { if len(args) == 0 { fmt.Println(ex.Name) } else if args[0] == ex.Name { match = ex.Get } } if len(args) == 0 { return } if match == nil { log.Fatalln("not found") } fmt.Print(match()) } var configExamples = []struct { Name string Get func() string }{ { "webhandlers", func() string { const webhandlers = `# Snippet of domains.conf to configure WebDomainRedirects and WebHandlers. # Redirect all requests for mox.example to https://www.mox.example. WebDomainRedirects: mox.example: www.mox.example # Each request is matched against these handlers until one matches and serves it. WebHandlers: - # Redirect all plain http requests to https, leaving path, query strings, etc # intact. When the request is already to https, the destination URL would have the # same scheme, host and path, causing this redirect handler to not match the # request (and not cause a redirect loop) and the webserver to serve the request # with a later handler. LogName: redirhttps Domain: www.mox.example PathRegexp: ^/ # Could leave DontRedirectPlainHTTP at false if it wasn't for this being an # example for doing this redirect. DontRedirectPlainHTTP: true WebRedirect: BaseURL: https://www.mox.example - # The name of the handler, used in logging and metrics. LogName: staticmjl # With ACME configured, each configured domain will automatically get a TLS # certificate on first request. Domain: www.mox.example PathRegexp: ^/who/mjl/ WebStatic: StripPrefix: /who/mjl # Requested path /who/mjl/inferno/ resolves to local web/mjl/inferno. # If a directory contains an index.html, it is served when a directory is requested. Root: web/mjl # With ListFiles true, if a directory does not contain an index.html, the contents are listed. ListFiles: true ResponseHeaders: X-Mox: hi - LogName: redir Domain: www.mox.example PathRegexp: ^/redir/a/b/c # Don't redirect from plain HTTP to HTTPS. DontRedirectPlainHTTP: true WebRedirect: # Just change the domain and add query string set fragment. No change to scheme. # Path will start with /redir/a/b/c (and whathever came after) because no # OrigPathRegexp+ReplacePath is set. BaseURL: //moxest.example?q=1#frag # Default redirection is 308 - Permanent Redirect. StatusCode: 307 - LogName: oldnew Domain: www.mox.example PathRegexp: ^/old/ WebRedirect: # Replace path, leaving rest of URL intact. OrigPathRegexp: ^/old/(.*) ReplacePath: /new/$1 - LogName: app Domain: www.mox.example PathRegexp: ^/app/ WebForward: # Strip the path matched by PathRegexp before forwarding the request. So original # request /app/api become just /api. StripPath: true # URL of backend, where requests are forwarded to. The path in the URL is kept, # so for incoming request URL /app/api, the outgoing request URL has path /app-v2/api. # Requests are made with Go's net/http DefaultTransporter, including using # HTTP_PROXY and HTTPS_PROXY environment variables. URL: http://127.0.0.1:8900/app-v2/ # Add headers to response. ResponseHeaders: X-Frame-Options: deny X-Content-Type-Options: nosniff ` // Parse just so we know we have the syntax right. // todo: ideally we would have a complete config file and parse it fully. var conf struct { WebDomainRedirects map[string]string WebHandlers []config.WebHandler } err := sconf.Parse(strings.NewReader(webhandlers), &conf) xcheckf(err, "parsing webhandlers example") return webhandlers }, }, { "transport", func() string { const moxconf = `# Snippet for mox.conf, defining a transport called Example that connects on the # SMTP submission with TLS port 465 ("submissions"), authenticating with # SCRAM-SHA-256-PLUS (other providers may not support SCRAM-SHA-256-PLUS, but they # typically do support the older CRAM-MD5).: # Transport are mechanisms for delivering messages. Transports can be referenced # from Routes in accounts, domains and the global configuration. There is always # an implicit/fallback delivery transport doing direct delivery with SMTP from the # outgoing message queue. Transports are typically only configured when using # smarthosts, i.e. when delivering through another SMTP server. Zero or one # transport methods must be set in a transport, never multiple. When using an # external party to send email for a domain, keep in mind you may have to add # their IP address to your domain's SPF record, and possibly additional DKIM # records. (optional) Transports: Example: # Submission SMTP over a TLS connection to submit email to a remote queue. # (optional) Submissions: # Host name to connect to and for verifying its TLS certificate. Host: smtp.example.com # If set, authentication credentials for the remote server. (optional) Auth: Username: user@example.com Password: test1234 Mechanisms: # Allowed authentication mechanisms. Defaults to SCRAM-SHA-256-PLUS, # SCRAM-SHA-256, SCRAM-SHA-1-PLUS, SCRAM-SHA-1, CRAM-MD5. Not included by default: # PLAIN. Specify the strongest mechanism known to be implemented by the server to # prevent mechanism downgrade attacks. (optional) - SCRAM-SHA-256-PLUS ` const domainsconf = `# Snippet for domains.conf, specifying a route that sends through the transport: # Routes for delivering outgoing messages through the queue. Each delivery attempt # evaluates account routes, domain routes and finally these global routes. The # transport of the first matching route is used in the delivery attempt. If no # routes match, which is the default with no configured routes, messages are # delivered directly from the queue. (optional) Routes: - Transport: Example ` var static struct { Transports map[string]config.Transport } var dynamic struct { Routes []config.Route } err := sconf.Parse(strings.NewReader(moxconf), &static) xcheckf(err, "parsing moxconf example") err = sconf.Parse(strings.NewReader(domainsconf), &dynamic) xcheckf(err, "parsing domainsconf example") return moxconf + "\n\n" + domainsconf }, }, } var exampleTime = time.Date(2024, time.March, 27, 0, 0, 0, 0, time.UTC) var examples = []struct { Name string Get func() string }{ { "webhook-outgoing-delivered", func() string { v := webhook.Outgoing{ Version: 0, Event: webhook.EventDelivered, QueueMsgID: 101, FromID: base64.RawURLEncoding.EncodeToString([]byte("0123456789abcdef")), MessageID: "", Subject: "subject of original message", WebhookQueued: exampleTime, Extra: map[string]string{}, SMTPCode: smtp.C250Completed, } return "Example webhook HTTP POST JSON body for successful outgoing delivery:\n\n\t" + formatJSON(v) }, }, { "webhook-outgoing-dsn-failed", func() string { v := webhook.Outgoing{ Version: 0, Event: webhook.EventFailed, DSN: true, Suppressing: true, QueueMsgID: 102, FromID: base64.RawURLEncoding.EncodeToString([]byte("0123456789abcdef")), MessageID: "", Subject: "subject of original message", WebhookQueued: exampleTime, Extra: map[string]string{"userid": "456"}, Error: "timeout connecting to host", SMTPCode: smtp.C554TransactionFailed, SMTPEnhancedCode: "5." + smtp.SeNet4Other0, } return `Example webhook HTTP POST JSON body for failed delivery based on incoming DSN message, with custom extra data fields (from original submission), and adding address to the suppression list: ` + formatJSON(v) }, }, { "webhook-incoming-basic", func() string { v := webhook.Incoming{ Version: 0, From: []webhook.NameAddress{{Address: "mox@localhost"}}, To: []webhook.NameAddress{{Address: "mjl@localhost"}}, Subject: "hi", MessageID: "", Date: &exampleTime, Text: "hello world ☺\n", Structure: webhook.Structure{ ContentType: "text/plain", ContentTypeParams: map[string]string{"charset": "utf-8"}, DecodedSize: int64(len("hello world ☺\r\n")), Parts: []webhook.Structure{}, }, Meta: webhook.IncomingMeta{ MsgID: 201, MailFrom: "mox@localhost", MailFromValidated: false, MsgFromValidated: true, RcptTo: "mjl@localhost", DKIMVerifiedDomains: []string{"localhost"}, RemoteIP: "127.0.0.1", Received: exampleTime.Add(3 * time.Second), MailboxName: "Inbox", Automated: false, }, } return "Example JSON body for webhooks for incoming delivery of basic message:\n\n\t" + formatJSON(v) }, }, } func formatJSON(v any) string { nv, _ := mox.FillNil(reflect.ValueOf(v)) v = nv.Interface() var b bytes.Buffer enc := json.NewEncoder(&b) enc.SetIndent("\t", "\t") enc.SetEscapeHTML(false) err := enc.Encode(v) xcheckf(err, "encoding to json") return b.String() }