--- /dev/null
+# DCC module
+
+This modules performs [DCC](http://www.dcc-servers.net/dcc/) lookups to determine
+the *bulkiness* of a message (e.g. how many recipients have seen it).
+
+Identifying bulk messages is very useful in composite rules e.g. if a message is
+from a freemail domain *AND* the message is reported as bulk by DCC then you can
+be sure the message is spam and can assign a greater weight to it.
+
+Please view the License terms on the DCC website before you enable this module.
+
+## Module configuration
+
+This module requires that you have the `dccifd` daemon configured, running and
+working correctly. To do this you must download and build the [latest DCC client]
+(https://www.dcc-servers.net/dcc/source/dcc.tar.Z). Once installed, edit
+`/var/dcc/dcc_conf` set `DCCIFD_ENABLE=on` and set `DCCM_LOG_AT=NEVER` and
+`DCCM_REJECT_AT=MANY`, then start the daemon by running `/var/dcc/libexec/rcDCC start`.
+
+Once the `dccifd` daemon is started it will listen on the UNIX domain socket /var/dcc/dccifd
+and all you have to do is tell the rspamd where `dccifd` is listening:
+
+~~~ucl
+dcc {
+ host = "/var/dcc/dccifd";
+ # Port is only required if `dccifd` listens on a TCP socket
+ # port = 1234
+}
+~~~
+
+Once this module is configured it will write the DCC output to the rspamd as each
+message is scanned:
+
+`````
+Apr 5 14:19:53 mail1-ewh rspamd: (normal) lua; dcc.lua:98: sending to dcc: client=217.78.2.204#015DNSERROR helo="003b046f.slimabs.top" envfrom="23SecondAbs@slimabs.top" envrcpt="xxxx@xxxx.com"
+Apr 5 14:19:53 mail1-ewh rspamd: (normal) lua; dcc.lua:65: DCC result=R disposition=R header="X-DCC--Metrics: xxxxx.xxxx.com 1282; bulk Body=1 Fuz1=1 Fuz2=many"
+`````
+
+Any messages that DCC returns a *reject* result for (based on the configured `DCCM_REJECT_AT`
+value) will cause the symbol `DCC_BULK` to fire.
--- /dev/null
+--[[
+Copyright (c) 2016, Steve Freegard <steve.freegard@fsl.com>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+]]--
+
+-- Check messages for 'bulkiness' using DCC
+
+local symbol = "DCC_CHECK"
+local symbol_bulk = "DCC_BULK"
+local opts = rspamd_config:get_all_opt('dcc')
+local logger = require "rspamd_logger"
+local tcp = require "rspamd_tcp"
+
+local function check_dcc (task)
+ -- Connection
+ local client = '0.0.0.0'
+ local client_ip = task:get_from_ip():to_string()
+ if client_ip then
+ client = client_ip
+ end
+ local client_host = task:get_hostname()
+ if client_host and client_host ~= 'unknown' then
+ client = client .. "\r" .. client_host
+ end
+
+ -- HELO
+ local helo = task:get_helo() or ''
+
+ -- Envelope From
+ local ef = task:get_from(1)
+ local envfrom = 'test@example.com'
+ if ef and ef[1] then
+ envfrom = ef[1]['addr']
+ end
+
+ -- Envelope To
+ local envrcpt = 'test@example.com'
+ local rcpts = task:get_recipients(1);
+ if rcpts then
+ local r = table.concat(totable(map(function(rcpt) return rcpt['addr'] end, rcpts)), '\n')
+ if r then
+ envrcpt = r
+ end
+ end
+
+ -- Callback function to receive async result from DCC
+ local function cb(err, data)
+ if (err) then
+ logger.errx('DCC error: %1', err)
+ return 0
+ end
+ -- Parse the response
+ local _,_,result,disposition,header = tostring(data):find("(.-)\n(.-)\n(.-)\n")
+ logger.infox('DCC result=%1 disposition=%2 header="%3"', result, disposition, header)
+ local _,_,info = header:find("; (.-)$")
+ if (result == 'A') then
+ -- Accept
+ elseif (result == 'G') then
+ -- Greylist
+ elseif (result == 'R') then
+ -- Reject
+ task:insert_result('DCC_BULK', 1.0, info)
+ elseif (result == 'S') then
+ -- Accept for some recipients only
+ elseif (result == 'T') then
+ -- Temporary failure
+ logger.errx('DCC returned a temporary failure result')
+ else
+ -- Unknown result
+ logger.errx('DCC result error: %1', result);
+ end
+ return 0
+ end
+
+ -- Build the DCC query
+ -- https://www.dcc-servers.net/dcc/dcc-tree/dccifd.html#Protocol
+ local data = {
+ "header\n",
+ client .. "\n",
+ helo .. "\n",
+ envfrom .. "\n",
+ envrcpt .. "\n",
+ "\n",
+ task:get_content()
+ }
+
+ logger.infox('sending to dcc: client=%1 helo="%2" envfrom="%3" envrcpt="%4"',
+ client, helo, envfrom, envrcpt)
+
+ tcp.request({
+ task = task,
+ host = opts['host'],
+ port = opts['port'] or 1,
+ shutdown = true,
+ data = data,
+ callback = cb
+ })
+end
+
+-- Configuration
+if opts and opts['host'] then
+ local id = rspamd_config:register_symbol(symbol, 1.0, check_dcc)
+ rspamd_config:register_virtual_symbol(symbol_bulk, 1.0, id)
+else
+ logger.infox('DCC module not configured');
+end