mirror of
https://github.com/hanzoai/mail.git
synced 2026-09-23 23:53:02 +00:00
Fork of github.com/mjl-/mox at 5656ca6d1b
(upstream v0.0.15 plus 55 commits).
Rename the module path in go.mod and the import paths in 268 Go files. Also
update the five scripts that "go install" the module (genwebsite.sh,
docker-release.sh, apidiff.sh, test-upgrade.sh, Dockerfile.release), which
would otherwise build upstream rather than this fork.
Add NOTICE recording the upstream project, author, license, and fork point,
and stating which license covers which files: MIT for the mox source and our
changes, MPL-2.0 for publicsuffix/public_suffix_list.txt alone, and the
vendored dependencies' own licenses under licenses/. Embed NOTICE so
"mail licenses" prints the lineage from the running binary. LICENSE.MIT,
LICENSE.MPLv2.0 and the licenses/ tree are preserved verbatim; no code is
relicensed.
The mox command name, config keys, data directory layout and website content
are unchanged. LLM.md records the fork point, the rename, what was left alone,
and the steps to stand the server up.
Co-authored-by: Hanzo Dev <[email protected]>
326 lines
10 KiB
Go
326 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mjl-/sconf"
|
|
|
|
"github.com/hanzoai/mail/config"
|
|
"github.com/hanzoai/mail/mox-"
|
|
"github.com/hanzoai/mail/smtp"
|
|
"github.com/hanzoai/mail/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: [email protected]
|
|
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: "<[email protected]>",
|
|
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: "<[email protected]>",
|
|
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: "<[email protected]>",
|
|
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()
|
|
}
|