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}