all repos — postern

@ d1a9230eb6918613c1748e78ba80ba486973663a

Modern mail management

clone
git clone https:///postern.git

main.go (view raw)

  1package main
  2
  3import (
  4	"cmp"
  5	"crypto/tls"
  6	"fmt"
  7	"log"
  8	"log/slog"
  9	"os"
 10	"path"
 11	"path/filepath"
 12	"runtime"
 13
 14	"postern/internal/cli"
 15	"postern/internal/db"
 16	"postern/internal/dkim"
 17	"postern/internal/imap"
 18	"postern/internal/inbound"
 19	"postern/internal/persistence"
 20	"postern/internal/policy"
 21	"postern/internal/submission"
 22)
 23
 24func main() {
 25	socketPath := resolveSocketPath()
 26
 27	// Create a bootstrap closure that encapsulates all the resource initialization
 28	// that is only needed when the "serve" command is executed.
 29	bootstrap := func() (*cli.BootstrapDeps, error) {
 30		posternPersistenceDir := cmp.Or(os.Getenv("POSTERN_PERSISTENCE_DIR"), os.Getenv("STATE_DIRECTORY"))
 31		if posternPersistenceDir == "" {
 32			return nil, fmt.Errorf("POSTERN_PERSISTENCE_DIR or STATE_DIRECTORY is required")
 33		}
 34		err := persistence.CheckDirWritable(posternPersistenceDir)
 35		if err != nil {
 36			return nil, fmt.Errorf("POSTERN_PERSISTENCE_DIR: %w", err)
 37		}
 38		slog.Info("using postern persistence", "dir", posternPersistenceDir)
 39
 40		persistenceMasterkey := os.Getenv("POSTERN_MASTERKEY_FILE")
 41		if persistenceMasterkey == "" {
 42			persistenceMasterkey = path.Join(os.Getenv("CREDENTIALS_DIRECTORY"), "masterkey")
 43		}
 44		p, err := persistence.New(persistence.Config{
 45			BlobRoot:      path.Join(posternPersistenceDir, "blobs"),
 46			MasterkeyFile: persistenceMasterkey,
 47		})
 48		if err != nil {
 49			return nil, fmt.Errorf("failed to initialize persistence: %w", err)
 50		}
 51
 52		posternDB, err := db.OpenDB(&db.Config{
 53			DBPath: path.Join(posternPersistenceDir, "meta.db"),
 54		})
 55		if err != nil {
 56			return nil, fmt.Errorf("failed to open database: %w", err)
 57		}
 58
 59		engine, err := policy.NewEngine(path.Join(os.Getenv("CONFIGURATION_DIRECTORY"), "policy.star"), posternDB)
 60		if err != nil {
 61			return nil, fmt.Errorf("failed to initialize policy engine: %w", err)
 62		}
 63
 64		dkimSvc := dkim.New(&dkim.Config{
 65			DB:          posternDB,
 66			Persistence: p,
 67		})
 68
 69		listenAddrs := make(map[string]policy.ListenerConfig)
 70		for _, l := range engine.Listeners {
 71			listenAddrs[l.Role] = l
 72		}
 73
 74		services := make(map[string]cli.Service)
 75
 76		if listener, ok := listenAddrs["imap"]; ok {
 77			cert, err := tls.LoadX509KeyPair(listener.TLSCertFile, listener.TLSKeyFile)
 78			if err != nil {
 79				return nil, fmt.Errorf("failed to load IMAP TLS cert: %w", err)
 80			}
 81
 82			tlsConfig := &tls.Config{
 83				Certificates: []tls.Certificate{cert},
 84			}
 85
 86			imapServer, err := imap.NewServer(&imap.Config{
 87				DB:           posternDB,
 88				Listen:       listener.Address.String(),
 89				Persistence:  p,
 90				TLSConfig:    tlsConfig,
 91				PolicyEngine: &engine,
 92			})
 93			if err != nil {
 94				return nil, fmt.Errorf("failed to create IMAP server: %w", err)
 95			}
 96			services["imap"] = imapServer
 97		}
 98
 99		if listener, ok := listenAddrs["smtp_inbound"]; ok {
100			cert, err := tls.LoadX509KeyPair(listener.TLSCertFile, listener.TLSKeyFile)
101			if err != nil {
102				return nil, fmt.Errorf("failed to load SMTP inbound TLS cert: %w", err)
103			}
104
105			tlsConfig := &tls.Config{
106				Certificates: []tls.Certificate{cert},
107			}
108
109			receptionServer, err := inbound.NewServer(&inbound.Config{
110				DB:           posternDB,
111				Deliverer:    services["imap"].(inbound.MessageDeliverer),
112				Listen:       listener.Address.String(),
113				TLS:          tlsConfig,
114				PolicyEngine: &engine,
115			})
116			if err != nil {
117				return nil, fmt.Errorf("failed to create SMTP inbound server: %w", err)
118			}
119			services["smtp_inbound"] = receptionServer
120		}
121
122		if listener, ok := listenAddrs["smtp_submission"]; ok {
123			cert, err := tls.LoadX509KeyPair(listener.TLSCertFile, listener.TLSKeyFile)
124			if err != nil {
125				return nil, fmt.Errorf("failed to load SMTP submission TLS cert: %w", err)
126			}
127
128			tlsConfig := &tls.Config{
129				Certificates: []tls.Certificate{cert},
130			}
131
132			submissionServer, err := submission.NewServer(&submission.Config{
133				DB:           posternDB,
134				DKIM:         dkimSvc,
135				Listen:       listener.Address.String(),
136				TLS:          tlsConfig,
137				PolicyEngine: &engine,
138			})
139			if err != nil {
140				return nil, fmt.Errorf("failed to create SMTP submission server: %w", err)
141			}
142			services["smtp_submission"] = submissionServer
143		}
144
145		if listener, ok := listenAddrs["smtp_local_submission"]; ok {
146			// Check that address is loopback. Other addresses are not allowed for security reasons.
147			if !listener.Address.Addr().IsLoopback() {
148				return nil, fmt.Errorf("smtp_local_submission address is not a loopback address")
149			}
150
151			submissionServer, err := submission.NewServer(&submission.Config{
152				DB:                posternDB,
153				DKIM:              dkimSvc,
154				Listen:            listener.Address.String(),
155				PolicyEngine:      &engine,
156				IsLocalSubmission: true,
157			})
158			if err != nil {
159				return nil, fmt.Errorf("failed to create local SMTP submission server: %w", err)
160			}
161			services["smtp_local_submission"] = submissionServer
162		}
163
164		return &cli.BootstrapDeps{
165			DB:          posternDB,
166			Persistence: p,
167			DKIM:        dkimSvc,
168			Services:    services,
169		}, nil
170	}
171
172	posternCLI, err := cli.New(&cli.Config{
173		SocketPath: socketPath,
174		Bootstrap:  bootstrap,
175	})
176	if err != nil {
177		log.Fatal(err)
178	}
179
180	if err := posternCLI.Execute(); err != nil {
181		os.Exit(1)
182	}
183}
184
185// resolveSocketPath is runtime agnostic (Linux, MacOS) to choose the admin http socket path
186func resolveSocketPath() string {
187	if p := os.Getenv("POSTERN_SOCKET"); p != "" {
188		return p
189	}
190	if rd := os.Getenv("RUNTIME_DIRECTORY"); rd != "" {
191		return filepath.Join(rd, "postern.sock")
192	}
193	if runtime.GOOS == "linux" {
194		return "/run/postern/postern.sock"
195	}
196	if home, err := os.UserHomeDir(); err == nil {
197		return filepath.Join(home, ".postern", "postern.sock")
198	}
199	return "/tmp/postern.sock"
200}