Blame view

libmu_sieve/extensions/spamd.c 13.8 KB
1
/* GNU Mailutils -- a suite of utilities for electronic mail
Sergey Poznyakoff authored
2
   Copyright (C) 2003-2005, 2007-2012, 2014-2017 Free Software
3
   Foundation, Inc.
4 5 6

   GNU Mailutils is free software; you can redistribute it and/or modify
   it under the terms of the GNU Lesser General Public License as published by
7
   the Free Software Foundation; either version 3, or (at your option)
8 9 10 11 12 13 14
   any later version.

   GNU Mailutils is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU Lesser General Public License for more details.

15
   You should have received a copy of the GNU Lesser General Public
16 17
   License along with GNU Mailutils.  If not, see
   <http://www.gnu.org/licenses/>. */
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32

/* This module implements sieve extension test "spamd": an interface to
   the SpamAssassin spamd daemon. See "Usage:" below for the description */

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif  

#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <string.h>
#include <signal.h>
Wojciech Polak authored
33
#include <mailutils/sieve.h>
34
#include <mailutils/mu_auth.h>
Sergey Poznyakoff authored
35
#include <mailutils/nls.h>
Sergey Poznyakoff authored
36 37
#include <mailutils/filter.h>
#include <mailutils/stream.h>
38 39 40 41 42 43 44

#define DEFAULT_SPAMD_PORT 783


/* Auxiliary functions */

static int
45
spamd_connect_tcp (mu_sieve_machine_t mach, mu_stream_t *stream,
46 47
		   char *host, int port)
{
48
  int rc = mu_tcp_stream_create (stream, host, port, MU_STREAM_RDWR);
49 50
  if (rc)
    {
51
      mu_sieve_error (mach, "mu_tcp_stream_create: %s", mu_strerror (rc));
52 53 54 55 56 57
      return rc;
    }
  return rc;
}

static int
58
spamd_connect_socket (mu_sieve_machine_t mach, mu_stream_t *stream, char *path)
59
{
60
  int rc = mu_socket_stream_create (stream, path, MU_STREAM_RDWR);
61 62
  if (rc)
    {
Sergey Poznyakoff authored
63
      mu_sieve_error (mach, "mu_socket_stream_create: %s", mu_strerror (rc));
64 65 66 67 68 69
      return rc;
    }
  return rc;
}

static void
70
spamd_destroy (mu_stream_t *stream)
71
{
72
  mu_stream_close (*stream);
73
  mu_stream_destroy (stream);
74 75 76
}

static void
77
spamd_send_command (mu_stream_t stream, const char *fmt, ...)
78 79 80 81 82 83 84 85
{
  char buf[512];
  size_t n;
  va_list ap;

  va_start (ap, fmt);
  n = vsnprintf (buf, sizeof buf, fmt, ap);
  va_end (ap);
86
  mu_stream_writeline (stream, buf, n);
87 88
}

