maddy

git clone git://git.lin.moe/fmaddy/maddy.git

  1/*
  2Maddy Mail Server - Composable all-in-one email server.
  3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
  4
  5This program is free software: you can redistribute it and/or modify
  6it under the terms of the GNU General Public License as published by
  7the Free Software Foundation, either version 3 of the License, or
  8(at your option) any later version.
  9
 10This program is distributed in the hope that it will be useful,
 11but WITHOUT ANY WARRANTY; without even the implied warranty of
 12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13GNU General Public License for more details.
 14
 15You should have received a copy of the GNU General Public License
 16along with this program.  If not, see <https://www.gnu.org/licenses/>.
 17*/
 18
 19package maddy
 20
 21import (
 22	"errors"
 23	"fmt"
 24	"io"
 25	"net/http"
 26	"os"
 27	"path/filepath"
 28	"runtime"
 29	"runtime/debug"
 30
 31	parser "github.com/foxcpp/maddy/framework/cfgparser"
 32	"github.com/foxcpp/maddy/framework/config"
 33	modconfig "github.com/foxcpp/maddy/framework/config/module"
 34	"github.com/foxcpp/maddy/framework/config/tls"
 35	"github.com/foxcpp/maddy/framework/hooks"
 36	"github.com/foxcpp/maddy/framework/log"
 37	"github.com/foxcpp/maddy/framework/module"
 38	"github.com/foxcpp/maddy/internal/authz"
 39	maddycli "github.com/foxcpp/maddy/internal/cli"
 40	"github.com/urfave/cli/v2"
 41
 42	// Import packages for side-effect of module registration.
 43	_ "github.com/foxcpp/maddy/internal/auth/dovecot_sasl"
 44	_ "github.com/foxcpp/maddy/internal/auth/external"
 45	_ "github.com/foxcpp/maddy/internal/auth/ldap"
 46	_ "github.com/foxcpp/maddy/internal/auth/netauth"
 47	_ "github.com/foxcpp/maddy/internal/auth/pam"
 48	_ "github.com/foxcpp/maddy/internal/auth/pass_table"
 49	_ "github.com/foxcpp/maddy/internal/auth/plain_separate"
 50	_ "github.com/foxcpp/maddy/internal/auth/shadow"
 51	_ "github.com/foxcpp/maddy/internal/check/authorize_sender"
 52	_ "github.com/foxcpp/maddy/internal/check/command"
 53	_ "github.com/foxcpp/maddy/internal/check/dkim"
 54	_ "github.com/foxcpp/maddy/internal/check/dns"
 55	_ "github.com/foxcpp/maddy/internal/check/dnsbl"
 56	_ "github.com/foxcpp/maddy/internal/check/milter"
 57	_ "github.com/foxcpp/maddy/internal/check/requiretls"
 58	_ "github.com/foxcpp/maddy/internal/check/rspamd"
 59	_ "github.com/foxcpp/maddy/internal/check/spf"
 60	_ "github.com/foxcpp/maddy/internal/endpoint/dovecot_sasld"
 61	_ "github.com/foxcpp/maddy/internal/endpoint/imap"
 62	_ "github.com/foxcpp/maddy/internal/endpoint/openmetrics"
 63	_ "github.com/foxcpp/maddy/internal/endpoint/smtp"
 64	_ "github.com/foxcpp/maddy/internal/imap_filter"
 65	_ "github.com/foxcpp/maddy/internal/imap_filter/command"
 66	_ "github.com/foxcpp/maddy/internal/modify"
 67	_ "github.com/foxcpp/maddy/internal/modify/dkim"
 68	_ "github.com/foxcpp/maddy/internal/storage/blob/fs"
 69	_ "github.com/foxcpp/maddy/internal/storage/blob/s3"
 70	_ "github.com/foxcpp/maddy/internal/storage/imapsql"
 71	_ "github.com/foxcpp/maddy/internal/table"
 72	_ "github.com/foxcpp/maddy/internal/target/queue"
 73	_ "github.com/foxcpp/maddy/internal/target/remote"
 74	_ "github.com/foxcpp/maddy/internal/target/smtp"
 75	_ "github.com/foxcpp/maddy/internal/tls"
 76)
 77
 78var (
 79	Version = "go-build"
 80
 81	enableDebugFlags = false
 82)
 83
 84func BuildInfo() string {
 85	version := Version
 86	if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "(devel)" {
 87		version = info.Main.Version
 88	}
 89
 90	return fmt.Sprintf(`%s %s/%s %s
 91
 92default config: %s
 93default state_dir: %s
 94default runtime_dir: %s`,
 95		version, runtime.GOOS, runtime.GOARCH, runtime.Version(),
 96		filepath.Join(ConfigDirectory, "maddy.conf"),
 97		DefaultStateDirectory,
 98		DefaultRuntimeDirectory)
 99}
