64 Commits

Author SHA1 Message Date
ai e0d11b742f обработан тип сообщений ICAL 2025-01-30 10:57:53 +03:00
ai f896c4ad0e переход на стандартный encoding/json по результатам тестирования производительности 2025-01-25 22:01:23 +03:00
ai eabe7b6eb0 форматирование 2025-01-25 14:40:00 +03:00
ai 3ebf21580a реализована поддержка direction для action и SYMBOLs 2025-01-05 16:53:23 +03:00
ai c0550a18fe обновление зависимостей 2025-01-04 15:00:09 +03:00
ai f23bd04dbe доработка README 2024-12-21 17:30:38 +03:00
ai 768cfd4e61 обработка actions и SYMBOLs при outbound 2024-12-21 17:18:30 +03:00
ai 265eacb356 доработка README 2024-12-19 20:03:35 +03:00
ai 27d5096d56 добавлена обработка исходящей почты 2024-12-19 16:58:47 +03:00
ai a5bb18126d доработка README 2024-12-19 12:17:05 +03:00
ai 1bcab2359d доработка README 2024-12-19 12:07:41 +03:00
ai 23e91eaee7 доработка README 2024-12-19 12:05:44 +03:00
ai 82618ee86d доработка README 2024-12-19 11:48:53 +03:00
ai 18bcfbe69e добавлен ключ командной строки debug 2024-12-19 10:16:08 +03:00
ai bb4e1a6fb8 доработка README 2024-12-13 10:43:32 +03:00
ai 88dce405fc доработка README 2024-12-12 23:32:50 +03:00
ai eec1effcb2 доработка README 2024-12-12 23:20:32 +03:00
ai 4aec643190 доработка README 2024-12-10 11:35:44 +03:00
ai 7b5ee82552 добавлено информационное сообщение при discard 2024-12-06 19:41:29 +03:00
ai 6711b3e93f исправление: отсутствовало закрытие файла 2024-12-05 23:41:00 +03:00
ai 36e44058f8 поддержка Hostname протокола Rspamd 2024-12-05 23:39:42 +03:00
ai fc83af2612 форматирование 2024-12-05 00:38:55 +03:00
ai 12be6f9542 исправлено название символа 2024-12-05 00:28:47 +03:00
ai cbf3d3c3ea обновление README 2024-12-02 13:28:40 +03:00
ai 7e6092c4b1 документирование конфигурационного файла 2024-12-02 13:20:08 +03:00
ai 12fbcdd400 поддержка Helo протокола Rspamd 2024-12-02 13:17:25 +03:00
ai b6c8138cc5 maps examples 2024-12-01 09:30:50 +03:00
ai a46f66a090 RPOP обрабатывается в Rspamd 2024-12-01 00:45:05 +03:00
ai 1bb820fee0 добавлена печать при debug 2024-12-01 00:43:37 +03:00
ai a55162d803 RPOP обрабатывается в Rspamd 2024-12-01 00:37:39 +03:00
ai f70e2f46e1 примеры конфигурационных файлов Rspamd 2024-11-26 21:10:36 +03:00
ai 6e49551391 расширен список внутренних источников данных CGP 2024-11-26 17:25:38 +03:00
ai 2fc0afacac расширен список внутренних источников данных CGP 2024-11-25 14:08:53 +03:00
ai 76428fc6ff исправление: не устанавливался признак аутентифицированности для сообщений, порождённых самим CGP 2024-11-19 00:13:44 +03:00
ai 3403b432af исправление: не устанавливался признак аутентифицированности для сообщений, порождённых самим CGP 2024-11-19 00:11:00 +03:00
ai 13a08b3725 посылка оповещений только локальным rcpts 2024-11-17 23:46:57 +03:00
ai 4ed58b477b удаление ненужного параметра self 2024-11-17 23:44:26 +03:00
ai 5b302def1b замена поля NotifySelf -> NotifyRcpts 2024-11-17 23:42:50 +03:00
ai b4c6d972de добавлены printAuth() и импорт описания символа из Rspamd 2024-11-17 15:36:01 +03:00
ai a2c7940ba1 разработка и оптимизация Rewrite Subject 2024-11-17 00:54:09 +03:00
ai 9ade57dc0c добавлены параметры 2024-11-17 00:50:42 +03:00
ai fe3d70f2fb разработка 2024-11-15 22:21:07 +03:00
ai 229929ee38 разработка и рефакторинг 2024-11-15 22:06:18 +03:00
ai 2c26e3ec35 оптимизация и рефакторинг 2024-11-15 22:03:24 +03:00
ai d1aafd582b добавлен NotifyFrom 2024-11-15 22:01:29 +03:00
ai 0e3bea28ff добавлены функции DiffSlice и NoSpace 2024-11-15 21:59:05 +03:00
ai fb55c39e0b добавлены оповещения 2024-11-15 21:57:13 +03:00
ai 49ac3dfe43 изменены права на файл 2024-11-09 12:40:32 +03:00
ai d14a95d49e изменены права на файл 2024-11-09 12:40:05 +03:00
ai c6772637a6 внутренние функции вынесены в отдельный файл 2024-11-04 20:00:01 +03:00
ai ca22973c25 переработано 2024-11-04 19:18:45 +03:00
ai db2f22658a внутренние функции вынесены в отдельный файл 2024-11-04 19:17:20 +03:00
ai efa8212eab удалена неиспользуемая функция 2024-11-04 19:16:13 +03:00
ai dc48f83f7e изменено название типа 2024-11-04 19:15:16 +03:00
ai 88a9cbcb1c изменено название типа, переупорядочены поля структуры 2024-11-04 19:14:13 +03:00
ai 8fac3092f2 приведена в соответствие go.mod, go.sum 2024-11-04 11:50:22 +03:00
ai 5e25542fec изменена работа с конфигом 2024-11-04 11:49:15 +03:00
ai 281b3a84a5 переработан конфиг 2024-11-04 11:48:32 +03:00
ai 2dbf407442 переработано 2024-11-04 11:46:58 +03:00
ai ec85ae1684 добавлен go get 2024-11-04 11:43:28 +03:00
ai a13c4b63f0 изменение версии v1.5.7 2024-10-27 15:57:02 +03:00
ai 2f5fc86d3d go vet с поддиректориями 2024-10-27 15:45:09 +03:00
ai 008f82f0f4 обновление модулей 2024-10-27 15:41:44 +03:00
ai 65a41c26db добавлено условие update 2024-10-27 15:39:55 +03:00
26 changed files with 1447 additions and 449 deletions
View File
+103 -14
View File
@@ -1,20 +1,109 @@
Rspamd helper for CommuniGate Pro 5.x, 6.x ## Rspamd helper for CommuniGate Pro 6.x, *версия 2.0.0*
Copyright (C) 2017-2023 Andrey Igoshin <ai@vsu.ru> ### Введение
Version 1.5.6
https://git.vsu.ru/ai/rspamd-cgp *Помощник* (внешний фильтр сообщений) **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).
### Установка
Конфигурационный файл *Помощника* [rspamd-cgp.yml](rspamd-cgp.yml) по умолчанию находится в той же директории, что и исполняемый файл **rspamd-cgp**. При необходимости другое местоположение конфигурационного файла можно указать в командной строке. Возможные настройки *Помощника* подробно описаны в конфигурационном файле.
> Ниже показаны настройки *Помощника* и *Правила* в интерфейсе *CommuniGate Pro*.
#### Для входящих сообщений
##### Установки -> Общее -> Помощники
![](./helper-in.png)
##### Установки -> Почта -> Правила -> RSPAMD_in
![](./rule-in.png)
#### Для исходящих сообщений
##### Установки -> Общее -> Помощники
![](./helper-out.png)
##### Установки -> Почта -> Правила -> RSPAMD_out
![](./rule-out.png)
#### Параметры командной строки
```
Usage of rspamd-cgp: Usage of rspamd-cgp:
-authserv-id string -config string
Authentication Identifier (default CommuniGate Pro Main Domain) Set configuration file (default "rspamd-cgp.yml")
-host string -configdump
Rspamd host to connect (default "localhost:11333") Perform configuration file dump
-mirror-discard -configtest
Mirror then discard selected messages Perform configuration file test
-mirror-to string -debug
Mirror selected messages to email Run in debug mode
-timeout duration -outbound
Rspamd request timeout (default 15s) 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>
+8 -2
View File
@@ -7,12 +7,18 @@ export GOPATH="${HOME}/src/rspamd-cgp"
export CGO_ENABLED=0 export CGO_ENABLED=0
if [ "$1" == "fmt" ]; then if [ "$1" == "fmt" ]; then
go fmt $* go fmt $2
elif [ "$1" == "get" ]; then
go get $2
elif [ "$1" == "tidy" ]; then elif [ "$1" == "tidy" ]; then
go mod tidy go mod tidy
elif [ "$1" == "update" ]; then
echo "update..."
go get -u ./...
go mod tidy
elif [ "$1" == "vet" ]; then elif [ "$1" == "vet" ]; then
echo "vet..." echo "vet..."
go vet go vet ./...
else else
go build go build
fi fi
+107 -334
View File
@@ -4,6 +4,7 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"crypto/sha256" "crypto/sha256"
"encoding/hex"
"fmt" "fmt"
"io" "io"
"os" "os"
@@ -11,7 +12,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"syscall" "syscall"
"unicode"
"git.vsu.ru/ai/rspamd-cgp/utils"
) )
const recvHdr = "Received:" const recvHdr = "Received:"
@@ -19,21 +21,21 @@ const seenHdr = "X-Rspamd-Seen:"
const subjHdr = "Subject:" const subjHdr = "Subject:"
const submitDir = "Submitted" const submitDir = "Submitted"
var MainDomain string var reHELO1 *regexp.Regexp
var reHELO2 *regexp.Regexp
var reMD *regexp.Regexp var reMD *regexp.Regexp
var reSELF *regexp.Regexp var reSELF *regexp.Regexp
var reSMTP *regexp.Regexp var reSMTP *regexp.Regexp
var protocol int var protocol int
func init() { 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+([^;]+);`) reMD = regexp.MustCompile(`^\s+DomainName\s+=\s+([^;]+);`)
reSELF = regexp.MustCompile(`^S (?:<([^>]+)> )?(?:DSN|GROUP|LIST|PBX|PIPE|RULE) \[0\.0\.0\.0\]`) 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|XIMSS|IMAP) \[([0-9a-f.:]+)\]`) reSMTP = regexp.MustCompile(`^S (?:<([^>]+)> )?(?:SMTP|HTTPU?|AIRSYNC|IMAP|RPOP|XIMSS) \[([0-9a-f.:]+)\]`)
err := setMainDomain()
if err != nil {
Putline("* Can not detect Main Domain: %v\n", err)
}
} }
func AddHeader(seq int, headers []string) { func AddHeader(seq int, headers []string) {
@@ -47,6 +49,47 @@ func AddHeader(seq int, headers []string) {
} }
} }
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) { 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 [%d]: Action: discard; from %s, rcpts %s\n", seq, qid, from, strings.Join(rcpts, ","))
Putline("%d DISCARD\n", seq) Putline("%d DISCARD\n", seq)
@@ -62,7 +105,34 @@ func Intf(seq int, ver string) {
Putline("%d INTF %d\n", seq, protocol) Putline("%d INTF %d\n", seq, protocol)
} }
func Message(filename string) (from string, rcpts []string, auth string, ip string, qid int, body []byte, seen bool, err error) { func MainDomain() (domain string, err error) {
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
}
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, '.')]) qid, err = strconv.Atoi((filename)[strings.LastIndexByte(filename, '/')+1 : strings.LastIndexByte(filename, '.')])
if err != nil { if err != nil {
@@ -106,8 +176,13 @@ func Message(filename string) (from string, rcpts []string, auth string, ip stri
if s := reSMTP.FindAllStringSubmatch(string(line), -1); s != nil { if s := reSMTP.FindAllStringSubmatch(string(line), -1); s != nil {
auth = s[0][1] auth = s[0][1]
ip = s[0][2] ip = s[0][2]
hostname = getHostname(ip)
} else if s := reSELF.FindAllStringSubmatch(string(line), -1); s != nil { } else if s := reSELF.FindAllStringSubmatch(string(line), -1); s != nil {
auth = s[0][1] if len(s[0][1]) > 0 {
auth = s[0][1]
} else {
auth = s[0][2] + "@trusted"
}
ip = "127.2.4.7" ip = "127.2.4.7"
} }
} }
@@ -138,7 +213,9 @@ func Message(filename string) (from string, rcpts []string, auth string, ip stri
return return
} }
rcpts = uniqueNonEmptyElementsOf(rcpts) helo = getHelo(body)
rcpts = utils.UniqueSliceElementsNonEmpty(rcpts)
if from == "" || len(rcpts) == 0 || n < len(body) { 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) 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)
@@ -147,43 +224,6 @@ func Message(filename string) (from string, rcpts []string, auth string, ip stri
return return
} }
func MirrorTo(seq int, qid int, to []string, headers []string, body []byte, mirrorDiscard bool) {
if protocol >= 4 {
if 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"))
mirrorTo := []string{}
for _, m := range to {
mirrorTo = append(mirrorTo, fmt.Sprintf("MIRRORTO \"%s\"", m))
}
if mirrorDiscard {
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 {
hdrs := replaceSpecChars(strings.Join(headers, "\n"))
Putline("%d ADDHEADER \"%s\" OK\n", seq, hdrs)
}
} else {
hdrs := replaceSpecChars(strings.Join(headers, "\n"))
Putline("%d ADDHEADER \"%s\"\n", seq, hdrs)
}
}
func Ok(seq int) { func Ok(seq int) {
Putline("%d OK\n", seq) Putline("%d OK\n", seq)
} }
@@ -202,15 +242,14 @@ func Reject(seq int) {
Putline("%d REJECT Try again later\n", seq) Putline("%d REJECT Try again later\n", seq)
} }
func RewriteSubject(seq int, headers []string, subject string, qid int, from string, rcpts []string, body []byte) { func RewriteSubject(seq int, headers []string, subject string, qid int, from string, rcpts []string, body []byte) (err error) {
var err error
var firstRecv bool = true var firstRecv bool = true
var line []byte
var m *bufio.Reader var m *bufio.Reader
var hdr string var hdr string
var pos int = 0
filename := fmt.Sprintf("%s/%drs.sub", submitDir, qid) filename := submitDir + "/" + strconv.Itoa(qid) + "rs.sub"
filetemp := strings.Replace(filename, "sub", "tmp", 1) filetemp := strings.Replace(filename, "sub", "tmp", 1)
fh, err := os.Create(filetemp) fh, err := os.Create(filetemp)
@@ -219,23 +258,21 @@ func RewriteSubject(seq int, headers []string, subject string, qid int, from str
} }
defer fh.Close() defer fh.Close()
_, err = fh.WriteString(strings.Join([]string{"Return-Path: ", from, "\n"}, "")) _, err = fh.WriteString("Return-Path: " + from + "\n")
if err != nil { if err != nil {
goto fin goto fin
} }
for _, rcpt := range rcpts { for _, rcpt := range rcpts {
_, err = fh.WriteString(strings.Join([]string{"Envelope-To: ", rcpt, "\n"}, "")) _, err = fh.WriteString("Envelope-To: " + rcpt + "\n")
if err != nil { if err != nil {
goto fin goto fin
} }
} }
for _, hdr = range headers { _, err = fh.WriteString(strings.Join(headers, "\n") + "\n")
_, err = fh.WriteString(hdr) if err != nil {
if err != nil { goto fin
goto fin
}
} }
m = bufio.NewReader(bytes.NewReader(body)) m = bufio.NewReader(bytes.NewReader(body))
@@ -253,8 +290,10 @@ func RewriteSubject(seq int, headers []string, subject string, qid int, from str
goto fin goto fin
} }
pos += len(hdr)
if hdr == "\n" { if hdr == "\n" {
// конец заголовка // конец RFC5322 заголовка
_, err = fh.WriteString(hdr) _, err = fh.WriteString(hdr)
if err != nil { if err != nil {
goto fin goto fin
@@ -264,9 +303,10 @@ func RewriteSubject(seq int, headers []string, subject string, qid int, from str
if firstRecv && strings.HasPrefix(hdr, recvHdr) { if firstRecv && strings.HasPrefix(hdr, recvHdr) {
sum := fmt.Sprintf("%x", sha256.Sum224([]byte(nospace(hdr)))) bs := sha256.Sum224([]byte(utils.NoSpace(hdr)))
sum := hex.EncodeToString(bs[:])
_, err = fh.WriteString(strings.Join([]string{seenHdr, " ", sum, "\n"}, "")) _, err = fh.WriteString(seenHdr + " " + sum + "\n")
if err != nil { if err != nil {
goto fin goto fin
} }
@@ -282,7 +322,7 @@ func RewriteSubject(seq int, headers []string, subject string, qid int, from str
if strings.HasPrefix(hdr, subjHdr) { if strings.HasPrefix(hdr, subjHdr) {
_, err = fh.WriteString(strings.Join([]string{subjHdr, " ", subject, "\n"}, "")) _, err = fh.WriteString(subjHdr + " " + subject + "\n")
if err != nil { if err != nil {
goto fin goto fin
} }
@@ -296,20 +336,9 @@ func RewriteSubject(seq int, headers []string, subject string, qid int, from str
} }
} }
for { _, err = fh.Write(body[pos:])
line, err = m.ReadSlice('\n') if err != nil {
if err == io.EOF { goto fin
err = nil
break
}
if err != nil {
goto fin
}
_, err = fh.Write(line)
if err != nil {
goto fin
}
} }
if err = fh.Close(); err != nil { if err = fh.Close(); err != nil {
@@ -319,261 +348,5 @@ func RewriteSubject(seq int, headers []string, subject string, qid int, from str
err = os.Rename(filetemp, filename) err = os.Rename(filetemp, filename)
fin: fin:
if err != nil {
Failure(seq, qid, err)
} else {
Discard(seq, qid, from, rcpts)
}
}
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 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) {
sum := fmt.Sprintf("%x", sha256.Sum224([]byte(nospace(hdr))))
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) {
seenhdr = fmt.Sprintf("%s %x", seenHdr, sha256.Sum224([]byte(nospace(hdr))))
break
}
}
return
}
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 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()
}
func setMainDomain() (err error) {
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
}
if s := reMD.FindAllStringSubmatch(string(line), -1); s != nil {
MainDomain = s[0][1]
break
}
}
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
View File
@@ -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
View File
@@ -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()
}
+92
View File
@@ -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)
}
}
+73 -20
View File
@@ -2,45 +2,98 @@ package config
import ( import (
"flag" "flag"
"strings" "fmt"
"os"
"path/filepath"
"time" "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 { type Config struct {
AuthservId string AuthservId string
Debug bool Debug bool
Host string Outbound bool
MirrorDiscard bool Host string `default:"localhost:11333"`
MirrorTo []string Timeout time.Duration `default:"15s"`
Timeout time.Duration NotifyFrom string `required:"true"`
Actions map[string]*Operation
Symbols map[string]*Operation
} }
func New() *Config { func New() *Config {
config := new(Config) config := new(Config)
var mirrorTo string dir, _ := filepath.Abs(filepath.Dir(os.Args[0]))
flag.StringVar(&config.AuthservId, "authserv-id", "", "Authentication Identifier (default CommuniGate Pro Main Domain)") var configfile string
flag.StringVar(&config.Host, "host", "localhost:11333", "Rspamd host to connect") var configdump bool
flag.BoolVar(&config.MirrorDiscard, "mirror-discard", false, "Mirror then discard selected messages") var configtest bool
flag.StringVar(&mirrorTo, "mirror-to", "", "Mirror selected messages to email") var debug bool
flag.DurationVar(&config.Timeout, "timeout", 15*time.Second, "Rspamd request timeout") var outbound bool
flag.BoolVar(&config.Debug, "debug", false, "Export debug information (for developers)")
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() flag.Parse()
if len(mirrorTo) > 0 { err := configor.Load(config, configfile)
config.MirrorTo = strings.Split(strings.ReplaceAll(mirrorTo, " ", ""), ",") if err != nil {
fmt.Println("config:", err)
os.Exit(1)
} }
if config.MirrorDiscard && len(config.MirrorTo) == 0 { if debug {
config.MirrorDiscard = false config.Debug = debug
} }
if config.Timeout < time.Second { if outbound {
config.Timeout *= time.Second 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 return config
} }
+105
View File
@@ -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
}
+1
View File
@@ -0,0 +1 @@
domain.ru
+1
View File
@@ -0,0 +1 @@
/domain\.ru/i
+41
View File
@@ -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";
}
+6
View File
@@ -0,0 +1,6 @@
local reconf = config['regexp']
reconf['CGP_RPOLL'] = {
description = 'CommuniGate Pro RPOLL Received header',
re = 'Received=/with RPOLL /{raw_header}'
}
+17
View File
@@ -0,0 +1,17 @@
################################################################################
# Пример settings.conf для Rspamd.
################################################################################
################################################################################
# Устанавливает символ AUTHENTICATED_USER для аутентифицированных сообщений.
#
authenticated {
priority = high;
authenticated = yes;
apply {
AUTHENTICATED_USER = 0.0;
}
symbols [
"AUTHENTICATED_USER"
]
}
+1
View File
@@ -0,0 +1 @@
1.2.3.4
+12 -4
View File
@@ -1,10 +1,18 @@
module git.vsu.ru/ai/rspamd-cgp module git.vsu.ru/ai/rspamd-cgp
go 1.18 go 1.23
require github.com/json-iterator/go v1.1.12
require ( require (
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect 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 github.com/modern-go/reflect2 v1.0.2 // indirect
) )
+19 -2
View File
@@ -1,15 +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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
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 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 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/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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=
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+4 -1
View File
@@ -7,6 +7,7 @@ import (
"os" "os"
"git.vsu.ru/ai/rspamd-cgp/cgp" "git.vsu.ru/ai/rspamd-cgp/cgp"
"git.vsu.ru/ai/rspamd-cgp/config"
"git.vsu.ru/ai/rspamd-cgp/rspamc" "git.vsu.ru/ai/rspamd-cgp/rspamc"
) )
@@ -18,6 +19,8 @@ func main() {
var err error var err error
var n int var n int
config := config.New()
in := bufio.NewReader(os.Stdin) in := bufio.NewReader(os.Stdin)
finalize: finalize:
@@ -36,7 +39,7 @@ finalize:
switch { switch {
case cmd == "FILE" && n == 3: case cmd == "FILE" && n == 3:
go rspamc.Scan(seq, arg) go rspamc.Scan(config, seq, arg)
case cmd == "INTF" && n == 3: case cmd == "INTF" && n == 3:
cgp.Intf(seq, arg) cgp.Intf(seq, arg)
+274
View File
@@ -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)
}
}
}
+57 -72
View File
@@ -2,66 +2,35 @@ package rspamc
import ( import (
"bytes" "bytes"
"encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"os"
"reflect"
"strconv" "strconv"
"strings"
json "github.com/json-iterator/go"
"git.vsu.ru/ai/rspamd-cgp/cgp" "git.vsu.ru/ai/rspamd-cgp/cgp"
"git.vsu.ru/ai/rspamd-cgp/config" "git.vsu.ru/ai/rspamd-cgp/config"
) )
const ( const (
headerCase string = "X-Rspamd-Case: "
headerJunkG string = "X-Junk-Score: [XX]" headerJunkG string = "X-Junk-Score: [XX]"
headerJunkA string = "X-Junk-Score: [XXXX]" headerJunkA string = "X-Junk-Score: [XXXX]"
headerJunkR string = "X-Junk-Score: [XXXXXXXXXX]" headerJunkR string = "X-Junk-Score: [XXXXXXXXXX]"
) )
var authservId string func Scan(conf *config.Config, seq int, filename string) {
var client *http.Client
var mirrorDiscard bool
var mirrorTo []string
var host string
var debug bool
func init() {
config := config.New()
if config.AuthservId != "" {
authservId = config.AuthservId
} else {
authservId = cgp.MainDomain
}
mirrorDiscard = config.MirrorDiscard
mirrorTo = config.MirrorTo
host = "http://" + config.Host + "/checkv2"
debug = config.Debug
tr := &http.Transport{ tr := &http.Transport{
DisableCompression: true, DisableCompression: true,
} }
client = &http.Client{ client := &http.Client{
Timeout: config.Timeout, Timeout: conf.Timeout,
Transport: tr, Transport: tr,
} }
}
func printResponse(v any) { from, rcpts, auth, ip, helo, hostname, qid, body, seen, err := cgp.Message(filename)
printed, _ := json.MarshalIndent(v, "", " ")
fmt.Fprintln(os.Stderr, string(printed))
}
func Scan(seq int, filename string) {
from, rcpts, auth, ip, qid, body, seen, err := cgp.Message(filename)
if err != nil { if err != nil {
cgp.Failure(seq, qid, err) cgp.Failure(seq, qid, err)
return return
@@ -72,13 +41,13 @@ func Scan(seq int, filename string) {
return return
} }
req, err := http.NewRequest("POST", host, bytes.NewReader(body)) req, err := http.NewRequest("POST", conf.Host, bytes.NewReader(body))
if err != nil { if err != nil {
cgp.Failure(seq, qid, err) cgp.Failure(seq, qid, err)
return return
} }
req.Header.Add("MTA-Tag", authservId) req.Header.Add("MTA-Tag", conf.AuthservId)
req.Header.Add("User-Agent", "rspamd-cgp") req.Header.Add("User-Agent", "rspamd-cgp")
req.Header.Add("From", from) req.Header.Add("From", from)
req.Header.Add("Queue-ID", strconv.Itoa(qid)) req.Header.Add("Queue-ID", strconv.Itoa(qid))
@@ -88,6 +57,12 @@ func Scan(seq int, filename string) {
if len(ip) > 0 { if len(ip) > 0 {
req.Header.Add("IP", ip) 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 { for _, rcpt := range rcpts {
req.Header.Add("Rcpt", rcpt) req.Header.Add("Rcpt", rcpt)
} }
@@ -111,66 +86,76 @@ func Scan(seq int, filename string) {
return return
} }
if debug { if conf.Debug {
printMsgInfo(from, rcpts, auth, ip, helo, hostname, qid, seen)
printResponse(res) printResponse(res)
} }
var headers []string action := res["action"].(string)
opsum, caseinfo, casework, desc := makeOpSum(conf, res, action)
if _, ok := res["dkim-signature"]; ok { headers := make([]string, 0, 8)
headers = append(headers, strings.Join([]string{"DKIM-Signature: ", res["dkim-signature"].(string)}, ""))
if conf.Outbound {
headers = makeHeadersOutbound(res)
} else {
headers = makeHeaders(res)
} }
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, strings.Join([]string{h, v}, ": "))
}
}
}
}
}
}
action := res["action"]
cgp.Putline("* %d [%d]: Action: %s; Score: %.2f/%.2f; Time elapsed: %.3fs\n", cgp.Putline("* %d [%d]: Action: %s; Score: %.2f/%.2f; Time elapsed: %.3fs\n",
seq, qid, action, res["score"], res["required_score"], res["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 { switch action {
case "no action": case "no action":
if len(headers) > 0 { procAction(seq, qid, opsum, res, headers, hci, from, rcpts, body, conf.NotifyFrom, desc, conf.Outbound, 0)
cgp.AddHeader(seq, headers)
} else {
cgp.Ok(seq)
}
case "discard": case "discard":
cgp.Discard(seq, qid, from, rcpts) 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": case "quarantine":
cgp.MirrorTo(seq, qid, mirrorTo, append(headers, headerJunkR), body, mirrorDiscard) fallthrough
case "reject": case "reject":
cgp.AddHeader(seq, append(headers, headerJunkR)) 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": case "rewrite subject":
if subject, ok := res["subject"]; ok { if subject, ok := res["subject"]; ok {
cgp.RewriteSubject(seq, append(headers, headerJunkA), subject.(string), qid, from, rcpts, body) procActionRS(seq, qid, opsum, res, headers, hci, subject.(string), from, rcpts, body, conf.NotifyFrom, desc, conf.Outbound)
} else { } else {
cgp.AddHeader(seq, append(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 "add header": case "add header":
cgp.AddHeader(seq, append(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": case "greylist":
fallthrough fallthrough
case "soft reject": case "soft reject":
cgp.AddHeader(seq, append(headers, headerJunkG)) if !conf.Outbound {
headers = append(headers, headerJunkG)
}
procAction(seq, qid, opsum, res, headers, hci, from, rcpts, body, conf.NotifyFrom, desc, conf.Outbound, 1)
default: default:
cgp.Failure(seq, qid, fmt.Errorf("Unknown action: %v", action)) cgp.Failure(seq, qid, fmt.Errorf("Unknown action: %v", action))
+63
View File
@@ -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
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+54
View File
@@ -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
}