View | Details | Raw Unified | Return to bug 5555
Collapse All | Expand All

(-)lib/Mail/SpamAssassin/Plugin/VBounce.pm (-2 / +653 lines)
Lines 29-34 Link Here
29
29
30
use Mail::SpamAssassin::Plugin;
30
use Mail::SpamAssassin::Plugin;
31
use Mail::SpamAssassin::Logger;
31
use Mail::SpamAssassin::Logger;
32
32
use strict;
33
use strict;
33
use warnings;
34
use warnings;
34
35
Lines 44-52 Link Here
44
45
45
  $self->register_eval_rule("have_any_bounce_relays");
46
  $self->register_eval_rule("have_any_bounce_relays");
46
  $self->register_eval_rule("check_whitelist_bounce_relays");
47
  $self->register_eval_rule("check_whitelist_bounce_relays");
48
  $self->register_eval_rule("check_bounced_message_legitimacy");
49
  $self->register_eval_rule("check_bounced_message_illegitimacy");
50
  $self->register_eval_rule("check_bounced_message_has_valid_dkim_signature");
51
  $self->register_eval_rule("check_bounced_message_has_our_dkim_signature");
52
  $self->register_eval_rule("check_bounced_message_has_no_dkim_signature");
53
  $self->register_eval_rule("check_bounced_message_has_valid_x_header");
47
54
48
  $self->set_config($mailsaobject->{conf});
55
  $self->set_config($mailsaobject->{conf});
49
56
57
  $self->{'dkim_module_loaded'} = 0;
58
50
  return $self;
59
  return $self;
51
}
60
}
52
61
Lines 85-90 Link Here
85
      type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST
94
      type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST
86
    });
95
    });
87
96
97
=item use_dkim_b3d (0|1)	(default: 0)
98
99
Whether to use DKIM DNS entries for backscatter detection.
100
101
This method is intended for sites that use DKIM signatures in their outgoing
102
emails. If a legitimate, and thus DKIM-signed message causes a bounce, the
103
DKIM signature should be found in the copy of the original message enclosed in
104
the bounce.
105
106
If enabled, bounce messages will be scanned for a copy of the orginial message
107
that caused the bounce. If found, the original message is extracted, and
108
checked for a DKIM signature. The original message's headers will then be
109
verified using the signature.
110
111
The outcome of the test (signature found? signature valid?) allows conclusions
112
regarding the validity of the bounce message. Note that the result for
113
non-bounce-messages is unspecified.
114
115
This method may also be used in combination with an alternate method based
116
on special headers that are included in outgoing emails
117
(see C<use_xheader_b3d>).
118
119
Using this method requires C<Mail::DKIM> perl module to be installed.
120
121
=cut
122
  push(@cmds, {
123
      setting => 'use_dkim_b3d',
124
      default => 0,
125
      type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
126
    });
127
128
=item b3d_dkim_domain domain	(default: *)
129
130
Domains to check DKIM signature against. Only DKIM signatures made under
131
one of these domains are considered legitimate.
132
133
Multiple domains may be given seperated by spaces, and multiple
134
C<b3d_dkim_domain> entries may be used. C<*> is accepted as wildcard.
135
136
Only used if the DKIM-method is activated (see C<use_dkim_b3d>).
137
138
=cut
139
  push(@cmds, {
140
      setting => 'b3d_dkim_domain',
141
      type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST,
142
    });
143
144
=item use_xheader_b3d (0|1)	(default: 0)
145
146
Whether to use a special header for b3d backscatter detection.
147
148
This method relies on a special header to be inserted into all of a site's
149
outgoing mails. If one of those emails is bounced, the special header should
150
be found in the bounce.
151
152
If enabled, incoming bounces will be scanned for an enclosed copy of the
153
original email. This copy will then be checked for the existence of the
154
special header, and whether the header contains the correct value.
155
156
The outcome of the test (header found? header valid?) allows conclusions
157
regarding the validity of the bounce message. Note that the result for
158
non-bounce-messages is unspecified.
159
160
This method may also be used in combination with an alternate method based
161
on special headers that are included in outgoing emails
162
(see C<use_dkim_b3d>).
163
164
=cut
165
  push(@cmds, {
166
      setting => 'use_xheader_b3d',
167
      default => 0,
168
      type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
169
    });
