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/ RUN set -ex ;\
WORKDIR maddy/ apk upgrade --no-cache --available ;\
apk add --no-cache bash git build-base
WORKDIR /maddy
ADD go.mod go.sum ./
ENV LDFLAGS -static ENV LDFLAGS -static
RUN apk --no-cache add bash git gcc musl-dev RUN go mod download
ADD . ./
RUN mkdir /pkg/ RUN mkdir -p /pkg/data
COPY maddy.conf /pkg/data/maddy.conf COPY maddy.conf /pkg/data/maddy.conf
# Monkey-patch config to use environment. # Monkey-patch config to use environment.
RUN sed -Ei 's!\$\(hostname\) = .+!$(hostname) = {env:MADDY_HOSTNAME}!' /pkg/data/maddy.conf 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 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 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/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/maddy /pkg/usr/local/bin/maddyctl /bin/
COPY --from=build-env /pkg/usr/local/bin/maddyctl /bin/maddyctl
EXPOSE 25 143 993 587 465 EXPOSE 25 143 993 587 465
VOLUME ["/data"] VOLUME ["/data"]

View file

@ -139,6 +139,7 @@ syn keyword maddyModDir
\ fail_open \ fail_open
\ file \ file
\ flags \ flags
\ force_ipv4
\ fs_dir \ fs_dir
\ fsstore \ fsstore
\ full_match \ 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. 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> **Syntax**: connect\_timeout _duration_ <br>
**Default**: 5m **Default**: 5m

2
go.mod
View file

@ -18,7 +18,7 @@ require (
github.com/emersion/go-milter v0.3.2 github.com/emersion/go-milter v0.3.2
github.com/emersion/go-msgauth v0.6.5 github.com/emersion/go-msgauth v0.6.5
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac 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-dovecot-sasl v0.0.0-20200522223722-c4699d7a24bf
github.com/foxcpp/go-imap-backend-tests v0.0.0-20220105184719-e80aa29a5e16 github.com/foxcpp/go-imap-backend-tests v0.0.0-20220105184719-e80aa29a5e16
github.com/foxcpp/go-imap-i18nlevel v0.0.0-20200208001533-d6ec88553005 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-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 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.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-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 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= 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 { if err != nil {
return fmt.Errorf("%s: invalid server endpoint: %v", modName, err) 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. // Dial once to check usability and also to get list of mechanisms.
conn, err := net.Dial(endp.Scheme, endp.Address()) 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, Conn: &s.connState,
SMTPOpts: opts, SMTPOpts: opts,
} }
msgMeta.ID, err = module.GenerateMsgID()
if err != nil {
return "", err
}
if s.connState.AuthUser != "" { if s.connState.AuthUser != "" {
s.log.Msg("incoming message", 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 // INTERNATIONALIZATION: Do not permit non-ASCII addresses unless SMTPUTF8 is
// used. // used.
for _, ch := range from { if !opts.UTF8 {
if ch > 128 && !opts.UTF8 { for _, ch := range from {
return "", &exterrors.SMTPError{ if ch > 128 {
Code: 550, return "", &exterrors.SMTPError{
EnhancedCode: exterrors.EnhancedCode{5, 6, 7}, Code: 550,
Message: "SMTPUTF8 is required for non-ASCII senders", 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 msgMeta.OriginalFrom = from
domain := "" domain := ""

View file

@ -31,6 +31,7 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"errors" "errors"
"fmt"
"io" "io"
"net" "net"
"runtime/trace" "runtime/trace"
@ -81,6 +82,7 @@ type C struct {
serverName string serverName string
cl *smtp.Client cl *smtp.Client
rcpts []string rcpts []string
lmtp bool
} }
// New creates the new instance of the C object, populating the required fields // 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) conn = tls.Client(conn, cfg)
} }
c.lmtp = lmtp
// This uses initial greeting timeout of 5 minutes (hardcoded). // This uses initial greeting timeout of 5 minutes (hardcoded).
if lmtp { if lmtp {
cl, err = smtp.NewClientLMTP(conn, endp.Host) cl, err = smtp.NewClientLMTP(conn, endp.Host)
@ -325,6 +328,10 @@ func (c *C) Client() *smtp.Client {
return c.cl return c.cl
} }
func (c *C) IsLMTP() bool {
return c.lmtp
}
// Rcpt sends the RCPT TO command to the remote server. // 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 // 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 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 // Data sends the DATA command to the remote server and then sends the message header
// and body. // 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 { func (c *C) Data(ctx context.Context, hdr textproto.Header, body io.Reader) error {
defer trace.StartRegion(ctx, "smtpconn/DATA").End() defer trace.StartRegion(ctx, "smtpconn/DATA").End()
if c.IsLMTP() {
return c.smtpToLMTPData(ctx, hdr, body)
}
wc, err := c.cl.Data() wc, err := c.cl.Data()
if err != nil { if err != nil {
return c.wrapClientErr(err, c.serverName) return c.wrapClientErr(err, c.serverName)

View file

@ -62,6 +62,7 @@ type Target struct {
name string name string
hostname string hostname string
localIP string localIP string
ipv4 bool
tlsConfig *tls.Config tlsConfig *tls.Config
resolver dns.Resolver resolver dns.Resolver
@ -107,6 +108,7 @@ func (rt *Target) Init(cfg *config.Map) error {
cfg.String("hostname", true, true, "", &rt.hostname) cfg.String("hostname", true, true, "", &rt.hostname)
cfg.String("local_ip", false, false, "", &rt.localIP) 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.Bool("debug", true, false, &rt.Log.Debug)
cfg.Custom("tls_client", true, false, func() (interface{}, error) { cfg.Custom("tls_client", true, false, func() (interface{}, error) {
return &tls.Config{}, nil return &tls.Config{}, nil
@ -168,6 +170,15 @@ func (rt *Target) Init(cfg *config.Map) error {
LocalAddr: addr, LocalAddr: addr,
}).DialContext }).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 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 type statusCollector map[string]error
func (sc *statusCollector) SetStatus(rcptTo string, err 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{}{} be.SourceEndpoints[state.RemoteAddr.String()] = struct{}{}
return &session{ return &session{
backend: be, backend: be,
state: &state, state: &state,
}, nil }, 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" ]