1/*2Maddy Mail Server - Composable all-in-one email server.3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors45This program is free software: you can redistribute it and/or modify6it under the terms of the GNU General Public License as published by7the Free Software Foundation, either version 3 of the License, or8(at your option) any later version.910This program is distributed in the hope that it will be useful,11but WITHOUT ANY WARRANTY; without even the implied warranty of12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the13GNU General Public License for more details.1415You should have received a copy of the GNU General Public License16along with this program. If not, see <https://www.gnu.org/licenses/>.17*/1819// Package smtp_downstream provides target.smtp module that implements20// transparent forwarding or messages to configured list of SMTP servers.21//22// Like remote module, this implementation doesn't handle atomic23// delivery properly since it is impossible to do with SMTP protocol24//25// Interfaces implemented:26// - module.DeliveryTarget27package smtp_downstream2829import (30 "context"31 "crypto/tls"32 "fmt"33 "net"34 "runtime/trace"35 "time"3637 "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)4950type Downstream struct {51 modName string52 instName string53 lmtp bool54 targetsArg []string5556 starttls bool57 hostname string58 endpoints []config.Endpoint59 saslFactory saslClientFactory60 tlsConfig *tls.Config6162 connectTimeout time.Duration63 commandTimeout time.Duration64 submissionTimeout time.Duration6566 log log.Logger67}6869func (u *Downstream) moduleError(err error) error {70 if err == nil {71 return nil72 }7374 return exterrors.WithFields(err, map[string]interface{}{75 "target": u.modName,76 })77}7879func 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 }, nil87}8889func (u *Downstream) Init(cfg *config.Map) error {90 var attemptTLS *bool9192 var targetsArg []string93 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 nil97 })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")100101 if len(node.Args) == 0 {102 trueVal := true103 attemptTLS = &trueVal104 return nil105 }106 if len(node.Args) != 1 {107 return config.NodeErr(node, "expected exactly 1 argument")108 }109110 b, err := config.ParseBool(node.Args[0])111 if err != nil {112 return err113 }114 attemptTLS = &b115 return nil116 })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, nil122 }, saslAuthDirective, &u.saslFactory)123 cfg.Custom("tls_client", true, false, func() (interface{}, error) {124 return &tls.Config{}, nil125 }, 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)129130 if _, err := cfg.Process(); err != nil {131 return err132 }133134 if attemptTLS != nil {135 u.starttls = *attemptTLS136 }137138 // INTERNATIONALIZATION: See RFC 6531 Section 3.7.1.139 var err error140 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 }144145 u.targetsArg = append(u.targetsArg, targetsArg...)146 for _, tgt := range u.targetsArg {147 endp, err := config.ParseEndpoint(tgt)148 if err != nil {149 return err150 }151152 u.endpoints = append(u.endpoints, endp)153 }154155 if len(u.endpoints) == 0 {156 return fmt.Errorf("%s: at least one target endpoint is required", u.modName)157 }158159 return nil160}161162func (u *Downstream) Name() string {163 return u.modName164}165166func (u *Downstream) InstanceName() string {167 return u.instName168}169170type delivery struct {171 u *Downstream172 log log.Logger173174 msgMeta *module.MsgMetadata175 mailFrom string176 rcpts []string177178 conn *smtpconn.C179}180181// lmtpDelivery implements module.PartialDelivery182type lmtpDelivery struct {183 *delivery184}185186func (u *Downstream) Start(ctx context.Context, msgMeta *module.MsgMetadata, mailFrom string) (module.Delivery, error) {187 defer trace.StartRegion(ctx, "target.smtp/Start").End()188189 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, err197 }198199 if err := d.conn.Mail(ctx, mailFrom, msgMeta.SMTPOpts); err != nil {200 d.conn.Close()201 return nil, err202 }203204 if u.lmtp {205 return &lmtpDelivery{delivery: d}, nil206 }207208 return d, nil209}210211func (d *delivery) connect(ctx context.Context) error {212 // TODO: Review possibility of connection pooling here.213 var lastErr error214215 conn := smtpconn.New()216 conn.Log = d.log217 conn.Hostname = d.u.hostname218 conn.AddrInSMTPMsg = false219 if d.u.connectTimeout != 0 {220 conn.ConnectTimeout = d.u.connectTimeout221 }222 if d.u.commandTimeout != 0 {223 conn.CommandTimeout = d.u.commandTimeout224 }225 if d.u.submissionTimeout != 0 {226 conn.SubmissionTimeout = d.u.submissionTimeout227 }228229 for _, endp := range d.u.endpoints {230 var err error231 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 = err241 continue242 }243244 d.log.DebugMsg("connected", "downstream_server", conn.ServerName())245246 lastErr = nil247 break248 }249 if lastErr != nil {250 return d.u.moduleError(lastErr)251 }252253 if d.u.saslFactory != nil {254 saslClient, err := d.u.saslFactory(d.msgMeta)255 if err != nil {256 conn.Close()257 return err258 }259260 if err := conn.Client().Auth(saslClient); err != nil {261 conn.Close()262 return err263 }264 }265266 d.conn = conn267268 return nil269}270271func (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 }276277 d.rcpts = append(d.rcpts, rcptTo)278 return nil279}280281func (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 }286287 defer r.Close()288 return d.u.moduleError(d.conn.Data(ctx, header, r))289}290291func (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()300301 rcptIndx := 0302 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}323324func (d *delivery) Abort(ctx context.Context) error {325 d.conn.Close()326 return nil327}328329func (d *delivery) Commit(ctx context.Context) error {330 return d.conn.Close()331}332333func init() {334 module.Register("target.smtp", NewDownstream)335 module.Register("target.lmtp", NewDownstream)336}