You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

local_scan.c.in 11KB

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