C:\Documents and Settings\Mosquito\Desktop\cleanfeed\cleanfeed\cleanfeed-my C:\Documents and Settings\Mosquito\Desktop\cleanfeed\cleanfeed-2026\cleanfeed
# vim: set tabstop=4 shiftwidth=4 expandtab syntax=perl: # vim: set tabstop=4 shiftwidth=4 expandtab syntax=perl:
# #
# Copyright 1999 Jeremy Nixon <jeremy@exit109.com> # Copyright 1999 Jeremy Nixon <jeremy@exit109.com>
# Copyright 2001 Marco d'Itri <md@linux.it> # Copyright 2001 Marco d'Itri <md@linux.it>
# #
# Modified by Steve Crook <steve@mixmin.net> # Modified by Steve Crook <steve@mixmin.net>
# Redistributed in accordance with the terms of the Artistic license. # Redistributed in accordance with the terms of the Artistic license.
# #
# This software is distributed under the terms of the Artistic License. # This software is distributed under the terms of the Artistic License.
# Please see the LICENSE file in the distribution. # Please see the LICENSE file in the distribution.
# #
   
# CHANGE THE BELOW SETTING! # CHANGE THE BELOW SETTING!
# Directory where cleanfeed.local and the other configuration files live. # Directory where cleanfeed.local and the other configuration files live.
# Set this to undef to not use any external file. # Set this to undef to not use any external file.
   
$config_dir = '/usr/inn/cleanfeed/etc';  $config_dir = exists $ENV{CLEANFEED_CONFIG_DIR} 
     ? ($ENV{CLEANFEED_CONFIG_DIR} eq '' ? undef : $ENV{CLEANFEED_CONFIG_DIR})  
     : '/usr/local/news/cleanfeed/etc';  
   
############################################################################## ##############################################################################
# Server configuration # Server configuration
# #
# Set $MODE according to what you're running. # Set $MODE according to what you're running.
# Acceptable values: inn, highwind. # Acceptable values: inn, highwind.
# If you are running a highwind-like server then the value set here is ignored # If you are running a highwind-like server then the value set here is ignored
# and the default from highwind.pl is used. # and the default from highwind.pl is used.
   
$MODE ||= 'inn'; $MODE ||= 'inn';
   
############################################################################## ##############################################################################
# WARNING: NO USER SERVICEABLE PARTS BELOW THIS LINE # WARNING: NO USER SERVICEABLE PARTS BELOW THIS LINE
# IF YOU WANT TO CHANGE SOMETHING, PLEASE USE cleanfeed.local # IF YOU WANT TO CHANGE SOMETHING, PLEASE USE cleanfeed.local
############################################################################## ##############################################################################
   
  # Cleanfeed 2026 maintained release: 2026.08  
  $cleanfeed_maintenance_version = '2026.08';  
   
# default configuration # default configuration
sub get_config { sub get_config {
   %config = (    %config = (
   verbose => 1,           # verbose rejection reasons in news.notice/logfile?    verbose => 1,           # verbose rejection reasons in news.notice/logfile?
   aggressive => 1,            # set to 0 if your lawyers are paranoid    aggressive => 1,            # set to 0 if your lawyers are paranoid
   maxgroups => 14,            # maximum number of groups in a crosspost    maxgroups => 14,            # maximum number of groups in a crosspost
   block_binaries => 1,        # block misplaced binaries    block_binaries => 1,        # block misplaced binaries
   block_all_binaries => 0,    # Reject all binary regardless of distribution    block_all_binaries => 0,    # Reject all binary regardless of distribution
   block_late_cancels => 0,    # block cancels of rejected articles    block_late_cancels => 0,    # block cancels of rejected articles
   block_user_spamcancels => 1,# reject spam cancels    block_user_spamcancels => 1,# reject spam cancels
   block_user_cancels => 0,    # accept only spam cancels    block_user_cancels => 0,    # accept only spam cancels
   block_extra_reposts => 1,   # block reposts for articles not cancelled    block_extra_reposts => 1,   # block reposts for articles not cancelled
   
   do_md5 => 1,                # do the md5 checks?    do_md5 => 1,                # do the md5 checks?
   do_phl => 1,                # do the posting-host/lines EMP check?    do_phl => 1,                # do the posting-host/lines EMP check?
   do_phn => 1,                # do the posting-host/newsgroups EMP check?    do_phn => 1,                # do the posting-host/newsgroups EMP check?
   do_phr => 1,                # do posting-host (high risk groups) check?    do_phr => 1,                # do posting-host (high risk groups) check?
   do_fsl => 1,                # do the from/subject/lines EMP check?    do_fsl => 1,                # do the from/subject/lines EMP check?
   do_scoring_filter => 1,     # use the scoring filter?    do_scoring_filter => 1,     # use the scoring filter?
   do_ratio_scoring => 0,      # Score articles based on Caps/Sym/URL ratios?    do_ratio_scoring => 0,      # Score articles based on Caps/Sym/URL ratios?
   bad_url_score => 5,         # What score to apply to bad_url hits    bad_url_score => 5,         # What score to apply to bad_url hits
   
   do_emp_dump => 1,           # dump EMP histories to a file for persistence?    do_emp_dump => 1,           # dump EMP histories to a file for persistence?
   emp_dump_file => '',        # file to dump EMP histories to    emp_dump_file => '',        # file to dump EMP histories to
   
   MD5RateCutoff => 5,         # reject if this many copies are in the history    MD5RateCutoff => 5,         # reject if this many copies are in the history
   MD5RateCeiling => 85,       # only count this high    MD5RateCeiling => 85,       # only count this high
   MD5RateBaseInterval => 7200,# How long to wait before decrementing the count    MD5RateBaseInterval => 7200,# How long to wait before decrementing the count
   PHLRateCutoff => 20,    PHLRateCutoff => 20,
   PHLRateCeiling => 80,    PHLRateCeiling => 80,
   PHLRateBaseInterval => 3600,    PHLRateBaseInterval => 3600,
   PHNRateCutoff => 150,    PHNRateCutoff => 150,
   PHNRateCeiling => 200,    PHNRateCeiling => 200,
   PHNRateBaseInterval => 1800,    PHNRateBaseInterval => 1800,
   PHRRateCutoff => 10,    PHRRateCutoff => 10,
   PHRRateCeiling => 80,    PHRRateCeiling => 80,
   PHRRateBaseInterval => 3600,    PHRRateBaseInterval => 3600,
   FSLRateCutoff => 20,    FSLRateCutoff => 20,
   FSLRateCeiling => 40,    FSLRateCeiling => 40,
   FSLRateBaseInterval => 1800,    FSLRateBaseInterval => 1800,
   
   fuzzy_md5 => 1,             # screw around with the body before md5ing?    fuzzy_md5 => 1,             # screw around with the body before md5ing?
   fuzzy_max_length => 700,    # don't screw with bodies over this many lines    fuzzy_max_length => 700,    # don't screw with bodies over this many lines
   md5_max_length => 2000,     # don't md5 articles over this many lines    md5_max_length => 2000,     # don't md5 articles over this many lines
   trim_interval => 900,       # trim hashes every N seconds    trim_interval => 900,       # trim hashes every N seconds
   stats_interval => 3600,     # write status file every N seconds    stats_interval => 3600,     # write status file every N seconds
   MIDmaxlife => 4,            # time to keep rejected message-ids, in hours    MIDmaxlife => 4,            # time to keep rejected message-ids, in hours
   md5_skips_followups => 1,   # avoid MD5 check on articles with References?    md5_skips_followups => 1,   # avoid MD5 check on articles with References?
   phn_aggressive => 1,        # use path for phn filter when no posting host    phn_aggressive => 1,        # use path for phn filter when no posting host
   phr_aggressive => 1,        # use path for phr filter when no posting host    phr_aggressive => 1,        # use path for phr filter when no posting host
   do_mid_filter => 1,         # use the message-id CHECK filter? (INN only)    do_mid_filter => 1,         # use the message-id CHECK filter? (INN only)
   do_supersedes_filter => 1,  # do the excessive supersedes filter?    do_supersedes_filter => 1,  # do the excessive supersedes filter?
     supersedes_mode => 'reject',   # off, audit, reject  
     supersedes_window => 900,     # rate window/base interval in seconds  
     supersedes_ceiling => 50,     # maximum retained count per source  
     supersedes_max_faq => 45,  
     supersedes_max_unmoderated => 6,  
     supersedes_max_moderated => 10,  
     supersedes_max_all_moderated => 35,  
     supersedes_max_unknown_active => 10,  
   drop_useless_controls => 1, # drop sendsys, senduuname, version control msg    drop_useless_controls => 1, # drop sendsys, senduuname, version control msg
   drop_ihave_sendme => 1,     # drop ihave, sendme control messages    drop_ihave_sendme => 1,     # drop ihave, sendme control messages
   bad_rate_reload => 10000,   # Reload bad_* files after this many articles    bad_rate_reload => 10000,   # Reload bad_* files after this many articles
   
     # Lightweight 2026 operational hardening.  
     bad_reload_mode => 'mtime',      # mtime, articles, or both  
     bad_reload_interval => 60,       # seconds between mtime checks  
     external_regex_max_length => 1048576,  
     external_regex_max_count => 5000,  
     external_regex_body_bytes => 65536,  
     external_regex_reject_unsafe => 1,  
   
     # Article structure and lightweight malformed-encoding checks.  
     max_header_line_bytes => 16384,  # 0 disables  
     max_body_line_bytes => 1048576,  # 0 disables  
     long_line_mode => 'audit',       # off, audit, reject  
     malformed_encoding_check => 1,  
     malformed_encoding_mode => 'audit', # off, audit, reject  
     yenc_size_tolerance_percent => 15,  
   
     # Lightweight byte-profile detector for opaque binary payloads.  
     binary_byte_profile_enabled => 1,  
     binary_nonprintable_ratio_percent => 10,  
     binary_ratio_min_bytes => 4096,  
     binary_ratio_scan_bytes => 65536,  
     binary_byte_profile_mode => 'audit', # off, audit, reject  
   
     # Path sanity and traffic anomaly audit.  
     path_sanity_enabled => 1,  
     path_max_hops => 100,  
     path_max_token_bytes => 255,  
     path_repeat_ceiling => 3,  
     path_sanity_mode => 'audit',     # off, audit, reject  
     anomaly_rate_enabled => 1,  
     anomaly_window => 60,  
     anomaly_peer_articles => 10000,  
     anomaly_host_articles => 1000,  
     anomaly_mode => 'audit',         # off, audit, reject  
     anomaly_max_keys => 1000,  
   
     # Bounded top-offender counters and configuration fingerprint.  
     top_offenders_enabled => 1,  
     top_offenders_limit => 10,  
     top_offenders_max_keys => 1000,  
     config_fingerprint_enabled => 1,  
   
     # Optional IDN normalization. No dependency is required; it activates only  
     # when Net::IDN::Encode is installed.  
     idn_normalization => 1,  
   
     # Trusted-list bypass is deliberately granular; full bypass is discouraged.  
     trusted_bypass_scoring => 1,  
     trusted_bypass_content => 0,  
     trusted_bypass_binary => 0,  
     trusted_bypass_crosspost => 0,  
     trusted_bypass_size => 0,  
   
   low_xpost_maxgroups => 6,   # max xposts in low_xpost_groups    low_xpost_maxgroups => 6,   # max xposts in low_xpost_groups
   meow_ext_maxgroups => 2,    # max xposts from meow_groups to other groups    meow_ext_maxgroups => 2,    # max xposts from meow_groups to other groups
   off_topic1_maxgroups => 2,  # How many off topic groups allowed in a distro    off_topic1_maxgroups => 2,  # How many off topic groups allowed in a distro
   on_topic1_maxgroups => 5,   # How many on-topic groups allowed in a distro    on_topic1_maxgroups => 5,   # How many on-topic groups allowed in a distro
   on_topic1_mingroups => 2,   # How many on-topic groups req'd to trigger    on_topic1_mingroups => 2,   # How many on-topic groups req'd to trigger
                               # off-topic filter                                # off-topic filter
   off_topic2_maxgroups => 2,    off_topic2_maxgroups => 2,
   on_topic2_maxgroups => 5,    on_topic2_maxgroups => 5,
   on_topic2_mingroups => 2,    on_topic2_mingroups => 2,
   
   binaries_in_mod_groups => 0,    # allow binaries in moderated groups?    binaries_in_mod_groups => 0,    # allow binaries in moderated groups?
   max_base64_lines => 150,    # Allow x lines Base64 encoding in non-bin grps    max_base64_lines => 150,    # Allow x bare Base64 lines in non-bin grps
     binary_scan_bytes => 262144,# Scan this many bytes from article start  
     binary_scan_tail_bytes => 16384, # Also scan tail (yEnc terminators)  
     detect_mime_binaries => 1,  # Detect MIME binary attachments, even if short  
     detect_malformed_yenc => 1, # Catch common non-compliant yEnc variants  
   
   block_mime_html => 1,       # block MIME encapsulated HTML    block_mime_html => 1,       # block MIME encapsulated HTML
   block_html_multipart => 1,  # block all multipart with html sections    block_html_multipart => 1,  # block all multipart with html sections
   block_html => 1,            # block native HTML (Content-Type text/html)    block_html => 1,            # block native HTML (Content-Type text/html)
   block_html_images => 1,     # block <img src> in non text/plain messages    block_html_images => 1,     # block <img src> in non text/plain messages
   
   active_file => '',  # active file to determine which groups are moderated    active_file => '',  # active file to determine which groups are moderated
   
   # Logging and pid_file don't work for INN (uses news.notice)    # Logging and pid_file don't work for INN (uses news.notice)
   log_directory => '',    log_directory => '',
   log_name => '',    log_name => '',
   log_accepts => 0,               # include accepted articles in the log?    log_accepts => 0,               # include accepted articles in the log?
   max_log_size => 0,    max_log_size => 0,
   rotate_file => '',              # rotate log if this file exists    rotate_file => '',              # rotate log if this file exists
   keep_old_logs => 7,             # how many old logfiles to keep    keep_old_logs => 7,             # how many old logfiles to keep
   
   pid_file => '',    pid_file => '',
   
   # crude stats on what the filter is doing    # crude stats on what the filter is doing
   statfile => '',    statfile => '',
   html_statfile => '',    html_statfile => '',
   inn_syslog_status => 0,         # status to syslog (late-model INN only)    inn_syslog_status => 0,         # status to syslog (late-model INN only)
   
   timer_info => 1,                # timing information (arts/second) in status report?    timer_info => 1,                # timing information (arts/second) in status report?
     body_preview_bytes => 16384,     # decoded/lowercase preview used by legacy rules  
     study_max_lines => 250,          # Perl study() only below this line count; 0 disables  
     validate_config => 1,            # validate numeric values and regular expressions  
   
     # Peer and hierarchy policy engine. The default is audit-only for safety.  
     policy_enabled => 1,  
     policy_mode => 'audit',          # off, audit, quarantine, reject  
     policy_default_max_bytes => 0,   # 0 means unlimited  
     policy_default_allow_binary => 1,  
     policy_log_matches => 1,  
     policy_log_accepts => 0,  
     policy_include_message_id => 1,  
     policy_include_peer => 1,  
     policy_include_groups => 1,  
     policy_max_peer_counters => 500,  
     policy_max_hierarchy_counters => 500,  
   
     # Simple operational metrics.  All outputs are optional.  
     metrics_enabled => 1,  
     metrics_status_file => '',       # atomic key=value snapshot  
     metrics_csv_file => '',          # append-only CSV history  
     metrics_csv_interval => 300,  
     metrics_syslog => 0,  
     metrics_by_rule => 1,  
     metrics_by_peer => 1,  
     metrics_by_hierarchy => 1,  
     metrics_prometheus_file => '',   # optional node_exporter textfile  
   
   debug_batch_directory => '',    # directory for debugging batches    debug_batch_directory => '',    # directory for debugging batches
   debug_batch_size => 0,          # max size of batch files before rotation    debug_batch_size => 0,          # max size of batch files before rotation
   
   ### binaries allowed if groups match    ### binaries allowed if groups match
   bin_allowed => '^bin[a.]|\.bin[aei.]|\.bin$|^fur\.artwork'.    bin_allowed => '^bin[a.]|\.bin[aei.]|\.bin$|^fur\.artwork'.
       '|^alt\.anonymous\.messages$|^de\.alt\.dateien|^rec\.games\.bolo$'.        '|^alt\.anonymous\.messages$|^de\.alt\.dateien|^rec\.games\.bolo$'.
       '|^comp\.security\.pgp\.test$|^sfnet\.tiedostot'.        '|^comp\.security\.pgp\.test$|^sfnet\.tiedostot'.
       '|^fido\.|^unidata\.|alt\.security\.keydist'.        '|^fido\.|^unidata\.|alt\.security\.keydist'.
       '|^linux\.debian\.bugs\.dist$|^lucky\.freebsd',        '|^linux\.debian\.bugs\.dist$|^lucky\.freebsd',
   
   # Groups matching this regex will accept binary UUenc and yEnc files    # Groups matching this regex will accept binary UUenc and yEnc files
   # where filename extensions match 'image_extensions'.    # where filename extensions match 'image_extensions'.
   image_allowed => '\.pictures',    image_allowed => '\.pictures',
   
   # Extensions on image files that are allowed in 'image_allowed' groups.    # Extensions on image files that are allowed in 'image_allowed' groups.
   # These are not case sensitive.    # These are not case sensitive.
   image_extensions => 'jpe?g|png|gif|icon?',    image_extensions => 'jpe?g|png|gif|icon?',
   
   ### no binaries allowed even if bin_allowed matches    ### no binaries allowed even if bin_allowed matches
   bad_bin => '\.d$|^alt\.chello',    bad_bin => '\.d$|^alt\.chello',
   
   ### md5 EMP check not done if groups match    ### md5 EMP check not done if groups match
   md5exclude => '^perl\.cpan\.testers',    md5exclude => '^perl\.cpan\.testers',
   
   ### reject all articles crossposted to groups matching this    ### reject all articles crossposted to groups matching this
   poison_groups => '^alt\.(?:binaires|bainaries)|sexzilla|^newsmon$'.    poison_groups => '^alt\.(?:binaires|bainaries)|sexzilla|^newsmon$'.
       '|h[i\d]pcl[o\d]ne|h\.i\.p\.c\.r\.i\.m\.e'.        '|h[i\d]pcl[o\d]ne|h\.i\.p\.c\.r\.i\.m\.e'.
       ($] >= 5.005 ? '|(?<!free\.)h[i\d]pcr[i\d]m[e\d]'        ($] >= 5.005 ? '|(?<!free\.)h[i\d]pcr[i\d]m[e\d]'
                   : '|^alt\.hipcrime|^us\.hipcrime|^hipcrime|h\dpcr\dme'),                    : '|^alt\.hipcrime|^us\.hipcrime|^hipcrime|h\dpcr\dme'),
   
   ### no checks done if groups match    ### no checks done if groups match
   allexclude => '^mailing\.|^linux\.',    allexclude => '^mailing\.|^linux\.',
   
   ### Exclude matching Newsgroups from the scoring filters    ### Exclude matching Newsgroups from the scoring filters
   score_exclude => 'alt\.anonymous\.messages',    score_exclude => 'alt\.anonymous\.messages',
   
   ### MIME HTML allowed here (if block_mime_html is True)    ### MIME HTML allowed here (if block_mime_html is True)
   mime_html_allowed => '^pgsql\.|^relcom\.|^gmane\.',    mime_html_allowed => '^pgsql\.|^relcom\.|^gmane\.',
   
   ### HTML allowed here (if block_html is True)    ### HTML allowed here (if block_html is True)
   html_allowed => "^relcom\.|^microsoft\.",    html_allowed => "^relcom\.|^microsoft\.",
   
   test_groups => '\.test(ing)?(?:$|\.)|^es\.pruebas|^borland\.public\.test2'.    test_groups => '\.test(ing)?(?:$|\.)|^es\.pruebas|^borland\.public\.test2'.
       '|^cern\.testnews',        '|^cern\.testnews',
   
   ### groups where we restrict crossposts even more than normal    ### groups where we restrict crossposts even more than normal
   low_xpost_groups => 'test|jobs|forsale',    low_xpost_groups => 'test|jobs|forsale',
   
   ### Groups where we restrict crossposts with other groups as a result of    ### Groups where we restrict crossposts with other groups as a result of
   ### the meow wars.  (<4curm4$r1@decaxp.harvard.edu>)    ### the meow wars.  (<4curm4$r1@decaxp.harvard.edu>)
   meow_groups => '^alt\.fan\.karl-malden\.nose|^alt\.flame|^alt\.troll'.    meow_groups => '^alt\.fan\.karl-malden\.nose|^alt\.flame|^alt\.troll'.
       '|^alt\.alien\.vampire\.flonk\.flonk\.flonk|^alt\.romath'.        '|^alt\.alien\.vampire\.flonk\.flonk\.flonk|^alt\.romath'.
       '|^alt\.snuh|^alt\.fan\.natasha',        '|^alt\.snuh|^alt\.fan\.natasha',
   
   ### Topic groups allow administrators to limit crossposting from defined    ### Topic groups allow administrators to limit crossposting from defined
   ### groups to undefined groups.  The allowed number of groups is defined    ### groups to undefined groups.  The allowed number of groups is defined
   ### in off_topic_maxgroups.    ### in off_topic_maxgroups.
   ### Examples: '\.politi[ck]', '\.pets', '\.sex'    ### Examples: '\.politi[ck]', '\.pets', '\.sex'
   topic1_groups => '',    topic1_groups => '',
   topic2_groups => '',    topic2_groups => '',
   
   ### cancel in these groups are not honored    ### cancel in these groups are not honored
   no_cancel_groups => '^alt\.religion\.scientology|^news\.admin\.net-abuse|^alt\.config$',    no_cancel_groups => '^alt\.religion\.scientology|^news\.admin\.net-abuse|^alt\.config$',
   
   ### domains starting/ending in "xxx" are never good news    ### domains starting/ending in "xxx" are never good news
   ### (checked against .com, .net, and .nu tld's only)    ### (checked against .com, .net, and .nu tld's only)
# FIXME currently disabled # FIXME currently disabled
#   baddomainpat => '[\w\-]+xxx|xxx[\w\-]+', #   baddomainpat => '[\w\-]+xxx|xxx[\w\-]+',
   
   ### Exclude these Newsgroups from the From / Subject / Lines filter    ### Exclude these Newsgroups from the From / Subject / Lines filter
   fsl_exclude => 'comp\.lang\.ruby',    fsl_exclude => 'comp\.lang\.ruby',
   
   ### Exclude these Newsgroups from the Posting-Host / Lines filter    ### Exclude these Newsgroups from the Posting-Host / Lines filter
   phl_exclude => 'comp\.lang\.ruby|^microsoft\.|^alt\.bestjobsusa'.    phl_exclude => 'comp\.lang\.ruby|^microsoft\.|^alt\.bestjobsusa'.
       '|\.bbs\.|^relcom\.hot-news|^szn\.news\.',        '|\.bbs\.|^relcom\.hot-news|^szn\.news\.',
   
   ### Exclude these Newsgroups from the Posting-Host / Newsgroup filter    ### Exclude these Newsgroups from the Posting-Host / Newsgroup filter
   phn_exclude => '^local\.|^alt\.anonymous\.messages'.    phn_exclude => '^local\.|^alt\.anonymous\.messages'.
       '|^\w+\.bin|^microsoft\.|\.bbs\.|^alt\.bestjobsusa|^mozilla\.'.        '|^\w+\.bin|^microsoft\.|\.bbs\.|^alt\.bestjobsusa|^mozilla\.'.
       '|^gnus?\.|^alt\.pictures\.|^gmane\.|^fa\.|^stu\.|^corel\.|\.cvs\.'.        '|^gnus?\.|^alt\.pictures\.|^gmane\.|^fa\.|^stu\.|^corel\.|\.cvs\.'.
       '|\.talk|^lists\.|^microsoft\.|news\.lists\.filters|^perl\.'.        '|\.talk|^lists\.|^microsoft\.|news\.lists\.filters|^perl\.'.
       '|\.marketplace|\.ebay|\.forsale|^relcom\.hot-news|^szn\.news\.'.        '|\.marketplace|\.ebay|\.forsale|^relcom\.hot-news|^szn\.news\.'.
       '|^comp\.lang\.python',        '|^comp\.lang\.python',
   
   # These hosts create unlinkable Posting-Host headers, rendering them    # These hosts create unlinkable Posting-Host headers, rendering them
   # useless for hashing purposes.  In these instances the NPH and PHR filter    # useless for hashing purposes.  In these instances the NPH and PHR filter
   # will use the Path header instead, (if phn_aggressive is true).  The PHL    # will use the Path header instead, (if phn_aggressive is true).  The PHL
   # filter will ignore posts from these hosts.    # filter will ignore posts from these hosts.
   bad_nph_hosts => 'newsguy\.com|tornevall\.net',    bad_nph_hosts => 'newsguy\.com|tornevall\.net',
         
   ### Exempt these hosts from the Posting-Host / Lines filter    ### Exempt these hosts from the Posting-Host / Lines filter
   phl_exempt => '^localhost$|webtv\.net$|^newscene\.newscene\.com$'.    phl_exempt => '^localhost$|webtv\.net$|^newscene\.newscene\.com$'.
       '|^freebsd\.csie\.nctu\.edu\.tw$|^ddt\.demos\.su$|^onlyNews customer$'.        '|^freebsd\.csie\.nctu\.edu\.tw$|^ddt\.demos\.su$|^onlyNews customer$'.
       '|localhost\.pld-linux\.org',        '|localhost\.pld-linux\.org',
   
   ### Exempt these hosts from the Posting-Host / Newsgroup filter    ### Exempt these hosts from the Posting-Host / Newsgroup filter
   phn_exempt => '^localhost$|^127\.0\.0\.1$|localhost\.pld-linux\.org',    phn_exempt => '^localhost$|^127\.0\.0\.1$|localhost\.pld-linux\.org',
   
   ### Exempt these hosts from the High-Risk Newsgroups filter    ### Exempt these hosts from the High-Risk Newsgroups filter
   phr_exempt => '^localhost$|^127\.0\.0\.1$',    phr_exempt => '^localhost$|^127\.0\.0\.1$',
   
   ### Exclude these groups from the ratio based scoring filters. (Only used    ### Exclude these groups from the ratio based scoring filters. (Only used
   ### if do_ratio_scoring is true).    ### if do_ratio_scoring is true).
   ratio_exclude => '^cn\.|^tw\.|^japan\.|^fj\.|\.china',    ratio_exclude => '^cn\.|^tw\.|^japan\.|^fj\.|\.china',
   
   ### Newsgroups that get frequently flooded.  This defines the groups    ### Newsgroups that get frequently flooded.  This defines the groups
   ### processed by the PHR filter.  It should be entirely user-defined.    ### processed by the PHR filter.  It should be entirely user-defined.
   flood_groups => '',    flood_groups => '',
   
   ### posting hosts exempt from excessive supersedes filter    ### posting hosts exempt from excessive supersedes filter
   supersedes_exempt => '^localhost$|^penguin-lust\.mit\.edu$',    supersedes_exempt => '^localhost$|^penguin-lust\.mit\.edu$',
   
   ### refuse articles with these in the message-id (INN only)    ### refuse articles with these in the message-id (INN only)
   refuse_messageids => 'HeadHunter\.NET>|none\d+\.yet>',    refuse_messageids => 'HeadHunter\.NET>|none\d+\.yet>',
   
   ### groups expected to contain bodies and/or subject lines from spam    ### groups expected to contain bodies and/or subject lines from spam
   spam_report_groups => '^(?:news|de)\.admin\.net-abuse'.    spam_report_groups => '^(?:news|de)\.admin\.net-abuse'.
       '|news\.lists\.filters|\.nocem|\.spamtrap$|\.spam\.sightings'.        '|news\.lists\.filters|\.nocem|\.spamtrap$|\.spam\.sightings'.
       '|^fr\.usenet\.abus\.rapports|^nl\.internet\.misbruik\.rapport$',        '|^fr\.usenet\.abus\.rapports|^nl\.internet\.misbruik\.rapport$',
   
   adult_groups => 'personals|sex|nud[ei]|erot|xxx|lolita'.    adult_groups => 'personals|sex|nud[ei]|erot|xxx|lolita'.
   '|neojapan|bondage|fetish|lesbian|porn|tasteless|voyeur|^it\.sesso'.    '|neojapan|bondage|fetish|lesbian|porn|tasteless|voyeur|^it\.sesso'.
   '|^alt\.(?:mag[\.a]|redh|stories'.    '|^alt\.(?:mag[\.a]|redh|stories'.
    '|fan\.(?:air|asp|pret|televisionx|pst|snuf))'.     '|fan\.(?:air|asp|pret|televisionx|pst|snuf))'.
   '|^alt\.binaries\.(?:aimee|adole|ass\b|great|images\.(?:sun|under)|full'.    '|^alt\.binaries\.(?:aimee|adole|ass\b|great|images\.(?:sun|under)|full'.
    '|linger|pent|pin-?up|nospam|scanm|pictures\.(?:aspa|bc|blon|blueb|bru'.     '|linger|pent|pin-?up|nospam|scanm|pictures\.(?:aspa|bc|blon|blueb|bru'.
     '|centerf|coc|girlfr|horny|hussy|strip)|multimedia\.(?:boy|natur))',      '|centerf|coc|girlfr|horny|hussy|strip)|multimedia\.(?:boy|natur))',
   
   not_adult_groups => 'sexual\.abuse|^soc.sex|^fr\.soc\.homosexualite'.    not_adult_groups => 'sexual\.abuse|^soc.sex|^fr\.soc\.homosexualite'.
       '|^alt\.(?:support|teens|answers)',        '|^alt\.(?:support|teens|answers)',
   
   faq_groups => '\.faqs?$|\.answers$|^news\.announce\.newgroups$'.    faq_groups => '\.faqs?$|\.answers$|^news\.announce\.newgroups$'.
       '|^news\.admin\.hierarchies$',        '|^news\.admin\.hierarchies$',
   
   local_approved_groups => 'alt\.|news\.admin\.net-abuse\.'.    local_approved_groups => 'alt\.|news\.admin\.net-abuse\.'.
   '|fr\.misc\.bavardages\.dinosaures|alt\.sysadmin\.recovery'.    '|fr\.misc\.bavardages\.dinosaures|alt\.sysadmin\.recovery'.
   '|alt\.tech-support\.recovery|alt\.dev\.null'    '|alt\.tech-support\.recovery|alt\.dev\.null'
   
   );    );
   
   # Store the Cleanfeed development revision number    # Store the Cleanfeed development revision number
   #($version) = q$Revision$ =~ /(\d+)/;    #($version) = q$Revision$ =~ /(\d+)/;
   #($version_date) = q$Date$ =~ /(\d{4}(\-\d{2}){2})/;    #($version_date) = q$Date$ =~ /(\d{4}(\-\d{2}){2})/;
   
   ### List of group patterns that don't allow outside crossposts.    ### List of group patterns that don't allow outside crossposts.
   ### Key is "friendly" name, value is the pattern.    ### Key is "friendly" name, value is the pattern.
   %Restricted_Groups = (    %Restricted_Groups = (
       cl      => '^cl\.',        cl      => '^cl\.',
       net     => '^net\.',        net     => '^net\.',
       bofh    => '^bofh\.',        bofh    => '^bofh\.',
       'de.alt.dateien' => '^de\.alt\.dateien',        'de.alt.dateien' => '^de\.alt\.dateien',
       sdnet   => '^sdnet\.',  # Requested by William Kronert        sdnet   => '^sdnet\.',  # Requested by William Kronert
   );    );
   
   # Load up the external config file    # Load up the external config file
   my $local_file = "$config_dir/cleanfeed.local";    my $local_file = "$config_dir/cleanfeed.local";
   $Local_Conf_Err = 0;    $Local_Conf_Err = 0;
   if ($config_dir and -e $local_file) {    if ($config_dir and -e $local_file) {
       undef %config_local;        undef %config_local;
       undef %config_append;        undef %config_append;
       if (open(CF, $local_file)) {        if (open(CF, $local_file)) {
           my $cf = join('', <CF>);            my $cf = join('', <CF>);
           close CF;            close CF;
           eval $cf;            eval $cf;
           if ($@) {            if ($@) {
               slog('E', "Cannot load $local_file: $@");                slog('E', "Cannot load $local_file: $@");
               $Local_Conf_Err = 1;                $Local_Conf_Err = 1;
           } else {            } else {
               local_config() if defined &local_config;                local_config() if defined &local_config;
           }            }
       } else {        } else {
           slog('E', "Cannot open $local_file: $!");            slog('E', "Cannot open $local_file: $!");
           $Local_Conf_Err = 1;            $Local_Conf_Err = 1;
       }        }
   
       # config_local overrides the config settings        # config_local overrides the config settings
       if (%config_local) {        if (%config_local) {
           $config{$_} = $config_local{$_} foreach keys %config_local;            $config{$_} = $config_local{$_} foreach keys %config_local;
           undef %config_local;            undef %config_local;
       }        }
       # config_append adds to the config regexps        # config_append adds to the config regexps
       if (%config_append) {        if (%config_append) {
           foreach (qw(bin_allowed bad_bin md5exclude poison_groups            foreach (qw(bin_allowed bad_bin md5exclude poison_groups
                   allexclude html_allowed mime_html_allowed low_xpost_groups                    allexclude html_allowed mime_html_allowed low_xpost_groups
                   test_groups no_cancel_groups baddomainpat fsl_exclude                    test_groups no_cancel_groups baddomainpat fsl_exclude
                   phl_exempt phl_exclude supersedes_exempt bad_nph_hosts                    phl_exempt phl_exclude supersedes_exempt bad_nph_hosts
                   phn_exempt phr_exempt phn_exclude flood_groups                    phn_exempt phr_exempt phn_exclude flood_groups
                   refuse_messageids net_abuse_groups spam_report_groups                    refuse_messageids net_abuse_groups spam_report_groups
                   adult_groups not_adult_groups faq_groups ratio_exclude                    adult_groups not_adult_groups faq_groups ratio_exclude
                   image_allowed image_extensions meow_groups                    image_allowed image_extensions meow_groups
                   topic1_groups topic2_groups local_approved_groups)) {                    topic1_groups topic2_groups local_approved_groups)) {
               if (defined $config_append{$_}) {                if (defined $config_append{$_}) {
                   $config{$_} .= "|$config_append{$_}";                    $config{$_} .= "|$config_append{$_}";
                   $config{$_} =~ s/\|\|/\|/g;                    $config{$_} =~ s/\|\|/\|/g;
               }                }
               $config{$_} =~ s/^\|//;                $config{$_} =~ s/^\|//;
               $config{$_} =~ s/\|$//;                $config{$_} =~ s/\|$//;
           }            }
           undef %config_append;            undef %config_append;
       }        }
   }    }
   
     validate_configuration() if $config{validate_config};  
   
   @Restricted_List = keys %Restricted_Groups;    @Restricted_List = keys %Restricted_Groups;
   
   # Create the logfile path. Will be undefined if logging is broken    # Create the logfile path. Will be undefined if logging is broken
   if ($config{log_directory} and $config{log_name}) {    if ($config{log_directory} and $config{log_name}) {
       $Log_File = "$config{log_directory}/$config{log_name}";        $Log_File = "$config{log_directory}/$config{log_name}";
   } else {    } else {
       undef $Log_File;        undef $Log_File;
   }    }
   
   # parse the active file if we've been given one.    # parse the active file if we've been given one.
   if ($config{active_file}) {    if ($config{active_file}) {
       %Moderated = ();        %Moderated = ();
       if (open(ACTIVE, $config{active_file})) {        if (open(ACTIVE, $config{active_file})) {
           while (<ACTIVE>) {            while (<ACTIVE>) {
               chomp;                chomp;
               my ($group, undef, undef, $flag) = split(/ /);                my ($group, undef, undef, $flag) = split(/ /);
               $Moderated{$group} = 1 if $flag eq 'm';                $Moderated{$group} = 1 if $flag eq 'm';
           }            }
           close ACTIVE;            close ACTIVE;
       } else {        } else {
           slog('E', "Cannot open $config{active_file}: $!");            slog('E', "Cannot open $config{active_file}: $!");
       }        }
   }    }
   
   # Try and load base64 decoding functionality.    # Try and load base64 decoding functionality.
   eval "use MIME::Base64; 1" or $config{nobase64} = 1;    eval "use MIME::Base64; 1" or $config{nobase64} = 1;
   
} # end of get_config() } # end of get_config()
   
