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 command2021import (22 "bufio"23 "bytes"24 "errors"25 "fmt"26 "io"27 "net"28 "os"29 "os/exec"30 "regexp"3132 "github.com/emersion/go-message/textproto"33 "github.com/foxcpp/maddy/framework/buffer"34 "github.com/foxcpp/maddy/framework/config"35 "github.com/foxcpp/maddy/framework/log"36 "github.com/foxcpp/maddy/framework/module"37)3839const modName = "imap.filter.command"4041var placeholderRe = regexp.MustCompile(`{[a-zA-Z0-9_]+?}`)4243type Check struct {44 instName string45 log log.Logger4647 cmd string48 cmdArgs []string49}5051func (c *Check) IMAPFilter(accountName string, rcptTo string, msgMeta *module.MsgMetadata, hdr textproto.Header, body buffer.Buffer) (folder string, flags []string, err error) {52 cmd, args := c.expandCommand(msgMeta, accountName, rcptTo, hdr)5354 var buf bytes.Buffer55 _ = textproto.WriteHeader(&buf, hdr)56 bR, err := body.Open()57 if err != nil {58 return "", nil, err59 }6061 return c.run(cmd, args, io.MultiReader(bytes.NewReader(buf.Bytes()), bR))62}6364func New(_, instName string, _, inlineArgs []string) (module.Module, error) {65 c := &Check{66 instName: instName,67 log: log.Logger{Name: modName, Debug: log.DefaultLogger.Debug},68 }6970 if len(inlineArgs) == 0 {71 return nil, errors.New("command: at least one argument is required (command name)")72 }7374 c.cmd = inlineArgs[0]75 c.cmdArgs = inlineArgs[1:]7677 return c, nil78}7980func (c *Check) Name() string {81 return modName82}8384func (c *Check) InstanceName() string {85 return c.instName86}8788func (c *Check) Init(cfg *config.Map) error {89 // Check whether the inline argument command is usable.90 if _, err := exec.LookPath(c.cmd); err != nil {91 return fmt.Errorf("command: %w", err)92 }9394 _, err := cfg.Process()95 return err96}9798func (c *Check) expandCommand(msgMeta *module.MsgMetadata, accountName string, rcptTo string, hdr textproto.Header) (string, []string) {99 expArgs := make([]string, len(c.cmdArgs))100101 for i, arg := range c.cmdArgs {102 expArgs[i] = placeholderRe.ReplaceAllStringFunc(arg, func(placeholder string) string {103 switch placeholder {104 case "{auth_user}":105 if msgMeta.Conn == nil {106 return ""107 }108 return msgMeta.Conn.AuthUser109 case "{source_ip}":110 if msgMeta.Conn == nil {111 return ""112 }113 tcpAddr, _ := msgMeta.Conn.RemoteAddr.(*net.TCPAddr)114 if tcpAddr == nil {115 return ""116 }117 return tcpAddr.IP.String()118 case "{source_host}":119 if msgMeta.Conn == nil {120 return ""121 }122 return msgMeta.Conn.Hostname123 case "{source_rdns}":124 if msgMeta.Conn == nil {125 return ""126 }127 valI, err := msgMeta.Conn.RDNSName.Get()128 if err != nil {129 return ""130 }131 if valI == nil {132 return ""133 }134 return valI.(string)135 case "{msg_id}":136 return msgMeta.ID137 case "{sender}":138 return msgMeta.OriginalFrom139 case "{rcpt_to}":140 return rcptTo141 case "{original_rcpt_to}":142 oldestOriginalRcpt := rcptTo143 for originalRcpt, ok := rcptTo, true; ok; originalRcpt, ok = msgMeta.OriginalRcpts[originalRcpt] {144 oldestOriginalRcpt = originalRcpt145 }146 return oldestOriginalRcpt147 case "{subject}":148 return hdr.Get("Subject")149 case "{account_name}":150 return accountName151 }152 return placeholder153 })154 }155156 return c.cmd, expArgs157}158159func (c *Check) run(cmdName string, args []string, stdin io.Reader) (string, []string, error) {160 c.log.Debugln("running", cmdName, args)161162 cmd := exec.Command(cmdName, args...)163 cmd.Stdin = stdin164 stdout, err := cmd.StdoutPipe()165 if err != nil {166 return "", nil, err167 }168169 if err := cmd.Start(); err != nil {170 return "", nil, err171 }172173 scnr := bufio.NewScanner(stdout)174 var (175 folder string176 flags []string177 )178 if scnr.Scan() {179 folder = scnr.Text()180 }181 for scnr.Scan() {182 flags = append(flags, scnr.Text())183 }184 if err := scnr.Err(); err != nil {185 return "", nil, err186 }187188 err = cmd.Wait()189 if err != nil {190 if _, ok := err.(*exec.ExitError); !ok {191 // If that's not ExitError, the process may still be running. We do192 // not want this.193 if err := cmd.Process.Signal(os.Interrupt); err != nil {194 c.log.Error("failed to kill process", err)195 }196 }197 return "", nil, err198 }199200 c.log.Debugf("folder: %s, extra flags: %v", folder, flags)201202 return folder, flags, nil203}204205func init() {206 module.Register(modName, New)207}