├── LICENSE ├── README.md ├── pyzor ├── README.md ├── pyzor.lua └── pyzorsocket │ ├── .gitignore │ ├── Dockerfile │ ├── MANIFEST.in │ ├── README.md │ ├── pyzorsocket.py │ ├── requirements.txt │ └── setup.py └── razor ├── README.md ├── debian ├── changelog ├── compat ├── control ├── copyright ├── docs ├── install ├── postinst ├── razor.socket ├── razor@.service ├── rules └── source │ └── format ├── razor.lua └── razorsocket /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2017, Christoffer G. Thomsen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | For "documentation", see https://github.com/cgt/rspamd-plugins/issues/1 2 | -------------------------------------------------------------------------------- /pyzor/README.md: -------------------------------------------------------------------------------- 1 | # pyzor-rspamd 2 | 3 | A [Pyzor](https://github.com/SpamExperts/pyzor) plugin for [Rspamd](https://rspamd.com/). 4 | 5 | Requires [pyzorsocket](https://github.com/cgt/rspamd-plugins/tree/master/pyzor/pyzorsocket). 6 | 7 | Distributed under the MIT license. See top-level LICENSE file. 8 | -------------------------------------------------------------------------------- /pyzor/pyzor.lua: -------------------------------------------------------------------------------- 1 | local ucl = require "ucl" 2 | local logger = require "rspamd_logger" 3 | local tcp = require "rspamd_tcp" 4 | 5 | local N = "pyzor" 6 | local symbol_pyzor = "PYZOR" 7 | local opts = rspamd_config:get_all_opt(N) 8 | 9 | -- Default settings 10 | local cfg_host = "localhost" 11 | local cfg_port = 5953 12 | 13 | --{"PV": "2.1", "Code": "200", "WL-Count": "0", "Count": "53", "Thread": "53416", "Diag": "OK"} 14 | 15 | local function check_pyzor(task) 16 | local function cb(err, data) 17 | if err then 18 | logger.errx(task, "request error: %s", err) 19 | return 20 | end 21 | logger.debugm(N, task, 'data: %s', tostring(data)) 22 | 23 | local parser = ucl.parser() 24 | local ok, err = parser:parse_string(tostring(data)) 25 | if not ok then 26 | logger.errx(task, "error parsing response: %s", err) 27 | return 28 | end 29 | 30 | local resp = parser:get_object() 31 | local whitelisted = tonumber(resp["WL-Count"]) 32 | local reported = tonumber(resp["Count"]) 33 | 34 | logger.infox(task, "count=%s wl=%s", reported, whitelisted) 35 | 36 | -- Make whitelists count a little bit. 37 | -- Maybe there's a better way to take whitelists into account, 38 | -- but at least this is something. 39 | reported = reported - whitelisted 40 | 41 | local weight = 0 42 | 43 | if reported >= 100 then 44 | weight = 1.5 45 | elseif reported >= 25 then 46 | weight = 1.25 47 | elseif reported >= 5 then 48 | weight = 1.0 49 | elseif reported >= 1 and whitelisted == 0 then 50 | weight = 0.2 51 | end 52 | 53 | if weight > 0 then 54 | task:insert_result(symbol_pyzor, weight, string.format("count=%d wl=%d", reported, whitelisted)) 55 | end 56 | end 57 | 58 | local request = { 59 | "CHECK\n", 60 | task:get_content(), 61 | } 62 | 63 | logger.debugm(N, task, "querying pyzor") 64 | 65 | tcp.request({ 66 | task = task, 67 | host = cfg_host, 68 | port = cfg_port, 69 | shutdown = true, 70 | data = request, 71 | callback = cb, 72 | }) 73 | end 74 | 75 | if opts then 76 | if opts.host then 77 | cfg_host = opts.host 78 | end 79 | if opts.port then 80 | cfg_port = opts.port 81 | end 82 | 83 | rspamd_config:register_symbol({ 84 | name = symbol_pyzor, 85 | callback = check_pyzor, 86 | }) 87 | else 88 | logger.infox("%s module not configured", N) 89 | end 90 | -------------------------------------------------------------------------------- /pyzor/pyzorsocket/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # dotenv 80 | .env 81 | 82 | # virtualenv 83 | .venv/ 84 | venv/ 85 | ENV/ 86 | 87 | # Spyder project settings 88 | .spyderproject 89 | 90 | # Rope project settings 91 | .ropeproject 92 | -------------------------------------------------------------------------------- /pyzor/pyzorsocket/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-onbuild 2 | EXPOSE 5953 3 | CMD ["python", "./pyzorsocket.py", "0.0.0.0", "5953"] 4 | -------------------------------------------------------------------------------- /pyzor/pyzorsocket/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /pyzor/pyzorsocket/README.md: -------------------------------------------------------------------------------- 1 | # pyzorsocket 2 | 3 | pyzorsocket exposes [pyzor](https://github.com/SpamExperts/pyzor) on a socket. 4 | 5 | ## Protocol 6 | The protocol is very simple. The client sends a command in all-caps 7 | followed by a newline (`\n`). The command corresponds to a pyzor command 8 | such as `check`. The client then sends the entire e-mail to be checked, 9 | just as when piping to the `pyzor` command. After all data has been 10 | sent, the client must close its socket for writing. 11 | 12 | The server hands off the e-mail to pyzor. Whatever pyzor responds with is 13 | serialized as JSON and sent back to the client. The connection is then closed. 14 | 15 | ### Example 16 | 17 | A client wants to check an e-mail with pyzor. With the `pyzor` command 18 | this is done by piping a mail to `pyzor check`. The example with pyzorsocket 19 | below is equivalent, except that the response is serialized as JSON. 20 | 21 | All newline characters in data shown escaped. 22 | Actual newlines in example only included as a visual aid. 23 | 24 | Client sends: 25 | 26 | CHECK\n 27 | From: John Doe \r\n 28 | To: Mary Smith \r\n 29 | Subject: Saying Hello\r\n 30 | Date: Fri, 21 Nov 1997 09:55:06 -0600\r\n 31 | Message-ID: <1234@local.machine.example>\r\n\r\n 32 | 33 | This is a message just to say hello.\r\n 34 | So, "Hello".\r\n 35 | 36 | The client closes its socket for writing, and the server replies: 37 | 38 | {"Thread": "16329", "Diag": "OK", "Count": "0", "Code": "200", "PV": "2.1", "WL-Count": "0"}\n 39 | 40 | ## License 41 | Distributed under the MIT license. See top-level LICENSE file. 42 | 43 | Note that this license does not apply to pyzor itself, 44 | which is distributed under the GNU General Public License version 2. 45 | -------------------------------------------------------------------------------- /pyzor/pyzorsocket/pyzorsocket.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import email.parser 3 | import email.policy 4 | import os 5 | import json 6 | from socketserver import TCPServer, ThreadingMixIn, StreamRequestHandler 7 | 8 | import pyzor.client 9 | import pyzor.digest 10 | 11 | 12 | class RequestHandler(StreamRequestHandler): 13 | 14 | def handle(self): 15 | cmd = self.rfile.readline().decode()[:-1] 16 | if cmd == "CHECK": 17 | self.handle_check() 18 | else: 19 | self.write_json({"error": "unknown command"}) 20 | 21 | def handle_check(self): 22 | parser = email.parser.BytesParser(policy=email.policy.SMTP) 23 | msg = parser.parse(self.rfile) 24 | 25 | digest = pyzor.digest.DataDigester(msg).value 26 | # whitelist 'default' digest (all messages with empty/short bodies) 27 | if digest != 'da39a3ee5e6b4b0d3255bfef95601890afd80709': 28 | check = pyzor.client.Client().check(digest) 29 | 30 | self.write_json({k: v for k, v in check.items()}) 31 | 32 | def write_json(self, d): 33 | j = json.dumps(d) + "\n" 34 | self.wfile.write(j.encode()) 35 | 36 | 37 | class Server(ThreadingMixIn, TCPServer): 38 | pass 39 | 40 | 41 | def main(): 42 | argp = argparse.ArgumentParser(description="Expose pyzor on a socket") 43 | argp.add_argument("addr", help="address to listen on") 44 | argp.add_argument("port", help="port to listen on") 45 | args = argp.parse_args() 46 | 47 | addr = (args.addr, int(args.port)) 48 | 49 | srv = Server(addr, RequestHandler) 50 | try: 51 | srv.serve_forever() 52 | finally: 53 | srv.server_close() 54 | 55 | 56 | if __name__ == "__main__": 57 | main() 58 | -------------------------------------------------------------------------------- /pyzor/pyzorsocket/requirements.txt: -------------------------------------------------------------------------------- 1 | pyzor==1.0.0 2 | -------------------------------------------------------------------------------- /pyzor/pyzorsocket/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | def readme(): 4 | with open("README.md") as f: 5 | return f.read() 6 | 7 | setup( 8 | name="pyzorsocket", 9 | version="0.1", 10 | license="MIT", 11 | 12 | author="Christoffer G. Thomsen", 13 | author_email="chris@cgt.name", 14 | 15 | url = "https://github.com/cgt/rspamd-plugins/tree/master/pyzor/pyzorsocket", 16 | description="Expose pyzor on a socket", 17 | long_description=readme(), 18 | 19 | py_modules=["pyzorsocket"], 20 | entry_points={ 21 | "console_scripts": [ 22 | "pyzorsocket=pyzorsocket:main", 23 | ], 24 | }, 25 | install_requires=[ 26 | "pyzor>=1.0.0", 27 | ], 28 | 29 | classifiers=[ 30 | "License :: OSI Approved :: MIT License", 31 | "Programming Language :: Python :: 3 :: Only", 32 | ], 33 | keywords="pyzor spam", 34 | ) 35 | -------------------------------------------------------------------------------- /razor/README.md: -------------------------------------------------------------------------------- 1 | # rspamd-razor 2 | 3 | Integrates [Vipul's Razor](http://razor.sourceforge.net/) with 4 | [Rspamd](https://rspamd.com/). 5 | 6 | Distributed under the MIT license. See top-level LICENSE file. 7 | -------------------------------------------------------------------------------- /razor/debian/changelog: -------------------------------------------------------------------------------- 1 | rspamd-razor (1) stretch; urgency=medium 2 | 3 | * Initial release. 4 | 5 | -- Christoffer G. Thomsen Fri, 17 Nov 2017 19:27:53 +0100 6 | -------------------------------------------------------------------------------- /razor/debian/compat: -------------------------------------------------------------------------------- 1 | 10 2 | -------------------------------------------------------------------------------- /razor/debian/control: -------------------------------------------------------------------------------- 1 | Source: rspamd-razor 2 | Section: mail 3 | Priority: optional 4 | Maintainer: Christoffer G. Thomsen 5 | Build-Depends: debhelper (>= 9) 6 | Standards-Version: 3.9.8 7 | Homepage: https://github.com/cgt/rspamd-plugins/razor 8 | 9 | Package: rspamd-razor 10 | Architecture: all 11 | Depends: razor (>= 2.85), rspamd (>= 1.6.5), systemd (>= 232), ${misc:Depends} 12 | Description: Integrate Vipul's Razor with Rspamd. 13 | -------------------------------------------------------------------------------- /razor/debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: rspamd-plugins 3 | Source: https://github.com/cgt/rspamd-plugins/razor 4 | 5 | Files: * 6 | Copyright: 2017 Christoffer G. Thomsen 7 | License: Expat 8 | -------------------------------------------------------------------------------- /razor/debian/docs: -------------------------------------------------------------------------------- 1 | README.md 2 | -------------------------------------------------------------------------------- /razor/debian/install: -------------------------------------------------------------------------------- 1 | razorsocket usr/bin 2 | razor.lua usr/share/rspamd-plugins 3 | -------------------------------------------------------------------------------- /razor/debian/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # postinst script for rspamd-razor 3 | # 4 | # see: dh_installdeb(1) 5 | 6 | set -e 7 | 8 | # summary of how this script can be called: 9 | # * `configure' 10 | # * `abort-upgrade' 11 | # * `abort-remove' `in-favour' 12 | # 13 | # * `abort-remove' 14 | # * `abort-deconfigure' `in-favour' 15 | # `removing' 16 | # 17 | # for details, see https://www.debian.org/doc/debian-policy/ or 18 | # the debian-policy package 19 | 20 | 21 | case "$1" in 22 | configure) 23 | adduser --system \ 24 | --group \ 25 | --home /nonexistent \ 26 | --no-create-home \ 27 | razorsocket 28 | ;; 29 | 30 | abort-upgrade|abort-remove|abort-deconfigure) 31 | ;; 32 | 33 | *) 34 | echo "postinst called with unknown argument \`$1'" >&2 35 | exit 1 36 | ;; 37 | esac 38 | 39 | # dh_installdeb will replace this with shell code automatically 40 | # generated by other debhelper scripts. 41 | 42 | #DEBHELPER# 43 | 44 | exit 0 45 | -------------------------------------------------------------------------------- /razor/debian/razor.socket: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Razor socket 3 | 4 | [Socket] 5 | ListenStream=127.0.0.1:9192 6 | Accept=yes 7 | 8 | [Install] 9 | WantedBy=sockets.target 10 | -------------------------------------------------------------------------------- /razor/debian/razor@.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Razor Socket Service 3 | Requires=razor.socket 4 | 5 | [Service] 6 | Type=simple 7 | ExecStart=/usr/bin/razorsocket 8 | StandardInput=socket 9 | StandardError=journal 10 | TimeoutStopSec=10 11 | 12 | User=razorsocket 13 | NoNewPrivileges=true 14 | PrivateDevices=true 15 | PrivateTmp=true 16 | PrivateUsers=true 17 | ProtectControlGroups=true 18 | ProtectHome=true 19 | ProtectKernelModules=true 20 | ProtectKernelTunables=true 21 | ProtectSystem=strict 22 | 23 | [Install] 24 | WantedBy=multi-user.target 25 | -------------------------------------------------------------------------------- /razor/debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | %: 4 | dh $@ 5 | 6 | override_dh_systemd_enable: 7 | dh_systemd_enable --name razor@ razor@.service 8 | dh_systemd_enable --name razor razor.socket 9 | -------------------------------------------------------------------------------- /razor/debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /razor/razor.lua: -------------------------------------------------------------------------------- 1 | local logger = require "rspamd_logger" 2 | local tcp = require "rspamd_tcp" 3 | 4 | local N = "razor" 5 | local symbol_razor = "RAZOR" 6 | local opts = rspamd_config:get_all_opt(N) 7 | 8 | -- Default settings 9 | local cfg_host = "127.0.0.1" 10 | local cfg_port = 9192 11 | 12 | local function check_razor(task) 13 | local function cb(err, data) 14 | if err then 15 | logger.errx(task, "request error: %s", err) 16 | return 17 | end 18 | local resp = tostring(data) 19 | if resp == "spam" then 20 | task:insert_result(symbol_razor, 1.0) 21 | logger.debugm(N, task, "spam") 22 | elseif resp == "ham" then 23 | logger.debugm(N, task, "ham") 24 | else 25 | logger.errx(task, "unknown response from razorsocket: %s", resp) 26 | end 27 | end 28 | 29 | tcp.request({ 30 | task = task, 31 | host = cfg_host, 32 | port = cfg_port, 33 | shutdown = true, 34 | data = task:get_content(), 35 | callback = cb, 36 | }) 37 | end 38 | 39 | if opts then 40 | if opts.host then 41 | cfg_host = opts.host 42 | end 43 | if opts.port then 44 | cfg_port = opts.port 45 | end 46 | 47 | rspamd_config:register_symbol({ 48 | name = symbol_razor, 49 | callback = check_razor, 50 | }) 51 | else 52 | logger.infox("%s module not configured", N) 53 | end 54 | -------------------------------------------------------------------------------- /razor/razorsocket: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | razor-check <&0 && echo -n "spam" || echo -n "ham" 4 | --------------------------------------------------------------------------------