package cli import ( "context" "fmt" "log/slog" "os" "os/signal" "sync" "syscall" "postern/internal/db" "postern/internal/dkim" "postern/internal/persistence" "github.com/spf13/cobra" ) type Service interface { Start() error Stop() error Status() string } type Config struct { DB *db.DB Persistence *persistence.Persistence DKIM *dkim.DKIM Services map[string]Service } type Command struct { db *db.DB persistence *persistence.Persistence dkim *dkim.DKIM services map[string]Service } func New(config *Config) (*Command, error) { return &Command{ db: config.DB, persistence: config.Persistence, dkim: config.DKIM, services: config.Services, }, nil } func (c *Command) rootCmd() *cobra.Command { rootCmd := &cobra.Command{ Use: "postern", Short: "Modern mail management", } serveCmd := &cobra.Command{ Use: "serve", Short: "Start the mail server", RunE: c.startServer, } pruneCmd := &cobra.Command{ Use: "prune", Short: "Delete orphaned mails", RunE: c.pruneMails, } userCmd := &cobra.Command{ Use: "user", Short: "Manage local login accounts", } userListCmd := &cobra.Command{ Use: "list", Short: "List all users", RunE: c.listUsers, } addUserCmd := &cobra.Command{ Use: "add ", Short: "Add a user", Args: cobra.ExactArgs(1), RunE: c.addUser, } addUserAddressCmd := &cobra.Command{ Use: "add-address
", Short: "Add an address for a user", Args: cobra.ExactArgs(2), RunE: c.addUserAddress, } userRemoveCmd := &cobra.Command{ Use: "remove ", Short: "Remove a user", Args: cobra.ExactArgs(1), RunE: c.removeUser, } userCmd.AddCommand(userListCmd) userCmd.AddCommand(addUserCmd) userCmd.AddCommand(addUserAddressCmd) userCmd.AddCommand(userRemoveCmd) backupMxCmd := &cobra.Command{ Use: "backup-mx", Short: "Manage inbound SMTP authentication", } backupMxListCmd := &cobra.Command{ Use: "list", Short: "List all SMTP auth users", Args: cobra.NoArgs, RunE: c.listSMTPAuth, } backupMxAddCmd := &cobra.Command{ Use: "add ", Short: "Add SMTP auth user", Args: cobra.ExactArgs(1), RunE: c.addSMTPAuth, } backupMxRemoveCmd := &cobra.Command{ Use: "remove ", Short: "Remove SMTP auth user", Args: cobra.ExactArgs(1), RunE: c.removeSMTPAuth, } backupMxCmd.AddCommand(backupMxListCmd) backupMxCmd.AddCommand(backupMxAddCmd) backupMxCmd.AddCommand(backupMxRemoveCmd) dkimCmd := &cobra.Command{ Use: "dkim", Short: "Manage DKIM selectors", } dkimListCmd := &cobra.Command{ Use: "list", Short: "List all available selectors", Args: cobra.NoArgs, RunE: c.listDKIM, } addDKIMCmd := &cobra.Command{ Use: "add [selector] [domain]", Short: "Add DKIM selector for domain", Args: cobra.ExactArgs(2), RunE: c.addDKIM, } getDKIMCmd := &cobra.Command{ Use: "get [selector] [domain]", Short: "Get public DKIM record for the given selector and domain", Args: cobra.ExactArgs(2), RunE: c.getDKIM, } enableDKIMCmd := &cobra.Command{ Use: "enable [selector] [domain]", Short: "Enable DKIM selector", Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { return c.setDKIMEnabled(cmd, args, true) }, } enableDKIMCmd.Flags().BoolP("force", "f", false, "Force enable DKIM even if validation fails") disableDKIMCmd := &cobra.Command{ Use: "disable [selector] [domain]", Short: "Disable DKIM selector", Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { return c.setDKIMEnabled(cmd, args, false) }, } dkimRemoveCmd := &cobra.Command{ Use: "remove [selector] [domain]", Short: "Remove DKIM selector", Args: cobra.ExactArgs(2), RunE: c.removeDKIM, } dkimCmd.AddCommand(dkimListCmd) dkimCmd.AddCommand(addDKIMCmd) dkimCmd.AddCommand(getDKIMCmd) dkimCmd.AddCommand(enableDKIMCmd) dkimCmd.AddCommand(disableDKIMCmd) dkimCmd.AddCommand(dkimRemoveCmd) rootCmd.AddCommand(serveCmd) rootCmd.AddCommand(pruneCmd) rootCmd.AddCommand(userCmd) rootCmd.AddCommand(backupMxCmd) rootCmd.AddCommand(dkimCmd) return rootCmd } func (c *Command) Execute() error { return c.rootCmd().Execute() } func (c *Command) startServer(_ *cobra.Command, _ []string) error { fmt.Println("Starting postern server...") slog.SetLogLoggerLevel(slog.LevelDebug) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() var wg sync.WaitGroup for name, svc := range c.services { wg.Go(func() { err := svc.Start() if err != nil { slog.Error("running service", "name", name, "error", err) } }) } go func() { <-ctx.Done() slog.Info("shutdown signal received, stopping services") for name, svc := range c.services { if err := svc.Stop(); err != nil { slog.Error("stopping service", "name", name, "error", err) } } }() wg.Wait() return nil }