Sergey Poznyakoff authored
89
static int
90
spamd_send_message (mu_stream_t stream, mu_message_t msg, int dbg)
91
{
Sergey Poznyakoff authored
92 93
  int rc;
  mu_stream_t mstr, flt;
94 95 96 97 98 99
  struct mu_buffer_query newbuf, oldbuf;
  int bufchg = 0;
  mu_debug_handle_t dlev;
  int xlev;
  int xlevchg = 0;
  
Sergey Poznyakoff authored
100 101 102
  rc = mu_message_get_streamref (msg, &mstr);
  if (rc)
    return rc;
103
  rc = mu_filter_create (&flt, mstr, "CRLF", MU_FILTER_ENCODE,
104
			 MU_STREAM_READ|MU_STREAM_SEEK);
Sergey Poznyakoff authored
105
  if (rc)
106
    {
Sergey Poznyakoff authored
107 108
      mu_stream_destroy (&mstr);
      return rc;
109
    }
Sergey Poznyakoff authored
110

111 112 113 114 115 116 117 118 119 120 121 122
  /* Ensure effective transport buffering */
  if (mu_stream_ioctl (stream, MU_IOCTL_TRANSPORT_BUFFER,
		       MU_IOCTL_OP_GET, &oldbuf) == 0)
    {
      newbuf.type = MU_TRANSPORT_OUTPUT;
      newbuf.buftype = mu_buffer_full;
      newbuf.bufsize = 64*1024;
      mu_stream_ioctl (stream, MU_IOCTL_TRANSPORT_BUFFER, MU_IOCTL_OP_SET, 
		       &newbuf);
      bufchg = 1;
    }

123 124
  if (dbg &&
      mu_debug_category_level ("sieve", 5, &dlev) == 0 &&
125 126 127 128 129 130 131 132 133
      !(dlev & MU_DEBUG_LEVEL_MASK (MU_DEBUG_TRACE9)))
    {
      /* Mark out the following data as payload */
      xlev = MU_XSCRIPT_PAYLOAD;
      if (mu_stream_ioctl (stream, MU_IOCTL_XSCRIPTSTREAM,
			   MU_IOCTL_XSCRIPTSTREAM_LEVEL, &xlev) == 0)
	xlevchg = 1;
    }
  
134
  rc = mu_stream_copy (stream, flt, 0, NULL);
Sergey Poznyakoff authored
135

136 137 138 139 140 141 142 143
  /* Restore prior transport buffering and xscript level */
  if (bufchg)
    mu_stream_ioctl (stream, MU_IOCTL_TRANSPORT_BUFFER, MU_IOCTL_OP_SET, 
		     &oldbuf);
  if (xlevchg)
    mu_stream_ioctl (stream, MU_IOCTL_XSCRIPTSTREAM,
		     MU_IOCTL_XSCRIPTSTREAM_LEVEL, &xlev);
  
Sergey Poznyakoff authored
144 145 146
  mu_stream_destroy (&mstr);
  mu_stream_destroy (&flt);
  return rc;
147 148 149 150 151
}

#define char_to_num(c) (c-'0')

static void
152
decode_float (long *vn, const char *str, int digits, char **endp)
153
{
154
  long v;
155 156 157
  size_t frac = 0;
  size_t base = 1;
  int i;
158
  int negative = 0;
159
  char *end;
160 161 162 163
  
  for (i = 0; i < digits; i++)
    base *= 10;
  
164 165
  v = strtol (str, &end, 10);
  str = end;
166 167 168 169 170 171
  if (v < 0)
    {
      negative = 1;
      v = - v;
    }
  
172 173 174
  v *= base;
  if (*str == '.')
    {
175 176
      for (str++, i = 0; *str && mu_isdigit (*str) && i < digits;
	   i++, str++)
177
	frac = frac * 10 + char_to_num (*str);
178
      if (*str && mu_isdigit (*str))
179 180 181
	{
	  if (char_to_num (*str) >= 5)
	    frac++;
182 183 184
	  if (endp)
	    while (*str && mu_isdigit (*str))
	      str++;
185 186 187 188 189 190
	}
      else
	for (; i < digits; i++)
	  frac *= 10;
    }
  *vn = v + frac;
191 192
  if (negative)
    *vn = - *vn;
193 194
  if (endp)
    *endp = (char*) str;
195 196 197 198 199
}

static int
decode_boolean (char *str)
{
200
  if (mu_c_strcasecmp (str, "true") == 0)
201
    return 1;
202
  else if (mu_c_strcasecmp (str, "false") == 0)
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
    return 0;
  /*else?*/
  return 0;
}


/* Signal handling */

typedef RETSIGTYPE (*signal_handler)(int);

static signal_handler
set_signal_handler (int sig, signal_handler h)
{
#ifdef HAVE_SIGACTION
  struct sigaction act, oldact;
  act.sa_handler = h;
  sigemptyset (&act.sa_mask);
  act.sa_flags = 0;
  sigaction (sig, &act, &oldact);
  return oldact.sa_handler;
#else
  return signal (sig, h);
#endif
}

