mirror of
https://blitiri.com.ar/repos/chasquid
synced 2025-12-17 14:37:02 +00:00
This patch makes the queue and couriers distinguish between permanent and transient errors when delivering mail to individual recipients. Pipe delivery errors are always permanent. Procmail delivery errors are almost always permanent, except if the command exited with code 75, which is an indication of transient. SMTP delivery errors are almost always transient, except if the DNS resolution for the domain failed.
119 lines
2.8 KiB
Go
119 lines
2.8 KiB
Go
package courier
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os/exec"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
"unicode"
|
|
|
|
"blitiri.com.ar/go/chasquid/internal/envelope"
|
|
"blitiri.com.ar/go/chasquid/internal/trace"
|
|
)
|
|
|
|
var (
|
|
errTimeout = fmt.Errorf("Operation timed out")
|
|
)
|
|
|
|
// Procmail delivers local mail via procmail.
|
|
type Procmail struct {
|
|
Binary string // Path to the binary.
|
|
Args []string // Arguments to pass.
|
|
Timeout time.Duration // Timeout for each invocation.
|
|
}
|
|
|
|
func (p *Procmail) Deliver(from string, to string, data []byte) (error, bool) {
|
|
tr := trace.New("Procmail", "Deliver")
|
|
defer tr.Finish()
|
|
|
|
// Sanitize, just in case.
|
|
from = sanitizeForProcmail(from)
|
|
to = sanitizeForProcmail(to)
|
|
|
|
tr.LazyPrintf("%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))
|
|
}
|
|
|
|
ctx, _ := context.WithDeadline(context.Background(),
|
|
time.Now().Add(p.Timeout))
|
|
cmd := exec.CommandContext(ctx, p.Binary, args...)
|
|
|
|
cmdStdin, err := cmd.StdinPipe()
|
|
if err != nil {
|
|
return tr.Errorf("StdinPipe: %v", err), true
|
|
}
|
|
|
|
output := &bytes.Buffer{}
|
|
cmd.Stdout = output
|
|
cmd.Stderr = output
|
|
|
|
err = cmd.Start()
|
|
if err != nil {
|
|
return tr.Errorf("Error starting procmail: %v", err), true
|
|
}
|
|
|
|
_, err = bytes.NewBuffer(data).WriteTo(cmdStdin)
|
|
if err != nil {
|
|
return tr.Errorf("Error sending data to procmail: %v", err), true
|
|
}
|
|
|
|
cmdStdin.Close()
|
|
|
|
err = cmd.Wait()
|
|
|
|
if ctx.Err() == context.DeadlineExceeded {
|
|
return tr.Error(errTimeout), 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
|
|
}
|
|
}
|
|
err = tr.Errorf("Procmail failed: %v - %q", err, output.String())
|
|
return err, permanent
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
|
|
// sanitizeForProcmail 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 sanitizeForProcmail(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)
|
|
}
|