100
101func init() {
102	maddycli.AddGlobalFlag(
103		&cli.PathFlag{
104			Name:    "config",
105			Usage:   "Configuration file to use",
106			EnvVars: []string{"MADDY_CONFIG"},
107			Value:   filepath.Join(ConfigDirectory, "maddy.conf"),
108		},
109	)
110	maddycli.AddGlobalFlag(&cli.BoolFlag{
111		Name:        "debug",
112		Usage:       "enable debug logging early",
113		Destination: &log.DefaultLogger.Debug,
114	})
115	maddycli.AddSubcommand(&cli.Command{
116		Name:  "run",
117		Usage: "Start the server",
118		Flags: []cli.Flag{
119			&cli.StringFlag{
120				Name:        "libexec",
121				Value:       DefaultLibexecDirectory,
122				Usage:       "path to the libexec directory",
123				Destination: &config.LibexecDirectory,
124			},
125			&cli.StringSliceFlag{
126				Name:  "log",
127				Usage: "default logging target(s)",
128				Value: cli.NewStringSlice("stderr"),
129			},
130			&cli.BoolFlag{
131				Name:   "v",
132				Usage:  "print version and build metadata, then exit",
133				Hidden: true,
134			},
135		},
136		Action: Run,
137	})
138	maddycli.AddSubcommand(&cli.Command{
139		Name:  "version",
140		Usage: "Print version and build metadata, then exit",
141		Action: func(c *cli.Context) error {
142			fmt.Println(BuildInfo())
143			return nil
144		},
145	})
146
147	if enableDebugFlags {
148		maddycli.AddGlobalFlag(&cli.StringFlag{
149			Name:  "debug.pprof",
150			Usage: "enable live profiler HTTP endpoint and listen on the specified address",
151		})
152		maddycli.AddGlobalFlag(&cli.IntFlag{
153			Name:  "debug.blockprofrate",
154			Usage: "set blocking profile rate",
155		})
156		maddycli.AddGlobalFlag(&cli.IntFlag{
157			Name:  "debug.mutexproffract",
158			Usage: "set mutex profile fraction",
159		})
160	}
161}
162
163// Run is the entry point for all server-running code. It takes care of command line arguments processing,
164// logging initialization, directives setup, configuration reading. After all that, it
165// calls moduleMain to initialize and run modules.
166func Run(c *cli.Context) error {
167	if c.NArg() != 0 {
168		return cli.Exit(fmt.Sprintln("usage:", os.Args[0], "[options]"), 2)
169	}
170
171	if c.Bool("v") {
172		fmt.Println("maddy", BuildInfo())
173		return nil
174	}
175
176	var err error
177	log.DefaultLogger.Out, err = LogOutputOption(c.StringSlice("log"))
178	if err != nil {
179		return cli.Exit(err.Error(), 2)
180	}
181
182	initDebug(c)
183
184	os.Setenv("PATH", config.LibexecDirectory+string(filepath.ListSeparator)+os.Getenv("PATH"))
185
186	f, err := os.Open(c.Path("config"))
187	if err != nil {
188		return cli.Exit(err.Error(), 2)
189	}
190	defer f.Close()
191
192	cfg, err := parser.Read(f, c.Path("config"))
193	if err != nil {
194		return cli.Exit(err.Error(), 2)
195	}
196
197	defer log.DefaultLogger.Out.Close()
198
199	if err := moduleMain(cfg); err != nil {
200		return cli.Exit(err.Error(), 1)
201	}
202
203	return nil
204}
205
206func initDebug(c *cli.Context) {
207	if !enableDebugFlags {
208		return
209	}
210
211	if c.IsSet("debug.pprof") {
212		profileEndpoint := c.String("debug.pprof")
213		go func() {
214			log.Println("listening on", "http://"+profileEndpoint, "for profiler requests")
215			log.Println("failed to listen on profiler endpoint:", http.ListenAndServe(profileEndpoint, nil))
216		}()
217	}
218
219	// These values can also be affected by environment so set them
220	// only if argument is specified.
221	if c.IsSet("debug.mutexproffract") {
222		runtime.SetMutexProfileFraction(c.Int("debug.mutexproffract"))
223	}
224	if c.IsSet("debug.blockprofrate") {
225		runtime.SetBlockProfileRate(c.Int("debug.blockprofrate"))
226	}
227}
228
229func InitDirs() error {
230	if config.StateDirectory == "" {
231		config.StateDirectory = DefaultStateDirectory
232	}
233	if config.RuntimeDirectory == "" {
234		config.RuntimeDirectory = DefaultRuntimeDirectory
235	}
236	if config.LibexecDirectory == "" {
237		config.LibexecDirectory = DefaultLibexecDirectory
238	}
239
240	if err := ensureDirectoryWritable(config.StateDirectory); err != nil {
241		return err
242	}
243	if err := ensureDirectoryWritable(config.RuntimeDirectory); err != nil {
244		return err
245	}
246
247	// Make sure all paths we are going to use are absolute
248	// before we change the working directory.
249	if !filepath.IsAbs(config.StateDirectory) {
250		return errors.New("statedir should be absolute")
251	}
252	if !filepath.IsAbs(config.RuntimeDirectory) {
253		return errors.New("runtimedir should be absolute")
254	}
255	if !filepath.IsAbs(config.LibexecDirectory) {
256		return errors.New("-libexec should be absolute")
257	}
258
259	// Change the working directory to make all relative paths
260	// in configuration relative to state directory.
261	if err := os.Chdir(config.StateDirectory); err != nil {
262		log.Println(err)
263	}
264
265	return nil
266}
267
268func ensureDirectoryWritable(path string) error {
269	if err := os.MkdirAll(path, 0o700); err != nil {
270		return err
271	}
272
273	testFile, err := os.Create(filepath.Join(path, "writeable-test"))
274	if err != nil {
275		return err
276	}
277	testFile.Close()
278	return os.RemoveAll(testFile.Name())
279}
280
281func ReadGlobals(cfg []config.Node) (map[string]interface{}, []config.Node, error) {
282	globals := config.NewMap(nil, config.Node{Children: cfg})
283	globals.String("state_dir", false, false, DefaultStateDirectory, &config.StateDirectory)
284	globals.String("runtime_dir", false, false, DefaultRuntimeDirectory, &config.RuntimeDirectory)
285	globals.String("hostname", false, false, "", nil)
286	globals.String("autogenerated_msg_domain", false, false, "", nil)
287	globals.Custom("tls", false, false, nil, tls.TLSDirective, nil)
288	globals.Custom("tls_client", false, false, nil, tls.TLSClientBlock, nil)
289	globals.Bool("storage_perdomain", false, false, nil)
290	globals.Bool("auth_perdomain", false, false, nil)
291	globals.StringList("auth_domains", false, false, nil, nil)
292	globals.Custom("log", false, false, defaultLogOutput, logOutput, &log.DefaultLogger.Out)
293	globals.Bool("debug", false, log.DefaultLogger.Debug, &log.DefaultLogger.Debug)
294	config.EnumMapped(globals, "auth_map_normalize", true, false, authz.NormalizeFuncs, authz.NormalizeAuto, nil)
295	modconfig.Table(globals, "auth_map", true, false, nil, nil)
296	globals.AllowUnknown()
297	unknown, err := globals.Process()
298	return globals.Values, unknown, err
299}
300
301func moduleMain(cfg []config.Node) error {
302	globals, modBlocks, err := ReadGlobals(cfg)
303	if err != nil {
304		return err
305	}
306
307	if err := InitDirs(); err != nil {
308		return err
309	}
310
311	hooks.AddHook(hooks.EventLogRotate, reinitLogging)
312
313	endpoints, mods, err := RegisterModules(globals, modBlocks)
314	if err != nil {
315		return err
316	}
317
318	err = initModules(globals, endpoints, mods)
319	if err != nil {
320		return err
321	}
322
323	handleSignals()
324
325	hooks.RunHooks(hooks.EventShutdown)
326
327	return nil
328}
329
330type ModInfo struct {
331	Instance module.Module
332	Cfg      config.Node
333}
334
335func RegisterModules(globals map[string]interface{}, nodes []config.Node) (endpoints, mods []ModInfo, err error) {
336	mods = make([]ModInfo, 0, len(nodes))
337
338	for _, block := range nodes {
339		var instName string
340		var modAliases []string
341		if len(block.Args) == 0 {
342			instName = block.Name
343		} else {
344			instName = block.Args[0]
345			modAliases = block.Args[1:]
346		}
347
348		modName := block.Name
349
350		endpFactory := module.GetEndpoint(modName)
351		if endpFactory != nil {
352			inst, err := endpFactory(modName, block.Args)
353			if err != nil {
354				return nil, nil, err
355			}
356
357			endpoints = append(endpoints, ModInfo{Instance: inst, Cfg: block})
358			continue
359		}
360
361		factory := module.Get(modName)
362		if factory == nil {
363			return nil, nil, config.NodeErr(block, "unknown module or global directive: %s", modName)
364		}
365
366		if module.HasInstance(instName) {
367			return nil, nil, config.NodeErr(block, "config block named %s already exists", instName)
368		}
369
370		inst, err := factory(modName, instName, modAliases, nil)
371		if err != nil {
372			return nil, nil, err
373		}
374
375		module.RegisterInstance(inst, config.NewMap(globals, block))
376		for _, alias := range modAliases {
377			if module.HasInstance(alias) {
378				return nil, nil, config.NodeErr(block, "config block named %s already exists", alias)
379			}
380			module.RegisterAlias(alias, instName)
381		}
382
383		log.Debugf("%v:%v: register config block %v %v", block.File, block.Line, instName, modAliases)
384		mods = append(mods, ModInfo{Instance: inst, Cfg: block})
385	}
386
387	if len(endpoints) == 0 {
388		return nil, nil, fmt.Errorf("at least one endpoint should be configured")
389	}
390
391	return endpoints, mods, nil
392}
393
394func initModules(globals map[string]interface{}, endpoints, mods []ModInfo) error {
395	for _, endp := range endpoints {
396		if err := endp.Instance.Init(config.NewMap(globals, endp.Cfg)); err != nil {
397			return err
398		}
399
400		if closer, ok := endp.Instance.(io.Closer); ok {
401			endp := endp
402			hooks.AddHook(hooks.EventShutdown, func() {
403				log.Debugf("close %s (%s)", endp.Instance.Name(), endp.Instance.InstanceName())
404				if err := closer.Close(); err != nil {
405					log.Printf("module %s (%s) close failed: %v", endp.Instance.Name(), endp.Instance.InstanceName(), err)
406				}
407			})
408		}
409	}
410
411	for _, inst := range mods {
412		if module.Initialized[inst.Instance.InstanceName()] {
413			continue
414		}
415
416		return fmt.Errorf("Unused configuration block at %s:%d - %s (%s)",
417			inst.Cfg.File, inst.Cfg.Line, inst.Instance.InstanceName(), inst.Instance.Name())
418	}
419
420	return nil
421}