Compare commits
96 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e0d11b742f | |||
| f896c4ad0e | |||
| eabe7b6eb0 | |||
| 3ebf21580a | |||
| c0550a18fe | |||
| f23bd04dbe | |||
| 768cfd4e61 | |||
| 265eacb356 | |||
| 27d5096d56 | |||
| a5bb18126d | |||
| 1bcab2359d | |||
| 23e91eaee7 | |||
| 82618ee86d | |||
| 18bcfbe69e | |||
| bb4e1a6fb8 | |||
| 88dce405fc | |||
| eec1effcb2 | |||
| 4aec643190 | |||
| 7b5ee82552 | |||
| 6711b3e93f | |||
| 36e44058f8 | |||
| fc83af2612 | |||
| 12be6f9542 | |||
| cbf3d3c3ea | |||
| 7e6092c4b1 | |||
| 12fbcdd400 | |||
| b6c8138cc5 | |||
| a46f66a090 | |||
| 1bb820fee0 | |||
| a55162d803 | |||
| f70e2f46e1 | |||
| 6e49551391 | |||
| 2fc0afacac | |||
| 76428fc6ff | |||
| 3403b432af | |||
| 13a08b3725 | |||
| 4ed58b477b | |||
| 5b302def1b | |||
| b4c6d972de | |||
| a2c7940ba1 | |||
| 9ade57dc0c | |||
| fe3d70f2fb | |||
| 229929ee38 | |||
| 2c26e3ec35 | |||
| d1aafd582b | |||
| 0e3bea28ff | |||
| fb55c39e0b | |||
| 49ac3dfe43 | |||
| d14a95d49e | |||
| c6772637a6 | |||
| ca22973c25 | |||
| db2f22658a | |||
| efa8212eab | |||
| dc48f83f7e | |||
| 88a9cbcb1c | |||
| 8fac3092f2 | |||
| 5e25542fec | |||
| 281b3a84a5 | |||
| 2dbf407442 | |||
| ec85ae1684 | |||
| a13c4b63f0 | |||
| 2f5fc86d3d | |||
| 008f82f0f4 | |||
| 65a41c26db | |||
| 9384b91fb7 | |||
| ce3894864a | |||
| ed96f0c66f | |||
| aae34c04f4 | |||
| d0f6547da9 | |||
| 1987653574 | |||
| eb9d4943e8 | |||
| 5bcfec0eaa | |||
| aa8f9a4987 | |||
| 6f9ef2c770 | |||
| 41d9f0df73 | |||
| 279bdd003b | |||
| 5f6631176d | |||
| 21df0d8fd7 | |||
| 3c41283fed | |||
| 1b0b9d7604 | |||
| fd72237063 | |||
| 36f4f49ff1 | |||
| 081fac831d | |||
| c2d8263e51 | |||
| ab4d093b8e | |||
| 415b8d932f | |||
| 91237e1000 | |||
| 1e2da3603d | |||
| fedbb53942 | |||
| 0e670b786a | |||
| 3314c74a10 | |||
| e8ea2d06ef | |||
| 717aba1e48 | |||
| 73161ebae3 | |||
| 8bf84dc18d | |||
| 0cef2a76ea |
@@ -1,3 +1,2 @@
|
||||
rspamd-cgp
|
||||
*.CU*
|
||||
vendor/github.com
|
||||
|
||||
@@ -1,22 +1,109 @@
|
||||
|
||||
Rspamd plugin for CommuniGate Pro 5.x, 6.x
|
||||
## Rspamd helper for CommuniGate Pro 6.x, *версия 2.0.0*
|
||||
|
||||
Installation and usage instructions can be found at
|
||||
http://www.communigate.com/CGPMcAfee/#Integrate
|
||||
### Введение
|
||||
|
||||
*Помощник* (внешний фильтр сообщений) **rspamd-cgp** используется для фильтрования спама. Он получает сообщения от *CommuniGate Pro* и передаёт их для обработки в *Rspamd*.<br/><br/>
|
||||
|
||||
### Возможности
|
||||
|
||||
* *Помощник* получает сообщение от *CommuniGate Pro* по протоколу *Интерфейса Внешнего Фильтра* и передаёт сообщение в *Rspamd*, используя *Rspamd protocol*.
|
||||
|
||||
* Если сообщение получено из аутентифицированного источника, *Помощник* передаёт заголовок *Auth:* в *Rspamd protocol*.
|
||||
|
||||
* *Помощник* определяет *HELO/EHLO* протокола *SMTP* на основании первого заголовка *Received:* сообщения и передаёт заголовок *Helo:* в *Rspamd protocol*.
|
||||
|
||||
* *Помощник* выполняет резольвинг ip-адреса источника сообщения, и, если резольвинг успешен, передаёт заголовок *Hostname:* в *Rspamd protocol*. Результаты резольвинга кешируются.
|
||||
|
||||
* *Помощник* различает сообщения, полученные из внешних источников, и сгенерированные *CommuniGate Pro*. При передаче в *Rspamd* сгенерированных сообщений *Помощник* помечает их как полученные из доверенного источника.
|
||||
|
||||
* *Помощник* формирует заголовок *X-Junk-Score:* на основании *action*, пришедшего от *Rspamd*. Этот заголовок обрабатывается встроенными правилами *Управление Спамом*, которые настраиваются в пользовательском интерфейсе *CommuniGate Pro*.
|
||||
|
||||
* *Помощник* добавляет в сообщение все полученные от *Rspamd* заголовки. Решение о добавлении этих заголовков принимается на стороне *Rspamd*.
|
||||
|
||||
* При получении *action: rewrite subject* *Помощник* переписывает *Subject:* сообщения как указано в ответе *Rspamd*. Эта операция вызывает проход сообщения через *CommuniGate Pro PIPE*.
|
||||
|
||||
* В ряде случаев, чтобы избежать двойной обработки или зацикливания сообщений, *Помощник* добавляет в обработанное сообщение заголовок *X-Rspamd-Seen:*.
|
||||
|
||||
* *Помощник* может выполнять дополнительную обработку сообщений на основании своего конфигурационного файла, используя данные из ответа *Rspamd*. Дополнительная обработка может быть применена к *действиям* и *символам*. Если для конкретного сообщения в конфигурационном файле *Помощника* совпали несколько *действий* и *символов*, для получения итогового результата они суммируются.
|
||||
|
||||
* *Помощник* может обрабатывать исходящие сообщения. Это позволяет задействовать ряд модулей *Rspamd* для уменьшения ложных срабатываний (FP).
|
||||
|
||||
|
||||
Copyright (C) 2017-2021 Andrey Igoshin <ai@vsu.ru>
|
||||
Version 1.3.0
|
||||
### Установка
|
||||
|
||||
https://git.vsu.ru/ai/rspamd-cgp
|
||||
Конфигурационный файл *Помощника* [rspamd-cgp.yml](rspamd-cgp.yml) по умолчанию находится в той же директории, что и исполняемый файл **rspamd-cgp**. При необходимости другое местоположение конфигурационного файла можно указать в командной строке. Возможные настройки *Помощника* подробно описаны в конфигурационном файле.
|
||||
|
||||
> Ниже показаны настройки *Помощника* и *Правила* в интерфейсе *CommuniGate Pro*.
|
||||
|
||||
#### Для входящих сообщений
|
||||
|
||||
##### Установки -> Общее -> Помощники
|
||||

|
||||
|
||||
##### Установки -> Почта -> Правила -> RSPAMD_in
|
||||

|
||||
|
||||
#### Для исходящих сообщений
|
||||
|
||||
##### Установки -> Общее -> Помощники
|
||||

|
||||
|
||||
##### Установки -> Почта -> Правила -> RSPAMD_out
|
||||

