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*/1819// Package modconfig provides matchers for config.Map that query20// modules registry and parse inline module definitions.21//22// They should be used instead of manual querying when there is need to23// reference a module instance in the configuration.24//25// See ModuleFromNode documentation for explanation of what is 'args'26// for some functions (DeliveryTarget).27package modconfig2829import (30 "fmt"31 "io"32 "reflect"33 "strings"3435 parser "github.com/foxcpp/maddy/framework/cfgparser"36 "github.com/foxcpp/maddy/framework/config"37 "github.com/foxcpp/maddy/framework/hooks"38 "github.com/foxcpp/maddy/framework/log"39 "github.com/foxcpp/maddy/framework/module"40)4142// createInlineModule is a helper function for config matchers that can create inline modules.43func createInlineModule(preferredNamespace, modName string, args []string) (module.Module, error) {44 var newMod module.FuncNewModule45 originalModName := modName4647 // First try to extend the name with preferred namespace unless the name48 // already contains it.49 if !strings.Contains(modName, ".") && preferredNamespace != "" {50 modName = preferredNamespace + "." + modName51 newMod = module.Get(modName)52 }5354 // Then try global namespace for compatibility and complex modules.55 if newMod == nil {56 newMod = module.Get(originalModName)57 }5859 // Bail if both failed.60 if newMod == nil {61 return nil, fmt.Errorf("unknown module: %s (namespace: %s)", originalModName, preferredNamespace)62 }6364 return newMod(modName, "", nil, args)65}6667// initInlineModule constructs "faked" config tree and passes it to module68// Init function to make it look like it is defined at top-level.69//70// args must contain at least one argument, otherwise initInlineModule panics.71func initInlineModule(modObj module.Module, globals map[string]interface{}, block config.Node) error {72 err := modObj.Init(config.NewMap(globals, block))73 if err != nil {74 return err75 }7677 if closer, ok := modObj.(io.Closer); ok {78 hooks.AddHook(hooks.EventShutdown, func() {79 log.Debugf("close %s (%s)", modObj.Name(), modObj.InstanceName())80 if err := closer.Close(); err != nil {81 log.Printf("module %s (%s) close failed: %v", modObj.Name(), modObj.InstanceName(), err)82 }83 })84 }8586 return nil87}8889// ModuleFromNode does all work to create or get existing module object with a certain type.90// It is not used by top-level module definitions, only for references from other91// modules configuration blocks.92//93// inlineCfg should contain configuration directives for inline declarations.94// args should contain values that are used to create module.95// It should be either module name + instance name or just module name. Further extensions96// may add other string arguments (currently, they can be accessed by module instances97// as inlineArgs argument to constructor).98//99// It checks using reflection whether it is possible to store a module object into modObj100// pointer (e.g. it implements all necessary interfaces) and stores it if everything is fine.101// If module object doesn't implement necessary module interfaces - error is returned.102// If modObj is not a pointer, ModuleFromNode panics.103//104// preferredNamespace is used as an implicit prefix for module name lookups.105// Module with name preferredNamespace + "." + args[0] will be preferred over just args[0].106// It can be omitted.107func ModuleFromNode(preferredNamespace string, args []string, inlineCfg config.Node, globals map[string]interface{}, moduleIface interface{}) error {108 if len(args) == 0 {109 return parser.NodeErr(inlineCfg, "at least one argument is required")110 }111112 referenceExisting := strings.HasPrefix(args[0], "&")113114 var modObj module.Module115 var err error116 if referenceExisting {117 if len(args) != 1 || inlineCfg.Children != nil {118 return parser.NodeErr(inlineCfg, "exactly one argument is required to use existing config block")119 }120 modObj, err = module.GetInstance(args[0][1:])121 log.Debugf("%s:%d: reference %s", inlineCfg.File, inlineCfg.Line, args[0])122 } else {123 log.Debugf("%s:%d: new module %s %v", inlineCfg.File, inlineCfg.Line, args[0], args[1:])124 modObj, err = createInlineModule(preferredNamespace, args[0], args[1:])125 }126 if err != nil {127 return err128 }129130 // NOTE: This will panic if moduleIface is not a pointer.131 modIfaceType := reflect.TypeOf(moduleIface).Elem()132 modObjType := reflect.TypeOf(modObj)133134 if modIfaceType.Kind() == reflect.Interface {135 // Case for assignment to module interface type.136 if !modObjType.Implements(modIfaceType) && !modObjType.AssignableTo(modIfaceType) {137 return parser.NodeErr(inlineCfg, "module %s (%s) doesn't implement %v interface", modObj.Name(), modObj.InstanceName(), modIfaceType)138 }139 } else if !modObjType.AssignableTo(modIfaceType) {140 // Case for assignment to concrete module type. Used in "module groups".141 return parser.NodeErr(inlineCfg, "module %s (%s) is not %v", modObj.Name(), modObj.InstanceName(), modIfaceType)142 }143144 reflect.ValueOf(moduleIface).Elem().Set(reflect.ValueOf(modObj))145146 if !referenceExisting {147 if err := initInlineModule(modObj, globals, inlineCfg); err != nil {148 return err149 }150 }151152 return nil153}154155// GroupFromNode provides a special kind of ModuleFromNode syntax that allows156// to omit the module name when defining inine configuration. If it is not157// present, name in defaultModule is used.158func GroupFromNode(defaultModule string, args []string, inlineCfg config.Node, globals map[string]interface{}, moduleIface interface{}) error {159 if len(args) == 0 {160 args = append(args, defaultModule)161 }162 return ModuleFromNode("", args, inlineCfg, globals, moduleIface)163}