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 *domain_esc = NULL;
  253. char *domain = NULL;
  254.  
  255. domain = strrchr(rcpt_to, '@');
  256. /* fallback to full rcpt_to if there's no domain */
  257. domain = (domain) ? domain + 1 : rcpt_to;
  258. domain_esc = malloc(strlen(domain)*2 + 1);
  259. mysql_real_escape_string(mysql, domain_esc, domain, strlen(domain));
  260. sprintf(query, "SET @uipaddr = inet_aton('%s'), @urcpt_to = '%s'; ", relay_ip, domain_esc);
  261. if (mysql_query_wrapper(mysql, query))
  262. {
  263. gllog(LOGLEVEL_ERROR, "greylisting: mysql: %s\n", mysql_error(mysql));
  264. return 0;
  265. }
  266. free(domain_esc);
  267.  
  268. #if MYSQL_VERSION_ID >= 50003
  269. sprintf(query,
  270. "SELECT `id`, `block_expires` >= UTC_TIMESTAMP(), `block_expires` < UTC_TIMESTAMP() "
  271. "FROM `greylisting_lists` "
  272. "WHERE `record_expires` > UTC_TIMESTAMP() "
  273. "AND ( "
  274. "( "
  275. "`rcpt_to` IS NULL "
  276. "AND `ipaddr_start` <= @uipaddr "
  277. "AND @uipaddr <= `ipaddr_end` "
  278. ") OR ( "
  279. "`ipaddr` IS NULL "
  280. "AND `rcpt_to` = @urcpt_to "
  281. ") "
  282. ") "
  283. "ORDER BY (`ipaddr_end` - `ipaddr_start`) ASC "
  284. "LIMIT 1");
  285. #else
  286. sprintf(query,
  287. "SELECT `id`, `block_expires` >= UTC_TIMESTAMP(), `block_expires` < UTC_TIMESTAMP() "
  288. "FROM `greylisting_lists` "
  289. "WHERE `record_expires` > UTC_TIMESTAMP() "
  290. "AND ( "
  291. "( "
  292. "`rcpt_to` IS NULL "
  293. "AND (@base := IF(INSTR(`ipaddr`, '.'), 32, 128)) "
  294. "AND IF( "
  295. "INSTR(`ipaddr`, '/'), "
  296. "(@ipaddr_start := inet_aton(substring_index(`ipaddr`, '/', 1))) "
  297. "AND (@ipaddr_count := POW(2, @base - substring_index(`ipaddr`, '/', -1))) "
  298. "AND (@ipaddr_end := @ipaddr_start + @ipaddr_count - 1), "
  299. "(@ipaddr_start := inet_aton(`ipaddr`)) "
  300. "AND (@ipaddr_end := @ipaddr_start) "
  301. ") "
  302. "AND @ipaddr_start <= @uipaddr "
  303. "AND @uipaddr <= @ipaddr_end "
  304. ") OR ( "
  305. "`ipaddr` IS NULL "
  306. "AND `rcpt_to` = @urcpt_to "
  307. ") "
  308. ") "
  309. "ORDER BY (@ipaddr_end - @ipaddr_start) ASC "
  310. "LIMIT 1");
  311. #endif
  312.  
  313. if (mysql_query_wrapper(mysql, query) ||
  314. !(res = mysql_store_result(mysql)))
  315. {
  316. gllog(LOGLEVEL_ERROR, "greylisting: mysql: %s\n", mysql_error(mysql));
  317. return 0;
  318. }
  319.  
  320. if ((row = mysql_fetch_row(res)))
  321. {
  322. if (atoi(row[1]))
  323. {
  324. found = RET_REJECT;
  325. gllog(LOGLEVEL_INFO, "greylisting: %s/%s is blacklisted (id=%s) - rejecting\n", relay_ip, domain, row[0]);
  326. }
  327. else if (atoi(row[2]))
  328. {
  329. found = RET_ACCEPT;
  330. gllog(LOGLEVEL_INFO, "greylisting: %s/%s is whitelisted (id=%s) - accepting\n", relay_ip, domain, row[0]);
  331. }
  332. }
  333.  
  334. mysql_free_result(res);
  335. return found;
  336. }
  337.  
  338. int check_greylisted(MYSQL *mysql)
  339. {
  340. MYSQL_RES *res;
  341. MYSQL_ROW row;
  342. char query[SQLCMDSIZE];
  343. char *mail_from_esc = NULL;
  344. char *rcpt_to_esc = NULL;
  345. char *relay_ip_sub = NULL;
  346. char *ptr;
  347. char ipdelimeter;
  348. int ret = RET_NOTFOUND;
  349.  
  350. mail_from_esc = malloc(strlen(mail_from)*2 + 1);
  351. rcpt_to_esc = malloc(strlen(rcpt_to)*2 + 1);
  352. mysql_real_escape_string(mysql, mail_from_esc, mail_from, strlen(mail_from));
  353. mysql_real_escape_string(mysql, rcpt_to_esc, rcpt_to, strlen(rcpt_to));
  354.  
  355. /*
  356.   * 0 ... query matches anything in the same /24 subnet
  357.   * 1 ... query does an exact ip match
  358.   */
  359. if (0)
  360. {
  361. sprintf(query,
  362. "SELECT `id`, `block_expires` < UTC_TIMESTAMP() "
  363. "FROM `greylisting_data` "
  364. "WHERE `record_expires` > UTC_TIMESTAMP() "
  365. "AND `relay_ip` = '%s' "
  366. "AND ( "
  367. "( "
  368. "`mail_from` = '%s' "
  369. "AND `rcpt_to` = '%s' "
  370. ") OR ( "
  371. "`mail_from` IS NULL "
  372. "AND `rcpt_to` IS NULL "
  373. ") "
  374. ") "
  375. "ORDER BY `mail_from` ASC "
  376. "LIMIT 1",
  377. relay_ip,
  378. mail_from_esc,
  379. rcpt_to_esc);
  380. }
  381. else
  382. {
  383. /* strip off the last octet */
  384. ipdelimeter = '.';
  385. if (!strchr(relay_ip, '.'))
  386. ipdelimeter = ':';
  387. relay_ip_sub = strdup(relay_ip);
  388. ptr = strrchr(relay_ip_sub, ipdelimeter);
  389. if (ptr)
  390. *ptr = '\0';
  391.  
  392. sprintf(query,
  393. "SELECT `id`, `block_expires` < UTC_TIMESTAMP() "
  394. "FROM `greylisting_data` "
  395. "WHERE `record_expires` > UTC_TIMESTAMP() "
  396. "AND `relay_ip` LIKE '%s%c%%' "
  397. "AND ( "
  398. "( "
  399. "`mail_from` = '%s' "
  400. "AND `rcpt_to` = '%s' "
  401. ") OR ( "
  402. "`mail_from` IS NULL "
  403. "AND `rcpt_to` IS NULL "
  404. ") "
  405. ") "
  406. "ORDER BY `mail_from` ASC "
  407. "LIMIT 1",
  408. relay_ip_sub,
  409. ipdelimeter,
  410. mail_from_esc,
  411. rcpt_to_esc);
  412. free(relay_ip_sub);
  413. }
  414.  
  415. if (mysql_query_wrapper(mysql, query) ||
  416. !(res = mysql_store_result(mysql)))
  417. {
  418. gllog(LOGLEVEL_ERROR, "greylisting: mysql: %s\n", mysql_error(mysql));
  419. return RET_NOTFOUND;
  420. }
  421.  
  422. if ((row = mysql_fetch_row(res)))
  423. {
  424. if (atoi(row[1]))
  425. {
  426. sprintf(query,
  427. "UPDATE `greylisting_data` "
  428. "SET `mail_from` = NULL, `rcpt_to` = NULL, `record_expires` = UTC_TIMESTAMP() + INTERVAL %u DAY, `passed_count` = `passed_count` + 1 "
  429. "WHERE `id` = '%s'",
  430. record_expire_good, row[0]);
  431. ret = RET_ACCEPT;
  432. gllog(LOGLEVEL_INFO, "greylisting: %s (%s -> %s) exists (id=%s) - accepting\n", relay_ip, mail_from, rcpt_to, row[0]);
  433. }
  434. else
  435. {
  436. sprintf(query,
  437. "UPDATE `greylisting_data` "
  438. "SET `blocked_count` = `blocked_count` + 1 "
  439. "WHERE `id` = '%s'",
  440. row[0]);
  441. ret = RET_TEMPREJECT;
  442. gllog(LOGLEVEL_INFO, "greylisting: %s (%s -> %s) is blocked (id=%s) - temp. rejecting\n", relay_ip, mail_from, rcpt_to, row[0]);
  443. }
  444. }
  445. else
  446. {
  447. sprintf(query,
  448. "INSERT INTO `greylisting_data` "
  449. "VALUES (0, '%s', '%s', '%s', UTC_TIMESTAMP() + INTERVAL %u MINUTE, UTC_TIMESTAMP() + INTERVAL %u MINUTE, 1, 0, 0, UTC_TIMESTAMP(), UTC_TIMESTAMP())",
  450. relay_ip, mail_from_esc, rcpt_to_esc, block_expire, record_expire);
  451. ret = RET_TEMPREJECT;
  452. gllog(LOGLEVEL_INFO, "greylisting: %s (%s -> %s) doesn't exist. - temp. rejecting\n", relay_ip, mail_from, rcpt_to);
  453. }
  454. mysql_free_result(res);
  455.  
  456. if (mysql_query_wrapper(mysql, query))
  457. {
  458. gllog(LOGLEVEL_ERROR, "greylisting: mysql: %s\n", mysql_error(mysql));
  459. return RET_NOTFOUND;
  460. }
  461.  
  462. free(mail_from_esc);
  463. free(rcpt_to_esc);
  464. return ret;
  465. }
  466.  
  467. int main()
  468. {
  469. int ret = 1;
  470. int greylisted = 0;
  471. MYSQL *mysql = NULL;
  472.  
  473. /* load config */
  474. if (ret && !load_config())
  475. ret = 0;
  476.  
  477. /* connect to mysql */
  478. if (ret)
  479. {
  480. mysql_library_init(-1, NULL, NULL);
  481. mysql = mysql_init(NULL);
  482. if (!mysql_real_connect(mysql, mysql_host, mysql_user, mysql_pass, mysql_db, mysql_port, NULL, 0))
  483. {
  484. gllog(LOGLEVEL_FATAL, "greylisting: mysql: %s\n", mysql_error(mysql));
  485. ret = 0;
  486. }
  487. }
  488.  
  489. /* greylisting checks */
  490. if (ret && !greylisted)
  491. {
  492. greylisted = check_listed(mysql);
  493. if (greylisted == RET_NOTFOUND)
  494. greylisted = check_greylisted(mysql);
  495. }
  496.  
  497. /* print smtp error code */
  498. if (ret)
  499. {
  500. switch(greylisted)
  501. {
  502. case RET_REJECT:
  503. printf(CMD_REJECT);
  504. break;
  505. case RET_TEMPREJECT:
  506. printf(CMD_TEMPREJECT);
  507. break;
  508. }
  509. }
  510.  
  511. /* cleanup stuff */
  512. gllog(LOGLEVEL_DEBUG, "greylisting: exiting\n");
  513. if (mysql)
  514. mysql_close(mysql);
  515. mysql_library_end();
  516. cleanup();
  517. return !ret;
  518. }
  519.  
  520.