170
171
=item b3d_x_header header-name	(default: X-B3D-Key)
172
173
Name of the header containing the secret key used for validating bounce-mails
174
when using the header method.
175
176
Only used if the special-header-method is activated (see C<use_xheader_b3d>).
177
178
=cut
179
  push(@cmds, {
180
      setting => 'b3d_x_header',
181
      default => 'X-B3B-Key',
182
      type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
183
    });
184
185
=item b3d_x_header_value secret-key
186
187
Secret key to use for validation of DSN-mails. Multiple keys may be given,
188
separated by spaces.
189
190
The keys given under C<b3d_x_header_value> are considered valid keys for all
191
incoming bounces.
192
193
Only used if the special-header-method is activated (see C<use_xheader_b3d>).
194
195
=cut
196
  push(@cmds, {
197
      setting => 'b3d_x_header_value',
198
      type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
199
    });
200
201
=item b3d_x_header_domain_value domain secret-key
202
203
Secret key to use for validation of bounces that are sent to recipients in a
204
given domain.
205
206
The specified domain-value is matched against the end of the recipient email of the DSN mail.
207
Multiple domain/secret-key pairs may be specified in multiple configuration lines.
208
209
If a secret key is given for a domain, mails sent to this domain will be tested for this key
210
and for all the keys given in C<b3d_x_header_value>.
211
Only used if the special-header-method is activated (see C<use_xheader_b3d>).
212
213
=cut
214
  push(@cmds, {
215
      setting => 'b3d_x_header_domain_value',
216
      type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE,
217
    });
218
88
  $conf->{parser}->register_commands(\@cmds);
219
  $conf->{parser}->register_commands(\@cmds);
89
}
220
}
90
221
Lines 141-147 Link Here
141
  my ($self, $pms, $relay) = @_;
272
  my ($self, $pms, $relay) = @_;
142
  return 1 if $self->_relay_is_in_list(
273
  return 1 if $self->_relay_is_in_list(
143
        $pms->{conf}->{whitelist_bounce_relays}, $pms, $relay);
274
        $pms->{conf}->{whitelist_bounce_relays}, $pms, $relay);
144
  dbg("rules: relay $relay doesn't match any whitelist");
275
  dbg("vbounce: [rules] relay $relay doesn't match any whitelist");
145
}
276
}
146
277
147
sub _relay_is_in_list {
278
sub _relay_is_in_list {
Lines 152-158 Link Here
152
283
153
  foreach my $regexp (values %{$list}) {
284
  foreach my $regexp (values %{$list}) {
154
    if ($relay =~ qr/$regexp/i) {
285
    if ($relay =~ qr/$regexp/i) {
155
      dbg("rules: relay $relay matches regexp: $regexp");
286
      dbg("vbounce: [rules] relay $relay matches regexp: $regexp");
156
      return 1;
287
      return 1;
157
    }
288
    }
158
  }
289
  }
Lines 160-165 Link Here
160
  return 0;
291
  return 0;
161
}
292
}
162
293
294
   #############################
295
   #                           #
296
   # Anti-BackScatter-Code B3D #
297
   #                           #
298
   #############################
