all repos — postern

@ v0.1.0-alpha

Modern mail management

clone
git clone https:///postern.git

main.go (view raw)

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