mirror of
https://blitiri.com.ar/repos/chasquid
synced 2026-10-29 15:02:30 +00:00
d74e6e3d2b
Delivery errors can be visible to the end-user via DSN responses, and some of them are not very clear, or are missing useful debugging information (such as the MX). This patch improves the messages by including the MX where appropriate, having explicit errors for cases where no servers are found/allowed, and using less technical language for the common explanations. There are no changes to the behaviour on errors, this change only affects the messages.
124 lines
3.6 KiB
Go
124 lines
3.6 KiB
Go
package courier
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os/exec"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
"unicode"
|
|
|
|
"blitiri.com.ar/go/chasquid/internal/envelope"
|
|
"blitiri.com.ar/go/chasquid/internal/normalize"
|
|
"blitiri.com.ar/go/chasquid/internal/trace"
|
|
)
|
|
|
|
var (
|
|
errTimeout = fmt.Errorf("operation timed out")
|
|
)
|
|
|
|
// MDA delivers local mail by executing a local binary, like procmail or
|
|
// maildrop. It works with any binary that:
|
|
// - Receives the email to deliver via stdin.
|
|
// - Exits with code EX_TEMPFAIL (75) for transient issues.
|
|
type MDA struct {
|
|
Binary string // Path to the binary.
|
|
Args []string // Arguments to pass.
|
|
Timeout time.Duration // Timeout for each invocation.
|
|
}
|
|
|
|
// Deliver an email. On failures, returns an error, and whether or not it is
|
|
// permanent.
|
|
func (p *MDA) Deliver(from string, to string, data []byte) (error, bool) {
|
|
tr := trace.New("Courier.MDA", to)
|
|
defer tr.Finish()
|
|
|
|
// Sanitize, just in case.
|
|
from = sanitizeForMDA(from)
|
|
to = sanitizeForMDA(to)
|
|
|
|
tr.Debugf("%s -> %s", from, to)
|
|
|
|
// Prepare the command, replacing the necessary arguments.
|
|
replacer := strings.NewReplacer(
|
|
"%from%", from,
|
|
"%from_user%", envelope.UserOf(from),
|
|
"%from_domain%", envelope.DomainOf(from),
|
|
|
|
"%to%", to,
|
|
"%to_user%", envelope.UserOf(to),
|
|
"%to_domain%", envelope.DomainOf(to),
|
|
)
|
|
|
|
args := []string{}
|
|
for _, a := range p.Args {
|
|
args = append(args, replacer.Replace(a))
|
|
}
|
|
tr.Debugf("%s %q", p.Binary, args)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), p.Timeout)
|
|
defer cancel()
|
|
cmd := exec.CommandContext(ctx, p.Binary, args...)
|
|
|
|
// Pass the email data via stdin. Normalize it to CRLF which is what the
|
|
// RFC-compliant representation require. By doing this at this end, we can
|
|
// keep a simpler internal representation and ensure there won't be any
|
|
// inconsistencies in newlines within the message (e.g. added headers).
|
|
cmd.Stdin = bytes.NewReader(normalize.ToCRLF(data))
|
|
|
|
output, err := cmd.CombinedOutput()
|
|
if ctx.Err() == context.DeadlineExceeded {
|
|
return tr.Errorf("local delivery failed: %w after %v",
|
|
errTimeout, p.Timeout), false
|
|
}
|
|
|
|
if err != nil {
|
|
// Determine if the error is permanent or not.
|
|
// Default to permanent, but error code 75 is transient by general
|
|
// convention (/usr/include/sysexits.h), and commonly relied upon.
|
|
permanent := true
|
|
if exiterr, ok := err.(*exec.ExitError); ok {
|
|
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
|
|
permanent = status.ExitStatus() != 75
|
|
}
|
|
}
|
|
// Include the output, as it usually contains the reason for the
|
|
// failure. Quote it, as it can contain arbitrary characters.
|
|
err = tr.Errorf("local delivery failed: %w (output: %q)",
|
|
err, strings.TrimSpace(string(output)))
|
|
return err, permanent
|
|
}
|
|
|
|
tr.Debugf("delivered")
|
|
return nil, false
|
|
}
|
|
|
|
// sanitizeForMDA cleans the string, removing characters that could be
|
|
// problematic considering we will run an external command.
|
|
//
|
|
// The server does not rely on this to do substitution or proper filtering,
|
|
// that's done at a different layer; this is just for defense in depth.
|
|
func sanitizeForMDA(s string) string {
|
|
valid := func(r rune) rune {
|
|
switch {
|
|
case unicode.IsSpace(r), unicode.IsControl(r),
|
|
strings.ContainsRune("/;\"'\\|*&$%()[]{}`!", r):
|
|
return rune(-1)
|
|
default:
|
|
return r
|
|
}
|
|
}
|
|
return strings.Map(valid, s)
|
|
}
|
|
|
|
var errForwardNotSupported = errors.New(
|
|
"forwarding not supported by the MDA courier")
|
|
|
|
// Forward is not supported by the MDA courier.
|
|
func (p *MDA) Forward(from string, to string, data []byte, servers []string) (error, bool) {
|
|
return errForwardNotSupported, true
|
|
}
|