all repos — postern @ fc287763b9ce6b6f9823e11c0b1173da0446ebc5

Modern mail management

internal/cli/cli.go (view raw)

  1package cli
  2
  3import (
  4	"context"
  5	"fmt"
  6	"log/slog"
  7	"os"
  8	"os/signal"
  9	"sync"
 10	"syscall"
 11
 12	"postern/internal/db"
 13	"postern/internal/dkim"
 14	"postern/internal/persistence"
 15
 16	"github.com/spf13/cobra"
 17)
 18
 19type Service interface {
 20	Start() error
 21	Stop() error
 22	Status() string
 23}
 24
 25type Config struct {
 26	DB          *db.DB
 27	Persistence *persistence.Persistence
 28	DKIM        *dkim.DKIM
 29	Services    map[string]Service
 30}
 31
 32type Command struct {
 33	db          *db.DB
 34	persistence *persistence.Persistence
 35	dkim        *dkim.DKIM
 36	services    map[string]Service
 37}
 38
 39func New(config *Config) (*Command, error) {
 40	return &Command{
 41		db:          config.DB,
 42		persistence: config.Persistence,
 43		dkim:        config.DKIM,
 44		services:    config.Services,
 45	}, nil
 46}
 47
 48func (c *Command) rootCmd() *cobra.Command {
 49	rootCmd := &cobra.Command{
 50		Use:   "postern",
 51		Short: "Modern mail management",
 52	}
 53
 54	serveCmd := &cobra.Command{
 55		Use:   "serve",
 56		Short: "Start the mail server",
 57		RunE:  c.startServer,
 58	}
 59
 60	pruneCmd := &cobra.Command{
 61		Use:   "prune",
 62		Short: "Delete orphaned mails",
 63		RunE:  c.pruneMails,
 64	}
 65
 66	userCmd := &cobra.Command{
 67		Use:   "user",
 68		Short: "Manage local login accounts",
 69	}
 70
 71	userListCmd := &cobra.Command{
 72		Use:   "list",
 73		Short: "List all users",
 74		RunE:  c.listUsers,
 75	}
 76
 77	addUserCmd := &cobra.Command{
 78		Use:   "add <username>",
 79		Short: "Add a user",
 80		Args:  cobra.ExactArgs(1),
 81		RunE:  c.addUser,
 82	}
 83
 84	addUserAddressCmd := &cobra.Command{
 85		Use:   "add-address <username> <address>",
 86		Short: "Add an address for a user",
 87		Args:  cobra.ExactArgs(2),
 88		RunE:  c.addUserAddress,
 89	}
 90
 91	userRemoveCmd := &cobra.Command{
 92		Use:   "remove <username>",
 93		Short: "Remove a user",
 94		Args:  cobra.ExactArgs(1),
 95		RunE:  c.removeUser,
 96	}
 97
 98	userCmd.AddCommand(userListCmd)
 99	userCmd.AddCommand(addUserCmd)
100	userCmd.AddCommand(addUserAddressCmd)
101	userCmd.AddCommand(userRemoveCmd)
102
103	backupMxCmd := &cobra.Command{
104		Use:   "backup-mx",
105		Short: "Manage inbound SMTP authentication",
106	}
107
108	backupMxListCmd := &cobra.Command{
109		Use:   "list",
110		Short: "List all SMTP auth users",
111		Args:  cobra.NoArgs,
112		RunE:  c.listSMTPAuth,
113	}
114
115	backupMxAddCmd := &cobra.Command{
116		Use:   "add <name>",
117		Short: "Add SMTP auth user",
118		Args:  cobra.ExactArgs(1),
119		RunE:  c.addSMTPAuth,
120	}
121
122	backupMxRemoveCmd := &cobra.Command{
123		Use:   "remove <name>",
124		Short: "Remove SMTP auth user",
125		Args:  cobra.ExactArgs(1),
126		RunE:  c.removeSMTPAuth,
127	}
128
129	backupMxCmd.AddCommand(backupMxListCmd)
130	backupMxCmd.AddCommand(backupMxAddCmd)
131	backupMxCmd.AddCommand(backupMxRemoveCmd)
132
133	dkimCmd := &cobra.Command{
134		Use:   "dkim",
135		Short: "Manage DKIM selectors",
136	}
137
138	dkimListCmd := &cobra.Command{
139		Use:   "list",
140		Short: "List all available selectors",
141		Args:  cobra.NoArgs,
142		RunE:  c.listDKIM,
143	}
144
145	addDKIMCmd := &cobra.Command{
146		Use:   "add [selector] [domain]",
147		Short: "Add DKIM selector for domain",
148		Args:  cobra.ExactArgs(2),
149		RunE:  c.addDKIM,
150	}
151
152	getDKIMCmd := &cobra.Command{
153		Use:   "get [selector] [domain]",
154		Short: "Get public DKIM record for the given selector and domain",
155		Args:  cobra.ExactArgs(2),
156		RunE:  c.getDKIM,
157	}
158
159	enableDKIMCmd := &cobra.Command{
160		Use:   "enable [selector] [domain]",
161		Short: "Enable DKIM selector",
162		Args:  cobra.ExactArgs(2),
163		RunE: func(cmd *cobra.Command, args []string) error {
164			return c.setDKIMEnabled(cmd, args, true)
165		},
166	}
167	enableDKIMCmd.Flags().BoolP("force", "f", false, "Force enable DKIM even if validation fails")
168
169	disableDKIMCmd := &cobra.Command{
170		Use:   "disable [selector] [domain]",
171		Short: "Disable DKIM selector",
172		Args:  cobra.ExactArgs(2),
173		RunE: func(cmd *cobra.Command, args []string) error {
174			return c.setDKIMEnabled(cmd, args, false)
175		},
176	}
177
178	dkimRemoveCmd := &cobra.Command{
179		Use:   "remove [selector] [domain]",
180		Short: "Remove DKIM selector",
181		Args:  cobra.ExactArgs(2),
182		RunE:  c.removeDKIM,
183	}
184
185	dkimCmd.AddCommand(dkimListCmd)
186	dkimCmd.AddCommand(addDKIMCmd)
187	dkimCmd.AddCommand(getDKIMCmd)
188	dkimCmd.AddCommand(enableDKIMCmd)
189	dkimCmd.AddCommand(disableDKIMCmd)
190	dkimCmd.AddCommand(dkimRemoveCmd)
191
192	rootCmd.AddCommand(serveCmd)
193	rootCmd.AddCommand(pruneCmd)
194	rootCmd.AddCommand(userCmd)
195	rootCmd.AddCommand(backupMxCmd)
196	rootCmd.AddCommand(dkimCmd)
197
198	return rootCmd
199}
200
201func (c *Command) Execute() error {
202	return c.rootCmd().Execute()
203}
204
205func (c *Command) startServer(_ *cobra.Command, _ []string) error {
206	fmt.Println("Starting postern server...")
207	slog.SetLogLoggerLevel(slog.LevelDebug)
208
209	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
210	defer stop()
211
212	var wg sync.WaitGroup
213	for name, svc := range c.services {
214		wg.Go(func() {
215			err := svc.Start()
216			if err != nil {
217				slog.Error("running service", "name", name, "error", err)
218			}
219		})
220	}
221
222	go func() {
223		<-ctx.Done()
224		slog.Info("shutdown signal received, stopping services")
225		for name, svc := range c.services {
226			if err := svc.Stop(); err != nil {
227				slog.Error("stopping service", "name", name, "error", err)
228			}
229		}
230	}()
231
232	wg.Wait()
233	return nil
234}