# Regexps for matching URLs # Regexps for matching URLs
$TLDs = '(?:[Cc][Oo][Mm]|[Nn][Ee][Tt]|[Oo][Rr][Gg]|[Ee][Dd][Uu]' . $TLDs = '(?:[Cc][Oo][Mm]|[Nn][Ee][Tt]|[Oo][Rr][Gg]|[Ee][Dd][Uu]' .
   '|[Cc][Oo]\.[Uu][Kk]|[Ff][Rr]' .    '|[Cc][Oo]\.[Uu][Kk]|[Ff][Rr]' .
   '|[Cc][Oo][Mm]\.[Aa][Uu]|[Nn][Ll]|[Dd][Ee]|[Nn][Oo]|[Dd][Kk]|[Cc][Hh]' .    '|[Cc][Oo][Mm]\.[Aa][Uu]|[Nn][Ll]|[Dd][Ee]|[Nn][Oo]|[Dd][Kk]|[Cc][Hh]' .
   '|[Ss][Ee]|[Nn][Uu]|[Tt][Oo]|[Rr][Uu]|[Uu][Aa]|[Cc][Aa]|[Cc][Xx])';    '|[Ss][Ee]|[Nn][Uu]|[Tt][Oo]|[Rr][Uu]|[Uu][Aa]|[Cc][Aa]|[Cc][Xx])';
#$IP = '\d\d\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?\b'; #$IP = '\d\d\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?\b';
$IP = '(?:\d{1,3}\.){3}\d{1,3}\b'; $IP = '(?:\d{1,3}\.){3}\d{1,3}\b';
$StealthIP = '(?:\d{10}|0[0-7]+\.0[0-7]+\.0[0-7]+\.0[0-7]+)'; $StealthIP = '(?:\d{10}|0[0-7]+\.0[0-7]+\.0[0-7]+\.0[0-7]+)';
# Make $WebHost only match if there's nothing before it (requires 5.005). # Make $WebHost only match if there's nothing before it (requires 5.005).
$WebHost = ($] >= 5.005 ? '(?<![\w.])' : '' ) . $WebHost = ($] >= 5.005 ? '(?<![\w.])' : '' ) .
   '(?:[Ww][Ww][Ww]\d?|[Ww][Ee][Bb]\d?|[Mm][Ee][Mm][Bb][Ee][Rr][Ss]' .    '(?:[Ww][Ww][Ww]\d?|[Ww][Ee][Bb]\d?|[Mm][Ee][Mm][Bb][Ee][Rr][Ss]' .
   '|[Uu][Ss][Ee][Rr][Ss]?|[Hh][Oo][Mm][Ee])';    '|[Uu][Ss][Ee][Rr][Ss]?|[Hh][Oo][Mm][Ee])';
$HTTP = '(?:www\.|https?:\/\/)'; $HTTP = '(?:www\.|https?:\/\/)';
$HOST = '[\w\-.]+'; # characters for the hostname $HOST = '[\w\-.]+'; # characters for the hostname
$PORT = '(?::\d+)?'; # always optional $PORT = '(?::\d+)?'; # always optional
$Hostname = '^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]+[a-zA-Z0-9])'. $Hostname = '^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]+[a-zA-Z0-9])'.
           '(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]+[a-zA-Z0-9]))+)';            '(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]+[a-zA-Z0-9]))+)';
   
$url = $HTTP.$HOST.'|'.$WebHost . $HOST .'\.'. "$TLDs|$HTTP(?:$IP|$StealthIP)"; $url = $HTTP.$HOST.'|'.$WebHost . $HOST .'\.'. "$TLDs|$HTTP(?:$IP|$StealthIP)";
# requires the http:// part but accepts any hostname or tld, or IP-based url. # requires the http:// part but accepts any hostname or tld, or IP-based url.
$url2 = "$HTTP(?:$HOST|$IP|$StealthIP)"; $url2 = "$HTTP(?:$HOST|$IP|$StealthIP)";
# http:// is optional if $WebHost matches, any tld accepted. # http:// is optional if $WebHost matches, any tld accepted.
$url3 = "(?:$HTTP|$WebHost)$HOST"; $url3 = "(?:$HTTP|$WebHost)$HOST";
$stealthURL = "$HTTP(?:$IP|$StealthIP)"; $stealthURL = "$HTTP(?:$IP|$StealthIP)";
$simpleURL = $HTTP . $HOST . $PORT . '(?:\/[^\s<>]+)?'; $simpleURL = $HTTP . $HOST . $PORT . '(?:\/[^\s<>]+)?';
# match the whole URL including path (used by body_urls() ) # match the whole URL including path (used by body_urls() )
$fullURL = $HTTP . $HOST . $PORT . '(?:\/[^\s<>]+)?'. $fullURL = $HTTP . $HOST . $PORT . '(?:\/[^\s<>]+)?'.
   "|(?:$WebHost\.$HOST\.$TLDs)$PORT" . '(?:\/[^\s<>]+)?';    "|(?:$WebHost\.$HOST\.$TLDs)$PORT" . '(?:\/[^\s<>]+)?';
$spacecom = 'www[^a-z\.]+[a-zA-Z0-9\-]+[^a-z\.]+com'; $spacecom = 'www[^a-z\.]+[a-zA-Z0-9\-]+[^a-z\.]+com';
# URL redirection services (Commonly used to obfuscate spam links). # URL redirection services (Commonly used to obfuscate spam links).
#$shorturl = 'tinyurl\.com|useurl\.us|linkurl\.r8\.org|urlbrief\.com'. #$shorturl = 'tinyurl\.com|useurl\.us|linkurl\.r8\.org|urlbrief\.com'.
#            '|sites\.google\.com/site/|groups\.google\.com/group/'. #            '|sites\.google\.com/site/|groups\.google\.com/group/'.
#            '|6x\.to|liteurl\.com|xurl\.jp|c4\.to|da\.ru|beam\.to|doiop\.com'. #            '|6x\.to|liteurl\.com|xurl\.jp|c4\.to|da\.ru|beam\.to|doiop\.com'.
#            '|kickme\.to|end\.at|has\.it|hotshorturl\.com'; #            '|kickme\.to|end\.at|has\.it|hotshorturl\.com';
   
# for the scoring filter # for the scoring filter
$sex = 'sex|xxx|fuck'; $sex = 'sex|xxx|fuck';
$free = 'free(?!dom|bsd|ppp|xp)'; $free = 'free(?!dom|bsd|ppp|xp)';
$pics = 'pi(?:c|x)'; $pics = 'pi(?:c|x)';
$drugs = 'ativan|carisoprodol|caverta|\bcialis|diazepam|kamagra|levitra|nizoral'. $drugs = 'ativan|carisoprodol|caverta|\bcialis|diazepam|kamagra|levitra|nizoral'.
        'sildenafil|tadalafil|valium|viagra|vicodin|xanax';         'sildenafil|tadalafil|valium|viagra|vicodin|xanax';
# US Phone 800-123-1234  UK Phone 09096-400141 # US Phone 800-123-1234  UK Phone 09096-400141
$phone = '\b[89]\d{2}[\.\s\-_*\)]*\d{2,3}[\.\s\-_*]*\d{4}\b'. $phone = '\b[89]\d{2}[\.\s\-_*\)]*\d{2,3}[\.\s\-_*]*\d{4}\b'.
        '|[\s\-_*0][89]\d{2,3}[\s\-_*]+\d{6}';         '|[\s\-_*0][89]\d{2,3}[\s\-_*]+\d{6}';
$desc1 = "hard.?core|teen|asian|extreme|live|outrageous|nasty|awesome|$free|adult"; $desc1 = "hard.?core|teen|asian|extreme|live|outrageous|nasty|awesome|$free|adult";
# ASCII Symbols, excluding space/tab. # ASCII Symbols, excluding space/tab.
$SYMBOL = '[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]'; $SYMBOL = '[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]';
$hws = '(?:[\t ])'; # Horizontal whitespace. (From Perl 5.10, this can be \h.) $hws = '(?:[\t ])'; # Horizontal whitespace. (From Perl 5.10, this can be \h.)
$messageid_re = messageid_re(); $messageid_re = messageid_re();
   
# Re-enable the following when we fix the false positives in the scoring section. # Re-enable the following when we fix the false positives in the scoring section.
#$site_desc = "$desc1|password"; #$site_desc = "$desc1|password";
#$servPre = "(?:$free|cheap|unlimited|nationwide|$site_desc)"; #$servPre = "(?:$free|cheap|unlimited|nationwide|$site_desc)";
#$servPost = '(?:$free|minute|samples|800|900|no.?charge)'; #$servPost = '(?:$free|minute|samples|800|900|no.?charge)';
#$servStr = "(?:phone.{0,15}(?:$sex|fun)|(?:adult|r.?a.?p.?e|$sex).{0,10}(?:chat|site)". #$servStr = "(?:phone.{0,15}(?:$sex|fun)|(?:adult|r.?a.?p.?e|$sex).{0,10}(?:chat|site)".
#    "|(?:$sex).{0,15}(?:show|call|connection|vid(?:eo|s)|dvd)". #    "|(?:$sex).{0,15}(?:show|call|connection|vid(?:eo|s)|dvd)".
#    '|hard.?core.(?:vid(?:eo|s)|dvd|amateur)|900.dateline|(?:mass|bulk).e?-?mail)'; #    '|hard.?core.(?:vid(?:eo|s)|dvd|amateur)|900.dateline|(?:mass|bulk).e?-?mail)';
#$services = "(?:$servPre.{0,30}?$servStr)|(?:$servStr.{0,30}?$servPost)"; #$services = "(?:$servPre.{0,30}?$servStr)|(?:$servStr.{0,30}?$servPost)";
   
$free_stuff = "$free.{0,20}(?:password|membership|$pics|chat)". $free_stuff = "$free.{0,20}(?:password|membership|$pics|chat)".
   "|(?:100\%|total|complete|absolut|all).{0,15}$free".    "|(?:100\%|total|complete|absolut|all).{0,15}$free".
   '|no.{0,6}(a(?:ge|dult).(?:verification|check)|avs)';    '|no.{0,6}(a(?:ge|dult).(?:verification|check)|avs)';
   
$sex_adjs = "$desc1|$sex|erotic|gay|amateur|lesbian|blow.?job|fetish". $sex_adjs = "$desc1|$sex|erotic|gay|amateur|lesbian|blow.?job|fetish".
   '|pre.?teen|nude|celeb|school.?girl|bondage|rape|torture';    '|pre.?teen|nude|celeb|school.?girl|bondage|rape|torture';
$porn = "(?:$sex_adjs).{0,25}(?:$pics|video|dvd|image|porn|photo|mpeg)"; $porn = "(?:$sex_adjs).{0,25}(?:$pics|video|dvd|image|porn|photo|mpeg)";
   
$one_point_words = "pre.?teen|\bteen|\bsex|credit|amateur|horne?y". $one_point_words = "pre.?teen|\bteen|\bsex|credit|amateur|horne?y".
   '|anal(?!yst)|oral|bondage|breast|vid(?:eo|s)|dvd|kink(?:y|ier)|mistress'.    '|anal(?!yst)|oral|bondage|breast|vid(?:eo|s)|dvd|kink(?:y|ier)|mistress'.
   '|orgy|erotic|porn|fetish|whore|nympho|sucking|password|membership'.    '|orgy|erotic|porn|fetish|whore|nympho|sucking|password|membership'.
   '|make.money|fast.cash|orgasm|incest|extreme\b|raunchy|panties|bang'.    '|make.money|fast.cash|orgasm|incest|extreme\b|raunchy|panties|bang'.
   '|taboo|muff\b|(?:young|cute|school).?girl|nasty|torture'.    '|taboo|muff\b|(?:young|cute|school).?girl|nasty|torture'.
   'suduc(?:e|tress)|cuckold|wicked|unlimited|masterbat';    'suduc(?:e|tress)|cuckold|wicked|unlimited|masterbat';
$two_point_words = "$drugs". $two_point_words = "$drugs".
   '|fuck|sluts|puss(?:y|ies)|\bcum|(?:hidden|live|free|dorm|spy).?cam'.    '|fuck|sluts|puss(?:y|ies)|\bcum|(?:hidden|live|free|dorm|spy).?cam'.
   '|le[sz]b(?:ian|o)|\btits?\b|dick(?!.?berg)|blow.?job|cock|clit'.    '|le[sz]b(?:ian|o)|\btits?\b|dick(?!.?berg)|blow.?job|cock|clit'.
   '|twat|cunt|hard-?core|[^x]xxx|facial|gangbang|strap.on|submissive'.    '|twat|cunt|hard-?core|[^x]xxx|facial|gangbang|strap.on|submissive'.
   '|(?:live|real|innocent).girl|phone.{0,5}(?:sex|fun|play)|she.?male'.    '|(?:live|real|innocent).girl|phone.{0,5}(?:sex|fun|play)|she.?male'.
   '|lolita|dildo|whore|fingering|barely.?(?:18|legal)|[gm]ilf|no.?limit'.    '|lolita|dildo|whore|fingering|barely.?(?:18|legal)|[gm]ilf|no.?limit'.
   '|uncensored|age.?play|pedo\b|pedophile';    '|uncensored|age.?play|pedo\b|pedophile';
   
# assorted spamware names found in X-Newreader/X-Mailer/etc headers # assorted spamware names found in X-Newreader/X-Mailer/etc headers
$Xbot = '^2\.\d\.(?:\d\d? [a-z]|\d\d?)$|newsgroup bulk mailer' $Xbot = '^2\.\d\.(?:\d\d? [a-z]|\d\d?)$|newsgroup bulk mailer'
   . '|calvacade *98|atomicpost|uncle *spam'    . '|calvacade *98|atomicpost|uncle *spam'
   . '|metanews \d|metapost|ng post|girlsdeluxe|usenet replayer'    . '|metanews \d|metapost|ng post|girlsdeluxe|usenet replayer'
   . '|express news poster|^superpost auto marketer';    . '|express news poster|^superpost auto marketer';
   
############################################################################## ##############################################################################
   
get_config(); get_config();
setup_stuff(); setup_stuff();
   
# is this a reload? # is this a reload?
if (defined $Start_Time) { if (defined $Start_Time) {
   writestats(1) if $MODE eq 'inn';        # write the stats file    writestats(1) if $MODE eq 'inn';        # write the stats file
} else { } else {
   restore_emp() if $config{do_emp_dump};  # load the saved state    restore_emp() if $config{do_emp_dump};  # load the saved state
   $Start_Time = time;    $Start_Time = time;
} }
   
  # Optional policy tables populated by cleanfeed.local.  
  # Keys are Perl regular expressions; values are hash references.  
  %Peer_Policies = () unless %Peer_Policies;  
  %Hierarchy_Policies = () unless %Hierarchy_Policies;  
  %policy_rule_count = ();  
  %policy_peer_count = ();  
  %policy_hierarchy_count = ();  
  %top_peer_reject = () unless %top_peer_reject;  
  %top_host_reject = () unless %top_host_reject;  
  %top_group_reject = () unless %top_group_reject;  
  %rate_peer = () unless %rate_peer;  
  %rate_host = () unless %rate_host;  
  %Bad_File_Mtime = () unless %Bad_File_Mtime;  
  $Last_Bad_Mtime_Check = 0 unless defined $Last_Bad_Mtime_Check;  
  $Config_Fingerprint = '' unless defined $Config_Fingerprint;  
  $Last_Metrics_CSV = time unless defined $Last_Metrics_CSV;  
   
$Last_Trim = time unless defined $Last_Trim; $Last_Trim = time unless defined $Last_Trim;
$Last_Stats = time unless defined $Last_Stats; $Last_Stats = time unless defined $Last_Stats;
$Do_Log = 0; $Do_Log = 0;
   
############################################################################## ##############################################################################
# end of the initialization code # end of the initialization code
############################################################################## ##############################################################################
   
# Set things up after we've got our configuration. # Set things up after we've got our configuration.
  sub validate_configuration {  
     my @errors;  
   
     my %boolean = map { $_ => 1 } qw(  
         verbose aggressive block_binaries block_all_binaries block_late_cancels  
         block_user_spamcancels block_user_cancels block_extra_reposts do_md5  
         do_phl do_phn do_phr do_fsl do_scoring_filter do_ratio_scoring  
         do_emp_dump fuzzy_md5 md5_skips_followups phn_aggressive phr_aggressive  
         do_mid_filter do_supersedes_filter drop_useless_controls drop_ihave_sendme  
         binaries_in_mod_groups block_mime_html block_html_multipart block_html  
         block_html_images log_accepts inn_syslog_status timer_info  
         detect_mime_binaries detect_malformed_yenc validate_config policy_enabled  
         policy_default_allow_binary policy_log_matches policy_log_accepts  
         policy_include_message_id policy_include_peer policy_include_groups  
         metrics_enabled metrics_syslog metrics_by_rule metrics_by_peer  
         metrics_by_hierarchy malformed_encoding_check binary_byte_profile_enabled  
         path_sanity_enabled anomaly_rate_enabled top_offenders_enabled  
         config_fingerprint_enabled idn_normalization external_regex_reject_unsafe  
         trusted_bypass_scoring trusted_bypass_content trusted_bypass_binary  
         trusted_bypass_crosspost trusted_bypass_size  
     );  
     for my $key (keys %boolean) {  
         push @errors, "$key must be 0 or 1"  
             if defined $config{$key} and $config{$key} !~ /\A[01]\z/;  
     }  
   
     my %nonnegative = map { $_ => 1 } qw(  
         maxgroups bad_url_score trim_interval stats_interval MIDmaxlife  
         bad_rate_reload low_xpost_maxgroups meow_ext_maxgroups  
         off_topic1_maxgroups on_topic1_maxgroups on_topic1_mingroups  
         off_topic2_maxgroups on_topic2_maxgroups on_topic2_mingroups  
         max_base64_lines max_log_size keep_old_logs debug_batch_size  
         fuzzy_max_length md5_max_length binary_scan_bytes binary_scan_tail_bytes  
         body_preview_bytes study_max_lines MD5RateCutoff MD5RateCeiling  
         MD5RateBaseInterval PHLRateCutoff PHLRateCeiling PHLRateBaseInterval  
         PHNRateCutoff PHNRateCeiling PHNRateBaseInterval PHRRateCutoff  
         PHRRateCeiling PHRRateBaseInterval FSLRateCutoff FSLRateCeiling  
         FSLRateBaseInterval supersedes_window supersedes_ceiling supersedes_max_faq  
         supersedes_max_unmoderated supersedes_max_moderated  
         supersedes_max_all_moderated supersedes_max_unknown_active  
         policy_default_max_bytes policy_max_peer_counters  
         policy_max_hierarchy_counters metrics_csv_interval bad_reload_interval  
         external_regex_max_length external_regex_max_count external_regex_body_bytes  
         max_header_line_bytes max_body_line_bytes yenc_size_tolerance_percent  
         binary_nonprintable_ratio_percent binary_ratio_min_bytes binary_ratio_scan_bytes  
         path_max_hops path_max_token_bytes path_repeat_ceiling anomaly_window  
         anomaly_peer_articles anomaly_host_articles anomaly_max_keys  
         top_offenders_limit top_offenders_max_keys  
     );  
     for my $key (keys %nonnegative) {  
         push @errors, "$key must be a non-negative integer"  
             if defined $config{$key} and $config{$key} !~ /\A\d+\z/;  
     }  
   
     for my $prefix (qw(MD5 PHL PHN PHR FSL)) {  
         my ($cut, $ceil) = @config{"${prefix}RateCutoff", "${prefix}RateCeiling"};  
         push @errors, "${prefix}RateCutoff must not exceed ${prefix}RateCeiling"  
             if defined $cut and defined $ceil and $cut > $ceil;  
     }  
   
     my @regex_keys = qw(  
         bin_allowed image_allowed image_extensions bad_bin md5exclude  
         poison_groups allexclude score_exclude mime_html_allowed html_allowed  
         test_groups low_xpost_groups meow_groups topic1_groups topic2_groups  
         no_cancel_groups baddomainpat fsl_exclude phl_exclude phn_exclude  
         bad_nph_hosts phl_exempt phn_exempt phr_exempt ratio_exclude flood_groups  
         supersedes_exempt refuse_messageids spam_report_groups adult_groups  
         not_adult_groups faq_groups local_approved_groups  
     );  
     for my $key (@regex_keys) {  
         next if not defined $config{$key} or $config{$key} eq '';  
         my $ok = eval { qr/$config{$key}/; 1 };  
         push @errors, "$key contains an invalid regular expression: $@" if not $ok;  
     }  
     for my $key (keys %Restricted_Groups) {  
         my $ok = eval { qr/$Restricted_Groups{$key}/; 1 };  
         push @errors, "Restricted_Groups{$key} is invalid: $@" if not $ok;  
     }  
   
   
     for my $pair (  
         ['bad_reload_mode', qr/\A(?:mtime|articles|both)\z/],  
         ['long_line_mode', qr/\A(?:off|audit|reject)\z/],  
         ['malformed_encoding_mode', qr/\A(?:off|audit|reject)\z/],  
         ['binary_byte_profile_mode', qr/\A(?:off|audit|reject)\z/],  
         ['path_sanity_mode', qr/\A(?:off|audit|reject)\z/],  
         ['anomaly_mode', qr/\A(?:off|audit|reject)\z/],  
     ) {  
         my ($key, $valid) = @$pair;  
         push @errors, "$key contains an invalid mode" if defined $config{$key} and $config{$key} !~ $valid;  
     }  
     push @errors, 'binary_nonprintable_ratio_percent must be between 0 and 100'  
         if $config{binary_nonprintable_ratio_percent} > 100;  
   
     push @errors, 'supersedes_mode must be off, audit, or reject'  
         if defined $config{supersedes_mode}  
             and $config{supersedes_mode} !~ /\A(?:off|audit|reject)\z/;  
   
     push @errors, 'policy_mode must be off, audit, quarantine, or reject'  
         if defined $config{policy_mode}  
             and $config{policy_mode} !~ /\A(?:off|audit|quarantine|reject)\z/;  
   
     for my $table_name (['Peer_Policies', \%Peer_Policies],  
                         ['Hierarchy_Policies', \%Hierarchy_Policies]) {  
         my ($name, $table) = @$table_name;  
         for my $pattern (keys %$table) {  
             my $ok = eval { qr/$pattern/; 1 };  
             push @errors, "$name pattern '$pattern' is invalid: $@" if not $ok;  
             push @errors, "$name entry '$pattern' must be a hash reference"  
                 if ref($table->{$pattern}) ne 'HASH';  
             next if ref($table->{$pattern}) ne 'HASH';  
             my $entry = $table->{$pattern};  
             push @errors, "$name entry '$pattern' mode is invalid"  
                 if defined $entry->{mode}  
                     and $entry->{mode} !~ /\A(?:off|audit|quarantine|reject)\z/;  
             push @errors, "$name entry '$pattern' max_bytes must be a non-negative integer"  
                 if defined $entry->{max_bytes} and $entry->{max_bytes} !~ /\A\d+\z/;  
             push @errors, "$name entry '$pattern' allow_binary must be 0 or 1"  
                 if defined $entry->{allow_binary} and $entry->{allow_binary} !~ /\A[01]\z/;  
         }  
     }  
   
     if (@errors) {  
         $Local_Conf_Err = 1;  
         slog('E', "Configuration validation failed: $_") for @errors;  
     }  
     return @errors ? 0 : 1;  
  }  
   
