maddy

Fork https://github.com/foxcpp/maddy

git clone git://git.lin.moe/go/maddy.git

  1/*
  2Maddy Mail Server - Composable all-in-one email server.
  3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
  4
  5This program is free software: you can redistribute it and/or modify
  6it under the terms of the GNU General Public License as published by
  7the Free Software Foundation, either version 3 of the License, or
  8(at your option) any later version.
  9
 10This program is distributed in the hope that it will be useful,
 11but WITHOUT ANY WARRANTY; without even the implied warranty of
 12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13GNU General Public License for more details.
 14
 15You should have received a copy of the GNU General Public License
 16along with this program.  If not, see <https://www.gnu.org/licenses/>.
 17*/
 18
 19package command
 20
 21import (
 22	"bufio"
 23	"bytes"
 24	"errors"
 25	"fmt"
 26	"io"
 27	"net"
 28	"os"
 29	"os/exec"
 30	"regexp"
 31
 32	"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)
 38
 39const modName = "imap.filter.command"
 40
 41var placeholderRe = regexp.MustCompile(`{[a-zA-Z0-9_]+?}`)
 42
 43type Check struct {
 44	instName string
 45	log      log.Logger
 46
 47	cmd     string
 48	cmdArgs []string
 49}
 50
 51func (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)
 53
 54	var buf bytes.Buffer
 55	_ = textproto.WriteHeader(&buf, hdr)
 56	bR, err := body.Open()
 57	if err != nil {
 58		return "", nil, err
 59	}
 60
 61	return c.run(cmd, args, io.MultiReader(bytes.NewReader(buf.Bytes()), bR))
 62}
 63
 64func 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	}
 69
 70	if len(inlineArgs) == 0 {
 71		return nil, errors.New("command: at least one argument is required (command name)")
 72	}
 73
 74	c.cmd = inlineArgs[0]
 75	c.cmdArgs = inlineArgs[1:]
 76
 77	return c, nil
 78}
 79
 80func (c *Check) Name() string {
 81	return modName
 82}
 83
 84func (c *Check) InstanceName() string {
 85	return c.instName
 86}
 87
 88func (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	}
 93
 94	_, err := cfg.Process()
 95	return err
 96}
 97
 98func (c *Check) expandCommand(msgMeta *module.MsgMetadata, accountName string, rcptTo string, hdr textproto.Header) (string, []string) {
 99	expArgs := make([]string, len(c.cmdArgs))
100
101	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.AuthUser
109			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.Hostname
123			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.ID
137			case "{sender}":
138				return msgMeta.OriginalFrom
139			case "{rcpt_to}":
140				return rcptTo
141			case "{original_rcpt_to}":
142				oldestOriginalRcpt := rcptTo
143				for originalRcpt, ok := rcptTo, true; ok; originalRcpt, ok = msgMeta.OriginalRcpts[originalRcpt] {
144					oldestOriginalRcpt = originalRcpt
145				}
146				return oldestOriginalRcpt
147			case "{subject}":
148				return hdr.Get("Subject")
149			case "{account_name}":
150				return accountName
151			}
152			return placeholder
153		})
154	}
155
156	return c.cmd, expArgs
157}
158
159func (c *Check) run(cmdName string, args []string, stdin io.Reader) (string, []string, error) {
160	c.log.Debugln("running", cmdName, args)
161
162	cmd := exec.Command(cmdName, args...)
163	cmd.Stdin = stdin
164	stdout, err := cmd.StdoutPipe()
165	if err != nil {
166		return "", nil, err
167	}
168
169	if err := cmd.Start(); err != nil {
170		return "", nil, err
171	}
172
173	scnr := bufio.NewScanner(stdout)
174	var (
175		folder string
176		flags  []string
177	)
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, err
186	}
187
188	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 do
192			// 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, err
198	}
199
200	c.log.Debugf("folder: %s, extra flags: %v", folder, flags)
201
202	return folder, flags, nil
203}
204
205func init() {
206	module.Register(modName, New)
207}