mirror of
https://blitiri.com.ar/repos/chasquid
synced 2025-12-20 15:07:03 +00:00
This patch tidies up the Procmail courier: - Move the configuration options to the courier instance, instead of using global variables. - Implement more useful string replacement options. - Use exec.CommandContext for running the command with a timeout. As a consequence of the first item, the queue now takes the couriers via its constructor.
106 lines
2.4 KiB
Go
106 lines
2.4 KiB
Go
package courier
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os/exec"
|
|
"strings"
|
|
"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 {
|
|
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)
|
|
}
|
|
|
|
output := &bytes.Buffer{}
|
|
cmd.Stdout = output
|
|
cmd.Stderr = output
|
|
|
|
err = cmd.Start()
|
|
if err != nil {
|
|
return tr.Errorf("Error starting procmail: %v", err)
|
|
}
|
|
|
|
_, err = bytes.NewBuffer(data).WriteTo(cmdStdin)
|
|
if err != nil {
|
|
return tr.Errorf("Error sending data to procmail: %v", err)
|
|
}
|
|
|
|
cmdStdin.Close()
|
|
|
|
err = cmd.Wait()
|
|
|
|
if ctx.Err() == context.DeadlineExceeded {
|
|
return tr.Error(errTimeout)
|
|
}
|
|
if err != nil {
|
|
return tr.Errorf("Procmail failed: %v - %q", err, output.String())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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)
|
|
}
|