Stable version of node-js port
This commit is contained in:
511
client.go
Normal file
511
client.go
Normal file
@@ -0,0 +1,511 @@
|
|||||||
|
package spamc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Error Types
|
||||||
|
|
||||||
|
const EX_OK = 0 //no problems
|
||||||
|
const EX_USAGE = 64 // command line usage error
|
||||||
|
const EX_DATAERR = 65 // data format error
|
||||||
|
const EX_NOINPUT = 66 // cannot open input
|
||||||
|
const EX_NOUSER = 67 // addressee unknown
|
||||||
|
const EX_NOHOST = 68 // host name unknown
|
||||||
|
const EX_UNAVAILABLE = 69 // service unavailable
|
||||||
|
const EX_SOFTWARE = 70 // internal software error
|
||||||
|
const EX_OSERR = 71 // system error (e.g., can't fork)
|
||||||
|
const EX_OSFILE = 72 // critical OS file missing
|
||||||
|
const EX_CANTCREAT = 73 // can't create (user) output file
|
||||||
|
const EX_IOERR = 74 // input/output error
|
||||||
|
const EX_TEMPFAIL = 75 // temp failure; user is invited to retry
|
||||||
|
const EX_PROTOCOL = 76 // remote error in protocol
|
||||||
|
const EX_NOPERM = 77 // permission denied
|
||||||
|
const EX_CONFIG = 78 // configuration error
|
||||||
|
const EX_TIMEOUT = 79 // read timeout
|
||||||
|
|
||||||
|
//map of default spamD protocol errors v1.5
|
||||||
|
var SpamDError = map[int]string{
|
||||||
|
EX_USAGE: "Command line usage error",
|
||||||
|
EX_DATAERR: "Data format error",
|
||||||
|
EX_NOINPUT: "Cannot open input",
|
||||||
|
EX_NOUSER: "Addressee unknown",
|
||||||
|
EX_NOHOST: "Host name unknown",
|
||||||
|
EX_UNAVAILABLE: "Service unavailable",
|
||||||
|
EX_SOFTWARE: "Internal software error",
|
||||||
|
EX_OSERR: "System error",
|
||||||
|
EX_OSFILE: "Critical OS file missing",
|
||||||
|
EX_CANTCREAT: "Can't create (user) output file",
|
||||||
|
EX_IOERR: "Input/output error",
|
||||||
|
EX_TEMPFAIL: "Temp failure; user is invited to retry",
|
||||||
|
EX_PROTOCOL: "Remote error in protocol",
|
||||||
|
EX_NOPERM: "Permission denied",
|
||||||
|
EX_CONFIG: "Configuration error",
|
||||||
|
EX_TIMEOUT: "Read timeout",
|
||||||
|
}
|
||||||
|
|
||||||
|
//Default parameters
|
||||||
|
const PROTOCOL_VERSION = "1.5"
|
||||||
|
const DEFAULT_TIMEOUT = 10
|
||||||
|
|
||||||
|
//Command types
|
||||||
|
const CHECK = "CHECK"
|
||||||
|
const SYMBOLS = "SYMBOLS"
|
||||||
|
const REPORT = "REPORT"
|
||||||
|
const REPORT_IGNOREWARNING = "REPORT_IGNOREWARNING"
|
||||||
|
const REPORT_IFSPAM = "REPORT_IFSPAM"
|
||||||
|
const SKIP = "SKIP"
|
||||||
|
const PING = "PING"
|
||||||
|
const TELL = "TELL"
|
||||||
|
const PROCESS = "PROCESS"
|
||||||
|
const HEADERS = "HEADERS"
|
||||||
|
|
||||||
|
//Learn types
|
||||||
|
const LEARN_SPAM = "SPAM"
|
||||||
|
const LEARN_HAM = "HAM"
|
||||||
|
const LEARN_NOTSPAM = "NOTSPAM"
|
||||||
|
const LEARN_NOT_SPAM = "NOT_SPAM"
|
||||||
|
const LEARN_FORGET = "FORGET"
|
||||||
|
|
||||||
|
//Test Types
|
||||||
|
const TEST_INFO = "info"
|
||||||
|
const TEST_BODY = "body"
|
||||||
|
const TEST_RAWBODY = "rawbody"
|
||||||
|
const TEST_HEADER = "header"
|
||||||
|
const TEST_FULL = "full"
|
||||||
|
const TEST_URI = "uri"
|
||||||
|
const TEST_TXT = "text"
|
||||||
|
|
||||||
|
//only for parse use !important
|
||||||
|
const SPLIT = "§"
|
||||||
|
const TABLE_MARK = "----"
|
||||||
|
|
||||||
|
//Types
|
||||||
|
type Client struct {
|
||||||
|
ConnTimoutSecs int
|
||||||
|
ProtocolVersion string
|
||||||
|
Host string
|
||||||
|
User string
|
||||||
|
}
|
||||||
|
|
||||||
|
//Default response struct
|
||||||
|
type SpamDOut struct {
|
||||||
|
Code int
|
||||||
|
Message string
|
||||||
|
Vars map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Default callback to SpanD response
|
||||||
|
type FnCallback func(*bufio.Reader) (*SpamDOut, error)
|
||||||
|
|
||||||
|
//new instance of Client
|
||||||
|
func New(host string, timeout int) *Client {
|
||||||
|
return &Client{timeout, PROTOCOL_VERSION, host, ""}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Client) SetUnixUser(user string) {
|
||||||
|
s.User = user
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a confirmation that spamd is alive.
|
||||||
|
func (s *Client) Ping() (r *SpamDOut, err error) {
|
||||||
|
return s.simpleCall(PING, []string{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just check if the passed message is spam or not and return score
|
||||||
|
func (s *Client) Check(msgpars ...string) (reply *SpamDOut, err error) {
|
||||||
|
return s.simpleCall(CHECK, msgpars)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Ignore this message -- client opened connection then changed its mind
|
||||||
|
func (s *Client) Skip(msgpars ...string) (reply *SpamDOut, err error) {
|
||||||
|
return s.simpleCall(SKIP, msgpars)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if message is spam or not, and return score plus list of symbols hit
|
||||||
|
func (s *Client) Symbols(msgpars ...string) (reply *SpamDOut, err error) {
|
||||||
|
return s.simpleCall(SYMBOLS, msgpars)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if message is spam or not, and return score plus report
|
||||||
|
func (s *Client) Report(msgpars ...string) (reply *SpamDOut, err error) {
|
||||||
|
return s.simpleCall(REPORT, msgpars)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if message is spam or not, and return score plus report
|
||||||
|
func (s *Client) ReportIgnoreWarning(msgpars ...string) (reply *SpamDOut, err error) {
|
||||||
|
return s.simpleCall(REPORT_IGNOREWARNING, msgpars)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if message is spam or not, and return score plus report if the message is spam
|
||||||
|
func (s *Client) ReportIfSpam(msgpars ...string) (reply *SpamDOut, err error) {
|
||||||
|
return s.simpleCall(REPORT_IFSPAM, msgpars)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Process this message and return a modified message - on deloy
|
||||||
|
func (s *Client) Process(msgpars ...string) (reply *SpamDOut, err error) {
|
||||||
|
return s.simpleCall(PROCESS, msgpars)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Same as PROCESS, but return only modified headers, not body (new in protocol 1.4)
|
||||||
|
func (s *Client) Headers(msgpars ...string) (reply *SpamDOut, err error) {
|
||||||
|
return s.simpleCall(HEADERS, msgpars)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Sign the message as spam
|
||||||
|
func (s *Client) ReportingSpam(msgpars ...string) (reply *SpamDOut, err error) {
|
||||||
|
headers := map[string]string{
|
||||||
|
"Message-class": "spam",
|
||||||
|
"Set": "local,remote",
|
||||||
|
}
|
||||||
|
return s.Tell(msgpars, &headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Sign the message as false-positive
|
||||||
|
func (s *Client) RevokeSpam(msgpars ...string) (reply *SpamDOut, err error) {
|
||||||
|
headers := map[string]string{
|
||||||
|
"Message-class": "ham",
|
||||||
|
"Set": "local,remote",
|
||||||
|
}
|
||||||
|
return s.Tell(msgpars, &headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Learn if a message is spam or not
|
||||||
|
func (s *Client) Learn(learnType string, msgpars ...string) (reply *SpamDOut, err error) {
|
||||||
|
headers := make(map[string]string)
|
||||||
|
switch strings.ToUpper(learnType) {
|
||||||
|
case LEARN_SPAM:
|
||||||
|
headers["Message-class"] = "spam"
|
||||||
|
headers["Set"] = "local"
|
||||||
|
case LEARN_HAM, LEARN_NOTSPAM, LEARN_NOT_SPAM:
|
||||||
|
headers["Message-class"] = "ham"
|
||||||
|
headers["Set"] = "local"
|
||||||
|
case LEARN_FORGET:
|
||||||
|
headers["Remove"] = "local"
|
||||||
|
default:
|
||||||
|
err = errors.New("Learn Type Not Found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return s.Tell(msgpars, &headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
//wrapper to simple calls
|
||||||
|
func (s *Client) simpleCall(cmd string, msgpars []string) (reply *SpamDOut, err error) {
|
||||||
|
return s.call(cmd, msgpars, func(data *bufio.Reader) (r *SpamDOut, e error) {
|
||||||
|
r, e = processResponse(cmd, data)
|
||||||
|
if r.Code == EX_OK {
|
||||||
|
e = nil
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
//external wrapper to simple call
|
||||||
|
func (s *Client) SimpleCall(cmd string, msgpars ...string) (reply *SpamDOut, err error) {
|
||||||
|
return s.simpleCall(strings.ToUpper(cmd), msgpars)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Tell what type of we are to process and what should be done
|
||||||
|
//with that message. This includes setting or removing a local
|
||||||
|
//or a remote database (learning, reporting, forgetting, revoking)
|
||||||
|
func (s *Client) Tell(msgpars []string, headers *map[string]string) (reply *SpamDOut, err error) {
|
||||||
|
return s.call(TELL, msgpars, func(data *bufio.Reader) (r *SpamDOut, e error) {
|
||||||
|
r, e = processResponse(TELL, data)
|
||||||
|
|
||||||
|
if r.Code == EX_UNAVAILABLE {
|
||||||
|
e = errors.New("TELL commands are not enabled, set the --allow-tell switch.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Code == EX_OK {
|
||||||
|
e = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
//here a TCP socket is created to call SPAMD
|
||||||
|
func (s *Client) call(cmd string, msgpars []string, onData FnCallback, extraHeaders *map[string]string) (reply *SpamDOut, err error) {
|
||||||
|
|
||||||
|
if extraHeaders == nil {
|
||||||
|
extraHeaders = &map[string]string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch len(msgpars) {
|
||||||
|
case 1:
|
||||||
|
if s.User != "" {
|
||||||
|
x := *extraHeaders
|
||||||
|
x["User"] = s.User
|
||||||
|
*extraHeaders = x
|
||||||
|
}
|
||||||
|
case 2:
|
||||||
|
x := *extraHeaders
|
||||||
|
x["User"] = msgpars[1]
|
||||||
|
*extraHeaders = x
|
||||||
|
default:
|
||||||
|
if cmd != PING {
|
||||||
|
err = errors.New("Message parameters wrong size")
|
||||||
|
} else {
|
||||||
|
msgpars = []string{""}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd == REPORT_IGNOREWARNING {
|
||||||
|
cmd = REPORT
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new connection
|
||||||
|
stream, err := net.Dial("tcp", s.Host)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err = errors.New("Connection dial error to spamd: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Set connection timeout
|
||||||
|
timeout := time.Now().Add(time.Duration(s.ConnTimoutSecs) * time.Duration(time.Second))
|
||||||
|
errTimeout := stream.SetDeadline(timeout)
|
||||||
|
if errTimeout != nil {
|
||||||
|
err = errors.New("Connection to spamd Timed Out:" + errTimeout.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer stream.Close()
|
||||||
|
|
||||||
|
// Create Command to Send to spamd
|
||||||
|
cmd += " SPAMC/" + s.ProtocolVersion + "\r\n"
|
||||||
|
cmd += "Content-length: " + fmt.Sprintf("%v\r\n", len(msgpars[0])+2)
|
||||||
|
//Process Extra Headers if Any
|
||||||
|
if len(*extraHeaders) > 0 {
|
||||||
|
for hname, hvalue := range *extraHeaders {
|
||||||
|
cmd = cmd + hname + ": " + hvalue + "\r\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmd += "\r\n" + msgpars[0] + "\r\n\r\n"
|
||||||
|
|
||||||
|
_, errwrite := stream.Write([]byte(cmd))
|
||||||
|
if errwrite != nil {
|
||||||
|
err = errors.New("spamd returned a error: " + errwrite.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute onData callback throwing the buffer like parameter
|
||||||
|
reply, err = onData(bufio.NewReader(stream))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//SpamD reply processor
|
||||||
|
func processResponse(cmd string, data *bufio.Reader) (returnObj *SpamDOut, err error) {
|
||||||
|
defer func() {
|
||||||
|
data.UnreadByte()
|
||||||
|
}()
|
||||||
|
|
||||||
|
returnObj = new(SpamDOut)
|
||||||
|
returnObj.Code = -1
|
||||||
|
//read the first line
|
||||||
|
line, _, _ := data.ReadLine()
|
||||||
|
lineStr := string(line)
|
||||||
|
|
||||||
|
r := regexp.MustCompile(`(?i)SPAMD\/([0-9\.]+)\s([0-9]+)\s([0-9A-Z_]+)`)
|
||||||
|
var result = r.FindStringSubmatch(lineStr)
|
||||||
|
if len(result) < 4 {
|
||||||
|
if cmd != "SKIP" {
|
||||||
|
err = errors.New("spamd unreconized reply:" + lineStr)
|
||||||
|
} else {
|
||||||
|
returnObj.Code = EX_OK
|
||||||
|
returnObj.Message = "SKIPPED"
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
returnObj.Code, _ = strconv.Atoi(result[2])
|
||||||
|
returnObj.Message = result[3]
|
||||||
|
|
||||||
|
//verify a mapped error...
|
||||||
|
if SpamDError[returnObj.Code] != "" {
|
||||||
|
err = errors.New(SpamDError[returnObj.Code])
|
||||||
|
returnObj.Vars = make(map[string]interface{})
|
||||||
|
returnObj.Vars["error_description"] = SpamDError[returnObj.Code]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
returnObj.Vars = make(map[string]interface{})
|
||||||
|
|
||||||
|
//start didSet
|
||||||
|
if cmd == TELL {
|
||||||
|
returnObj.Vars["didSet"] = false
|
||||||
|
returnObj.Vars["didRemove"] = false
|
||||||
|
for {
|
||||||
|
line, _, err = data.ReadLine()
|
||||||
|
|
||||||
|
if err == io.EOF || err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if strings.Contains(string(line), "DidRemove") {
|
||||||
|
returnObj.Vars["didRemove"] = true
|
||||||
|
}
|
||||||
|
if strings.Contains(string(line), "DidSet") {
|
||||||
|
returnObj.Vars["didSet"] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//read the second line
|
||||||
|
line, _, err = data.ReadLine()
|
||||||
|
|
||||||
|
//finish here if line is empty
|
||||||
|
if len(line) == 0 {
|
||||||
|
if err == io.EOF {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//ignore content-length header..
|
||||||
|
lineStr = string(line)
|
||||||
|
switch cmd {
|
||||||
|
|
||||||
|
case SYMBOLS, CHECK, REPORT, REPORT_IFSPAM, REPORT_IGNOREWARNING, PROCESS, HEADERS:
|
||||||
|
|
||||||
|
switch cmd {
|
||||||
|
case SYMBOLS, REPORT, REPORT_IFSPAM, REPORT_IGNOREWARNING, PROCESS, HEADERS:
|
||||||
|
//ignore content-length header..
|
||||||
|
line, _, err = data.ReadLine()
|
||||||
|
lineStr = string(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := regexp.MustCompile(`(?i)Spam:\s(True|False|Yes|No)\s;\s([0-9\.]+)\s\/\s([0-9\.]+)`)
|
||||||
|
var result = r.FindStringSubmatch(lineStr)
|
||||||
|
|
||||||
|
if len(result) > 0 {
|
||||||
|
returnObj.Vars["isSpam"] = false
|
||||||
|
switch result[1][0:1] {
|
||||||
|
case "T", "t", "Y", "y":
|
||||||
|
returnObj.Vars["isSpam"] = true
|
||||||
|
}
|
||||||
|
returnObj.Vars["spamScore"], _ = strconv.ParseFloat(result[2], 64)
|
||||||
|
returnObj.Vars["baseSpamScore"], _ = strconv.ParseFloat(result[3], 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch cmd {
|
||||||
|
case PROCESS, HEADERS:
|
||||||
|
lines := ""
|
||||||
|
for {
|
||||||
|
line, _, err = data.ReadLine()
|
||||||
|
if err == io.EOF || err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lines += string(line) + "\r\n"
|
||||||
|
returnObj.Vars["body"] = lines
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case SYMBOLS:
|
||||||
|
//ignore line break...
|
||||||
|
data.ReadLine()
|
||||||
|
//read
|
||||||
|
line, _, err = data.ReadLine()
|
||||||
|
returnObj.Vars["symbolList"] = strings.Split(string(line), ",")
|
||||||
|
|
||||||
|
case REPORT, REPORT_IFSPAM, REPORT_IGNOREWARNING:
|
||||||
|
//ignore line break...
|
||||||
|
data.ReadLine()
|
||||||
|
|
||||||
|
for {
|
||||||
|
line, _, err = data.ReadLine()
|
||||||
|
|
||||||
|
if len(line) > 0 {
|
||||||
|
lineStr = string(line)
|
||||||
|
|
||||||
|
//TXT Table found, prepare to parse..
|
||||||
|
if lineStr[0:4] == TABLE_MARK {
|
||||||
|
|
||||||
|
section := []map[string]interface{}{}
|
||||||
|
tt := 0
|
||||||
|
for {
|
||||||
|
line, _, err = data.ReadLine()
|
||||||
|
//Stop read the text table if last line or Void line
|
||||||
|
if err == io.EOF || err != nil || len(line) == 0 {
|
||||||
|
if err == io.EOF {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
//Parsing
|
||||||
|
lineStr = string(line)
|
||||||
|
spc := 2
|
||||||
|
if lineStr[0:1] == "-" {
|
||||||
|
spc = 1
|
||||||
|
}
|
||||||
|
lineStr = strings.Replace(lineStr, " ", SPLIT, spc)
|
||||||
|
lineStr = strings.Replace(lineStr, " ", SPLIT, 1)
|
||||||
|
if spc > 1 {
|
||||||
|
lineStr = " " + lineStr[2:]
|
||||||
|
}
|
||||||
|
x := strings.Split(lineStr, SPLIT)
|
||||||
|
if lineStr[1:3] == SPLIT {
|
||||||
|
section[tt-1]["message"] = fmt.Sprintf("%v %v", section[tt-1]["message"], strings.TrimSpace(lineStr[5:]))
|
||||||
|
} else {
|
||||||
|
if len(x) != 0 {
|
||||||
|
message := strings.TrimSpace(x[2])
|
||||||
|
score, _ := strconv.ParseFloat(strings.TrimSpace(x[0]), 64)
|
||||||
|
|
||||||
|
section = append(section, map[string]interface{}{
|
||||||
|
"score": score,
|
||||||
|
"symbol": x[1],
|
||||||
|
"message": message,
|
||||||
|
})
|
||||||
|
|
||||||
|
tt++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if REPORT_IGNOREWARNING == cmd {
|
||||||
|
nsection := []map[string]interface{}{}
|
||||||
|
for _, c := range section {
|
||||||
|
if c["score"].(float64) != 0 {
|
||||||
|
nsection = append(nsection, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
section = nsection
|
||||||
|
}
|
||||||
|
|
||||||
|
returnObj.Vars["report"] = section
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == io.EOF || err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != io.EOF {
|
||||||
|
for {
|
||||||
|
line, _, err = data.ReadLine()
|
||||||
|
if err == io.EOF || err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
44
examples/report.go
Normal file
44
examples/report.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go-spamc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
html := "<html><title>Test</title><body>Hello world. I'm not a Spam, don't kill me spamassassin!</body></html>"
|
||||||
|
|
||||||
|
client := spamc.New("127.0.0.1:783", 10)
|
||||||
|
|
||||||
|
//the 2nd parameter is optional, you can set who (the unix user) do the call
|
||||||
|
//looks like client.Report(html, "saintienn")
|
||||||
|
|
||||||
|
reply, err := client.Report(html)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
fmt.Println(reply)
|
||||||
|
} else {
|
||||||
|
fmt.Println(reply, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Example Response
|
||||||
|
{
|
||||||
|
Code: 0,
|
||||||
|
Message: 'EX_OK',
|
||||||
|
Vars:{
|
||||||
|
isSpam: true,
|
||||||
|
spamScore: 6.9,
|
||||||
|
baseSpamScore: 5,
|
||||||
|
report:[
|
||||||
|
{
|
||||||
|
"score": score,
|
||||||
|
"symbol": x[1],
|
||||||
|
"message": message,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
/**
|
|
||||||
* User: Carl Glaysher
|
|
||||||
* Date: 17/03/2012
|
|
||||||
* Time: 08:46
|
|
||||||
* Description: Front end to check spamc client
|
|
||||||
*/
|
|
||||||
var spamc = require('./spamc');
|
|
||||||
var client = new spamc();
|
|
||||||
|
|
||||||
client.report('My Message as String',function(result){
|
|
||||||
console.log(result);
|
|
||||||
});
|
|
||||||
|
|
||||||
/* Example Response
|
|
||||||
{
|
|
||||||
responseCode: 0,
|
|
||||||
responseMessage: 'EX_OK',
|
|
||||||
isSpam: true,
|
|
||||||
spamScore: 6.9,
|
|
||||||
baseSpamScore: 5,
|
|
||||||
report:[
|
|
||||||
{
|
|
||||||
score: '0.0',
|
|
||||||
name: 'NO_RELAYS',
|
|
||||||
description: 'Informational',
|
|
||||||
type: 'message'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
334
index.js
334
index.js
@@ -1,334 +0,0 @@
|
|||||||
/**
|
|
||||||
* Author: Carl Glaysher
|
|
||||||
* Date Created: 17/03/2012
|
|
||||||
* Description: Module to emulate SPAMC client in a node way
|
|
||||||
* Available Commands:
|
|
||||||
*
|
|
||||||
* ping - returns boolean
|
|
||||||
* check - returns object
|
|
||||||
* symbols - returns object with matches
|
|
||||||
* report - returns objects with matches and descriptions
|
|
||||||
* reportIfSpam - returns object with matches and descriptions
|
|
||||||
* process - returns object with modified message
|
|
||||||
* headers - returns object with modified headers only
|
|
||||||
* learn - TELL spamassassin message is Spam or Ham
|
|
||||||
* tell - TELL spamassassin message is Spam
|
|
||||||
* revoke - remove Spammed Message as being spam from spamassassin
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
var net = require('net');
|
|
||||||
|
|
||||||
var spamc = function (host, port, timeout) {
|
|
||||||
var self = this;
|
|
||||||
var protocolVersion = 1.5;
|
|
||||||
var host = (host == undefined) ? '127.0.0.1' : host;
|
|
||||||
var port = (port == undefined) ? 783 : port;
|
|
||||||
var connTimoutSecs = (timeout == undefined) ? 10 : timeout;
|
|
||||||
/*
|
|
||||||
* Description: Sends a Ping to spamd and returns Pong on response
|
|
||||||
* Param: onResponse {function}
|
|
||||||
* Returns: self
|
|
||||||
*/
|
|
||||||
this.ping = function(onResponse){
|
|
||||||
exec('PING',null,function(data){
|
|
||||||
/* Check Response has the word PONG */
|
|
||||||
if(data[0].indexOf('PONG')>0){
|
|
||||||
onResponse(true);
|
|
||||||
}else{
|
|
||||||
onResponse(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return self;
|
|
||||||
};
|
|
||||||
/*
|
|
||||||
* Description: returns spam score
|
|
||||||
* Param: message {string}
|
|
||||||
* Param: onResponse {function}
|
|
||||||
* Returns: self
|
|
||||||
*/
|
|
||||||
this.check = function(message,onResponse){
|
|
||||||
exec('CHECK',message,function(data){
|
|
||||||
var response = processResponse('CHECK',data);
|
|
||||||
if(typeof(onResponse)=='function') onResponse(response);
|
|
||||||
});
|
|
||||||
return self;
|
|
||||||
};
|
|
||||||
/*
|
|
||||||
* Description: Returns Spam Score and Matches
|
|
||||||
* Param: message {string}
|
|
||||||
* Param: onResponse {function}
|
|
||||||
* Returns: self
|
|
||||||
*/
|
|
||||||
this.symbols = function(message,onResponse){
|
|
||||||
exec('SYMBOLS',message,function(data){
|
|
||||||
var response = processResponse('SYMBOLS',data);
|
|
||||||
if(typeof(onResponse)=='function') onResponse(response);
|
|
||||||
});
|
|
||||||
return self;
|
|
||||||
};
|
|
||||||
/*
|
|
||||||
* Description: Returns an object report
|
|
||||||
* Param: message {string}
|
|
||||||
* Param: onResponse {function}
|
|
||||||
* Returns: self
|
|
||||||
*/
|
|
||||||
this.report = function(message,onResponse){
|
|
||||||
exec('REPORT',message,function(data){
|
|
||||||
var response = processResponse('REPORT',data);
|
|
||||||
if(typeof(onResponse)=='function') onResponse(response);
|
|
||||||
});
|
|
||||||
return self;
|
|
||||||
};
|
|
||||||
/*
|
|
||||||
* Description: Returns Object Report if is spam
|
|
||||||
* Param: message {string}
|
|
||||||
* Param: onResponse {function}
|
|
||||||
* Returns: self
|
|
||||||
*/
|
|
||||||
this.reportIfSpam = function(message,onResponse){
|
|
||||||
exec('REPORT_IFSPAM',message,function(data){
|
|
||||||
var response = processResponse('REPORT_IFSPAM',data);
|
|
||||||
if(typeof(onResponse)=='function') onResponse(response);
|
|
||||||
});
|
|
||||||
return self;
|
|
||||||
};
|
|
||||||
/*
|
|
||||||
* Description: Returns back a report for the message + the message
|
|
||||||
* Param: message {string}
|
|
||||||
* Param: onResponse {function}
|
|
||||||
* Returns: self
|
|
||||||
*/
|
|
||||||
this.process = function(message,onResponse){
|
|
||||||
exec('PROCESS',message,function(data){
|
|
||||||
var response = processResponse('PROCESS',data);
|
|
||||||
if(typeof(onResponse)=='function') onResponse(response);
|
|
||||||
});
|
|
||||||
return self;
|
|
||||||
};
|
|
||||||
/*
|
|
||||||
* Description: Returns headers for the message
|
|
||||||
* Param: message {string}
|
|
||||||
* Param: onResponse {function}
|
|
||||||
* Returns: self
|
|
||||||
*/
|
|
||||||
this.headers = function(message,onResponse){
|
|
||||||
exec('HEADERS',message,function(data){
|
|
||||||
var response = processResponse('HEADERS',data);
|
|
||||||
if(typeof(onResponse)=='function') onResponse(response);
|
|
||||||
});
|
|
||||||
return self;
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Description: Tell spamd to learn message is spam/ham or forget
|
|
||||||
* Param: message {string}
|
|
||||||
* Param: learnType {string}
|
|
||||||
* Param: onResponse {function}
|
|
||||||
* Returns: self
|
|
||||||
*/
|
|
||||||
this.learn = function(message,learnType,onResponse){
|
|
||||||
var headers;
|
|
||||||
switch(learnType.toUpperCase()){
|
|
||||||
case 'SPAM':
|
|
||||||
headers=[
|
|
||||||
{name:'Message-class','value':'spam'},
|
|
||||||
{name:'Set','value':'local'}
|
|
||||||
];
|
|
||||||
break;
|
|
||||||
case 'HAM':
|
|
||||||
case 'NOTSPAM':
|
|
||||||
case 'NOT_SPAM':
|
|
||||||
headers=[
|
|
||||||
{name:'Message-class','value':'ham'},
|
|
||||||
{name:'Set','value':'local'}
|
|
||||||
];
|
|
||||||
break;
|
|
||||||
case 'FORGET':
|
|
||||||
headers=[
|
|
||||||
{name:'Remove','value':'local'}
|
|
||||||
];
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error('Learn Type Not Found');
|
|
||||||
}
|
|
||||||
exec('TELL',message,function(data){
|
|
||||||
var response = processResponse('HEADERS',data);
|
|
||||||
if(response.responseCode==69){
|
|
||||||
throw new Error('TELL commands are not enabled, set the --allow-tell switch.');
|
|
||||||
}
|
|
||||||
if(typeof(onResponse)=='function') onResponse(response);
|
|
||||||
},headers);
|
|
||||||
return self;
|
|
||||||
};
|
|
||||||
/*
|
|
||||||
* Description: tell spamd message is not spam
|
|
||||||
* Param: message {string}
|
|
||||||
* Param: onResponse {function}
|
|
||||||
* Returns: self
|
|
||||||
*/
|
|
||||||
this.revoke = function(message,onResponse){
|
|
||||||
headers=[
|
|
||||||
{name:'Message-class','value':'ham'},
|
|
||||||
{name:'Set','value':'local,remote'}
|
|
||||||
];
|
|
||||||
exec('TELL',message,function(data){
|
|
||||||
var response = processResponse('HEADERS',data);
|
|
||||||
if(response.responseCode==69){
|
|
||||||
throw new Error('TELL commands are not enabled, set the --allow-tell switch.');
|
|
||||||
}
|
|
||||||
if(typeof(onResponse)=='function') onResponse(response);
|
|
||||||
},headers);
|
|
||||||
return self;
|
|
||||||
};
|
|
||||||
/*
|
|
||||||
* Description: Tell spamd message is spam
|
|
||||||
* Param: message {string}
|
|
||||||
* Param: onResponse {function}
|
|
||||||
* Returns: self
|
|
||||||
*/
|
|
||||||
this.tell = function(message,onResponse){
|
|
||||||
headers=[
|
|
||||||
{name:'Message-class','value':'spam'},
|
|
||||||
{name:'Set','value':'local,remote'}
|
|
||||||
];
|
|
||||||
exec('TELL',message,function(data){
|
|
||||||
var response = processResponse('HEADERS',data);
|
|
||||||
if(response.responseCode==69){
|
|
||||||
throw new Error('TELL commands are not enabled, set the --allow-tell switch.');
|
|
||||||
}
|
|
||||||
if(typeof(onResponse)=='function') onResponse(response);
|
|
||||||
},headers);
|
|
||||||
return self;
|
|
||||||
};
|
|
||||||
/*
|
|
||||||
* Description: Sends a command to spamd
|
|
||||||
* Param: cmd {string}
|
|
||||||
* Param: message {string}
|
|
||||||
* Param: onData {function(data)}
|
|
||||||
*/
|
|
||||||
var exec = function(cmd,message,onData,extraHeaders){
|
|
||||||
var responseData = [];
|
|
||||||
var stream = net.createConnection(port,host);
|
|
||||||
stream.setTimeout(connTimoutSecs*1000,function(){
|
|
||||||
throw new Error('Connection to spamd Timed Out');
|
|
||||||
});
|
|
||||||
stream.on('connect',function(){
|
|
||||||
/* Create Command to Send to spamd */
|
|
||||||
cmd = cmd+" SPAMC/"+protocolVersion+"\r\n";
|
|
||||||
if(typeof(message)=='string'){
|
|
||||||
message = message+'\r\n';
|
|
||||||
cmd = cmd+"Content-length: "+(message.length)+"\r\n";
|
|
||||||
/* Process Extra Headers if Any */
|
|
||||||
if(typeof(extraHeaders)=='object'){
|
|
||||||
for(var i=0;i<extraHeaders.length;i++){
|
|
||||||
cmd = cmd+extraHeaders[i].name+": "+extraHeaders[i].value+"\r\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cmd = cmd+"\r\n"+message;
|
|
||||||
}
|
|
||||||
stream.write(cmd+"\r\n");
|
|
||||||
});
|
|
||||||
stream.on('error',function(data){
|
|
||||||
throw new Error('spamd returned a error: '+data.toString());
|
|
||||||
});
|
|
||||||
stream.on('data',function(data){
|
|
||||||
var data = data.toString();
|
|
||||||
/* Remove Last new Line and Return and Split Lines into Array */
|
|
||||||
data = data.split("\r\n");
|
|
||||||
for(var i=0;i<data.length;i++){
|
|
||||||
if(data[i].length>0){
|
|
||||||
responseData[responseData.length]=data[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
stream.on('close',function(){
|
|
||||||
onData(responseData);
|
|
||||||
})
|
|
||||||
};
|
|
||||||
/*
|
|
||||||
* Description: Processes Response from spamd and put into a formatted object
|
|
||||||
* Param: cmd {string}
|
|
||||||
* Param: lines {array[string]}
|
|
||||||
* Return: {object}
|
|
||||||
*/
|
|
||||||
var processResponse = function(cmd,lines){
|
|
||||||
var returnObj = {};
|
|
||||||
var result = lines[0].match(/SPAMD\/([0-9\.]+)\s([0-9]+)\s([0-9A-Z_]+)/);
|
|
||||||
if(result==null){
|
|
||||||
throw new Error('spamd unreconized response:'+lines[0]);
|
|
||||||
}
|
|
||||||
returnObj.responseCode = parseInt(result[2]);
|
|
||||||
returnObj.responseMessage = result[3];
|
|
||||||
if(cmd=='TELL'){
|
|
||||||
returnObj.didSet=false;
|
|
||||||
returnObj.didRemove=false;
|
|
||||||
}
|
|
||||||
for(var i=0;i<lines.length;i++){
|
|
||||||
var result = lines[i].match(/Spam:\s(True|False|Yes|No)\s;\s([0-9\.]+)\s\/\s([0-9\.]+)/);
|
|
||||||
if(result!=null){
|
|
||||||
returnObj.isSpam =false;
|
|
||||||
if(result[1]=='True' || result[1]=='Yes'){
|
|
||||||
returnObj.isSpam = true;
|
|
||||||
}
|
|
||||||
returnObj.spamScore = parseFloat(result[2]);
|
|
||||||
returnObj.baseSpamScore = parseFloat(result[3]);
|
|
||||||
}
|
|
||||||
if(result==null){
|
|
||||||
var result = lines[i].match(/([A-Z0-9\_]+)\,/g);
|
|
||||||
if(result!=null){
|
|
||||||
returnObj.matches =[];
|
|
||||||
for(var ii=0;ii<result.length;ii++){
|
|
||||||
returnObj.matches[ii] = result[ii].substring(0,result[ii].length-1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(result==null && cmd!='PROCESS'){
|
|
||||||
var pattern = /(\s|-)([0-9\.]+)\s([A-Z0-9\_]+)\s([^:]+)\:\s([^\n]+)/g;
|
|
||||||
var result = lines[i].match(pattern);
|
|
||||||
if(result!=null){
|
|
||||||
returnObj.report =[];
|
|
||||||
for(var ii=0;ii<result.length;ii++){
|
|
||||||
/* Remove New Line if Found */
|
|
||||||
result[ii] = result[ii].replace(/\n([\s]*)/, ' ');
|
|
||||||
/* Match Sections */
|
|
||||||
var pattern = /(\s|-)([0-9\.]+)\s([A-Z0-9\_]+)\s([^:]+)\:\s([^\s]+)/;
|
|
||||||
var matches = result[ii].match(pattern);
|
|
||||||
returnObj.report[returnObj.report.length] = {
|
|
||||||
score:matches[2],
|
|
||||||
name:matches[3],
|
|
||||||
description:matches[4].replace(/^\s*([\S\s]*)\b\s*$/, '$1'),
|
|
||||||
type:matches[5]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
if(lines[i].indexOf('DidSet:')>=0){
|
|
||||||
returnObj.didSet=true;
|
|
||||||
}
|
|
||||||
if(lines[i].indexOf('DidRemove:')>=0){
|
|
||||||
returnObj.didRemove=true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(cmd=='PROCESS'){
|
|
||||||
returnObj.message = '';
|
|
||||||
for(var i=3;i<lines.length;i++){
|
|
||||||
returnObj.message=returnObj.message+lines[i]+"\r\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(cmd=='HEADERS'){
|
|
||||||
returnObj.headers = [];
|
|
||||||
for(var i=3;i<lines.length;i++){
|
|
||||||
if(lines[i].indexOf('\t')<0){
|
|
||||||
returnObj.headers[returnObj.headers.length]=lines[i];
|
|
||||||
}else{
|
|
||||||
returnObj.headers[returnObj.headers.length-1]=returnObj.headers[returnObj.headers.length-1]+lines[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return returnObj;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = spamc;
|
|
||||||
18
package.json
18
package.json
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"name":"node-spamc",
|
|
||||||
"version":"0.0.1",
|
|
||||||
"author":"Carl Glaysher <carl@coxeh.com>",
|
|
||||||
"description":"Connect to spamassassin via spamd daemon and check spam score on a email",
|
|
||||||
"contributors":[
|
|
||||||
{
|
|
||||||
"name":"Carl Glaysher",
|
|
||||||
"email":"carl@coxeh.com"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/coxeh/node-spamc"
|
|
||||||
},
|
|
||||||
"main":"./index.js",
|
|
||||||
"keywords":["spamd","spamc","spamassassin","email","spam"]
|
|
||||||
}
|
|
||||||
59
readme.md
59
readme.md
@@ -1,34 +1,55 @@
|
|||||||
# node-spamc
|
# go-spamc
|
||||||
|
|
||||||
node-spamc is a nodejs module that connects to spamassassin's spamd daemon. You are able to:
|
go-spamc is a golang package that connects to spamassassin's spamd daemon.
|
||||||
|
Is a code port of nodejs module node-spamc(https://github.com/coxeh/node-spamc)
|
||||||
|
|
||||||
|
Thanks for your amazing code Carl Glaysher ;)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
You are able to:
|
||||||
|
|
||||||
- Check a message for a spam score and return back what spamassassin matched on
|
- Check a message for a spam score and return back what spamassassin matched on
|
||||||
- Ability to send messages to spamassassin to learn from
|
- Ability to send messages to spamassassin to learn from
|
||||||
- Ability to do everything that `spamc` is capable of
|
- Ability to do everything that `spamc` is capable of
|
||||||
|
|
||||||
## Commands Available
|
## Methods Available
|
||||||
|
|
||||||
- `check` checks a message for a spam score and returns an object of information
|
|
||||||
- `symbols` like `check` but also returns what the message matched on
|
|
||||||
- `report` like `symbols` but matches also includes a small description
|
|
||||||
- `reportIfSpam` only returns a result if message is spam
|
|
||||||
- `process` like `check` but also returns a processed message with extra headers
|
|
||||||
- `headers` like `check` but also returns the message headers in a array
|
|
||||||
- `learn` abilty to parse a message to spamassassin and learn it as spam or ham
|
|
||||||
- `tell` ability to tell spamassassin that the message is spam
|
|
||||||
- `revoke` abilty to tell spamassassin that the message is not spam
|
|
||||||
|
|
||||||
|
- `Check` checks a message for a spam score and returns an object of information
|
||||||
|
- `Symbols` like `check` but also returns what the message matched on
|
||||||
|
- `Report` like `symbols` but matches also includes a small description
|
||||||
|
- `ReportIfSpam` only returns a result if message is spam
|
||||||
|
- `ReportIgnoreWarning` like report but matches only symbols with score > 0 "New"
|
||||||
|
- `Process` like `check` but also returns a processed message with extra headers
|
||||||
|
- `Headers` like `check` but also returns the message headers in a array
|
||||||
|
- `Learn` abilty to parse a message to spamassassin and learn it as spam or ham
|
||||||
|
- `ReportingSpam` ability to tell spamassassin that the message is spam
|
||||||
|
- `RevokeSpam` abilty to tell spamassassin that the message is not spam
|
||||||
|
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
example.go
|
||||||
|
|
||||||
This example will parse a message to spamassassin to perform a report and will callback on success.
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"spamc"
|
||||||
|
)
|
||||||
|
|
||||||
var spamc = require('./spamc');
|
func main() {
|
||||||
var client = new spamc();
|
|
||||||
|
html := "<html>Hello world. I'm not a Spam, don't kill me SpamAssassin!</html>"
|
||||||
|
client := spamc.New()
|
||||||
|
|
||||||
|
//the 2nd parameter is optional, you can set who (the unix user) do the call
|
||||||
|
reply, err := client.Check(html, "saintienn")
|
||||||
|
|
||||||
|
fmt.Println(reply.Code)
|
||||||
|
fmt.Println(reply.Message)
|
||||||
|
fmt.Println(reply.Vars)
|
||||||
|
}
|
||||||
|
|
||||||
client.report('My Message as String',function(result){
|
|
||||||
console.log(result);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user