231 lines
7.5 KiB
Go
231 lines
7.5 KiB
Go
// 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()))
|
|
}
|