test / test (push) Canceled after 0s
The runtime base is debian:trixie-slim, the build stage golang:1.26-trixie:
Postfix 3.10.13 with upstream's 2026 security fixes up to the August
batch, libmilter 8.18.1, OpenSSL 3.5. Every citation of another program in
code, scripts and documents was re-read against the trixie sources
(dev/upstream; bookworm's kept in dev/upstream-bookworm) and updated;
development.md's cited-package table holds the trixie versions.
What the re-reading changed in behaviour:
- logtail: reject lines with sasl_method/sasl_username/sasl_sender
(smtpd_check.c:1019-1026); cleanup's "removed (canceled)" forgets a
port-25 id (cleanup_api.c:363-364).
- internal/postfix/retry.go reads postconf's stdout only: since 3.9 a
deprecated-parameter warning on stderr made one value too many and the
panel fell back to the compiled-in retry policy.
- logrotate-loop.sh: logrotate 3.22 exits 1 when it ignores a config;
the loop refuses on that output whatever the status.
- postfix-config.sh: cyrus_sasl_config_path set explicitly (bookworm's
07_sasl_config.diff is gone); maillog_file_permissions=0640; mynetworks
explicit; Dockerfile: SYNC_CHROOT= in /etc/default/postfix,
opendkim-tools dropped, openssl named, systemd-standalone-sysusers so
cron does not bring the whole of systemd.
- The selfpost group is 10998, outside the system range 100-999: in
trixie a package took 998 and the build failed (owner's decision:
a conflict-free id, the distribution's own ids untouched). The
entrypoint hands /data back by name when the user or the group differs.
Guide, backup refusal text, help (design commit 68ec8b2), e2e.
- smtpd_hide_client_session=yes on smtps and submission (owner's
decision): Received: names no client, TLS, login or protocol; port 25
keeps the full header. e2e checks both.
- test/upstream/opendkim-probe.sh makes keys with openssl genrsa.
Tests: on the server, make e2e exit 0 (674 s, the image built on trixie),
go vet + go test 27 packages ok. CHANGELOG [Unreleased], guide,
architecture.md, security.md, roadmap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
113 lines
3.6 KiB
Go
113 lines
3.6 KiB
Go
package postfix
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// QueuedMessage is one message Postfix is holding, as `postqueue -j` reports
|
|
// it: where it is, and since when.
|
|
//
|
|
// Postfix 3.10.13, src/postqueue/showq_json.c, format_json(): one object and a
|
|
// newline per message - "queue_name", "queue_id", "arrival_time" (seconds,
|
|
// %ld), "message_size", "forced_expire", "sender", "recipients" [{"address",
|
|
// "delay_reason"}]; the queue names are those of src/global/mail_queue.h that
|
|
// src/showq/showq.c, showq_service(), scans.
|
|
type QueuedMessage struct {
|
|
ID string
|
|
Queue string // "deferred", "active", "hold", "incoming", "maildrop"
|
|
Sender string
|
|
Recipients []string
|
|
Arrival time.Time
|
|
}
|
|
|
|
// queueJSONTimeout is longer than the listing for a page gets: the notifier
|
|
// asks once an hour and can wait, and a queue large enough to be slow to list
|
|
// is the one that must not go unreported.
|
|
const queueJSONTimeout = 30 * time.Second
|
|
|
|
// readQueueJSON runs `postqueue -j`. It is a var so tests can answer without a
|
|
// running Postfix, the way readRetryConf is.
|
|
var readQueueJSON = postqueueJSON
|
|
|
|
func postqueueJSON() ([]byte, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), queueJSONTimeout)
|
|
defer cancel()
|
|
// A fixed flag and no user input, never through a shell (security.md).
|
|
cmd := exec.CommandContext(ctx, "postqueue", "-j")
|
|
out := &limitWriter{remaining: maxQueueOutput}
|
|
errOut := &limitWriter{remaining: 4096}
|
|
cmd.Stdout = out
|
|
cmd.Stderr = errOut
|
|
err := cmd.Run()
|
|
if ctx.Err() == context.DeadlineExceeded {
|
|
return nil, fmt.Errorf("postqueue -j timed out after %s", queueJSONTimeout)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("postqueue -j: %w: %s", err, strings.TrimSpace(errOut.buf.String()))
|
|
}
|
|
if out.truncated {
|
|
// A listing cut at the size limit says nothing about the messages past
|
|
// the cut - the oldest may be among them. It is no listing.
|
|
return nil, fmt.Errorf("postqueue -j: listing is larger than %d bytes", maxQueueOutput)
|
|
}
|
|
return out.buf.Bytes(), nil
|
|
}
|
|
|
|
// QueueMessages returns the messages Postfix holds with the time each arrived,
|
|
// for the notification about mail that has waited too long. An error means the
|
|
// queue could not be listed and says nothing about any message.
|
|
func QueueMessages() ([]QueuedMessage, error) {
|
|
out, err := readQueueJSON()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return parseQueueJSON(out)
|
|
}
|
|
|
|
// parseQueueJSON reads `postqueue -j` output: one JSON object per line. A line
|
|
// that is not one is skipped; a line too long to read ends the listing with an
|
|
// error, since what follows it is unknown.
|
|
func parseQueueJSON(out []byte) ([]QueuedMessage, error) {
|
|
var msgs []QueuedMessage
|
|
sc := bufio.NewScanner(strings.NewReader(string(out)))
|
|
sc.Buffer(make([]byte, 0, 64*1024), 1<<20)
|
|
for sc.Scan() {
|
|
line := strings.TrimSpace(sc.Text())
|
|
if line == "" {
|
|
continue
|
|
}
|
|
var e struct {
|
|
QueueName string `json:"queue_name"`
|
|
QueueID string `json:"queue_id"`
|
|
ArrivalTime int64 `json:"arrival_time"`
|
|
Sender string `json:"sender"`
|
|
Recipients []struct {
|
|
Address string `json:"address"`
|
|
} `json:"recipients"`
|
|
}
|
|
if err := json.Unmarshal([]byte(line), &e); err != nil || e.QueueID == "" || e.ArrivalTime <= 0 {
|
|
continue
|
|
}
|
|
m := QueuedMessage{
|
|
ID: e.QueueID,
|
|
Queue: e.QueueName,
|
|
Sender: e.Sender,
|
|
Arrival: time.Unix(e.ArrivalTime, 0).UTC(),
|
|
}
|
|
for _, r := range e.Recipients {
|
|
m.Recipients = append(m.Recipients, r.Address)
|
|
}
|
|
msgs = append(msgs, m)
|
|
}
|
|
if err := sc.Err(); err != nil {
|
|
return nil, fmt.Errorf("read postqueue -j: %w", err)
|
|
}
|
|
return msgs, nil
|
|
}
|