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 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"4142 // 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)7778var (79 Version = "go-build"8081 enableDebugFlags = false82)8384func BuildInfo() string {85 version := Version86 if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "(devel)" {87 version = info.Main.Version88 }8990 return fmt.Sprintf(`%s %s/%s %s9192default config: %s93default state_dir: %s94default runtime_dir: %s`,95 version, runtime.GOOS, runtime.GOARCH, runtime.Version(),96 filepath.Join(ConfigDirectory, "maddy.conf"),97 DefaultStateDirectory,98 DefaultRuntimeDirectory)99}100101func 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 nil144 },145 })146147 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}162163// 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, it165// 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 }170171 if c.Bool("v") {172 fmt.Println("maddy", BuildInfo())173 return nil174 }175176 var err error177 log.DefaultLogger.Out, err = LogOutputOption(c.StringSlice("log"))178 if err != nil {179 return cli.Exit(err.Error(), 2)180 }181182 initDebug(c)183184 os.Setenv("PATH", config.LibexecDirectory+string(filepath.ListSeparator)+os.Getenv("PATH"))185186 f, err := os.Open(c.Path("config"))187 if err != nil {188 return cli.Exit(err.Error(), 2)189 }190 defer f.Close()191192 cfg, err := parser.Read(f, c.Path("config"))193 if err != nil {194 return cli.Exit(err.Error(), 2)195 }196197 defer log.DefaultLogger.Out.Close()198199 if err := moduleMain(cfg); err != nil {200 return cli.Exit(err.Error(), 1)201 }202203 return nil204}205206func initDebug(c *cli.Context) {207 if !enableDebugFlags {208 return209 }210211 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 }218219 // These values can also be affected by environment so set them220 // 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}228229func InitDirs() error {230 if config.StateDirectory == "" {231 config.StateDirectory = DefaultStateDirectory232 }233 if config.RuntimeDirectory == "" {234 config.RuntimeDirectory = DefaultRuntimeDirectory235 }236 if config.LibexecDirectory == "" {237 config.LibexecDirectory = DefaultLibexecDirectory238 }239240 if err := ensureDirectoryWritable(config.StateDirectory); err != nil {241 return err242 }243 if err := ensureDirectoryWritable(config.RuntimeDirectory); err != nil {244 return err245 }246247 // Make sure all paths we are going to use are absolute248 // 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 }258259 // Change the working directory to make all relative paths260 // in configuration relative to state directory.261 if err := os.Chdir(config.StateDirectory); err != nil {262 log.Println(err)263 }264265 return nil266}267268func ensureDirectoryWritable(path string) error {269 if err := os.MkdirAll(path, 0o700); err != nil {270 return err271 }272273 testFile, err := os.Create(filepath.Join(path, "writeable-test"))274 if err != nil {275 return err276 }277 testFile.Close()278 return os.RemoveAll(testFile.Name())279}280281func 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, err299}300301func moduleMain(cfg []config.Node) error {302 globals, modBlocks, err := ReadGlobals(cfg)303 if err != nil {304 return err305 }306307 if err := InitDirs(); err != nil {308 return err309 }310311 hooks.AddHook(hooks.EventLogRotate, reinitLogging)312313 endpoints, mods, err := RegisterModules(globals, modBlocks)314 if err != nil {315 return err316 }317318 err = initModules(globals, endpoints, mods)319 if err != nil {320 return err321 }322323 handleSignals()324325 hooks.RunHooks(hooks.EventShutdown)326327 return nil328}329330type ModInfo struct {331 Instance module.Module332 Cfg config.Node333}334335func RegisterModules(globals map[string]interface{}, nodes []config.Node) (endpoints, mods []ModInfo, err error) {336 mods = make([]ModInfo, 0, len(nodes))337338 for _, block := range nodes {339 var instName string340 var modAliases []string341 if len(block.Args) == 0 {342 instName = block.Name343 } else {344 instName = block.Args[0]345 modAliases = block.Args[1:]346 }347348 modName := block.Name349350 endpFactory := module.GetEndpoint(modName)351 if endpFactory != nil {352 inst, err := endpFactory(modName, block.Args)353 if err != nil {354 return nil, nil, err355 }356357 endpoints = append(endpoints, ModInfo{Instance: inst, Cfg: block})358 continue359 }360361 factory := module.Get(modName)362 if factory == nil {363 return nil, nil, config.NodeErr(block, "unknown module or global directive: %s", modName)364 }365366 if module.HasInstance(instName) {367 return nil, nil, config.NodeErr(block, "config block named %s already exists", instName)368 }369370 inst, err := factory(modName, instName, modAliases, nil)371 if err != nil {372 return nil, nil, err373 }374375 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 }382383 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 }386387 if len(endpoints) == 0 {388 return nil, nil, fmt.Errorf("at least one endpoint should be configured")389 }390391 return endpoints, mods, nil392}393394func 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 err398 }399400 if closer, ok := endp.Instance.(io.Closer); ok {401 endp := endp402 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 }410411 for _, inst := range mods {412 if module.Initialized[inst.Instance.InstanceName()] {413 continue414 }415416 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 }419420 return nil421}