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
|
rspamd-cgp
|
||||||
*.CU*
|
*.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:
|
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
|
||||||
-reject-action string
|
-configtest
|
||||||
Reject action: "add_header" or "discard" (default "add_header")
|
Perform configuration file test
|
||||||
-timeout duration
|
-debug
|
||||||
Rspamd request timeout (default 15s)
|
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
|
||||||
+213
-77
@@ -2,40 +2,96 @@ package cgp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"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 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) \[([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) {
|
||||||
|
|
||||||
|
hdrs := replaceSpecChars(strings.Join(headers, "\n"))
|
||||||
|
|
||||||
if protocol >= 4 {
|
if protocol >= 4 {
|
||||||
Putline("%d ADDHEADER \"%s\" OK\n", seq, *headers)
|
Putline("%d ADDHEADER \"%s\" OK\n", seq, hdrs)
|
||||||
} else {
|
} 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)
|
Putline("%d DISCARD\n", seq)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,14 +105,41 @@ 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, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h, err := os.Open(*filename)
|
h, err := os.Open(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -65,7 +148,9 @@ func Message(filename *string) (from string, rcpts []string, auth string, ip str
|
|||||||
var line []byte
|
var line []byte
|
||||||
var pos int64
|
var pos int64
|
||||||
|
|
||||||
for m := bufio.NewReader(h); ; {
|
m := bufio.NewReader(h)
|
||||||
|
|
||||||
|
for {
|
||||||
|
|
||||||
line, err = m.ReadSlice('\n')
|
line, err = m.ReadSlice('\n')
|
||||||
if err != nil {
|
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 {
|
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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rcpts = uniqueNonEmptyElementsOf(rcpts)
|
seen, err = isSeen(m)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if seen {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
fi, err := h.Stat()
|
fi, err := h.Stat()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -116,6 +213,10 @@ func Message(filename *string) (from string, rcpts []string, auth string, ip str
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@@ -127,6 +228,11 @@ func Ok(seq int) {
|
|||||||
Putline("%d OK\n", seq)
|
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{}) {
|
func Putline(format string, a ...interface{}) {
|
||||||
s := fmt.Sprintf(format, a...)
|
s := fmt.Sprintf(format, a...)
|
||||||
syscall.Write(int(os.Stdout.Fd()), []byte(s))
|
syscall.Write(int(os.Stdout.Fd()), []byte(s))
|
||||||
@@ -136,81 +242,111 @@ func Reject(seq int) {
|
|||||||
Putline("%d REJECT Try again later\n", seq)
|
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 {
|
fh, err := os.Create(filetemp)
|
||||||
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")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
goto fin
|
||||||
}
|
}
|
||||||
defer h.Close()
|
defer fh.Close()
|
||||||
|
|
||||||
var line []byte
|
_, err = fh.WriteString("Return-Path: " + from + "\n")
|
||||||
|
if err != nil {
|
||||||
|
goto fin
|
||||||
|
}
|
||||||
|
|
||||||
for m := bufio.NewReader(h); ; {
|
for _, rcpt := range rcpts {
|
||||||
|
_, err = fh.WriteString("Envelope-To: " + rcpt + "\n")
|
||||||
line, err = m.ReadSlice('\n')
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
goto fin
|
||||||
}
|
|
||||||
|
|
||||||
if s := reMD.FindAllStringSubmatch(string(line), -1); s != nil {
|
|
||||||
MainDomain = s[0][1]
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
_, err = fh.WriteString(strings.Join(headers, "\n") + "\n")
|
||||||
}
|
if err != nil {
|
||||||
|
goto fin
|
||||||
|
}
|
||||||
|
|
||||||
func uniqueNonEmptyElementsOf(s []string) []string {
|
m = bufio.NewReader(bytes.NewReader(body))
|
||||||
|
|
||||||
unique := make(map[string]bool, len(s))
|
for {
|
||||||
us := make([]string, len(unique))
|
|
||||||
|
|
||||||
for _, elem := range s {
|
hdr, err = getHeader(m)
|
||||||
if len(elem) != 0 {
|
if err == io.EOF {
|
||||||
if !unique[elem] {
|
err = nil
|
||||||
us = append(us, elem)
|
if len(hdr) == 0 {
|
||||||
unique[elem] = true
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return us
|
_, 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
|
||||||
}
|
}
|
||||||
|
|||||||
+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 (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
|
"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
|
||||||
Discard bool
|
Debug bool
|
||||||
Host string
|
Outbound bool
|
||||||
Timeout time.Duration
|
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 {
|
func New() *Config {
|
||||||
|
|
||||||
config := 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)")
|
var configfile string
|
||||||
flag.StringVar(&config.Host, "host", "localhost:11333", "Rspamd host to connect")
|
var configdump bool
|
||||||
flag.StringVar(&rejectAction, "reject-action", "add_header", "Reject action: \"add_header\" or \"discard\"")
|
var configtest bool
|
||||||
flag.DurationVar(&config.Timeout, "timeout", 15*time.Second, "Rspamd request timeout")
|
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()
|
flag.Parse()
|
||||||
|
|
||||||
if rejectAction == "discard" {
|
err := configor.Load(config, configfile)
|
||||||
config.Discard = true
|
if err != nil {
|
||||||
|
fmt.Println("config:", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Timeout < time.Second {
|
if debug {
|
||||||
config.Timeout *= time.Second
|
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
|
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
|
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.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/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/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
|
github.com/jinzhu/configor v1.2.2 h1:sLgh6KMzpCmaQB4e+9Fu/29VErtBUqsS2t8C9BNIVsA=
|
||||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/jinzhu/configor v1.2.2/go.mod h1:iFFSfOBKP3kC2Dku0ZGB3t3aulfQgTGJknodhFavsU8=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
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/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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
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/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/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=
|
||||||
|
|||||||
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"
|
"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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+76
-74
@@ -2,75 +2,52 @@ package rspamc
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
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 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"
|
|
||||||
|
|
||||||
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 appendHeaders(hdrs, junk *string) *string {
|
from, rcpts, auth, ip, helo, hostname, qid, body, seen, err := cgp.Message(filename)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cgp.Failure(seq, qid, err)
|
cgp.Failure(seq, qid, err)
|
||||||
return
|
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 {
|
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))
|
||||||
@@ -80,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)
|
||||||
}
|
}
|
||||||
@@ -97,65 +80,84 @@ func Scan(seq int, filename string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var js map[string]interface{}
|
var res map[string]interface{}
|
||||||
if err := json.Unmarshal(rbody, &js); err != nil {
|
if err := json.Unmarshal(rbody, &res); err != nil {
|
||||||
cgp.Failure(seq, qid, err)
|
cgp.Failure(seq, qid, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var headers string
|
if conf.Debug {
|
||||||
|
printMsgInfo(from, rcpts, auth, ip, helo, hostname, qid, seen)
|
||||||
if _, ok := js["dkim-signature"]; ok {
|
printResponse(res)
|
||||||
headers = "DKIM-Signature: " + js["dkim-signature"].(string)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if milter, ok := js["milter"]; ok {
|
action := res["action"].(string)
|
||||||
if hdrs, ok := milter.(map[string]interface{})["add_headers"]; ok {
|
opsum, caseinfo, casework, desc := makeOpSum(conf, res, action)
|
||||||
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
|
|
||||||
} else {
|
|
||||||
headers = h + ": " + v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
action := js["action"]
|
headers := make([]string, 0, 8)
|
||||||
|
|
||||||
|
if conf.Outbound {
|
||||||
|
headers = makeHeadersOutbound(res)
|
||||||
|
} else {
|
||||||
|
headers = makeHeaders(res)
|
||||||
|
}
|
||||||
|
|
||||||
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, 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 {
|
switch action {
|
||||||
case "no action":
|
case "no action":
|
||||||
if headers != "" {
|
procAction(seq, qid, opsum, res, headers, hci, from, rcpts, body, conf.NotifyFrom, desc, conf.Outbound, 0)
|
||||||
cgp.AddHeader(seq, cgp.ReplaceSpecChars(&headers))
|
|
||||||
} else {
|
|
||||||
cgp.Ok(seq)
|
|
||||||
}
|
|
||||||
|
|
||||||
case "reject":
|
case "discard":
|
||||||
if discard {
|
if !conf.Outbound {
|
||||||
cgp.Discard(seq)
|
headers = append(headers, headerJunkR)
|
||||||
} else {
|
|
||||||
cgp.AddHeader(seq, cgp.ReplaceSpecChars(appendHeaders(&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":
|
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":
|
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":
|
case "greylist":
|
||||||
cgp.AddHeader(seq, cgp.ReplaceSpecChars(appendHeaders(&headers, &headerJunkG)))
|
fallthrough
|
||||||
|
|
||||||
case "soft reject":
|
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:
|
default:
|
||||||
cgp.Failure(seq, qid, fmt.Errorf("Unknown action: %v", action))
|
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