mirror of
https://github.com/axllent/mailpit.git
synced 2025-03-21 21:47:19 +02:00
246 lines
5.4 KiB
Go
246 lines
5.4 KiB
Go
// Package spamc provides a client for the SpamAssassin spamd protocol.
|
|
// http://svn.apache.org/repos/asf/spamassassin/trunk/spamd/PROTOCOL
|
|
//
|
|
// Modified to add timeouts from https://github.com/cgt/spamc
|
|
package spamc
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// ProtoVersion is the protocol version
|
|
const ProtoVersion = "1.5"
|
|
|
|
var (
|
|
spamInfoRe = regexp.MustCompile(`(.+)\/(.+) (\d+) (.+)`)
|
|
spamMainRe = regexp.MustCompile(`^Spam: (.+) ; (.+) . (.+)$`)
|
|
spamDetailsRe = regexp.MustCompile(`^\s?(-?[0-9\.]+)\s([a-zA-Z0-9_]*)(\W*)(.*)`)
|
|
)
|
|
|
|
// connection is like net.Conn except that it also has a CloseWrite method.
|
|
// CloseWrite is implemented by net.TCPConn and net.UnixConn, but for some
|
|
// reason it is not present in the net.Conn interface.
|
|
type connection interface {
|
|
net.Conn
|
|
CloseWrite() error
|
|
}
|
|
|
|
// Client is a spamd client.
|
|
type Client struct {
|
|
net string
|
|
addr string
|
|
timeout int
|
|
}
|
|
|
|
// NewTCP returns a *Client that connects to spamd via the given TCP address.
|
|
func NewTCP(addr string, timeout int) *Client {
|
|
return &Client{"tcp", addr, timeout}
|
|
}
|
|
|
|
// NewUnix returns a *Client that connects to spamd via the given Unix socket.
|
|
func NewUnix(addr string) *Client {
|
|
return &Client{"unix", addr, 0}
|
|
}
|
|
|
|
// Rule represents a matched SpamAssassin rule.
|
|
type Rule struct {
|
|
Points string
|
|
Name string
|
|
Description string
|
|
}
|
|
|
|
// Result struct
|
|
type Result struct {
|
|
ResponseCode int
|
|
Message string
|
|
Spam bool
|
|
Score float64
|
|
Threshold float64
|
|
Rules []Rule
|
|
}
|
|
|
|
// dial connects to spamd through TCP or a Unix socket.
|
|
func (c *Client) dial() (connection, error) {
|
|
if c.net == "tcp" {
|
|
tcpAddr, err := net.ResolveTCPAddr("tcp", c.addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return net.DialTCP("tcp", nil, tcpAddr)
|
|
} else if c.net == "unix" {
|
|
unixAddr, err := net.ResolveUnixAddr("unix", c.addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return net.DialUnix("unix", nil, unixAddr)
|
|
}
|
|
panic("Client.net must be either \"tcp\" or \"unix\"")
|
|
}
|
|
|
|
// Report checks if message is spam or not, and returns score plus report
|
|
func (c *Client) Report(email []byte) (Result, error) {
|
|
output, err := c.report(email)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
|
|
return c.parseOutput(output), nil
|
|
}
|
|
|
|
func (c *Client) report(email []byte) ([]string, error) {
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer conn.Close()
|
|
|
|
if err := conn.SetDeadline(time.Now().Add(time.Duration(c.timeout) * time.Second)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
bw := bufio.NewWriter(conn)
|
|
_, err = bw.WriteString("REPORT SPAMC/" + ProtoVersion + "\r\n")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_, err = bw.WriteString("Content-length: " + strconv.Itoa(len(email)) + "\r\n\r\n")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_, err = bw.Write(email)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = bw.Flush()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Client is supposed to close its writing side of the connection
|
|
// after sending its request.
|
|
err = conn.CloseWrite()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var (
|
|
lines []string
|
|
br = bufio.NewReader(conn)
|
|
)
|
|
for {
|
|
line, err := br.ReadString('\n')
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
line = strings.TrimRight(line, " \t\r\n")
|
|
lines = append(lines, line)
|
|
}
|
|
|
|
// join lines, and replace multi-line descriptions with single line for each
|
|
tmp := strings.Join(lines, "\n")
|
|
re := regexp.MustCompile("\n ")
|
|
n := re.ReplaceAllString(tmp, " ")
|
|
|
|
//split lines again
|
|
return strings.Split(n, "\n"), nil
|
|
}
|
|
|
|
func (c *Client) parseOutput(output []string) Result {
|
|
var result Result
|
|
var reachedRules bool
|
|
for _, row := range output {
|
|
// header
|
|
if spamInfoRe.MatchString(row) {
|
|
res := spamInfoRe.FindStringSubmatch(row)
|
|
if len(res) == 5 {
|
|
resCode, err := strconv.Atoi(res[3])
|
|
if err == nil {
|
|
result.ResponseCode = resCode
|
|
}
|
|
result.Message = res[4]
|
|
continue
|
|
}
|
|
}
|
|
// summary
|
|
if spamMainRe.MatchString(row) {
|
|
res := spamMainRe.FindStringSubmatch(row)
|
|
if len(res) == 4 {
|
|
if strings.ToLower(res[1]) == "true" || strings.ToLower(res[1]) == "yes" {
|
|
result.Spam = true
|
|
} else {
|
|
result.Spam = false
|
|
}
|
|
resFloat, err := strconv.ParseFloat(res[2], 32)
|
|
if err == nil {
|
|
result.Score = resFloat
|
|
continue
|
|
}
|
|
resFloat, err = strconv.ParseFloat(res[3], 32)
|
|
if err == nil {
|
|
result.Threshold = resFloat
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
if strings.HasPrefix(row, "Content analysis details") {
|
|
reachedRules = true
|
|
continue
|
|
}
|
|
// details
|
|
// row = strings.Trim(row, " \t\r\n")
|
|
if reachedRules && spamDetailsRe.MatchString(row) {
|
|
res := spamDetailsRe.FindStringSubmatch(row)
|
|
if len(res) == 5 {
|
|
rule := Rule{Points: res[1], Name: res[2], Description: res[4]}
|
|
result.Rules = append(result.Rules, rule)
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Ping the spamd
|
|
func (c *Client) Ping() error {
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close()
|
|
|
|
if err := conn.SetDeadline(time.Now().Add(time.Duration(c.timeout) * time.Second)); err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = io.WriteString(conn, fmt.Sprintf("PING SPAMC/%s\r\n\r\n", ProtoVersion))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = conn.CloseWrite()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
br := bufio.NewReader(conn)
|
|
for {
|
|
_, err = br.ReadSlice('\n')
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|