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 table2021import (22 "bufio"23 "context"24 "fmt"25 "os"26 "runtime/debug"27 "strings"28 "sync"29 "time"3031 "github.com/foxcpp/maddy/framework/config"32 "github.com/foxcpp/maddy/framework/hooks"33 "github.com/foxcpp/maddy/framework/log"34 "github.com/foxcpp/maddy/framework/module"35)3637const FileModName = "table.file"3839type File struct {40 instName string41 file string4243 m map[string][]string44 mLck sync.RWMutex45 mStamp time.Time4647 stopReloader chan struct{}48 forceReload chan struct{}4950 log log.Logger51}5253func NewFile(_, instName string, _, inlineArgs []string) (module.Module, error) {54 m := &File{55 instName: instName,56 m: make(map[string][]string),57 stopReloader: make(chan struct{}),58 forceReload: make(chan struct{}),59 log: log.Logger{Name: FileModName},60 }6162 switch len(inlineArgs) {63 case 1:64 m.file = inlineArgs[0]65 case 0:66 default:67 return nil, fmt.Errorf("%s: cannot use multiple files with single %s, use %s multiple times to do so", FileModName, FileModName, FileModName)68 }6970 return m, nil71}7273func (f *File) Name() string {74 return FileModName75}7677func (f *File) InstanceName() string {78 return f.instName79}8081func (f *File) Init(cfg *config.Map) error {82 var file string83 cfg.Bool("debug", true, false, &f.log.Debug)84 cfg.String("file", false, false, "", &file)85 if _, err := cfg.Process(); err != nil {86 return err87 }8889 if file != "" {90 if f.file != "" {91 return fmt.Errorf("%s: file path specified both in directive and in argument, do it once", FileModName)92 }93 f.file = file94 }9596 if err := readFile(f.file, f.m); err != nil {97 if !os.IsNotExist(err) {98 return err99 }100 f.log.Printf("ignoring non-existent file: %s", f.file)101 }102103 go f.reloader()104 hooks.AddHook(hooks.EventReload, func() {105 f.forceReload <- struct{}{}106 })107108 return nil109}110111var reloadInterval = 15 * time.Second112113func (f *File) reloader() {114 defer func() {115 if err := recover(); err != nil {116 stack := debug.Stack()117 log.Printf("panic during m reload: %v\n%s", err, stack)118 }119 }()120121 t := time.NewTicker(reloadInterval)122 defer t.Stop()123124 for {125 select {126 case <-t.C:127 f.reload()128129 case <-f.forceReload:130 f.reload()131132 case <-f.stopReloader:133 f.stopReloader <- struct{}{}134 return135 }136 }137}138139func (f *File) reload() {140 info, err := os.Stat(f.file)141 if err != nil {142 if os.IsNotExist(err) {143 f.mLck.Lock()144 f.m = map[string][]string{}145 f.mLck.Unlock()146 return147 }148 f.log.Error("os stat", err)149 }150 if info.ModTime().Before(f.mStamp) || time.Since(info.ModTime()) < (reloadInterval/2) {151 return // reload not necessary152 }153154 f.log.Debugf("reloading")155156 newm := make(map[string][]string, len(f.m)+5)157 if err := readFile(f.file, newm); err != nil {158 if os.IsNotExist(err) {159 f.log.Printf("ignoring non-existent file: %s", f.file)160 return161 }162163 f.log.Println(err)164 return165 }166 // after reading we need to check whether file has changed in between167 info2, err := os.Stat(f.file)168 if err != nil {169 f.log.Println(err)170 return171 }172173 if !info2.ModTime().Equal(info.ModTime()) {174 // file has changed in the meantime175 return176 }177178 f.mLck.Lock()179 f.m = newm180 f.mStamp = info.ModTime()181 f.mLck.Unlock()182}183184func (f *File) Close() error {185 f.stopReloader <- struct{}{}186 <-f.stopReloader187 return nil188}189190func readFile(path string, out map[string][]string) error {191 f, err := os.Open(path)192 if err != nil {193 return err194 }195196 scnr := bufio.NewScanner(f)197 lineCounter := 0198199 parseErr := func(text string) error {200 return fmt.Errorf("%s:%d: %s", path, lineCounter, text)201 }202203 for scnr.Scan() {204 lineCounter++205 if strings.HasPrefix(scnr.Text(), "#") {206 continue207 }208209 text := strings.TrimSpace(scnr.Text())210 if text == "" {211 continue212 }213214 parts := strings.SplitN(text, ":", 2)215 if len(parts) == 1 {216 parts = append(parts, "")217 }218219 from := strings.TrimSpace(parts[0])220 if len(from) == 0 {221 return parseErr("empty address before colon")222 }223224 for _, to := range strings.Split(parts[1], ",") {225 to := strings.TrimSpace(to)226 out[from] = append(out[from], to)227 }228 }229 return scnr.Err()230}231232func (f *File) Lookup(_ context.Context, val string) (string, bool, error) {233 // The existing map is never modified, instead it is replaced with a new234 // one if reload is performed.235 f.mLck.RLock()236 usedFile := f.m237 f.mLck.RUnlock()238239 newVal, ok := usedFile[val]240241 if len(newVal) == 0 {242 return "", false, nil243 }244245 return newVal[0], ok, nil246}247248func (f *File) LookupMulti(_ context.Context, val string) ([]string, error) {249 f.mLck.RLock()250 usedFile := f.m251 f.mLck.RUnlock()252253 return usedFile[val], nil254}255256func init() {257 module.Register(FileModName, NewFile)258}