Publish Alpine musl distroless Postfix as the sole latest image
Build and Push Docker Image / build (push) Successful in 1m25s
Build and Push Docker Image / build (push) Successful in 1m25s
This commit is contained in:
1 parent
2a9a7c6baa
commit
7ead00647b
12 files changed
+776
-1274
No files matched your search
@@ -0,0 +1,230 @@
|
||||
// Native configuration and process launcher for the Alpine-derived runtime.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
func fail(err error) { fmt.Fprintln(os.Stderr, "postfix-entrypoint:", err); os.Exit(1) }
|
||||
func check(err error) {
|
||||
if err != nil {
|
||||
fail(err)
|
||||
}
|
||||
}
|
||||
func env(name, fallback string) string {
|
||||
value := os.Getenv(name)
|
||||
if value == "" {
|
||||
value = fallback
|
||||
}
|
||||
if strings.ContainsAny(value, "\r\n\x00") {
|
||||
fail(fmt.Errorf("%s must be a single line", name))
|
||||
}
|
||||
return value
|
||||
}
|
||||
func run(command string, args ...string) {
|
||||
c := exec.Command(command, args...)
|
||||
c.Stdout = os.Stdout
|
||||
c.Stderr = os.Stderr
|
||||
check(c.Run())
|
||||
}
|
||||
func health() error {
|
||||
c, err := net.DialTimeout("tcp4", "127.0.0.1:25", 3*time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer c.Close()
|
||||
if err = c.SetDeadline(time.Now().Add(3 * time.Second)); err != nil {
|
||||
return err
|
||||
}
|
||||
line, err := bufio.NewReader(c).ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !strings.HasPrefix(line, "220 ") {
|
||||
return fmt.Errorf("unexpected SMTP greeting: %s", line)
|
||||
}
|
||||
_, err = c.Write([]byte("QUIT\r\n"))
|
||||
return err
|
||||
}
|
||||
func directory(path string, uid, gid int, mode os.FileMode) {
|
||||
check(os.MkdirAll(path, mode))
|
||||
info, err := os.Lstat(path)
|
||||
check(err)
|
||||
if !info.IsDir() {
|
||||
fail(fmt.Errorf("%s must be a directory, not a symlink", path))
|
||||
}
|
||||
check(os.Chown(path, uid, gid))
|
||||
check(os.Chmod(path, mode))
|
||||
}
|
||||
func initQueue() {
|
||||
account, err := user.Lookup("postfix")
|
||||
check(err)
|
||||
group, err := user.LookupGroup("postdrop")
|
||||
check(err)
|
||||
uid, err := strconv.Atoi(account.Uid)
|
||||
check(err)
|
||||
gid, err := strconv.Atoi(group.Gid)
|
||||
check(err)
|
||||
directory("/var/lib/postfix", uid, 0, 0700)
|
||||
directory("/var/spool/postfix", 0, 0, 0755)
|
||||
directory("/var/spool/postfix/pid", 0, 0, 0755)
|
||||
for _, name := range strings.Fields("active bounce corrupt defer deferred flush hold incoming private saved trace") {
|
||||
directory("/var/spool/postfix/"+name, uid, 0, 0700)
|
||||
}
|
||||
directory("/var/spool/postfix/maildrop", uid, gid, 0730)
|
||||
directory("/var/spool/postfix/public", uid, gid, 0710)
|
||||
// Keep the pid file: master locks it and safely handles stale contents itself.
|
||||
// Unlinking a live lock could permit two masters on a shared queue.
|
||||
run("/usr/sbin/postsuper")
|
||||
}
|
||||
func networks() string {
|
||||
local := env("LOCAL_NETWORK", "")
|
||||
if local == "" {
|
||||
addresses, err := net.InterfaceAddrs()
|
||||
check(err)
|
||||
for _, address := range addresses {
|
||||
n, ok := address.(*net.IPNet)
|
||||
if ok && n.IP.To4() != nil && !n.IP.IsLoopback() {
|
||||
local = (&net.IPNet{IP: n.IP.Mask(n.Mask), Mask: n.Mask}).String()
|
||||
break
|
||||
}
|
||||
}
|
||||
if local == "" {
|
||||
fail(errors.New("cannot detect local network; set LOCAL_NETWORK"))
|
||||
}
|
||||
}
|
||||
values := []string{"127.0.0.0/8", "[::1]/128"}
|
||||
for _, item := range append([]string{local}, strings.Split(env("SMTP_NETWORKS", ""), ",")...) {
|
||||
item = strings.TrimSpace(item)
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
ip, network, err := net.ParseCIDR(item)
|
||||
if err != nil || ip.To4() == nil {
|
||||
fail(fmt.Errorf("invalid IPv4 network: %s", item))
|
||||
}
|
||||
values = append(values, network.String())
|
||||
}
|
||||
return strings.Join(values, ", ")
|
||||
}
|
||||
func configure() {
|
||||
server := env("SMTP_SERVER", "")
|
||||
username := env("SMTP_USERNAME", "")
|
||||
password := env("SMTP_PASSWORD", "")
|
||||
if server == "" || username == "" || password == "" {
|
||||
fail(errors.New("SMTP_SERVER, SMTP_USERNAME and SMTP_PASSWORD are required"))
|
||||
}
|
||||
if strings.ContainsAny(server, " \t[]") {
|
||||
fail(errors.New("SMTP_SERVER must be a hostname or IP address"))
|
||||
}
|
||||
port := env("SMTP_PORT", "587")
|
||||
number, err := strconv.Atoi(port)
|
||||
if err != nil || number < 1 || number > 65535 {
|
||||
fail(errors.New("SMTP_PORT must be between 1 and 65535"))
|
||||
}
|
||||
hostname, err := os.Hostname()
|
||||
check(err)
|
||||
hostname = env("SERVER_HOSTNAME", hostname)
|
||||
domain := env("DOMAIN", "")
|
||||
if domain == "" {
|
||||
_, suffix, ok := strings.Cut(hostname, ".")
|
||||
if ok {
|
||||
domain = suffix
|
||||
} else {
|
||||
domain = "localdomain"
|
||||
}
|
||||
}
|
||||
timezone := env("TIMEZONE", "America/New_York")
|
||||
if filepath.IsAbs(timezone) || strings.Contains(timezone, "..") {
|
||||
fail(errors.New("invalid TIMEZONE"))
|
||||
}
|
||||
zone, err := os.ReadFile(filepath.Join("/usr/share/zoneinfo", timezone))
|
||||
check(err)
|
||||
check(os.WriteFile("/etc/localtime", zone, 0644))
|
||||
check(os.WriteFile("/etc/timezone", []byte(timezone+"\n"), 0644))
|
||||
relay := fmt.Sprintf("[%s]:%s", server, port)
|
||||
config := map[string]string{
|
||||
"myhostname": hostname, "mydomain": domain, "mydestination": "$myhostname", "myorigin": "$mydomain",
|
||||
"mynetworks": networks(), "inet_interfaces": "all", "inet_protocols": "ipv4", "relayhost": relay,
|
||||
"relay_domains": "*", "smtp_sasl_auth_enable": "yes", "smtp_sasl_password_maps": "lmdb:/etc/postfix/sasl_passwd",
|
||||
"smtp_sasl_security_options": "noanonymous", "smtp_sasl_tls_security_options": "noanonymous",
|
||||
"smtp_sasl_mechanism_filter": "plain, login", "smtp_tls_security_level": "may",
|
||||
"smtp_tls_CAfile": "/etc/ssl/certs/ca-certificates.crt",
|
||||
"header_size_limit": "4096000", "mailbox_size_limit": "0", "message_size_limit": "52428800",
|
||||
"recipient_delimiter": "+", "smtpd_relay_restrictions": "permit_mynetworks, reject_unauth_destination",
|
||||
"smtpd_recipient_restrictions": "permit_mynetworks, reject_unauth_destination", "smtpd_sasl_local_domain": domain,
|
||||
"maillog_file": "/dev/stdout", "maillog_file_prefixes": "/var, /dev", "maximal_queue_lifetime": "1d",
|
||||
"bounce_queue_lifetime": "1d", "queue_run_delay": "300s", "minimal_backoff_time": "300s", "maximal_backoff_time": "4000s",
|
||||
}
|
||||
for _, provider := range []string{"mailtrap.io", "sendgrid", "maileroo"} {
|
||||
if strings.Contains(strings.ToLower(server), provider) {
|
||||
config["smtp_tls_security_level"] = "encrypt"
|
||||
config["smtp_tls_session_cache_database"] = "lmdb:${data_directory}/smtp_scache"
|
||||
config["smtp_tls_loglevel"] = "1"
|
||||
}
|
||||
}
|
||||
if env("DEBUG", "no") == "yes" {
|
||||
config["debug_peer_level"] = "2"
|
||||
}
|
||||
tag := env("SMTP_HEADER_TAG", "")
|
||||
if tag != "" {
|
||||
check(os.WriteFile("/etc/postfix/header_tag", []byte("/^MIME-Version:/i PREPEND RelayTag: "+tag+"\n/^Content-Transfer-Encoding:/i PREPEND RelayTag: "+tag+"\n"), 0644))
|
||||
config["header_checks"] = "regexp:/etc/postfix/header_tag"
|
||||
} else {
|
||||
config["header_checks"] = ""
|
||||
}
|
||||
// Remove the obsolete setting even when reusing an existing configuration.
|
||||
run("/usr/sbin/postconf", "-X", "smtp_use_tls")
|
||||
// Pass arguments directly; no shell expansion or secret-bearing command line.
|
||||
args := []string{"-e"}
|
||||
for key, value := range config {
|
||||
args = append(args, key+" = "+value)
|
||||
}
|
||||
run("/usr/sbin/postconf", args...)
|
||||
// Restrict both the source and LMDB files, including any pre-existing files.
|
||||
syscall.Umask(0077)
|
||||
check(os.WriteFile("/etc/postfix/sasl_passwd", []byte(relay+" "+username+":"+password+"\n"), 0600))
|
||||
check(os.Chmod("/etc/postfix/sasl_passwd", 0600))
|
||||
run("/usr/sbin/postmap", "lmdb:/etc/postfix/sasl_passwd")
|
||||
check(os.Chmod("/etc/postfix/sasl_passwd.lmdb", 0600))
|
||||
syscall.Umask(0022)
|
||||
check(os.Unsetenv("SMTP_PASSWORD"))
|
||||
check(os.Unsetenv("SMTP_USERNAME"))
|
||||
initQueue()
|
||||
fmt.Fprintf(os.Stderr, "Postfix configured: hostname=%s relay=%s\n", hostname, relay)
|
||||
}
|
||||
func main() {
|
||||
if len(os.Args) > 1 {
|
||||
switch os.Args[1] {
|
||||
case "--healthcheck":
|
||||
check(health())
|
||||
return
|
||||
case "--check":
|
||||
run("/usr/sbin/postconf", "-n")
|
||||
run("/usr/sbin/postconf", "-M")
|
||||
return
|
||||
default:
|
||||
path, err := exec.LookPath(os.Args[1])
|
||||
check(err)
|
||||
check(syscall.Exec(path, os.Args[1:], os.Environ()))
|
||||
return
|
||||
}
|
||||
}
|
||||
configure()
|
||||
mode := "-s"
|
||||
if os.Getpid() == 1 {
|
||||
mode = "-i"
|
||||
}
|
||||
check(syscall.Exec("/usr/libexec/postfix/master", []string{"master", mode}, os.Environ()))
|
||||
}
|
||||
Reference in new issue
Block a user