299
300
# DKIM test return codes, see _check_bounced_message_legitimacy_by_dkim for details.
301
my $DKIM_RESULT_VALID = 4;
302
my $DKIM_RESULT_ALTERED = 3;
303
my $DKIM_RESULT_INVALID = 2;
304
my $DKIM_RESULT_OTHER = 1;
305
my $DKIM_RESULT_NONE = 0;
306
my $DKIM_RESULT_UNKNOWN = -1;
307
my $DKIM_RESULT_NOCONF = -2;
308
309
# X-Header test return codes, see _check_bounced_message_legitimacy_by_xheader for details.
310
my $XHEADER_RESULT_SUCCESS = 1;
311
my $XHEADER_RESULT_FAILURE = 0;
312
my $XHEADER_RESULT_UNKNOWN = -1;
313
my $XHEADER_RESULT_NOCONF = -2;
314
315
# TODO: Should the message-id check pushed up to the rules-level?
316
#       I.e. only check for DKIM/Secret-header here, and do the interpretation through rules?
317
318
# Check whether a bounced message contains a legitimate original message,
319
# i.e. one containing either a valid DKIM signature or a predefined header.
320
#
321
# The function returns 1 if bounce legitimacy checking is enabled and
322
# if the bounced message is legitimate with high certainty, 0 otherwise.
323
sub check_bounced_message_legitimacy {
324
  my ($self, $pms) = @_;
325
326
  unless ($pms->{conf}->{use_dkim_b3d} || $pms->{conf}->{use_xheader_b3d}) {
327
    dbg("vbounce: [check_bounced_message_legitimacy] Skipping B3D checks, not activated");
328
    return 0;
329
  }
330
331
  dbg("vbounce: [check_bounced_message_legitimacy] Started X");
332
333
  if ($pms->{conf}->{use_dkim_b3d}) {
334
    $self->_perform_bounced_message_legitimacy_dkim_tests($pms);
335
    dbg("vbounce: [check_bounced_message_legitimacy] DKIM test result is ".$pms->{b3d_dkim_checks_result});
336
337
    if ($pms->{b3d_dkim_checks_result} == $DKIM_RESULT_VALID
338
        || $pms->{b3d_dkim_checks_result} == $DKIM_RESULT_ALTERED
339
        || $pms->{b3d_dkim_checks_result} == $DKIM_RESULT_INVALID) {
340
      # Success if a DKIM signature is found and if it is completely valid, valid for the headers only,
341
      # or at least contains our domain as signing domain.
342
      dbg("vbounce: [check_bounced_message_legitimacy] DKIM test succeeded, returning 1");
343
      return 1;
344
    }
345
  } else {
346
    dbg("vbounce: [check_bounced_message_legitimacy] Skipping DKIM check, not activated");
347
  }
348
349
  if ($pms->{conf}->{use_xheader_b3d}) {
350
    $self->_perform_bounced_message_legitimacy_xheader_tests($pms);
351
    dbg("vbounce: [check_bounced_message_legitimacy] X-Header test result is ".$pms->{b3d_xheader_checks_result}.", returning this value");
352
    return ($pms->{b3d_xheader_checks_result} == $XHEADER_RESULT_SUCCESS ? 1 : 0);
353
  } else {
354
    dbg("vbounce: [check_bounced_message_legitimacy] Skipping X-Header check, not activated");
355
  }
356
357
  dbg("vbounce: [check_bounced_message_legitimacy] Returning 0");
358
  return 0;
359
}
360
361
# Check whether a bounced message does not contain a legitimate original message,
362
# i.e. one containing either a valid DKIM signature or a predefined header.
363
#
364
# The function returns 1 if bounce legitimacy checking is enabled and
365
# the bounced message is illegitimate with high certainty, 0 otherwise.
366
sub check_bounced_message_illegitimacy {
367
  my ($self, $pms) = @_;
368
369
  unless ($pms->{conf}->{use_dkim_b3d} || $pms->{conf}->{use_xheader_b3d}) {
370
    dbg("vbounce: [check_bounced_message_illegitimacy] Skipping B3D checks, not activated");
371
    return 0;
372
  }
373
374
  dbg("vbounce: [check_bounced_message_illegitimacy] Started");
375
376
  my $res = 0;
377
378
  if ($pms->{conf}->{use_dkim_b3d}) {
379
    $self->_perform_bounced_message_legitimacy_dkim_tests($pms);
380
    dbg("vbounce: [check_bounced_message_illegitimacy] DKIM test result is ".$pms->{b3d_dkim_checks_result});
381
382
    if ($pms->{b3d_dkim_checks_result} == $DKIM_RESULT_VALID
383
        || $pms->{b3d_dkim_checks_result} == $DKIM_RESULT_ALTERED
384
        || $pms->{b3d_dkim_checks_result} == $DKIM_RESULT_INVALID) {
385
      # Success if a DKIM signature is found and if it is completely valid, valid for the headers only,
386
      # or at least contains our domain as signing domain.
387
      dbg("vbounce: [check_bounced_message_illegitimacy] DKIM test succeeded, thus no illegitimacy, returning 0");
388
      return 0;
389
    } elsif ($pms->{b3d_dkim_checks_result} == $DKIM_RESULT_UNKNOWN) {
390
      # Maybe the reporting MTA did not include the headers, etc.
391
      $res = 0;
392
    } else {
393
      # No DKIM signature was found even though original headers were included.
394
      $res = 1;	
395
    }
396
  } else {
397
    dbg("vbounce: [check_bounced_message_illegitimacy] Skipping DKIM check, not activated");
398
  }
399
400
  if ($pms->{conf}->{use_xheader_b3d}) {
401
    $self->_perform_bounced_message_legitimacy_xheader_tests($pms);
402
    dbg("vbounce: [check_bounced_message_illegitimacy] X-Header test result is ".$pms->{b3d_xheader_checks_result}.", returning this value");
403
    return ($pms->{b3d_xheader_checks_result} == $XHEADER_RESULT_FAILURE ? 1 : 0);
404
  } else {
405
    dbg("vbounce: [check_bounced_message_illegitimacy] Skipping X-Header check, not activated");
406
  }
407
408
  dbg("vbounce: [check_bounced_message_illegitimacy] Returning $res");
409
  return $res;
410
}
411
412
# Check whether a bounced message contains a valid DKIM signature.
413
#
414
# The function returns 1 if bounce legitimacy checking through DKIM is enabled and
415
# the bounced message contains a valid DKIM signature for its headers, 0 otherwise.
416
sub check_bounced_message_has_valid_dkim_signature {
417
  my ($self, $pms) = @_;
418
419
  unless ($pms->{conf}->{use_dkim_b3d}) {
420
    dbg("vbounce: [check_bounced_message_has_valid_dkim_signature] Skipping check, not activated");
421
    return 0;
422
  }
423
424
  dbg("vbounce: [check_bounced_message_has_valid_dkim_signature] Started");
425
426
  $self->_perform_bounced_message_legitimacy_dkim_tests($pms);
427
428
  # Success if a DKIM signature is found and if it is completely valid or valid for the headers only
429
  my $res = ($pms->{b3d_dkim_checks_result} == $DKIM_RESULT_VALID
430
    || $pms->{b3d_dkim_checks_result} == $DKIM_RESULT_ALTERED
431
    ? 1 : 0);
432
433
  dbg("vbounce: [check_bounced_message_has_valid_dkim_signature] Returning $res");
434
  return $res;
435
}
436
437
# Check whether a bounced message contains a DKIM signature made using our domain.
438
#
439
# The function returns 1 if bounce legitimacy checking through DKIM is enabled and
440
# the bounced message contains our DKIM signature, 0 otherwise.
441
sub check_bounced_message_has_our_dkim_signature {
442
  my ($self, $pms) = @_;
443
444
  unless ($pms->{conf}->{use_dkim_b3d}) {
445
    dbg("vbounce: [check_bounced_message_has_our_dkim_signature] Skipping B3D checks, not activated");
446
    return 0;
447
  }
448
449
  dbg("vbounce: [check_bounced_message_has_our_dkim_signature] Started");
450
451
  $self->_perform_bounced_message_legitimacy_dkim_tests($pms);
452
453
  # Success if a DKIM signature is found and if it is completely valid, valid for the headers only,
454
  # or at least contains our domain as signing domain.
455
  my $res = ($pms->{b3d_dkim_checks_result} == $DKIM_RESULT_VALID
456
    || $pms->{b3d_dkim_checks_result} == $DKIM_RESULT_ALTERED
457
    || $pms->{b3d_dkim_checks_result} == $DKIM_RESULT_INVALID
458
    ? 1 : 0);
459
460
  dbg("vbounce: [check_bounced_message_has_our_dkim_signature] Returning $res");
461
  return $res;
462
}
463
464
# Check whether no DKIM signature made using our domain can be found in a bounced message.
465
#
466
# The function returns 1 if bounce legitimacy checking through DKIM is enabled and
467
# the bounced message seems not to contain a DKIM signature, 0 otherwise.
468
sub check_bounced_message_has_no_dkim_signature {
469
  my ($self, $pms) = @_;
470
471
  unless ($pms->{conf}->{use_dkim_b3d}) {
472
    dbg("vbounce: [check_bounced_message_has_no_dkim_signature] Skipping B3D checks, not activated");
473
    return 0;
474
  }
475
476
  dbg("vbounce: [check_bounced_message_has_no_dkim_signature] Started");
477
478
  $self->_perform_bounced_message_legitimacy_dkim_tests($pms);
479
480
  # Success if no DKIM signature was found
481
  my $res = ($pms->{b3d_dkim_checks_result} == $DKIM_RESULT_NONE
482
    || $pms->{b3d_dkim_checks_result} == $DKIM_RESULT_UNKNOWN
483
    ? 1 : 0);
484
485
  dbg("vbounce: [check_bounced_message_has_no_dkim_signature] Returning $res");
486
  return $res;
487
}
488
489
# Check whether a bounced message contains a special header/value combination.
490
#
491
# The function returns 1 if header based bounce legitimacy checking is enabled and
492
# the bounced message contains the header with the specified value, 0 otherwise.
493
sub check_bounced_message_has_valid_x_header {
494
  my ($self, $pms) = @_;
495
496
  unless ($pms->{conf}->{use_xheader_b3d}) {
497
    dbg("vbounce: [check_bounced_message_has_valid_x_header] Skipping B3D checks, not activated");
498
    return 0;
499
  }
500
501
  dbg("vbounce: [check_bounced_message_has_valid_x_header] Started");
502
503
  $self->_perform_bounced_message_legitimacy_xheader_tests($pms);
504
505
  my $res = ($pms->{b3d_xheader_checks_result} ? 1 : 0);
506
507
  dbg("vbounce: [check_bounced_message_has_valid_x_header] Returning $res");
508
  return $res;
509
}
510
511
sub _perform_bounced_message_legitimate_tests {
512
  my ($self, $pms) = @_;
513
514
  $self->_perform_bounced_message_legitimacy_dkim_tests($pms);
515
  $self->_perform_bounced_message_legitimacy_xheader_tests($pms);
516
}
517
518
sub _perform_bounced_message_legitimacy_dkim_tests {
519
  my ($self, $pms) = @_;
520
521
  if ($pms->{conf}->{use_dkim_b3d} && ! $self->{'dkim_module_loaded'}) {
522
    if (eval { require Mail::DKIM::Verifier; }) {
523
      $self->{'dkim_module_loaded'} = 1;
524
    } else {
525
      $pms->{conf}->{use_dkim_b3d} = 0;
526
      dbg("vbounce: DKIM module not found, DKIM check is disabled!");
527
    }
528
  }
529
530
  if ($pms->{conf}->{use_dkim_b3d}) {
531
    if (! $pms->{b3d_dkim_checks_run}) {
532
      $pms->{b3d_dkim_checks_run} = 1;
533
      $pms->{b3d_dkim_checks_result} = $self->_check_bounced_message_legitimacy_by_dkim($pms);
534
      dbg("vbounce: [_perform_bounced_message_legitimacy_dkim_tests] DKIM test performed, resulted in ".$pms->{b3d_dkim_checks_result});
535
    } else {
536
      dbg("vbounce: [_perform_bounced_message_legitimacy_dkim_tests] DKIM test already performed, skipping");
537
    }
538
  } else {
539
    dbg("vbounce: [_perform_bounced_message_legitimacy_dkim_tests] DKIM test not enabled, skipping");
540
  }
541
}
542
543
sub _perform_bounced_message_legitimacy_xheader_tests {
544
  my ($self, $pms) = @_;
545
546
  if ($pms->{conf}->{use_xheader_b3d}) {
547
    if (! $pms->{b3d_xheader_checks_run}) {
548
      $pms->{b3d_xheader_checks_run} = 1;
549
      $pms->{b3d_xheader_checks_result} = $self->_check_bounced_message_legitimacy_by_xheader($pms);
550
      dbg("vbounce: [_perform_bounced_message_legitimacy_xheader_tests] X-header test performed, resulted in ".$pms->{b3d_xheader_checks_result});
551
    } else {
552
      dbg("vbounce: [_perform_bounced_message_legitimacy_xheader_tests] X-header test already performed, skipping");
553
    }
554
  } else {
555
    dbg("vbounce: [_perform_bounced_message_legitimacy_xheader_tests] X-header test not enabled, skipping");
556
  }
557
}
558
559
# Check a message's legitimacy by reconstruction the original message
560
# and checking it for a valid DKIM signature.
561
#
562
# Returns  $DKIM_RESULT_VALID   if a valid DKIM signature is found,
563
#          $DKIM_RESULT_ALTERED if a valid DKIM signature is found for the headers,
564
#          $DKIM_RESULT_INVALID if an invalid DKIM signature is found that matches the sender's domain,
565
#          $DKIM_RESULT_OTHER   if any is found that does not match the sender's domain,
566
#          $DKIM_RESULT_NONE    if no DKIM signature is found, but a message-id header is found,
567
#          $DKIM_RESULT_UNKNOWN otherwise.
568
sub _check_bounced_message_legitimacy_by_dkim {
569
  my ($self, $pms) = @_;
570
571
### Allow no config value as *-pattern ###
572
#  unless ($pms->{conf}->{b3d_dkim_domain}) {
573
#    warn("vbounce: Missing 'b3d_dkim_domain' configuration parameter, aborting DKIM test");
574
#    return $DKIM_RESULT_UNKNOWN;
575
#  }
576
577
  my $res = $DKIM_RESULT_UNKNOWN;
578
579
  my $dkim = Mail::DKIM::Verifier->new_object();
580
  my $original = $self->_extract_original_message($pms->get_message());
581
582
  # If we cannot get the original message, return "undecided".
583
  unless ($original) {
584
    dbg("vbounce: [_check_bounced_message_legitimacy_by_dkim] Could not get original message, returning -1");
585
    return $DKIM_RESULT_UNKNOWN;
586
  }
587
588
  my $m = 0;
589
  my $h = 0;
590
591
  foreach my $line (split('(?:\015|\012|\015\012)', $original)) {
592
    if ($line =~ qr/DKIM-Signature:/i) {
593
      dbg("vbounce: [_check_bounced_message_legitimacy_by_dkim] Found DKIM signature");
594
      $h = 1;
595
      last;
596
    }
597
    if ($line =~ qr/Message-Id:/i) {
598
      dbg("vbounce: [_check_bounced_message_legitimacy_by_dkim] Found Message-ID header");
599
      $m = 1;
600
    }
601
  }
602
603
  # Consider bounce illegitimate if the original message contains no DKIM-signature, but a message-id header.
604
  if ($m && ! $h) {
605
    dbg("vbounce: [_check_bounced_message_legitimacy_by_dkim] Message-ID but no DKIM signature, returning 0");
606
    return $DKIM_RESULT_NONE;
607
  }
608
609
  # Consider bounce undecidable if the original message contains no DKIM-signature and no message-id header.
610
  if (! $h) {
611
    dbg("vbounce: [_check_bounced_message_legitimacy_by_dkim] No DKIM signature and no Message-ID, returning -1");
612
    return $DKIM_RESULT_UNKNOWN;
613
  }
614
615
  # Reformat the message to meet the DKIM module's expectancies,
616
  # and pass the message to the module line by line.
617
  foreach (split('(?:\015|\012|\015\012)', $original)) {
618
#    chomp;
619
    $dkim->PRINT("$_\015\012");
620
  }
621
622
  $dkim->CLOSE();
623
624
  # Check the signature.
625
  my $sigmatch = 1;
626
  if ($dkim->result() ne 'none') { #$dkim->signature()
627
    dbg("vbounce: [_check_bounced_message_legitimacy_by_dkim] Found ".($#{$dkim->{signatures}}+1)." DKIM signature(s)");
628
    my $signature = ${$dkim->{signatures}}[0];
629
    $sigmatch = 0;
630
631
    if (scalar keys %{$pms->{conf}->{b3d_dkim_domain}} > 0) {
632
      foreach my $dom (values %{$pms->{conf}->{b3d_dkim_domain}}) {
633
        my $domain = $dom.'$';
634
        
635
        if ($signature->domain() =~ qr/$domain/) {
636
          $sigmatch = 1; last;
637
        }
638
      }
639
    } else {
640
      dbg("vbounce: [_check_bounced_message_legitimacy_by_dkim] No domains specified, accepting any signature");
641
    }
642
643
    if ($sigmatch) { #$dkim->signature
644
      if ($dkim->result() eq 'pass') {
645
        dbg("vbounce: [_check_bounced_message_legitimacy_by_dkim] DKIM signature is valid");
646
        $res = $DKIM_RESULT_VALID;
647
      } elsif ($dkim->result() eq 'fail' && $dkim->result_detail() =~ /body has been altered/i) {
648
        dbg("vbounce: [_check_bounced_message_legitimacy_by_dkim] DKIM signature is valid for headers");
649
        $res = $DKIM_RESULT_ALTERED;
650
      } else {
651
        dbg("vbounce: [_check_bounced_message_legitimacy_by_dkim] DKIM signature is not valid (".$dkim->result_detail().")");
652
        $res = $DKIM_RESULT_INVALID;
653
      }
654
    } else {
655
      dbg("vbounce: [_check_bounced_message_legitimacy_by_dkim] DKIM domain (".$signature->domain().
656
          ") does not match any of the user domains (".join(', ', values %{$pms->{conf}->{b3d_dkim_domain}}).")");
657
      $res = $DKIM_RESULT_OTHER;
658
    }
659
  } else {
660
    dbg("vbounce: [_check_bounced_message_legitimacy_by_dkim] No DKIM signature found");
661
    $res = $DKIM_RESULT_NONE;
662
  }
663
664
  dbg("vbounce: [_check_bounced_message_legitimacy_by_dkim] Result of DKIM check is $res");
665
  return $res;
666
}
667
668
# Extract the original message (or just it headers) from the bounce, if possible.
669
# Returns an empty string if the original message cannot be extracted.
670
sub _extract_original_message {
671
  my ($self, $msg) = @_;
672
  
673
  my $ctype = $msg->get_header('Content-Type');
674
  my $body = '';
675
676
  # If we have a multipart message, return the first rfc822-headers part
677
  if ($ctype && $ctype =~ /^multipart\//i) {
678
    dbg("vbounce: [_extract_original_message] Multipart message");
679
    my @parts = $msg->find_parts('message/rfc822');
680
    @parts = $msg->find_parts('text/rfc822-headers') if $#parts == -1;
681
682
    if ($#parts >= 0) {
683
      my $part = $parts[0];
684
      dbg("vbounce: [_extract_original_message] Found ".($#parts + 1)." text/rfc822-headers part(s)");
685
#      dbg("vbounce: [_extract_original_message] Body parts: ".(join(", ", keys %{${$part->{body_parts}}[0]}))."\n");
686
      $body = ${$part->{body_parts}}[0]->{pristine_headers};
687
    }
688
  }
689
690
  # Otherwise, return everything that looks like a header
691
  if (! $body) {
692
    $body = $msg->get_body();
693
    my @b = ();
694
    my $h = 0;
695
696
    for (my $i = 0; $i <= $#{$body}; $i++) {
697
      if ($body->[$i] =~ /^([\w-]+):\s+/) {
698
        push(@b, $body->[$i]);
699
        $h = 1;
700
      } elsif ($h && $body->[$i] =~ /^\s+\S/) {
701
        push(@b, $body->[$i]);
702
      } else {
703
        $h = 0;
704
      }
705
    }
706
707
    $body = join("\n", @b);
708
    dbg("vbounce: [_extract_original_message] Extracted $#b header lines from bounce body");
709
  }
710
711
  dbg("vbounce: [_extract_original_message] Returned body is not empty: ".($body ne ''));
712
  return $body;
713
}
714
715
# This function's code is mainly copied from check_whitelist_bounce_relays.
716
#
717
# The function checks whether the original message's headers contain a specific legitimacy-header
718
# (see b3d_uses_x_header, b3d_x_header_value and b3d_x_header).
719
# It returns $XHEADER_RESULT_SUCCESS if the legitimacy-header is found,
720
#            $XHEADER_RESULT_FAILURE if a message-id header is found, but the legitimacy-header is not found,
721
#            $XHEADER_RESULT_UNKNOWN otherwise.
722
sub _check_bounced_message_legitimacy_by_xheader {
723
  my ($self, $pms) = @_;
724
725
  unless ($pms->{conf}->{b3d_x_header}) {
726
    warn("vbounce: Missing 'b3d_x_header' configuration parameter, aborting header test");
727
    return $XHEADER_RESULT_UNKNOWN;
728
  }
729
730
  unless ($pms->{conf}->{b3d_x_header_value} || scalar keys %{$pms->{conf}->{b3d_x_header_domain_value}} >= 0) {
731
    warn("vbounce: Missing 'b3d_x_header_value' and 'b3d_x_header_domain_value' configuration parameter, aborting header test");
732
    return $XHEADER_RESULT_UNKNOWN;
733
  }
734
735
  my $body = $pms->get_decoded_stripped_body_text_array();
736
  my $recipient = $pms->get('ToCc:addr');
737
  my @keys = ();
738
739
  foreach my $dom (keys %{$pms->{conf}->{b3d_x_header_domain_value}}) {
740
    my $domptrn = $dom.'$';
741
    next unless $recipient =~ qr/$domptrn/;
742
    push(@keys, $pms->{conf}->{b3d_x_header}.':\s+'.$pms->{conf}->{b3d_x_header_domain_value}->{$dom});
743
  }
744
745
  push(@keys, map { $pms->{conf}->{b3d_x_header}.':\s+'.$_ } split('\s+', $pms->{conf}->{b3d_x_header_value}));
746
  dbg("vbounce: [_check_bounced_message_legitimacy_by_xheader] Testing for one of the following keys: ".join(', ', @keys));
747
748
  my $m = 0;
749
  my $line = '';
750
  my $key = '';
751
752
  # Check the plain-text body for our header, first.
753
  foreach $line (@{$body}) {
754
    # It's a good bounce if it has our header.
755
    foreach $key (@keys) {
756
      if ($line =~ qr/$key/i) {
757
        dbg("vbounce: [_check_bounced_message_legitimacy_by_xheader] Found pattern '$key', returning $XHEADER_RESULT_SUCCESS");
758
        return $XHEADER_RESULT_SUCCESS;
759
      }
760
    }
761
762
    if ($line =~ qr/Message-Id:/i) {
763
      dbg("vbounce: [_check_bounced_message_legitimacy_by_xheader] Found Message-ID during first scan");
764
      $m = 1;
765
    }
766
  }
767
768
  # If we found a message id header, but not the legitimacy header, consider the bounce illegitimate.
769
  if ($m) {
770
    dbg("vbounce: [_check_bounced_message_legitimacy_by_xheader] Found Message-ID but not special header, returning $XHEADER_RESULT_FAILURE");
771
    return $XHEADER_RESULT_FAILURE;
772
  }
773
774
  # now check any "message/anything" attachment MIME parts, too.
775
  # don't use the more efficient find_parts() method until bug 5331 is
776
  # fixed, otherwise we'll miss some messages due to their MIME structure
777
  
778
  my $pristine = $pms->{msg}->get_pristine();
779
  # skip past the headers
780
  my $foundnlnl = 0;
781
  foreach $line ($pristine =~ /^(.*)$/gm) {
782
    if ($line =~ /^$/) {
783
      $foundnlnl = 1;
784
    }
785
786
    # and now through the pristine body
787
    if ($foundnlnl) {
788
      # It's a good bounce if it has our header.<>
789
      foreach $key (@keys) {
790
        if ($line =~ qr/$key/i) {
791
          dbg("vbounce: [_check_bounced_message_legitimacy_by_xheader] Found pattern '$key', returning $XHEADER_RESULT_SUCCESS");
792
          return $XHEADER_RESULT_SUCCESS;
793
        }
794
      }
795
796
      if ($line =~ qr/^.{0,3}Message-Id:/i) {
797
        dbg("vbounce: [_check_bounced_message_legitimacy_by_xheader] Found Message-ID during second scan");
798
        $m = 1;
799
      }
800
    }
801
  }
802
803
  # If we can't find the end of the headers, return "undecided".
804
  unless ($foundnlnl) {
805
    dbg("vbounce: [_check_bounced_message_legitimacy_by_xheader] Could not find original message body, returning $XHEADER_RESULT_UNKNOWN");
806
    return $XHEADER_RESULT_UNKNOWN;
807
  }
808
809
  # If we found a message id header, but not our header, consider the bounce illegitimate.
810
  dbg("vbounce: [_check_bounced_message_legitimacy_by_xheader] Returning ".($m ? $XHEADER_RESULT_FAILURE : $XHEADER_RESULT_UNKNOWN));
811
  return ($m ? $XHEADER_RESULT_FAILURE : $XHEADER_RESULT_UNKNOWN);
812
}
813
163
1;
814
1;
164
__DATA__
815
__DATA__
165
816

Return to bug 5555