home *** CD-ROM | disk | FTP | other *** search
/ ftp.f-secure.com / 2014.06.ftp.f-secure.com.tar / ftp.f-secure.com / support / hotfix / fsis / IS-SpamControl.fsfix / iufssc / lib / Mail / SpamAssassin / Util.pm < prev   
Text File  |  2006-11-29  |  43KB  |  1,421 lines

  1. # <@LICENSE>
  2. # Licensed to the Apache Software Foundation (ASF) under one or more
  3. # contributor license agreements.  See the NOTICE file distributed with
  4. # this work for additional information regarding copyright ownership.
  5. # The ASF licenses this file to you under the Apache License, Version 2.0
  6. # (the "License"); you may not use this file except in compliance with
  7. # the License.  You may obtain a copy of the License at:
  8. #     http://www.apache.org/licenses/LICENSE-2.0
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. # </@LICENSE>
  15.  
  16. =head1 NAME
  17.  
  18. Mail::SpamAssassin::Util - utility functions
  19.  
  20. =head1 DESCRIPTION
  21.  
  22. A general class for utility functions.  Please use this for functions that
  23. stand alone, without requiring a $self object, Portability functions
  24. especially.
  25.  
  26. NOTE: The functions in this module are to be considered private.  Their API may
  27. change at any point, and it's expected that they'll only be used by other
  28. Mail::SpamAssassin modules. (TODO: we should probably revisit this if
  29. it's useful for plugin development.)
  30.  
  31. =over 4
  32.  
  33. =cut
  34.  
  35. package Mail::SpamAssassin::Util;
  36.  
  37. use strict;
  38. use warnings;
  39. use bytes;
  40.  
  41. use Mail::SpamAssassin::Logger;
  42.  
  43. use vars qw (
  44.   @ISA @EXPORT
  45.   $AM_TAINTED
  46. );
  47.  
  48. require Exporter;
  49.  
  50. @ISA = qw(Exporter);
  51. @EXPORT = qw(local_tz base64_decode);
  52.  
  53. use Mail::SpamAssassin;
  54. use Mail::SpamAssassin::Util::RegistrarBoundaries;
  55.  
  56. use Config;
  57. use File::Spec;
  58. use Time::Local;
  59. use Sys::Hostname (); # don't import hostname() into this namespace!
  60. use Fcntl;
  61. use POSIX (); # don't import anything unless we ask explicitly!
  62. use Text::Wrap ();
  63. use Errno qw(EEXIST);
  64.  
  65. ###########################################################################
  66.  
  67. use constant HAS_MIME_BASE64 => eval { require MIME::Base64; };
  68. use constant RUNNING_ON_WINDOWS => ($^O =~ /^(?:mswin|dos|os2)/oi);
  69.  
  70. ###########################################################################
  71.  
  72. # find an executable in the current $PATH (or whatever for that platform)
  73. {
  74.   # Show the PATH we're going to explore only once.
  75.   my $displayed_path = 0;
  76.  
  77.   sub find_executable_in_env_path {
  78.     my ($filename) = @_;
  79.  
  80.     clean_path_in_taint_mode();
  81.     if ( !$displayed_path++ ) {
  82.       dbg("util: current PATH is: ".join($Config{'path_sep'},File::Spec->path()));
  83.     }
  84.     foreach my $path (File::Spec->path()) {
  85.       my $fname = File::Spec->catfile ($path, $filename);
  86.       if ( -f $fname ) {
  87.         if (-x $fname) {
  88.           dbg("util: executable for $filename was found at $fname");
  89.           return $fname;
  90.         }
  91.         else {
  92.           dbg("util: $filename was found at $fname, but isn't executable");
  93.         }
  94.       }
  95.     }
  96.     return undef;
  97.   }
  98. }
  99.  
  100. ###########################################################################
  101.  
  102. # taint mode: delete more unsafe vars for exec, as per perlsec
  103. {
  104.   # We only need to clean the environment once, it stays clean ...
  105.   my $cleaned_taint_path = 0;
  106.  
  107.   sub clean_path_in_taint_mode {
  108.     return if ($cleaned_taint_path++);
  109.     return unless am_running_in_taint_mode();
  110.  
  111.     dbg("util: taint mode: deleting unsafe environment variables, resetting PATH");
  112.  
  113.     if (RUNNING_ON_WINDOWS) {
  114.       dbg("util: running on Win32, skipping PATH cleaning");
  115.       return;
  116.     }
  117.  
  118.     delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
  119.  
  120.     # Go through and clean the PATH out
  121.     my @path = ();
  122.     my @stat;
  123.     foreach my $dir (File::Spec->path()) {
  124.       next unless $dir;
  125.  
  126.       $dir =~ /^(.+)$/; # untaint, then clean ( 'foo/./bar' -> 'foo/bar', etc. )
  127.       $dir = File::Spec->canonpath($1);
  128.  
  129.       if (!File::Spec->file_name_is_absolute($dir)) {
  130.     dbg("util: PATH included '$dir', which is not absolute, dropping");
  131.     next;
  132.       }
  133.       elsif (!(@stat=stat($dir))) {
  134.     dbg("util: PATH included '$dir', which doesn't exist, dropping");
  135.     next;
  136.       }
  137.       elsif (!-d _) {
  138.     dbg("util: PATH included '$dir', which isn't a directory, dropping");
  139.     next;
  140.       }
  141.       elsif (($stat[2]&2) != 0) {
  142.         # World-Writable directories are considered insecure.
  143.         # We could be more paranoid and check all of the parent directories as well,
  144.         # but it's good for now.
  145.     dbg("util: PATH included '$dir', which is world writable, dropping");
  146.     next;
  147.       }
  148.  
  149.       dbg("util: PATH included '$dir', keeping");
  150.       push(@path, $dir);
  151.     }
  152.  
  153.     $ENV{'PATH'} = join($Config{'path_sep'}, @path);
  154.     dbg("util: final PATH set to: ".$ENV{'PATH'});
  155.   }
  156. }
  157.  
  158. # taint mode: are we running in taint mode? 1 for yes, 0 for no.
  159. sub am_running_in_taint_mode {
  160.   return $AM_TAINTED if defined $AM_TAINTED;
  161.  
  162.   if ($] >= 5.008) {
  163.     # perl 5.8 and above, ${^TAINT} is a syntax violation in 5.005
  164.     $AM_TAINTED = eval q(no warnings q(syntax); ${^TAINT});
  165.   }
  166.   else {
  167.     # older versions
  168.     my $blank;
  169.     for my $d ((File::Spec->curdir, File::Spec->rootdir, File::Spec->tmpdir)) {
  170.       opendir(TAINT, $d) || next;
  171.       $blank = readdir(TAINT);
  172.       closedir(TAINT);
  173.       last;
  174.     }
  175.     if (!(defined $blank && $blank)) {
  176.       # these are sometimes untainted, so this is less preferable than readdir
  177.       $blank = join('', values %ENV, $0, @ARGV);
  178.     }
  179.     $blank = substr($blank, 0, 0);
  180.     # seriously mind-bending perl
  181.     $AM_TAINTED = not eval { eval "1 || $blank" || 1 };
  182.   }
  183.   dbg("util: running in taint mode? ". ($AM_TAINTED ? "yes" : "no"));
  184.   return $AM_TAINTED;
  185. }
  186.  
  187. ###########################################################################
  188.  
  189. sub am_running_on_windows {
  190.   return RUNNING_ON_WINDOWS;
  191. }
  192.  
  193. ###########################################################################
  194.  
  195. # untaint a path to a file, e.g. "/home/jm/.spamassassin/foo",
  196. # "C:\Program Files\SpamAssassin\tmp\foo", "/home/⌡ⁿt/etc".
  197. #
  198. # TODO: this does *not* handle locales well.  We cannot use "use locale"
  199. # and \w, since that will not detaint the data.  So instead just allow the
  200. # high-bit chars from ISO-8859-1, none of which have special metachar
  201. # meanings (as far as I know).
  202. #
  203. sub untaint_file_path {
  204.   my ($path) = @_;
  205.  
  206.   return unless defined($path);
  207.   return '' if ($path eq '');
  208.  
  209.   # Barry Jaspan: allow ~ and spaces, good for Windows.  Also return ''
  210.   # if input is '', as it is a safe path.
  211.   my $chars = '-_A-Za-z\xA0-\xFF0-9\.\%\@\=\+\,\/\\\:';
  212.   my $re = qr/^\s*([$chars][${chars}~ ]*)$/o;
  213.  
  214.   if ($path =~ $re) {
  215.     return $1;
  216.   } else {
  217.     warn "util: cannot untaint path: \"$path\"\n";
  218.     return $path;
  219.   }
  220. }
  221.  
  222. sub untaint_hostname {
  223.   my ($host) = @_;
  224.  
  225.   return unless defined($host);
  226.   return '' if ($host eq '');
  227.  
  228.   # from RFC 1035, but allowing domains starting with numbers:
  229.   #   $label = q/[A-Za-z\d](?:[A-Za-z\d-]{0,61}[A-Za-z\d])?/;
  230.   #   $domain = qq<$label(?:\.$label)*>;
  231.   #   length($host) <= 255 && $host =~ /^($domain)$/
  232.   # expanded (no variables in the re) because of a tainting bug in Perl 5.8.0
  233.   if (length($host) <= 255 && $host =~ /^([a-z\d](?:[a-z\d-]{0,61}[a-z\d])?(?:\.[a-z\d](?:[a-z\d-]{0,61}[a-z\d])?)*)$/i) {
  234.     return $1;
  235.   }
  236.   else {
  237.     warn "util: cannot untaint hostname: \"$host\"\n";
  238.     return $host;
  239.   }
  240. }
  241.  
  242. # This sub takes a scalar or a reference to an array, hash, scalar or another
  243. # reference and recursively untaints all its values (and keys if it's a
  244. # reference to a hash). It should be used with caution as blindly untainting
  245. # values subverts the purpose of working in taint mode. It will return the
  246. # untainted value if requested but to avoid unnecessary copying, the return
  247. # value should be ignored when working on lists.
  248. # Bad:
  249. #  %ENV = untaint_var(\%ENV);
  250. # Better:
  251. #  untaint_var(\%ENV);
  252. #
  253. sub untaint_var {
  254.   local ($_) = @_;
  255.   return undef unless defined;
  256.  
  257.   unless (ref) {
  258.     /^(.*)$/s;
  259.     return $1;
  260.   }
  261.   elsif (ref eq 'ARRAY') {
  262.     @{$_} = map { $_ = untaint_var($_) } @{$_};
  263.     return @{$_} if wantarray;
  264.   }
  265.   elsif (ref eq 'HASH') {
  266.     while (my ($k, $v) = each %{$_}) {
  267.       if (!defined $v && $_ == \%ENV) {
  268.     delete ${$_}{$k};
  269.     next;
  270.       }
  271.       ${$_}{untaint_var($k)} = untaint_var($v);
  272.     }
  273.     return %{$_} if wantarray;
  274.   }
  275.   elsif (ref eq 'SCALAR' or ref eq 'REF') {
  276.     ${$_} = untaint_var(${$_});
  277.   }
  278.   else {
  279.     warn "util: can't untaint a " . ref($_) . "!\n";
  280.   }
  281.   return $_;
  282. }
  283.  
  284. ###########################################################################
  285.  
  286. # timezone mappings: in case of conflicts, use RFC 2822, then most
  287. # common and least conflicting mapping
  288. my %TZ = (
  289.     # standard
  290.     'UT'   => '+0000',
  291.     'UTC'  => '+0000',
  292.     # US and Canada
  293.     'NDT'  => '-0230',
  294.     'AST'  => '-0400',
  295.     'ADT'  => '-0300',
  296.     'NST'  => '-0330',
  297.     'EST'  => '-0500',
  298.     'EDT'  => '-0400',
  299.     'CST'  => '-0600',
  300.     'CDT'  => '-0500',
  301.     'MST'  => '-0700',
  302.     'MDT'  => '-0600',
  303.     'PST'  => '-0800',
  304.     'PDT'  => '-0700',
  305.     'HST'  => '-1000',
  306.     'AKST' => '-0900',
  307.     'AKDT' => '-0800',
  308.     'HADT' => '-0900',
  309.     'HAST' => '-1000',
  310.     # Europe
  311.     'GMT'  => '+0000',
  312.     'BST'  => '+0100',
  313.     'IST'  => '+0100',
  314.     'WET'  => '+0000',
  315.     'WEST' => '+0100',
  316.     'CET'  => '+0100',
  317.     'CEST' => '+0200',
  318.     'EET'  => '+0200',
  319.     'EEST' => '+0300',
  320.     'MSK'  => '+0300',
  321.     'MSD'  => '+0400',
  322.     'MET'  => '+0100',
  323.     'MEZ'  => '+0100',
  324.     'MEST' => '+0200',
  325.     'MESZ' => '+0200',
  326.     # South America
  327.     'BRST' => '-0200',
  328.     'BRT'  => '-0300',
  329.     # Australia
  330.     'AEST' => '+1000',
  331.     'AEDT' => '+1100',
  332.     'ACST' => '+0930',
  333.     'ACDT' => '+1030',
  334.     'AWST' => '+0800',
  335.     # New Zealand
  336.     'NZST' => '+1200',
  337.     'NZDT' => '+1300',
  338.     # Asia
  339.     'JST'  => '+0900',
  340.     'KST'  => '+0900',
  341.     'HKT'  => '+0800',
  342.     'SGT'  => '+0800',
  343.     'PHT'  => '+0800',
  344.     # Middle East
  345.     'IDT'  => '+0300',
  346.     );
  347.  
  348. # month mappings
  349. my %MONTH = (jan => 1, feb => 2, mar => 3, apr => 4, may => 5, jun => 6,
  350.          jul => 7, aug => 8, sep => 9, oct => 10, nov => 11, dec => 12);
  351.  
  352. sub local_tz {
  353.   # standard method for determining local timezone
  354.   my $time = time;
  355.   my @g = gmtime($time);
  356.   my @t = localtime($time);
  357.   my $z = $t[1]-$g[1]+($t[2]-$g[2])*60+($t[7]-$g[7])*1440+($t[5]-$g[5])*525600;
  358.   return sprintf("%+.2d%.2d", $z/60, $z%60);
  359. }
  360.  
  361. sub parse_rfc822_date {
  362.   my ($date) = @_;
  363.   local ($_);
  364.   my ($yyyy, $mmm, $dd, $hh, $mm, $ss, $mon, $tzoff);
  365.  
  366.   # make it a bit easier to match
  367.   $_ = " $date "; s/, */ /gs; s/\s+/ /gs;
  368.  
  369.   # now match it in parts.  Date part first:
  370.   if (s/ (\d+) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{4}) / /i) {
  371.     $dd = $1; $mon = lc($2); $yyyy = $3;
  372.   } elsif (s/ (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) +(\d+) \d+:\d+:\d+ (\d{4}) / /i) {
  373.     $dd = $2; $mon = lc($1); $yyyy = $3;
  374.   } elsif (s/ (\d+) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{2,3}) / /i) {
  375.     $dd = $1; $mon = lc($2); $yyyy = $3;
  376.   } else {
  377.     dbg("util: time cannot be parsed: $date");
  378.     return undef;
  379.   }
  380.  
  381.   # handle two and three digit dates as specified by RFC 2822
  382.   if (defined $yyyy) {
  383.     if (length($yyyy) == 2 && $yyyy < 50) {
  384.       $yyyy += 2000;
  385.     }
  386.     elsif (length($yyyy) != 4) {
  387.       # three digit years and two digit years with values between 50 and 99
  388.       $yyyy += 1900;
  389.     }
  390.   }
  391.  
  392.   # hh:mm:ss
  393.   if (s/ (\d?\d):(\d\d)(:(\d\d))? / /) {
  394.     $hh = $1; $mm = $2; $ss = $4 || 0;
  395.   }
  396.  
  397.   # numeric timezones
  398.   if (s/ ([-+]\d{4}) / /) {
  399.     $tzoff = $1;
  400.   }
  401.   # common timezones
  402.   elsif (s/\b([A-Z]{2,4}(?:-DST)?)\b/ / && exists $TZ{$1}) {
  403.     $tzoff = $TZ{$1};
  404.   }
  405.   # all other timezones are considered equivalent to "-0000"
  406.   $tzoff ||= '-0000';
  407.  
  408.   # months
  409.   if (exists $MONTH{$mon}) {
  410.     $mmm = $MONTH{$mon};
  411.   }
  412.  
  413.   $hh ||= 0; $mm ||= 0; $ss ||= 0; $dd ||= 0; $mmm ||= 0; $yyyy ||= 0;
  414.  
  415.   # Time::Local (v1.10 at least) throws warnings when the dates cause
  416.   # a 32-bit overflow.  So force a min/max for year.
  417.   if ($yyyy > 2037) {
  418.     dbg("util: year after supported range, forcing year to 2037: $date");
  419.     $yyyy = 2037;
  420.   }
  421.   elsif ($yyyy < 1970) {
  422.     dbg("util: year before supported range, forcing year to 1970: $date");
  423.     $yyyy = 1971;
  424.   }
  425.  
  426.   # Fudge invalid times so that we get a usable date.
  427.   if ($ss > 59) { 
  428.     dbg("util: second after supported range, forcing second to 59: $date");  
  429.     $ss = 59;
  430.   } 
  431.  
  432.   if ($mm > 59) { 
  433.     dbg("util: minute after supported range, forcing minute to 59: $date");
  434.     $mm = 59;
  435.   }
  436.  
  437.   if ($hh > 23) { 
  438.     dbg("util: hour after supported range, forcing hour to 23: $date"); 
  439.     $hh = 23;
  440.   }
  441.  
  442.   my $time;
  443.   eval {        # could croak
  444.     $time = timegm($ss, $mm, $hh, $dd, $mmm-1, $yyyy);
  445.   };
  446.  
  447.   if ($@) {
  448.     dbg("util: time cannot be parsed: $date, $yyyy-$mmm-$dd $hh:$mm:$ss");
  449.     return undef;
  450.   }
  451.  
  452.   if ($tzoff =~ /([-+])(\d\d)(\d\d)$/)    # convert to seconds difference
  453.   {
  454.     $tzoff = (($2 * 60) + $3) * 60;
  455.     if ($1 eq '-') {
  456.       $time += $tzoff;
  457.     } else {
  458.       $time -= $tzoff;
  459.     }
  460.   }
  461.  
  462.   return $time;
  463. }
  464.  
  465. sub time_to_rfc822_date {
  466.   my($time) = @_;
  467.  
  468.   my @days = qw/Sun Mon Tue Wed Thu Fri Sat/;
  469.   my @months = qw/Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec/;
  470.   my @localtime = localtime($time || time);
  471.   $localtime[5]+=1900;
  472.  
  473.   sprintf("%s, %02d %s %4d %02d:%02d:%02d %s", $days[$localtime[6]], $localtime[3],
  474.     $months[$localtime[4]], @localtime[5,2,1,0], local_tz());
  475. }
  476.  
  477. ###########################################################################
  478.  
  479. # This is a wrapper for the Text::Wrap::wrap routine which makes its usage
  480. # a bit safer.  It accepts values for almost all options which can be set
  481. # in Text::Wrap.   All parameters are optional (leaving away the first one 
  482. # probably doesn't make too much sense though), either a missing or a false
  483. # value will fall back to the default.  Note that the parameter order and
  484. # default values aren't always the isame as in Text::Wrap itself.
  485. # The parameters are:
  486. #  1st:  The string to wrap.  Only one string is allowed (unlike the original
  487. #        wrap() routine).  (default: "")
  488. #  2nd:  The prefix to be put in front of all lines except the first one. 
  489. #        (default: "")
  490. #  3rd:  The prefix for the first line.  (default:  "")
  491. #  4th:  The number of columns available (no line will be longer than this
  492. #        parameter minus one).  See $Text::Wrap::columns.  (default:  77)
  493. #  5th:  Enable or disable overflow mode.  A false value is 'overflow', a
  494. #        true one 'wrap'; see $Text::Wrap::huge.  (default: 0)
  495. #  6th:  The sequence/expression to wrap at.  See $Text::Wrap::break
  496. #        (default: '\s');
  497. #  7th:  The string to join the lines again.  See $Text::Wrap::separator.
  498. #        (default: "\n")
  499. #  8th:  All tabs (except any in the prefix strings) are first replaced
  500. #        with 8 spaces.  This parameter controls if any 8-space sequence 
  501. #        is replaced with tabs again later.  See $Text::Wrap::unexpand but
  502. #        note that we use a different default value.  (default: 0)
  503.  
  504. sub wrap {
  505.   local($Text::Wrap::columns)   = $_[3] || 77;
  506.   local($Text::Wrap::huge)      = $_[4] ? 'overflow' : 'wrap';
  507.   local($Text::Wrap::break)     = $_[5] || '\s';
  508.   local($Text::Wrap::separator) = $_[6] || "\n";
  509.   local($Text::Wrap::unexpand)  = $_[7] || 0;
  510.   # There's a die() in there which "shouldn't happen", but better be
  511.   # paranoid.  We'll return the unwrapped string if anything went wrong.
  512.   my $text = $_[0] || "";
  513.   eval {
  514.     $text = Text::Wrap::wrap($_[2] || "", $_[1] || "", $text);
  515.   };
  516.   return $text;
  517. }
  518.  
  519. ###########################################################################
  520.  
  521. # Some base64 decoders will remove intermediate "=" characters, others
  522. # will stop decoding on the first "=" character, this one translates "="
  523. # characters to null.
  524. sub base64_decode {
  525.   local $_ = shift;
  526.   my $decoded_length = shift;
  527.  
  528.   s/\s+//g;
  529.   if (HAS_MIME_BASE64 && (length($_) % 4 == 0) &&
  530.       m|^(?:[A-Za-z0-9+/=]{2,}={0,2})$|s)
  531.   {
  532.     # only use MIME::Base64 when the XS and Perl are both correct and quiet
  533.     s/(=+)(?!=*$)/'A' x length($1)/ge;
  534.  
  535.     # If only a certain number of bytes are requested, truncate the encoded
  536.     # version down to the appropriate size and return the requested bytes
  537.     if (defined $decoded_length) {
  538.       $_ = substr $_, 0, 4 * (int($decoded_length/3) + 1);
  539.       my $decoded = MIME::Base64::decode_base64($_);
  540.       return substr $decoded, 0, $decoded_length;
  541.     }
  542.  
  543.     # otherwise, just decode the whole thing and return it
  544.     return MIME::Base64::decode_base64($_);
  545.   }
  546.   tr{A-Za-z0-9+/=}{}cd;            # remove non-base64 characters
  547.   s/=+$//;                # remove terminating padding
  548.   tr{A-Za-z0-9+/=}{ -_`};        # translate to uuencode
  549.   s/.$// if (length($_) % 4 == 1);    # unpack cannot cope with extra byte
  550.  
  551.   my $length;
  552.   my $out = '';
  553.   while ($_) {
  554.     $length = (length >= 84) ? 84 : length;
  555.     $out .= unpack("u", chr(32 + $length * 3/4) . substr($_, 0, $length, ''));
  556.     last if (defined $decoded_length && length $out >= $decoded_length);
  557.   }
  558.  
  559.   # If only a certain number of bytes are requested, truncate the encoded
  560.   # version down to the appropriate size and return the requested bytes
  561.   if (defined $decoded_length) {
  562.     return substr $out, 0, $decoded_length;
  563.   }
  564.  
  565.   return $out;
  566. }
  567.  
  568. sub qp_decode {
  569.   local $_ = shift;
  570.  
  571.   s/\=\r?\n//gs;
  572.   s/\=([0-9a-fA-F]{2})/chr(hex($1))/ge;
  573.   return $_;
  574. }
  575.  
  576. sub base64_encode {
  577.   local $_ = shift;
  578.  
  579.   if (HAS_MIME_BASE64) {
  580.     return MIME::Base64::encode_base64($_);
  581.   }
  582.  
  583.   $_ = pack("u57", $_);
  584.   s/^.//mg;
  585.   tr| -_`|A-Za-z0-9+/A|; # -> #`# <- kluge against vim syntax issues
  586.   s/(A+)$/'=' x length $1/e;
  587.   return $_;
  588. }
  589.  
  590. ###########################################################################
  591.  
  592. sub portable_getpwuid {
  593.   if (defined &Mail::SpamAssassin::Util::_getpwuid_wrapper) {
  594.     return Mail::SpamAssassin::Util::_getpwuid_wrapper(@_);
  595.   }
  596.  
  597.   if (!RUNNING_ON_WINDOWS) {
  598.     eval ' sub _getpwuid_wrapper { getpwuid($_[0]); } ';
  599.   } else {
  600.     dbg("util: defining getpwuid() wrapper using 'unknown' as username");
  601.     eval ' sub _getpwuid_wrapper { _fake_getpwuid($_[0]); } ';
  602.   }
  603.  
  604.   if ($@) {
  605.     warn "util: failed to define getpwuid() wrapper: $@\n";
  606.   } else {
  607.     return Mail::SpamAssassin::Util::_getpwuid_wrapper(@_);
  608.   }
  609. }
  610.  
  611. sub _fake_getpwuid {
  612.   return (
  613.     'unknown',        # name,
  614.     'x',        # passwd,
  615.     $_[0],        # uid,
  616.     0,            # gid,
  617.     '',            # quota,
  618.     '',            # comment,
  619.     '',            # gcos,
  620.     '/',        # dir,
  621.     '',            # shell,
  622.     '',            # expire
  623.   );
  624. }
  625.  
  626. ###########################################################################
  627.  
  628. # Given a string, extract an IPv4 address from it.  Required, since
  629. # we currently have no way to portably unmarshal an IPv4 address from
  630. # an IPv6 one without kludging elsewhere.
  631. #
  632. sub extract_ipv4_addr_from_string {
  633.   my ($str) = @_;
  634.  
  635.   return unless defined($str);
  636.  
  637.   if ($str =~ /\b(
  638.             (?:1\d\d|2[0-4]\d|25[0-5]|\d\d|\d)\.
  639.             (?:1\d\d|2[0-4]\d|25[0-5]|\d\d|\d)\.
  640.             (?:1\d\d|2[0-4]\d|25[0-5]|\d\d|\d)\.
  641.             (?:1\d\d|2[0-4]\d|25[0-5]|\d\d|\d)
  642.               )\b/ix)
  643.   {
  644.     if (defined $1) { return $1; }
  645.   }
  646.  
  647.   # ignore native IPv6 addresses; currently we have no way to deal with
  648.   # these if we could extract them, as the DNSBLs don't provide a way
  649.   # to query them!  TODO, eventually, once IPv6 spam starts to appear ;)
  650.   return;
  651. }
  652.  
  653. ###########################################################################
  654. {
  655.   my($hostname, $fq_hostname);
  656.  
  657. # get the current host's unqalified domain name (better: return whatever
  658. # Sys::Hostname thinks our hostname is, might also be a full qualified one)
  659.   sub hostname {
  660.     return $hostname if defined($hostname);
  661.  
  662.     # Sys::Hostname isn't taint safe and might fall back to `hostname`. So we've
  663.     # got to clean PATH before we may call it.
  664.     clean_path_in_taint_mode();
  665.     $hostname = Sys::Hostname::hostname();
  666.  
  667.     return $hostname;
  668.   }
  669.  
  670. # get the current host's fully-qualified domain name, if possible.  If
  671. # not possible, return the unqualified hostname.
  672.   sub fq_hostname {
  673.     return $fq_hostname if defined($fq_hostname);
  674.  
  675.     $fq_hostname = hostname();
  676.     if ($fq_hostname !~ /\./) { # hostname doesn't contain a dot, so it can't be a FQDN
  677.       my @names = grep(/^\Q${fq_hostname}.\E/o,                         # grep only FQDNs
  678.                     map { split } (gethostbyname($fq_hostname))[0 .. 1] # from all aliases
  679.                   );
  680.       $fq_hostname = $names[0] if (@names); # take the first FQDN, if any 
  681.     }
  682.  
  683.     return $fq_hostname;
  684.   }
  685. }
  686.  
  687. ###########################################################################
  688.  
  689. sub ips_match_in_16_mask {
  690.   my ($ipset1, $ipset2) = @_;
  691.   my ($b1, $b2);
  692.  
  693.   foreach my $ip1 (@{$ipset1}) {
  694.     foreach my $ip2 (@{$ipset2}) {
  695.       next unless defined $ip1;
  696.       next unless defined $ip2;
  697.       next unless ($ip1 =~ /^(\d+\.\d+\.)/); $b1 = $1;
  698.       next unless ($ip2 =~ /^(\d+\.\d+\.)/); $b2 = $1;
  699.       if ($b1 eq $b2) { return 1; }
  700.     }
  701.   }
  702.  
  703.   return 0;
  704. }
  705.  
  706. sub ips_match_in_24_mask {
  707.   my ($ipset1, $ipset2) = @_;
  708.   my ($b1, $b2);
  709.  
  710.   foreach my $ip1 (@{$ipset1}) {
  711.     foreach my $ip2 (@{$ipset2}) {
  712.       next unless defined $ip1;
  713.       next unless defined $ip2;
  714.       next unless ($ip1 =~ /^(\d+\.\d+\.\d+\.)/); $b1 = $1;
  715.       next unless ($ip2 =~ /^(\d+\.\d+\.\d+\.)/); $b2 = $1;
  716.       if ($b1 eq $b2) { return 1; }
  717.     }
  718.   }
  719.  
  720.   return 0;
  721. }
  722.  
  723. ###########################################################################
  724.  
  725. sub my_inet_aton { unpack("N", pack("C4", split(/\./, $_[0]))) }
  726.  
  727. ###########################################################################
  728.  
  729. sub parse_content_type {
  730.   # This routine is typically called by passing a
  731.   # get_header("content-type") which passes all content-type headers
  732.   # (array context).  If there are multiple Content-type headers (invalid,
  733.   # but it happens), MUAs seem to take the last one and so that's what we
  734.   # should do here.
  735.   #
  736.   my $ct = $_[-1] || 'text/plain; charset=us-ascii';
  737.  
  738.   # This could be made a bit more rigid ...
  739.   # the actual ABNF, BTW (RFC 1521, section 7.2.1):
  740.   # boundary := 0*69<bchars> bcharsnospace
  741.   # bchars := bcharsnospace / " "
  742.   # bcharsnospace :=    DIGIT / ALPHA / "'" / "(" / ")" / "+" /"_"
  743.   #               / "," / "-" / "." / "/" / ":" / "=" / "?"
  744.   #
  745.   # The boundary may be surrounded by double quotes.
  746.   # "the boundary parameter, which consists of 1 to 70 characters from
  747.   # a set of characters known to be very robust through email gateways,
  748.   # and NOT ending with white space.  (If a boundary appears to end with
  749.   # white space, the white space must be presumed to have been added by
  750.   # a gateway, and must be deleted.)"
  751.   #
  752.   # In practice:
  753.   # - MUAs accept whitespace before and after the "=" character
  754.   # - only an opening double quote seems to be needed
  755.   # - non-quoted boundaries should be followed by space, ";", or end of line
  756.   # - blank boundaries seem to not work
  757.   #
  758.   my($boundary) = $ct =~ m!\bboundary\s*=\s*("[^"]+|[^\s";]+(?=[\s;]|$))!i;
  759.  
  760.   # remove double-quotes in boundary (should only be at start and end)
  761.   #
  762.   $boundary =~ tr/"//d if defined $boundary;
  763.  
  764.   # Parse out the charset and name, if they exist.
  765.   #
  766.   my($charset) = $ct =~ /\bcharset\s*=\s*["']?(.*?)["']?(?:;|$)/i;
  767.   my($name) = $ct =~ /\b(?:file)?name\s*=\s*["']?(.*?)["']?(?:;|$)/i;
  768.  
  769.   # Get the actual MIME type out ...
  770.   # Note: the header content may not be whitespace unfolded, so make sure the
  771.   # REs do /s when appropriate.
  772.   #
  773.   $ct =~ s/^\s+//;            # strip leading whitespace
  774.   $ct =~ s/;.*$//s;            # strip everything after first ';'
  775.   $ct =~ s@^([^/]+(?:/[^/]*)?).*$@$1@s;    # only something/something ...
  776.   # strip inappropriate chars
  777.   $ct =~ tr/\000-\040\177-\377\042\050\051\054\056\072-\077\100\133-\135//d;
  778.   $ct = lc $ct;
  779.  
  780.   # bug 4298: If at this point we don't have a content-type, assume text/plain
  781.   $ct ||= "text/plain";
  782.  
  783.   # Now that the header has been parsed, return the requested information.
  784.   # In scalar context, just the MIME type, in array context the
  785.   # four important data parts (type, boundary, charset, and filename).
  786.   #
  787.   return wantarray ? ($ct,$boundary,$charset,$name) : $ct;
  788. }
  789.  
  790. ###########################################################################
  791.  
  792. sub url_encode {
  793.   my ($url) = @_;
  794.   my (@characters) = split(/(\%[0-9a-fA-F]{2})/, $url);
  795.   my (@unencoded) = ();
  796.   my (@encoded) = ();
  797.  
  798.   foreach (@characters) {
  799.     # escaped character set ...
  800.     if (/\%[0-9a-fA-F]{2}/) {
  801.       # IF it is in the range of 0x00-0x20 or 0x7f-0xff
  802.       #    or it is one of  "<", ">", """, "#", "%",
  803.       #                     ";", "/", "?", ":", "@", "=" or "&"
  804.       # THEN preserve its encoding
  805.       unless (/(20|7f|[0189a-fA-F][0-9a-fA-F])/i) {
  806.     s/\%([2-7][0-9a-fA-F])/sprintf "%c", hex($1)/e;
  807.     push(@unencoded, $_);
  808.       }
  809.     }
  810.     # other stuff
  811.     else {
  812.       # 0x00-0x20, 0x7f-0xff, ", %, <, >
  813.       s/([\000-\040\177-\377\042\045\074\076])
  814.       /push(@encoded, $1) && sprintf "%%%02x", unpack("C",$1)/egx;
  815.     }
  816.   }
  817.   if (wantarray) {
  818.     return(join("", @characters), join("", @unencoded), join("", @encoded));
  819.   }
  820.   else {
  821.     return join("", @characters);
  822.   }
  823. }
  824.  
  825. ###########################################################################
  826.  
  827. =item $module = first_available_module (@module_list)
  828.  
  829. Return the name of the first module that can be successfully loaded with
  830. C<require> from the list.  Returns C<undef> if none are available.
  831.  
  832. This is used instead of C<AnyDBM_File> as follows:
  833.  
  834.   my $module = Mail::SpamAssassin::Util::first_available_module
  835.                         (qw(DB_File GDBM_File NDBM_File SDBM_File));
  836.   tie %hash, $module, $path, [... args];
  837.  
  838. Note that C<SDBM_File> is guaranteed to be present, since it comes
  839. with Perl.
  840.  
  841. =cut
  842.  
  843. sub first_available_module {
  844.   my (@packages) = @_;
  845.   foreach my $mod (@packages) {
  846.     if (eval 'require '.$mod.'; 1; ') {
  847.       return $mod;
  848.     }
  849.   }
  850.   undef;
  851. }
  852.  
  853. ###########################################################################
  854.  
  855. =item my ($filepath, $filehandle) = secure_tmpfile();
  856.  
  857. Generates a filename for a temporary file, opens it exclusively and
  858. securely, and returns a filehandle to the open file (opened O_RDWR).
  859.  
  860. If it cannot open a file after 20 tries, it returns C<undef>.
  861.  
  862. =cut
  863.  
  864. # thanks to http://www2.picante.com:81/~gtaylor/autobuse/ for this code
  865. sub secure_tmpfile {
  866.   my $tmpdir = Mail::SpamAssassin::Util::untaint_file_path(File::Spec->tmpdir());
  867.  
  868.   if (!$tmpdir) {
  869.     # Note: we would prefer to keep this fatal, as not being able to
  870.     # find a writable tmpdir is a big deal for the calling code too.
  871.     # That would be quite a psychotic case, also.
  872.     warn "util: cannot find a temporary directory, set TMP or TMPDIR in environment";
  873.     return;
  874.   }
  875.  
  876.   my ($reportfile, $tmpfile);
  877.   my $umask = umask 077;
  878.  
  879.   for (my $retries = 20; $retries > 0; $retries--) {
  880.     # we do not rely on the obscurity of this name for security,
  881.     # we use a average-quality PRG since this is all we need
  882.     my $suffix = join('', (0..9,'A'..'Z','a'..'z')[rand 62, rand 62, rand 62,
  883.                            rand 62, rand 62, rand 62]);
  884.     $reportfile = File::Spec->catfile($tmpdir,".spamassassin${$}${suffix}tmp");
  885.  
  886.     # instead, we require O_EXCL|O_CREAT to guarantee us proper
  887.     # ownership of our file, read the open(2) man page
  888.     if (sysopen($tmpfile, $reportfile, O_RDWR|O_CREAT|O_EXCL, 0600)) {
  889.       last;
  890.     }
  891.  
  892.     if ($!{EEXIST}) {
  893.       # it is acceptable if $tmpfile already exists, try another
  894.       next;
  895.     }
  896.     
  897.     # error, maybe "out of quota" or "too many open files" (bug 4017)
  898.     warn "util: secure_tmpfile failed to create file '$reportfile': $!\n";
  899.  
  900.     # ensure the file handle is not semi-open in some way
  901.     if ($tmpfile) {
  902.       close $tmpfile;
  903.     }
  904.   }
  905.  
  906.   umask $umask;
  907.  
  908.   if (!$tmpfile) {
  909.     warn "util: secure_tmpfile failed to create file, giving up";
  910.     return;    # undef
  911.   }
  912.  
  913.   return ($reportfile, $tmpfile);
  914. }
  915.  
  916. =item my ($dirpath) = secure_tmpdir();
  917.  
  918. Generates a directory for temporary files.  Creates it securely and
  919. returns the path to the directory.
  920.  
  921. If it cannot create a directory after 20 tries, it returns C<undef>.
  922.  
  923. =cut
  924.  
  925. # stolen from secure_tmpfile()
  926. sub secure_tmpdir {
  927.   my $tmpdir = Mail::SpamAssassin::Util::untaint_file_path(File::Spec->tmpdir());
  928.  
  929.   if (!$tmpdir) {
  930.     # Note: we would prefer to keep this fatal, as not being able to
  931.     # find a writable tmpdir is a big deal for the calling code too.
  932.     # That would be quite a psychotic case, also.
  933.     warn "util: cannot find a temporary directory, set TMP or TMPDIR in environment";
  934.     return;
  935.   }
  936.  
  937.   my ($reportpath, $tmppath);
  938.   my $umask = umask 077;
  939.  
  940.   for (my $retries = 20; $retries > 0; $retries--) {
  941.     # we do not rely on the obscurity of this name for security,
  942.     # we use a average-quality PRG since this is all we need
  943.     my $suffix = join('', (0..9,'A'..'Z','a'..'z')[rand 62, rand 62, rand 62,
  944.                            rand 62, rand 62, rand 62]);
  945.     $reportpath = File::Spec->catfile($tmpdir,".spamassassin${$}${suffix}tmp");
  946.  
  947.     # instead, we require O_EXCL|O_CREAT to guarantee us proper
  948.     # ownership of our file, read the open(2) man page
  949.     if (mkdir $reportpath, 0700) {
  950.       $tmppath = $reportpath;
  951.       last;
  952.     }
  953.  
  954.     if ($!{EEXIST}) {
  955.       # it is acceptable if $reportpath already exists, try another
  956.       next;
  957.     }
  958.     
  959.     # error, maybe "out of quota" or "too many open files" (bug 4017)
  960.     warn "util: secure_tmpdir failed to create file '$reportpath': $!\n";
  961.   }
  962.  
  963.   umask $umask;
  964.  
  965.   warn "util: secure_tmpdir failed to create a directory, giving up" if (!$tmppath);
  966.  
  967.   return $tmppath;
  968. }
  969.  
  970.  
  971. ###########################################################################
  972.  
  973. sub uri_to_domain {
  974.   my ($uri) = @_;
  975.  
  976.   # Javascript is not going to help us, so return.
  977.   return if ($uri =~ /^javascript:/i);
  978.  
  979.   $uri =~ s,#.*$,,gs;            # drop fragment
  980.   $uri =~ s#^[a-z]+:/{0,2}##gsi;    # drop the protocol
  981.   $uri =~ s,^[^/]*\@,,gs;        # username/passwd
  982.  
  983.   # strip path and CGI params.  note: bug 4213 shows that "&" should
  984.   # *not* be likewise stripped here -- it's permitted in hostnames by
  985.   # some common MUAs!
  986.   $uri =~ s,[/\?].*$,,gs;              
  987.  
  988.   $uri =~ s,:\d*$,,gs;            # port, bug 4191: sometimes the # is missing
  989.  
  990.   # skip undecoded URIs if the encoded bits shouldn't be.
  991.   # we'll see the decoded version as well.  see url_encode()
  992.   return if $uri =~ /\%(?:2[1-9a-fA-F]|[3-6][0-9a-fA-f]|7[0-9a-eA-E])/;
  993.  
  994.   # keep IPs intact
  995.   if ($uri !~ /^\d+\.\d+\.\d+\.\d+$/) { 
  996.     # get rid of hostname part of domain, understanding delegation
  997.     $uri = Mail::SpamAssassin::Util::RegistrarBoundaries::trim_domain($uri);
  998.  
  999.     # ignore invalid domains
  1000.     return unless
  1001.         (Mail::SpamAssassin::Util::RegistrarBoundaries::is_domain_valid($uri));
  1002.   }
  1003.   
  1004.   # $uri is now the domain only
  1005.   return lc $uri;
  1006. }
  1007.  
  1008. sub uri_list_canonify {
  1009.   my($redirector_patterns, @uris) = @_;
  1010.  
  1011.   # make sure we catch bad encoding tricks
  1012.   my @nuris = ();
  1013.   for my $uri (@uris) {
  1014.     # we're interested in http:// and so on, skip mailto: and
  1015.     # email addresses with no protocol
  1016.     next if $uri =~ /^mailto:/i || $uri =~ /^[^:]*\@/;
  1017.  
  1018.     # sometimes we catch URLs on multiple lines
  1019.     $uri =~ s/\n//g;
  1020.  
  1021.     # URLs won't have leading/trailing whitespace
  1022.     $uri =~ s/^\s+//;
  1023.     $uri =~ s/\s+$//;
  1024.  
  1025.     # CRs just confuse things down below, so trash them now
  1026.     $uri =~ s/\r//g;
  1027.  
  1028.     # Make a copy so we don't trash the original in the array
  1029.     my $nuri = $uri;
  1030.  
  1031.     # bug 4390: certain MUAs treat back slashes as front slashes.
  1032.     # since backslashes are supposed to be encoded in a URI, swap non-encoded
  1033.     # ones with front slashes.
  1034.     $nuri =~ tr@\\@/@;
  1035.  
  1036.     # http:www.foo.biz -> http://www.foo.biz
  1037.     $nuri =~ s#^(https?:)/{0,2}#$1//#i;
  1038.  
  1039.     # *always* make a dup with all %-encoding decoded, since
  1040.     # important parts of the URL may be encoded (such as the
  1041.     # scheme). (bug 4213)
  1042.     if ($nuri =~ /\%[0-9a-fA-F]{2}/) {
  1043.       $nuri = Mail::SpamAssassin::Util::url_encode($nuri);
  1044.     }
  1045.  
  1046.     # www.foo.biz -> http://www.foo.biz
  1047.     # unschemed URIs: assume default of "http://" as most MUAs do
  1048.     if ($nuri !~ /^[-_a-z0-9]+:/i) {
  1049.       if ($nuri =~ /^ftp\./) {
  1050.     $nuri =~ s@^@ftp://@g;
  1051.       }
  1052.       else {
  1053.     $nuri =~ s@^@http://@g;
  1054.       }
  1055.     }
  1056.  
  1057.     # http://www.foo.biz?id=3 -> http://www.foo.biz/?id=3
  1058.     $nuri =~ s@^(https?://[^/?]+)\?@$1/?@i;
  1059.  
  1060.     # deal with encoding of chars, this is just the set of printable
  1061.     # chars minus ' ' (that is, dec 33-126, hex 21-7e)
  1062.     $nuri =~ s/\&\#0*(3[3-9]|[4-9]\d|1[01]\d|12[0-6]);/sprintf "%c",$1/ge;
  1063.     $nuri =~ s/\&\#x0*(2[1-9]|[3-6][a-fA-F0-9]|7[0-9a-eA-E]);/sprintf "%c",hex($1)/ge;
  1064.  
  1065.     # put the new URI on the new list if it's different
  1066.     if ($nuri ne $uri) {
  1067.       push(@nuris, $nuri);
  1068.     }
  1069.  
  1070.     # deal with wierd hostname parts, remove user/pass, etc.
  1071.     if ($nuri =~ m{^(https?://)([^/]+?)((?::\d*)?\/.*)?$}i) {
  1072.       my($proto, $host, $rest) = ($1,$2,$3);
  1073.  
  1074.       # not required
  1075.       $rest ||= '';
  1076.  
  1077.       # bug 4146: deal with non-US ASCII 7-bit chars in the host portion
  1078.       # of the URI according to RFC 1738 that's invalid, and the tested
  1079.       # browsers (Firefox, IE) remove them before usage...
  1080.       if ($host =~ tr/\000-\040\200-\377//d) {
  1081.         push(@nuris, join ('', $proto, $host, $rest));
  1082.       }
  1083.  
  1084.       # deal with http redirectors.  strip off one level of redirector
  1085.       # and add back to the array.  the foreach loop will go over those
  1086.       # and deal appropriately.
  1087.       # bug 3308: redirectors like yahoo only need one '/' ... <grrr>
  1088.       if ($rest =~ m{(https?:/{0,2}.+)$}i) {
  1089.         push(@uris, $1);
  1090.       }
  1091.  
  1092.       # resort to redirector pattern matching if the generic https? check
  1093.       # doesn't result in a match -- bug 4176
  1094.       else {
  1095.     foreach (@{$redirector_patterns}) {
  1096.       if ("$proto$host$rest" =~ $_) {
  1097.         next unless defined $1;
  1098.         dbg("uri: parsed uri pattern: $_");
  1099.         dbg("uri: parsed uri found: $1 in redirector: $proto$host$rest");
  1100.         push (@uris, $1);
  1101.         last;
  1102.       }
  1103.     }
  1104.       }
  1105.  
  1106.       ########################
  1107.       ## TVD: known issue, if host has multiple combinations of the following,
  1108.       ## all permutations will be put onto @nuris.  shouldn't be an issue.
  1109.  
  1110.       # Get rid of cruft that could cause confusion for rules...
  1111.  
  1112.       # remove "www.fakehostname.com@" username part
  1113.       if ($host =~ s/^[^\@]+\@//gs) {
  1114.         push(@nuris, join ('', $proto, $host, $rest));
  1115.       }
  1116.  
  1117.       # bug 3186: If in a sentence, we might pick up odd characters ...
  1118.       # ie: "visit http://example.biz." or "visit http://example.biz!!!"
  1119.       # the host portion should end in some form of alpha-numeric, strip off
  1120.       # the rest.
  1121.       if ($host =~ s/[^0-9A-Za-z]+$//) {
  1122.         push(@nuris, join ('', $proto, $host, $rest));
  1123.       }
  1124.  
  1125.       ########################
  1126.  
  1127.       # deal with hosts which are IPs
  1128.       # also handle things like:
  1129.       # http://89.0x00000000000000000000068.0000000000000000000000160.0x00000000000011
  1130.       #    both hex (0x) and oct (0+) encoded octets, etc.
  1131.  
  1132.       if ($host =~ /^
  1133.         ((?:0x[0-9a-f]{2,}|\d+)\.)
  1134.     ((?:0x[0-9a-f]{2,}|\d+)\.)
  1135.     ((?:0x[0-9a-f]{2,}|\d+)\.)
  1136.     (0x[0-9a-f]{2,}|\d+)
  1137.     $/ix) {
  1138.         my @chunk = ($1,$2,$3,$4);
  1139.         foreach my $octet (@chunk) {
  1140.           $octet =~ s/^0x0*([0-9a-f][0-9a-f])/sprintf "%d",hex($1)/gei;
  1141.           $octet =~ s/^0+([1-3][0-7]{0,2}|[4-7][0-7]?)\b/sprintf "%d",oct($1)/ge;
  1142.       $octet =~ s/^0+//;
  1143.         }
  1144.         push(@nuris, join ('', $proto, @chunk, $rest));
  1145.       }
  1146.  
  1147.       # "http://0x7f000001/"
  1148.       elsif ($host =~ /^0x[0-9a-f]+$/i) {
  1149.         # only take last 4 octets
  1150.         $host =~ s/^0x[0-9a-f]*?([0-9a-f]{1,8})$/sprintf "%d",hex($1)/gei;
  1151.         push(@nuris, join ('', $proto, decode_ulong_to_ip($host), $rest));
  1152.       }
  1153.  
  1154.       # "http://1113343453/"
  1155.       elsif ($host =~ /^[0-9]+$/) {
  1156.         push(@nuris, join ('', $proto, decode_ulong_to_ip($host), $rest));
  1157.       }
  1158.  
  1159.     }
  1160.   }
  1161.  
  1162.   # remove duplicates, merge nuris and uris
  1163.   my %uris = map { $_ => 1 } @uris, @nuris;
  1164.  
  1165.   return keys %uris;
  1166. }
  1167.  
  1168. sub decode_ulong_to_ip {
  1169.   return join(".", unpack("CCCC",pack("H*", sprintf "%08lx", $_[0])));
  1170. }
  1171.  
  1172. ###########################################################################
  1173.  
  1174. sub first_date {
  1175.   my (@strings) = @_;
  1176.  
  1177.   foreach my $string (@strings) {
  1178.     my $time = parse_rfc822_date($string);
  1179.     return $time if defined($time) && $time;
  1180.   }
  1181.   return undef;
  1182. }
  1183.  
  1184. sub receive_date {
  1185.   my ($header) = @_;
  1186.  
  1187.   $header ||= '';
  1188.   $header =~ s/\n[ \t]+/ /gs;    # fix continuation lines
  1189.  
  1190.   my @rcvd = ($header =~ /^Received:(.*)/img);
  1191.   my @local;
  1192.   my $time;
  1193.  
  1194.   if (@rcvd) {
  1195.     if ($rcvd[0] =~ /qmail \d+ invoked by uid \d+/ ||
  1196.     $rcvd[0] =~ /\bfrom (?:localhost\s|(?:\S+ ){1,2}\S*\b127\.0\.0\.1\b)/)
  1197.     {
  1198.       push @local, (shift @rcvd);
  1199.     }
  1200.     if (@rcvd && ($rcvd[0] =~ m/\bby localhost with \w+ \(fetchmail-[\d.]+/)) {
  1201.       push @local, (shift @rcvd);
  1202.     }
  1203.     elsif (@local) {
  1204.       unshift @rcvd, (shift @local);
  1205.     }
  1206.   }
  1207.  
  1208.   if (@rcvd) {
  1209.     $time = first_date(shift @rcvd);
  1210.     return $time if defined($time);
  1211.   }
  1212.   if (@local) {
  1213.     $time = first_date(@local);
  1214.     return $time if defined($time);
  1215.   }
  1216.   if ($header =~ /^(?:From|X-From-Line:)\s+(.+)$/im) {
  1217.     my $string = $1;
  1218.     $string .= " ".local_tz() unless $string =~ /(?:[-+]\d{4}|\b[A-Z]{2,4}\b)/;
  1219.     $time = first_date($string);
  1220.     return $time if defined($time);
  1221.   }
  1222.   if (@rcvd) {
  1223.     $time = first_date(@rcvd);
  1224.     return $time if defined($time);
  1225.   }
  1226.   if ($header =~ /^Resent-Date:\s*(.+)$/im) {
  1227.     $time = first_date($1);
  1228.     return $time if defined($time);
  1229.   }
  1230.   if ($header =~ /^Date:\s*(.+)$/im) {
  1231.     $time = first_date($1);
  1232.     return $time if defined($time);
  1233.   }
  1234.  
  1235.   return time;
  1236. }
  1237.  
  1238. ###########################################################################
  1239.  
  1240. sub setuid_to_euid {
  1241.   return if (RUNNING_ON_WINDOWS);
  1242.  
  1243.   # remember the target uid, the first number is the important one
  1244.   my $touid = $>;
  1245.  
  1246.   if ($< != $touid) {
  1247.     dbg("util: changing real uid from $< to match effective uid $touid");
  1248.     $< = $touid; # try the simple method first
  1249.  
  1250.     # bug 3586: Some perl versions, typically those on a BSD-based
  1251.     # platform, require RUID==EUID (and presumably == 0) before $<
  1252.     # can be changed.  So this is a kluge for us to get around the
  1253.     # typical spamd-ish behavior of: $< = 0, $> = someuid ...
  1254.     if ( $< != $touid ) {
  1255.       dbg("util: initial attempt to change real uid failed, trying BSD workaround");
  1256.  
  1257.       $> = $<;            # revert euid to ruid
  1258.       $< = $touid;        # change ruid to target
  1259.       $> = $touid;        # change euid back to target
  1260.     }
  1261.  
  1262.     # Check that we have now accomplished the setuid
  1263.     if ($< != $touid) {
  1264.       # keep this fatal: it's a serious security problem if it fails
  1265.       die "util: setuid $< to $touid failed!";
  1266.     }
  1267.   }
  1268. }
  1269.  
  1270. # helper app command-line open
  1271. sub helper_app_pipe_open {
  1272.   if (RUNNING_ON_WINDOWS) {
  1273.     return helper_app_pipe_open_windows (@_);
  1274.   } else {
  1275.     return helper_app_pipe_open_unix (@_);
  1276.   }
  1277. }
  1278.  
  1279. sub helper_app_pipe_open_windows {
  1280.   my ($fh, $stdinfile, $duperr2out, @cmdline) = @_;
  1281.  
  1282.   # use a traditional open(FOO, "cmd |")
  1283.   my $cmd = join(' ', @cmdline);
  1284.   if ($stdinfile) { $cmd .= qq/ < "$stdinfile"/; }
  1285.   if ($duperr2out) { $cmd .= " 2>&1"; }
  1286.   return open ($fh, $cmd.'|');
  1287. }
  1288.  
  1289. sub force_die {
  1290.   my ($msg) = @_;
  1291.  
  1292.   # note use of eval { } scope in logging -- paranoia to ensure that a broken
  1293.   # $SIG{__WARN__} implementation will not interfere with the flow of control
  1294.   # here, where we *have* to die.
  1295.   eval { warn $msg; };
  1296.  
  1297.   POSIX::_exit(1);  # avoid END and destructor processing 
  1298.   kill('KILL',$$);  # still kicking? die! 
  1299. }
  1300.  
  1301. sub helper_app_pipe_open_unix {
  1302.   my ($fh, $stdinfile, $duperr2out, @cmdline) = @_;
  1303.  
  1304.   # do a fork-open, so we can setuid() back
  1305.   my $pid = open ($fh, '-|');
  1306.   if (!defined $pid) {
  1307.     # acceptable to die() here, calling code catches it
  1308.     die "util: cannot fork: $!";
  1309.   }
  1310.  
  1311.   if ($pid != 0) {
  1312.     return $pid;          # parent process; return the child pid
  1313.   }
  1314.  
  1315.   # else, child process.  
  1316.   # from now on, we cannot die(), as a parent-process eval { } scope
  1317.   # could intercept it! use force_die() instead  (bug 4370, cmt 2)
  1318.  
  1319.   # go setuid...
  1320.   setuid_to_euid();
  1321.   dbg("util: setuid: ruid=$< euid=$>");
  1322.  
  1323.   # now set up the fds.  due to some wierdness, we may have to ensure that we
  1324.   # *really* close the correct fd number, since some other code may have
  1325.   # redirected the meaning of STDOUT/STDIN/STDERR it seems... (bug 3649). use
  1326.   # POSIX::close() for that. it's safe to call close() and POSIX::close() on
  1327.   # the same fd; the latter is a no-op in that case.
  1328.  
  1329.   if (!$stdinfile) {              # < $tmpfile
  1330.     # ensure we have *some* kind of fd 0.
  1331.     $stdinfile = "/dev/null";
  1332.   }
  1333.  
  1334.   my $f = fileno(STDIN);
  1335.   close STDIN;
  1336.  
  1337.   # sanity: was that the *real* STDIN? if not, close that one too ;)
  1338.   if ($f != 0) {
  1339.     POSIX::close(0);
  1340.   }
  1341.  
  1342.   open (STDIN, "<$stdinfile") or force_die "util: cannot open $stdinfile: $!";
  1343.  
  1344.   # this should be impossible; if we just closed fd 0, UNIX
  1345.   # fd behaviour dictates that the next fd opened (the new STDIN)
  1346.   # will be the lowest unused fd number, which should be 0.
  1347.   # so die with a useful error if this somehow isn't the case.
  1348.   if (fileno(STDIN) != 0) {
  1349.     force_die "util: setuid: oops: fileno(STDIN) [".fileno(STDIN)."] != 0";
  1350.   }
  1351.  
  1352.   # ensure STDOUT is open.  since we just created a pipe to ensure this, it has
  1353.   # to be open to that pipe, and if it isn't, something's seriously screwy.
  1354.   # Update: actually, this fails! see bug 3649 comment 37.  For some reason,
  1355.   # fileno(STDOUT) can be 0; possibly because open("-|") didn't change the fh
  1356.   # named STDOUT, instead changing fileno(1) directly.  So this is now
  1357.   # commented.
  1358.   # if (fileno(STDOUT) != 1) {
  1359.   # die "setuid: oops: fileno(STDOUT) [".fileno(STDOUT)."] != 1";
  1360.   # }
  1361.  
  1362.   if ($duperr2out) {             # 2>&1
  1363.     my $f = fileno(STDERR);
  1364.     close STDERR;
  1365.  
  1366.     # sanity: was that the *real* STDERR? if not, close that one too ;)
  1367.     if ($f != 2) {
  1368.       POSIX::close(2);
  1369.     }
  1370.  
  1371.     open (STDERR, ">&STDOUT") or force_die "util: dup STDOUT failed: $!";
  1372.  
  1373.     # STDERR must be fd 2 to be useful to subprocesses! (bug 3649)
  1374.     if (fileno(STDERR) != 2) {
  1375.       force_die "util: oops: fileno(STDERR) [".fileno(STDERR)."] != 2";
  1376.     }
  1377.   }
  1378.  
  1379.   exec @cmdline;
  1380.   warn "util: exec failed: $!";
  1381.  
  1382.   # bug 4370: we really have to exit here; break any eval traps
  1383.   POSIX::_exit(1);  # avoid END and destructor processing 
  1384.   kill('KILL',$$);  # still kicking? die! 
  1385.   die;  # must be a die() otherwise -w will complain
  1386. }
  1387.  
  1388. ###########################################################################
  1389.  
  1390. # As "perldoc perlvar" notes, in perl 5.8.0, the concept of "safe" signal
  1391. # handling was added, which means that signals cannot interrupt a running OP.
  1392. # unfortunately, a regexp match is a single OP, so a psychotic m// can
  1393. # effectively "hang" the interpreter as a result, and a $SIG{ALRM} handler
  1394. # will never get called.
  1395. #
  1396. # However, by using "unsafe" signals, we can still interrupt that -- and
  1397. # POSIX::sigaction can create an unsafe handler on 5.8.x.   So this function
  1398. # provides a portable way to do that.
  1399.  
  1400. sub trap_sigalrm_fully {
  1401.   my ($handler) = @_;
  1402.   if ($] < 5.008) {
  1403.     # signals are always unsafe, just use %SIG
  1404.     $SIG{ALRM} = $handler;
  1405.   } else {
  1406.     # may be using "safe" signals with %SIG; use POSIX to avoid it
  1407.     POSIX::sigaction POSIX::SIGALRM(), new POSIX::SigAction $handler;
  1408.   }
  1409. }
  1410.  
  1411. ###########################################################################
  1412.  
  1413. 1;
  1414.  
  1415. =back
  1416.  
  1417. =cut
  1418.