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 rspamd2021import (22 "bytes"23 "context"24 "crypto/tls"25 "encoding/json"26 "fmt"27 "io"28 "net"29 "net/http"30 "strconv"31 "strings"3233 "github.com/emersion/go-message/textproto"34 "github.com/foxcpp/maddy/framework/buffer"35 "github.com/foxcpp/maddy/framework/config"36 modconfig "github.com/foxcpp/maddy/framework/config/module"37 tls2 "github.com/foxcpp/maddy/framework/config/tls"38 "github.com/foxcpp/maddy/framework/exterrors"39 "github.com/foxcpp/maddy/framework/log"40 "github.com/foxcpp/maddy/framework/module"41 "github.com/foxcpp/maddy/internal/target"42)4344const modName = "check.rspamd"4546type Check struct {47 instName string48 log log.Logger4950 apiPath string51 flags string52 settingsID string53 tag string54 mtaName string5556 ioErrAction modconfig.FailAction57 errorRespAction modconfig.FailAction58 addHdrAction modconfig.FailAction59 rewriteSubjAction modconfig.FailAction6061 client *http.Client62}6364func New(modName, instName string, _, inlineArgs []string) (module.Module, error) {65 c := &Check{66 instName: instName,67 client: http.DefaultClient,68 log: log.Logger{Name: modName, Debug: log.DefaultLogger.Debug},69 }7071 switch len(inlineArgs) {72 case 1:73 c.apiPath = inlineArgs[0]74 case 0:75 c.apiPath = "http://127.0.0.1:11333"76 default:77 return nil, fmt.Errorf("%s: unexpected amount of inline arguments", modName)78 }7980 return c, nil81}8283func (c *Check) Name() string {84 return modName85}8687func (c *Check) InstanceName() string {88 return c.instName89}9091func (c *Check) Init(cfg *config.Map) error {92 var (93 tlsConfig tls.Config94 flags []string95 )9697 cfg.Custom("tls_client", true, false, func() (interface{}, error) {98 return tls.Config{}, nil99 }, tls2.TLSClientBlock, &tlsConfig)100 cfg.String("api_path", false, false, c.apiPath, &c.apiPath)101 cfg.String("settings_id", false, false, "", &c.settingsID)102 cfg.String("tag", false, false, "maddy", &c.tag)103 cfg.String("hostname", true, false, "", &c.mtaName)104 cfg.Custom("io_error_action", false, false,105 func() (interface{}, error) {106 return modconfig.FailAction{}, nil107 }, modconfig.FailActionDirective, &c.ioErrAction)108 cfg.Custom("error_resp_action", false, false,109 func() (interface{}, error) {110 return modconfig.FailAction{}, nil111 }, modconfig.FailActionDirective, &c.errorRespAction)112 cfg.Custom("add_header_action", false, false,113 func() (interface{}, error) {114 return modconfig.FailAction{Quarantine: true}, nil115 }, modconfig.FailActionDirective, &c.addHdrAction)116 cfg.Custom("rewrite_subj_action", false, false,117 func() (interface{}, error) {118 return modconfig.FailAction{Quarantine: true}, nil119 }, modconfig.FailActionDirective, &c.rewriteSubjAction)120 cfg.StringList("flags", false, false, []string{"pass_all"}, &flags)121 if _, err := cfg.Process(); err != nil {122 return err123 }124125 c.client = &http.Client{126 Transport: &http.Transport{127 TLSClientConfig: &tlsConfig,128 },129 }130 c.flags = strings.Join(flags, ",")131132 return nil133}134135type state struct {136 c *Check137 msgMeta *module.MsgMetadata138 log log.Logger139140 mailFrom string141 rcpt []string142}143144func (c *Check) CheckStateForMsg(ctx context.Context, msgMeta *module.MsgMetadata) (module.CheckState, error) {145 return &state{146 c: c,147 msgMeta: msgMeta,148 log: target.DeliveryLogger(c.log, msgMeta),149 }, nil150}151152func (s *state) CheckConnection(ctx context.Context) module.CheckResult {153 return module.CheckResult{}154}155156func (s *state) CheckSender(ctx context.Context, addr string) module.CheckResult {157 s.mailFrom = addr158 return module.CheckResult{}159}160161func (s *state) CheckRcpt(ctx context.Context, addr string) module.CheckResult {162 s.rcpt = append(s.rcpt, addr)163 return module.CheckResult{}164}165166func addConnHeaders(r *http.Request, meta *module.MsgMetadata, mailFrom string, rcpts []string) {167 r.Header.Add("From", mailFrom)168 for _, rcpt := range rcpts {169 r.Header.Add("Rcpt", rcpt)170 }171172 r.Header.Add("Queue-ID", meta.ID)173174 conn := meta.Conn175 if conn != nil {176 if meta.Conn.AuthUser != "" {177 r.Header.Add("User", meta.Conn.AuthUser)178 }179180 if tcpAddr, ok := conn.RemoteAddr.(*net.TCPAddr); ok {181 r.Header.Add("IP", tcpAddr.IP.String())182 }183 r.Header.Add("Helo", conn.Hostname)184 name, err := conn.RDNSName.Get()185 if err == nil && name != nil {186 r.Header.Add("Hostname", name.(string))187 }188189 if conn.TLS.HandshakeComplete {190 r.Header.Add("TLS-Cipher", tls.CipherSuiteName(conn.TLS.CipherSuite))191 switch conn.TLS.Version {192 case tls.VersionTLS13:193 r.Header.Add("TLS-Version", "1.3")194 case tls.VersionTLS12:195 r.Header.Add("TLS-Version", "1.2")196 case tls.VersionTLS11:197 r.Header.Add("TLS-Version", "1.1")198 case tls.VersionTLS10:199 r.Header.Add("TLS-Version", "1.0")200 }201 }202 }203}204205func (s *state) CheckBody(ctx context.Context, hdr textproto.Header, body buffer.Buffer) module.CheckResult {206 bodyR, err := body.Open()207 if err != nil {208 return module.CheckResult{209 Reject: true,210 Reason: exterrors.WithFields(err, map[string]interface{}{"check": modName}),211 }212 }213214 var buf bytes.Buffer215 if err := textproto.WriteHeader(&buf, hdr); err != nil {216 return module.CheckResult{217 Reject: true,218 Reason: exterrors.WithFields(err, map[string]interface{}{"check": modName}),219 }220 }221222 r, err := http.NewRequest("POST", s.c.apiPath+"/checkv2", io.MultiReader(&buf, bodyR))223 if err != nil {224 return module.CheckResult{225 Reject: true,226 Reason: exterrors.WithFields(err, map[string]interface{}{"check": modName}),227 }228 }229230 r.Header.Add("Pass", "all") // TODO: does that need to be configurable?231 // TODO: include version (needs maddy.Version moved somewhere to break circular dependency)232 r.Header.Add("User-Agent", "maddy")233 if s.c.tag != "" {234 r.Header.Add("MTA-Tag", s.c.tag)235 }236 if s.c.settingsID != "" {237 r.Header.Add("Settings-ID", s.c.settingsID)238 }239 if s.c.mtaName != "" {240 r.Header.Add("MTA-Name", s.c.mtaName)241 }242243 addConnHeaders(r, s.msgMeta, s.mailFrom, s.rcpt)244 r.Header.Add("Content-Length", strconv.Itoa(body.Len()))245246 resp, err := s.c.client.Do(r)247 if err != nil {248 return s.c.ioErrAction.Apply(module.CheckResult{249 Reason: &exterrors.SMTPError{250 Code: 451,251 EnhancedCode: exterrors.EnhancedCode{4, 7, 0},252 Message: "Internal error during policy check",253 CheckName: modName,254 Err: err,255 },256 })257 }258 if resp.StatusCode/100 != 2 {259 return s.c.errorRespAction.Apply(module.CheckResult{260 Reason: &exterrors.SMTPError{261 Code: 451,262 EnhancedCode: exterrors.EnhancedCode{4, 7, 0},263 Message: "Internal error during policy check",264 CheckName: modName,265 Err: fmt.Errorf("HTTP %d", resp.StatusCode),266 },267 })268 }269 defer resp.Body.Close()270271 var respData response272 if err := json.NewDecoder(resp.Body).Decode(&respData); err != nil {273 return s.c.ioErrAction.Apply(module.CheckResult{274 Reason: &exterrors.SMTPError{275 Code: 451,276 EnhancedCode: exterrors.EnhancedCode{4, 9, 0},277 Message: "Internal error during policy check",278 CheckName: modName,279 Err: err,280 },281 })282 }283284 switch respData.Action {285 case "no action":286 return module.CheckResult{}287 case "greylist":288 // uuh... TODO: Implement greylisting?289 hdrAdd := textproto.Header{}290 hdrAdd.Add("X-Spam-Score", strconv.FormatFloat(respData.Score, 'f', 2, 64))291 return module.CheckResult{292 Header: hdrAdd,293 }294 case "add header":295 hdrAdd := textproto.Header{}296 hdrAdd.Add("X-Spam-Flag", "Yes")297 hdrAdd.Add("X-Spam-Score", strconv.FormatFloat(respData.Score, 'f', 2, 64))298 return s.c.addHdrAction.Apply(module.CheckResult{299 Reason: &exterrors.SMTPError{300 Code: 450,301 EnhancedCode: exterrors.EnhancedCode{4, 7, 0},302 Message: "Message rejected due to local policy",303 CheckName: modName,304 Misc: map[string]interface{}{"action": "add header"},305 },306 Header: hdrAdd,307 })308 case "rewrite subject":309 hdrAdd := textproto.Header{}310 hdrAdd.Add("X-Spam-Flag", "Yes")311 hdrAdd.Add("X-Spam-Score", strconv.FormatFloat(respData.Score, 'f', 2, 64))312 return s.c.rewriteSubjAction.Apply(module.CheckResult{313 Reason: &exterrors.SMTPError{314 Code: 450,315 EnhancedCode: exterrors.EnhancedCode{4, 7, 0},316 Message: "Message rejected due to local policy",317 CheckName: modName,318 Misc: map[string]interface{}{"action": "rewrite subject"},319 },320 Header: hdrAdd,321 })322 case "soft reject":323 return module.CheckResult{324 Reject: true,325 Reason: &exterrors.SMTPError{326 Code: 450,327 EnhancedCode: exterrors.EnhancedCode{4, 7, 0},328 Message: "Message rejected due to local policy",329 CheckName: modName,330 Misc: map[string]interface{}{"action": "soft reject"},331 },332 }333 case "reject":334 return module.CheckResult{335 Reject: true,336 Reason: &exterrors.SMTPError{337 Code: 550,338 EnhancedCode: exterrors.EnhancedCode{5, 7, 0},339 Message: "Message rejected due to local policy",340 CheckName: modName,341 Misc: map[string]interface{}{"action": "reject"},342 },343 }344 }345346 s.log.Msg("unhandled action", "action", respData.Action)347348 return module.CheckResult{}349}350351type response struct {352 Score float64 `json:"score"`353 Action string `json:"action"`354 Subject string `json:"subject"`355 Symbols map[string]struct {356 Name string `json:"name"`357 Score float64 `json:"score"`358 }359}360361func (s *state) Close() error {362 return nil363}364365func init() {366 module.Register(modName, New)367}