|
||||
|
||||
#### Параметры командной строки
|
||||
|
||||
```
|
||||
Usage of rspamd-cgp:
|
||||
-authserv-id string
|
||||
Authentication Identifier (default CommuniGate Pro Main Domain)
|
||||
-host string
|
||||
Rspamd host to connect (default "localhost:11333")
|
||||
-reject-action string
|
||||
Reject action: "add_header" or "discard" (default "add_header")
|
||||
-timeout duration
|
||||
Rspamd request timeout (default 15s)
|
||||
-config string
|
||||
Set configuration file (default "rspamd-cgp.yml")
|
||||
-configdump
|
||||
Perform configuration file dump
|
||||
-configtest
|
||||
Perform configuration file test
|
||||
-debug
|
||||
Run in debug mode
|
||||
-outbound
|
||||
Outbound message flow processing
|
||||
```
|
||||
|
||||
**config**
|
||||
|
||||
Указывает альтернативный конфигурационный файл.
|
||||
|
||||
**configdump**
|
||||
|
||||
Выводит конфигурационный файл в форматированном виде.
|
||||
|
||||
**configtest**
|
||||
|
||||
Проверяет синтаксическую корректность конфигурационного файла.
|
||||
|
||||
**debug**
|
||||
|
||||
Выводит в форматированном виде ответ *Rspamd* (JSON). Может быть
|
||||
использован для контроля возвращаемых *Rspamd* *символов* и других данных. Входной файл должен быть в формате файла очереди *CommuniGate Pro*. ***Применять только при запуске из командной строки!!!***
|
||||
|
||||
**outbound**
|
||||
|
||||
Обрабатывает поток исходящих сообщений. Если исходящие сообщения отправляются на внешние MTA, в них не добавляются заголовки, являющиеся результатом проверки на спам. Какие именно сообщения обрабатываются таким образом определяется *Правилом*
|
||||
*CommuniGate Pro*.
|
||||
|
||||
<br/>
|
||||
|
||||
### Лицензия
|
||||
|
||||
BSD License, [LICENSE.md](LICENSE.md)<br/><br/>
|
||||
|
||||
### Автор
|
||||
|
||||
Andrey Igoshin <<ai@vsu.ru>><br/><br/>
|
||||
|
||||
### Ссылки
|
||||
|
||||
* Репозиторий: <https://git.vsu.ru/ai/rspamd-cgp>
|
||||
* Сайт CommuniGate Pro: <https://communigatepro.ru>
|
||||
* Протокол Помощника: <https://old.communigatepro.ru/CommuniGatePro/russian/Helpers.html#Filters>
|
||||
* Сайт Rspamd: <https://rspamd.com>
|
||||
* Протокол Rspamd: <https://rspamd.com/doc/developers/protocol.html>
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
|
||||
export GOPATH="${HOME}/src/rspamd-cgp"
|
||||
|
||||
# если на целевой ОС не совпадает glibc, то собираем без зависимостей.
|
||||
# результирующий файл, возможно, получится медленнее и большего размера.
|
||||
export CGO_ENABLED=0
|
||||
|
||||
if [ "$1" == "fmt" ]; then
|
||||
go fmt $2
|
||||
elif [ "$1" == "get" ]; then
|
||||
go get $2
|
||||
elif [ "$1" == "tidy" ]; then
|
||||
go mod tidy
|
||||
elif [ "$1" == "update" ]; then
|
||||
echo "update..."
|
||||
go get -u ./...
|
||||
go mod tidy
|
||||
elif [ "$1" == "vet" ]; then
|
||||
echo "vet..."
|
||||
go vet ./...
|
||||
else
|
||||
go build
|
||||
fi
|
||||
+216
-80
@@ -2,40 +2,96 @@ package cgp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"git.vsu.ru/ai/rspamd-cgp/utils"
|
||||
)
|
||||
|
||||
var MainDomain string
|
||||
const recvHdr = "Received:"
|
||||
const seenHdr = "X-Rspamd-Seen:"
|
||||
const subjHdr = "Subject:"
|
||||
const submitDir = "Submitted"
|
||||
|
||||
var reHELO1 *regexp.Regexp
|
||||
var reHELO2 *regexp.Regexp
|
||||
var reMD *regexp.Regexp
|
||||
var reSELF *regexp.Regexp
|
||||
var reSMTP *regexp.Regexp
|
||||
var protocol int
|
||||
|
||||
func init() {
|
||||
// Received: from muus52.sndsy.ru ([185.235.30.52] verified)
|
||||
reHELO1 = regexp.MustCompile(`^Received: from (\S+) \(.* ?\[\S+\] verified\)`)
|
||||
// Received: from [10.19.5.40] (account edu@vsu.ru HELO edu.vsu.ru)
|
||||
reHELO2 = regexp.MustCompile(`^Received: from \[\S+\] \(.*(?: ?HELO (\S+))\)`)
|
||||
reMD = regexp.MustCompile(`^\s+DomainName\s+=\s+([^;]+);`)
|
||||
reSELF = regexp.MustCompile(`^S (?:<([^>]+)> )?(?:DSN|GROUP|LIST|PBX|PIPE|RULE) \[0\.0\.0\.0\]`)
|
||||
reSMTP = regexp.MustCompile(`^S (?:<([^>]+)> )?(?:SMTP|HTTPU?|AIRSYNC|XIMSS) \[([0-9a-f.:]+)\]`)
|
||||
|
||||
err := setMainDomain()
|
||||
if err != nil {
|
||||
Putline("* Can not detect Main Domain: %v\n", err)
|
||||
}
|
||||
reSELF = regexp.MustCompile(`^S (?:<([^>]+)> )?(ALARM|DSN|GROUP|ICAL|LIST|LSTM|LSTO|MDN|PBX|PIPE|RULE|WEBUSER) \[0\.0\.0\.0\]`)
|
||||
reSMTP = regexp.MustCompile(`^S (?:<([^>]+)> )?(?:SMTP|HTTPU?|AIRSYNC|IMAP|RPOP|XIMSS) \[([0-9a-f.:]+)\]`)
|
||||
}
|
||||
|
||||
func AddHeader(seq int, headers *string) {
|
||||
func AddHeader(seq int, headers []string) {
|
||||
|
||||
hdrs := replaceSpecChars(strings.Join(headers, "\n"))
|
||||
|
||||
if protocol >= 4 {
|
||||
Putline("%d ADDHEADER \"%s\" OK\n", seq, *headers)
|
||||
Putline("%d ADDHEADER \"%s\" OK\n", seq, hdrs)
|
||||
} else {
|
||||
Putline("%d ADDHEADER \"%s\"\n", seq, *headers)
|
||||
Putline("%d ADDHEADER \"%s\"\n", seq, hdrs)
|
||||
}
|
||||
}
|
||||
|
||||
func Discard(seq int) {
|
||||
func AddHeaderWithMirrorTo(seq int, qid int, to []string, discard bool, headers []string,
|
||||
body []byte, outbound bool) {
|
||||
|
||||
if !outbound || len(to) > 0 {
|
||||
seenHdr, err := makeSeen(body)
|
||||
if err != nil {
|
||||
Failure(seq, qid, err)
|
||||
return
|
||||
}
|
||||
headers = append(headers, seenHdr)
|
||||
}
|
||||
|
||||
hdrs := replaceSpecChars(strings.Join(headers, "\n"))
|
||||
|
||||
if protocol >= 4 {
|
||||
|
||||
if len(to) > 0 {
|
||||
mirrorTo := make([]string, 0, len(to))
|
||||
for _, m := range to {
|
||||
mirrorTo = append(mirrorTo, "MIRRORTO \""+m+"\"")
|
||||
}
|
||||
|
||||
if discard {
|
||||
Putline("%d ADDHEADER \"%s\" %s DISCARD\n", seq, hdrs, strings.Join(mirrorTo, " "))
|
||||
} else {
|
||||
Putline("%d ADDHEADER \"%s\" %s OK\n", seq, hdrs, strings.Join(mirrorTo, " "))
|
||||
}
|
||||
|
||||
} else {
|
||||
if discard {
|
||||
Putline("%d ADDHEADER \"%s\" DISCARD\n", seq, hdrs)
|
||||
} else {
|
||||
Putline("%d ADDHEADER \"%s\" OK\n", seq, hdrs)
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
Putline("%d ADDHEADER \"%s\"\n", seq, hdrs)
|
||||
}
|
||||
}
|
||||
|
||||
func Discard(seq int, qid int, from string, rcpts []string) {
|
||||
Putline("* %d [%d]: Action: discard; from %s, rcpts %s\n", seq, qid, from, strings.Join(rcpts, ","))
|
||||
Putline("%d DISCARD\n", seq)
|
||||
}
|
||||
|
||||
@@ -49,14 +105,41 @@ func Intf(seq int, ver string) {
|
||||
Putline("%d INTF %d\n", seq, protocol)
|
||||
}
|
||||
|
||||
func Message(filename *string) (from string, rcpts []string, auth string, ip string, qid int, body []byte, err error) {
|
||||
func MainDomain() (domain string, err error) {
|
||||
|
||||
qid, err = strconv.Atoi((*filename)[strings.LastIndexByte(*filename, '/')+1 : strings.LastIndexByte(*filename, '.')])
|
||||
h, err := os.Open("Settings/Main.settings")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer h.Close()
|
||||
|
||||
var line []byte
|
||||
|
||||
for m := bufio.NewReader(h); ; {
|
||||
|
||||
line, err = m.ReadSlice('\n')
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
h, err := os.Open(*filename)
|
||||
if s := reMD.FindAllStringSubmatch(string(line), -1); s != nil {
|
||||
domain = s[0][1]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func Message(filename string) (from string, rcpts []string, auth string, ip string, helo string,
|
||||
hostname string, qid int, body []byte, seen bool, err error) {
|
||||
|
||||
qid, err = strconv.Atoi((filename)[strings.LastIndexByte(filename, '/')+1 : strings.LastIndexByte(filename, '.')])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
h, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -65,7 +148,9 @@ func Message(filename *string) (from string, rcpts []string, auth string, ip str
|
||||
var line []byte
|
||||
var pos int64
|
||||
|
||||
for m := bufio.NewReader(h); ; {
|
||||
m := bufio.NewReader(h)
|
||||
|
||||
for {
|
||||
|
||||
line, err = m.ReadSlice('\n')
|
||||
if err != nil {
|
||||
@@ -91,14 +176,26 @@ func Message(filename *string) (from string, rcpts []string, auth string, ip str
|
||||
if s := reSMTP.FindAllStringSubmatch(string(line), -1); s != nil {
|
||||
auth = s[0][1]
|
||||
ip = s[0][2]
|
||||
hostname = getHostname(ip)
|
||||
} else if s := reSELF.FindAllStringSubmatch(string(line), -1); s != nil {
|
||||
if len(s[0][1]) > 0 {
|
||||
auth = s[0][1]
|
||||
} else {
|
||||
auth = s[0][2] + "@trusted"
|
||||
}
|
||||
ip = "127.2.4.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rcpts = uniqueNonEmptyElementsOf(rcpts)
|
||||
seen, err = isSeen(m)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if seen {
|
||||
return
|
||||
}
|
||||
|
||||
fi, err := h.Stat()
|
||||
if err != nil {
|
||||
@@ -116,6 +213,10 @@ func Message(filename *string) (from string, rcpts []string, auth string, ip str
|
||||
return
|
||||
}
|
||||
|
||||
helo = getHelo(body)
|
||||
|
||||
rcpts = utils.UniqueSliceElementsNonEmpty(rcpts)
|
||||
|
||||
if from == "" || len(rcpts) == 0 || n < len(body) {
|
||||
err = fmt.Errorf("cgp.Message() error: from='%s', len(to)=%d, auth='%s' ip='%s', size=%d/%d", from, len(rcpts), auth, ip, len(body), n)
|
||||
}
|
||||
@@ -127,6 +228,11 @@ func Ok(seq int) {
|
||||
Putline("%d OK\n", seq)
|
||||
}
|
||||
|
||||
func OkSeen(seq, qid int) {
|
||||
Putline("* %d [%d]: Already seen by Rspamd\n", seq, qid)
|
||||
Putline("%d OK\n", seq)
|
||||
}
|
||||
|
||||
func Putline(format string, a ...interface{}) {
|
||||
s := fmt.Sprintf(format, a...)
|
||||
syscall.Write(int(os.Stdout.Fd()), []byte(s))
|
||||
@@ -136,81 +242,111 @@ func Reject(seq int) {
|
||||
Putline("%d REJECT Try again later\n", seq)
|
||||
}
|
||||
|
||||
func ReplaceSpecChars(msgo *string) *string {
|
||||
func RewriteSubject(seq int, headers []string, subject string, qid int, from string, rcpts []string, body []byte) (err error) {
|
||||
|
||||
var msgn string
|
||||
var firstRecv bool = true
|
||||
var m *bufio.Reader
|
||||
var hdr string
|
||||
var pos int = 0
|
||||
|
||||
for _, symbol := range *msgo {
|
||||
filename := submitDir + "/" + strconv.Itoa(qid) + "rs.sub"
|
||||
filetemp := strings.Replace(filename, "sub", "tmp", 1)
|
||||
|
||||
switch symbol {
|
||||
case rune('\\'):
|
||||
// replace \ -> \\ (CGP backslash)
|
||||
msgn += "\\\\"
|
||||
|
||||
case 0x0000:
|
||||
fallthrough
|
||||
case rune('\r'):
|
||||
continue
|
||||
|
||||
case rune('\n'):
|
||||
// replace \n -> \\e (CGP End-of-Line)
|
||||
msgn += "\\e"
|
||||
|
||||
case rune('\t'):
|
||||
// replace \t -> \\t (CGP Tab)
|
||||
msgn += "\\t"
|
||||
|
||||
case rune('"'):
|
||||
// replace \" -> \\" (CGP quote)
|
||||
msgn += "\\\""
|
||||
|
||||
default:
|
||||
msgn += string(symbol)
|
||||
}
|
||||
}
|
||||
|
||||
return &msgn
|
||||
}
|
||||
|
||||
func setMainDomain() (err error) {
|
||||
|
||||
h, err := os.Open("Settings/Main.settings")
|
||||
fh, err := os.Create(filetemp)
|
||||
if err != nil {
|
||||
return
|
||||
goto fin
|
||||
}
|
||||
defer h.Close()
|
||||
defer fh.Close()
|
||||
|
||||
var line []byte
|
||||
|
||||
for m := bufio.NewReader(h); ; {
|
||||
|
||||
line, err = m.ReadSlice('\n')
|
||||
_, err = fh.WriteString("Return-Path: " + from + "\n")
|
||||
if err != nil {
|
||||
return
|
||||
goto fin
|
||||
}
|
||||
|
||||
if s := reMD.FindAllStringSubmatch(string(line), -1); s != nil {
|
||||
MainDomain = s[0][1]
|
||||
for _, rcpt := range rcpts {
|
||||
_, err = fh.WriteString("Envelope-To: " + rcpt + "\n")
|
||||
if err != nil {
|
||||
goto fin
|
||||
}
|
||||
}
|
||||
|
||||
_, err = fh.WriteString(strings.Join(headers, "\n") + "\n")
|
||||
if err != nil {
|
||||
goto fin
|
||||
}
|
||||
|
||||
m = bufio.NewReader(bytes.NewReader(body))
|
||||
|
||||
for {
|
||||
|
||||
hdr, err = getHeader(m)
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
if len(hdr) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
goto fin
|
||||
}
|
||||
|
||||
pos += len(hdr)
|
||||
|
||||
if hdr == "\n" {
|
||||
// конец RFC5322 заголовка
|
||||
_, err = fh.WriteString(hdr)
|
||||
if err != nil {
|
||||
goto fin
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if firstRecv && strings.HasPrefix(hdr, recvHdr) {
|
||||
|
||||
bs := sha256.Sum224([]byte(utils.NoSpace(hdr)))
|
||||
sum := hex.EncodeToString(bs[:])
|
||||
|
||||
_, err = fh.WriteString(seenHdr + " " + sum + "\n")
|
||||
if err != nil {
|
||||
goto fin
|
||||
}
|
||||
|
||||
_, err = fh.WriteString(hdr)
|
||||
if err != nil {
|
||||
goto fin
|
||||
}
|
||||
|
||||
firstRecv = false
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(hdr, subjHdr) {
|
||||
|
||||
_, err = fh.WriteString(subjHdr + " " + subject + "\n")
|
||||
if err != nil {
|
||||
goto fin
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = fh.WriteString(hdr)
|
||||
if err != nil {
|
||||
goto fin
|
||||
}
|
||||
}
|
||||
|
||||
_, err = fh.Write(body[pos:])
|
||||
if err != nil {
|
||||
goto fin
|
||||
}
|
||||
|
||||
if err = fh.Close(); err != nil {
|
||||
goto fin
|
||||
}
|
||||
|
||||
err = os.Rename(filetemp, filename)
|
||||
|
||||
fin:
|
||||
return
|
||||
}
|
||||
|
||||
func uniqueNonEmptyElementsOf(s []string) []string {
|
||||
|
||||
unique := make(map[string]bool, len(s))
|
||||
us := make([]string, len(unique))
|
||||
|
||||
for _, elem := range s {
|
||||
if len(elem) != 0 {
|
||||
if !unique[elem] {
|
||||
us = append(us, elem)
|
||||
unique[elem] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return us
|
||||
}
|
||||
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
package cgp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/maypok86/otter"
|
||||
)
|
||||
|
||||
func filterNames(names []string) []string {
|
||||
|
||||
fqdn := make([]string, 0, len(names))
|
||||
nonfqdn := make([]string, 0, len(names))
|
||||
|
||||
for _, name := range names {
|
||||
if IsValidDomain(name) {
|
||||
fqdn = append(fqdn, name)
|
||||
} else {
|
||||
nonfqdn = append(nonfqdn, name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(fqdn) > 0 {
|
||||
return fqdn
|
||||
} else {
|
||||
return nonfqdn
|
||||
}
|
||||
}
|
||||
|
||||
func findLongerItem(items []string) (res string) {
|
||||
if len(items) > 0 {
|
||||
var maxlen = 0
|
||||
var pos int
|
||||
for p, item := range items {
|
||||
if len(item) > maxlen {
|
||||
maxlen = len(item)
|
||||
pos = p
|
||||
}
|
||||
}
|
||||
res = items[pos]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func findShorterItem(items []string) (res string) {
|
||||
if len(items) > 0 {
|
||||
var maxlen = len(items[0])
|
||||
var pos int
|
||||
for p, item := range items {
|
||||
if len(item) < maxlen {
|
||||
maxlen = len(item)
|
||||
pos = p
|
||||
}
|
||||
}
|
||||
res = items[pos]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var getHostname = func() func(addr string) (hostname string) {
|
||||
|
||||
cache, _ := otter.MustBuilder[netip.Addr, string](1000).
|
||||
CollectStats().
|
||||
Cost(func(key netip.Addr, value string) uint32 {
|
||||
return 1
|
||||
}).
|
||||
WithVariableTTL().
|
||||
Build()
|
||||
|
||||
return func(addr string) (hostname string) {
|
||||
|
||||
ipAddr, err := netip.ParseAddr(addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
hostname, ok := cache.Get(ipAddr)
|
||||
if !ok {
|
||||
hostname = lookupAddr(ipAddr)
|
||||
if len(hostname) > 0 {
|
||||
cache.Set(ipAddr, hostname, time.Hour)
|
||||
} else {
|
||||
cache.Set(ipAddr, hostname, 5*time.Minute)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
var IsValidDomain = func() func(domain string) bool {
|
||||
rx := regexp.MustCompile(`^(?i)[a-z0-9-]+(\.[a-z0-9-]+)+\.?$`)
|
||||
return func(domain string) bool {
|
||||
return rx.MatchString(domain)
|
||||
}
|
||||
}()
|
||||
|
||||
func lookupAddr(ipAddr netip.Addr) (hostname string) {
|
||||
|
||||
r := &net.Resolver{PreferGo: true}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
names, err := r.LookupAddr(ctx, ipAddr.String())
|
||||
cancel()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(names) == 1 {
|
||||
hostname = names[0]
|
||||
} else if len(names) > 1 {
|
||||
|
||||
found := make([]string, 0, len(names))
|
||||
|
||||
for _, name := range names {
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
ips, err := r.LookupHost(ctx, name)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
ipTest, err := netip.ParseAddr(ip)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if ipTest == ipAddr {
|
||||
found = append(found, name)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch len(found) {
|
||||
case 0:
|
||||
// если ни один name не имеет корректного прямого резольвинга
|
||||
// в исходный ip, выбираем самый длинный.
|
||||
hostname = findLongerItem(filterNames(names))
|
||||
|
||||
case 1:
|
||||
hostname = found[0]
|
||||
|
||||
default:
|
||||
// если несколько name имеют корректный прямой резольвинг
|
||||
// в исходный ip, выбираем самый короткий.
|
||||
hostname = findShorterItem(filterNames(found))
|
||||
}
|
||||
}
|
||||
|
||||
if len(hostname) > 0 {
|
||||
hostname = strings.TrimSuffix(hostname, ".")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
package cgp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"git.vsu.ru/ai/rspamd-cgp/utils"
|
||||
)
|
||||
|
||||
func getHeader(m *bufio.Reader) (hdr string, err error) {
|
||||
|
||||
var c byte
|
||||
var line []byte
|
||||
|
||||
var b strings.Builder
|
||||
b.Grow(384)
|
||||
|
||||
for {
|
||||
c, err = m.ReadByte()
|
||||
if err == io.EOF {
|
||||
if c == 0 {
|
||||
break
|
||||
} else {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if b.Len() == 0 {
|
||||
|
||||
if c == ' ' || c == '\t' {
|
||||
err = m.UnreadByte()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("bad header")
|
||||
return
|
||||
|
||||
} else if c == '\n' {
|
||||
|
||||
b.WriteByte(c)
|
||||
break
|
||||
|
||||
} else {
|
||||
|
||||
b.WriteByte(c)
|
||||
|
||||
line, err = m.ReadSlice('\n')
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b.Write(line)
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
if c == ' ' || c == '\t' {
|
||||
|
||||
b.WriteByte(c)
|
||||
|
||||
line, err = m.ReadSlice('\n')
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b.Write(line)
|
||||
|
||||
} else {
|
||||
err = m.UnreadByte()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hdr = b.String()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getHelo(body []byte) (helo string) {
|
||||
|
||||
var hdr string
|
||||
var err error
|
||||
|
||||
m := bufio.NewReader(bytes.NewReader(body))
|
||||
|
||||
for {
|
||||
|
||||
hdr, err = m.ReadString('\n')
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
if len(hdr) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if hdr == "\n" {
|
||||
// конец RFC5322 заголовка
|
||||
break
|
||||
}
|
||||
|
||||
if strings.HasPrefix(hdr, recvHdr) {
|
||||
if s := reHELO1.FindAllStringSubmatch(hdr, -1); s != nil {
|
||||
helo = s[0][1]
|
||||
} else if s := reHELO2.FindAllStringSubmatch(hdr, -1); s != nil {
|
||||
helo = s[0][1]
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func isSeen(m *bufio.Reader) (seen bool, err error) {
|
||||
|
||||
var found bool
|
||||
var seenSum string
|
||||
var hdr string
|
||||
|
||||
for {
|
||||
|
||||
hdr, err = getHeader(m)
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
if len(hdr) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if hdr == "\n" {
|
||||
// конец RFC5322 заголовка
|
||||
break
|
||||
}
|
||||
|
||||
if !found {
|
||||
|
||||
if seenSum, found = strings.CutPrefix(hdr, seenHdr); found {
|
||||
seenSum = strings.TrimSpace(seenSum)
|
||||
}
|
||||
|
||||
} else if strings.HasPrefix(hdr, recvHdr) {
|
||||
|
||||
bs := sha256.Sum224([]byte(utils.NoSpace(hdr)))
|
||||
sum := hex.EncodeToString(bs[:])
|
||||
if seenSum == sum {
|
||||
seen = true
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func makeSeen(body []byte) (seenhdr string, err error) {
|
||||
|
||||
var hdr string
|
||||
|
||||
m := bufio.NewReader(bytes.NewReader(body))
|
||||
|
||||
for {
|
||||
|
||||
hdr, err = getHeader(m)
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
if len(hdr) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if hdr == "\n" {
|
||||
break
|
||||
}
|
||||
|
||||
if strings.HasPrefix(hdr, recvHdr) {
|
||||
bs := sha256.Sum224([]byte(utils.NoSpace(hdr)))
|
||||
seenhdr = seenHdr + " " + hex.EncodeToString(bs[:])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func replaceSpecChars(msg string) string {
|
||||
|
||||
var sb strings.Builder
|
||||
sb.Grow(len(msg) + 128)
|
||||
|
||||
for _, symbol := range msg {
|
||||
|
||||
switch symbol {
|
||||
case rune('\\'):
|
||||
// replace \ -> \\ (CGP backslash)
|
||||
sb.WriteString("\\\\")
|
||||
|
||||
case 0x0000:
|
||||
fallthrough
|
||||
case rune('\r'):
|
||||
continue
|
||||
|
||||
case rune('\n'):
|
||||
// replace \n -> \\e (CGP End-of-Line)
|
||||
sb.WriteString("\\e")
|
||||
|
||||
case rune('\t'):
|
||||
// replace \t -> \\t (CGP Tab)
|
||||
sb.WriteString("\\t")
|
||||
|
||||
case rune('"'):
|
||||
// replace \" -> \\" (CGP quote)
|
||||
sb.WriteString("\\\"")
|
||||
|
||||
default:
|
||||
sb.WriteRune(symbol)
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package cgp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func NotifyTo(seq int, qid int, to []string, header string, from string, rcpts []string,
|
||||
body []byte, notifyfrom string, desc string) {
|
||||
|
||||
date := time.Now().Format(time.RFC1123Z)
|
||||
mailid := strconv.Itoa(qid)
|
||||
boundary := "nextPart" + mailid + "." + strconv.Itoa(seq)
|
||||
off := bytes.Index(body, []byte("\n\n"))
|
||||
|
||||
var sb strings.Builder
|
||||
sb.Grow(2048 + off)
|
||||
|
||||
sb.WriteString("Return-Path: <>\n")
|
||||
for _, rcpt := range to {
|
||||
sb.WriteString("Envelope-To: " + rcpt + "\n")
|
||||
}
|
||||
sb.WriteString("From: " + notifyfrom + "\n")
|
||||
sb.WriteString("Subject: Notify Case " + mailid + "\n")
|
||||
sb.WriteString("Date: " + date + "\n")
|
||||
sb.WriteString(header + "\n")
|
||||
sb.WriteString("MIME-Version: 1.0\n")
|
||||
sb.WriteString("Content-Type: multipart/mixed; boundary=\"" + boundary + "\"\n")
|
||||
sb.WriteString("\n")
|
||||
|
||||
sb.WriteString("This is a multi-part message in MIME format.\n\n")
|
||||
|
||||
sb.WriteString("--" + boundary + "\n")
|
||||
sb.WriteString("Content-Type: text/plain; charset=UTF-8; format=flowed\n")
|
||||
sb.WriteString("Content-Transfer-Encoding: 8bit\n")
|
||||
sb.WriteString("\n")
|
||||
|
||||
sb.WriteString("mail id: " + mailid + "\n")
|
||||
sb.WriteString("mail from: " + from + "\n")
|
||||
|
||||
sb.WriteString("rcpt to: " + rcpts[0] + "\n")
|
||||
for _, rcpt := range rcpts[1:] {
|
||||
sb.WriteString(" " + rcpt + "\n")
|
||||
}
|
||||
|
||||
sb.WriteString("date: " + date + "\n")
|
||||
sb.WriteString("\n")
|
||||
|
||||
sb.WriteString("case conditions:\n")
|
||||
sb.WriteString("----------------\n")
|
||||
sb.WriteString(desc + "\n")
|
||||
sb.WriteString("\n\n\n")
|
||||
|
||||
sb.WriteString("--" + boundary + "\n")
|
||||
sb.WriteString("Content-Disposition: attachment; filename=\"headers\"\n")
|
||||
sb.WriteString("Content-Type: text/plain; charset=UTF-8\n")
|
||||
sb.WriteString("Content-Transfer-Encoding: 8bit\n")
|
||||
sb.WriteString("\n")
|
||||
|
||||
sb.Write(body[:off+1])
|
||||
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString("--" + boundary + "--\n")
|
||||
|
||||
filename := submitDir + "/" + mailid + "no.sub"
|
||||
filetemp := strings.Replace(filename, "sub", "tmp", 1)
|
||||
|
||||
fh, err := os.Create(filetemp)
|
||||
if err != nil {
|
||||
goto fin
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
_, err = fh.WriteString(sb.String())
|
||||
if err != nil {
|
||||
goto fin
|
||||
}
|
||||
|
||||
if err = fh.Close(); err != nil {
|
||||
goto fin
|
||||
}
|
||||
|
||||
err = os.Rename(filetemp, filename)
|
||||
|
||||
fin:
|
||||
if err != nil {
|
||||
Putline("* %d [%d]: notify: %s\n", seq, qid, err)
|
||||
}
|
||||
}
|
||||
+74
-12
@@ -2,36 +2,98 @@ package config
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/configor"
|
||||
|
||||
"git.vsu.ru/ai/rspamd-cgp/cgp"
|
||||
)
|
||||
|
||||
type Operation struct {
|
||||
Description string
|
||||
Direction string
|
||||
Discard bool
|
||||
MirrorTo []string
|
||||
NotifyRcpts bool
|
||||
NotifyTo []string
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
AuthservId string
|
||||
Discard bool
|
||||
Host string
|
||||
Timeout time.Duration
|
||||
Debug bool
|
||||
Outbound bool
|
||||
Host string `default:"localhost:11333"`
|
||||
Timeout time.Duration `default:"15s"`
|
||||
NotifyFrom string `required:"true"`
|
||||
Actions map[string]*Operation
|
||||
Symbols map[string]*Operation
|
||||
}
|
||||
|
||||
func New() *Config {
|
||||
|
||||
config := new(Config)
|
||||
|
||||
var rejectAction string
|
||||
dir, _ := filepath.Abs(filepath.Dir(os.Args[0]))
|
||||
|
||||
flag.StringVar(&config.AuthservId, "authserv-id", "", "Authentication Identifier (default CommuniGate Pro Main Domain)")
|
||||
flag.StringVar(&config.Host, "host", "localhost:11333", "Rspamd host to connect")
|
||||
flag.StringVar(&rejectAction, "reject-action", "add_header", "Reject action: \"add_header\" or \"discard\"")
|
||||
flag.DurationVar(&config.Timeout, "timeout", 15*time.Second, "Rspamd request timeout")
|
||||
var configfile string
|
||||
var configdump bool
|
||||
var configtest bool
|
||||
var debug bool
|
||||
var outbound bool
|
||||
|
||||
flag.StringVar(&configfile, "config", dir+"/rspamd-cgp.yml", "Set configuration file")
|
||||
flag.BoolVar(&configdump, "configdump", false, "Perform configuration file dump")
|
||||
flag.BoolVar(&configtest, "configtest", false, "Perform configuration file test")
|
||||
flag.BoolVar(&debug, "debug", false, "Run in debug mode")
|
||||
flag.BoolVar(&outbound, "outbound", false, "Outbound message flow processing")
|
||||
flag.Parse()
|
||||
|
||||
if rejectAction == "discard" {
|
||||
config.Discard = true
|
||||
err := configor.Load(config, configfile)
|
||||
if err != nil {
|
||||
fmt.Println("config:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if config.Timeout < time.Second {
|
||||
config.Timeout *= time.Second
|
||||
if debug {
|
||||
config.Debug = debug
|
||||
}
|
||||
|
||||
if outbound {
|
||||
config.Outbound = outbound
|
||||
}
|
||||
|
||||
if configdump {
|
||||
dumpConfig(config)
|
||||
if err = validateConfig(config); err != nil {
|
||||
fmt.Println("config:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if configtest {
|
||||
if err = validateConfig(config); err != nil {
|
||||
fmt.Println("config:", err)
|
||||
os.Exit(1)
|
||||
} else {
|
||||
fmt.Println("config: Syntax OK")
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
if config.AuthservId == "" {
|
||||
if config.AuthservId, err = cgp.MainDomain(); err != nil {
|
||||
fmt.Println("Can not detect Main Domain:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
setOpDefaultDirection(config)
|
||||
|
||||
config.Host = "http://" + config.Host + "/checkv2"
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var reMail *regexp.Regexp
|
||||
|
||||
func dumpConfig(config *Config) {
|
||||
yml, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
fmt.Println("config:", err)
|
||||
} else {
|
||||
fmt.Println(string(yml))
|
||||
}
|
||||
}
|
||||
|
||||
func setOpDefaultDirection(config *Config) {
|
||||
|
||||
setDir := func(entry map[string]*Operation) {
|
||||
for _, op := range entry {
|
||||
if len(op.Direction) == 0 {
|
||||
op.Direction = "both"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setDir(config.Actions)
|
||||
setDir(config.Symbols)
|
||||
}
|
||||
|
||||
func validateConfig(config *Config) (err error) {
|
||||
|
||||
reMail = regexp.MustCompile(`^\S+?@\S+$`)
|
||||
|
||||
if err = validateConfigOp(config.NotifyFrom); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = validateConfigEntry(config.Actions); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = validateConfigEntry(config.Symbols); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func validateConfigEntry(entry map[string]*Operation) (err error) {
|
||||
|
||||
for e, op := range entry {
|
||||
|
||||
if err = validateDirection(op.Direction); err != nil {
|
||||
err = fmt.Errorf("%s: Direction: %v", e, err)
|
||||
break
|
||||
}
|
||||
|
||||
if err = validateConfigOps(op.NotifyTo); err != nil {
|
||||
err = fmt.Errorf("%s: NotifyTo: %v", e, err)
|
||||
break
|
||||
}
|
||||
|
||||
if err = validateConfigOps(op.MirrorTo); err != nil {
|
||||
err = fmt.Errorf("%s: MirrorTo: %v", e, err)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func validateConfigOp(m string) (err error) {
|
||||
|
||||
if !reMail.MatchString(m) {
|
||||
err = fmt.Errorf("invalid mail: %s", m)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func validateConfigOps(mail []string) (err error) {
|
||||
|
||||
for _, m := range mail {
|
||||
if err = validateConfigOp(m); err != nil {
|
||||
err = fmt.Errorf("invalid mail: %s", m)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func validateDirection(dir string) (err error) {
|
||||
|
||||
if dir != "both" && dir != "in" && dir != "out" && dir != "" {
|
||||
err = fmt.Errorf("unknown direction: %s", dir)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
domain.ru
|
||||
@@ -0,0 +1 @@
|
||||
/domain\.ru/i
|
||||
@@ -0,0 +1,41 @@
|
||||
################################################################################
|
||||
# Пример multimap.conf для Rspamd
|
||||
################################################################################
|
||||
|
||||
FAKE_LOCAL_FROM {
|
||||
require_symbols = "!AUTHENTICATED_USER & !TRUSTED_HOST & !CGP_RPOLL";
|
||||
type = "header";
|
||||
header = "from";
|
||||
filter = "email:domain";
|
||||
map = "$LOCAL_CONFDIR/local.d/maps.d/local_domains.map";
|
||||
description = "Fake local mail in From: header";
|
||||
}
|
||||
|
||||
FAKE_LOCAL_FROM_NAME {
|
||||
require_symbols = "!AUTHENTICATED_USER & !TRUSTED_HOST & !CGP_RPOLL";
|
||||
type = "header";
|
||||
header = "from";
|
||||
filter = "email:name";
|
||||
map = "$LOCAL_CONFDIR/local.d/maps.d/local_domains_re.map";
|
||||
regexp = true;
|
||||
description = "Fake local mail in Realname of From: header";
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Используется rspamd-cgp для исключения внешних адресов из рассылки оповещений.
|
||||
# Применяется, если в конфигурационном файле rspamd-cgp.yml указан
|
||||
# notifyrcpts: true
|
||||
#
|
||||
RCPTS_DOMAINS_LOCAL {
|
||||
type = "rcpt";
|
||||
extract_from = "smtp";
|
||||
filter = "email:domain";
|
||||
map = "$LOCAL_CONFDIR/local.d/maps.d/local_domains.map";
|
||||
description = "Recipients domains are local";
|
||||
}
|
||||
|
||||
TRUSTED_HOST {
|
||||
type = "ip";
|
||||
map = "$LOCAL_CONFDIR/local.d/maps.d/trusted_hosts.map";
|
||||
description = "Trusted hosts";
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
local reconf = config['regexp']
|
||||
|
||||
reconf['CGP_RPOLL'] = {
|
||||
description = 'CommuniGate Pro RPOLL Received header',
|
||||
re = 'Received=/with RPOLL /{raw_header}'
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
################################################################################
|
||||
# Пример settings.conf для Rspamd.
|
||||
################################################################################
|
||||
|
||||
################################################################################
|
||||
# Устанавливает символ AUTHENTICATED_USER для аутентифицированных сообщений.
|
||||
#
|
||||
authenticated {
|
||||
priority = high;
|
||||
authenticated = yes;
|
||||
apply {
|
||||
AUTHENTICATED_USER = 0.0;
|
||||
}
|
||||
symbols [
|
||||
"AUTHENTICATED_USER"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
1.2.3.4
|
||||
@@ -1,5 +1,18 @@
|
||||
module git.vsu.ru/ai/rspamd-cgp
|
||||
|
||||
go 1.16
|
||||
go 1.23
|
||||
|
||||
require github.com/json-iterator/go v1.1.10
|
||||
require (
|
||||
github.com/jinzhu/configor v1.2.2
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/maypok86/otter v1.2.4
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.4.0 // indirect
|
||||
github.com/dolthub/maphash v0.1.0 // indirect
|
||||
github.com/gammazero/deque v1.0.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
)
|
||||
|
||||
@@ -1,12 +1,32 @@
|
||||
github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
|
||||
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
|
||||
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
|
||||
github.com/gammazero/deque v1.0.0 h1:LTmimT8H7bXkkCy6gZX7zNLtkbz4NdS2z8LZuor3j34=
|
||||
github.com/gammazero/deque v1.0.0/go.mod h1:iflpYvtGfM3U8S8j+sZEKIak3SAKYpA5/SQewgfXDKo=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||
github.com/jinzhu/configor v1.2.2 h1:sLgh6KMzpCmaQB4e+9Fu/29VErtBUqsS2t8C9BNIVsA=
|
||||
github.com/jinzhu/configor v1.2.2/go.mod h1:iFFSfOBKP3kC2Dku0ZGB3t3aulfQgTGJknodhFavsU8=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/maypok86/otter v1.2.4 h1:HhW1Pq6VdJkmWwcZZq19BlEQkHtI8xgsQzBVXJU0nfc=
|
||||
github.com/maypok86/otter v1.2.4/go.mod h1:mKLfoI7v1HOmQMwFgX4QkRk23mX6ge3RDvjdHOWG4R4=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
|
||||
"git.vsu.ru/ai/rspamd-cgp/cgp"
|
||||
"git.vsu.ru/ai/rspamd-cgp/config"
|
||||
"git.vsu.ru/ai/rspamd-cgp/rspamc"
|
||||
)
|
||||
|
||||
@@ -18,6 +19,8 @@ func main() {
|
||||
var err error
|
||||
var n int
|
||||
|
||||
config := config.New()
|
||||
|
||||
in := bufio.NewReader(os.Stdin)
|
||||
|
||||
finalize:
|
||||
@@ -36,7 +39,7 @@ finalize:
|
||||
|
||||
switch {
|
||||
case cmd == "FILE" && n == 3:
|
||||
go rspamc.Scan(seq, arg)
|
||||
go rspamc.Scan(config, seq, arg)
|
||||
|
||||
case cmd == "INTF" && n == 3:
|
||||
cgp.Intf(seq, arg)
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
package rspamc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
json "github.com/json-iterator/go"
|
||||
|
||||
"git.vsu.ru/ai/rspamd-cgp/cgp"
|
||||
"git.vsu.ru/ai/rspamd-cgp/config"
|
||||
"git.vsu.ru/ai/rspamd-cgp/utils"
|
||||
)
|
||||
|
||||
func filterLocalRcpts(rcpts []string, res map[string]interface{}) []string {
|
||||
|
||||
filtered := make([]string, 0, len(rcpts))
|
||||
|
||||
if v, ok := res["symbols"].(map[string]interface{})["RCPTS_DOMAINS_LOCAL"]; ok {
|
||||
if domains, ok := v.(map[string]interface{})["options"]; ok {
|
||||
for _, rcpt := range rcpts {
|
||||
for _, domain := range domains.([]interface{}) {
|
||||
if rcpt[strings.IndexByte(rcpt, '@')+1:len(rcpt)-1] == domain.(string) {
|
||||
filtered = append(filtered, rcpt)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
func isOpAccept(direction string, outbound bool) (a bool) {
|
||||
|
||||
if outbound {
|
||||
|
||||
if direction == "out" || direction == "both" {
|
||||
a = true
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
if direction == "in" || direction == "both" {
|
||||
a = true
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func makeHeaders(res map[string]interface{}) (headers []string) {
|
||||
|
||||
if _, ok := res["dkim-signature"]; ok {
|
||||
headers = append(headers, "DKIM-Signature: "+res["dkim-signature"].(string))
|
||||
}
|
||||
|
||||
if milter, ok := res["milter"]; ok {
|
||||
if hdrs, ok := milter.(map[string]interface{})["add_headers"]; ok {
|
||||
if reflect.TypeOf(hdrs).String() == "map[string]interface {}" {
|
||||
for h, vh := range hdrs.(map[string]interface{}) {
|
||||
if reflect.TypeOf(vh).String() == "map[string]interface {}" {
|
||||
if v, ok := vh.(map[string]interface{})["value"].(string); ok && v != "" {
|
||||
headers = append(headers, h+": "+v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func makeHeadersOutbound(res map[string]interface{}) (headers []string) {
|
||||
|
||||
if _, ok := res["dkim-signature"]; ok {
|
||||
headers = append(headers, "DKIM-Signature: "+res["dkim-signature"].(string))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func makeOpSum(conf *config.Config, res map[string]interface{}, action string) (*config.Operation, string, string, string) {
|
||||
|
||||
var casea []string
|
||||
var desca []string
|
||||
opsum := new(config.Operation)
|
||||
|
||||
if op, ok := conf.Actions[action]; ok {
|
||||
if isOpAccept(conf.Actions[action].Direction, conf.Outbound) {
|
||||
opsum.Discard = op.Discard
|
||||
opsum.MirrorTo = op.MirrorTo
|
||||
opsum.NotifyRcpts = op.NotifyRcpts
|
||||
opsum.NotifyTo = op.NotifyTo
|
||||
casea = append(casea, action)
|
||||
|
||||
if len(op.Description) > 0 {
|
||||
desca = append(desca, action+": "+op.Description)
|
||||
} else {
|
||||
desca = append(desca, action)
|
||||
}
|
||||
|
||||
if conf.Debug {
|
||||
printSelectedOp("Action", action, conf.Actions[action].Direction, conf.Outbound)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for symbol, op := range conf.Symbols {
|
||||
if isOpAccept(conf.Symbols[symbol].Direction, conf.Outbound) {
|
||||
if v, ok := res["symbols"].(map[string]interface{})[symbol]; ok {
|
||||
opsum.Discard = opsum.Discard || op.Discard
|
||||
opsum.MirrorTo = append(opsum.MirrorTo, op.MirrorTo...)
|
||||
opsum.NotifyRcpts = opsum.NotifyRcpts || op.NotifyRcpts
|
||||
opsum.NotifyTo = append(opsum.NotifyTo, op.NotifyTo...)
|
||||
casea = append(casea, symbol)
|
||||
|
||||
if len(op.Description) > 0 {
|
||||
desca = append(desca, symbol+": "+op.Description)
|
||||
} else {
|
||||
if desc, ok := v.(map[string]interface{})["description"]; ok {
|
||||
desca = append(desca, symbol+": "+desc.(string))
|
||||
} else {
|
||||
desca = append(desca, symbol)
|
||||
}
|
||||
}
|
||||
|
||||
if conf.Debug {
|
||||
printSelectedOp("Symbol", symbol, conf.Symbols[symbol].Direction, conf.Outbound)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(casea) > 0 {
|
||||
|
||||
var sb strings.Builder
|
||||
sb.Grow(256)
|
||||
|
||||
sb.WriteString("discard ")
|
||||
sb.WriteString(strconv.FormatBool(opsum.Discard))
|
||||
|
||||
sb.WriteString("; notifyrcpts ")
|
||||
sb.WriteString(strconv.FormatBool(opsum.NotifyRcpts))
|
||||
|
||||
if len(opsum.MirrorTo) > 0 {
|
||||
opsum.MirrorTo = utils.UniqueSliceElementsNonEmpty(opsum.MirrorTo)
|
||||
sb.WriteString("; mirrorto ")
|
||||
sb.WriteString(strings.Join(opsum.MirrorTo, ","))
|
||||
}
|
||||
|
||||
if len(opsum.NotifyTo) > 0 {
|
||||
opsum.NotifyTo = utils.UniqueSliceElementsNonEmpty(opsum.NotifyTo)
|
||||
sb.WriteString("; notifyto ")
|
||||
sb.WriteString(strings.Join(opsum.NotifyTo, ","))
|
||||
}
|
||||
|
||||
return opsum, strings.Join(casea, ","), sb.String(), strings.Join(desca, "\n")
|
||||
}
|
||||
|
||||
return nil, "", "", ""
|
||||
}
|
||||
|
||||
func printMsgInfo(from string, rcpts []string, auth string, ip string, helo string, hostname string,
|
||||
qid int, seen bool) {
|
||||
|
||||
fmt.Fprintln(os.Stderr, "from: ", from)
|
||||
fmt.Fprintln(os.Stderr, "rcpts: ", rcpts)
|
||||
fmt.Fprintln(os.Stderr, "ip: ", ip)
|
||||
fmt.Fprintln(os.Stderr, "helo: ", helo)
|
||||
fmt.Fprintln(os.Stderr, "hostname:", hostname)
|
||||
fmt.Fprintln(os.Stderr, "qid: ", qid)
|
||||
if len(auth) > 0 {
|
||||
fmt.Fprintln(os.Stderr, "auth: ", auth)
|
||||
} else {
|
||||
fmt.Fprintln(os.Stderr, "auth: not authenticated")
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, "seen: ", seen)
|
||||
fmt.Fprintln(os.Stderr, "")
|
||||
}
|
||||
|
||||
func printSelectedOp(optype, opname, direction string, outbound bool) {
|
||||
|
||||
if outbound {
|
||||
fmt.Fprintf(os.Stderr, "%s '%s' selected for outbound flow: direction %s\n", optype, opname, direction)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "%s '%s' selected for inbound flow: direction %s\n", optype, opname, direction)
|
||||
}
|
||||
}
|
||||
|
||||
func printResponse(v any) {
|
||||
printed, _ := json.MarshalIndent(v, "", " ")
|
||||
fmt.Fprintln(os.Stderr, string(printed), "\n")
|
||||
}
|
||||
|
||||
func procAction(seq int, qid int, opsum *config.Operation, res map[string]interface{},
|
||||
headers []string, hci string, from string, rcpts []string, body []byte, notifyfrom string,
|
||||
desc string, outbound bool, t int) {
|
||||
|
||||
if opsum != nil {
|
||||
|
||||
if opsum.Discard {
|
||||
cgp.Putline("* %d [%d]: Action: discard; from %s, rcpts %s\n", seq, qid, from, strings.Join(rcpts, ","))
|
||||
cgp.AddHeaderWithMirrorTo(seq, qid, opsum.MirrorTo, opsum.Discard, headers, body, outbound)
|
||||
} else {
|
||||
to := utils.DiffSlice(opsum.MirrorTo, rcpts)
|
||||
cgp.AddHeaderWithMirrorTo(seq, qid, to, opsum.Discard, headers, body, outbound)
|
||||
}
|
||||
|
||||
if len(opsum.NotifyTo) > 0 || opsum.NotifyRcpts {
|
||||
if opsum.NotifyRcpts {
|
||||
to := make([]string, 0, len(opsum.NotifyTo)+len(rcpts))
|
||||
to = append(to, opsum.NotifyTo...)
|
||||
rcpts_filtered := filterLocalRcpts(rcpts, res)
|
||||
to = append(to, rcpts_filtered...)
|
||||
to = utils.UniqueSliceElementsNonEmpty(to)
|
||||
cgp.NotifyTo(seq, qid, to, hci, from, rcpts, body, notifyfrom, desc)
|
||||
} else {
|
||||
cgp.NotifyTo(seq, qid, opsum.NotifyTo, hci, from, rcpts, body, notifyfrom, desc)
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
switch t {
|
||||
case 0:
|
||||
if len(headers) > 0 {
|
||||
cgp.AddHeader(seq, headers)
|
||||
} else {
|
||||
cgp.Ok(seq)
|
||||
}
|
||||
|
||||
case 1:
|
||||
cgp.AddHeader(seq, headers)
|
||||
|
||||
case 2:
|
||||
cgp.Discard(seq, qid, from, rcpts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func procActionRS(seq int, qid int, opsum *config.Operation, res map[string]interface{},
|
||||
headers []string, hci string, subject string, from string, rcpts []string, body []byte,
|
||||
notifyfrom string, desc string, outbound bool) {
|
||||
|
||||
err := cgp.RewriteSubject(seq, headers, subject, qid, from, rcpts, body)
|
||||
if err != nil {
|
||||
cgp.Failure(seq, qid, err)
|
||||
} else {
|
||||
|
||||
if opsum != nil {
|
||||
cgp.AddHeaderWithMirrorTo(seq, qid, opsum.MirrorTo, opsum.Discard, headers, body, outbound)
|
||||
if len(opsum.NotifyTo) > 0 || opsum.NotifyRcpts {
|
||||
if opsum.NotifyRcpts {
|
||||
to := make([]string, 0, len(opsum.NotifyTo)+len(rcpts))
|
||||
to = append(to, opsum.NotifyTo...)
|
||||
rcpts_filtered := filterLocalRcpts(rcpts, res)
|
||||
to = append(to, rcpts_filtered...)
|
||||
to = utils.UniqueSliceElementsNonEmpty(to)
|
||||
cgp.NotifyTo(seq, qid, to, hci, from, rcpts, body, notifyfrom, desc)
|
||||
} else {
|
||||
cgp.NotifyTo(seq, qid, opsum.NotifyTo, hci, from, rcpts, body, notifyfrom, desc)
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
cgp.Discard(seq, qid, from, rcpts)
|
||||
}
|
||||
}
|
||||
}
|
||||
+75
-73
@@ -2,75 +2,52 @@ package rspamc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
json "github.com/json-iterator/go"
|
||||
|
||||
"git.vsu.ru/ai/rspamd-cgp/cgp"
|
||||
"git.vsu.ru/ai/rspamd-cgp/config"
|
||||
)
|
||||
|
||||
var (
|
||||
const (
|
||||
headerCase string = "X-Rspamd-Case: "
|
||||
headerJunkG string = "X-Junk-Score: [XX]"
|
||||
headerJunkA string = "X-Junk-Score: [XXXX]"
|
||||
headerJunkR string = "X-Junk-Score: [XXXXXXXXXX]"
|
||||
)
|
||||
|
||||
var authservId string
|
||||
var client *http.Client
|
||||
var discard bool
|
||||
var host string
|
||||
|
||||
func init() {
|
||||
|
||||
config := config.New()
|
||||
|
||||
if config.AuthservId != "" {
|
||||
authservId = config.AuthservId
|
||||
} else {
|
||||
authservId = cgp.MainDomain
|
||||
}
|
||||
|
||||
discard = config.Discard
|
||||
host = "http://" + config.Host + "/checkv2"
|
||||
func Scan(conf *config.Config, seq int, filename string) {
|
||||
|
||||
tr := &http.Transport{
|
||||
DisableCompression: true,
|
||||
}
|
||||
|
||||
client = &http.Client{
|
||||
Timeout: config.Timeout,
|
||||
client := &http.Client{
|
||||
Timeout: conf.Timeout,
|
||||
Transport: tr,
|
||||
}
|
||||
}
|
||||
|
||||
func appendHeaders(hdrs, junk *string) *string {
|
||||
if *hdrs != "" {
|
||||
headers := *hdrs + "\n" + *junk
|
||||
return &headers
|
||||
} else {
|
||||
return junk
|
||||
}
|
||||
}
|
||||
|
||||
func Scan(seq int, filename string) {
|
||||
|
||||
from, rcpts, auth, ip, qid, body, err := cgp.Message(&filename)
|
||||
from, rcpts, auth, ip, helo, hostname, qid, body, seen, err := cgp.Message(filename)
|
||||
if err != nil {
|
||||
cgp.Failure(seq, qid, err)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", host, bytes.NewReader(body))
|
||||
if seen {
|
||||
cgp.OkSeen(seq, qid)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", conf.Host, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
cgp.Failure(seq, qid, err)
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Add("MTA-Tag", authservId)
|
||||
req.Header.Add("MTA-Tag", conf.AuthservId)
|
||||
req.Header.Add("User-Agent", "rspamd-cgp")
|
||||
req.Header.Add("From", from)
|
||||
req.Header.Add("Queue-ID", strconv.Itoa(qid))
|
||||
@@ -80,6 +57,12 @@ func Scan(seq int, filename string) {
|
||||
if len(ip) > 0 {
|
||||
req.Header.Add("IP", ip)
|
||||
}
|
||||
if len(helo) > 0 {
|
||||
req.Header.Add("Helo", helo)
|
||||
}
|
||||
if len(hostname) > 0 {
|
||||
req.Header.Add("Hostname", hostname)
|
||||
}
|
||||
for _, rcpt := range rcpts {
|
||||
req.Header.Add("Rcpt", rcpt)
|
||||
}
|
||||
@@ -97,65 +80,84 @@ func Scan(seq int, filename string) {
|
||||
return
|
||||
}
|
||||
|
||||
var js map[string]interface{}
|
||||
if err := json.Unmarshal(rbody, &js); err != nil {
|
||||
var res map[string]interface{}
|
||||
if err := json.Unmarshal(rbody, &res); err != nil {
|
||||
cgp.Failure(seq, qid, err)
|
||||
return
|
||||
}
|
||||
|
||||
var headers string
|
||||
|
||||
if _, ok := js["dkim-signature"]; ok {
|
||||
headers = "DKIM-Signature: " + js["dkim-signature"].(string)
|
||||
if conf.Debug {
|
||||
printMsgInfo(from, rcpts, auth, ip, helo, hostname, qid, seen)
|
||||
printResponse(res)
|
||||
}
|
||||
|
||||
if milter, ok := js["milter"]; ok {
|
||||
if hdrs, ok := milter.(map[string]interface{})["add_headers"]; ok {
|
||||
for h, vh := range hdrs.(map[string]interface{}) {
|
||||
if v, ok := vh.(map[string]interface{})["value"].(string); ok && v != "" {
|
||||
if headers != "" {
|
||||
headers += "\n" + h + ": " + v
|
||||
action := res["action"].(string)
|
||||
opsum, caseinfo, casework, desc := makeOpSum(conf, res, action)
|
||||
|
||||
headers := make([]string, 0, 8)
|
||||
|
||||
if conf.Outbound {
|
||||
headers = makeHeadersOutbound(res)
|
||||
} else {
|
||||
headers = h + ": " + v
|
||||
headers = makeHeaders(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
action := js["action"]
|
||||
|
||||
cgp.Putline("* %d [%d]: Action: %s; Score: %.2f/%.2f; Time elapsed: %.3fs\n",
|
||||
seq, qid, action, js["score"], js["required_score"], js["time_real"])
|
||||
seq, qid, action, res["score"], res["required_score"], res["time_real"])
|
||||
|
||||
var hci string
|
||||
|
||||
if opsum != nil {
|
||||
cgp.Putline("* %d [%d]: Case: %s; %s\n", seq, qid, caseinfo, casework)
|
||||
hci = headerCase + caseinfo
|
||||
if !conf.Outbound {
|
||||
headers = append(headers, hci)
|
||||
}
|
||||
}
|
||||
|
||||
switch action {
|
||||
case "no action":
|
||||
if headers != "" {
|
||||
cgp.AddHeader(seq, cgp.ReplaceSpecChars(&headers))
|
||||
} else {
|
||||
cgp.Ok(seq)
|
||||
}
|
||||
procAction(seq, qid, opsum, res, headers, hci, from, rcpts, body, conf.NotifyFrom, desc, conf.Outbound, 0)
|
||||
|
||||
case "reject":
|
||||
if discard {
|
||||
cgp.Discard(seq)
|
||||
} else {
|
||||
cgp.AddHeader(seq, cgp.ReplaceSpecChars(appendHeaders(&headers, &headerJunkR)))
|
||||
case "discard":
|
||||
if !conf.Outbound {
|
||||
headers = append(headers, headerJunkR)
|
||||
}
|
||||
procAction(seq, qid, opsum, res, headers, hci, from, rcpts, body, conf.NotifyFrom, desc, conf.Outbound, 2)
|
||||
|
||||
case "quarantine":
|
||||
fallthrough
|
||||
case "reject":
|
||||
if !conf.Outbound {
|
||||
headers = append(headers, headerJunkR)
|
||||
}
|
||||
procAction(seq, qid, opsum, res, headers, hci, from, rcpts, body, conf.NotifyFrom, desc, conf.Outbound, 1)
|
||||
|
||||
case "rewrite subject":
|
||||
fallthrough
|
||||
if subject, ok := res["subject"]; ok {
|
||||
procActionRS(seq, qid, opsum, res, headers, hci, subject.(string), from, rcpts, body, conf.NotifyFrom, desc, conf.Outbound)
|
||||
} else {
|
||||
if !conf.Outbound {
|
||||
headers = append(headers, headerJunkA)
|
||||
}
|
||||
procAction(seq, qid, opsum, res, headers, hci, from, rcpts, body, conf.NotifyFrom, desc, conf.Outbound, 1)
|
||||
}
|
||||
|
||||
case "add header":
|
||||
cgp.AddHeader(seq, cgp.ReplaceSpecChars(appendHeaders(&headers, &headerJunkA)))
|
||||
if !conf.Outbound {
|
||||
headers = append(headers, headerJunkA)
|
||||
}
|
||||
procAction(seq, qid, opsum, res, headers, hci, from, rcpts, body, conf.NotifyFrom, desc, conf.Outbound, 1)
|
||||
|
||||
case "greylist":
|
||||
cgp.AddHeader(seq, cgp.ReplaceSpecChars(appendHeaders(&headers, &headerJunkG)))
|
||||
|
||||
fallthrough
|
||||
case "soft reject":
|
||||
cgp.Reject(seq)
|
||||
if !conf.Outbound {
|
||||
headers = append(headers, headerJunkG)
|
||||
}
|
||||
procAction(seq, qid, opsum, res, headers, hci, from, rcpts, body, conf.NotifyFrom, desc, conf.Outbound, 1)
|
||||
|
||||
default:
|
||||
cgp.Failure(seq, qid, fmt.Errorf("Unknown action: %v", action))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
################################################################################
|
||||
# rspamd-cgp config file
|
||||
################################################################################
|
||||
|
||||
################################################################################
|
||||
# Устанавливает значение authserv-id в заголовке Authentication-Results, RFC7001
|
||||
# Если не задан, ему присваивается имя главного домена CommuniGate Pro.
|
||||
#
|
||||
#authservid: mx.domain.ru
|
||||
|
||||
################################################################################
|
||||
# Адрес хоста и порт Rspamd, тайм-аут.
|
||||
#
|
||||
host: 127.0.0.1:11333
|
||||
timeout: 15s
|
||||
|
||||
################################################################################
|
||||
# Устанавливает значение заголовка From: в оповещениях.
|
||||
#
|
||||
notifyfrom: rspamd-cgp-notify@domain.ru
|
||||
|
||||
################################################################################
|
||||
# Секция описывает действия (actions) и дополнительную обработку для каждого
|
||||
# действия. Здесь указываются действия, которым нужна дополнительная обработка.
|
||||
#
|
||||
# discard: # название действия
|
||||
# description: "discard action" # короткое описание действия
|
||||
# direction: in|out|both # применить действие для направления
|
||||
# discard: true # доставить сообщение или выбросить
|
||||
# notifyrcpts: false # оповещать ли получателей сообщения
|
||||
# notifyto: [] # список получателей оповещения
|
||||
# mirrorto: [aa@ba.ru, cc@dd.ru] # список получателей копии сообщения
|
||||
#
|
||||
actions:
|
||||
discard:
|
||||
description: "discard action"
|
||||
discard: true
|
||||
notifyrcpts: false
|
||||
notifyto: []
|
||||
mirrorto: [a@b.r, c@d.r]
|
||||
|
||||
################################################################################
|
||||
# Секция описывает символы (SYMBOLs) и дополнительную обработку для каждого
|
||||
# символа. Здесь указываются символы, которым нужна дополнительная обработка.
|
||||
#
|
||||
# FAKE_LOCAL_FROM: # название символа
|
||||
# description: "" # по умолчанию берётся из ответа Rspamd
|
||||
# direction: in|out|both # применить символ для направления
|
||||
# discard: true # доставить сообщение или выбросить
|
||||
# notifyrcpts: false # оповещать ли получателей сообщения
|
||||
# notifyto: [] # список получателей оповещения
|
||||
# mirrorto: [aa@ba.ru, cc@dd.ru] # список получателей копии сообщения
|
||||
#
|
||||
symbols:
|
||||
FAKE_LOCAL_FROM:
|
||||
discard: true
|
||||
notifyto: [a@b.ru]
|
||||
mirrorto: [c@d.ru]
|
||||
|
||||
FAKE_LOCAL_FROM_NAME:
|
||||
discard: true
|
||||
notifyto: [a@b.ru]
|
||||
mirrorto: [c@d.ru]
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,54 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
func DiffSlice[T comparable](s1, s2 []T) []T {
|
||||
|
||||
s2m := make(map[T]bool, len(s2))
|
||||
for _, v := range s2 {
|
||||
s2m[v] = true
|
||||
}
|
||||
|
||||
var diff []T
|
||||
for _, v := range s1 {
|
||||
if _, found := s2m[v]; !found {
|
||||
diff = append(diff, v)
|
||||
}
|
||||
}
|
||||
|
||||
return diff
|
||||
}
|
||||
|
||||
func NoSpace(s string) string {
|
||||
|
||||
var b strings.Builder
|
||||
b.Grow(len(s))
|
||||
|
||||
for _, c := range s {
|
||||
if !unicode.IsSpace(c) {
|
||||
b.WriteRune(c)
|
||||
}
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func UniqueSliceElementsNonEmpty[T ~string](s []T) []T {
|
||||
|
||||
unique := make([]T, 0, len(s))
|
||||
seen := make(map[T]bool, len(s))
|
||||
|
||||
for _, e := range s {
|
||||
if len(e) > 0 {
|
||||
if !seen[e] {
|
||||
unique = append(unique, e)
|
||||
seen[e] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return unique
|
||||
}
|
||||
Vendored
-7
@@ -1,7 +0,0 @@
|
||||
# github.com/json-iterator/go v1.1.10
|
||||
## explicit
|
||||
github.com/json-iterator/go
|
||||
# github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421
|
||||
github.com/modern-go/concurrent
|
||||
# github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742
|
||||
github.com/modern-go/reflect2
|
||||
Reference in New Issue
Block a user