Merge branch 'master' into dev

This commit is contained in:
fox.cpp 2022-02-19 14:08:16 +03:00
commit 2677e190dc
No known key found for this signature in database
GPG key ID: 5B991F6215D2FCC0
14 changed files with 213 additions and 26 deletions

View file

@ -1,12 +1,15 @@
FROM golang:1.16.3-alpine3.13 AS build-env
FROM golang:1.17-alpine AS build-env
COPY . maddy/
WORKDIR maddy/
RUN set -ex ;\
apk upgrade --no-cache --available ;\
apk add --no-cache bash git build-base
WORKDIR /maddy
ADD go.mod go.sum ./
ENV LDFLAGS -static
RUN apk --no-cache add bash git gcc musl-dev
RUN mkdir /pkg/
RUN go mod download
ADD . ./
RUN mkdir -p /pkg/data
COPY maddy.conf /pkg/data/maddy.conf
# Monkey-patch config to use environment.
RUN sed -Ei 's!\$\(hostname\) = .+!$(hostname) = {env:MADDY_HOSTNAME}!' /pkg/data/maddy.conf
@ -15,13 +18,15 @@ RUN sed -Ei 's!^tls .+!tls file /data/tls_cert.pem /data/tls_key.pem!' /pkg/data
RUN ./build.sh --builddir /tmp --destdir /pkg/ --tags docker build install
FROM alpine:3.13.4
FROM alpine:3.15.0
LABEL maintainer="fox.cpp@disroot.org"
LABEL org.opencontainers.image.source=https://github.com/foxcpp/maddy
RUN apk --no-cache add ca-certificates
RUN set -ex ;\
apk upgrade --no-cache --available ;\
apk --no-cache add ca-certificates
COPY --from=build-env /pkg/data/maddy.conf /data/maddy.conf
COPY --from=build-env /pkg/usr/local/bin/maddy /bin/maddy
COPY --from=build-env /pkg/usr/local/bin/maddyctl /bin/maddyctl
COPY --from=build-env /pkg/usr/local/bin/maddy /pkg/usr/local/bin/maddyctl /bin/
EXPOSE 25 143 993 587 465
VOLUME ["/data"]

View file

@ -139,6 +139,7 @@ syn keyword maddyModDir
\ fail_open
\ file
\ flags
\ force_ipv4
\ fs_dir
\ fsstore
\ full_match

37
docker-build-multiarch.sh Executable file
View file

@ -0,0 +1,37 @@
#!/bin/bash
set -eEuo pipefail
AMD64_DOCKER_HOST=${AMD64_DOCKER_HOST:-"unix:///var/run/docker.sock"}
ARM_DOCKER_HOST=${ARM_DOCKER_HOST:-"tcp://raspberrypi.local:2375"}
if [ ! -x ${HOME}/.docker/cli-plugins/docker-buildx ]; then
mkdir -p ${HOME}/.docker/cli-plugins/
wget https://github.com/docker/buildx/releases/download/v0.7.0/buildx-v0.7.0.linux-amd64 -O ${HOME}/.docker/cli-plugins/docker-buildx
chmod +x ${HOME}/.docker/cli-plugins/docker-buildx
fi
docker buildx version
BUILDER="multiarch-builder"
CONFIG=${PWD}/multiarch/buildkitd.toml
docker buildx create --name ${BUILDER} --buildkitd-flags '--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host' --config=${CONFIG} --driver=docker-container --driver-opt image=moby/buildkit:latest,network=host --platform=linux/amd64 --use ${AMD64_DOCKER_HOST}
docker buildx create --name ${BUILDER} --buildkitd-flags '--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host' --config=${CONFIG} --driver=docker-container --driver-opt image=moby/buildkit:latest,network=host --platform=linux/arm64,linux/arm/v7,linux/arm/v6 --append ${ARM_DOCKER_HOST}
stopbuilders() {
set +x
echo stopping builders
docker buildx stop ${BUILDER}
docker buildx rm ${BUILDER}
}
trap stopbuilders INT TERM EXIT
docker buildx inspect --bootstrap --builder=${BUILDER}
PLATFORM="${PLATFORM:-"linux/amd64,linux/arm/v7,linux/arm64"}"
docker --log-level=debug \
buildx build ${PWD} \
--builder=${BUILDER} \
--allow security.insecure \
--platform=${PLATFORM} \
$@

View file

@ -34,6 +34,17 @@ per-source/per-destination are as observed when message exits the server.
Choose the local IP to bind for outbound SMTP connections.
**Syntax**: force\_ipv4 _boolean_ <br>
**Default**: false
Force resolving outbound SMTP domains to IPv4 addresses. Some server providers
do not offer a way to properly set reverse PTR domains for IPv6 addresses; this
option makes maddy only connect to IPv4 addresses so that its public IPv4 address
is used to connect to that server, and thus reverse PTR checks are made against
its IPv4 address.
Warning: this may break sending outgoing mail to IPv6-only SMTP servers.
**Syntax**: connect\_timeout _duration_ <br>
**Default**: 5m

