1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
|
/*
* This program is RSPAMD agent for use with
* exim (http://www.exim.org) MTA by its local_scan feature.
*
* To enable exim local scan please copy this file to exim source tree
* Local/local_scan.c, edit Local/Makefile to add
*
* LOCAL_SCAN_SOURCE=Local/local_scan.c
* LOCAL_SCAN_HAS_OPTIONS=yes
*
* and compile exim.
*
* For exim compilation with local scan feature details please visit
* http://www.exim.org/exim-html-current/doc/html/spec_html/ch42.html
*
* For RSPAMD details please visit
* https://bitbucket.org/vstakhov/rspamd/
*
* Example configuration:
* **********************
*
* local_scan_timeout = 50s
*
* begin local_scan
* rspam_ip = 127.0.0.1
* rspam_port = 11333
* rspam_skip_sasl_authenticated = true
* # don't reject message if on of recipients from this list
* rspam_skip_rcpt = postmaster@example.com : some_user@example.com
* rspam_message = "Spam rejected; If this is not spam, please contact <postmaster@example.com>"
*
*
* $Id: local_scan.c 646 2010-08-11 11:49:36Z ayuzhaninov $
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/uio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <math.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include "local_scan.h"
#define REQUEST_LINES 64
#define REPLY_BUF_SIZE 16384
#define HEADER_STATUS "X-Rspam-Status"
#define HEADER_METRIC "X-Rspam-Metric"
#define HEADER_SCORE "X-Rspam-Score"
/* configuration options */
static uschar *daemon_ip = US"127.0.0.1";
static int max_scan_size = 4 * 1024 * 1024;
static uschar *reject_message = US"Spam message rejected";
static int daemon_port = 11333;
static BOOL skip_authenticated = TRUE;
static uschar *want_spam_rcpt_list = US"";
/* the entries must appear in alphabetical order */
optionlist local_scan_options[] = {
{ "rspam_ip", opt_stringptr, &daemon_ip },
{ "rspam_max_scan_size", opt_mkint, &max_scan_size },
{ "rspam_message", opt_stringptr, &reject_message },
{ "rspam_port", opt_int, &daemon_port },
{ "rspam_skip_rcpt", opt_stringptr, &want_spam_rcpt_list },
{ "rspam_skip_sasl_authenticated", opt_bool, &skip_authenticated },
};
int local_scan_options_count = sizeof(local_scan_options) / sizeof(optionlist);
/* push formatted line into vector */
int push_line(struct iovec *iov, int i, const char *fmt, ...);
int
local_scan(int fd, uschar **return_text)
{
struct stat sb;
struct sockaddr_in server_in;
int s, i, r, request_p = 0, headers_count = 0, is_spam = 0, is_reject = 0;
off_t message_size;
struct iovec request_v[REQUEST_LINES], *headers_v;
#if "@CMAKE_SYSTEM_NAME@" == "FreeBSD"
struct sf_hdtr headers_sf;
#endif
uschar *helo, *log_buf;
header_line *header_p;
char reply_buf[REPLY_BUF_SIZE], io_buf[BUFSIZ];
ssize_t size;
char *tok_ptr, *str;
char mteric[128], result[8];
float score, required_score;
*return_text = reject_message;
/*
* one msaage can be send via exim+rspamd twice
* remove header from previous pass
*/
header_remove(0, US HEADER_STATUS);
header_remove(0, US HEADER_METRIC);
/* check message size */
fstat(fd,&sb); /* XXX shuld check error */
message_size = sb.st_size - SPOOL_DATA_START_OFFSET;
if (message_size > max_scan_size) {
header_add(' ', HEADER_STATUS ": skip_big\n");
log_write (0, LOG_MAIN, "rspam: message larger than rspam_max_scan_size, accept");
return LOCAL_SCAN_ACCEPT;
}
/* don't scan mail from authenticated hosts */
if (skip_authenticated && sender_host_authenticated != NULL) {
header_add(' ', HEADER_STATUS ": skip_authenticated\n");
log_write(0, LOG_MAIN, "rspam: from=<%s> ip=%s authenticated (%s), skip check\n",
sender_address,
sender_host_address == NULL ? US"localhost" : sender_host_address,
sender_host_authenticated);
return LOCAL_SCAN_ACCEPT;
}
/*
* add status header, which mean, that message was not scanned
* if message will be scanned, this header will be replaced
*/
header_add(' ', HEADER_STATUS ": check_error\n");
/* create socket */
memset(&server_in, 0, sizeof(server_in));
server_in.sin_family = AF_INET;
server_in.sin_port = htons(daemon_port);
server_in.sin_addr.s_addr = inet_addr(daemon_ip);
if ((s = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
log_write(0, LOG_MAIN, "rspam: socket (%d: %s)", errno, strerror(errno));
return LOCAL_SCAN_ACCEPT;
}
if (connect(s, (struct sockaddr *) &server_in, sizeof(server_in)) < 0) {
close(s);
log_write(0, LOG_MAIN, "rspam: can't connect to %s:%d (%d: %s)", daemon_ip, daemon_port, errno, strerror(errno));
return LOCAL_SCAN_ACCEPT;
}
/* count message headers */
for (header_p = header_list; header_p != NULL; header_p = header_p->next) {
/* header type '*' is used for replaced or deleted header */
if (header_p->type == '*')
continue;
headers_count++;
}
/* write message headers to vector */
#if "@CMAKE_SYSTEM_NAME@" == "FreeBSD"
memset(&headers_sf, 0, sizeof(headers_sf));
if (headers_count > 0) {
headers_v = store_get((headers_count + 1)* sizeof(*headers_v));
i = 0;
for (header_p = header_list; header_p != NULL; header_p = header_p->next) {
if (header_p->type == '*')
continue;
headers_v[i].iov_base = header_p->text;
headers_v[i].iov_len = header_p->slen;
i++;
message_size += header_p->slen;
}
headers_v[i].iov_base = "\n";
headers_v[i].iov_len = strlen("\n");
message_size += strlen("\n");
headers_sf.headers = headers_v;
headers_sf.hdr_cnt = headers_count + 1;
}
#else
if (headers_count > 0) {
headers_v = store_get((headers_count + 1)* sizeof(*headers_v));
i = 0;
for (header_p = header_list; header_p != NULL; header_p = header_p->next) {
if (header_p->type == '*')
continue;
headers_v[i].iov_base = header_p->text;
headers_v[i].iov_len = header_p->slen;
i++;
message_size += header_p->slen;
}
headers_v[i].iov_base = "\n";
headers_v[i].iov_len = strlen("\n");
message_size += strlen("\n");
#endif
/* write request to vector */
r = 0;
r += push_line(request_v, request_p++, "SYMBOLS RSPAMC/1.1\r\n");
r += push_line(request_v, request_p++, "Content-length: " OFF_T_FMT "\r\n", message_size);
r += push_line(request_v, request_p++, "Queue-Id: %s\r\n", message_id);
r += push_line(request_v, request_p++, "From: %s\r\n", sender_address);
r += push_line(request_v, request_p++, "Recipient-Number: %d\r\n", recipients_count);
for (i = 0; i < recipients_count; i ++)
r += push_line(request_v, request_p++, "Rcpt: %s\r\n", recipients_list[i].address);
if ((helo = expand_string(US"$sender_helo_name")) != NULL && *helo != '\0')
r += push_line(request_v, request_p++, "Helo: %s\r\n", helo);
if (sender_host_address != NULL)
r += push_line(request_v, request_p++, "IP: %s\r\n", sender_host_address);
r += push_line(request_v, request_p++, "\r\n");
if (r < 0) {
close(s);
return LOCAL_SCAN_ACCEPT;
}
/* send request */
if (writev(s, request_v, request_p) < 0) {
close(s);
log_write(0, LOG_MAIN, "rspam: can't send request to %s:%d (%d: %s)", daemon_ip, daemon_port, errno, strerror(errno));
return LOCAL_SCAN_ACCEPT;
}
#if "@CMAKE_SYSTEM_NAME@" == "FreeBSD"
/* send headers (from iovec) and message body (from file) */
if (sendfile(fd, s, SPOOL_DATA_START_OFFSET, 0, &headers_sf, NULL, 0) < 0) {
close(s);
log_write(0, LOG_MAIN, "rspam: can't send message to %s:%d (%d: %s)", daemon_ip, daemon_port, errno, strerror(errno));
return LOCAL_SCAN_ACCEPT;
}
#else
/* send headers */
if (writev(s, headers_v, headers_count) < 0) {
close(s);
log_write(0, LOG_MAIN, "rspam: can't send headers to %s:%d (%d: %s)", daemon_ip, daemon_port, errno, strerror(errno));
return LOCAL_SCAN_ACCEPT;
}
/* Send message */
while ((r = read (fd, io_buf, sizeof (io_buf))) > 0) {
if (write (s, io_buf, r) < 0) {
close(s);
log_write(0, LOG_MAIN, "rspam: can't send message to %s:%d (%d: %s)", daemon_ip, daemon_port, errno, strerror(errno));
return LOCAL_SCAN_ACCEPT;
}
}
#endif
/* read reply from rspamd */
reply_buf[0] = '\0';
size = 0;
while ((r = read(s, reply_buf + size, sizeof(reply_buf) - size - 1)) > 0 && size < sizeof(reply_buf) - 1) {
size += r;
}
if (r < 0) {
close(s);
log_write(0, LOG_MAIN, "rspam: can't read from %s:%d (%d: %s)", daemon_ip, daemon_port, errno, strerror(errno));
return LOCAL_SCAN_ACCEPT;
}
reply_buf[size] = '\0';
close(s);
if (size >= REPLY_BUF_SIZE - 1) {
log_write(0, LOG_MAIN, "rspam: buffer is full, reply may be truncated");
}
/* parse reply */
tok_ptr = reply_buf;
/*
* rspamd can use several metrics, logic implemented here:
* if any metric more than reject_score - will reject
* if any metric true - message will be marked as spam
*/
/* First line is: <PROTOCOL>/<VERSION> <ERROR_CODE> <ERROR_REPLY> */
str = strsep(&tok_ptr, "\r\n");
if (str != NULL && sscanf(str, "%*s %d %*s", &i) == 1) {
if (i != 0) {
log_write(0, LOG_MAIN, "rspam: server error: %s", str);
return LOCAL_SCAN_ACCEPT;
}
} else {
log_write(0, LOG_MAIN, "rspam: bad reply from server: %s", str);
return LOCAL_SCAN_ACCEPT;
}
while ((str = strsep(&tok_ptr, "\r\n")) != NULL) {
/* skip empty tockens */
if (*str == '\0')
continue;
if (strncmp(str, "Metric:", strlen("Metric:")) == 0) {
/*
* parse line like
* Metric: default; False; 27.00 / 30.00
*/
if (sscanf(str, "Metric: %s %s %f / %f",
mteric, result, &score, &required_score) == 4) {
log_write(0, LOG_MAIN, "rspam: metric %s %s %.2f / %.2f",
mteric, result, score, required_score);
header_add(' ', HEADER_METRIC ": %s %s %.2f / %.2f\n",
mteric, result, score, required_score);
/* integers score for use in sieve ascii-numeric comparator */
if (strcmp(mteric, "default;") == 0)
header_add(' ', HEADER_SCORE ": %d\n",
(int)round(score));
} else {
log_write(0, LOG_MAIN, "rspam: can't parse: %s", str);
return LOCAL_SCAN_ACCEPT;
}
} else if (strncmp(str, "Action:", strlen("Action:")) == 0) {
/* line like Action: add header */
str += strlen("Action: ");
if (strncmp(str, "reject", strlen("reject")) == 0) {
is_reject = 1;
is_spam = 1;
} else if (strncmp(str, "add header", strlen("add header")) == 0) {
is_spam = 1;
}
}
}
/* XXX many allocs by string_sprintf()
* better to sprintf() to single buffer allocated by store_get()
*/
log_buf = string_sprintf("message to");
for (i = 0; i < recipients_count; i ++) {
log_buf = string_sprintf("%s %s", log_buf, recipients_list[i].address);
if (is_reject && lss_match_address(recipients_list[i].address, want_spam_rcpt_list, TRUE) == OK) {
is_reject = 0;
log_write(0, LOG_MAIN, "rspam: %s want spam, don't reject this message", recipients_list[i].address);
}
}
if (is_reject) {
log_write(0, LOG_MAIN, "rspam: reject %s", log_buf);
return LOCAL_SCAN_REJECT;
}
header_remove(0, US HEADER_STATUS);
if (is_spam) {
header_add(' ', HEADER_STATUS ": spam\n");
log_write(0, LOG_MAIN, "rspam: message marked as spam");
} else {
header_add(' ', HEADER_STATUS ": ham\n");
log_write(0, LOG_MAIN, "rspam: message marked as ham");
}
return LOCAL_SCAN_ACCEPT;
}
int
push_line(struct iovec *iov, const int i, const char *fmt, ...)
{
va_list ap;
size_t len;
char buf[512];
if (i >= REQUEST_LINES) {
log_write(0, LOG_MAIN, "rspam: %s: index out of bounds", __FUNCTION__);
return (-1);
}
va_start(ap, fmt);
len = vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
iov[i].iov_base = string_copy(US buf);
iov[i].iov_len = len;
if (len >= sizeof(buf)) {
log_write(0, LOG_MAIN, "rspam: %s: error, string was longer than %d", __FUNCTION__, sizeof(buf));
return (-1);
}
return 0;
}
|