maddy

Fork https://github.com/foxcpp/maddy

git clone git://git.lin.moe/go/maddy.git

  1/*
  2Maddy Mail Server - Composable all-in-one email server.
  3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
  4
  5This program is free software: you can redistribute it and/or modify
  6it under the terms of the GNU General Public License as published by
  7the Free Software Foundation, either version 3 of the License, or
  8(at your option) any later version.
  9
 10This program is distributed in the hope that it will be useful,
 11but WITHOUT ANY WARRANTY; without even the implied warranty of
 12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13GNU General Public License for more details.
 14
 15You should have received a copy of the GNU General Public License
 16along with this program.  If not, see <https://www.gnu.org/licenses/>.
 17*/
 18
 19// Package smtp_downstream provides target.smtp module that implements
 20// transparent forwarding or messages to configured list of SMTP servers.
 21//
 22// Like remote module, this implementation doesn't handle atomic
 23// delivery properly since it is impossible to do with SMTP protocol
 24//
 25// Interfaces implemented:
 26// - module.DeliveryTarget
 27package smtp_downstream
 28
 29import (
 30	"context"
 31	"crypto/tls"
 32	"fmt"
 33	"net"
 34	"runtime/trace"
 35	"time"
 36
 37	"github.com/emersion/go-message/textproto"
 38	"github.com/emersion/go-smtp"
 39	"github.com/foxcpp/maddy/framework/buffer"
 40	"github.com/foxcpp/maddy/framework/config"
 41	tls2 "github.com/foxcpp/maddy/framework/config/tls"
 42	"github.com/foxcpp/maddy/framework/exterrors"
 43	"github.com/foxcpp/maddy/framework/log"
 44	"github.com/foxcpp/maddy/framework/module"
 45	"github.com/foxcpp/maddy/internal/smtpconn"
 46	"github.com/foxcpp/maddy/internal/target"
 47	"golang.org/x/net/idna"
 48)
 49
 50type Downstream struct {
 51	modName    string
 52	instName   string
 53	lmtp       bool
 54	targetsArg []string
 55
 56	starttls    bool
 57	hostname    string
 58	endpoints   []config.Endpoint
 59	saslFactory saslClientFactory
 60	tlsConfig   *tls.Config
 61
 62	connectTimeout    time.Duration
 63	commandTimeout    time.Duration
 64	submissionTimeout time.Duration
 65
 66	log log.Logger
 67}
 68
 69func (u *Downstream) moduleError(err error) error {
 70	if err == nil {
 71		return nil
 72	}
 73
 74	return exterrors.WithFields(err, map[string]interface{}{
 75		"target": u.modName,
 76	})
 77}
 78
 79func NewDownstream(modName, instName string, _, inlineArgs []string) (module.Module, error) {
 80	return &Downstream{
 81		modName:    modName,
 82		instName:   instName,
 83		lmtp:       modName == "target.lmtp" || modName == "lmtp_downstream", /* compatibility with 0.3 configs */
 84		targetsArg: inlineArgs,
 85		log:        log.Logger{Name: modName},
 86	}, nil
 87}
 88
 89func (u *Downstream) Init(cfg *config.Map) error {
 90	var attemptTLS *bool
 91
 92	var targetsArg []string
 93	cfg.Bool("debug", true, false, &u.log.Debug)
 94	cfg.Callback("require_tls", func(m *config.Map, node config.Node) error {
 95		u.log.Msg("require_tls directive is deprecated and ignored")
 96		return nil
 97	})
 98	cfg.Callback("attempt_starttls", func(m *config.Map, node config.Node) error {
 99		u.log.Msg("attempt_starttls directive is deprecated and equivalent to starttls")
100
101		if len(node.Args) == 0 {
102			trueVal := true
103			attemptTLS = &trueVal
104			return nil
105		}
106		if len(node.Args) != 1 {
107			return config.NodeErr(node, "expected exactly 1 argument")
108		}
109
110		b, err := config.ParseBool(node.Args[0])
111		if err != nil {
112			return err
113		}
114		attemptTLS = &b
115		return nil
116	})
117	cfg.Bool("starttls", false, !u.lmtp, &u.starttls)
118	cfg.String("hostname", true, true, "", &u.hostname)
119	cfg.StringList("targets", false, false, nil, &targetsArg)
120	cfg.Custom("auth", false, false, func() (interface{}, error) {
121		return nil, nil
122	}, saslAuthDirective, &u.saslFactory)
123	cfg.Custom("tls_client", true, false, func() (interface{}, error) {
124		return &tls.Config{}, nil
125	}, tls2.TLSClientBlock, &u.tlsConfig)
126	cfg.Duration("connect_timeout", false, false, 5*time.Minute, &u.connectTimeout)
127	cfg.Duration("command_timeout", false, false, 5*time.Minute, &u.commandTimeout)
128	cfg.Duration("submission_timeout", false, false, 5*time.Minute, &u.submissionTimeout)
129
130	if _, err := cfg.Process(); err != nil {
131		return err
132	}
133
134	if attemptTLS != nil {
135		u.starttls = *attemptTLS
136	}
137
138	// INTERNATIONALIZATION: See RFC 6531 Section 3.7.1.
139	var err error
140	u.hostname, err = idna.ToASCII(u.hostname)
141	if err != nil {
142		return fmt.Errorf("%s: cannot represent the hostname as an A-label name: %w", u.modName, err)
143	}
144
145	u.targetsArg = append(u.targetsArg, targetsArg...)
146	for _, tgt := range u.targetsArg {
147		endp, err := config.ParseEndpoint(tgt)
148		if err != nil {
149			return err
150		}
151
152		u.endpoints = append(u.endpoints, endp)
153	}
154
155	if len(u.endpoints) == 0 {
156		return fmt.Errorf("%s: at least one target endpoint is required", u.modName)
157	}
158
159	return nil
160}
161
162func (u *Downstream) Name() string {
163	return u.modName
164}
165
166func (u *Downstream) InstanceName() string {
167	return u.instName
168}
169
170type delivery struct {
171	u   *Downstream
172	log log.Logger
173
174	msgMeta  *module.MsgMetadata
175	mailFrom string
176	rcpts    []string
177
178	conn *smtpconn.C
179}
180
181// lmtpDelivery implements module.PartialDelivery
182type lmtpDelivery struct {
183	*delivery
184}
185
186func (u *Downstream) Start(ctx context.Context, msgMeta *module.MsgMetadata, mailFrom string) (module.Delivery, error) {
187	defer trace.StartRegion(ctx, "target.smtp/Start").End()
188
189	d := &delivery{
190		u:        u,
191		log:      target.DeliveryLogger(u.log, msgMeta),
192		msgMeta:  msgMeta,
193		mailFrom: mailFrom,
194	}
195	if err := d.connect(ctx); err != nil {
196		return nil, err
197	}
198
199	if err := d.conn.Mail(ctx, mailFrom, msgMeta.SMTPOpts); err != nil {
200		d.conn.Close()
201		return nil, err
202	}
203
204	if u.lmtp {
205		return &lmtpDelivery{delivery: d}, nil
206	}
207
208	return d, nil
209}
210
211func (d *delivery) connect(ctx context.Context) error {
212	// TODO: Review possibility of connection pooling here.
213	var lastErr error
214
215	conn := smtpconn.New()
216	conn.Log = d.log
217	conn.Hostname = d.u.hostname
218	conn.AddrInSMTPMsg = false
219	if d.u.connectTimeout != 0 {
220		conn.ConnectTimeout = d.u.connectTimeout
221	}
222	if d.u.commandTimeout != 0 {
223		conn.CommandTimeout = d.u.commandTimeout
224	}
225	if d.u.submissionTimeout != 0 {
226		conn.SubmissionTimeout = d.u.submissionTimeout
227	}
228
229	for _, endp := range d.u.endpoints {
230		var err error
231		if d.u.lmtp {
232			_, err = conn.ConnectLMTP(ctx, endp, d.u.starttls, d.u.tlsConfig)
233		} else {
234			_, err = conn.Connect(ctx, endp, d.u.starttls, d.u.tlsConfig)
235		}
236		if err != nil {
237			if len(d.u.endpoints) != 1 {
238				d.log.Msg("connect error", err, "downstream_server", net.JoinHostPort(endp.Host, endp.Port))
239			}
240			lastErr = err
241			continue
242		}
243
244		d.log.DebugMsg("connected", "downstream_server", conn.ServerName())
245
246		lastErr = nil
247		break
248	}
249	if lastErr != nil {
250		return d.u.moduleError(lastErr)
251	}
252
253	if d.u.saslFactory != nil {
254		saslClient, err := d.u.saslFactory(d.msgMeta)
255		if err != nil {
256			conn.Close()
257			return err
258		}
259
260		if err := conn.Client().Auth(saslClient); err != nil {
261			conn.Close()
262			return err
263		}
264	}
265
266	d.conn = conn
267
268	return nil
269}
270
271func (d *delivery) AddRcpt(ctx context.Context, rcptTo string, opts smtp.RcptOptions) error {
272	err := d.conn.Rcpt(ctx, rcptTo, opts)
273	if err != nil {
274		return d.u.moduleError(err)
275	}
276
277	d.rcpts = append(d.rcpts, rcptTo)
278	return nil
279}
280
281func (d *delivery) Body(ctx context.Context, header textproto.Header, body buffer.Buffer) error {
282	r, err := body.Open()
283	if err != nil {
284		return exterrors.WithFields(err, map[string]interface{}{"target": d.u.modName})
285	}
286
287	defer r.Close()
288	return d.u.moduleError(d.conn.Data(ctx, header, r))
289}
290
291func (d *lmtpDelivery) BodyNonAtomic(ctx context.Context, sc module.StatusCollector, header textproto.Header, body buffer.Buffer) {
292	r, err := body.Open()
293	if err != nil {
294		modErr := d.u.moduleError(err)
295		for _, rcpt := range d.rcpts {
296			sc.SetStatus(rcpt, modErr)
297		}
298	}
299	defer r.Close()
300
301	rcptIndx := 0
302	err = d.conn.LMTPData(ctx, header, r, func(rcpt string, err *smtp.SMTPError) {
303		if err == nil {
304			sc.SetStatus(rcpt, nil)
305		} else {
306			sc.SetStatus(rcpt, &exterrors.SMTPError{
307				Code:         err.Code,
308				EnhancedCode: exterrors.EnhancedCode(err.EnhancedCode),
309				Message:      err.Message,
310				TargetName:   d.u.modName,
311				Err:          err,
312			})
313		}
314		rcptIndx++
315	})
316	if err != nil {
317		modErr := d.u.moduleError(err)
318		for _, rcpt := range d.rcpts[rcptIndx:] {
319			sc.SetStatus(rcpt, modErr)
320		}
321	}
322}
323
324func (d *delivery) Abort(ctx context.Context) error {
325	d.conn.Close()
326	return nil
327}
328
329func (d *delivery) Commit(ctx context.Context) error {
330	return d.conn.Close()
331}
332
333func init() {
334	module.Register("target.smtp", NewDownstream)
335	module.Register("target.lmtp", NewDownstream)
336}