Download | Plain Text | No Line Numbers


  1. /*
  2.  * Copyright (C) 2006 Manuel Mausz (manuel@mausz.at)
  3.  * Origin code copyright (c) mjd@digitaleveryware.com 2003
  4.  * (http://www.digitaleveryware.com/projects/greylisting/)
  5.  *
  6.  * This program is free software; you can redistribute it and/or
  7.  * modify it under the terms of the GNU General Public License
  8.  * as published by the Free Software Foundation; either
  9.  * version 2 of the License, or (at your option) any later
  10.  * version.
  11.  *
  12.  * This program is distributed in the hope that it will be useful,
  13.  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14.  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15.  * GNU General Public License for more details.
  16.  *
  17.  * You should have received a copy of the GNU General Public License
  18.  * along with this program; if not, write to the Free Software Foundation,
  19.  * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  20.  */
  21.  
  22. #include <stdio.h>
  23. #include <stdlib.h>
  24. #include <string.h>
  25. #include <stdarg.h>
  26. #include <errno.h>
  27. #include <mysql.h>
  28.  
  29. #define SQLCMDSIZE 2048
  30. #define RET_NOTFOUND 0
  31. #define RET_ACCEPT 1
  32. #define RET_REJECT 2
  33. #define RET_TEMPREJECT 3
  34. #define CMD_TEMPREJECT "E451 temporary failure (#4.3.0)\n"
  35. #define CMD_REJECT "E553 sorry, your envelope sender has been denied (#5.7.1)\n"
  36. #define LOGLEVEL_FATAL 1
  37. #define LOGLEVEL_ERROR 2
  38. #define LOGLEVEL_WARN 3
  39. #define LOGLEVEL_INFO 4
  40. #define LOGLEVEL_DEBUG 5
  41. #define MAXCONFIGLINESIZE 1024 // maybe change this to dynamic allocation sometime
  42. #if MYSQL_VERSION_ID >= 50003
  43. # define QUERYSIZE 500
  44. #else
  45. # define QUERYSIZE 700
  46. #endif
  47.  
  48. static char *configfile = "control/greylisting";
  49. static char *mysql_host = NULL;
  50. static char *mysql_user = NULL;
  51. static char *mysql_pass = NULL;
  52. static char *mysql_db = NULL;
  53. unsigned int mysql_port = 3306;
  54. unsigned int block_expire = 55;
  55. unsigned int record_expire = 500;
  56. unsigned int record_expire_good = 36;
  57. static char *relay_ip;
  58. static char *mail_from;
  59. static char *rcpt_to;
  60. static int loglevel = LOGLEVEL_WARN;
  61.  
  62. void gllog(unsigned int level, char* format, ...)
  63. {
  64. va_list args;
  65. if (level > loglevel)
  66. return;
  67. va_start(args, format);
  68. vfprintf(stderr, format, args);
  69. va_end(args);
  70. }
  71.  
  72. int load_config(void)
  73. {
  74. char *tmp, *delim, *atpos;
  75. FILE *config;
  76. char buf[MAXCONFIGLINESIZE];
  77. int i;
  78. unsigned int userlen;
  79.  
  80. /* first check for logging var */
  81. tmp = getenv("GLLOGLEVEL");
  82. if (tmp)
  83. loglevel = atoi(tmp);
  84.  
  85. /* check if greylisting is enabled */
  86. if (!getenv("GREYLISTING") || getenv("RELAYCLIENT"))
  87. {
  88. gllog(LOGLEVEL_DEBUG, "greylisting: greylisting is not enabled\n");
  89. return 0;
  90. }
  91.  
  92. /* basic environment variables needed */
  93. relay_ip = getenv("TCPREMOTEIP");
  94. mail_from = getenv("SMTPMAILFROM");
  95. rcpt_to = getenv("SMTPRCPTTO");
  96. if (!relay_ip || !mail_from || !rcpt_to)
  97. {
  98. gllog(LOGLEVEL_FATAL, "greylisting: one of the following envvars is undefined: TCPREMOTEIP, SMTPMAILFROM, SMTPRCPTTO\n");
  99. return 0;
  100. }
  101.  
  102. /* check for BATV ("prvs=X=u@d.t" minimum) */
  103. if (strlen(mail_from) > 11
  104. && mail_from[0] == 'p' && mail_from[1] == 'r' && mail_from[2] == 'v'
  105. && mail_from[3] == 's' && mail_from[4] == '=')
  106. {
  107. /* BATV: prvs=HASH=user@domain.tld */
  108. if ((delim = strchr(mail_from + 5, '=')))
  109. mail_from = delim + 1;
  110. /* BATV: prvs=user/HASH@domain.tld */
  111. else if ((delim = strchr(mail_from + 5, '/')) && (atpos = strchr(delim, '@')))
  112. {
  113. userlen = delim - mail_from - 5;
  114. memmove(atpos - userlen, mail_from + 5, userlen);
  115. mail_from = atpos - userlen;
  116. }
  117. }
  118.  
  119. /* avoid buffer overflows (max. query is ~410 chars long) */
  120. if (strlen(relay_ip) + strlen(mail_from) + strlen(rcpt_to) > SQLCMDSIZE - QUERYSIZE)
  121. {
  122. gllog(LOGLEVEL_FATAL, "greylisting: buffer overflow protection occurs\n");
  123. return 0;
  124. }
  125.  
  126. /* fetch config file path */
  127. tmp = getenv("GLCONFIGFILE");
  128. if (tmp)
  129. configfile = tmp;
  130.  
  131. /* fetch config file content */
  132. gllog(LOGLEVEL_DEBUG, "greylisting: configfile=%s\n", configfile);
  133. config = fopen(configfile, "r");
  134. if (!config)
  135. gllog(LOGLEVEL_DEBUG, "greylisting: configfile error: %s\n", strerror(errno));
  136. else
  137. {
  138. while((tmp = fgets(buf, sizeof(buf), config)))
  139. {
  140. if (buf[0] == '#' || buf[0] == ';')
  141. continue;
  142. for(i = 0; i < strlen(buf) && buf[i] != '\r' && buf[i] != '\n'; i++);
  143. buf[i] = 0;
  144. if (strstr(tmp, "mysql_host=") == tmp)
  145. {
  146. free(mysql_host);
  147. mysql_host = strdup(tmp + strlen("mysql_host="));
  148. }
  149. else if (strstr(tmp, "mysql_port=") == tmp)
  150. mysql_port = atoi(tmp + strlen("mysql_port="));
  151. else if (strstr(tmp, "mysql_user=") == tmp)
  152. {
  153. free(mysql_user);
  154. mysql_user = strdup(tmp + strlen("mysql_user="));
  155. }
  156. else if (strstr(tmp, "mysql_pass=") == tmp)
  157. {
  158. free(mysql_pass);
  159. mysql_pass = strdup(tmp + strlen("mysql_pass="));
  160. }
  161. else if (strstr(tmp, "mysql_db=") == tmp)
  162. {
  163. free(mysql_db);
  164. mysql_db = strdup(tmp + strlen("mysql_db="));
  165. }
  166. else if (strstr(tmp, "block_expire=") == tmp)
  167. block_expire = atoi(tmp + strlen("block_expire="));
  168. else if (strstr(tmp, "record_expire=") == tmp)
  169. record_expire = atoi(tmp + strlen("record_expire="));
  170. else if (strstr(tmp, "record_expire_good=") == tmp)
  171. record_expire_good = atoi(tmp + strlen("record_expire_good="));
  172. else if (strstr(tmp, "loglevel=") == tmp && !getenv("GLLOGLEVEL"))
  173. loglevel = atoi(tmp + strlen("loglevel="));
  174. }
  175. fclose(config);
  176. }
  177.  
  178. /* environment variables */
  179. tmp = getenv("GLMYSQLHOST");
  180. if (tmp)
  181. {
  182. free(mysql_host);
  183. mysql_host = strdup(tmp);
  184. }
  185.  
  186. tmp = getenv("GLMYSQLPORT");
  187. if (tmp)
  188. mysql_port = atoi(tmp);
  189.  
  190. tmp = getenv("GLMYSQLUSER");
  191. if (tmp)
  192. {
  193. free(mysql_user);
  194. mysql_user = strdup(tmp);
  195. }
  196.  
  197. tmp = getenv("GLMYSQLPASS");
  198. if (tmp)
  199. {
  200. free(mysql_pass);
  201. mysql_pass = strdup(tmp);
  202. }
  203.  
  204. tmp = getenv("GLMYSQLDB");
  205. if (tmp)
  206. {
  207. free(mysql_db);
  208. mysql_db = strdup(tmp);
  209. }
  210.  
  211. tmp = getenv("GLBLOCKEXPIRE");
  212. if (tmp)
  213. block_expire = atoi(tmp);
  214.  
  215. tmp = getenv("GLRECORDEXPIRE");
  216. if (tmp)
  217. record_expire = atoi(tmp);
  218.  
  219. tmp = getenv("GLRECORDEXPIREGOOD");
  220. if (tmp)
  221. record_expire_good = atoi(tmp);
  222.  
  223. /* logging */
  224. gllog(LOGLEVEL_DEBUG, "greylisting: mysql: host=%s, port=%d, user=%s, pass=******\n", mysql_host, mysql_port, mysql_user);
  225. gllog(LOGLEVEL_DEBUG, "greylisting: block_expire=%d, record_expire=%d, record_expire_good=%d\n", block_expire, record_expire, record_expire_good);
  226. return 1;
  227. }
  228.  
  229. void cleanup()
  230. {
  231. free(mysql_host);
  232. free(mysql_user);
  233. free(mysql_pass);
  234. }
  235.  
  236. int mysql_query_wrapper(MYSQL *mysql, char *query)
  237. {
  238. int result;
  239.  
  240. result = mysql_query(mysql, query);
  241. gllog(LOGLEVEL_DEBUG, "greylisting: mysql: %s - ret=%d\n", query, result);
  242. return result;
  243. }
  244.  
  245. /* check if relay_ip or rcpt_to is white-/blacklisted */
  246. int check_listed(MYSQL *mysql)
  247. {
  248. MYSQL_RES *res;
  249. MYSQL_ROW row;
  250. char query[SQLCMDSIZE];
  251. int found = RET_NOTFOUND;
  252. char *rcpt_to_esc = NULL;
  253. char *domain_esc = NULL;
  254. char *domain = NULL;
  255.  
  256. domain = strrchr(rcpt_to, '@');
  257. /* fallback to full rcpt_to if there's no domain */
  258. domain = (domain) ? domain + 1 : rcpt_to;
  259. domain_esc = malloc(strlen(domain)*2 + 1);
  260. mysql_real_escape_string(mysql, domain_esc, domain, strlen(domain));
  261. rcpt_to_esc = malloc(strlen(rcpt_to)*2 + 1);
  262. mysql_real_escape_string(mysql, rcpt_to_esc, rcpt_to, strlen(rcpt_to));
  263. sprintf(query, "SET @uipaddr = inet_aton('%s'), @udomain = '%s', @urcpt_to = '%s'; ", relay_ip, domain_esc, rcpt_to_esc);
  264. if (mysql_query_wrapper(mysql, query))
  265. {
  266. gllog(LOGLEVEL_ERROR, "greylisting: mysql: %s\n", mysql_error(mysql));
  267. return 0;
  268. }
  269. free(domain_esc);
  270. free(rcpt_to_esc);
  271.  
  272. #if MYSQL_VERSION_ID >= 50003
  273. sprintf(query,
  274. "SELECT `id`, `block_expires` >= UTC_TIMESTAMP(), `block_expires` < UTC_TIMESTAMP() "
  275. "FROM `greylisting_lists` "
  276. "WHERE `record_expires` > UTC_TIMESTAMP() "
  277. "AND ( "
  278. "( "
  279. "`rcpt_to` IS NULL "
  280. "AND `ipaddr_start` <= @uipaddr "
  281. "AND @uipaddr <= `ipaddr_end` "
  282. ") OR ( "
  283. "`ipaddr` IS NULL "
  284. "AND ( "
  285. "`rcpt_to` = @udomain "
  286. "OR `rcpt_to` = @urcpt_to "
  287. ") "
  288. ") "
  289. ") "
  290. "ORDER BY (`ipaddr_end` - `ipaddr_start`) ASC "
  291. "LIMIT 1");
  292. #else
  293. sprintf(query,
  294. "SELECT `id`, `block_expires` >= UTC_TIMESTAMP(), `block_expires` < UTC_TIMESTAMP() "
  295. "FROM `greylisting_lists` "
  296. "WHERE `record_expires` > UTC_TIMESTAMP() "
  297. "AND ( "
  298. "( "
  299. "`rcpt_to` IS NULL "
  300. "AND (@base := IF(INSTR(`ipaddr`, '.'), 32, 128)) "
  301. "AND IF( "
  302. "INSTR(`ipaddr`, '/'), "
  303. "(@ipaddr_start := inet_aton(substring_index(`ipaddr`, '/', 1))) "
  304. "AND (@ipaddr_count := POW(2, @base - substring_index(`ipaddr`, '/', -1))) "
  305. "AND (@ipaddr_end := @ipaddr_start + @ipaddr_count - 1), "
  306. "(@ipaddr_start := inet_aton(`ipaddr`)) "
  307. "AND (@ipaddr_end := @ipaddr_start) "
  308. ") "
  309. "AND @ipaddr_start <= @uipaddr "
  310. "AND @uipaddr <= @ipaddr_end "
  311. ") OR ( "
  312. "`ipaddr` IS NULL "
  313. "AND ( "
  314. "`rcpt_to` = @udomain "
  315. "OR `rcpt_to` = @urcpt_to "
  316. ") "
  317. ") "
  318. ") "
  319. "ORDER BY (@ipaddr_end - @ipaddr_start) ASC "
  320. "LIMIT 1");
  321. #endif
  322.  
  323. if (mysql_query_wrapper(mysql, query) ||
  324. !(res = mysql_store_result(mysql)))
  325. {
  326. gllog(LOGLEVEL_ERROR, "greylisting: mysql: %s\n", mysql_error(mysql));
  327. return 0;
  328. }
  329.  
  330. if ((row = mysql_fetch_row(res)))
  331. {
  332. if (atoi(row[1]))
  333. {
  334. found = RET_REJECT;
  335. gllog(LOGLEVEL_INFO, "greylisting: %s/%s is blacklisted (id=%s) - rejecting\n", relay_ip, domain, row[0]);
  336. }
  337. else if (atoi(row[2]))
  338. {
  339. found = RET_ACCEPT;
  340. gllog(LOGLEVEL_INFO, "greylisting: %s/%s is whitelisted (id=%s) - accepting\n", relay_ip, domain, row[0]);
  341. }
  342. }
  343.  
  344. mysql_free_result(res);
  345. return found;
  346. }
  347.  
  348. int check_greylisted(MYSQL *mysql)
  349. {
  350. MYSQL_RES *res;
  351. MYSQL_ROW row;
  352. char query[SQLCMDSIZE];
  353. char *mail_from_esc = NULL;
  354. char *rcpt_to_esc = NULL;
  355. char *relay_ip_sub = NULL;
  356. char *ptr;
  357. char ipdelimeter;
  358. int ret = RET_NOTFOUND;
  359.  
  360. mail_from_esc = malloc(strlen(mail_from)*2 + 1);
  361. rcpt_to_esc = malloc(strlen(rcpt_to)*2 + 1);
  362. mysql_real_escape_string(mysql, mail_from_esc, mail_from, strlen(mail_from));
  363. mysql_real_escape_string(mysql, rcpt_to_esc, rcpt_to, strlen(rcpt_to));
  364.  
  365. /*
  366.   * 0 ... query matches anything in the same /24 subnet
  367.   * 1 ... query does an exact ip match
  368.   */
  369. if (0)
  370. {
  371. sprintf(query,
  372. "SELECT `id`, `block_expires` < UTC_TIMESTAMP() "
  373. "FROM `greylisting_data` "
  374. "WHERE `record_expires` > UTC_TIMESTAMP() "
  375. "AND `relay_ip` = '%s' "
  376. "AND `mail_from` = '%s' "
  377. "AND `rcpt_to` = '%s' "
  378. "LIMIT 1",
  379. relay_ip,
  380. mail_from_esc,
  381. rcpt_to_esc);
  382. }
  383. else
  384. {
  385. /* strip off the last octet */
  386. ipdelimeter = '.';
  387. if (!strchr(relay_ip, '.'))
  388. ipdelimeter = ':';
  389. relay_ip_sub = strdup(relay_ip);
  390. ptr = strrchr(relay_ip_sub, ipdelimeter);
  391. if (ptr)
  392. *ptr = '\0';
  393.  
  394. sprintf(query,
  395. "SELECT `id`, `block_expires` < UTC_TIMESTAMP() "
  396. "FROM `greylisting_data` "
  397. "WHERE `record_expires` > UTC_TIMESTAMP() "
  398. "AND `relay_ip` LIKE '%s%c%%' "
  399. "AND `mail_from` = '%s' "
  400. "AND `rcpt_to` = '%s' "
  401. "LIMIT 1",
  402. relay_ip_sub,
  403. ipdelimeter,
  404. mail_from_esc,
  405. rcpt_to_esc);
  406. free(relay_ip_sub);
  407. }
  408.  
  409. if (mysql_query_wrapper(mysql, query) ||
  410. !(res = mysql_store_result(mysql)))
  411. {
  412. gllog(LOGLEVEL_ERROR, "greylisting: mysql: %s\n", mysql_error(mysql));
  413. return RET_NOTFOUND;
  414. }
  415.  
  416. if ((row = mysql_fetch_row(res)))
  417. {
  418. if (atoi(row[1]))
  419. {
  420. sprintf(query,
  421. "UPDATE `greylisting_data` "
  422. "SET `record_expires` = UTC_TIMESTAMP() + INTERVAL %u DAY, `passed_count` = `passed_count` + 1 "
  423. "WHERE `id` = '%s'",
  424. record_expire_good, row[0]);
  425. ret = RET_ACCEPT;
  426. gllog(LOGLEVEL_INFO, "greylisting: %s (%s -> %s) exists (id=%s) - accepting\n", relay_ip, mail_from, rcpt_to, row[0]);
  427. }
  428. else
  429. {
  430. sprintf(query,
  431. "UPDATE `greylisting_data` "
  432. "SET `blocked_count` = `blocked_count` + 1 "
  433. "WHERE `id` = '%s'",
  434. row[0]);
  435. ret = RET_TEMPREJECT;
  436. gllog(LOGLEVEL_INFO, "greylisting: %s (%s -> %s) is blocked (id=%s) - temp. rejecting\n", relay_ip, mail_from, rcpt_to, row[0]);
  437. }
  438. }
  439. else
  440. {
  441. sprintf(query,
  442. "INSERT INTO `greylisting_data` "
  443. "VALUES (0, '%s', '%s', '%s', UTC_TIMESTAMP() + INTERVAL %u MINUTE, UTC_TIMESTAMP() + INTERVAL %u MINUTE, 1, 0, 0, UTC_TIMESTAMP(), UTC_TIMESTAMP())",
  444. relay_ip, mail_from_esc, rcpt_to_esc, block_expire, record_expire);
  445. ret = RET_TEMPREJECT;
  446. gllog(LOGLEVEL_INFO, "greylisting: %s (%s -> %s) doesn't exist. - temp. rejecting\n", relay_ip, mail_from, rcpt_to);
  447. }
  448. mysql_free_result(res);
  449.  
  450. if (mysql_query_wrapper(mysql, query))
  451. {
  452. gllog(LOGLEVEL_ERROR, "greylisting: mysql: %s\n", mysql_error(mysql));
  453. return RET_NOTFOUND;
  454. }
  455.  
  456. free(mail_from_esc);
  457. free(rcpt_to_esc);
  458. return ret;
  459. }
  460.  
  461. int main()
  462. {
  463. int ret = 1;
  464. int greylisted = 0;
  465. MYSQL *mysql = NULL;
  466.  
  467. /* load config */
  468. if (ret && !load_config())
  469. ret = 0;
  470.  
  471. /* connect to mysql */
  472. if (ret)
  473. {
  474. mysql_library_init(-1, NULL, NULL);
  475. mysql = mysql_init(NULL);
  476. if (!mysql_real_connect(mysql, mysql_host, mysql_user, mysql_pass, mysql_db, mysql_port, NULL, 0))
  477. {
  478. gllog(LOGLEVEL_FATAL, "greylisting: mysql: %s\n", mysql_error(mysql));
  479. ret = 0;
  480. }
  481. }
  482.  
  483. /* greylisting checks */
  484. if (ret && !greylisted)
  485. {
  486. greylisted = check_listed(mysql);
  487. if (greylisted == RET_NOTFOUND)
  488. greylisted = check_greylisted(mysql);
  489. }
  490.  
  491. /* print smtp error code */
  492. if (ret)
  493. {
  494. switch(greylisted)
  495. {
  496. case RET_REJECT:
  497. printf(CMD_REJECT);
  498. break;
  499. case RET_TEMPREJECT:
  500. printf(CMD_TEMPREJECT);
  501. break;
  502. }
  503. }
  504.  
  505. /* cleanup stuff */
  506. gllog(LOGLEVEL_DEBUG, "greylisting: exiting\n");
  507. if (mysql)
  508. mysql_close(mysql);
  509. mysql_library_end();
  510. cleanup();
  511. return !ret;
  512. }
  513.  
  514.