2
go.mod
View file

@ -18,7 +18,7 @@ require (
github.com/emersion/go-milter v0.3.2
github.com/emersion/go-msgauth v0.6.5
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac
github.com/emersion/go-smtp v0.15.1-0.20211006082444-62f6b38f85e4
github.com/emersion/go-smtp v0.15.1-0.20220119142625-1c322d2783aa
github.com/foxcpp/go-dovecot-sasl v0.0.0-20200522223722-c4699d7a24bf
github.com/foxcpp/go-imap-backend-tests v0.0.0-20220105184719-e80aa29a5e16
github.com/foxcpp/go-imap-i18nlevel v0.0.0-20200208001533-d6ec88553005

2
go.sum
View file

@ -121,6 +121,8 @@ github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac h1:tn/OQ2PmwQ0XFV
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.15.1-0.20211006082444-62f6b38f85e4 h1:6unG0XYwWUlJjsbYDI06qcRH5Fe0o978bgL8zNydJ8k=
github.com/emersion/go-smtp v0.15.1-0.20211006082444-62f6b38f85e4/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/emersion/go-smtp v0.15.1-0.20220119142625-1c322d2783aa h1:PZiDDRpQS7p6nFZFt9Pbco8a5FYa5kMhu6V7fTsYE4k=
github.com/emersion/go-smtp v0.15.1-0.20220119142625-1c322d2783aa/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=

View file

@ -101,9 +101,6 @@ func (a *Auth) Init(cfg *config.Map) error {
if err != nil {
return fmt.Errorf("%s: invalid server endpoint: %v", modName, err)
}
if endp.Path == "" {
return fmt.Errorf("%s: unexpected path in endpoint ", modName)
}
// Dial once to check usability and also to get list of mechanisms.
conn, err := net.Dial(endp.Scheme, endp.Address())

View file

@ -182,6 +182,10 @@ func (s *Session) startDelivery(ctx context.Context, from string, opts smtp.Mail
Conn: &s.connState,
SMTPOpts: opts,
}
msgMeta.ID, err = module.GenerateMsgID()
if err != nil {
return "", err
}
if s.connState.AuthUser != "" {
s.log.Msg("incoming message",
@ -202,12 +206,14 @@ func (s *Session) startDelivery(ctx context.Context, from string, opts smtp.Mail
// INTERNATIONALIZATION: Do not permit non-ASCII addresses unless SMTPUTF8 is
// used.
for _, ch := range from {
if ch > 128 && !opts.UTF8 {
return "", &exterrors.SMTPError{
Code: 550,
EnhancedCode: exterrors.EnhancedCode{5, 6, 7},
Message: "SMTPUTF8 is required for non-ASCII senders",
if !opts.UTF8 {
for _, ch := range from {
if ch > 128 {
return "", &exterrors.SMTPError{
Code: 550,
EnhancedCode: exterrors.EnhancedCode{5, 6, 7},
Message: "SMTPUTF8 is required for non-ASCII senders",
}
}
}
}
@ -225,10 +231,6 @@ func (s *Session) startDelivery(ctx context.Context, from string, opts smtp.Mail
}
}
msgMeta.ID, err = module.GenerateMsgID()
if err != nil {
return "", err
}
msgMeta.OriginalFrom = from
domain := ""

View file

@ -31,6 +31,7 @@ import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"runtime/trace"
@ -81,6 +82,7 @@ type C struct {
serverName string
cl *smtp.Client
rcpts []string
lmtp bool
}
// New creates the new instance of the C object, populating the required fields
@ -217,6 +219,7 @@ func (c *C) attemptConnect(ctx context.Context, lmtp bool, endp config.Endpoint,
conn = tls.Client(conn, cfg)
}
c.lmtp = lmtp
// This uses initial greeting timeout of 5 minutes (hardcoded).
if lmtp {
cl, err = smtp.NewClientLMTP(conn, endp.Host)
@ -325,6 +328,10 @@ func (c *C) Client() *smtp.Client {
return c.cl
}
func (c *C) IsLMTP() bool {
return c.lmtp
}
// Rcpt sends the RCPT TO command to the remote server.
//
// If the address is non-ASCII and cannot be converted to ASCII and the remote
@ -358,6 +365,60 @@ func (c *C) Rcpt(ctx context.Context, to string) error {
return nil
}
type lmtpError map[string]*smtp.SMTPError
func (l lmtpError) SetStatus(rcptTo string, err *smtp.SMTPError) {
l[rcptTo] = err
}
func (l lmtpError) singleError() *smtp.SMTPError {
nonNils := 0
for _, e := range l {
if e != nil {
nonNils++
}
}
if nonNils == 1 {
for _, err := range l {
if err != nil {
return err
}
}
}
return nil
}
func (l lmtpError) Unwrap() error {
if err := l.singleError(); err != nil {
return err
}
return nil
}
func (l lmtpError) Error() string {
if err := l.singleError(); err != nil {
return err.Error()
}
return fmt.Sprintf("multiple errors reported by LMTP downstream: %v", map[string]*smtp.SMTPError(l))
}
func (c *C) smtpToLMTPData(ctx context.Context, hdr textproto.Header, body io.Reader) error {
statusCb := lmtpError{}
if err := c.LMTPData(ctx, hdr, body, statusCb.SetStatus); err != nil {
return err
}
hasAnyFailures := false
for _, err := range statusCb {
if err != nil {
hasAnyFailures = true
}
}
if hasAnyFailures {
return statusCb
}
return nil
}
// Data sends the DATA command to the remote server and then sends the message header
// and body.
//
@ -366,6 +427,10 @@ func (c *C) Rcpt(ctx context.Context, to string) error {
func (c *C) Data(ctx context.Context, hdr textproto.Header, body io.Reader) error {
defer trace.StartRegion(ctx, "smtpconn/DATA").End()
if c.IsLMTP() {
return c.smtpToLMTPData(ctx, hdr, body)
}
wc, err := c.cl.Data()
if err != nil {
return c.wrapClientErr(err, c.serverName)

View file

@ -62,6 +62,7 @@ type Target struct {
name string
hostname string
localIP string
ipv4 bool
tlsConfig *tls.Config
resolver dns.Resolver
@ -107,6 +108,7 @@ func (rt *Target) Init(cfg *config.Map) error {
cfg.String("hostname", true, true, "", &rt.hostname)
cfg.String("local_ip", false, false, "", &rt.localIP)
cfg.Bool("force_ipv4", false, false, &rt.ipv4)
cfg.Bool("debug", true, false, &rt.Log.Debug)
cfg.Custom("tls_client", true, false, func() (interface{}, error) {
return &tls.Config{}, nil
@ -168,6 +170,15 @@ func (rt *Target) Init(cfg *config.Map) error {
LocalAddr: addr,
}).DialContext
}
if rt.ipv4 {
dial := rt.dialer
rt.dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
if network == "tcp" {
network = "tcp4"
}
return dial(ctx, network, addr)
}
}
return nil
}

View file

@ -115,6 +115,40 @@ func TestDownstreamDelivery_LMTP(t *testing.T) {
}
}
func TestDownstreamDelivery_LMTP_ErrorCoerce(t *testing.T) {
be, srv := testutils.SMTPServer(t, "127.0.0.1:"+testPort, func(srv *smtp.Server) {
srv.LMTP = true
})
be.LMTPDataErr = []error{
nil,
&smtp.SMTPError{
Code: 501,
Message: "nop",
},
}
defer srv.Close()
defer testutils.CheckSMTPConnLeak(t, srv)
mod := &Downstream{
hostname: "mx.example.invalid",
endpoints: []config.Endpoint{
{
Scheme: "tcp",
Host: "127.0.0.1",
Port: testPort,
},
},
modName: "target.lmtp",
lmtp: true,
log: testutils.Logger(t, "lmtp_downstream"),
}
_, err := testutils.DoTestDeliveryErr(t, mod, "test@example.invalid", []string{"rcpt1@example.invalid", "rcpt2@example.invalid"})
if err == nil {
t.Error("expected failure")
}
}
type statusCollector map[string]error
func (sc *statusCollector) SetStatus(rcptTo string, err error) {

View file

@ -63,8 +63,8 @@ func (be *SMTPBackend) NewSession(state smtp.ConnectionState, _ string) (smtp.Se
}
be.SourceEndpoints[state.RemoteAddr.String()] = struct{}{}
return &session{
backend: be,
state: &state,
backend: be,
state: &state,
}, nil
}

15
multiarch/README.md Normal file
View file

@ -0,0 +1,15 @@
# Mutliarch builds
## Requirements
An ARM64 server with docker daemon exposed (for example, a raspberry pi 4 with Raspberry Pi OS 64bits)
## Build
At repository root, launch :
```
./docker-build-multiarch.sh --tag=TAG --push
```
It will build and push multi-arch docker images as TAG.

7
multiarch/buildkitd.toml Normal file
View file

@ -0,0 +1,7 @@
###################
## https://github.com/moby/buildkit/blob/master/docs/buildkitd.toml.md
debug = true
# insecure-entitlements allows insecure entitlements, disabled by default.
insecure-entitlements = [ "network.host", "security.insecure" ]