1/*2Maddy Mail Server - Composable all-in-one email server.3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors45This program is free software: you can redistribute it and/or modify6it under the terms of the GNU General Public License as published by7the Free Software Foundation, either version 3 of the License, or8(at your option) any later version.910This program is distributed in the hope that it will be useful,11but WITHOUT ANY WARRANTY; without even the implied warranty of12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the13GNU General Public License for more details.1415You should have received a copy of the GNU General Public License16along with this program. If not, see <https://www.gnu.org/licenses/>.17*/1819package maddy2021import (22 "errors"23 "fmt"24 "io"25 "net/http"26 "os"27 "path/filepath"28 "runtime"29 "runtime/debug"3031 "github.com/caddyserver/certmagic"32 parser "github.com/foxcpp/maddy/framework/cfgparser"33 "github.com/foxcpp/maddy/framework/config"34 modconfig "github.com/foxcpp/maddy/framework/config/module"35 "github.com/foxcpp/maddy/framework/config/tls"36 "github.com/foxcpp/maddy/framework/hooks"37 "github.com/foxcpp/maddy/framework/log"38 "github.com/foxcpp/maddy/framework/module"39 "github.com/foxcpp/maddy/internal/authz"40 maddycli "github.com/foxcpp/maddy/internal/cli"41 "github.com/urfave/cli/v2"4243 // Import packages for side-effect of module registration.44 _ "github.com/foxcpp/maddy/internal/auth/dovecot_sasl"45 _ "github.com/foxcpp/maddy/internal/auth/external"46 _ "github.com/foxcpp/maddy/internal/auth/ldap"47 _ "github.com/foxcpp/maddy/internal/auth/netauth"48 _ "github.com/foxcpp/maddy/internal/auth/pam"49 _ "github.com/foxcpp/maddy/internal/auth/pass_table"50 _ "github.com/foxcpp/maddy/internal/auth/plain_separate"51 _ "github.com/foxcpp/maddy/internal/auth/shadow"52 _ "github.com/foxcpp/maddy/internal/check/authorize_sender"53 _ "github.com/foxcpp/maddy/internal/check/command"54 _ "github.com/foxcpp/maddy/internal/check/dkim"55 _ "github.com/foxcpp/maddy/internal/check/dns"56 _ "github.com/foxcpp/maddy/internal/check/dnsbl"57 _ "github.com/foxcpp/maddy/internal/check/milter"58 _ "github.com/foxcpp/maddy/internal/check/requiretls"59 _ "github.com/foxcpp/maddy/internal/check/rspamd"60 _ "github.com/foxcpp/maddy/internal/check/spf"61 _ "github.com/foxcpp/maddy/internal/endpoint/dovecot_sasld"62 _ "github.com/foxcpp/maddy/internal/endpoint/imap"63 _ "github.com/foxcpp/maddy/internal/endpoint/openmetrics"64 _ "github.com/foxcpp/maddy/internal/endpoint/smtp"65 _ "github.com/foxcpp/maddy/internal/imap_filter"66 _ "github.com/foxcpp/maddy/internal/imap_filter/command"67 _ "github.com/foxcpp/maddy/internal/libdns"68 _ "github.com/foxcpp/maddy/internal/modify"69 _ "github.com/foxcpp/maddy/internal/modify/dkim"70 _ "github.com/foxcpp/maddy/internal/storage/blob/fs"71 _ "github.com/foxcpp/maddy/internal/storage/blob/s3"72 _ "github.com/foxcpp/maddy/internal/storage/imapsql"73 _ "github.com/foxcpp/maddy/internal/table"74 _ "github.com/foxcpp/maddy/internal/target/queue"75 _ "github.com/foxcpp/maddy/internal/target/remote"76 _ "github.com/foxcpp/maddy/internal/target/smtp"77 _ "github.com/foxcpp/maddy/internal/tls"78 _ "github.com/foxcpp/maddy/internal/tls/acme"79)8081var (82 Version = "go-build"8384 enableDebugFlags = false85)8687func BuildInfo() string {88 version := Version89 if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "(devel)" {90 version = info.Main.Version91 }9293 return fmt.Sprintf(`%s %s/%s %s9495default config: %s96default state_dir: %s97default runtime_dir: %s`,98 version, runtime.GOOS, runtime.GOARCH, runtime.Version(),99 filepath.Join(ConfigDirectory, "maddy.conf"),100 DefaultStateDirectory,101 DefaultRuntimeDirectory)102}103104func init() {105 maddycli.AddGlobalFlag(106 &cli.PathFlag{107 Name: "config",108 Usage: "Configuration file to use",109 EnvVars: []string{"MADDY_CONFIG"},110 Value: filepath.Join(ConfigDirectory, "maddy.conf"),111 },112 )113 maddycli.AddGlobalFlag(&cli.BoolFlag{114 Name: "debug",115 Usage: "enable debug logging early",116 Destination: &log.DefaultLogger.Debug,117 })118 maddycli.AddSubcommand(&cli.Command{119 Name: "run",120 Usage: "Start the server",121 Flags: []cli.Flag{122 &cli.StringFlag{123 Name: "libexec",124 Value: DefaultLibexecDirectory,125 Usage: "path to the libexec directory",126 Destination: &config.LibexecDirectory,127 },128 &cli.StringSliceFlag{129 Name: "log",130 Usage: "default logging target(s)",131 Value: cli.NewStringSlice("stderr"),132 },133 &cli.BoolFlag{134 Name: "v",135 Usage: "print version and build metadata, then exit",136 Hidden: true,137 },138 },139 Action: Run,140 })141 maddycli.AddSubcommand(&cli.Command{142 Name: "version",143 Usage: "Print version and build metadata, then exit",144 Action: func(c *cli.Context) error {145 fmt.Println(BuildInfo())146 return nil147 },148 })149150 if enableDebugFlags {151 maddycli.AddGlobalFlag(&cli.StringFlag{152 Name: "debug.pprof",153 Usage: "enable live profiler HTTP endpoint and listen on the specified address",154 })155 maddycli.AddGlobalFlag(&cli.IntFlag{156 Name: "debug.blockprofrate",157 Usage: "set blocking profile rate",158 })159 maddycli.AddGlobalFlag(&cli.IntFlag{160 Name: "debug.mutexproffract",161 Usage: "set mutex profile fraction",162 })163 }164}165166// Run is the entry point for all server-running code. It takes care of command line arguments processing,167// logging initialization, directives setup, configuration reading. After all that, it168// calls moduleMain to initialize and run modules.169func Run(c *cli.Context) error {170 certmagic.UserAgent = "maddy/" + Version171172 if c.NArg() != 0 {173 return cli.Exit(fmt.Sprintln("usage:", os.Args[0], "[options]"), 2)174 }175176 if c.Bool("v") {177 fmt.Println("maddy", BuildInfo())178 return nil179 }180181 var err error182 log.DefaultLogger.Out, err = LogOutputOption(c.StringSlice("log"))183 if err != nil {184 systemdStatusErr(err)185 return cli.Exit(err.Error(), 2)186 }187188 initDebug(c)189190 os.Setenv("PATH", config.LibexecDirectory+string(filepath.ListSeparator)+os.Getenv("PATH"))191192 f, err := os.Open(c.Path("config"))193 if err != nil {194 systemdStatusErr(err)195 return cli.Exit(err.Error(), 2)196 }197 defer f.Close()198199 cfg, err := parser.Read(f, c.Path("config"))200 if err != nil {201 systemdStatusErr(err)202 return cli.Exit(err.Error(), 2)203 }204205 defer log.DefaultLogger.Out.Close()206207 if err := moduleMain(cfg); err != nil {208 systemdStatusErr(err)209 return cli.Exit(err.Error(), 1)210 }211212 return nil213}214215func initDebug(c *cli.Context) {216 if !enableDebugFlags {217 return218 }219220 if c.IsSet("debug.pprof") {221 profileEndpoint := c.String("debug.pprof")222 go func() {223 log.Println("listening on", "http://"+profileEndpoint, "for profiler requests")224 log.Println("failed to listen on profiler endpoint:", http.ListenAndServe(profileEndpoint, nil))225 }()226 }227228 // These values can also be affected by environment so set them229 // only if argument is specified.230 if c.IsSet("debug.mutexproffract") {231 runtime.SetMutexProfileFraction(c.Int("debug.mutexproffract"))232 }233 if c.IsSet("debug.blockprofrate") {234 runtime.SetBlockProfileRate(c.Int("debug.blockprofrate"))235 }236}237238func InitDirs() error {239 if config.StateDirectory == "" {240 config.StateDirectory = DefaultStateDirectory241 }242 if config.RuntimeDirectory == "" {243 config.RuntimeDirectory = DefaultRuntimeDirectory244 }245 if config.LibexecDirectory == "" {246 config.LibexecDirectory = DefaultLibexecDirectory247 }248249 if err := ensureDirectoryWritable(config.StateDirectory); err != nil {250 return err251 }252 if err := ensureDirectoryWritable(config.RuntimeDirectory); err != nil {253 return err254 }255256 // Make sure all paths we are going to use are absolute257 // before we change the working directory.258 if !filepath.IsAbs(config.StateDirectory) {259 return errors.New("statedir should be absolute")260 }261 if !filepath.IsAbs(config.RuntimeDirectory) {262 return errors.New("runtimedir should be absolute")263 }264 if !filepath.IsAbs(config.LibexecDirectory) {265 return errors.New("-libexec should be absolute")266 }267268 // Change the working directory to make all relative paths269 // in configuration relative to state directory.270 if err := os.Chdir(config.StateDirectory); err != nil {271 log.Println(err)272 }273274 return nil275}276277func ensureDirectoryWritable(path string) error {278 if err := os.MkdirAll(path, 0o700); err != nil {279 return err280 }281282 testFile, err := os.Create(filepath.Join(path, "writeable-test"))283 if err != nil {284 return err285 }286 testFile.Close()287 return os.RemoveAll(testFile.Name())288}289290func ReadGlobals(cfg []config.Node) (map[string]interface{}, []config.Node, error) {291 globals := config.NewMap(nil, config.Node{Children: cfg})292 globals.String("state_dir", false, false, DefaultStateDirectory, &config.StateDirectory)293 globals.String("runtime_dir", false, false, DefaultRuntimeDirectory, &config.RuntimeDirectory)294 globals.String("hostname", false, false, "", nil)295 globals.String("autogenerated_msg_domain", false, false, "", nil)296 globals.Custom("tls", false, false, nil, tls.TLSDirective, nil)297 globals.Custom("tls_client", false, false, nil, tls.TLSClientBlock, nil)298 globals.Bool("storage_perdomain", false, false, nil)299 globals.Bool("auth_perdomain", false, false, nil)300 globals.StringList("auth_domains", false, false, nil, nil)301 globals.Custom("log", false, false, defaultLogOutput, logOutput, &log.DefaultLogger.Out)302 globals.Bool("debug", false, log.DefaultLogger.Debug, &log.DefaultLogger.Debug)303 config.EnumMapped(globals, "auth_map_normalize", true, false, authz.NormalizeFuncs, authz.NormalizeAuto, nil)304 modconfig.Table(globals, "auth_map", true, false, nil, nil)305 globals.AllowUnknown()306 unknown, err := globals.Process()307 return globals.Values, unknown, err308}309310func moduleMain(cfg []config.Node) error {311 globals, modBlocks, err := ReadGlobals(cfg)312 if err != nil {313 return err314 }315316 if err := InitDirs(); err != nil {317 return err318 }319320 hooks.AddHook(hooks.EventLogRotate, reinitLogging)321322 endpoints, mods, err := RegisterModules(globals, modBlocks)323 if err != nil {324 return err325 }326327 err = initModules(globals, endpoints, mods)328 if err != nil {329 return err330 }331332 systemdStatus(SDReady, "Listening for incoming connections...")333334 handleSignals()335336 systemdStatus(SDStopping, "Waiting for running transactions to complete...")337338 hooks.RunHooks(hooks.EventShutdown)339340 return nil341}342343type ModInfo struct {344 Instance module.Module345 Cfg config.Node346}347348func RegisterModules(globals map[string]interface{}, nodes []config.Node) (endpoints, mods []ModInfo, err error) {349 mods = make([]ModInfo, 0, len(nodes))350351 for _, block := range nodes {352 var instName string353 var modAliases []string354 if len(block.Args) == 0 {355 instName = block.Name356 } else {357 instName = block.Args[0]358 modAliases = block.Args[1:]359 }360361 modName := block.Name362363 endpFactory := module.GetEndpoint(modName)364 if endpFactory != nil {365 inst, err := endpFactory(modName, block.Args)366 if err != nil {367 return nil, nil, err368 }369370 endpoints = append(endpoints, ModInfo{Instance: inst, Cfg: block})371 continue372 }373374 factory := module.Get(modName)375 if factory == nil {376 return nil, nil, config.NodeErr(block, "unknown module or global directive: %s", modName)377 }378379 if module.HasInstance(instName) {380 return nil, nil, config.NodeErr(block, "config block named %s already exists", instName)381 }382383 inst, err := factory(modName, instName, modAliases, nil)384 if err != nil {385 return nil, nil, err386 }387388 module.RegisterInstance(inst, config.NewMap(globals, block))389 for _, alias := range modAliases {390 if module.HasInstance(alias) {391 return nil, nil, config.NodeErr(block, "config block named %s already exists", alias)392 }393 module.RegisterAlias(alias, instName)394 }395396 log.Debugf("%v:%v: register config block %v %v", block.File, block.Line, instName, modAliases)397 mods = append(mods, ModInfo{Instance: inst, Cfg: block})398 }399400 if len(endpoints) == 0 {401 return nil, nil, fmt.Errorf("at least one endpoint should be configured")402 }403404 return endpoints, mods, nil405}406407func initModules(globals map[string]interface{}, endpoints, mods []ModInfo) error {408 for _, endp := range endpoints {409 if err := endp.Instance.Init(config.NewMap(globals, endp.Cfg)); err != nil {410 return err411 }412413 if closer, ok := endp.Instance.(io.Closer); ok {414 endp := endp415 hooks.AddHook(hooks.EventShutdown, func() {416 log.Debugf("close %s (%s)", endp.Instance.Name(), endp.Instance.InstanceName())417 if err := closer.Close(); err != nil {418 log.Printf("module %s (%s) close failed: %v", endp.Instance.Name(), endp.Instance.InstanceName(), err)419 }420 })421 }422 }423424 for _, inst := range mods {425 if module.Initialized[inst.Instance.InstanceName()] {426 continue427 }428429 return fmt.Errorf("Unused configuration block at %s:%d - %s (%s)",430 inst.Cfg.File, inst.Cfg.Line, inst.Instance.InstanceName(), inst.Instance.Name())431 }432433 return nil434}