void
229
spamd_abort (mu_sieve_machine_t mach, mu_stream_t *stream, signal_handler handler)
230 231 232
{
  spamd_destroy (stream);
  set_signal_handler (SIGPIPE, handler);
233
  mu_sieve_abort (mach);
234 235 236
}

static int got_sigpipe;
237
static signal_handler handler;
238 239

static RETSIGTYPE
240
sigpipe_handler (int sig MU_ARG_UNUSED)
241 242 243 244
{
  got_sigpipe = 1;
}

245 246 247 248 249 250 251
static void
spamd_read_line (mu_sieve_machine_t mach, mu_stream_t stream,
		 char **pbuffer, size_t *psize)
{
  size_t n;
  int rc = mu_stream_getline (stream, pbuffer, psize, &n);
  if (rc == 0)
252
    mu_rtrim_class (*pbuffer, MU_CTYPE_ENDLN);
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
  else
    {
      /* FIXME: Need an 'onabort' mechanism in Sieve machine, which
	 would restore the things to their prior state.  This will
	 also allow to make handler local again. */
      free (pbuffer);
      mu_sieve_error (mach, "read error: %s", mu_strerror (rc));
      spamd_abort (mach, &stream, handler);
    }
}

static int
parse_response_line (mu_sieve_machine_t mach, const char *buffer)
{
  const char *str;
  char *end;
  long version;
  unsigned long resp;
  
  str = buffer;
  if (strncmp (str, "SPAMD/", 6))
    return MU_ERR_BADREPLY;
  str += 6;

  decode_float (&version, str, 1, &end);
  if (version < 10)
    {
      mu_sieve_error (mach, _("unsupported SPAMD version: %s"), str);
      return MU_ERR_BADREPLY;
    }

  str = mu_str_skip_class (end, MU_CTYPE_SPACE);
  if (!*str || !mu_isdigit (*str))
    {
      mu_sieve_error (mach, _("malformed spamd response: %s"), buffer);
      return MU_ERR_BADREPLY;
    }

  resp = strtoul (str, &end, 10);
  if (resp != 0)
    {
      mu_sieve_error (mach, _("spamd failure: %lu %s"), resp, end);
      return MU_ERR_REPLY;
    }
  return 0;
}
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
/* Compute the "real" size of the message.  This takes into account filtering
   applied by spamd_send_message (LF->CRLF transcription).

   FIXME: Previous versions used mu_message_size, but it turned out to be
   unreliable, because it sometimes returns a "normalized" size, which differs
   from the real one.  This happens when the underlying implementation does
   not provide a _get_size method, so that the size is computed as a sum of
   message body and header sizes.  This latter is returned by mu_header_size,
   which ignores extra whitespace around each semicolon (see header_parse in
   libmailutils/mailbox/header.c).
 */
static int
get_real_message_size (mu_message_t msg, size_t *psize)
{
  mu_stream_t null;
  mu_stream_stat_buffer stat;
  int rc;
  
  rc = mu_nullstream_create (&null, MU_STREAM_WRITE);
  if (rc)
    return rc;
  mu_stream_set_stat (null, MU_STREAM_STAT_MASK (MU_STREAM_STAT_OUT), stat);
  rc = spamd_send_message (null, msg, 0);
  mu_stream_destroy (&null);
  if (rc == 0)
    *psize = stat[MU_STREAM_STAT_OUT];
  return rc;
}

329 330
/* The test proper */

331
/* Syntax: spamd [":host" <tcp-host: string>]
332 333
                 [":port" <tcp-port: number> /
                  ":socket" <unix-socket: string>]
Sergey Poznyakoff authored
334
		 [":user" <name: string>] 
335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355
		 [":over" / ":under" <limit: string>]

   The "spamd" test is an interface to "spamd" facility of
   SpamAssassin mail filter. It evaluates to true if SpamAssassin
   recognized the message as spam, or the message spam score
   satisfies the given relation.

   If the argument is ":over" and the spam score is greater than
   or equal to the number provided, the test is true; otherwise,
   it is false.

   If the argument is ":under" and the spam score is less than
   or equal to the number provided, the test is true; otherwise,
   it is false.

   Spam score is a floating point number. The comparison takes into
   account three decimal digits.

*/