sub setup_stuff { sub setup_stuff {
   # Try to load up MD5 module (use Digest::MD5, but old MD5 still works).    # Try to load up MD5 module (use Digest::MD5, but old MD5 still works).
   if ($config{do_md5}) {    if ($config{do_md5}) {
       eval { require Digest::MD5; import Digest::MD5 qw(md5_hex); };        eval { require Digest::MD5; import Digest::MD5 qw(md5_hex); };
       if ($@) {        if ($@) {
           undef $config{do_md5};            undef $config{do_md5};
           slog('E', 'Cannot load MD5: ' . $@);            slog('E', 'Cannot load MD5: ' . $@);
       }        }
   } else {    } else {
       undef $config{do_md5};        undef $config{do_md5};
   }    }
   
   # Try to load up Data::Dumper if we want to save the EMP histories.    # Try to load up Data::Dumper if we want to save the EMP histories.
   if ($config{do_emp_dump}) {    if ($config{do_emp_dump}) {
       eval { require Data::Dumper; };        eval { require Data::Dumper; };
       if ($@) {        if ($@) {
           undef $config{do_emp_dump};            undef $config{do_emp_dump};
           slog('E', 'Cannot load Data::Dumper: ' . $@);            slog('E', 'Cannot load Data::Dumper: ' . $@);
       }        }
   }    }
   
   # Load up IO::File if we want logging.    # Load up IO::File if we want logging.
   if ($Log_File) {    if ($Log_File) {
       eval { require IO::File; };        eval { require IO::File; };
       if ($@) {        if ($@) {
           undef $Log_File;            undef $Log_File;
           slog('E', 'Cannot load IO::File: ' . $@);            slog('E', 'Cannot load IO::File: ' . $@);
       }        }
   }    }
   
   # Read all the bad_* files    # Read all the bad_* files
   read_hashes();    read_hashes();
   
   #Initialise status counters    #Initialise status counters
   %status = ();    %status = ();
   $status{accepted} = 0;    $status{accepted} = 0;
   $status{rejected} = 0;    $status{rejected} = 0;
   $status{refused} = 0;    $status{refused} = 0;
   $status{do_mid_filter} = 0;    $status{do_mid_filter} = 0;
     $status{audited} = 0;  
     $status{quarantined} = 0;  
   
   # initialise the rate filters    # initialise the rate filters
   if ($config{do_md5}) {    if ($config{do_md5}) {
       $MD5history = new Cleanfeed::RateLimit;        $MD5history = new Cleanfeed::RateLimit;
       $MD5history->init($config{MD5RateCutoff}, $config{MD5RateCeiling},        $MD5history->init($config{MD5RateCutoff}, $config{MD5RateCeiling},
           $config{MD5RateBaseInterval});            $config{MD5RateBaseInterval});
   } else {    } else {
       undef $MD5history;        undef $MD5history;
   }    }
   if ($config{do_phl}) {    if ($config{do_phl}) {
       $PHLhistory = new Cleanfeed::RateLimit;        $PHLhistory = new Cleanfeed::RateLimit;
       $PHLhistory->init($config{PHLRateCutoff}, $config{PHLRateCeiling},        $PHLhistory->init($config{PHLRateCutoff}, $config{PHLRateCeiling},
           $config{PHLRateBaseInterval});            $config{PHLRateBaseInterval});
   } else {    } else {
       undef $PHLhistory;        undef $PHLhistory;
   }    }
   if ($config{do_phn}) {    if ($config{do_phn}) {
       $PHNhistory = new Cleanfeed::RateLimit;        $PHNhistory = new Cleanfeed::RateLimit;
       $PHNhistory->init($config{PHNRateCutoff}, $config{PHNRateCeiling},        $PHNhistory->init($config{PHNRateCutoff}, $config{PHNRateCeiling},
           $config{PHNRateBaseInterval});            $config{PHNRateBaseInterval});
   } else {    } else {
       undef $PHNhistory;        undef $PHNhistory;
   }    }
   if ($config{do_phr}) {    if ($config{do_phr}) {
       $PHRhistory = new Cleanfeed::RateLimit;        $PHRhistory = new Cleanfeed::RateLimit;
       $PHRhistory->init($config{PHRRateCutoff}, $config{PHRRateCeiling},        $PHRhistory->init($config{PHRRateCutoff}, $config{PHRRateCeiling},
           $config{PHRRateBaseInterval});            $config{PHRRateBaseInterval});
   } else {    } else {
       undef $PHRhistory;        undef $PHRhistory;
   }    }
   if ($config{do_fsl}) {    if ($config{do_fsl}) {
       $FSLhistory = new Cleanfeed::RateLimit;        $FSLhistory = new Cleanfeed::RateLimit;
       $FSLhistory->init($config{FSLRateCutoff}, $config{FSLRateCeiling},        $FSLhistory->init($config{FSLRateCutoff}, $config{FSLRateCeiling},
           $config{FSLRateBaseInterval});            $config{FSLRateBaseInterval});
   } else {    } else {
       undef $FSLhistory;        undef $FSLhistory;
   }    }
   if ($config{do_supersedes_filter}) {    if ($config{do_supersedes_filter}) {
       $Suphistory = new Cleanfeed::RateLimit;        $Suphistory = new Cleanfeed::RateLimit;
       $Suphistory->init(0, 50, 900);         $Suphistory->init(0, $config{supersedes_ceiling}, $config{supersedes_window}); 
   }    }
   
   $MIDhistory = new Cleanfeed::Queue;    $MIDhistory = new Cleanfeed::Queue;
   $MIDhistory->maxlife($config{MIDmaxlife} * 3600) if $config{MIDmaxlife};    $MIDhistory->maxlife($config{MIDmaxlife} * 3600) if $config{MIDmaxlife};
   
   $timer{time} = time if $config{timer_info} and not $timer{time};    $timer{time} = time if $config{timer_info} and not $timer{time};
} }
   
sub filter_art { sub filter_art {
   $now = time;    $now = time;
   undef $body;    undef $body;
   undef $score;           # String representation of article score    undef $score;           # String representation of article score
   %state = ();            # Initialize our state hash    %state = ();            # Initialize our state hash
   ##### State Keys #####    ##### State Keys #####
   # body_has_url;     Bool:   True if body contains a URL    # body_has_url;     Bool:   True if body contains a URL
   # file_extension;   Str:    File extension on yEnc or UUencoded binaries    # file_extension;   Str:    File extension on yEnc or UUencoded binaries
   # cache_is_binary;  Flag:   True if content is Binary    # cache_is_binary;  Flag:   True if content is Binary
   # localfeed;        Flag:   True if article source is local    # localfeed;        Flag:   True if article source is local
   # spamsource;       Flag:   True if article origin is spam friendly    # spamsource;       Flag:   True if article origin is spam friendly
   # xreader;          Str:    Newsreader application, if specified    # xreader;          Str:    Newsreader application, if specified
   # scoreval;         Int:    Numeric representation of article score    # scoreval;         Int:    Numeric representation of article score
   # urlcount;         Int:    Number of URL's in an article    # urlcount;         Int:    Number of URL's in an article
   # charset;          Str:    Article charset (if defined in Content-Type)    # charset;          Str:    Article charset (if defined in Content-Type)
   # posting_host:     Str:    Posting host/address    # posting_host:     Str:    Posting host/address
   # injection_host    Str:    Injection host    # injection_host    Str:    Injection host
   # grpcnt:           Int:    Count of groups in Newsgroups header    # grpcnt:           Int:    Count of groups in Newsgroups header
   # fupcnt            Int:    Count of groups in Followup-To header    # fupcnt            Int:    Count of groups in Followup-To header
   # grpfupcnt:        Int:    Count of combined grpcnt and fupcnt (Unique)    # grpfupcnt:        Int:    Count of combined grpcnt and fupcnt (Unique)
   # lines             Int:    Number of lines reported by inn    # lines             Int:    Number of lines reported by inn
   # letters           Int:    Number of letters in the post [a-zA-Z]    # letters           Int:    Number of letters in the post [a-zA-Z]
   # uppercase         Int:    Number of uppercase letters in the post [A-Z]    # uppercase         Int:    Number of uppercase letters in the post [A-Z]
   # symbols           Int:    Number of symbols [!"#$%&'()*+,-:;=?@{|}~]    # symbols           Int:    Number of symbols [!"#$%&'()*+,-:;=?@{|}~]
   
   $status{articles}++;    $status{articles}++;
   $timer{articles}++ if $config{timer_info};    $timer{articles}++ if $config{timer_info};
   
   # count the lines in the article - late-model INN does this for us.    # count the lines in the article - late-model INN does this for us.
   if (defined $hdr{__LINES__}) {    if (defined $hdr{__LINES__}) {
       $state{lines} = $hdr{__LINES__};        $state{lines} = $hdr{__LINES__};
   } else {    } else {
       $state{lines} = ($hdr{__BODY__} =~ tr/\n//);        $state{lines} = ($hdr{__BODY__} =~ tr/\n//);
   };    };
   $lines = $state{lines}; # TODO Remove after grace period.     $lines = $state{lines}; # Legacy alias retained for local hooks. 
   
   # Study the message BODY.  This used to be in the local config file but    # Study the message BODY.  This used to be in the local config file but
   # this seems more logical.    # this seems more logical.
   # TODO: Make the number of lines configurable.    
   if (not $config{nobase64}    if (not $config{nobase64}
   and $hdr{'Content-Transfer-Encoding'} =~ /^base64$/i    and $hdr{'Content-Transfer-Encoding'} =~ /^base64$/i
   and $hdr{'Content-Type'} =~ /^text/i) {    and $hdr{'Content-Type'} =~ /^text/i) {
       $body = lc substr(MIME::Base64::decode($hdr{__BODY__}), 0, 4000);         $body = lc substr(MIME::Base64::decode($hdr{__BODY__} || ''), 0, $config{body_preview_bytes}); 
   } else {    } else {
       $body = lc substr($hdr{__BODY__}, 0, 4000);         $body = lc substr($hdr{__BODY__} || '', 0, $config{body_preview_bytes}); 
       study $hdr{__BODY__} if $state{lines} <= 250;         study $hdr{__BODY__} if $config{study_max_lines} 
             and $state{lines} <= $config{study_max_lines};  
   };    };
   
   # Reload the bad_* files every $bad_rate_reload articles accepted     maybe_reload_bad_files(); 
   if ($status{accepted} > 0 and $config{bad_rate_reload} > 0    
       and $status{accepted} % $config{bad_rate_reload} == 0    
       and $status{accepted} > $status{bad_reloaded}) {    
       slog('N', "Reloading bad files after $status{accepted} articles");    
       read_hashes();    
       # Prevent looping whilst waiting for another accepted article    
       $status{bad_reloaded} = $status{accepted};    
   };    
   
   # Try and ascertain the source news service.    # Try and ascertain the source news service.
   if ($hdr{'Injection-Info'} =~ /^$hws*($Hostname)[ \t;]/) {    if ($hdr{'Injection-Info'} =~ /^$hws*($Hostname)[ \t;]/) {
       $state{injection_host} = "$1"        $state{injection_host} = "$1"
   } elsif ($hdr{'X-Trace'} =~ /^$hws*($Hostname)$hws/) {    } elsif ($hdr{'X-Trace'} =~ /^$hws*($Hostname)$hws/) {
       $state{injection_host} = "$1"        $state{injection_host} = "$1"
   } else {    } else {
       $state{injection_host} = first_path_host($hdr{Path});        $state{injection_host} = first_path_host($hdr{Path});
   };    };
   
   # Try and ascertain the Posting-Host info    # Try and ascertain the Posting-Host info
   if ($hdr{'Injection-Info'} =~ /posting-host$hws*=$hws*"?([^";]+)/) {    if ($hdr{'Injection-Info'} =~ /posting-host$hws*=$hws*"?([^";]+)/) {
       $state{posting_host} = "$1";        $state{posting_host} = "$1";
   } elsif ($hdr{'NNTP-Posting-Host'}) {    } elsif ($hdr{'NNTP-Posting-Host'}) {
       $state{posting_host} = $hdr{'NNTP-Posting-Host'};        $state{posting_host} = $hdr{'NNTP-Posting-Host'};
   } else {    } else {
       $state{posting_host} = 0;        $state{posting_host} = 0;
   };    };
     $state{posting_host} = normalize_domain($state{posting_host}) if $state{posting_host};  
     $state{injection_host} = normalize_domain($state{injection_host}) if $state{injection_host};  
   
   # Provide a user-defined function for tagging posts that are fed from    # Provide a user-defined function for tagging posts that are fed from
   # sources the operator considers local.  Note: This includes, but is    # sources the operator considers local.  Note: This includes, but is
   # not limited to articles received via nnrpd.    # not limited to articles received via nnrpd.
   if (defined &local_flag_localfeed) {    if (defined &local_flag_localfeed) {
       $state{localfeed} = local_flag_localfeed();        $state{localfeed} = local_flag_localfeed();
   };    };
   
   # Provide a user-defined function for tagging posts that are fed from    # Provide a user-defined function for tagging posts that are fed from
   # sources the operator considers spam friendly.  These could easily be    # sources the operator considers spam friendly.  These could easily be
   # hardcoded but doing so might get me kneecapped.    # hardcoded but doing so might get me kneecapped.
   if (defined &local_flag_spamsource) {    if (defined &local_flag_spamsource) {
       $state{spamsource} = local_flag_spamsource();        $state{spamsource} = local_flag_spamsource();
   };    };
   
   # break out newsgroups into an array    # break out newsgroups into an array
   @groups = sort(split(/[,\s]+/, $hdr{Newsgroups}));    @groups = sort(split(/[,\s]+/, $hdr{Newsgroups}));
   if ($hdr{'Followup-To'}) {    if ($hdr{'Followup-To'}) {
       @followups = split(/[,\s]+/, $hdr{'Followup-To'});        @followups = split(/[,\s]+/, $hdr{'Followup-To'});
   } else {    } else {
       @followups = @groups;        @followups = @groups;
   };    };
   
   # Produce a Newsgroups string sorted alphanumerically.  This is    # Produce a Newsgroups string sorted alphanumerically.  This is
   # useful for filters on Newsgroups where spammers manipulate the    # useful for filters on Newsgroups where spammers manipulate the
   # distribution in order to generate non-colliding hashes.    # distribution in order to generate non-colliding hashes.
   my $sortgrps = join(',', @groups);    my $sortgrps = join(',', @groups);
   
   # Create a merged array of groups and followups with no duplicates.    # Create a merged array of groups and followups with no duplicates.
   # This is useful in instances where we consider follow-up groups to be    # This is useful in instances where we consider follow-up groups to be
   # part of the distribution.    # part of the distribution.
   my %merge = ();    my %merge = ();
   my @grpfup = grep { ! $merge{ $_ }++ } (@groups, @followups);    my @grpfup = grep { ! $merge{ $_ }++ } (@groups, @followups);
   
   # Count the number of groups in the distribution    # Count the number of groups in the distribution
   $state{grpcnt} = scalar @groups;    $state{grpcnt} = scalar @groups;
   $state{fupcnt} = scalar @followups;    $state{fupcnt} = scalar @followups;
   $state{grpfupcnt} = scalar @grpfup;    $state{grpfupcnt} = scalar @grpfup;
     $state{article_bytes} = length($hdr{__BODY__} || '');  
     $state{peer} = policy_peer_identity();  
   
     my $guard_result = apply_lightweight_guards();  
     return $guard_result if defined $guard_result and $guard_result ne '';  
   
     if ($config{policy_enabled}) {  
         my $policy_result = apply_policy_engine();  
         return $policy_result if defined $policy_result and $policy_result ne '';  
     }  
   
   trimhashes() if $now - $Last_Trim >= $config{trim_interval};    trimhashes() if $now - $Last_Trim >= $config{trim_interval};
   writestats() if $now - $Last_Stats >= $config{stats_interval};    writestats() if $now - $Last_Stats >= $config{stats_interval};
   
   # Check out the Newsgroups to which the article is posted    # Check out the Newsgroups to which the article is posted
   %gr = ();    %gr = ();
   for (@groups) {    for (@groups) {
       foreach my $item (@Restricted_List) {        foreach my $item (@Restricted_List) {
           $gr{'rg_'.$item}++ if /$Restricted_Groups{$item}/;            $gr{'rg_'.$item}++ if /$Restricted_Groups{$item}/;
       }        }
       $gr{binary}++ if $config{bin_allowed} and /$config{bin_allowed}/o;        $gr{binary}++ if $config{bin_allowed} and /$config{bin_allowed}/o;
       $gr{image}++ if $config{image_allowed} and /$config{image_allowed}/o;        $gr{image}++ if $config{image_allowed} and /$config{image_allowed}/o;
       $gr{bad_bin}++ if $config{bad_bin} and /$config{bad_bin}/o;        $gr{bad_bin}++ if $config{bad_bin} and /$config{bad_bin}/o;
       $gr{html}++ if $config{html_allowed} and /$config{html_allowed}/o;        $gr{html}++ if $config{html_allowed} and /$config{html_allowed}/o;
       $gr{mime_html}++ if $config{mime_html_allowed}        $gr{mime_html}++ if $config{mime_html_allowed}
           and /$config{mime_html_allowed}/o;            and /$config{mime_html_allowed}/o;
       $gr{poison}++ if $config{poison_groups}        $gr{poison}++ if $config{poison_groups}
           and /$config{poison_groups}/o;            and /$config{poison_groups}/o;
       $gr{reports}++ if $config{spam_report_groups}        $gr{reports}++ if $config{spam_report_groups}
           and /$config{spam_report_groups}/o;            and /$config{spam_report_groups}/o;
       $gr{no_cancel}++ if $config{no_cancel_groups}        $gr{no_cancel}++ if $config{no_cancel_groups}
           and /$config{no_cancel_groups}/o;            and /$config{no_cancel_groups}/o;
       $gr{test}++ if /$config{test_groups}/o;        $gr{test}++ if /$config{test_groups}/o;
       $gr{adult}++ if /$config{adult_groups}/o        $gr{adult}++ if /$config{adult_groups}/o
           and not /$config{not_adult_groups}/o;            and not /$config{not_adult_groups}/o;
       $gr{faq}++ if /$config{faq_groups}/o;        $gr{faq}++ if /$config{faq_groups}/o;
       $gr{ratio}++ if /$config{ratio_exclude}/o;        $gr{ratio}++ if /$config{ratio_exclude}/o;
       $gr{approved}++ if /$config{local_approved_groups}/o;        $gr{approved}++ if /$config{local_approved_groups}/o;
       if ($config{active_file}) {        if ($config{active_file}) {
           $gr{mod}++ if $Moderated{$_};            $gr{mod}++ if $Moderated{$_};
       } elsif (defined &INN::newsgroup) {        } elsif (defined &INN::newsgroup) {
           $gr{mod}++ if INN::newsgroup($_) eq 'm';            $gr{mod}++ if INN::newsgroup($_) eq 'm';
       };        };
   
   };    };
   
   # As above but check Newsgroups and Followup-To headers    # As above but check Newsgroups and Followup-To headers
   for (@grpfup) {    for (@grpfup) {
       $gr{skip}++ if $config{allexclude} and /$config{allexclude}/o;        $gr{skip}++ if $config{allexclude} and /$config{allexclude}/o;
       $gr{fslskip}++ if $config{fslexclude} and /$config{fslexclude}/o;         $gr{fslskip}++ if $config{fsl_exclude} and /$config{fsl_exclude}/o; 
       $gr{md5skip}++ if $config{md5exclude} and /$config{md5exclude}/o;        $gr{md5skip}++ if $config{md5exclude} and /$config{md5exclude}/o;
       $gr{phnskip}++ if $config{phn_exclude} and /$config{phn_exclude}/o;        $gr{phnskip}++ if $config{phn_exclude} and /$config{phn_exclude}/o;
       $gr{phlskip}++ if $config{phl_exclude} and /$config{phl_exclude}/o;        $gr{phlskip}++ if $config{phl_exclude} and /$config{phl_exclude}/o;
       $gr{scoreskip}++ if $config{score_exclude}        $gr{scoreskip}++ if $config{score_exclude}
           and /$config{score_exclude}/o;            and /$config{score_exclude}/o;
       $gr{phrinc}++ if $config{flood_groups} and /$config{flood_groups}/o;        $gr{phrinc}++ if $config{flood_groups} and /$config{flood_groups}/o;
       $gr{low_xpost}++ if $config{low_xpost_groups}        $gr{low_xpost}++ if $config{low_xpost_groups}
           and /$config{low_xpost_groups}/o;            and /$config{low_xpost_groups}/o;
       $gr{meow}++ if $config{meow_groups}        $gr{meow}++ if $config{meow_groups}
           and /$config{meow_groups}/o;            and /$config{meow_groups}/o;
       $gr{topic1}++ if $config{topic1_groups}        $gr{topic1}++ if $config{topic1_groups}
           and /$config{topic1_groups}/o;            and /$config{topic1_groups}/o;
       $gr{topic2}++ if $config{topic2_groups}        $gr{topic2}++ if $config{topic2_groups}
           and /$config{topic2_groups}/o;            and /$config{topic2_groups}/o;
       $gr{localhier}++ if /^local\./;        $gr{localhier}++ if /^local\./;
   };    };
   
   # These only count if all Newsgroups match    # These only count if all Newsgroups match
   $gr{image} = (($gr{image} + $gr{binary}) >= $state{grpcnt});    $gr{image} = (($gr{image} + $gr{binary}) >= $state{grpcnt});
   $gr{binary} = ($gr{binary} == $state{grpcnt});    $gr{binary} = ($gr{binary} == $state{grpcnt});
   $gr{reports} = ($gr{reports} == $state{grpcnt});    $gr{reports} = ($gr{reports} == $state{grpcnt});
   $gr{binary} = 0 if $gr{bad_bin};    $gr{binary} = 0 if $gr{bad_bin};
   $gr{html} = ($gr{html} == $state{grpcnt});    $gr{html} = ($gr{html} == $state{grpcnt});
   $gr{mime_html} = ($gr{mime_html} == $state{grpcnt});    $gr{mime_html} = ($gr{mime_html} == $state{grpcnt});
   $gr{allmod} = ($gr{mod} == $state{grpcnt});    $gr{allmod} = ($gr{mod} == $state{grpcnt});
   $gr{alltest} = ($gr{test} == $state{grpcnt});    $gr{alltest} = ($gr{test} == $state{grpcnt});
   $gr{alladult} = ($gr{adult} == $state{grpcnt});    $gr{alladult} = ($gr{adult} == $state{grpcnt});
   $gr{ratio} = ($gr{ratio} == $state{grpcnt});    $gr{ratio} = ($gr{ratio} == $state{grpcnt});
   $gr{approved} = ($gr{approved} == $state{grpcnt});    $gr{approved} = ($gr{approved} == $state{grpcnt});
   
   # Same as above but include Followup_To in addition to Newsgroups    # Same as above but include Followup_To in addition to Newsgroups
   $gr{skip} = ($gr{skip} == $state{grpfupcnt});    $gr{skip} = ($gr{skip} == $state{grpfupcnt});
   $gr{fslskip} = ($gr{fslskip} == $state{grpfupcnt});    $gr{fslskip} = ($gr{fslskip} == $state{grpfupcnt});
   $gr{md5skip} = ($gr{md5skip} == $state{grpfupcnt});    $gr{md5skip} = ($gr{md5skip} == $state{grpfupcnt});
   $gr{phnskip} = ($gr{phnskip} == $state{grpfupcnt});    $gr{phnskip} = ($gr{phnskip} == $state{grpfupcnt});
   $gr{phlskip} = ($gr{phlskip} == $state{grpfupcnt});    $gr{phlskip} = ($gr{phlskip} == $state{grpfupcnt});
   $gr{scoreskip} = ($gr{scoreskip} == $state{grpfupcnt});    $gr{scoreskip} = ($gr{scoreskip} == $state{grpfupcnt});
   $gr{alllocal} = ($gr{localhier} == $state{grpfupcnt});    $gr{alllocal} = ($gr{localhier} == $state{grpfupcnt});
   
   # If all newsgroups are excluded from filtering, bail now    # If all newsgroups are excluded from filtering, bail now
   return '' if $gr{skip};    return '' if $gr{skip};
   
   foreach (@Restricted_List) {    foreach (@Restricted_List) {
       $gr{'rg_'.$_.'_only'} = ($gr{'rg_'.$_} == $state{grpcnt});        $gr{'rg_'.$_.'_only'} = ($gr{'rg_'.$_} == $state{grpcnt});
   }    }
   
   if ($hdr{'Content-Type'} =~ /charset="?([^";\s]+)/io) {    if ($hdr{'Content-Type'} =~ /charset="?([^";\s]+)/io) {
       $state{charset} = lc($1);        $state{charset} = lc($1);
   };    };
   
   # checks common to all article types #####################################    # checks common to all article types #####################################
   return reject("Bad host ($state{posting_host})", 'Bad site')    return reject("Bad host ($state{posting_host})", 'Bad site')
       if exists $Bad_Hosts{$state{posting_host}}        if exists $Bad_Hosts{$state{posting_host}}
       or exists $Bad_Hosts_Central{$state{posting_host}};        or exists $Bad_Hosts_Central{$state{posting_host}};
   
   @Path_Entries = split(/!/, $hdr{Path});    @Path_Entries = split(/!/, $hdr{Path});
   foreach (@Path_Entries) {    foreach (@Path_Entries) {
       return reject("Bad path ($_)", 'Bad site') if exists $Bad_Path{$_};        return reject("Bad path ($_)", 'Bad site') if exists $Bad_Path{$_};
   }    }
   
   # check for the most simple newsagent variations    # check for the most simple newsagent variations
   if ($hdr{'Message-ID'} =~    if ($hdr{'Message-ID'} =~
           /^<            /^<
               (?:cancel\.)*                (?:cancel\.)*
               [0-9A-F]{8,15}\.[a-z]{4,11}                [0-9A-F]{8,15}\.[a-z]{4,11}
               \@[a-z]{4,11}\.(?:net|mil|gov|org|edu|com)                \@[a-z]{4,11}\.(?:net|mil|gov|org|edu|com)
           >$/x) {            >$/x) {
       if ($hdr{'X-Cancelled-By'}) {        if ($hdr{'X-Cancelled-By'}) {
           return reject('Cancel for rejected article');            return reject('Cancel for rejected article');
       } else {        } else {
           return reject('NewsAgent', 'Bot signature');            return reject('NewsAgent', 'Bot signature');
       }        }
   }    }
   
   # This check should only trap articles in inn < 2.6. From that time,    # This check should only trap articles in inn < 2.6. From that time,
   # Message-ID validation to RFC5536 is integrated.    # Message-ID validation to RFC5536 is integrated.
   if ($hdr{'Message-ID'} !~ $messageid_re) {    if ($hdr{'Message-ID'} !~ $messageid_re) {
       return reject('Bad Message-ID', 'Invalid Header');        return reject('Bad Message-ID', 'Invalid Header');
   };    };
   
   return reject('NewsAgent (Path)')    return reject('NewsAgent (Path)')
       if $hdr{Path} =~ /\.(?:posted|mismatch)$/;        if $hdr{Path} =~ /\.(?:posted|mismatch)$/;
   
   # regular articles #######################################################    # regular articles #######################################################
   if (not $hdr{Control}) {    if (not $hdr{Control}) {
       # Good Usenet lines are terminated with CRLF        # Good Usenet lines are terminated with CRLF
       $state{badlines} = 0;        $state{badlines} = 0;
       $state{badlines}++ while $hdr{__BODY__} =~ /[^\r]\n/g;        $state{badlines}++ while $hdr{__BODY__} =~ /[^\r]\n/g;
       # lowercase some headers for later        # lowercase some headers for later
       undef %lch;        undef %lch;
       $lch{from}          = lc $hdr{From}        $lch{from}          = lc $hdr{From}
           || return reject('Malformed article');            || return reject('Malformed article');
       $lch{subject}       = lc $hdr{Subject}        $lch{subject}       = lc $hdr{Subject}
           || return reject('Malformed article');            || return reject('Malformed article');
       $lch{'message-id'}  = lc $hdr{'Message-ID'}        $lch{'message-id'}  = lc $hdr{'Message-ID'}
           || return reject('Malformed article');            || return reject('Malformed article');
       $lch{sender}        = lc $hdr{Sender} || '';        $lch{sender}        = lc $hdr{Sender} || '';
       $lch{organization}  = lc $hdr{Organization} || '';        $lch{organization}  = lc $hdr{Organization} || '';
       $lch{'content-type'}= lc $hdr{'Content-Type'} || '';        $lch{'content-type'}= lc $hdr{'Content-Type'} || '';
   
       if (defined &local_filter_first) {        if (defined &local_filter_first) {
           my @result = local_filter_first();            my @result = local_filter_first();
           return reject(@result) if $result[0];            return reject(@result) if $result[0];
       }        }
   
       # first thing, handle reposts ########################################        # first thing, handle reposts ########################################
       if ($config{block_extra_reposts} and $hdr{Subject} =~ /^REPOST: /        if ($config{block_extra_reposts} and $hdr{Subject} =~ /^REPOST: /
               and $hdr{Path} =~ /!resurrector!/) {                and $hdr{Path} =~ /!resurrector!/) {
           my ($canid, $canpath);            my ($canid, $canpath);
   
           $canid = $1 if $hdr{__BODY__} =~            $canid = $1 if $hdr{__BODY__} =~
               /\n========= WAS CANCELLED BY =======:.*\nMessage-ID: (.*?)\n/s;                /\n========= WAS CANCELLED BY =======:.*\nMessage-ID: (.*?)\n/s;
           return reject('Redundant REPOST (cache)')            return reject('Redundant REPOST (cache)')
               if $canid and $MIDhistory->check($canid);                if $canid and $MIDhistory->check($canid);
           return reject('Redundant REPOST (ID)')            return reject('Redundant REPOST (ID)')
               if $canid =~ /^<(?:[a-z]{16,17}|[0-9]{10}|[0-9]{10})\@/                if $canid =~ /^<(?:[a-z]{16,17}|[0-9]{10}|[0-9]{10})\@/
                   or $canid =~ /^<(?:cancel\.)*[0-9A-F]{8,15}\.[a-z]{4,11}\@[a-z]{4,11}\.(?:net|mil|gov|org|edu|com)>$/;                    or $canid =~ /^<(?:cancel\.)*[0-9A-F]{8,15}\.[a-z]{4,11}\@[a-z]{4,11}\.(?:net|mil|gov|org|edu|com)>$/;
       }        }
   
       # basic checks on headers ############################################        # basic checks on headers ############################################
       if ($gr{adult}) {        if ($gr{adult}) {
           foreach (@Path_Entries) {            foreach (@Path_Entries) {
               return reject("Bad path ($_)", 'Bad site')                return reject("Bad path ($_)", 'Bad site')
                   if exists $Bad_Adult_Path{$_};                    if exists $Bad_Adult_Path{$_};
           }            }
       }        }
       # If an article is fed locally and contains an Approved header, reject        # If an article is fed locally and contains an Approved header, reject
       # it unless all groups match config{local_approved_groups}.        # it unless all groups match config{local_approved_groups}.
       if ($state{localfeed} and $hdr{Approved}) {        if ($state{localfeed} and $hdr{Approved}) {
           return reject("Forged approval: $hdr{Approved}",            return reject("Forged approval: $hdr{Approved}",
                         'Forged Approval')                          'Forged Approval')
               unless $gr{approved};                unless $gr{approved};
       };        };
   
       # Do any elements of the Path appear more than once.        # Do any elements of the Path appear more than once.
       # TODO Wow, this filter catches a lot.  Too much!  Need to make it a        # TODO Wow, this filter catches a lot.  Too much!  Need to make it a
       #      scoring element rather than a reject.        #      scoring element rather than a reject.
       #%seen = {};        #%seen = {};
       #@Path_Entries2 = grep {!$seen{$_}++ } @Path_Entries;        #@Path_Entries2 = grep {!$seen{$_}++ } @Path_Entries;
       #return reject('Duplicated Path entry', 'Bad Path')        #return reject('Duplicated Path entry', 'Bad Path')
       #    if (scalar @Path_Entries) != (scalar @Path_Entries2);        #    if (scalar @Path_Entries) != (scalar @Path_Entries2);
   
       return reject('U2 violation - invalid distribution', 'U2 violation')        return reject('U2 violation - invalid distribution', 'U2 violation')
           if $gr{rg_net} and $hdr{Distribution} !~ /^[ \t]*4[Gg][Hh][ \t]*$/;            if $gr{rg_net} and $hdr{Distribution} !~ /^[ \t]*4[Gg][Hh][ \t]*$/;
   
       return reject('U2 violation - excessive crossposting', 'U2 violation')        return reject('U2 violation - excessive crossposting', 'U2 violation')
           if $gr{rg_net} and $state{fupcnt} > 3;            if $gr{rg_net} and $state{fupcnt} > 3;
   
       return reject('bofh violation - excessive crossposting','U2 violation')        return reject('bofh violation - excessive crossposting','U2 violation')
           if $gr{rg_bofh} and $state{fupcnt} > 3;            if $gr{rg_bofh} and $state{fupcnt} > 3;
   
       return reject('bofh violation - invalid distribution', 'U2 violation')        return reject('bofh violation - invalid distribution', 'U2 violation')
           if $gr{rg_bofh}            if $gr{rg_bofh}
               and $hdr{Distribution} !~ /^[ \t]*[Bb][Oo][Ff][Hh][ \t]*$/;                and $hdr{Distribution} !~ /^[ \t]*[Bb][Oo][Ff][Hh][ \t]*$/;
   
       return reject('Too many newsgroups')        return reject('Too many newsgroups')
           if $state{fupcnt} > $config{maxgroups};            if $state{fupcnt} > $config{maxgroups};
   
       return reject('Too many newsgroups (low_xpost)', 'Too many newsgroups')        return reject('Too many newsgroups (low_xpost)', 'Too many newsgroups')
           if $gr{low_xpost}            if $gr{low_xpost}
               and $state{fupcnt} > $config{low_xpost_maxgroups};                and $state{fupcnt} > $config{low_xpost_maxgroups};
   
       return reject('Too many newsgroups (meow)', 'Too many newsgroups')        return reject('Too many newsgroups (meow)', 'Too many newsgroups')
           if $gr{meow}            if $gr{meow}
               #and $gr{meow} != scalar @groups                #and $gr{meow} != scalar @groups
               and $config{meow_ext_maxgroups}                and $config{meow_ext_maxgroups}
               and ($state{fupcnt} - $gr{meow}) > $config{meow_ext_maxgroups};                and ($state{fupcnt} - $gr{meow}) > $config{meow_ext_maxgroups};
   
       return reject('Topic Filter (Off-Topic1)', 'Topic Filter')        return reject('Topic Filter (Off-Topic1)', 'Topic Filter')
           if $gr{topic1}            if $gr{topic1}
               and $gr{topic1} >= $config{on_topic1_mingroups}                and $gr{topic1} >= $config{on_topic1_mingroups}
               and $config{off_topic1_maxgroups}                and $config{off_topic1_maxgroups}
               and $state{fupcnt} - $gr{topic1} > $config{off_topic1_maxgroups};                and $state{fupcnt} - $gr{topic1} > $config{off_topic1_maxgroups};
   
       return reject('Topic Filter (On-Topic1)', 'Topic Filter')        return reject('Topic Filter (On-Topic1)', 'Topic Filter')
           if $gr{topic1} and $config{on_topic1_maxgroups}            if $gr{topic1} and $config{on_topic1_maxgroups}
               and $gr{topic1} > $config{on_topic1_maxgroups};                and $gr{topic1} > $config{on_topic1_maxgroups};
   
       return reject('Topic Filter (topic2)', 'Topic Filter')        return reject('Topic Filter (topic2)', 'Topic Filter')
           if $gr{topic2}            if $gr{topic2}
               and $gr{topic2} >= $config{on_topic2_mingroups}                and $gr{topic2} >= $config{on_topic2_mingroups}
               and $config{off_topic2_maxgroups}                and $config{off_topic2_maxgroups}
               and $state{fupcnt} - $gr{topic2} > $config{off_topic2_maxgroups};                and $state{fupcnt} - $gr{topic2} > $config{off_topic2_maxgroups};
   
       return reject('Topic Filter (On-Topic2)', 'Topic Filter')        return reject('Topic Filter (On-Topic2)', 'Topic Filter')
           if $gr{topic2} and $config{on_topic2_maxgroups}            if $gr{topic2} and $config{on_topic2_maxgroups}
               and $gr{topic2} > $config{on_topic2_maxgroups};                and $gr{topic2} > $config{on_topic2_maxgroups};
   
       return reject('Too many test groups in crosspost',        return reject('Too many test groups in crosspost',
           'Too many newsgroups') if $gr{test} > 2;            'Too many newsgroups') if $gr{test} > 2;
   
       return reject('Excessively crossposted test article',        return reject('Excessively crossposted test article',
           'Too many newsgroups') if $gr{test} and $state{fupcnt} > 4;            'Too many newsgroups') if $gr{test} and $state{fupcnt} > 4;
   
       return reject('Adult group ECP', 'Too many newsgroups')        return reject('Adult group ECP', 'Too many newsgroups')
           if $state{fupcnt} > 6 and $gr{adult} > $state{grpcnt} / 2;            if $state{fupcnt} > 6 and $gr{adult} > $state{grpcnt} / 2;
   
       return reject('Poison newsgroup') if $gr{poison} and $state{grpcnt} > 1;        return reject('Poison newsgroup') if $gr{poison} and $state{grpcnt} > 1;
   
       foreach (@Restricted_List) {        foreach (@Restricted_List) {
           return reject("hierarchy violation - crosspost outside $_")            return reject("hierarchy violation - crosspost outside $_")
               if $gr{'rg_'.$_} and not $gr{'rg_'.$_.'_only'};                if $gr{'rg_'.$_} and not $gr{'rg_'.$_.'_only'};
       }        }
   
       # binaries and MIME checks ###########################################        # binaries and MIME checks ###########################################
# XXX this protects the binary filters, but should not be needed anymore # XXX this protects the binary filters, but should not be needed anymore
# with (?>...). If your server seems to hang try uncommenting this # with (?>...). If your server seems to hang try uncommenting this
       # killer article?        # killer article?
#       return '' if $lines > 8000 and length $hdr{__BODY__} < $lines * 4; #       return '' if $lines > 8000 and length $hdr{__BODY__} < $lines * 4;
   
       # short uuencoded html, text, exe, url files        # short uuencoded html, text, exe, url files
       return reject("UUencoded $1")        return reject("UUencoded $1")
           if $state{lines} > 3 and $state{lines} < 2000            if $state{lines} > 3 and $state{lines} < 2000
               and $hdr{__BODY__} =~ /                and $hdr{__BODY__} =~ /
                   ^[Bb][Ee][Gg][Ii][Nn]$hws+[0-7]{3,4}$hws+ # begin 666                    ^[Bb][Ee][Gg][Ii][Nn]$hws+[0-7]{3,4}$hws+ # begin 666
                   \S?.{0,45}?\S*          # file name                    \S?.{0,45}?\S*          # file name
                   \.(                     # file extensions                    \.(                     # file extensions
                       [Tt][Ee]?[Xx][Tt]|                        [Tt][Ee]?[Xx][Tt]|
                       [Hh][Tt][Mm][Ll]?|                        [Hh][Tt][Mm][Ll]?|
                       [Ee][Xx][Ee]|                        [Ee][Xx][Ee]|
                       [Uu][Rr][Ll]                        [Uu][Rr][Ll]
                   )                    )
                   $hws+                     # end of line                    $hws+                     # end of line
                   (?:                    (?:
                       ^[ \t|>]*           # skip quoting marks, if any                        ^[ \t|>]*           # skip quoting marks, if any
                       (?>                 # disable backtracking                        (?>                 # disable backtracking
                       M[\x20-\x60]{60,61} # uuencoded line                        M[\x20-\x60]{60,61} # uuencoded line
                       )                        )
                       $hws*\n               # trailing spaces and end of line                        $hws*\n               # trailing spaces and end of line
                   ){2,}?                  # 0 or > 2 lines                    ){2,}?                  # 0 or > 2 lines
               /mx;                /mx;
   
       # binaries in non-binary newsgroups        # binaries in non-binary newsgroups
       if ($config{block_binaries} or $config{block_all_binaries}) {        if ($config{block_binaries} or $config{block_all_binaries}) {
           unless ($config{binaries_in_mod_groups} and $gr{allmod}) {            unless ($config{binaries_in_mod_groups} and $gr{allmod}) {
               # We're only interested in binaries                # We're only interested in binaries
               if (is_binary()) {                if (is_binary()) {
                   # Is the binary an image?                    # Is the binary an image?
                   if ($config{image_extensions}                    if ($config{image_extensions}
                     and defined $state{file_extension}  
                   and $state{file_extension} =~ /$config{image_extensions}/) {                    and $state{file_extension} =~ /$config{image_extensions}/) {
                       return reject("Binary Image: misplaced $state{file_extension}")                        return reject("Binary Image: misplaced $state{file_extension}")
                       if not $gr{image};                        if not $gr{image};
                       # gr{image} is true when distro matches bin_allowed                        # gr{image} is true when distro matches bin_allowed
                       # or image_allowed                        # or image_allowed
                       # gr{binary} is true when distro matches bin_allowed                        # gr{binary} is true when distro matches bin_allowed
                   } else {                    } else {
                       if (not $gr{binary}) {                        if (not $gr{binary}) {
                           return reject("Binary: misplaced binary");                            return reject("Binary: misplaced binary");
                       };                        };
                   }; # End of misplaced binary check                    }; # End of misplaced binary check
                   # If configured to reject all binary, we do it last so                    # If configured to reject all binary, we do it last so
                   # misplaced stuff is still highlighted.                    # misplaced stuff is still highlighted.
                   return reject("Binary Payload")                    return reject("Binary Payload")
                   if $config{block_all_binaries};                    if $config{block_all_binaries};
               }; # End of is_binary                }; # End of is_binary
           }; # End of moderated groups            }; # End of moderated groups
       }; # End of block binaries        }; # End of block binaries
   
       if ($config{block_mime_html} and not $gr{mime_html}) {        if ($config{block_mime_html} and not $gr{mime_html}) {
           # MIME encapsulated HTML (attached *.html file)            # MIME encapsulated HTML (attached *.html file)
           return reject('HTML file attachment', 'HTML')            return reject('HTML file attachment', 'HTML')
               if $lch{'content-type'} =~ /multipart/                if $lch{'content-type'} =~ /multipart/
               and ($hdr{__BODY__} =~ /^Content-Disposition:.*filename.*\.html?/imo                and ($hdr{__BODY__} =~ /^Content-Disposition:.*filename.*\.html?/imo
               or $hdr{__BODY__} =~ /^Content-Base:.*file:.*\.html?/imo);                or $hdr{__BODY__} =~ /^Content-Base:.*file:.*\.html?/imo);
   
           # Only one of the following two subsections can be applied,            # Only one of the following two subsections can be applied,
           # depending on the state of config{block_html_multipart}.  If it's            # depending on the state of config{block_html_multipart}.  If it's
           # True then all multipart mime with html elements will be            # True then all multipart mime with html elements will be
           # rejected.  If it's false, only multipart *without* text/plain            # rejected.  If it's false, only multipart *without* text/plain
           # elements will be rejected.            # elements will be rejected.
   
           # MIME text/html without text/plain            # MIME text/html without text/plain
           return reject('HTML Multipart without Text/Plain.', 'HTML')            return reject('HTML Multipart without Text/Plain.', 'HTML')
               if not $config{block_html_multipart}                if not $config{block_html_multipart}
               and $lch{'content-type'} =~ /multipart/                and $lch{'content-type'} =~ /multipart/
               and $hdr{__BODY__} =~ m#^Content-Type:[\t ]+text/html#imo                and $hdr{__BODY__} =~ m#^Content-Type:[\t ]+text/html#imo
               and not $hdr{__BODY__} =~ m#^Content-Type:[\t ]+text/plain#imo;                and not $hdr{__BODY__} =~ m#^Content-Type:[\t ]+text/plain#imo;
   
           # MIME HTML regardless of text/plain element            # MIME HTML regardless of text/plain element
           return reject('HTML Multipart', 'HTML')            return reject('HTML Multipart', 'HTML')
               if $config{block_html_multipart}                if $config{block_html_multipart}
               and $lch{'content-type'} =~ /multipart/                and $lch{'content-type'} =~ /multipart/
               and $hdr{__BODY__} =~ m#^Content-Type:[\t ]+text/html#imo;                and $hdr{__BODY__} =~ m#^Content-Type:[\t ]+text/html#imo;
       };        };
   
       # HTML Inline        # HTML Inline
       if ($config{block_html} and not $gr{html}) {        if ($config{block_html} and not $gr{html}) {
           # HTML formatted postings            # HTML formatted postings
               return reject('HTML post', 'HTML')                return reject('HTML post', 'HTML')
                   if $lch{'content-type'} =~ m#text/html#;                    if $lch{'content-type'} =~ m#text/html#;
       };        };
   
       # Some consider <img> tags as a greater evil than other HTML.        # Some consider <img> tags as a greater evil than other HTML.
       if ($config{block_html_images}        if ($config{block_html_images}
       and $lch{'content-type'} !~ m#text/plain#) {        and $lch{'content-type'} !~ m#text/plain#) {
           return reject('HTML Image Tags', 'HTML')            return reject('HTML Image Tags', 'HTML')
           if $hdr{__BODY__} =~ /\<img$hws+src/i;            if $hdr{__BODY__} =~ /\<img$hws+src/i;
       };        };
   
                         
   
       # bot checks #########################################################        # bot checks #########################################################
       return reject('MID-Bot', 'Bot signature')        return reject('MID-Bot', 'Bot signature')
           if $lch{'message-id'} =~            if $lch{'message-id'} =~
               /(?:                /(?:
                   ^<\d{12}\@[a-z]{10}>$|                    ^<\d{12}\@[a-z]{10}>$|
                   \@\d+>$|                    \@\d+>$|
                   msgidabcxyz\.com>$|                    msgidabcxyz\.com>$|
                   no(?:ne|where)\d+\.yet>$|                    no(?:ne|where)\d+\.yet>$|
                   strip_path>$|                    strip_path>$|
                   ^<[^ \t\.]+\@\d+G\d+O\d+O\d+F\d+.com>$                    ^<[^ \t\.]+\@\d+G\d+O\d+O\d+F\d+.com>$
               )/x;                )/x;
   
       if ($hdr{'User-Agent'}) {        if ($hdr{'User-Agent'}) {
       } elsif ($hdr{'X-Mailer'}) {        } elsif ($hdr{'X-Mailer'}) {
           return reject('Message-ID/X-Mailer bot', 'Bot signature')            return reject('Message-ID/X-Mailer bot', 'Bot signature')
               if $hdr{'Message-ID'} =~ /^<(.*)@/                if $hdr{'Message-ID'} =~ /^<(.*)@/
                   and $hdr{'X-Mailer'} eq $1;                    and $hdr{'X-Mailer'} eq $1;
       } elsif ($hdr{'X-Newsreader'}) {        } elsif ($hdr{'X-Newsreader'}) {
           return reject('Smart Post Pro', 'Bot signature')            return reject('Smart Post Pro', 'Bot signature')
               if $hdr{'X-Newsreader'} =~ /^[a-z]{7,11}$/                if $hdr{'X-Newsreader'} =~ /^[a-z]{7,11}$/
                   and $hdr{From} =~ /^[a-z]{7,13}\@[a-z]{7,12}\.com$/;                    and $hdr{From} =~ /^[a-z]{7,13}\@[a-z]{7,12}\.com$/;
       } else {        } else {
           my $pathtail = '';            my $pathtail = '';
           my $fromhost = '';            my $fromhost = '';
           $hdr{Path} =~ /.*!(.*)$/ and $pathtail = $1;            $hdr{Path} =~ /.*!(.*)$/ and $pathtail = $1;
           $hdr{From} =~ /@(.*?)>?$/ and $fromhost = $1;            $hdr{From} =~ /@(.*?)>?$/ and $fromhost = $1;
   
           # Path/Newsgroups bot, contains just one MIME part            # Path/Newsgroups bot, contains just one MIME part
           return reject('PN bot', 'Bot signature')            return reject('PN bot', 'Bot signature')
               if $pathtail eq $hdr{Newsgroups}                if $pathtail eq $hdr{Newsgroups}
                   and $hdr{From} !~ /\Q$pathtail\E\@/                    and $hdr{From} !~ /\Q$pathtail\E\@/
                   and $hdr{'Content-Type'}                    and $hdr{'Content-Type'}
                       =~ /^multipart; boundary="_NextPart_/;                        =~ /^multipart; boundary="_NextPart_/;
   
           # Path/From/Message-ID bot            # Path/From/Message-ID bot
           if ($hdr{'Message-ID'} =~ /^<\d{8}\.?\d{4}\@\Q$fromhost\E>$/) {            if ($hdr{'Message-ID'} =~ /^<\d{8}\.?\d{4}\@\Q$fromhost\E>$/) {
               return reject('PFM bot path') if $pathtail eq $fromhost;                return reject('PFM bot path') if $pathtail eq $fromhost;
               return reject('PFM bot misc', 'Bot signature')                return reject('PFM bot misc', 'Bot signature')
                   if $hdr{Subject} !~ / \d+ bytes \(\d+\/\d+\)$/;                    if $hdr{Subject} !~ / \d+ bytes \(\d+\/\d+\)$/;
           }            }
       } # no X-Mailer/X-Newsreader/User-Agent header        } # no X-Mailer/X-Newsreader/User-Agent header
   
       $state{xreader} = x_reader();        $state{xreader} = x_reader();
       return reject("X-Bot ($state{xreader})", 'Bot signature')        return reject("X-Bot ($state{xreader})", 'Bot signature')
           if $state{xreader} =~ /$Xbot/;            if $state{xreader} =~ /$Xbot/;
   
       return reject('Email Platinum', 'Bot signature')        return reject('Email Platinum', 'Bot signature')
           if $lch{organization} =~ /email platinum/;            if $lch{organization} =~ /email platinum/;
   
       if (not $gr{reports} and not $hdr{References}) {        if (not $gr{reports} and not $hdr{References}) {
           return reject('Bot - Newsgroup autoposter', 'Bot signature')            return reject('Bot - Newsgroup autoposter', 'Bot signature')
               if $hdr{__BODY__}                if $hdr{__BODY__}
                   =~ /\n---[\r\n]+[A-Z][a-z \t]{120,}\.?[\r\n]+/;                    =~ /\n---[\r\n]+[A-Z][a-z \t]{120,}\.?[\r\n]+/;
           return reject('Angle-bracket bot', 'Bot signature')            return reject('Angle-bracket bot', 'Bot signature')
               if $hdr{__BODY__} =~ /[\r<=>]+\r[\r<=>]+$/m;                if $hdr{__BODY__} =~ /[\r<=>]+\r[\r<=>]+$/m;
       }        }
   
       if (defined &local_filter_bot) {        if (defined &local_filter_bot) {
           my @result = local_filter_bot();            my @result = local_filter_bot();
           return reject(@result) if $result[0];            return reject(@result) if $result[0];
       }        }
   
       # EMP checks #########################################################        # EMP checks #########################################################
       # create MD5 body checksum hash.        # create MD5 body checksum hash.
       if ($config{do_md5} and not $gr{md5skip} and not $gr{alltest}        if ($config{do_md5} and not $gr{md5skip} and not $gr{alltest}
               and not ($hdr{References} and $config{md5_skips_followups})                and not ($hdr{References} and $config{md5_skips_followups})
               and (($config{md5_max_length}                and (($config{md5_max_length}
                       and $state{lines} < $config{md5_max_length})                        and $state{lines} < $config{md5_max_length})
                   or not $config{md5_max_length})                    or not $config{md5_max_length})
               and $state{lines} > 0 and ($state{lines} > 2                and $state{lines} > 0 and ($state{lines} > 2
                   or ($state{lines} < 3                    or ($state{lines} < 3
                   and $hdr{__BODY__} !~ /^\s{0,8}$/))) {                    and $hdr{__BODY__} !~ /^\s{0,8}$/))) {
           my $mbody;            my $mbody;
           if ($config{fuzzy_md5}            if ($config{fuzzy_md5}
                   and (($config{fuzzy_max_length}                    and (($config{fuzzy_max_length}
                           and $state{lines} < $config{fuzzy_max_length})                            and $state{lines} < $config{fuzzy_max_length})
                       or not $config{fuzzy_max_length})                        or not $config{fuzzy_max_length})
                   and not is_binary()) {                    and not is_binary()) {
               $mbody = lc $hdr{__BODY__};                $mbody = lc $hdr{__BODY__};
               $mbody =~ s/^(?!http)\S{7,70}\r?$//mg;                $mbody =~ s/^(?!http)\S{7,70}\r?$//mg;
               $mbody =~ s/^>+\s.*//mg;  # Strip quoted lines                $mbody =~ s/^>+\s.*//mg;  # Strip quoted lines
               $mbody =~ s/\r{3}.*$//mg;                $mbody =~ s/\r{3}.*$//mg;
               $mbody =~ s/$hws+$//;                $mbody =~ s/$hws+$//;
               $mbody =~ s/^[^\n]*\Z//m if $state{lines} > 5;                $mbody =~ s/^[^\n]*\Z//m if $state{lines} > 5;
               $mbody =~ tr/a-z0-9//cd;                $mbody =~ tr/a-z0-9//cd;
           }            }
           return reject('EMP (md5)', 'EMP')            return reject('EMP (md5)', 'EMP')
               if $MD5history->add(md5_hex($mbody || $hdr{__BODY__}));                if $MD5history->add(md5_hex($mbody || $hdr{__BODY__}));
       }        }
   
       if (not $gr{reports}) {        if (not $gr{reports}) {
           # create posting-host/lines hash            # create posting-host/lines hash
           if ($config{do_phl} and not $gr{allmod}            if ($config{do_phl} and not $gr{allmod}
           and $state{posting_host} and not $gr{phlskip}            and $state{posting_host} and not $gr{phlskip}
           and not is_binary() and not $gr{alltest}            and not is_binary() and not $gr{alltest}
           and not $state{posting_host} =~ /(?:$config{phl_exempt})/o            and not $state{posting_host} =~ /(?:$config{phl_exempt})/o
           and not $state{injection_host} =~ /(?:$config{bad_nph_hosts})/o            and not $state{injection_host} =~ /(?:$config{bad_nph_hosts})/o
           and not ($gr{binary} and $state{lines} > 100            and not ($gr{binary} and $state{lines} > 100
               and $hdr{Subject} =~ /[\(\[]\d+\/\d+[\)\]]/)) {                and $hdr{Subject} =~ /[\(\[]\d+\/\d+[\)\]]/)) {
                   return reject('EMP (phl)', 'EMP')                    return reject('EMP (phl)', 'EMP')
                       if $PHLhistory->add("$state{posting_host} $state{lines}");                        if $PHLhistory->add("$state{posting_host} $state{lines}");
           }; # End of PHL filter            }; # End of PHL filter
   
           # create posting-host/newsgroups hash            # create posting-host/newsgroups hash
           if ($config{do_phn} and not $gr{phrinc} and not $gr{phnskip}            if ($config{do_phn} and not $gr{phrinc} and not $gr{phnskip}
           and not $gr{alltest} and not $gr{allmod}            and not $gr{alltest} and not $gr{allmod}
           and not ($gr{binary} and $state{lines} > 100)) {            and not ($gr{binary} and $state{lines} > 100)) {
               if ($state{posting_host}                if ($state{posting_host}
               and not $state{injection_host} =~ /(?:$config{bad_nph_hosts})/o) {                and not $state{injection_host} =~ /(?:$config{bad_nph_hosts})/o) {
                   if (not $state{posting_host} =~ /(?:$config{phn_exempt})/o) {                    if (not $state{posting_host} =~ /(?:$config{phn_exempt})/o) {
                       return reject('EMP (phn nph)', 'EMP')                        return reject('EMP (phn nph)', 'EMP')
                           if $PHNhistory->add("$state{posting_host} $sortgrps");                            if $PHNhistory->add("$state{posting_host} $sortgrps");
                   };                    };
               } elsif ($config{phn_aggressive}) {                } elsif ($config{phn_aggressive}) {
                   return reject('EMP (phn path)', 'EMP')                    return reject('EMP (phn path)', 'EMP')
                       if $PHNhistory->add("$state{injection_host} $sortgrps");                        if $PHNhistory->add("$state{injection_host} $sortgrps");
               }; # End of aggressive mode                }; # End of aggressive mode
           }; # End of PHN filter            }; # End of PHN filter
   
           # create from/subject/lines hash            # create from/subject/lines hash
           if ($config{do_fsl} and not $gr{fslskip} and not $gr{alltest}) {            if ($config{do_fsl} and not $gr{fslskip} and not $gr{alltest}) {
               my $hash1;                my $hash1;
               if (defined $hdr{Sender}) {                if (defined $hdr{Sender}) {
                   $hash1 = lc "$hdr{Sender} $hdr{Subject}";                    $hash1 = lc "$hdr{Sender} $hdr{Subject}";
               } else {                } else {
                   $hash1 = lc "$hdr{From} $hdr{Subject}";                    $hash1 = lc "$hdr{From} $hdr{Subject}";
               };                };
               $hash1 =~ s/\d+$//;                $hash1 =~ s/\d+$//;
               $hash1 =~ tr/a-z0-9\@\x80-\xFF//cd;                $hash1 =~ tr/a-z0-9\@\x80-\xFF//cd;
               $hash1 = "$hash1 $state{lines}";                $hash1 = "$hash1 $state{lines}";
               return reject('EMP (fsl)', 'EMP') if $FSLhistory->add($hash1);                return reject('EMP (fsl)', 'EMP') if $FSLhistory->add($hash1);
           }; # End of FSL filter            }; # End of FSL filter
       }; # not reports groups        }; # not reports groups
   
       # create high-risk newsgroups hash        # create high-risk newsgroups hash
       if ($config{do_phr} and $gr{phrinc}        if ($config{do_phr} and $gr{phrinc}
           and not ($gr{binary} and $state{lines} > 100)) {            and not ($gr{binary} and $state{lines} > 100)) {
           if ($state{posting_host}            if ($state{posting_host}
           and not $state{injection_host} =~ /(?:$config{bad_nph_hosts})/o) {            and not $state{injection_host} =~ /(?:$config{bad_nph_hosts})/o) {
               if (not $state{posting_host} =~ /(?:$config{phr_exempt})/o) {                if (not $state{posting_host} =~ /(?:$config{phr_exempt})/o) {
                   return reject('EMP (phr nph)', 'EMP')                    return reject('EMP (phr nph)', 'EMP')
                       if $PHRhistory->add("$state{posting_host}");                        if $PHRhistory->add("$state{posting_host}");
               }                }
           } elsif ($config{phr_aggressive}) {            } elsif ($config{phr_aggressive}) {
               my $server;                my $server;
               $server = lc "$hdr{Path}";                $server = lc "$hdr{Path}";
               $server =~ s/(![^\.]+)+$//; # Strip right-most non-FQDN's                $server =~ s/(![^\.]+)+$//; # Strip right-most non-FQDN's
               my $exc_count = ($server =~ tr/!//); # Count Path entries                my $exc_count = ($server =~ tr/!//); # Count Path entries
               # We (probably) don't want to filter our immediate peers.                # We (probably) don't want to filter our immediate peers.
               if ($exc_count > 1) {                if ($exc_count > 1) {
                   $server =~ s/.*!//; # Strip all but the right-most entry.                    $server =~ s/.*!//; # Strip all but the right-most entry.
                   return reject('EMP (phr path)', 'EMP')                    return reject('EMP (phr path)', 'EMP')
                       if $PHRhistory->add("$server");                        if $PHRhistory->add("$server");
               }                }
           }            }
       } # End of PHR filter        } # End of PHR filter
   
       # Supersedes checks ##################################################        # Supersedes checks ##################################################
       if ($hdr{Supersedes}) {        if ($hdr{Supersedes}) {
           foreach (@Path_Entries) {            foreach (@Path_Entries) {
               return reject("Supersedes with $_ in path", 'Rogue Supersedes')                return reject("Supersedes with $_ in path", 'Rogue Supersedes')
                   if exists $Bad_Cancel_Path{$_};                    if exists $Bad_Cancel_Path{$_};
           }            }
       }        }
   
       if ($config{do_supersedes_filter} and $hdr{Supersedes}        if ($config{do_supersedes_filter} and $hdr{Supersedes}
           and not $state{posting_host} =~ /$config{supersedes_exempt}/o) {            and not $state{posting_host} =~ /$config{supersedes_exempt}/o) {
           my $source;            my $source;
           if ($state{posting_host}) {            if ($state{posting_host}) {
               $source = lc $state{posting_host};                $source = lc $state{posting_host};
               $source =~ tr/a-z.//cd;                $source =~ tr/a-z.//cd;
           }            }
   
           if ($source) {            if ($source) {
               my $max;                my $max;
               if    ($gr{faq})        { $max = 45 }                if    ($gr{faq})        { $max = $config{supersedes_max_faq} }
               elsif (not ($config{active_file} or defined &INN::newsgroup))                elsif (not ($config{active_file} or defined &INN::newsgroup))
                                       { $max = 10 }                                        { $max = $config{supersedes_max_unknown_active} }
               elsif ($gr{allmod})     { $max = 35 }                elsif ($gr{allmod})     { $max = $config{supersedes_max_all_moderated} }
               elsif ($gr{mod})        { $max = 10 }                elsif ($gr{mod})        { $max = $config{supersedes_max_moderated} }
               else                    { $max = 6  }                else                    { $max = $config{supersedes_max_unmoderated} }
   
               return reject('Excessive Supersedes '                 if ($config{supersedes_mode} ne 'off' 
                       ."($state{posting_host})", 'Excessive Supersedes')                         and $Suphistory->add2($source, $max)) { 
                   if $Suphistory->add2($source, $max);                     if ($config{supersedes_mode} eq 'audit') { 
                         $status{audited}++;  
                         policy_count('supersedes', $state{peer}, $groups[0]);  
                         policy_log_event('audit', 'supersedes',  
                             "Excessive Supersedes ($state{posting_host})");  
                     } else {  
                         return reject('Excessive Supersedes '  
                             ."($state{posting_host})", 'Excessive Supersedes');  
                     }  
                 }  
           }            }
       }        }
   
       if (defined &local_filter_after_emp) {        if (defined &local_filter_after_emp) {
           my @result = local_filter_after_emp();            my @result = local_filter_after_emp();
           return reject(@result) if $result[0];            return reject(@result) if $result[0];
       }        }
   
       # bot checks, the second part ########################################        # bot checks, the second part ########################################
       return reject('Fake multipart bot', 'Bot signature')        return reject('Fake multipart bot', 'Bot signature')
           if $hdr{Subject} =~ m#\[(\d+)/(\d+)\]$# and $1 > $2;            if $hdr{Subject} =~ m#\[(\d+)/(\d+)\]$# and $1 > $2;
       #        #
   
       # Reject bad From headers.  We also check Sender and Reply-To headers        # Reject bad From headers.  We also check Sender and Reply-To headers
       # against the same Regular Expression.        # against the same Regular Expression.
       if ($Bad_From) {        if ($Bad_From) {
           return reject("Banned Reply-To ($1)", 'Bad Reply-To')            return reject("Banned Reply-To ($1)", 'Bad Reply-To')
               if $hdr{'Reply-To'} =~ $Bad_From_RE;                if $hdr{'Reply-To'} =~ $Bad_From_RE;
           return reject("Banned Sender ($1)", 'Bad Sender')            return reject("Banned Sender ($1)", 'Bad Sender')
               if $hdr{Sender} =~ $Bad_From_RE;                if $hdr{Sender} =~ $Bad_From_RE;
           return reject("Banned From ($1)", 'Bad From')            return reject("Banned From ($1)", 'Bad From')
               if $hdr{From} =~ $Bad_From_RE;                if $hdr{From} =~ $Bad_From_RE;
       };        };
   
       # Reject bad Subject headers.        # Reject bad Subject headers.
       return reject("Subject ($1)", 'Bad Subject')        return reject("Subject ($1)", 'Bad Subject')
           if $Bad_Subject and $hdr{Subject} =~ $Bad_Subject_RE;            if $Bad_Subject and $hdr{Subject} =~ $Bad_Subject_RE;
   
       # Set a flag if the message body contains a URL.  This means later,        # Set a flag if the message body contains a URL.  This means later,
       # more complex tests can be bypassed on many messages.        # more complex tests can be bypassed on many messages.
       if ($body =~ /$HTTP/io) {        if ($body =~ /$HTTP/io) {
           $state{body_has_url} = 1;            $state{body_has_url} = 1;
       };        };
   
       # Check payload against bad_body and bad_url files.        # Check payload against bad_body and bad_url files.
       if (not $gr{reports} and not $hdr{References}) {        if (not $gr{reports} and not $hdr{References}) {
           return reject("Body ($1)", 'Bad Body')            return reject("Body ($1)", 'Bad Body')
              if $Bad_Body and $body =~ $Bad_Body_RE;               if $Bad_Body and $body =~ $Bad_Body_RE;
       };        };
   
       # Enforce Altopia policy on Path pre-loading.        # Enforce Altopia policy on Path pre-loading.
       #if ($hdr{Path} =~ /news\.alt\.net!.*[!\.]alt\.net/) {        #if ($hdr{Path} =~ /news\.alt\.net!.*[!\.]alt\.net/) {
       #    return reject("TOS Violation")        #    return reject("TOS Violation")
       #        if ($hdr{Path} !~ /[!\.]alt\.net(!not-for-mail)?$/);        #        if ($hdr{Path} !~ /[!\.]alt\.net(!not-for-mail)?$/);
       #};        #};
   
       # bad words and scoring filter #######################################        # bad words and scoring filter #######################################
       if ($config{do_scoring_filter} and not $gr{reports}        if ($config{do_scoring_filter} and not $gr{reports}
           and not $gr{scoreskip}) {            and not $gr{scoreskip}) {
   
           #FIXME All that's MIME isn't WebTV            #FIXME All that's MIME isn't WebTV
           #$score .= "!!!webtv" if $lch{'content-type'}            #$score .= "!!!webtv" if $lch{'content-type'}
           #        =~ m#multipart/(?:related|mixed).*boundary#            #        =~ m#multipart/(?:related|mixed).*boundary#
           #    and $hdr{'NNTP-Posting-Host'} !~ /webtv\.net$/            #    and $hdr{'NNTP-Posting-Host'} !~ /webtv\.net$/
           #    and $lch{'message-id'} !~ /webtv\.net>$/;            #    and $lch{'message-id'} !~ /webtv\.net>$/;
   
           # Score the huge volume of commercial sex spam currently posted            # Score the huge volume of commercial sex spam currently posted
           # to these groups. (20110817)            # to these groups. (20110817)
           if ($state{spamsource}) {            if ($state{spamsource}) {
               if ($hdr{Newsgroups} =~ /\.sex\.|^alt\.fan\.utb\./) {                if ($hdr{Newsgroups} =~ /\.sex\.|^alt\.fan\.utb\./) {
                   $score .= "!!!LongSexLine" if $hdr{__BODY__} =~ /^.{150}/m;                    $score .= "!!!LongSexLine" if $hdr{__BODY__} =~ /^.{150}/m;
                   $score .= "!!SexLines1" if $state{lines} > 50;                    $score .= "!!SexLines1" if $state{lines} > 50;
                   $score .= "!!SexLines2" if $state{lines} > 100;                    $score .= "!!SexLines2" if $state{lines} > 100;
                   $score .= "!!!SexURL" if $state{body_has_url};                    $score .= "!!!SexURL" if $state{body_has_url};
                   $score .= "!SexNoRef" if not $hdr{References};                    $score .= "!SexNoRef" if not $hdr{References};
               };                };
           };            };
   
           # Score poor netiquette on Follow-up's            # Score poor netiquette on Follow-up's
           $score .= "!!ExpFup($state{grpcnt}-$state{grpfupcnt})"            $score .= "!!ExpFup($state{grpcnt}-$state{grpfupcnt})"
               if $state{grpfupcnt} > $state{grpcnt};                if $state{grpfupcnt} > $state{grpcnt};
           if (not $hdr{'Followup-To'}) {            if (not $hdr{'Followup-To'}) {
               $score .= "!NoFup($state{grpcnt})"                $score .= "!NoFup($state{grpcnt})"
                   if $state{grpcnt} > 1 and $state{grpcnt} < 4;                    if $state{grpcnt} > 1 and $state{grpcnt} < 4;
               $score .= "!!NoFup($state{grpcnt})" if $state{grpcnt} >= 4;                $score .= "!!NoFup($state{grpcnt})" if $state{grpcnt} >= 4;
           };            };
   
           $score .= "!!!!FromURL2" if $lch{from} =~ /$url2/o;            $score .= "!!!!FromURL2" if $lch{from} =~ /$url2/o;
           $score .= "!FromNoLC" if $lch{from} !~ /[a-z]/o;            $score .= "!FromNoLC" if $lch{from} !~ /[a-z]/o;
           $score .= "!FromNo@" if $lch{from} !~ /\@/;            $score .= "!FromNo@" if $lch{from} !~ /\@/;
           $score .= "!FromLen" if length($hdr{From}) > 80;            $score .= "!FromLen" if length($hdr{From}) > 80;
   
           $score .= "!SubURL" if $lch{subject} =~ /$url/o;            $score .= "!SubURL" if $lch{subject} =~ /$url/o;
           $score .= "!!!!!SubStealthURL" if $lch{subject} =~ /$stealthURL/o;            $score .= "!!!!!SubStealthURL" if $lch{subject} =~ /$stealthURL/o;
           $score .= "!!Sub15Space" if $hdr{Subject} =~ / {15,}[^ ]/;            $score .= "!!Sub15Space" if $hdr{Subject} =~ / {15,}[^ ]/;
           # Digits at the end are bad, but not if they are a year            # Digits at the end are bad, but not if they are a year
           $score .= "!!!SubDigit" if $hdr{Subject} =~ /[\s~]\d{2,}$/            $score .= "!!!SubDigit" if $hdr{Subject} =~ /[\s~]\d{2,}$/
               and $hdr{Subject} !~ /\D20[0-9]{2}$/;                and $hdr{Subject} !~ /\D20[0-9]{2}$/;
                       
           $score .= "!!!!SubDigitJPG" if $lch{subject} =~ /\s\d{1,3}\.jpg$/;            $score .= "!!!!SubDigitJPG" if $lch{subject} =~ /\s\d{1,3}\.jpg$/;
           $score .= "!SubSymbols" if $hdr{Subject} =~ /^\W{3}/;            $score .= "!SubSymbols" if $hdr{Subject} =~ /^\W{3}/;
           $score .= "!!!SubPhone" if $hdr{Subject} =~ /$phone/;            $score .= "!!!SubPhone" if $hdr{Subject} =~ /$phone/;
           $score .= "!SubCR" if $hdr{Subject} =~ /\r/;            $score .= "!SubCR" if $hdr{Subject} =~ /\r/;
           $score .= "!SubLength" if length($hdr{Subject}) > 210;            $score .= "!SubLength" if length($hdr{Subject}) > 210;
           $score .= "!!SubNoLC" if $hdr{Subject} !~ /[a-z]/;            $score .= "!!SubNoLC" if $hdr{Subject} !~ /[a-z]/;
           $score .= "!!!SubSpaced" if $hdr{Subject} =~ /(\w$hws+){8}/o;            $score .= "!!!SubSpaced" if $hdr{Subject} =~ /(\w$hws+){8}/o;
   
           # Job Spam            # Job Spam
           $score .= "!!!Jobs" if $hdr{Newsgroups} =~ /\.jobs|\.bestjobs/            $score .= "!!!Jobs" if $hdr{Newsgroups} =~ /\.jobs|\.bestjobs/
               and $hdr{From} =~ /job/;                and $hdr{From} =~ /job/;
   
           if ($config{aggressive} and not $gr{alladult}) {            if ($config{aggressive} and not $gr{alladult}) {
               $score .= "!Sub1Pt($1)" while $lch{subject} =~ /($one_point_words)/go;                $score .= "!Sub1Pt($1)" while $lch{subject} =~ /($one_point_words)/go;
               $score .= "!!Sub2Pt($1)" while $lch{subject} =~ /($two_point_words)/go;                $score .= "!!Sub2Pt($1)" while $lch{subject} =~ /($two_point_words)/go;
               $score .= "!From1Pt" while $lch{from} =~ /$one_point_words/go;                $score .= "!From1Pt" while $lch{from} =~ /$one_point_words/go;
               $score .= "!!From2Pt" while $lch{from} =~ /$two_point_words/go;                $score .= "!!From2Pt" while $lch{from} =~ /$two_point_words/go;
               $score .= "!MID1Pt" while $lch{'message-id'} =~ /$one_point_words/go;                $score .= "!MID1Pt" while $lch{'message-id'} =~ /$one_point_words/go;
               $score .= "!!MID2Pt" while $lch{'message-id'} =~ /$two_point_words/go;                $score .= "!!MID2Pt" while $lch{'message-id'} =~ /$two_point_words/go;
               $score .= "!Org1Pt" while $lch{organization} =~ /$one_point_words/go;                $score .= "!Org1Pt" while $lch{organization} =~ /$one_point_words/go;
               $score .= "!!Org2Pt" while $lch{organization} =~ /$two_point_words/go;                $score .= "!!Org2Pt" while $lch{organization} =~ /$two_point_words/go;
   
               local $_ = $lch{subject};                local $_ = $lch{subject};
               tr/a-z0-9 //cd;                tr/a-z0-9 //cd;
               #FIXME Next two return false positives.                #FIXME Next two return false positives.
               #$score .= "!!!!!SubServices" if /$services/o;                #$score .= "!!!!!SubServices" if /$services/o;
               #$score .= "!!!SubSiteDesc" if /$site_desc.{0,20}site/o;                #$score .= "!!!SubSiteDesc" if /$site_desc.{0,20}site/o;
               $score .= "!SubPorn" if /(?:$free_stuff|$porn)/o;                $score .= "!SubPorn" if /(?:$free_stuff|$porn)/o;
           };            };
   
           $score .= "!!SubImage" if $state{lines} < 30            $score .= "!!SubImage" if $state{lines} < 30
               and $lch{subject}=~ /\w\.(?:jpe?g|gif)/;                and $lch{subject}=~ /\w\.(?:jpe?g|gif)/;
           $score .= "!!MalfLines($state{badlines})" if $state{badlines} > 0;            $score .= "!!MalfLines($state{badlines})" if $state{badlines} > 0;
           $score .= "!!!NoOrg" if $lch{organization} =~ /<no organization>/;            $score .= "!!!NoOrg" if $lch{organization} =~ /<no organization>/;
           $score .= "!!!!!!!StealthOrg" if $lch{organization} =~ /$stealthURL/o;            $score .= "!!!!!!!StealthOrg" if $lch{organization} =~ /$stealthURL/o;
           $score .= "!!!!!MIDDigit" if $hdr{'Message-ID'}=~/^<(?:\d{8}\.?\d{4}|\d{4,5})\@/;            $score .= "!!!!!MIDDigit" if $hdr{'Message-ID'}=~/^<(?:\d{8}\.?\d{4}|\d{4,5})\@/;
   
           if ($lch{'content-type'} =~ m#^(?:multipart|text/html)#) {            if ($lch{'content-type'} =~ m#^(?:multipart|text/html)#) {
               $score .= "***PGP" if $lch{'content-type'} =~ /^multipart\/signed/;                $score .= "***PGP" if $lch{'content-type'} =~ /^multipart\/signed/;
               $score .= "!!!!!!!HTMLRefresh" if $body =~ /<meta http-equiv=.?refresh/;                $score .= "!!!!!!!HTMLRefresh" if $body =~ /<meta http-equiv=.?refresh/;
               $score .= "!!!!!!!HTMLPopup" if $body =~ /window\.open\(/;                $score .= "!!!!!!!HTMLPopup" if $body =~ /window\.open\(/;
               $score .= "!!!!!!HTMLJava" if $body =~ /<script language=.?javascript/;                $score .= "!!!!!!HTMLJava" if $body =~ /<script language=.?javascript/;
               $score .= "!!!!!!HTMLLive" if $body =~ /<script language=.?livescript/;                $score .= "!!!!!!HTMLLive" if $body =~ /<script language=.?livescript/;
               $score .= "!!HTMLAlternate" if $body =~ /^content-type:$hws+multipart\/alternative/m;                $score .= "!!HTMLAlternate" if $body =~ /^content-type:$hws+multipart\/alternative/m;
           };            };
   
           #$score .= "!CR" if $body =~ /\r/;            #$score .= "!CR" if $body =~ /\r/;
           $score .= "!!!2CR" if $body =~ /\r\r/;            $score .= "!!!2CR" if $body =~ /\r\r/;
           $score .= "!!Newlines" if $hdr{__BODY__} =~ /(\r?\n){15}/;            $score .= "!!Newlines" if $hdr{__BODY__} =~ /(\r?\n){15}/;
           $score .= "!!Phone" if $body =~ /$phone/;            $score .= "!!Phone" if $body =~ /$phone/;
           # Score rates per minute (1.99/m or 1.99 per min)            # Score rates per minute (1.99/m or 1.99 per min)
           $score .= "!!!Rate"            $score .= "!!!Rate"
               if $body =~ /\d\.\d{2}\s?(?:[\\\/]\s?m|$hws+per\bmin)/;                if $body =~ /\d\.\d{2}\s?(?:[\\\/]\s?m|$hws+per\bmin)/;
           $score .= "!!!!!HttpIP" if $body =~ /$HTTP$IP/o;            $score .= "!!!!!HttpIP" if $body =~ /$HTTP$IP/o;
           $score .= "!!!!!!!Stealth" if $body =~ /$HTTP$StealthIP/o;            $score .= "!!!!!!!Stealth" if $body =~ /$HTTP$StealthIP/o;
           $score .= "!!!!SpaceCom" if $body =~ /$spacecom/o;            $score .= "!!!!SpaceCom" if $body =~ /$spacecom/o;
           # Power Post generates lots of spam.            # Power Post generates lots of spam.
           $score .= "!!!PowerPost" if $hdr{'X-Newsposter'} and not $gr{binary};            $score .= "!!!PowerPost" if $hdr{'X-Newsposter'} and not $gr{binary};
   
           # only URL            # only URL
           $score .= "!!!!!!OnlyURL"            $score .= "!!!!!!OnlyURL"
               if $state{lines} < 3 and $body =~ /^$hws*$url3\S*$hws*$/o;                if $state{lines} < 3 and $body =~ /^$hws*$url3\S*$hws*$/o;
   
           if ($state{body_has_url}) {            if ($state{body_has_url}) {
               if ($Bad_URL) {                if ($Bad_URL) {
                   $score .= "!" x $config{'bad_url_score'} . 'bad_url'                    $score .= "!" x $config{'bad_url_score'} . 'bad_url'
                   if $body =~ $Bad_URL_RE;                    if $body =~ $Bad_URL_RE;
               };                };
               if ($Bad_URL_Central) {                if ($Bad_URL_Central) {
                   $score .= "!" x $config{'bad_url_score'} . 'bad_url_cen'                    $score .= "!" x $config{'bad_url_score'} . 'bad_url_cen'
                   if $body =~ $Bad_URL_Central_RE;                    if $body =~ $Bad_URL_Central_RE;
               };                };
   
               # Emphasized URL (>>> http://www.foo <<<)                # Emphasized URL (>>> http://www.foo <<<)
               if (not $hdr{References}) {                if (not $hdr{References}) {
                   if ($body =~ /$SYMBOL{3}[\x09\x20]+$simpleURL[\x09\x20]+$SYMBOL{3}/o) {                    if ($body =~ /$SYMBOL{3}[\x09\x20]+$simpleURL[\x09\x20]+$SYMBOL{3}/o) {
                       $score .= "!!!EmphURL3";                        $score .= "!!!EmphURL3";
                   } elsif ($body =~ /$SYMBOL{3}[\x09\x20]+$simpleURL/o) {                    } elsif ($body =~ /$SYMBOL{3}[\x09\x20]+$simpleURL/o) {
                       $score .= "!EmphURL1";                        $score .= "!EmphURL1";
                   };                    };
               }; # End of References                }; # End of References
           }; # End of body_has_url            }; # End of body_has_url
   
           if ($hdr{References}) {            if ($hdr{References}) {
               if ($hdr{References} =~ /^<[^>]+>\s*</) {                if ($hdr{References} =~ /^<[^>]+>\s*</) {
                   $score .= "***Refs";                    $score .= "***Refs";
               } elsif ($hdr{References} =~ /^<[^>]+>\s*$/) {                } elsif ($hdr{References} =~ /^<[^>]+>\s*$/) {
                   $score .= "**Ref";                    $score .= "**Ref";
               } else {                } else {
                   $score .= "!!BadRef";                    $score .= "!!BadRef";
               };                };
   
               if ($state{lines} > 0) {                if ($state{lines} > 0) {
                   # How much of the post is quoted?                    # How much of the post is quoted?
                   $state{reflines} = 0;                    $state{reflines} = 0;
                   $state{reflines}++ while $hdr{__BODY__} =~ /^>/mg;                    $state{reflines}++ while $hdr{__BODY__} =~ /^>/mg;
                   $state{pctrefs} = int(($state{reflines} /                    $state{pctrefs} = int(($state{reflines} /
                       $state{lines}) * 100) / 100;                        $state{lines}) * 100) / 100;
                   if ($state{pctrefs} > 0.9 and $state{lines} > 50) {                    if ($state{pctrefs} > 0.9 and $state{lines} > 50) {
                       $score .= "!!PctQuote($state{pctrefs})";                        $score .= "!!PctQuote($state{pctrefs})";
                   } elsif ($state{pctrefs} > 0.7 and $state{lines} > 30) {                    } elsif ($state{pctrefs} > 0.7 and $state{lines} > 30) {
                       $score .= "!PctQuote($state{pctrefs})";                        $score .= "!PctQuote($state{pctrefs})";
                   } elsif ($state{reflines} == 0 and $state{lines} > 5) {                    } elsif ($state{reflines} == 0 and $state{lines} > 5) {
                       $score .= "!NoQuote";                        $score .= "!NoQuote";
                   };                    };
               };                };
           }; # End of References conditional            }; # End of References conditional
   
           # Long signatures are poor netiquette but not spam, so only            # Long signatures are poor netiquette but not spam, so only
           # give them low scores.            # give them low scores.
           #$score .= "!SigLen" if $body =~ /\n-- (\r?\n.*){5,}\n\w/;            #$score .= "!SigLen" if $body =~ /\n-- (\r?\n.*){5,}\n\w/;
           #$score .= "!SigLen2" if $body =~ /\n-- (\r?\n.*){9,}\n\w/;            #$score .= "!SigLen2" if $body =~ /\n-- (\r?\n.*){9,}\n\w/;
   
           # Check messages for high ratios of upper case and urls.            # Check messages for high ratios of upper case and urls.
           if ($config{do_ratio_scoring} and not $gr{ratio}            if ($config{do_ratio_scoring} and not $gr{ratio}
           and $state{lines} < 4000 and not is_binary()            and $state{lines} < 4000 and not is_binary()
           and not $hdr{'Content-Transfer-Encoding'} =~ /base64/            and not $hdr{'Content-Transfer-Encoding'} =~ /base64/
           and not $state{charset} =~ /big5|iso-2022|koi8-r/) {            and not $state{charset} =~ /big5|iso-2022|koi8-r/) {
               $fuzzy = $hdr{__BODY__};                $fuzzy = $hdr{__BODY__};
               $state{urlcount} = $fuzzy =~ s/http:\/\/www\.\S+//g;       # Strip urls                $state{urlcount} = $fuzzy =~ s/http:\/\/www\.\S+//g;       # Strip urls
               $state{urlcount} += $fuzzy =~ s/$HTTP\S+//g;   # Strip urls                $state{urlcount} += $fuzzy =~ s/$HTTP\S+//g;   # Strip urls
               $state{nonasc} += $fuzzy =~ s/[\x80-\xFF]//g; # Strip non-ascii                $state{nonasc} += $fuzzy =~ s/[\x80-\xFF]//g; # Strip non-ascii
               $fuzzy =~ s/[\-=]{5,}//g;           # Strip horizontal rules                $fuzzy =~ s/[\-=]{5,}//g;           # Strip horizontal rules
               $fuzzy =~ s/[a-zA-Z]{20,}//g;       # Strip long char strings                $fuzzy =~ s/[a-zA-Z]{20,}//g;       # Strip long char strings
               $state{letters} = $fuzzy =~ y/a-zA-Z//;    # Count letters                $state{letters} = $fuzzy =~ y/a-zA-Z//;    # Count letters
               $state{uppercase} = $fuzzy =~ y/A-Z//;     # Count upper                $state{uppercase} = $fuzzy =~ y/A-Z//;     # Count upper
               $state{symbols} = $fuzzy =~ y/!"#$%&'()*+,-:;=?@{|}~//; # Count syms                $state{symbols} = $fuzzy =~ y/!"#$%&'()*+,-:;=?@{|}~//; # Count syms
               undef $fuzzy;                undef $fuzzy;
               # Where non-ASCII chars are present, there should be a charset                # Where non-ASCII chars are present, there should be a charset
               # specified in the Content-Type.                # specified in the Content-Type.
               if (not $state{charset} and $state{nonasc}) {                if (not $state{charset} and $state{nonasc}) {
                   $score .= "!!!NoCharset" if $state{nonasc} > 50;                    $score .= "!!!NoCharset" if $state{nonasc} > 50;
               };                };
               if (not $state{charset} =~ /iso-2022|koi8-r/) {                if (not $state{charset} =~ /iso-2022|koi8-r/) {
                   if ($state{uppercase} > 50) {                    if ($state{uppercase} > 50) {
                       $state{capratio} = int(($state{uppercase} /                        $state{capratio} = int(($state{uppercase} /
                           $state{letters}) * 100) / 100;                            $state{letters}) * 100) / 100;
                       if ($state{capratio} > 0.8) {                        if ($state{capratio} > 0.8) {
                           $score .= "!!!CapRat($state{capratio})";                            $score .= "!!!CapRat($state{capratio})";
                       } elsif ($state{capratio} > 0.5) {                        } elsif ($state{capratio} > 0.5) {
                           $score .= "!!CapRat($state{capratio})";                            $score .= "!!CapRat($state{capratio})";
                       } elsif ($state{capratio} > 0.2) {                        } elsif ($state{capratio} > 0.2) {
                           $score .= "!CapRat($state{capratio})";                            $score .= "!CapRat($state{capratio})";
                       };                        };
                   };                    };
                   if ($state{letters} > 10 and not $hdr{References}) {                    if ($state{letters} > 10 and not $hdr{References}) {
                       $state{symratio} = int(($state{symbols} /                        $state{symratio} = int(($state{symbols} /
                           $state{letters}) * 100) / 100;                            $state{letters}) * 100) / 100;
                       if ($state{symratio} > 1) {                        if ($state{symratio} > 1) {
                           $score .= "!!!SymRat($state{symratio})";                            $score .= "!!!SymRat($state{symratio})";
                       } elsif ($state{symratio} > 0.5) {                        } elsif ($state{symratio} > 0.5) {
                           $score .= "!!SymRat($state{symratio})";                            $score .= "!!SymRat($state{symratio})";
                       } elsif ($state{symratio} > 0.3) {                        } elsif ($state{symratio} > 0.3) {
                           $score .= "!SymRat($state{symratio})";                            $score .= "!SymRat($state{symratio})";
                       };                        };
                   };                    };
               }; # End of charset conditional                }; # End of charset conditional
   
               # Grant some slack to posts with no url's in them.                # Grant some slack to posts with no url's in them.
               $score .= "**NoUrl" if $state{urlcount} == 0;                $score .= "**NoUrl" if $state{urlcount} == 0;
               # Score on URL ratios                # Score on URL ratios
               if ($state{lines} > 0 and $state{urlcount} > 1) {                if ($state{lines} > 0 and $state{urlcount} > 1) {
                   $urlscore = 0;                    $urlscore = 0;
                   $state{urlratio} = int(($state{urlcount} /                    $state{urlratio} = int(($state{urlcount} /
                       $state{lines}) * 100) / 100;                        $state{lines}) * 100) / 100;
                   if ($state{urlratio} > 0.5 and  $state{lines} > 40) {                    if ($state{urlratio} > 0.5 and  $state{lines} > 40) {
                       $urlscore = 3;                        $urlscore = 3;
                   } elsif ($state{urlratio} > 0.3 and  $state{lines} > 10) {                    } elsif ($state{urlratio} > 0.3 and  $state{lines} > 10) {
                       $urlscore = 2;                        $urlscore = 2;
                   } elsif ($state{urlratio} > 0.1 and  $state{lines} > 5) {                    } elsif ($state{urlratio} > 0.1 and  $state{lines} > 5) {
                       $urlscore = 1;                        $urlscore = 1;
                   }; # End of url ratio calculation                    }; # End of url ratio calculation
                   # Double the score for spamsources                    # Double the score for spamsources
                   $urlscore = $urlscore * 2 if $state{spamsource};                    $urlscore = $urlscore * 2 if $state{spamsource};
                   # Convert the score integer to text format                    # Convert the score integer to text format
                   if ($urlscore > 0) {                    if ($urlscore > 0) {
                       $score .= "!" x $urlscore . "UrlRat($state{urlratio})";                        $score .= "!" x $urlscore . "UrlRat($state{urlratio})";
                   };                    };
               }; # End of url ratio scoring                }; # End of url ratio scoring
           }; # End of do_ratio_scoring            }; # End of do_ratio_scoring
   
           # Grant some slack to adult groups posts, providing they don't            # Grant some slack to adult groups posts, providing they don't
           # originate from a spam source.            # originate from a spam source.
           if ($gr{alladult} and not $state{spamsource}) {            if ($gr{alladult} and not $state{spamsource}) {
               if ($hdr{References}) {                if ($hdr{References}) {
                   $score .= "***Adult";                    $score .= "***Adult";
               } else {                } else {
                   $score .= "**Adult" if not $state{body_has_url};                    $score .= "**Adult" if not $state{body_has_url};
                   $score .= "#Adult+Url" if $state{body_has_url};                    $score .= "#Adult+Url" if $state{body_has_url};
               };                };
           }; # End of Adult slack            }; # End of Adult slack
   
           if ($config{active_file} or defined &INN::newsgroup) {            if ($config{active_file} or defined &INN::newsgroup) {
               if ($gr{allmod}) {                if ($gr{allmod}) {
                   #$score -= 6;                    #$score -= 6;
                   $score .= "******AllMod";                    $score .= "******AllMod";
               } elsif ($gr{mod}) {                } elsif ($gr{mod}) {
                   #$score -= 4;                    #$score -= 4;
                   $score .= "****Mod";                    $score .= "****Mod";
               };                };
           };            };
   
           $state{scoreval} = ($score =~ tr/!//) - ($score =~ tr/*//);            $state{scoreval} = ($score =~ tr/!//) - ($score =~ tr/*//);
           return reject("Scoring filter", "Scoring filter")            return reject("Scoring filter", "Scoring filter")
               if $state{scoreval} > 7;                if $state{scoreval} > 7;
       }; # End of Scoring filters        }; # End of Scoring filters
   
       if (defined &local_filter_last) {        if (defined &local_filter_last) {
           my @result = local_filter_last();            my @result = local_filter_last();
           return reject(@result) if $result[0];            return reject(@result) if $result[0];
       }        }
   
   # cancel messages ########################################################    # cancel messages ########################################################
   } elsif ($hdr{Control} =~ /^$hws*cancel/) {    } elsif ($hdr{Control} =~ /^$hws*cancel/) {
       foreach (@Path_Entries) {        foreach (@Path_Entries) {
           return reject("Cancel with $_ in path", 'Rogue cancel')            return reject("Cancel with $_ in path", 'Rogue cancel')
               if exists $Bad_Cancel_Path{$_};                if exists $Bad_Cancel_Path{$_};
       }        }
   
       return reject('User-issued spam cancel')        return reject('User-issued spam cancel')
           if $config{block_user_spamcancels}            if $config{block_user_spamcancels}
               and $state{injection_host} and $state{posting_host}                and $state{injection_host} and $state{posting_host}
               and $hdr{Path} =~ /!cyberspam!/;                and $hdr{Path} =~ /!cyberspam!/;
   
       return reject('User-issued cancel')        return reject('User-issued cancel')
           if $config{block_user_cancels}            if $config{block_user_cancels}
               and not $hdr{Path} =~ /!cyberspam!/;                and not $hdr{Path} =~ /!cyberspam!/;
   
       return reject('Cancel in forbidden group', 'Rogue cancel')        return reject('Cancel in forbidden group', 'Rogue cancel')
           if $gr{no_cancel} and not $hdr{Path} =~ /!cyberspam!/;            if $gr{no_cancel} and not $hdr{Path} =~ /!cyberspam!/;
   
       if ($config{block_late_cancels}        if ($config{block_late_cancels}
               and $hdr{Control} =~ /^cancel$hws+(.+)$/) {                and $hdr{Control} =~ /^cancel$hws+(.+)$/) {
           return reject('Cancel for rejected article')            return reject('Cancel for rejected article')
               if $MIDhistory->check($1);                if $MIDhistory->check($1);
       }        }
   
       return reject('Cancel with Supersedes header')        return reject('Cancel with Supersedes header')
           if $hdr{Supersedes};            if $hdr{Supersedes};
   
       return reject('Rogue cancel (newsgroups)', 'Rogue cancel')        return reject('Rogue cancel (newsgroups)', 'Rogue cancel')
           if grep(/^control(?:\.cancel)?$/, @groups);            if grep(/^control(?:\.cancel)?$/, @groups);
   
       # from Ricardo's "FAQ" + hipcrime signatures        # from Ricardo's "FAQ" + hipcrime signatures
       return reject("Rogue cancel ($1)", 'Rogue cancel')        return reject("Rogue cancel ($1)", 'Rogue cancel')
           if $hdr{Path} =~ /(h[i\d]pcr[i\d]me|(?:hip|hacker|crack|porn|cripple|gimp|cunt|hole|fag|aids|faq|god|hindu|dothead|jew|kike|moslem|towelhead|nazi|kraut|nerd|geek|nigger|redneck|rice|slanteye|spick|whine)cancel|cyberwhin(?:er|ing))/;            if $hdr{Path} =~ /(h[i\d]pcr[i\d]me|(?:hip|hacker|crack|porn|cripple|gimp|cunt|hole|fag|aids|faq|god|hindu|dothead|jew|kike|moslem|towelhead|nazi|kraut|nerd|geek|nigger|redneck|rice|slanteye|spick|whine)cancel|cyberwhin(?:er|ing))/;
   
       if ($hdr{'X-Cancelled-By'} or $hdr{'X-Canceled-By'}) {        if ($hdr{'X-Cancelled-By'} or $hdr{'X-Canceled-By'}) {
           my $xcb = lc ($hdr{'X-Cancelled-By'} || $hdr{'X-Canceled-By'});            my $xcb = lc ($hdr{'X-Cancelled-By'} || $hdr{'X-Canceled-By'});
           return reject('Bad X-Cancelled-By', 'Rogue cancel')            return reject('Bad X-Cancelled-By', 'Rogue cancel')
               if $xcb !~ /\w\@\w/;                if $xcb !~ /\w\@\w/;
       }        }
   
       if (defined &local_filter_cancel) {        if (defined &local_filter_cancel) {
           my @result = local_filter_cancel();            my @result = local_filter_cancel();
           return reject(@result) if $result[0];            return reject(@result) if $result[0];
       }        }
   
   # newgroup and rmgroup messages ##########################################    # newgroup and rmgroup messages ##########################################
   } elsif ($hdr{Control} =~ /^$hws*((?:new|rm)group)$hws+(.*)/) {    } elsif ($hdr{Control} =~ /^$hws*((?:new|rm)group)$hws+(.*)/) {
       my $control_type = $1;        my $control_type = $1;
       my $control_group = $2;        my $control_group = $2;
   
       return reject("Bogus $control_type message from Collabra luser",        return reject("Bogus $control_type message from Collabra luser",
           'Bad control message')            'Bad control message')
           if $hdr{Distribution} =~ /collabra-internal/ or $hdr{__BODY__}            if $hdr{Distribution} =~ /collabra-internal/ or $hdr{__BODY__}
               =~ /Control message generated by Netscape Collabra Server/;                =~ /Control message generated by Netscape Collabra Server/;
   
       if ($control_group        if ($control_group
               =~ /^(?:comp|misc|news|rec|soc|sci|humanities|talk)\./) {                =~ /^(?:comp|misc|news|rec|soc|sci|humanities|talk)\./) {
           return reject("Big 8 $control_type message from wrong address",            return reject("Big 8 $control_type message from wrong address",
                   'Bad control message')                    'Bad control message')
               if $hdr{From} !~ /group-admin\@isc\.org/;                if $hdr{From} !~ /group-admin\@isc\.org/;
       } else {        } else {
           return reject("Forged non-big-8 $control_type message supposedly from tale", 'Bad control message')            return reject("Forged non-big-8 $control_type message supposedly from tale", 'Bad control message')
               if $hdr{From}                if $hdr{From}
                   =~ /(?:group-admin|tale)\@isc\.org|tale\@uunet\.uu\.net/;                    =~ /(?:group-admin|tale)\@isc\.org|tale\@uunet\.uu\.net/;
       }        }
   
       return reject("Unapproved $control_type message",        return reject("Unapproved $control_type message",
           'Bad control message') if not $hdr{Approved};            'Bad control message') if not $hdr{Approved};
   
       return reject("Newgroup for poison group $control_group",        return reject("Newgroup for poison group $control_group",
           'Bad control message')            'Bad control message')
           if $control_type eq 'newgroup'            if $control_type eq 'newgroup'
               and $control_group =~ /$config{poison_groups}/o;                and $control_group =~ /$config{poison_groups}/o;
   
       if (defined &local_filter_control) {        if (defined &local_filter_control) {
           my @result = local_filter_control();            my @result = local_filter_control();
           return reject(@result) if $result[0];            return reject(@result) if $result[0];
       };        };
   
   # other control messages #################################################    # other control messages #################################################
   } elsif ($hdr{Control} =~ /^$hws*(\w+)(?:$hws+(.*))?/) {    } elsif ($hdr{Control} =~ /^$hws*(\w+)(?:$hws+(.*))?/) {
       my $control_type = $1;        my $control_type = $1;
       my $control_group = $2;        my $control_group = $2;
         
       return reject("$control_type with Supersedes header")        return reject("$control_type with Supersedes header")
           if $hdr{Supersedes};            if $hdr{Supersedes};
   
       return reject("Obsolete $1 control message", 'Bad control message')        return reject("Obsolete $1 control message", 'Bad control message')
           if $config{drop_useless_controls}            if $config{drop_useless_controls}
               and $control_type =~ /^(sendsys|senduuname|version|whogets)$/;                and $control_type =~ /^(sendsys|senduuname|version|whogets)$/;
       return reject("Unwanted $1 control message", 'Bad control message')        return reject("Unwanted $1 control message", 'Bad control message')
           if $config{drop_ihave_sendme}            if $config{drop_ihave_sendme}
               and $control_type =~ /^(ihave|sendme)$/;                and $control_type =~ /^(ihave|sendme)$/;
   
   }    }
   ##########################################################################    ##########################################################################
   
   $status{accepted}++;    $status{accepted}++;
     policy_count('accepted', $state{peer}, $groups[0]) if $config{metrics_enabled};  
     policy_log_event('accept', 'accepted', '') if $config{policy_log_accepts};  
   $timer{accepted}++ if $config{timer_info};    $timer{accepted}++ if $config{timer_info};
   return '';    return '';
} }
   
  # Build a bounded scan window.  Binary markers normally occur near the start,  
  # but yEnc terminators and MIME part headers can also be useful near the end.  
  # Avoid copying an arbitrarily large article into another full-size scalar.  
  sub binary_scan_body {  
     my $raw = defined $hdr{__BODY__} ? $hdr{__BODY__} : '';  
     my $head_bytes = $config{binary_scan_bytes} || 262144;  
     my $tail_bytes = $config{binary_scan_tail_bytes} || 16384;  
   
     return $raw if length($raw) <= $head_bytes + $tail_bytes;  
     return substr($raw, 0, $head_bytes) . "\n" . substr($raw, -$tail_bytes);  
  }  
   
  sub set_binary_extension {  
     my ($filename) = @_;  
     return unless defined $filename;  
     $filename =~ s/^["']|["']$//g;  
     if ($filename =~ /\.([A-Za-z0-9]{1,16})\s*$/) {  
         $state{file_extension} = lc $1;  
     }  
  }  
   
  # Return a descriptive string for a MIME binary attachment, or false.  
  sub mime_binary_type {  
     return 0 unless $config{detect_mime_binaries};  
   
     my $scan = binary_scan_body();  
     my $top_ct = $hdr{'Content-Type'} || '';  
     my $top_cd = $hdr{'Content-Disposition'} || '';  
     my $top_ce = $hdr{'Content-Transfer-Encoding'} || '';  
   
     # A top-level non-text MIME entity is binary regardless of its size.  
     if ($top_ct =~ m{^\s*(?:application|audio|video|image|model|font)/}i) {  
         if ($top_ct =~ /\bname\s*=\s*(?:"([^"]+)"|([^;\s]+))/i) {  
             set_binary_extension(defined $1 ? $1 : $2);  
         }  
         if ($top_cd =~ /\bfilename\s*=\s*(?:"([^"]+)"|([^;\s]+))/i) {  
             set_binary_extension(defined $1 ? $1 : $2);  
         }  
         return "MIME binary ($1)" if $top_ct =~ m{^\s*([^;\s]+)};  
         return 'MIME binary';  
     }  
   
     # Multipart bodies carry their own MIME headers.  Only classify a part as  
     # binary when its declared media type is non-text, or it is explicitly an  
     # attachment with a filename and a binary transfer encoding.  
     while ($scan =~ /(?:\A|\r?\n)Content-Type:\s*([^\r\n]+)(?:\r?\n[ \t]+[^\r\n]+)*\r?\n(?:(?:Content-(?:Transfer-Encoding|Disposition):[^\r\n]*(?:\r?\n[ \t]+[^\r\n]+)*\r?\n){0,3})/ig) {  
         my $block = $&;  
         my $ct = $1;  
         next if $ct =~ m{^\s*(?:text|message)/}i;  
         next unless $ct =~ m{^\s*(?:application|audio|video|image|model|font)/}i;  
         if ($block =~ /\b(?:name|filename)\s*=\s*(?:"([^"]+)"|([^;\s]+))/i) {  
             set_binary_extension(defined $1 ? $1 : $2);  
         }  
         return "MIME binary part ($1)" if $ct =~ m{^\s*([^;\s]+)};  
         return 'MIME binary part';  
     }  
   
     # Some broken posters omit Content-Type on an attachment part.  
     if ($scan =~ /(?:\A|\r?\n)Content-Disposition:\s*attachment\b[^\r\n]*(?:\r?\n[ \t]+[^\r\n]+)*\r?\n/ig) {  
         my $block = $&;  
         if ($block =~ /\bfilename\s*=\s*(?:"([^"]+)"|([^;\s]+))/i) {  
             set_binary_extension(defined $1 ? $1 : $2);  
         }  
         return 'MIME attachment'  
             if $scan =~ /(?:\A|\r?\n)Content-Transfer-Encoding:\s*(?:base64|binary)\s*$/im;  
     }  
   
     return 0;  
  }  
   
# Return true if the article is a binary, false otherwise. # Return true if the article is a binary, false otherwise.
sub is_binary { sub is_binary {
   return $state{cache_is_binary} if defined $state{cache_is_binary};    return $state{cache_is_binary} if defined $state{cache_is_binary};
   
   # Return False if the article is Base64 encoded.  We want to match binary     my $scan = binary_scan_body(); 
   # content, *not* Base64 encoding.    
   if ($hdr{'Content-Transfer-Encoding'} =~ /^base64$/i     # MIME-declared binary payloads must be caught even when they are much 
   and $hdr{'Content-Type'} =~ /^text/i) {     # shorter than max_base64_lines.  The old code missed these completely. 
       $state{cache_is_binary} = 0;     my $mime_type = mime_binary_type(); 
     if ($mime_type) {  
         $state{cache_is_binary} = $mime_type;  
       return $state{cache_is_binary};        return $state{cache_is_binary};
   };     } 
   
   # yEnc checks: According to yEnc spec, all encoded parts start with     # A base64-encoded text/* article is text, not a misplaced binary. 
   # =ybegin.     if (($hdr{'Content-Transfer-Encoding'} || '') =~ /^\s*base64\s*$/i 
   if ($body =~ /(?:^|\n)=ybegin$hws+(.+)/o) {    and ($hdr{'Content-Type'} || '') =~ /^\s*text\//i) {
       local $_ = $1;         $state{cache_is_binary} = 0; 
       if (/line=/ and /size=/ and /name=/) {         return 0; 
           $state{cache_is_binary} = "yEnc Encoded";     } 
           return $state{cache_is_binary};    
       };    
   };    
   
   # uuEncoded check     # yEnc.  Scan the raw body rather than the lower-cased 4 kB body preview. 
   if ($body =~ /(?:^|\n)begin$hws+(.+)/io) {     # Accept attribute order variations and common non-compliant generators. 
       local $_ = $1;     if ($scan =~ /(?:\A|\r?\n)[ \t]*=ybegin\b([^\r\n]*)/i) { 
       if (/[0-7]{3,4}$hws.*\.(\w{2,4})/o) {         my $attrs = $1; 
           $state{file_extension} = $1;         my ($name) = $attrs =~ /\bname\s*=\s*(.+?)\s*$/i; 
           $state{cache_is_binary} = "uuencoded";         set_binary_extension($name) if defined $name; 
   
         my $has_size = $attrs =~ /\bsize\s*=\s*\d+/i;  
         my $has_line = $attrs =~ /\bline\s*=\s*\d+/i;  
         my $has_name = $attrs =~ /\bname\s*=\s*\S+/i;  
         my $has_part = $attrs =~ /\bpart\s*=\s*\d+/i  
             || $scan =~ /(?:\A|\r?\n)[ \t]*=ypart\b[^\r\n]*/i;  
         my $has_end = $scan =~ /(?:\A|\r?\n)[ \t]*=yend\b[^\r\n]*/i;  
   
         if (($has_size && $has_name && ($has_line || $has_part || $has_end))  
         || ($config{detect_malformed_yenc} && ($has_end || $has_part))) {  
             $state{cache_is_binary} = $has_part ? 'yEnc multipart' : 'yEnc encoded';  
           return $state{cache_is_binary};            return $state{cache_is_binary};
       };         } 
   };     } 
   
     # Catch malformed/case-damaged yEnc produced by a few old or custom  
     # posters, while requiring a second marker to reduce false positives in  
     # discussions that merely mention "=ybegin".  
     if ($config{detect_malformed_yenc}  
     and $scan =~ /(?:\A|\r?\n)[ \t]*=ybegin\b/i  
     and $scan =~ /(?:\A|\r?\n)[ \t]*=y(?:part|end)\b/i) {  
         $state{cache_is_binary} = 'Malformed yEnc';  
         return $state{cache_is_binary};  
     }  
   
   if ($hdr{__BODY__} =~ /     # uuEncoded check. 
     if ($scan =~ /(?:\A|\r?\n)[ \t]*begin$hws+([0-7]{3,4})$hws+([^\r\n]+)/io) {  
         set_binary_extension($2);  
         $state{cache_is_binary} = 'uuencoded';  
         return $state{cache_is_binary};  
     }  
   
     if ($scan =~ /  
       (?:        (?:
           [ \t|>]*                     # Skip Quote Marks              [ \t|>]*  
           (?>                         # Disable Backtracking             (?>M[\x20-\x60]{60,61}) 
           M[\x20-\x60]{60,61}         # uuencoded line             $hws*\x0D?\x0A 
           )$hws*\x0D?\x0A               # End of line         ){4} 
       ){4}                            # Require at least four encoded lines    
   /ox) {    /ox) {
       $state{cache_is_binary} = "uuencoded multi-part";         $state{cache_is_binary} = 'uuencoded multi-part'; 
       return $state{cache_is_binary};        return $state{cache_is_binary};
   };     } 
   
   # We only need to count Base64 lines if the number of lines in the post     # Heuristic for raw Base64 without MIME headers. 
   # exceeds the configured Base64 maximum allowed.    
   if ($state{lines} > $config{max_base64_lines}) {    if ($state{lines} > $config{max_base64_lines}) {
       $state{b64lines} = 0;        $state{b64lines} = 0;
       $state{b64lines}++        $state{b64lines}++
           while $hdr{__BODY__} =~ /^$hws*[A-Za-z0-9\+\/]{59,76}$hws*$/gmo;             while $scan =~ /^$hws*[A-Za-z0-9+\/]{59,76}={0,2}$hws*$/gmo; 
       if ($state{b64lines} > $config{max_base64_lines}) {        if ($state{b64lines} > $config{max_base64_lines}) {
           $state{cache_is_binary} = "Base64 ($state{b64lines} lines)";            $state{cache_is_binary} = "Base64 ($state{b64lines} lines)";
           return $state{cache_is_binary};            return $state{cache_is_binary};
       };         } 
   };     } 
   
   # Message is not binary    
   $state{cache_is_binary} = 0;    $state{cache_is_binary} = 0;
   return 0;    return 0;
}; };
   
# Extract the right-most FQDN from a Path header # Extract the right-most FQDN from a Path header
sub first_path_host{ sub first_path_host{
   local $_ = shift;    local $_ = shift;
   s/(![^\.]+)+$//; # Strip RH non-FQDNs    s/(![^\.]+)+$//; # Strip RH non-FQDNs
   s/!\.POSTED.*//; # Strip diagnostic host entry    s/!\.POSTED.*//; # Strip diagnostic host entry
   s/.*!//; # Strip all but RH path entry    s/.*!//; # Strip all but RH path entry
   return $_;    return $_;
}; };
   
# Attempt to determine the client software # Attempt to determine the client software
sub x_reader { sub x_reader {
   return  lc $hdr{'X-Newsreader'} ||    return  lc $hdr{'X-Newsreader'} ||
           lc $hdr{'User-Agent'}   ||            lc $hdr{'User-Agent'}   ||
           lc $hdr{'X-Newsposter'} ||            lc $hdr{'X-Newsposter'} ||
           lc $hdr{'X-Poster'}     ||            lc $hdr{'X-Poster'}     ||
           lc $hdr{'X-Mailer'}     || '';            lc $hdr{'X-Mailer'}     || '';
} }
   
sub reject { sub reject {
   my ($verbose_reason, $short_reason) = @_;    my ($verbose_reason, $short_reason) = @_;
   
   if (defined &local_filter_reject) {    if (defined &local_filter_reject) {
       ($verbose_reason, $short_reason) = local_filter_reject(@_);        ($verbose_reason, $short_reason) = local_filter_reject(@_);
       return if not $verbose_reason;        return if not $verbose_reason;
   }    }
   
   $short_reason = $verbose_reason unless $short_reason;    $short_reason = $verbose_reason unless $short_reason;
     my $rule = policy_reason_key($short_reason || $verbose_reason);  
     if (trusted_bypass_for_rule($rule)) {  
         policy_count('trusted.bypass', $state{peer}, $groups[0]) if $config{metrics_enabled};  
         policy_log_event('bypass', $rule, $verbose_reason) if $config{policy_log_matches};  
         return '';  
     }  
   
   if ($config{block_late_cancels}    if ($config{block_late_cancels}
# XXX $config{block_extra_reposts} # XXX $config{block_extra_reposts}
# XXX for reposts       and not $hdr{Control} # XXX for reposts       and not $hdr{Control}
       ) {        ) {
       $MIDhistory->add($hdr{'Message-ID'});        $MIDhistory->add($hdr{'Message-ID'});
   }    }
   
   $status{rejected}++;    $status{rejected}++;
     policy_count($rule, $state{peer}, $groups[0]) if $config{metrics_enabled};  
     policy_log_event('reject', $rule, $verbose_reason) if $config{policy_log_matches};  
   
   return $config{verbose} ? $verbose_reason : $short_reason;     record_top_offender($rule); 
     my $code = reject_code($rule);  
     return $config{verbose} ? "[$code] $verbose_reason" : "[$code] $short_reason";  
  }  
   
   
   
  ##############################################################################  
  # Lightweight operational hardening, diagnostics, and anomaly detection  
  ##############################################################################  
  sub reject_code {  
     my ($rule) = @_;  
     my %codes = (  
         'binary.yenc'=>'CF-BINARY-YENC','binary.uuencode'=>'CF-BINARY-UUENCODE',  
         'binary.base64'=>'CF-BINARY-BASE64','binary.mime'=>'CF-BINARY-MIME',  
         'binary.byte_profile'=>'CF-BINARY-BYTE-PROFILE','policy.binary'=>'CF-POLICY-BINARY',  
         'policy.size'=>'CF-POLICY-SIZE','crosspost'=>'CF-CROSSPOST',  
         'malformed.encoding'=>'CF-MALFORMED-ENCODING','structure.long_line'=>'CF-LONG-LINE',  
         'structure.path'=>'CF-PATH-SANITY','anomaly.rate'=>'CF-RATE-ANOMALY',  
         'emp.md5'=>'CF-EMP-MD5','emp.phl'=>'CF-EMP-PHL','emp.phn'=>'CF-EMP-PHN',  
         'emp.phr'=>'CF-EMP-PHR','emp.fsl'=>'CF-EMP-FSL','header.subject'=>'CF-BAD-SUBJECT',  
         'header.from'=>'CF-BAD-FROM','body.pattern'=>'CF-BAD-BODY','body.url'=>'CF-BAD-URL',  
         'supersedes'=>'CF-SUPERSEDES','control'=>'CF-CONTROL','scoring'=>'CF-SCORING',  
         'message_id'=>'CF-MESSAGE-ID','other'=>'CF-OTHER'  
     );  
     return $codes{$rule} || 'CF-' . uc(policy_safe_key($rule || 'other'));  
  }  
   
  sub external_regex_safe {  
     my ($text, $file) = @_;  
     return 1 unless $config{external_regex_reject_unsafe};  
     return 0 if length($text) > $config{external_regex_max_length};  
     my $count = 1 + ($text =~ tr/|/|/);  
     return 0 if $count > $config{external_regex_max_count};  
     return 0 if $text =~ /\(\?\??\{/;                    # executable Perl regex code  
     return 0 if $text =~ /(?:\*|\+|\{\d+,?\})\s*\)(?:\*|\+|\{\d+,?\})/;  
     return 0 if $text =~ /\([^)]*(?:\.\*|\.\+)[^)]*\)(?:\*|\+)/;  
     return 1;  
  }  
   
  sub bad_file_names {  
     return qw(bad_paths bad_cancel_paths bad_adult_paths bad_hosts bad_hosts_central  
               bad_from bad_subject bad_body bad_url bad_url_central trusted_paths  
               trusted_hosts trusted_from trusted_message_ids);  
  }  
  sub update_bad_file_mtimes {  
     return unless $config_dir;  
     for my $f (bad_file_names()) { my $p="$config_dir/$f"; $Bad_File_Mtime{$f}=(-e $p ? (stat($p))[9]||0 : 0); }  
  }  
  sub bad_files_changed {  
     return 0 unless $config_dir;  
     for my $f (bad_file_names()) { my $p="$config_dir/$f"; my $m=(-e $p ? (stat($p))[9]||0 : 0); return 1 if !defined($Bad_File_Mtime{$f}) || $m != $Bad_File_Mtime{$f}; }  
     return 0;  
  }  
  sub maybe_reload_bad_files {  
     my $mode=$config{bad_reload_mode}||'articles';  
     my $reload=0;  
     if (($mode eq 'mtime'||$mode eq 'both') && $now-$Last_Bad_Mtime_Check >= $config{bad_reload_interval}) {  
         $Last_Bad_Mtime_Check=$now; $reload=1 if bad_files_changed();  
     }  
     if (($mode eq 'articles'||$mode eq 'both') && $status{accepted}>0 && $config{bad_rate_reload}>0  
         && $status{accepted}%$config{bad_rate_reload}==0 && $status{accepted}>($status{bad_reloaded}||0)) {  
         $status{bad_reloaded}=$status{accepted}; $reload=1;  
     }  
     if ($reload) { slog('N','Reloading external cleanfeed lists'); read_hashes(); }  
  }  
   
  sub update_config_fingerprint {  
     return unless eval { require Digest::SHA; 1 };  
     my $sha=Digest::SHA->new(256); $sha->add($cleanfeed_maintenance_version);  
     if ($config_dir) { for my $f ('cleanfeed.local',bad_file_names()) { my $p="$config_dir/$f"; next unless -f $p; if(open my $fh,'<',$p){binmode $fh;$sha->addfile($fh);close $fh;} } }  
     $Config_Fingerprint='sha256:'.$sha->hexdigest;  
  }  
   
  sub normalize_domain {  
     my ($d)=@_; $d=lc($d||''); $d =~ s/^\s+|\s+$//g;  
     if ($config{idn_normalization} && eval { require Net::IDN::Encode; 1 }) { eval { $d=Net::IDN::Encode::domain_to_ascii($d); }; }  
     return $d;  
  }  
  sub trusted_article {  
     return 1 if $Trusted_Paths_RE && ($hdr{Path}||'') =~ $Trusted_Paths_RE;  
     return 1 if $Trusted_Hosts_RE && (($state{posting_host}||'') =~ $Trusted_Hosts_RE || ($state{peer}||'') =~ $Trusted_Hosts_RE);  
     return 1 if $Trusted_From_RE && ($hdr{From}||'') =~ $Trusted_From_RE;  
     return 1 if $Trusted_Message_IDs_RE && ($hdr{'Message-ID'}||'') =~ $Trusted_Message_IDs_RE;  
     return 0;  
  }  
  sub trusted_bypass_for_rule {  
     my ($r)=@_; return 0 unless trusted_article();  
     return $config{trusted_bypass_scoring} if $r =~ /^(?:scoring|emp\.)/;  
     return $config{trusted_bypass_content} if $r =~ /^(?:header\.|body\.|malformed)/;  
     return $config{trusted_bypass_binary} if $r =~ /^(?:binary\.|policy\.binary)/;  
     return $config{trusted_bypass_crosspost} if $r eq 'crosspost';  
     return $config{trusted_bypass_size} if $r eq 'policy.size';  
     return 0;  
  }  
   
  sub mode_result {  
     my ($mode,$rule,$reason)=@_; return '' if !$mode || $mode eq 'off';  
     if ($mode eq 'audit') { $status{audited}++; policy_count($rule,$state{peer},$groups[0]); policy_log_event('audit',$rule,$reason); return ''; }  
     return reject($reason,$rule) if $mode eq 'reject'; return '';  
  }  
  sub max_line_length {  
     my ($text)=@_; my $max=0; for my $line (split(/\n/,$text||'',-1)){ my $l=length($line);$max=$l if $l>$max;} return $max;  
  }  
  sub malformed_encoding_reason {  
     return '' unless $config{malformed_encoding_check}; my $scan=binary_scan_body();  
     my $ct=$hdr{'Content-Type'}||''; my $cte=$hdr{'Content-Transfer-Encoding'}||'';  
     return 'Malformed MIME multipart: boundary parameter missing' if $ct =~ /^multipart\//i && $ct !~ /boundary\s*=/i;  
     if ($ct =~ /boundary\s*=\s*"?([^";\s]+)/i) { my $b=$1; return 'Malformed MIME multipart: declared boundary not found' if index($scan,"--$b")<0; }  
     return 'Malformed Base64 transfer encoding: no plausible payload' if $cte =~ /^base64$/i && $scan !~ /(?:^|\n)[A-Za-z0-9+\/]{16,}={0,2}\r?$/m;  
     my $yb=()=$scan=~/(?:^|\n)\s*=ybegin\b/ig; my $ye=()=$scan=~/(?:^|\n)\s*=yend\b/ig;  
     return 'Malformed yEnc: =ybegin without =yend' if $yb && !$ye;  
     return 'Malformed yEnc: =yend without =ybegin' if $ye && !$yb;  
     my ($ybegin_size) = $scan =~ /(?:^|\n)\s*=ybegin\b[^\r\n]*\bsize=(\d+)/i;  
     my ($yend_size) = $scan =~ /(?:^|\n)\s*=yend\b[^\r\n]*\bsize=(\d+)/i;  
     if (defined $ybegin_size && defined $yend_size) {  
         my($a,$b)=($ybegin_size,$yend_size); my $tol=$config{yenc_size_tolerance_percent}; return "Malformed yEnc: inconsistent declared sizes $a/$b" if $a && abs($a-$b)*100/$a > $tol;  
     }  
     return '';  
  }  
  sub byte_profile_reason {  
     return '' unless $config{binary_byte_profile_enabled}; my $raw=substr($hdr{__BODY__}||'',0,$config{binary_ratio_scan_bytes});  
     return '' if length($raw)<$config{binary_ratio_min_bytes}; my $bad=($raw =~ tr/\x00-\x08\x0b\x0c\x0e-\x1f\x7f//);  
     my $pct=int($bad*100/length($raw)); return $pct>$config{binary_nonprintable_ratio_percent} ? "Binary byte profile: nonprintable ratio ${pct}%" : '';  
  }  
  sub path_sanity_reason {  
     return '' unless $config{path_sanity_enabled}; my @p=split(/!/,$hdr{Path}||'',-1);  
     return 'Path sanity: excessive hop count' if @p>$config{path_max_hops}; my %n;  
     for my $t(@p){ return 'Path sanity: empty token' if $t eq ''; return 'Path sanity: token too long' if length($t)>$config{path_max_token_bytes}; $n{lc$t}++; return 'Path sanity: repeated token' if $n{lc$t}>$config{path_repeat_ceiling}; }  
     return '';  
  }  
  sub rate_anomaly_reason {  
     return '' unless $config{anomaly_rate_enabled}; my $bucket=int($now/$config{anomaly_window});  
     for my $x ([$state{peer}||'unknown',\%rate_peer,$config{anomaly_peer_articles},'peer'],[$state{posting_host}||'unknown',\%rate_host,$config{anomaly_host_articles},'posting host']) {  
         my($k,$h,$lim,$label)=@$x; next if !$lim || !$k||$k eq 'unknown'||$k eq '0'; %$h=() if scalar(keys %$h)>$config{anomaly_max_keys}; my $id="$bucket\0$k"; $h->{$id}++; return "Rate anomaly: $label exceeded $lim articles per window" if $h->{$id}>$lim;  
     } return '';  
  }  
  sub apply_lightweight_guards {  
     my $mh=max_line_length(join("\n",map { ($_||'') } values %hdr));  
     if($config{max_header_line_bytes} && $mh>$config{max_header_line_bytes}){ my$r=mode_result($config{long_line_mode},'structure.long_line',"Header line exceeds $config{max_header_line_bytes} bytes");return$r if $r; }  
     my $mb=max_line_length($hdr{__BODY__}||'');  
     if($config{max_body_line_bytes} && $mb>$config{max_body_line_bytes}){ my$r=mode_result($config{long_line_mode},'structure.long_line',"Body line exceeds $config{max_body_line_bytes} bytes");return$r if $r; }  
     for my $c ([malformed_encoding_reason(),$config{malformed_encoding_mode},'malformed.encoding'],[byte_profile_reason(),$config{binary_byte_profile_mode},'binary.byte_profile'],[path_sanity_reason(),$config{path_sanity_mode},'structure.path'],[rate_anomaly_reason(),$config{anomaly_mode},'anomaly.rate']) { my($reason,$mode,$rule)=@$c; next unless $reason; my$r=mode_result($mode,$rule,$reason);return$r if $r; }  
     return '';  
  }  
  sub record_top_offender {  
     my($rule)=@_; return unless $config{top_offenders_enabled};  
     for my $x ([\%top_peer_reject,$state{peer}],[\%top_host_reject,$state{posting_host}],[\%top_group_reject,$groups[0]]) { my($h,$k)=@$x;next unless $k; %$h=() if scalar(keys %$h)>$config{top_offenders_max_keys};$h->{policy_safe_key($k)}++; }  
  }  
  sub append_top_metrics {  
     my($lines)=@_; return unless $config{top_offenders_enabled};  
     for my $set (['top_reject_peer',\%top_peer_reject],['top_reject_host',\%top_host_reject],['top_reject_group',\%top_group_reject]) { my($prefix,$h)=@$set;my$i=0;for my $k(sort{$h->{$b}<=>$h->{$a}||$a cmp $b}keys%$h){last if ++$i>$config{top_offenders_limit};push @$lines,"${prefix}_${i}=$k,$h->{$k}";} }  
  }  
   
  ##############################################################################  
  # Peer/hierarchy policy engine and lightweight metrics  
  ##############################################################################  
  sub policy_peer_identity {  
     return $state{injection_host} if $state{injection_host};  
     return first_path_host($hdr{Path}) if $hdr{Path};  
     return 'unknown';  
  }  
   
  sub policy_merge {  
     my ($base, $extra) = @_;  
     my %merged = %{$base || {}};  
     @merged{keys %$extra} = values %$extra if $extra;  
     return \%merged;  
  }  
   
  sub policy_for_article {  
     my $policy = {  
         mode => $config{policy_mode} || 'audit',  
         max_bytes => $config{policy_default_max_bytes} || 0,  
         allow_binary => $config{policy_default_allow_binary} ? 1 : 0,  
     };  
     my $peer = $state{peer} || 'unknown';  
     for my $pattern (sort { length($a) <=> length($b) || $a cmp $b } keys %Peer_Policies) {  
         next unless $peer =~ /$pattern/;  
         $policy = policy_merge($policy, $Peer_Policies{$pattern});  
     }  
     for my $group (@groups) {  
         for my $pattern (sort { length($a) <=> length($b) || $a cmp $b } keys %Hierarchy_Policies) {  
             next unless $group =~ /$pattern/;  
             $policy = policy_merge($policy, $Hierarchy_Policies{$pattern});  
         }  
     }  
     return $policy;  
  }  
   
  sub apply_policy_engine {  
     my $policy = policy_for_article();  
     my @matches;  
     if ($policy->{max_bytes} && $state{article_bytes} > $policy->{max_bytes}) {  
         push @matches, ['policy.size', "Article size $state{article_bytes} exceeds policy limit $policy->{max_bytes}"];  
     }  
     if (defined $policy->{allow_binary} && !$policy->{allow_binary}) {  
         my $binary = is_binary();  
         push @matches, ['policy.binary', 'Binary payload forbidden by peer/hierarchy policy'] if $binary;  
     }  
     return '' unless @matches;  
   
     my $mode = $policy->{mode} || $config{policy_mode} || 'audit';  
     return '' if $mode eq 'off';  
     for my $match (@matches) {  
         my ($rule, $reason) = @$match;  
         policy_count($rule, $state{peer}, $groups[0]) if $mode ne 'reject';  
         policy_log_event($mode, $rule, $reason) if $config{policy_log_matches};  
     }  
     if ($mode eq 'audit') {  
         $status{audited}++;  
         return '';  
     }  
     if ($mode eq 'quarantine') {  
         $status{quarantined}++;  
         # Logical quarantine: article is accepted but distinctly logged/counted.  
         # Storage or routing decisions belong in INN feed configuration.  
         return '';  
     }  
     return reject($matches[0][1], $matches[0][0]) if $mode eq 'reject';  
     return '';  
  }  
   
  sub policy_reason_key {  
     my ($reason) = @_;  
     $reason = lc($reason || 'other');  
     return 'binary.yenc' if $reason =~ /yenc/;  
     return 'binary.uuencode' if $reason =~ /uuenc/;  
     return 'binary.base64' if $reason =~ /base64/;  
     return 'binary.mime' if $reason =~ /mime|binary payload|misplaced binary/;  
     return 'crosspost' if $reason =~ /crosspost|too many newsgroups|poison newsgroup/;  
     return 'emp.md5' if $reason =~ /md5/;  
     return 'emp.phl' if $reason =~ /phl/;  
     return 'emp.phn' if $reason =~ /phn/;  
     return 'emp.phr' if $reason =~ /phr/;  
     return 'emp.fsl' if $reason =~ /fsl/;  
     return 'header.subject' if $reason =~ /subject/;  
     return 'header.from' if $reason =~ /from|sender|reply-to/;  
     return 'body.pattern' if $reason =~ /body/;  
     return 'body.url' if $reason =~ /url/;  
     return 'supersedes' if $reason =~ /supersedes/;  
     return 'control' if $reason =~ /control|newgroup|rmgroup/;  
     return 'scoring' if $reason =~ /scoring/;  
     return 'message_id' if $reason =~ /message-id/;  
     return 'malformed.encoding' if $reason =~ /malformed|boundary|encoding/;  
     return 'structure.long_line' if $reason =~ /line.*bytes|long line/;  
     return 'structure.path' if $reason =~ /path sanity|path token|path hop|path repeat/;  
     return 'binary.byte_profile' if $reason =~ /nonprintable|byte profile/;  
     return 'anomaly.rate' if $reason =~ /rate anomaly/;  
     return 'other';  
  }  
   
  sub policy_safe_key {  
     my ($value) = @_;  
     $value = lc($value || 'unknown');  
     $value =~ s/[^a-z0-9_.-]+/_/g;  
     return substr($value, 0, 120);  
  }  
   
  sub policy_count {  
     my ($rule, $peer, $hierarchy) = @_;  
     $rule = policy_safe_key($rule);  
     $policy_rule_count{$rule}++ if $config{metrics_by_rule};  
     if ($config{metrics_by_peer} && scalar(keys %policy_peer_count) < $config{policy_max_peer_counters}) {  
         $policy_peer_count{policy_safe_key($peer)}++;  
     }  
     if ($config{metrics_by_hierarchy} && scalar(keys %policy_hierarchy_count) < $config{policy_max_hierarchy_counters}) {  
         my ($root) = split(/\./, $hierarchy || 'unknown', 2);  
         $policy_hierarchy_count{policy_safe_key($root)}++;  
     }  
  }  
   
  sub policy_log_event {  
     my ($action, $rule, $reason) = @_;  
     my @parts = ('cleanfeed_event', "action=" . policy_safe_key($action), "rule=" . policy_safe_key($rule));  
     push @parts, 'peer=' . policy_safe_key($state{peer}) if $config{policy_include_peer};  
     push @parts, 'groups=' . policy_safe_key(join(',', @groups)) if $config{policy_include_groups};  
     push @parts, 'message_id=' . policy_safe_key($hdr{'Message-ID'}) if $config{policy_include_message_id};  
     my $safe_reason = $reason || '';  
     $safe_reason =~ s/[\r\n\t]+/ /g;  
     $safe_reason =~ s/\s+/ /g;  
     push @parts, 'reason="' . substr($safe_reason, 0, 240) . '"' if $safe_reason ne '';  
     slog('N', join(' ', @parts));  
  }  
   
  sub atomic_write_file {  
     my ($path, $content) = @_;  
     return 1 if not $path;  
     my $tmp = "$path.tmp.$$";  
     my $fh;  
     if (!open($fh, '>', $tmp)) { slog('E', "Cannot open $tmp: $!"); return 0; }  
     print {$fh} $content;  
     if (!close($fh)) { unlink $tmp; slog('E', "Cannot close $tmp: $!"); return 0; }  
     if (!rename($tmp, $path)) { unlink $tmp; slog('E', "Cannot rename $tmp to $path: $!"); return 0; }  
     return 1;  
  }  
   
  sub metric_snapshot_lines {  
     my @lines;  
     push @lines, "cleanfeed_version=$cleanfeed_maintenance_version";  
     push @lines, "generated_epoch=$now";  
     push @lines, "started_epoch=$Start_Time";  
     push @lines, 'uptime_seconds=' . ($now - $Start_Time);  
     for my $key (qw(articles accepted rejected refused audited quarantined)) {  
         push @lines, "$key=" . ($status{$key} || 0);  
     }  
     push @lines, "rate_articles_per_second=" . ($timer{rate} || 0);  
     push @lines, "rate_accepted_per_second=" . ($timer{accept_rate} || 0);  
     push @lines, "configuration_fingerprint=$Config_Fingerprint" if $Config_Fingerprint;  
     append_top_metrics(\@lines);  
     push @lines, "reject_rule_$_=$policy_rule_count{$_}" for sort keys %policy_rule_count;  
     push @lines, "peer_$_=$policy_peer_count{$_}" for sort keys %policy_peer_count;  
     push @lines, "hierarchy_$_=$policy_hierarchy_count{$_}" for sort keys %policy_hierarchy_count;  
     return join("\n", @lines) . "\n";  
  }  
   
  sub write_metrics {  
     return unless $config{metrics_enabled};  
     atomic_write_file($config{metrics_status_file}, metric_snapshot_lines()) if $config{metrics_status_file};  
     if ($config{metrics_csv_file} && $now - $Last_Metrics_CSV >= $config{metrics_csv_interval}) {  
         my $new = !-e $config{metrics_csv_file} || -z $config{metrics_csv_file};  
         my $csv;  
         if (open($csv, '>>', $config{metrics_csv_file})) {  
             print {$csv} "epoch,articles,accepted,rejected,refused,audited,quarantined\n" if $new;  
             print {$csv} join(',', $now, map { $status{$_} || 0 } qw(articles accepted rejected refused audited quarantined)) . "\n";  
             close($csv);  
             $Last_Metrics_CSV = $now;  
         } else { slog('E', "Cannot append $config{metrics_csv_file}: $!"); }  
     }  
     if ($config{metrics_syslog}) {  
         slog('N', "metrics articles=" . ($status{articles}||0) . " accepted=" . ($status{accepted}||0) . " rejected=" . ($status{rejected}||0) . " audited=" . ($status{audited}||0) . " quarantined=" . ($status{quarantined}||0));  
     }  
     if ($config{metrics_prometheus_file}) {  
         my @prom = (  
             "cleanfeed_articles_total " . ($status{articles}||0),  
             "cleanfeed_articles_accepted_total " . ($status{accepted}||0),  
             "cleanfeed_articles_rejected_total " . ($status{rejected}||0),  
             "cleanfeed_articles_audited_total " . ($status{audited}||0),  
             "cleanfeed_articles_quarantined_total " . ($status{quarantined}||0),  
         );  
         push @prom, 'cleanfeed_reject_rule_total{rule="' . $_ . '"} ' . $policy_rule_count{$_} for sort keys %policy_rule_count;  
         atomic_write_file($config{metrics_prometheus_file}, join("\n", @prom) . "\n");  
     }  
} }
   
############################################################################## ##############################################################################
# other functions called by INN # other functions called by INN
############################################################################## ##############################################################################
   
# examine message-id during CHECK and IHAVE transactions (INN only) # examine message-id during CHECK and IHAVE transactions (INN only)
sub filter_messageid { sub filter_messageid {
   return '' if not $config{do_mid_filter};    return '' if not $config{do_mid_filter};
   my ($id) = @_;    my ($id) = @_;
   
   if ($config{refuse_messageids} and $id =~ /$config{refuse_messageids}/io) {    if ($config{refuse_messageids} and $id =~ /$config{refuse_messageids}/io) {
       $status{refused}++;        $status{refused}++;
       return 'No';        return 'No';
   }    }
   
   if ($config{block_late_cancels}    if ($config{block_late_cancels}
           and (($id =~ /^<cancel\.[a-z0-9]{4}\.(.+)/            and (($id =~ /^<cancel\.[a-z0-9]{4}\.(.+)/
                   and $MIDhistory->check('<'.$1))                    and $MIDhistory->check('<'.$1))
               or ($id =~ /^<cancel\.(.+)/ and $MIDhistory->check('<'.$1)))) {                or ($id =~ /^<cancel\.(.+)/ and $MIDhistory->check('<'.$1)))) {
       $status{refused}++;        $status{refused}++;
       return 'No';        return 'No';
   }    }
   
   return '';    return '';
} }
   
sub filter_mode { sub filter_mode {
   if ($config{do_emp_dump}) {    if ($config{do_emp_dump}) {
       if ($mode{NewMode} eq 'throttled') {        if ($mode{NewMode} eq 'throttled') {
           dump_emp();            dump_emp();
       } elsif ($mode{NewMode} eq 'running') {        } elsif ($mode{NewMode} eq 'running') {
           restore_emp() if $mode{Mode} eq 'throttled';            restore_emp() if $mode{Mode} eq 'throttled';
       }        }
   }    }
   
   slog('N', 'Meow unto the greatness of Fluffy, Ruler of All Usenet')    slog('N', 'Meow unto the greatness of Fluffy, Ruler of All Usenet')
       if lc $mode{reason} eq 'meow';        if lc $mode{reason} eq 'meow';
   
   return;    return;
} }
   
# a status line in "ctlinnd mode" output (INN only). # a status line in "ctlinnd mode" output (INN only).
# (requires the "mode.patch" to innd or equivalent). # (requires the "mode.patch" to innd or equivalent).
sub filter_stats { sub filter_stats {
   my $md5hashentries = $MD5history ? $MD5history->count : 0;    my $md5hashentries = $MD5history ? $MD5history->count : 0;
   my $phlhashentries = $PHLhistory ? $PHLhistory->count : 0;    my $phlhashentries = $PHLhistory ? $PHLhistory->count : 0;
   my $phnhashentries = $PHNhistory ? $PHNhistory->count : 0;    my $phnhashentries = $PHNhistory ? $PHNhistory->count : 0;
   my $phrhashentries = $PHRhistory ? $PHRhistory->count : 0;    my $phrhashentries = $PHRhistory ? $PHRhistory->count : 0;
   my $fslhashentries = $FSLhistory ? $FSLhistory->count : 0;    my $fslhashentries = $FSLhistory ? $FSLhistory->count : 0;
     
   my $string = "Pass: $status{accepted}  Reject: $status{rejected}";    my $string = "Pass: $status{accepted}  Reject: $status{rejected}";
   $string .= "  Refuse: $status{refused}" if $config{do_mid_filter};    $string .= "  Refuse: $status{refused}" if $config{do_mid_filter};
   $string .= "  MD5: $md5hashentries  PHL: $phlhashentries  PHN: $phnhashentries";    $string .= "  MD5: $md5hashentries  PHL: $phlhashentries  PHN: $phnhashentries";
   $string .= "  PHR: $phrhashentries  FSL: $fslhashentries";    $string .= "  PHR: $phrhashentries  FSL: $fslhashentries";
   $string .= "  Arts/sec: $timer{rate}  Accept/sec: $timer{accept_rate}"    $string .= "  Arts/sec: $timer{rate}  Accept/sec: $timer{accept_rate}"
       if $config{timer_info} and $timer{rate};        if $config{timer_info} and $timer{rate};
   $string .= "  cleanfeed.conf NOT loaded!" if $Local_Conf_Err;    $string .= "  cleanfeed.conf NOT loaded!" if $Local_Conf_Err;
   
   return $string;    return $string;
} }
   
############################################################################## ##############################################################################
# functions to write the report files # functions to write the report files
############################################################################## ##############################################################################
   
# Write an HTML statfile # Write an HTML statfile
sub write_html_stats { sub write_html_stats {
   if (not open(HTML, ">$config{html_statfile}")) {    if (not open(HTML, ">$config{html_statfile}")) {
       slog('E', "Cannot open $config{html_statfile}: $!");        slog('E', "Cannot open $config{html_statfile}: $!");
       return;        return;
   }    }
   
   print HTML "<html>\n<head>\n"    print HTML "<html>\n<head>\n"
   . "<title>Cleanfeed Status</title>\n"    . "<title>Cleanfeed Status</title>\n"
   . "</head>\n<body>\n\n"    . "</head>\n<body>\n\n"
   . "<p>\n";    . "<p>\n";
   #print HTML "<b>Cleanfeed Version:</b> $version ($version_date)<br>\n"    #print HTML "<b>Cleanfeed Version:</b> $version ($version_date)<br>\n"
   #    if $version and $version_date;    #    if $version and $version_date;
   print HTML "<b>Filter started:</b> " . scalar(localtime $Start_Time) . "<br>\n"    print HTML "<b>Filter started:</b> " . scalar(localtime $Start_Time) . "<br>\n"
   . "<b>Report generated:</b> " . scalar(localtime) . "<br>\n"    . "<b>Report generated:</b> " . scalar(localtime) . "<br>\n"
   . 'Uptime: ' . ($now - $Start_Time) . " seconds\n"    . 'Uptime: ' . ($now - $Start_Time) . " seconds\n"
   . "\n<p>\n"    . "\n<p>\n"
   . "<b>Accepted:</b> $status{accepted}<br>\n"    . "<b>Accepted:</b> $status{accepted}<br>\n"
   . "<b>Rejected:</b> $status{rejected}\n";    . "<b>Rejected:</b> $status{rejected}\n";
   print HTML "<br><b>Refused:</b> $status{refused}\n"    print HTML "<br><b>Refused:</b> $status{refused}\n"
       if $config{do_mid_filter};        if $config{do_mid_filter};
   
   if ($config{timer_info} and $timer{rate}) {    if ($config{timer_info} and $timer{rate}) {
       print HTML "\n<p>\n"        print HTML "\n<p>\n"
       . "Period since last report: $timer{interval} seconds<br>\n"        . "Period since last report: $timer{interval} seconds<br>\n"
       . "Articles examined (this period): $timer{rate}/s<br>\n"        . "Articles examined (this period): $timer{rate}/s<br>\n"
       . "Articles accepted (this period): $timer{accept_rate}/s<br>\n"        . "Articles accepted (this period): $timer{accept_rate}/s<br>\n"
       . "Articles examined (entire uptime): $timer{total_rate}/s<br>\n"        . "Articles examined (entire uptime): $timer{total_rate}/s<br>\n"
       . "Articles accepted (entire uptime): $timer{total_accept_rate}/s\n";        . "Articles accepted (entire uptime): $timer{total_accept_rate}/s\n";
   }    }
   
   my $md5hashentries = $MD5history ? $MD5history->count : 0;    my $md5hashentries = $MD5history ? $MD5history->count : 0;
   my $phlhashentries = $PHLhistory ? $PHLhistory->count : 0;    my $phlhashentries = $PHLhistory ? $PHLhistory->count : 0;
   my $phnhashentries = $PHNhistory ? $PHNhistory->count : 0;    my $phnhashentries = $PHNhistory ? $PHNhistory->count : 0;
   my $phrhashentries = $PHRhistory ? $PHRhistory->count : 0;    my $phrhashentries = $PHRhistory ? $PHRhistory->count : 0;
   my $fslhashentries = $FSLhistory ? $FSLhistory->count : 0;    my $fslhashentries = $FSLhistory ? $FSLhistory->count : 0;
   my $superentries   = $Suphistory ? $Suphistory->count : 0;    my $superentries   = $Suphistory ? $Suphistory->count : 0;
   my $midhistentries = $MIDhistory->count;    my $midhistentries = $MIDhistory->count;
   my $md5count = $MD5history ? $MD5history->overflowed : 0;    my $md5count = $MD5history ? $MD5history->overflowed : 0;
   my $phlcount = $PHLhistory ? $PHLhistory->overflowed : 0;    my $phlcount = $PHLhistory ? $PHLhistory->overflowed : 0;
   my $phncount = $PHNhistory ? $PHNhistory->overflowed : 0;    my $phncount = $PHNhistory ? $PHNhistory->overflowed : 0;
   my $phrcount = $PHRhistory ? $PHRhistory->overflowed : 0;    my $phrcount = $PHRhistory ? $PHRhistory->overflowed : 0;
   my $fslcount = $FSLhistory ? $FSLhistory->overflowed : 0;    my $fslcount = $FSLhistory ? $FSLhistory->overflowed : 0;
   
   print HTML "\n<p>\n"    print HTML "\n<p>\n"
   . "<b>MD5 entries:</b> $md5hashentries <b>Rejecting:</b> $md5count<br>\n"    . "<b>MD5 entries:</b> $md5hashentries <b>Rejecting:</b> $md5count<br>\n"
   . "<b>PHL entries:</b> $phlhashentries <b>Rejecting:</b> $phlcount<br>\n"    . "<b>PHL entries:</b> $phlhashentries <b>Rejecting:</b> $phlcount<br>\n"
   . "<b>PHN entries:</b> $phnhashentries <b>Rejecting:</b> $phncount<br>\n"    . "<b>PHN entries:</b> $phnhashentries <b>Rejecting:</b> $phncount<br>\n"
   . "<b>PHR entries:</b> $phrhashentries <b>Rejecting:</b> $phrcount<br>\n"    . "<b>PHR entries:</b> $phrhashentries <b>Rejecting:</b> $phrcount<br>\n"
   . "<b>FSL entries:</b> $fslhashentries <b>Rejecting:</b> $fslcount<br>\n"    . "<b>FSL entries:</b> $fslhashentries <b>Rejecting:</b> $fslcount<br>\n"
   . "<b>MID history:</b> $midhistentries\n";    . "<b>MID history:</b> $midhistentries\n";
   
   print HTML "\n<p>\n<blink>cleanfeed.conf <b>NOT</b> loaded!</blink>\n"    print HTML "\n<p>\n<blink>cleanfeed.conf <b>NOT</b> loaded!</blink>\n"
       if $Local_Conf_Err;        if $Local_Conf_Err;
   
   print HTML "\n<p>\nSupersedes entries: $superentries\n";    print HTML "\n<p>\nSupersedes entries: $superentries\n";
   if ($Suphistory) {    if ($Suphistory) {
       print HTML "<ul>\n";        print HTML "<ul>\n";
       my $items = $Suphistory->items;        my $items = $Suphistory->items;
       foreach (sort keys %$items) {        foreach (sort keys %$items) {
           print HTML "<li>$_: $items->{$_}\n";            print HTML "<li>$_: $items->{$_}\n";
       }        }
       print HTML "</ul>\n";        print HTML "</ul>\n";
   }    }
   
   print HTML "</body></html>\n";    print HTML "</body></html>\n";
   close HTML;    close HTML;
} }
   
# write a crude stat file including accept/reject numbers, # write a crude stat file including accept/reject numbers,
# hash sizes, and current configuration # hash sizes, and current configuration
sub writestats { sub writestats {
   my $noreset = $_[0] || 0;    my $noreset = $_[0] || 0;
   $Last_Stats = $now unless $noreset;    $Last_Stats = $now unless $noreset;
   
   timer_stats() if $config{timer_info};    timer_stats() if $config{timer_info};
   
   write_html_stats() if $config{html_statfile};    write_html_stats() if $config{html_statfile};
     write_metrics();  
   
   return if not ($config{statfile} or $config{inn_syslog_status});    return if not ($config{statfile} or $config{inn_syslog_status});
   
   my $md5hashentries = $MD5history ? $MD5history->count : 0;    my $md5hashentries = $MD5history ? $MD5history->count : 0;
   my $phlhashentries = $PHLhistory ? $PHLhistory->count : 0;    my $phlhashentries = $PHLhistory ? $PHLhistory->count : 0;
   my $phnhashentries = $PHNhistory ? $PHNhistory->count : 0;    my $phnhashentries = $PHNhistory ? $PHNhistory->count : 0;
   my $phrhashentries = $PHRhistory ? $PHRhistory->count : 0;    my $phrhashentries = $PHRhistory ? $PHRhistory->count : 0;
   my $fslhashentries = $FSLhistory ? $FSLhistory->count : 0;    my $fslhashentries = $FSLhistory ? $FSLhistory->count : 0;
   my $superentries   = $Suphistory ? $Suphistory->count : 0;    my $superentries   = $Suphistory ? $Suphistory->count : 0;
   my $midhistentries = $MIDhistory->count;    my $midhistentries = $MIDhistory->count;
   
   if ($config{inn_syslog_status}) {    if ($config{inn_syslog_status}) {
       my $message = 'status: ';        my $message = 'status: ';
       $message .= "accepted $status{accepted} rejected $status{rejected}";        $message .= "accepted $status{accepted} rejected $status{rejected}";
       $message .= " refused $status{refused}" if $config{do_mid_filter};        $message .= " refused $status{refused}" if $config{do_mid_filter};
       $message .= " md5 $md5hashentries" if $md5hashentries;        $message .= " md5 $md5hashentries" if $md5hashentries;
       $message .= " phl $phlhashentries" if $phlhashentries;        $message .= " phl $phlhashentries" if $phlhashentries;
       $message .= " fsl $fslhashentries" if $fslhashentries;        $message .= " fsl $fslhashentries" if $fslhashentries;
       $message .= " arts/s $timer{rate} accept/s $timer{accept_rate}"        $message .= " arts/s $timer{rate} accept/s $timer{accept_rate}"
           if $config{timer_info} and $timer{rate};            if $config{timer_info} and $timer{rate};
       $message .= " WARNING cleanfeed.local NOT loaded" if $Local_Conf_Err;        $message .= " WARNING cleanfeed.local NOT loaded" if $Local_Conf_Err;
   
       slog('N', $message);        slog('N', $message);
   }    }
   
   return if not $config{statfile};    return if not $config{statfile};
   
   if (not open FILE, ">$config{statfile}") {    if (not open FILE, ">$config{statfile}") {
       slog('E', "Cannot open $config{statfile}: $!");        slog('E', "Cannot open $config{statfile}: $!");
       return;        return;
   }    }
   print FILE "Cleanfeed Version: $version ($version_date)\n"    print FILE "Cleanfeed Version: $version ($version_date)\n"
       if $version and $version_date;        if $version and $version_date;
   print FILE 'Filter started: ' . scalar(localtime $Start_Time) . "\n"    print FILE 'Filter started: ' . scalar(localtime $Start_Time) . "\n"
   . 'Report generated: ' . scalar(localtime) . "\n"    . 'Report generated: ' . scalar(localtime) . "\n"
   . 'Uptime: ' . ($now - $Start_Time) . " seconds\n\n"    . 'Uptime: ' . ($now - $Start_Time) . " seconds\n\n"
   . "Accepted: $status{accepted}\nRejected: $status{rejected}\n";    . "Accepted: $status{accepted}\nRejected: $status{rejected}\n";
   print FILE "Refused: $status{refused}\n" if $config{do_mid_filter};    print FILE "Refused: $status{refused}\n" if $config{do_mid_filter};
   print FILE "MD5 entries: $md5hashentries\n"    print FILE "MD5 entries: $md5hashentries\n"
   . "PHL entries: $phlhashentries\n"    . "PHL entries: $phlhashentries\n"
   . "PHN entries: $phnhashentries\n"    . "PHN entries: $phnhashentries\n"
   . "PHR entries: $phrhashentries\n"    . "PHR entries: $phrhashentries\n"
   . "FSL entries: $fslhashentries\n"    . "FSL entries: $fslhashentries\n"
   . "MID history: $midhistentries\n\n";    . "MID history: $midhistentries\n\n";
   if ($config{timer_info} and $timer{rate}) {    if ($config{timer_info} and $timer{rate}) {
       print FILE "Articles examined per second: $timer{rate}\n";        print FILE "Articles examined per second: $timer{rate}\n";
       print FILE "Articles accepted per second: $timer{accept_rate}\n";        print FILE "Articles accepted per second: $timer{accept_rate}\n";
   }    }
   
   print FILE "\ncleanfeed.local NOT loaded! Check file permissions.\n"    print FILE "\ncleanfeed.local NOT loaded! Check file permissions.\n"
       if $Local_Conf_Err;        if $Local_Conf_Err;
   
   print FILE "\nSupersedes entries: $superentries\n";    print FILE "\nSupersedes entries: $superentries\n";
   if ($Suphistory) {    if ($Suphistory) {
       my $items = $Suphistory->items;        my $items = $Suphistory->items;
       foreach (sort keys %$items) {        foreach (sort keys %$items) {
           print FILE "  $_: $items->{$_}\n";            print FILE "  $_: $items->{$_}\n";
       }        }
   }    }
   
   print FILE "\n\nCurrent configuration:\n\n";    print FILE "\n\nCurrent configuration:\n\n";
   foreach my $item (sort keys %config) {    foreach my $item (sort keys %config) {
       print FILE "$item: $config{$item}\n"        print FILE "$item: $config{$item}\n"
   }    }
   
   # Report on bad_ files read as Hash tables    # Report on bad_ files read as Hash tables
   print FILE "\n\nBad Hashes from Files:\n\n";    print FILE "\n\nBad Hashes from Files:\n\n";
   foreach (qw(Bad_Path Bad_Cancel_Path Bad_Adult_Path Bad_Hosts    foreach (qw(Bad_Path Bad_Cancel_Path Bad_Adult_Path Bad_Hosts
               Bad_Hosts_Central)) {                Bad_Hosts_Central)) {
       print FILE "$_:\n";        print FILE "$_:\n";
       if (keys(%$_) > 0) {        if (keys(%$_) > 0) {
           for my $hitem ( sort keys %$_ ) {            for my $hitem ( sort keys %$_ ) {
               print FILE "$hitem\n";                print FILE "$hitem\n";
           };            };
       } else {        } else {
           print FILE "Not Defined\n";            print FILE "Not Defined\n";
       };        };
       print FILE "\n";        print FILE "\n";
   };    };
   
   # Report on bad_ files read as Regexs    # Report on bad_ files read as Regexs
   print FILE "\nBad Regular Expressions from Files:\n\n";    print FILE "\nBad Regular Expressions from Files:\n\n";
   foreach (qw(Bad_From Bad_Body Bad_Subject Bad_URL Bad_URL_Central)) {    foreach (qw(Bad_From Bad_Body Bad_Subject Bad_URL Bad_URL_Central)) {
       print FILE "$_:\n$$_\n" if $$_;        print FILE "$_:\n$$_\n" if $$_;
       print FILE "$_:\nNot Defined\n\n" if not $$_;        print FILE "$_:\nNot Defined\n\n" if not $$_;
   };    };
   
   close FILE;    close FILE;
} }
   
# figure out how many articles per second we're looking at and accepting # figure out how many articles per second we're looking at and accepting
# $timer{articles} - how many we've seen since last time # $timer{articles} - how many we've seen since last time
# $timer{accepted} - how many we've accepted since last time # $timer{accepted} - how many we've accepted since last time
# $timer{time} - time of last check # $timer{time} - time of last check
# $timer{interval} - interval time for this check # $timer{interval} - interval time for this check
# $timer{rate} - articles checked per second during this interval # $timer{rate} - articles checked per second during this interval
# $timer{accept_rate} - articles accepted per second during this interval # $timer{accept_rate} - articles accepted per second during this interval
# $timer{total_rate} - articles checked per second since we've been running # $timer{total_rate} - articles checked per second since we've been running
# $timer{total_accept_rate} - art. accepted per second since we've been running # $timer{total_accept_rate} - art. accepted per second since we've been running
sub timer_stats { sub timer_stats {
   my $uptime = $now - $Start_Time;    my $uptime = $now - $Start_Time;
   
   $timer{interval} = $now - $timer{time} || 1;    $timer{interval} = $now - $timer{time} || 1;
   $timer{rate} = (int ($timer{articles} / $timer{interval} * 10)) / 10;    $timer{rate} = (int ($timer{articles} / $timer{interval} * 10)) / 10;
   $timer{accept_rate} = (int ($timer{accepted} / $timer{interval} * 10)) / 10;    $timer{accept_rate} = (int ($timer{accepted} / $timer{interval} * 10)) / 10;
   $timer{total_rate} = (int ($status{articles} / $uptime * 10)) / 10;    $timer{total_rate} = (int ($status{articles} / $uptime * 10)) / 10;
   $timer{total_accept_rate} = (int ($status{accepted} / $uptime * 10)) / 10;    $timer{total_accept_rate} = (int ($status{accepted} / $uptime * 10)) / 10;
   
   $timer{time} = $now;    $timer{time} = $now;
   $timer{articles} = 0;    $timer{articles} = 0;
   $timer{accepted} = 0;    $timer{accepted} = 0;
   return 1;    return 1;
} }
   
sub trimhashes { sub trimhashes {
   $MD5history->trim if $MD5history;    $MD5history->trim if $MD5history;
   $PHLhistory->trim if $PHLhistory;    $PHLhistory->trim if $PHLhistory;
   $PHNhistory->trim if $PHNhistory;    $PHNhistory->trim if $PHNhistory;
   $PHRhistory->trim if $PHRhistory;    $PHRhistory->trim if $PHRhistory;
   $FSLhistory->trim if $FSLhistory;    $FSLhistory->trim if $FSLhistory;
   $Suphistory->trim if $Suphistory;    $Suphistory->trim if $Suphistory;
   $MIDhistory->trim;    $MIDhistory->trim;
   
   # rotate log if necessary    # rotate log if necessary
   if ($Do_Log == 1) {    if ($Do_Log == 1) {
       if (($config{max_log_size} and -s $Log_File > $config{max_log_size})        if (($config{max_log_size} and -s $Log_File > $config{max_log_size})
               or -e $config{rotate_file}) {                or -e $config{rotate_file}) {
           rotate_log();            rotate_log();
           unlink $config{rotate_file};            unlink $config{rotate_file};
       }        }
   }          }      
   
   $Last_Trim = $now;    $Last_Trim = $now;
} }
   
############################################################################## ##############################################################################
# debugging functions to save articles # debugging functions to save articles
############################################################################## ##############################################################################
sub logart { sub logart {
   # Output format is controlled at a bit level.    # Output format is controlled at a bit level.
   # Some args are ambiguous, for example if bit 3 (Include Body) is 0 then    # Some args are ambiguous, for example if bit 3 (Include Body) is 0 then
   # bits 4 and 5 are ignored.    # bits 4 and 5 are ignored.
   # 1(1)   = Include Post Headers    # 1(1)   = Include Post Headers
   # 2(2)   = Cleanfeed Internals    # 2(2)   = Cleanfeed Internals
   # 3(4)   = Include Body    # 3(4)   = Include Body
   # 4(8)   = Truncate Body (On / Off)    # 4(8)   = Truncate Body (On / Off)
   # 5(16)  = Processed Body (Lowercase, Truncated, Base64 Decoded)    # 5(16)  = Processed Body (Lowercase, Truncated, Base64 Decoded)
   # 6(32)  = Just Posting-Host    # 6(32)  = Just Posting-Host
   # 7(64)  = Just Newsgroups    # 7(64)  = Just Newsgroups
   # 8(128) = Just Message-ID    # 8(128) = Just Message-ID
   # 9(256) = Debug Cleanfeed Internals    # 9(256) = Debug Cleanfeed Internals
   
   my ($file, $info, $format) = @_;    my ($file, $info, $format) = @_;
   # Default format value is 15.    # Default format value is 15.
   $format ||= 15;    $format ||= 15;
   
   return if not $config{debug_batch_directory};    return if not $config{debug_batch_directory};
   checkrotate("$config{debug_batch_directory}/$file");    checkrotate("$config{debug_batch_directory}/$file");
   
   if (not open(LOCAL, ">>$config{debug_batch_directory}/$file")) {    if (not open(LOCAL, ">>$config{debug_batch_directory}/$file")) {
       slog('E', "Cannot open $file: $!");        slog('E', "Cannot open $file: $!");
       return;        return;
   }    }
   
   # Begin Header cutmark    # Begin Header cutmark
   if ($format & 259) {    if ($format & 259) {
       print LOCAL "From foo\@bar Thu Jan  1 00:00:01 1970\n";        print LOCAL "From foo\@bar Thu Jan  1 00:00:01 1970\n";
   };    };
   
   # Cleanfeed Internals    # Cleanfeed Internals
   if ($format & 2) {    if ($format & 2) {
       print LOCAL "INFO: $info\n" if $info;        print LOCAL "INFO: $info\n" if $info;
       print LOCAL "Score: $score\n" if $score;        print LOCAL "Score: $score\n" if $score;
       print LOCAL "Binary: $state{cache_is_binary}\n" if $state{cache_is_binary}        print LOCAL "Binary: $state{cache_is_binary}\n" if $state{cache_is_binary}
           and not $format & 256;            and not $format & 256;
   };    };
   
   # Article Headers    # Article Headers
   if ($format & 1) {    if ($format & 1) {
       foreach (sort keys %hdr) {        foreach (sort keys %hdr) {
           next if $_ eq '__BODY__' or $_ eq '__LINES__';            next if $_ eq '__BODY__' or $_ eq '__LINES__';
           print LOCAL "$_: $hdr{$_}\n";            print LOCAL "$_: $hdr{$_}\n";
       };        };
   } else {    } else {
       # Just Posting-Host        # Just Posting-Host
       print LOCAL "$state{posting_host}\n" if $format & 32;        print LOCAL "$state{posting_host}\n" if $format & 32;
       # Just Newsgroups        # Just Newsgroups
       print LOCAL "$hdr{'Newsgroups'}\n" if $format & 64;        print LOCAL "$hdr{'Newsgroups'}\n" if $format & 64;
       # Just Message-ID        # Just Message-ID
       print LOCAL "$hdr{'Message-ID'}\n" if $format & 128;        print LOCAL "$hdr{'Message-ID'}\n" if $format & 128;
   };    };
   
   # Debug Cleanfeed Internals    # Debug Cleanfeed Internals
   if ($format & 256) {    if ($format & 256) {
       foreach (sort keys %state) {        foreach (sort keys %state) {
           print LOCAL "state{$_}: $state{$_}\n" if $state{$_};            print LOCAL "state{$_}: $state{$_}\n" if $state{$_};
       };        };
       foreach (sort keys %gr) {        foreach (sort keys %gr) {
           print LOCAL "gr{$_}: $gr{$_}\n" if $gr{$_};            print LOCAL "gr{$_}: $gr{$_}\n" if $gr{$_};
       };        };
   };    };
   
   # End of headers cutmark    # End of headers cutmark
   if ($format & 259) {    if ($format & 259) {
       print LOCAL "\n";        print LOCAL "\n";
   };    };
   
   if ($format & 4) {    if ($format & 4) {
       # Truncated and processed __BODY__        # Truncated and processed __BODY__
       if (not $format & 16) {        if (not $format & 16) {
           # Full, unprocessed body            # Full, unprocessed body
           print LOCAL "$hdr{__BODY__}\n" if not $format & 8;            print LOCAL "$hdr{__BODY__}\n" if not $format & 8;
           # Truncated, unprocessed body            # Truncated, unprocessed body
           print LOCAL substr($hdr{__BODY__}, 0, 4000) . "\n\n"            print LOCAL substr($hdr{__BODY__}, 0, 4000) . "\n\n"
           if $format & 8 and not is_binary();            if $format & 8 and not is_binary();
           # Make binary even shorter            # Make binary even shorter
           print LOCAL substr($hdr{__BODY__}, 0, 320) . "\n\n"            print LOCAL substr($hdr{__BODY__}, 0, 320) . "\n\n"
           if $format & 8 and is_binary();            if $format & 8 and is_binary();
       } else {        } else {
           # processed body (Both truncated or not)            # processed body (Both truncated or not)
           print LOCAL "$body\n\n";            print LOCAL "$body\n\n";
       };        };
   }; # End of body    }; # End of body
   close LOCAL;    close LOCAL;
   return;    return;
}; # End of logart }; # End of logart
   
sub saveart { sub saveart {
   # We recognise two formatting options:    # We recognise two formatting options:
   # 0: Header and body truncated if over 50 lines (Default)    # 0: Header and body truncated if over 50 lines (Default)
   # 1: Header and full body regardless of length    # 1: Header and full body regardless of length
   
   my ($file, $info, $format) = @_;    my ($file, $info, $format) = @_;
   $format ||= 0;    $format ||= 0;
   
   return if not $config{debug_batch_directory};    return if not $config{debug_batch_directory};
   checkrotate("$config{debug_batch_directory}/$file");    checkrotate("$config{debug_batch_directory}/$file");
   
   if (not open(LOCAL, ">>$config{debug_batch_directory}/$file")) {    if (not open(LOCAL, ">>$config{debug_batch_directory}/$file")) {
       slog('E', "Cannot open $file: $!");        slog('E', "Cannot open $file: $!");
       return;        return;
   }    }
   
   # Headers common to all remaining report types.    # Headers common to all remaining report types.
   print LOCAL "From foo\@bar Thu Jan  1 00:00:01 1970\n";    print LOCAL "From foo\@bar Thu Jan  1 00:00:01 1970\n";
   print LOCAL "INFO: $info\n" if $info;    print LOCAL "INFO: $info\n" if $info;
   print LOCAL "Score: $score\n" if $score;    print LOCAL "Score: $score\n" if $score;
   print LOCAL "Binary: $state{cache_is_binary}\n"    print LOCAL "Binary: $state{cache_is_binary}\n"
       if is_binary();        if is_binary();
   foreach (sort keys %hdr) {    foreach (sort keys %hdr) {
       next if $_ eq '__BODY__' or $_ eq '__LINES__';        next if $_ eq '__BODY__' or $_ eq '__LINES__';
       print LOCAL "$_: $hdr{$_}\n";        print LOCAL "$_: $hdr{$_}\n";
   };    };
   
   if ($format == 1) {     # Format 1 - Full message payload.    if ($format == 1) {     # Format 1 - Full message payload.
       print LOCAL "$hdr{__BODY__}\n";        print LOCAL "$hdr{__BODY__}\n";
   } elsif ($format == 0) {     # Format 0 - Cropped payload    } elsif ($format == 0) {     # Format 0 - Cropped payload
       print LOCAL substr($hdr{__BODY__}, 0, 3000) . "\n\n";        print LOCAL substr($hdr{__BODY__}, 0, 3000) . "\n\n";
   } else {    # Undefined format, give up (just headers logged)    } else {    # Undefined format, give up (just headers logged)
       print LOCAL "\n";        print LOCAL "\n";
   };    };
   
   close LOCAL;    close LOCAL;
   return;    return;
}; };
   
# See if batch file is oversized and if so, rotate it # See if batch file is oversized and if so, rotate it
sub checkrotate { sub checkrotate {
   my ($batchfile) = @_;    my ($batchfile) = @_;
   my $num = 1;    my $num = 1;
   
   return if not $config{debug_batch_size}    return if not $config{debug_batch_size}
       or -s $batchfile < $config{debug_batch_size};        or -s $batchfile < $config{debug_batch_size};
   
   $num += 1 while -e "$batchfile.$num";       # Ensure filename is unique    $num += 1 while -e "$batchfile.$num";       # Ensure filename is unique
   rename $batchfile, "$batchfile.$num";       # Move it out of the way    rename $batchfile, "$batchfile.$num";       # Move it out of the way
} }
   
############################################################################## ##############################################################################
# internal state dump and restore # internal state dump and restore
############################################################################## ##############################################################################
sub dump_emp { sub dump_emp {
   return if not $config{emp_dump_file};    return if not $config{emp_dump_file};
   
   if (not open(DUMP, ">$config{emp_dump_file}")) {    if (not open(DUMP, ">$config{emp_dump_file}")) {
       slog('E', "EMP database could not be dumped: $!");        slog('E', "EMP database could not be dumped: $!");
       return;        return;
   }    }
   
   $MD5history->dump('MD5history', \*DUMP) if $MD5history;    $MD5history->dump('MD5history', \*DUMP) if $MD5history;
   $PHLhistory->dump('PHLhistory', \*DUMP) if $PHLhistory;    $PHLhistory->dump('PHLhistory', \*DUMP) if $PHLhistory;
   $PHNhistory->dump('PHNhistory', \*DUMP) if $PHNhistory;    $PHNhistory->dump('PHNhistory', \*DUMP) if $PHNhistory;
   $PHRhistory->dump('PHRhistory', \*DUMP) if $PHRhistory;    $PHRhistory->dump('PHRhistory', \*DUMP) if $PHRhistory;
   $FSLhistory->dump('FSLhistory', \*DUMP) if $FSLhistory;    $FSLhistory->dump('FSLhistory', \*DUMP) if $FSLhistory;
   
   close DUMP;    close DUMP;
   
   slog('N', 'Saved EMP database.');    slog('N', 'Saved EMP database.');
} }
   
sub restore_emp { sub restore_emp {
   return if not $config{emp_dump_file} or not -r $config{emp_dump_file};    return if not $config{emp_dump_file} or not -r $config{emp_dump_file};
   
   do $config{emp_dump_file};    do $config{emp_dump_file};
   
   # delete the data of checks which have been disabled since the last dump    # delete the data of checks which have been disabled since the last dump
   undef $MD5history if not $config{do_md5};    undef $MD5history if not $config{do_md5};
   undef $PHLhistory if not $config{do_phl};    undef $PHLhistory if not $config{do_phl};
   undef $PHNhistory if not $config{do_phn};    undef $PHNhistory if not $config{do_phn};
   undef $PHRhistory if not $config{do_phr};    undef $PHRhistory if not $config{do_phr};
   undef $FSLhistory if not $config{do_fsl};    undef $FSLhistory if not $config{do_fsl};
   
   # We can't syslog at startup because INN doesn't provide the callbacks    # We can't syslog at startup because INN doesn't provide the callbacks
   # in time    # in time
   slog('N', 'Restored EMP database.') if not defined $Start_Time;    slog('N', 'Restored EMP database.') if not defined $Start_Time;
} }
   
sub slog { sub slog {
   return if not defined &INN::syslog;    return if not defined &INN::syslog;
   INN::syslog(@_);    INN::syslog(@_);
} }
   
############################################################################## ##############################################################################
# parse the data files # parse the data files
############################################################################## ##############################################################################
sub read_hashes { sub read_hashes {
   read_hash('bad_paths', \%Bad_Path);    read_hash('bad_paths', \%Bad_Path);
   read_hash('bad_cancel_paths', \%Bad_Cancel_Path);    read_hash('bad_cancel_paths', \%Bad_Cancel_Path);
   read_hash('bad_adult_paths', \%Bad_Adult_Path);    read_hash('bad_adult_paths', \%Bad_Adult_Path);
   read_hash('bad_hosts', \%Bad_Hosts);    read_hash('bad_hosts', \%Bad_Hosts);
   read_hash('bad_hosts_central', \%Bad_Hosts_Central);    read_hash('bad_hosts_central', \%Bad_Hosts_Central);
     load_external_regex('trusted_paths', 'Trusted_Paths', 'Trusted_Paths_RE', sub { qr/(?:$_[0])/i });  
   # Bad From Headers     load_external_regex('trusted_hosts', 'Trusted_Hosts', 'Trusted_Hosts_RE', sub { qr/(?:$_[0])/i }); 
   read_regex('bad_from', \$Bad_From);     load_external_regex('trusted_from', 'Trusted_From', 'Trusted_From_RE', sub { qr/(?:$_[0])/i }); 
   $Bad_From_RE = qr/($Bad_From)/i if $Bad_From;     load_external_regex('trusted_message_ids', 'Trusted_Message_IDs', 'Trusted_Message_IDs_RE', sub { qr/(?:$_[0])/i }); 
   # Bad Subject Headers    
   read_regex('bad_subject', \$Bad_Subject);     # Compile external regex files defensively. A malformed expression must not 
   $Bad_Subject_RE = qr/($Bad_Subject)/i if $Bad_Subject;     # terminate innd or replace the last known-good compiled expression. 
   # Bad Body     load_external_regex('bad_from', 'Bad_From', 'Bad_From_RE', sub { qr/($_[0])/i }); 
   read_regex('bad_body', \$Bad_Body);     load_external_regex('bad_subject', 'Bad_Subject', 'Bad_Subject_RE', sub { qr/($_[0])/i }); 
   $Bad_Body_RE = qr/($Bad_Body)/ if $Bad_Body;     load_external_regex('bad_body', 'Bad_Body', 'Bad_Body_RE', sub { qr/($_[0])/ }); 
   # Bad URL's in Body     load_external_regex('bad_url', 'Bad_URL', 'Bad_URL_RE', sub { qr/$HTTP\S*($_[0])/i }); 
   read_regex('bad_url', \$Bad_URL);     load_external_regex('bad_url_central', 'Bad_URL_Central', 'Bad_URL_Central_RE', sub { qr/$HTTP\S*($_[0])/i }); 
   $Bad_URL_RE = qr/$HTTP\S*($Bad_URL)/i if $Bad_URL;     update_bad_file_mtimes(); 
   # Bad URL's in Body (Central Resource)     update_config_fingerprint() if $config{config_fingerprint_enabled}; 
   read_regex('bad_url_central', \$Bad_URL_Central);    
   $Bad_URL_Central_RE = qr/$HTTP\S*($Bad_URL_Central)/i if $Bad_URL_Central;    
}; };
   
  sub load_external_regex {  
     my ($file, $text_name, $compiled_name, $builder) = @_;  
     no strict 'refs';  
   
     my $candidate = '';  
     read_regex($file, \$candidate);  
     if ($candidate eq '') {  
         ${$text_name} = '';  
         ${$compiled_name} = undef;  
         return 1;  
     }  
   
     if (!external_regex_safe($candidate, $file)) {  
         slog('E', "Ignoring unsafe $file regex; keeping previous compiled value");  
         return 0;  
     }  
     my $compiled = eval { $builder->($candidate) };  
     if (not $compiled or $@) {  
         slog('E', "Ignoring invalid $file regex; keeping previous compiled value: $@");  
         return 0;  
     }  
     ${$text_name} = $candidate;  
     ${$compiled_name} = $compiled;  
     return 1;  
  }  
   
sub read_hash { sub read_hash {
   my ($file, $hash) = @_;    my ($file, $hash) = @_;
   
   my @list;    my @list;
   read_file("$config_dir/$file", \@list);    read_file("$config_dir/$file", \@list);
   %$hash = map { $_ => 1 } @list;    %$hash = map { $_ => 1 } @list;
} }
   
sub read_regex { sub read_regex {
   my ($file, $regex) = @_;    my ($file, $regex) = @_;
   
   my @list;    my @list;
   read_file("$config_dir/$file", \@list);    read_file("$config_dir/$file", \@list);
   $$regex = join('|', @list);    $$regex = join('|', @list);
   $$regex =~ s#\|\|#|#g;    $$regex =~ s#\|\|#|#g;
} }
   
sub read_file { sub read_file {
   my ($file, $array) = @_;    my ($file, $array) = @_;
   
   return if not -e $file;    return if not -e $file;
   if (not open(FILE, $file)) {    if (not open(FILE, $file)) {
       slog('E', "Cannot open $file: $!");        slog('E', "Cannot open $file: $!");
       return;        return;
   }    }
   while (<FILE>) {    while (<FILE>) {
       s/#.*//;        s/#.*//;
       s/^$hws*(.*?)$hws*$/$1/;        s/^$hws*(.*?)$hws*$/$1/;
       next if /^$/;        next if /^$/;
       if (/\s/) {        if (/\s/) {
           push @$array, split;            push @$array, split;
       } else {        } else {
           push @$array, $_;            push @$array, $_;
       }        }
   }    }
   close FILE;    close FILE;
} }
   
sub messageid_re { sub messageid_re {
   my $mdtext = '[\x21-\x3d\x3f\x41-\x5a\x5e-\x7e]+';    my $mdtext = '[\x21-\x3d\x3f\x41-\x5a\x5e-\x7e]+';
   my $nofoldliteral = '\[' . $mdtext . '\]';    my $nofoldliteral = '\[' . $mdtext . '\]';
   my $idright = '(?:' . $mdtext . '|' . $nofoldliteral . ')';    my $idright = '(?:' . $mdtext . '|' . $nofoldliteral . ')';
   my $idleft = $mdtext;    my $idleft = $mdtext;
   my $msgidcore = $idleft . '\@' . $idright;    my $msgidcore = $idleft . '\@' . $idright;
   my $msgid = '<' . $msgidcore . '>';    my $msgid = '<' . $msgidcore . '>';
   my $messageid = '^[\x20\x09]*' . $msgid . '[\x20\x09]*$';    my $messageid = '^[\x20\x09]*' . $msgid . '[\x20\x09]*$';
   my $messageidre = qr/$messageid/;    my $messageidre = qr/$messageid/;
   return $messageidre;    return $messageidre;
}; };
   
print $fullURL if 0; # lint food print $fullURL if 0; # lint food
   
############################################################################## ##############################################################################
# EMP filters # EMP filters
############################################################################## ##############################################################################
package Cleanfeed::RateLimit; package Cleanfeed::RateLimit;
   
use strict; use strict;
   
sub new { sub new {
   my $class = shift;    my $class = shift;
   my $self = {    my $self = {
       ratecutoff => 4,    # reject if this many copies are in the history        ratecutoff => 4,    # reject if this many copies are in the history
       rateceiling => 85,  # only count this high        rateceiling => 85,  # only count this high
       ratebaseinterval => 7200, # how long to wait before decrementing count        ratebaseinterval => 7200, # how long to wait before decrementing count
       history => { },        history => { },
   };    };
   bless $self, $class;    bless $self, $class;
   return $self;    return $self;
} }
   
sub init { sub init {
   my ($self, $rco, $rc, $rb) = @_;    my ($self, $rco, $rc, $rb) = @_;
   $self->{ratecutoff} = $rco if defined $rco;    $self->{ratecutoff} = $rco if defined $rco;
   $self->{rateceiling} = $rc if defined $rc;    $self->{rateceiling} = $rc if defined $rc;
   $self->{ratebaseinterval} = $rb if defined $rb;    $self->{ratebaseinterval} = $rb if defined $rb;
   
   $self->{dectable} = $self->make_curve_table($self->{rateceiling} + 1,    $self->{dectable} = $self->make_curve_table($self->{rateceiling} + 1,
       $self->{ratebaseinterval});        $self->{ratebaseinterval});
} }
   
# return true if over ratecutoff # return true if over ratecutoff
sub add { sub add {
   my ($self, $elem) = @_;    my ($self, $elem) = @_;
   
   $self->{history}->{$elem}[0] = 0 if not exists $self->{history}->{$elem};    $self->{history}->{$elem}[0] = 0 if not exists $self->{history}->{$elem};
   $self->{history}->{$elem} = [ $self->{history}->{$elem}[0] + 1, time ];    $self->{history}->{$elem} = [ $self->{history}->{$elem}[0] + 1, time ];
   $self->{history}->{$elem}[0] = $self->{rateceiling}    $self->{history}->{$elem}[0] = $self->{rateceiling}
       if $self->{history}->{$elem}[0] > $self->{rateceiling};        if $self->{history}->{$elem}[0] > $self->{rateceiling};
   
   return 1 if $self->{history}->{$elem}[0] > $self->{ratecutoff};    return 1 if $self->{history}->{$elem}[0] > $self->{ratecutoff};
   return 0;    return 0;
} }
   
sub add2 { sub add2 {
   my ($self, $elem, $ratecutoff) = @_;    my ($self, $elem, $ratecutoff) = @_;
   
   $self->{history}->{$elem}[0] = 0 if not exists $self->{history}->{$elem};    $self->{history}->{$elem}[0] = 0 if not exists $self->{history}->{$elem};
   $self->{history}->{$elem} = [ $self->{history}->{$elem}[0] + 1, time ];    $self->{history}->{$elem} = [ $self->{history}->{$elem}[0] + 1, time ];
   $self->{history}->{$elem}[0] = $self->{rateceiling}    $self->{history}->{$elem}[0] = $self->{rateceiling}
       if $self->{history}->{$elem}[0] > $self->{rateceiling};        if $self->{history}->{$elem}[0] > $self->{rateceiling};
   
   return 1 if $self->{history}->{$elem}[0] > $ratecutoff;    return 1 if $self->{history}->{$elem}[0] > $ratecutoff;
   return 0;    return 0;
} }
   
sub trim { sub trim {
   my ($self) = @_;    my ($self) = @_;
   my $now = time;    my $now = time;
   
   my @del;    my @del;
   while (my ($id, $val) = each %{$self->{history}}) {    while (my ($id, $val) = each %{$self->{history}}) {
       if ($now - $val->[1] > $self->{dectable}->[$val->[0]]) {        if ($now - $val->[1] > $self->{dectable}->[$val->[0]]) {
           $self->{history}->{$id}[0]--;            $self->{history}->{$id}[0]--;
           $self->{history}->{$id}[1] = $now;            $self->{history}->{$id}[1] = $now;
       }        }
       push @del, $id if $self->{history}->{$id}[0] < 1;        push @del, $id if $self->{history}->{$id}[0] < 1;
   }    }
   delete @{$self->{history}}{@del};    delete @{$self->{history}}{@del};
} }
   
sub count { sub count {
   my ($self) = @_;    my ($self) = @_;
   return scalar keys %{$self->{history}};    return scalar keys %{$self->{history}};
} }
   
sub overflowed { sub overflowed {
   my ($self) = @_;    my ($self) = @_;
   my $count = 0;    my $count = 0;
   
   foreach (keys %{$self->{history}}) {    foreach (keys %{$self->{history}}) {
       $count++ if $self->{history}->{$_}[0] > $self->{ratecutoff};        $count++ if $self->{history}->{$_}[0] > $self->{ratecutoff};
   }    }
   return $count;    return $count;
} }
   
sub dump { sub dump {
   my ($self, $name, $fd) = @_;    my ($self, $name, $fd) = @_;
   
   my $dd = Data::Dumper->new([ $self->{history} ], [ $name.'->{history}' ]);    my $dd = Data::Dumper->new([ $self->{history} ], [ $name.'->{history}' ]);
   $dd->Indent(1);    $dd->Indent(1);
   print $fd $dd->Dumpxs;    print $fd $dd->Dumpxs;
} }
   
sub items { sub items {
   my ($self) = @_;    my ($self) = @_;
   
   return {    return {
       map { $_ => @{$self->{history}->{$_}}[0] } keys %{$self->{history}}        map { $_ => @{$self->{history}->{$_}}[0] } keys %{$self->{history}}
   };    };
} }
   
# Create a lookup table of values on a descending curve # Create a lookup table of values on a descending curve
sub make_curve_table { sub make_curve_table {
   my ($self, $xmax, $ymax) = @_;    my ($self, $xmax, $ymax) = @_;
   my @values;    my @values;
   
   for (1..$xmax) {    for (1..$xmax) {
       $values[$_] = $ymax - int((($_ / $xmax) ** 2) * $ymax);        $values[$_] = $ymax - int((($_ / $xmax) ** 2) * $ymax);
   }    }
   return \@values;    return \@values;
} }
   
############################################################################## ##############################################################################
package Cleanfeed::Queue; package Cleanfeed::Queue;
   
sub new { sub new {
   my $class = shift;    my $class = shift;
   my $self = {    my $self = {
       maxlife => 3600,        maxlife => 3600,
       history => { },        history => { },
   };    };
   bless $self, $class;    bless $self, $class;
   return $self;    return $self;
} }
   
sub add { sub add {
   my ($self, $elem) = @_;    my ($self, $elem) = @_;
   
   $self->{history}->{$elem} = time;    $self->{history}->{$elem} = time;
} }
   
sub check { sub check {
   my ($self, $elem) = @_;    my ($self, $elem) = @_;
   
   return 1 if exists $self->{history}->{$elem};    return 1 if exists $self->{history}->{$elem};
   return 0;    return 0;
} }
   
sub count { sub count {
   return scalar keys %{$_[0]->{history}};    return scalar keys %{$_[0]->{history}};
} }
   
sub maxlife { sub maxlife {
   my $self = $_[0];    my $self = $_[0];
   $self->{maxlife} = $_[1] if $_[1];    $self->{maxlife} = $_[1] if $_[1];
   $self->{maxlife} = $_[1];    $self->{maxlife} = $_[1];
} }
   
sub trim { sub trim {
   my ($self) = @_;    my ($self) = @_;
   my $now = time;    my $now = time;
   
   my @del;    my @del;
   while (my ($id, $val) = each %{$self->{history}}) {    while (my ($id, $val) = each %{$self->{history}}) {
       push @del, $id if $now - $val > $self->{maxlife};        push @del, $id if $now - $val > $self->{maxlife};
   }    }
   delete @{$self->{history}}{@del};    delete @{$self->{history}}{@del};
} }
   
1; 1;