static int
356
spamd_test (mu_sieve_machine_t mach)
357
{
358
  char *buffer = NULL;
359
  size_t size;
360
  char spam_str[6], score_str[21], threshold_str[21];
361
  int rc;
362
  int result;
363
  long score, threshold, limit;
364
  mu_stream_t stream = NULL, null;
365
  mu_message_t msg;
366
  char *host;
367 368
  size_t num;
  char *str;
369
  mu_header_t hdr;
370
  mu_debug_handle_t lev = 0;
Sergey Poznyakoff authored
371
  
Sergey Poznyakoff authored
372 373 374
  if (mu_sieve_is_dry_run (mach))
    return 0;
  
375 376 377 378 379 380 381 382 383
  msg = mu_sieve_get_message (mach);
  rc = get_real_message_size (msg, &size);
  if (rc)
    {
      mu_sieve_error (mach, _("cannot get real message size: %s"),
		      mu_strerror (rc));
      mu_sieve_abort (mach);
    }
  
384
  if (!mu_sieve_get_tag (mach, "host", SVT_STRING, &host))
385 386
    host = "127.0.0.1";
  
387
  if (mu_sieve_get_tag (mach, "port", SVT_NUMBER, &num))
388
    result = spamd_connect_tcp (mach, &stream, host, num);
389
  else if (mu_sieve_get_tag (mach, "socket", SVT_STRING, &str))
390
    result = spamd_connect_socket (mach, &stream, str);
391 392 393
  else
    result = spamd_connect_tcp (mach, &stream, host, DEFAULT_SPAMD_PORT);
  if (result) /* spamd_connect_ already reported error */
394
    mu_sieve_abort (mach);
395

396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420
  mu_stream_set_buffer (stream, mu_buffer_line, 0);
  if (mu_debug_category_level ("sieve", 5, &lev) == 0 &&
      (lev & MU_DEBUG_LEVEL_MASK (MU_DEBUG_PROT)))
    {
      int rc;
      mu_stream_t dstr, xstr;
      
      rc = mu_dbgstream_create (&dstr, MU_DIAG_DEBUG);
      if (rc)
	mu_error (_("cannot create debug stream; transcript disabled: %s"),
		  mu_strerror (rc));
      else
	{
	  rc = mu_xscript_stream_create (&xstr, stream, dstr, NULL);
	  if (rc)
	    mu_error (_("cannot create transcript stream: %s"),
		      mu_strerror (rc));
	  else
	    {
	      mu_stream_unref (stream);
	      stream = xstr;
	    }
	}
    }

421
  spamd_send_command (stream, "SYMBOLS SPAMC/1.2");
422
  
423
  spamd_send_command (stream, "Content-length: %lu", (u_long) size);
424
  if (mu_sieve_get_tag (mach, "user", SVT_STRING, &str))
425
    spamd_send_command (stream, "User: %s", str);
Sergey Poznyakoff authored
426 427 428 429 430 431
  else
    {
      struct mu_auth_data *auth = mu_get_auth_by_uid (geteuid ());
      spamd_send_command (stream, "User: %s", auth ? auth->name : "root");
      mu_auth_data_free (auth);
    }
432 433 434 435 436

  got_sigpipe = 0;
  handler = set_signal_handler (SIGPIPE, sigpipe_handler);
  
  spamd_send_command (stream, "");
437
  spamd_send_message (stream, msg, 1);
Sergey Poznyakoff authored
438
  mu_stream_shutdown (stream, MU_STREAM_WRITE);
439

440
  spamd_read_line (mach, stream, &buffer, &size);
441 442 443

  if (got_sigpipe)
    {
Sergey Poznyakoff authored
444
      mu_sieve_error (mach, _("remote side has closed connection"));
445 446 447
      spamd_abort (mach, &stream, handler);
    }

448 449
  if (parse_response_line (mach, buffer))
    spamd_abort (mach, &stream, handler);
450
  
451
  spamd_read_line (mach, stream, &buffer, &size);
452 453 454
  if (sscanf (buffer, "Spam: %5s ; %20s / %20s",
	      spam_str, score_str, threshold_str) != 3)
    {
Sergey Poznyakoff authored
455 456
      mu_sieve_error (mach, _("spamd responded with bad Spam header '%s'"), 
                      buffer);
457 458 459 460 461
      spamd_abort (mach, &stream, handler);
    }

  result = decode_boolean (spam_str);
  score = strtoul (score_str, NULL, 10);
462 463
  decode_float (&score, score_str, 3, NULL);
  decode_float (&threshold, threshold_str, 3, NULL);
464 465 466

  if (!result)
    {
467
      if (mu_sieve_get_tag (mach, "over", SVT_STRING, &str))
468
	{
469
	  decode_float (&limit, str, 3, NULL);
470 471
	  result = score >= limit;
	}
472
      else if (mu_sieve_get_tag (mach, "under", SVT_STRING, &str))
473
	{
474
	  decode_float (&limit, str, 3, NULL);
475 476 477 478 479
	  result = score <= limit;	  
	}
    }
  
  /* Skip newline */
480
  spamd_read_line (mach, stream, &buffer, &size);
481
  /* Read symbol list */
482
  spamd_read_line (mach, stream, &buffer, &size);
483

484
  rc = mu_message_get_header (msg, &hdr);
485 486
  if (rc)
    {
Sergey Poznyakoff authored
487 488
      mu_sieve_error (mach, _("cannot get message header: %s"), 
                      mu_strerror (rc));
489 490 491
      spamd_abort (mach, &stream, handler);
    }

492 493 494 495
  mu_header_append (hdr, "X-Spamd-Status", spam_str);
  mu_header_append (hdr, "X-Spamd-Score", score_str);
  mu_header_append (hdr, "X-Spamd-Threshold", threshold_str);
  mu_header_append (hdr, "X-Spamd-Keywords", buffer);
496

497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512
  free (buffer);

  /* Create a data sink */
  mu_nullstream_create (&null, MU_STREAM_WRITE);

  /* Mark out the following data as payload */
  if (!(lev & MU_DEBUG_LEVEL_MASK (MU_DEBUG_TRACE9)))
    {
      int xlev = MU_XSCRIPT_PAYLOAD;
      mu_stream_ioctl (stream, MU_IOCTL_XSCRIPTSTREAM,
		       MU_IOCTL_XSCRIPTSTREAM_LEVEL, &xlev);
    }
  mu_stream_copy (null, stream, 0, NULL);
  mu_stream_destroy (&null);
  mu_stream_destroy (&stream);

513 514 515 516 517 518 519 520 521
  set_signal_handler (SIGPIPE, handler);

  return result;
}


/* Initialization */
   
/* Required arguments: */
522
static mu_sieve_data_type spamd_req_args[] = {
523 524 525 526
  SVT_VOID
};

/* Tagged arguments: */
527
static mu_sieve_tag_def_t spamd_tags[] = {
528 529 530
  { "host", SVT_STRING },
  { "port", SVT_NUMBER },
  { "socket", SVT_STRING },
Sergey Poznyakoff authored
531
  { "user", SVT_STRING },
532 533 534 535 536
  { "over", SVT_STRING },
  { "under", SVT_STRING },
  { NULL }
};

537
static mu_sieve_tag_group_t spamd_tag_groups[] = {
538 539 540 541 542 543 544
  { spamd_tags, NULL },
  { NULL }
};


/* Initialization function. */
int
545
SIEVE_EXPORT(spamd,init) (mu_sieve_machine_t mach)
546
{
547 548 549
  mu_sieve_register_test (mach, "spamd", spamd_test,
			  spamd_req_args, spamd_tag_groups, 1);
  return 0;
550 551
}