]> git.proxmox.com Git - proxmox-spamassassin.git/commitdiff
update SpamAssassin to 4.0.0
authorStoiko Ivanov <s.ivanov@proxmox.com>
Mon, 13 Mar 2023 20:13:17 +0000 (21:13 +0100)
committerStoiko Ivanov <s.ivanov@proxmox.com>
Mon, 13 Mar 2023 20:13:17 +0000 (21:13 +0100)
generated by make update-upstream

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
475 files changed:
upstream/CREDITS
upstream/Changes
upstream/INSTALL
upstream/INSTALL.VMS
upstream/MANIFEST
upstream/MANIFEST.SKIP
upstream/META.json
upstream/META.yml
upstream/Makefile.PL
upstream/PACKAGING
upstream/README
upstream/UPGRADE
upstream/USAGE
upstream/build/mkrules
upstream/build/sha256sum.pl
upstream/build/sha512sum.pl
upstream/ldap/README
upstream/lib/Mail/SpamAssassin.pm
upstream/lib/Mail/SpamAssassin/ArchiveIterator.pm
upstream/lib/Mail/SpamAssassin/AsyncLoop.pm
upstream/lib/Mail/SpamAssassin/AutoWelcomelist.pm [new file with mode: 0644]
upstream/lib/Mail/SpamAssassin/AutoWhitelist.pm [deleted file]
upstream/lib/Mail/SpamAssassin/Bayes.pm
upstream/lib/Mail/SpamAssassin/BayesStore/BDB.pm
upstream/lib/Mail/SpamAssassin/BayesStore/DBM.pm
upstream/lib/Mail/SpamAssassin/BayesStore/MySQL.pm
upstream/lib/Mail/SpamAssassin/BayesStore/PgSQL.pm
upstream/lib/Mail/SpamAssassin/BayesStore/Redis.pm
upstream/lib/Mail/SpamAssassin/BayesStore/SQL.pm
upstream/lib/Mail/SpamAssassin/Client.pm
upstream/lib/Mail/SpamAssassin/Conf.pm
upstream/lib/Mail/SpamAssassin/Conf/LDAP.pm
upstream/lib/Mail/SpamAssassin/Conf/Parser.pm
upstream/lib/Mail/SpamAssassin/Conf/SQL.pm
upstream/lib/Mail/SpamAssassin/Constants.pm
upstream/lib/Mail/SpamAssassin/DBBasedAddrList.pm
upstream/lib/Mail/SpamAssassin/Dns.pm
upstream/lib/Mail/SpamAssassin/DnsResolver.pm
upstream/lib/Mail/SpamAssassin/GeoDB.pm [new file with mode: 0644]
upstream/lib/Mail/SpamAssassin/HTML.pm
upstream/lib/Mail/SpamAssassin/Locales.pm
upstream/lib/Mail/SpamAssassin/Locker.pm
upstream/lib/Mail/SpamAssassin/Locker/Flock.pm
upstream/lib/Mail/SpamAssassin/Locker/UnixNFSSafe.pm
upstream/lib/Mail/SpamAssassin/Locker/Win32.pm
upstream/lib/Mail/SpamAssassin/Logger.pm
upstream/lib/Mail/SpamAssassin/Logger/File.pm
upstream/lib/Mail/SpamAssassin/Logger/Stderr.pm
upstream/lib/Mail/SpamAssassin/Logger/Syslog.pm
upstream/lib/Mail/SpamAssassin/Message.pm
upstream/lib/Mail/SpamAssassin/Message/Metadata/Received.pm
upstream/lib/Mail/SpamAssassin/Message/Node.pm
upstream/lib/Mail/SpamAssassin/NetSet.pm
upstream/lib/Mail/SpamAssassin/PerMsgLearner.pm
upstream/lib/Mail/SpamAssassin/PerMsgStatus.pm
upstream/lib/Mail/SpamAssassin/PersistentAddrList.pm
upstream/lib/Mail/SpamAssassin/Plugin.pm
upstream/lib/Mail/SpamAssassin/Plugin/ASN.pm
upstream/lib/Mail/SpamAssassin/Plugin/AWL.pm
upstream/lib/Mail/SpamAssassin/Plugin/AccessDB.pm
upstream/lib/Mail/SpamAssassin/Plugin/AntiVirus.pm
upstream/lib/Mail/SpamAssassin/Plugin/AskDNS.pm
upstream/lib/Mail/SpamAssassin/Plugin/AuthRes.pm [new file with mode: 0644]
upstream/lib/Mail/SpamAssassin/Plugin/AutoLearnThreshold.pm
upstream/lib/Mail/SpamAssassin/Plugin/Bayes.pm
upstream/lib/Mail/SpamAssassin/Plugin/BodyEval.pm
upstream/lib/Mail/SpamAssassin/Plugin/BodyRuleBaseExtractor.pm
upstream/lib/Mail/SpamAssassin/Plugin/Check.pm
upstream/lib/Mail/SpamAssassin/Plugin/DCC.pm
upstream/lib/Mail/SpamAssassin/Plugin/DKIM.pm
upstream/lib/Mail/SpamAssassin/Plugin/DMARC.pm [new file with mode: 0644]
upstream/lib/Mail/SpamAssassin/Plugin/DNSEval.pm
upstream/lib/Mail/SpamAssassin/Plugin/DecodeShortURLs.pm [new file with mode: 0644]
upstream/lib/Mail/SpamAssassin/Plugin/ExtractText.pm [new file with mode: 0644]
upstream/lib/Mail/SpamAssassin/Plugin/FreeMail.pm
upstream/lib/Mail/SpamAssassin/Plugin/FromNameSpoof.pm
upstream/lib/Mail/SpamAssassin/Plugin/HTMLEval.pm
upstream/lib/Mail/SpamAssassin/Plugin/HTTPSMismatch.pm
upstream/lib/Mail/SpamAssassin/Plugin/HashBL.pm
upstream/lib/Mail/SpamAssassin/Plugin/Hashcash.pm [deleted file]
upstream/lib/Mail/SpamAssassin/Plugin/HeaderEval.pm
upstream/lib/Mail/SpamAssassin/Plugin/ImageInfo.pm
upstream/lib/Mail/SpamAssassin/Plugin/MIMEEval.pm
upstream/lib/Mail/SpamAssassin/Plugin/MIMEHeader.pm
upstream/lib/Mail/SpamAssassin/Plugin/OLEVBMacro.pm
upstream/lib/Mail/SpamAssassin/Plugin/OneLineBodyRuleType.pm
upstream/lib/Mail/SpamAssassin/Plugin/PDFInfo.pm
upstream/lib/Mail/SpamAssassin/Plugin/PhishTag.pm
upstream/lib/Mail/SpamAssassin/Plugin/Phishing.pm
upstream/lib/Mail/SpamAssassin/Plugin/Pyzor.pm
upstream/lib/Mail/SpamAssassin/Plugin/Razor2.pm
upstream/lib/Mail/SpamAssassin/Plugin/RelayCountry.pm
upstream/lib/Mail/SpamAssassin/Plugin/RelayEval.pm
upstream/lib/Mail/SpamAssassin/Plugin/ReplaceTags.pm
upstream/lib/Mail/SpamAssassin/Plugin/ResourceLimits.pm
upstream/lib/Mail/SpamAssassin/Plugin/Reuse.pm
upstream/lib/Mail/SpamAssassin/Plugin/Rule2XSBody.pm
upstream/lib/Mail/SpamAssassin/Plugin/SPF.pm
upstream/lib/Mail/SpamAssassin/Plugin/Shortcircuit.pm
upstream/lib/Mail/SpamAssassin/Plugin/SpamCop.pm
upstream/lib/Mail/SpamAssassin/Plugin/TextCat.pm
upstream/lib/Mail/SpamAssassin/Plugin/TxRep.pm
upstream/lib/Mail/SpamAssassin/Plugin/URIDNSBL.pm
upstream/lib/Mail/SpamAssassin/Plugin/URIDetail.pm
upstream/lib/Mail/SpamAssassin/Plugin/URIEval.pm
upstream/lib/Mail/SpamAssassin/Plugin/URILocalBL.pm
upstream/lib/Mail/SpamAssassin/Plugin/VBounce.pm
upstream/lib/Mail/SpamAssassin/Plugin/WLBLEval.pm
upstream/lib/Mail/SpamAssassin/Plugin/WelcomeListSubject.pm [new file with mode: 0644]
upstream/lib/Mail/SpamAssassin/Plugin/WhiteListSubject.pm [deleted file]
upstream/lib/Mail/SpamAssassin/PluginHandler.pm
upstream/lib/Mail/SpamAssassin/RegistryBoundaries.pm
upstream/lib/Mail/SpamAssassin/SQLBasedAddrList.pm
upstream/lib/Mail/SpamAssassin/SpamdForkScaling.pm
upstream/lib/Mail/SpamAssassin/Util.pm
upstream/lib/Mail/SpamAssassin/Util/DependencyInfo.pm
upstream/lib/Mail/SpamAssassin/Util/Progress.pm
upstream/lib/spamassassin-run.pod
upstream/rules-extras/10_uridnsbl_skip_financial.cf [new file with mode: 0644]
upstream/rules-extras/README.txt [new file with mode: 0644]
upstream/rules.README [deleted file]
upstream/rules/20_aux_tlds.cf
upstream/rules/active.list
upstream/rules/init.pre
upstream/rules/languages
upstream/rules/local.cf
upstream/rules/v310.pre
upstream/rules/v312.pre
upstream/rules/v341.pre
upstream/rules/v342.pre
upstream/rules/v343.pre
upstream/rules/v400.pre [new file with mode: 0644]
upstream/sa-awl.raw
upstream/sa-check_spamd.raw
upstream/sa-compile.raw
upstream/sa-learn.raw
upstream/sa-update.raw
upstream/spamassassin.raw
upstream/spamc/configure
upstream/spamc/configure.in
upstream/spamc/libspamc.c
upstream/spamc/libspamc.h
upstream/spamc/spamc.c
upstream/spamc/spamc.pod
upstream/spamd-apache2/README.apache
upstream/spamd-apache2/lib/Mail/SpamAssassin/Spamd/Config.pm
upstream/spamd/README
upstream/spamd/spamd.raw
upstream/sql/README
upstream/sql/README.awl
upstream/sql/bayes_mysql.sql
upstream/sql/decodeshorturl_mysql.sql [new file with mode: 0644]
upstream/sql/decodeshorturl_pg.sql [new file with mode: 0644]
upstream/sql/decodeshorturl_sqlite.sql [new file with mode: 0644]
upstream/sql/txrep_pg.sql
upstream/sql/txrep_sqlite.sql
upstream/t/README
upstream/t/SATest.pm
upstream/t/all_modules.t
upstream/t/arc.t [new file with mode: 0755]
upstream/t/askdns.t [new file with mode: 0755]
upstream/t/authres.t [new file with mode: 0755]
upstream/t/autolearn.t
upstream/t/autolearn_force.t
upstream/t/autolearn_force_fail.t
upstream/t/basic_lint.t
upstream/t/basic_lint_net.t [new file with mode: 0755]
upstream/t/basic_lint_without_sandbox.t
upstream/t/basic_meta.t
upstream/t/basic_meta2.t [changed mode: 0644->0755]
upstream/t/basic_obj_api.t
upstream/t/bayesbdb.t
upstream/t/bayesdbm.t
upstream/t/bayesdbm_flock.t
upstream/t/bayessdbm.t
upstream/t/bayessdbm_seen_delete.t
upstream/t/bayessql.t
upstream/t/blacklist_autolearn.t
upstream/t/blocklist_autolearn.t [new file with mode: 0755]
upstream/t/body_mod.t
upstream/t/body_str.t [changed mode: 0644->0755]
upstream/t/check_implemented.t
upstream/t/cidrs.t
upstream/t/config.dist
upstream/t/config_errs.t
upstream/t/config_tree_recurse.t
upstream/t/cross_user_config_leak.t [changed mode: 0644->0755]
upstream/t/data/01_test_rules.cf
upstream/t/data/01_test_rules.pre
upstream/t/data/Dumpheaders.pm
upstream/t/data/dkim/arc/ko01.eml [new file with mode: 0644]
upstream/t/data/dkim/arc/ok01.eml [new file with mode: 0644]
upstream/t/data/geodb/GeoIP2-City.mmdb [new file with mode: 0644]
upstream/t/data/geodb/GeoIP2-Country.mmdb [new file with mode: 0644]
upstream/t/data/geodb/GeoIP2-ISP.mmdb [new file with mode: 0644]
upstream/t/data/geodb/GeoIPCity.dat [new file with mode: 0644]
upstream/t/data/geodb/GeoIPISP.dat [new file with mode: 0644]
upstream/t/data/geodb/create_GeoIPCity.README [new file with mode: 0644]
upstream/t/data/geodb/create_GeoIPISP.README [new file with mode: 0644]
upstream/t/data/geodb/create_ipcc.sh [new file with mode: 0644]
upstream/t/data/geodb/ipcc.db [new file with mode: 0644]
upstream/t/data/nice.mbox [new file with mode: 0644]
upstream/t/data/nice/authres [new file with mode: 0644]
upstream/t/data/nice/dmarc/noneok.eml [new file with mode: 0644]
upstream/t/data/nice/dmarc/quarok.eml [new file with mode: 0644]
upstream/t/data/nice/dmarc/rejectok.eml [new file with mode: 0644]
upstream/t/data/nice/dmarc/strictrejectok.eml [new file with mode: 0644]
upstream/t/data/nice/orig_ip_hdr.eml
upstream/t/data/nice/spf1
upstream/t/data/nice/spf2
upstream/t/data/nice/spf3
upstream/t/data/nice/spf3-received-spf
upstream/t/data/nice/spf4-received-spf-nofold [new file with mode: 0644]
upstream/t/data/nice/spf5-received-spf-crlf [new file with mode: 0644]
upstream/t/data/nice/spf6-received-spf-crlf2 [new file with mode: 0644]
upstream/t/data/nice/unicode1 [new file with mode: 0644]
upstream/t/data/nice/unicode2 [new file with mode: 0644]
upstream/t/data/spam/decodeshorturl/base.eml [new file with mode: 0644]
upstream/t/data/spam/decodeshorturl/base2.eml [new file with mode: 0644]
upstream/t/data/spam/decodeshorturl/chain.eml [new file with mode: 0644]
upstream/t/data/spam/dmarc/nodmarc.eml [new file with mode: 0644]
upstream/t/data/spam/dmarc/noneko.eml [new file with mode: 0644]
upstream/t/data/spam/dmarc/quarko.eml [new file with mode: 0644]
upstream/t/data/spam/dmarc/rejectko.eml [new file with mode: 0644]
upstream/t/data/spam/dmarc/strictrejectko.eml [new file with mode: 0644]
upstream/t/data/spam/extracttext/gtube_b64_oct.eml [new file with mode: 0644]
upstream/t/data/spam/extracttext/gtube_pdf.eml [new file with mode: 0644]
upstream/t/data/spam/extracttext/gtube_png.eml [new file with mode: 0644]
upstream/t/data/spam/freemail1 [new file with mode: 0644]
upstream/t/data/spam/freemail2 [new file with mode: 0644]
upstream/t/data/spam/freemail3 [new file with mode: 0644]
upstream/t/data/spam/fromnamespoof/spoof1 [new file with mode: 0644]
upstream/t/data/spam/gtubedcc_crlf.eml [new file with mode: 0644]
upstream/t/data/spam/hashbl [new file with mode: 0644]
upstream/t/data/spam/olevbmacro/target_uri.eml [new file with mode: 0644]
upstream/t/data/spam/pyzor [new file with mode: 0644]
upstream/t/data/spam/razor2
upstream/t/data/spam/relayUS.eml
upstream/t/data/spam/unicode1 [new file with mode: 0644]
upstream/t/data/spam/urilocalbl_net.eml [new file with mode: 0644]
upstream/t/data/welcomelists/action.eff.org [new file with mode: 0644]
upstream/t/data/welcomelists/amazon_co_uk_ship [new file with mode: 0644]
upstream/t/data/welcomelists/amazon_com_ship [new file with mode: 0644]
upstream/t/data/welcomelists/cert.org [new file with mode: 0644]
upstream/t/data/welcomelists/debian_bts_reassign [new file with mode: 0644]
upstream/t/data/welcomelists/ibm_enews_de [new file with mode: 0644]
upstream/t/data/welcomelists/infoworld [new file with mode: 0644]
upstream/t/data/welcomelists/linuxplanet [new file with mode: 0644]
upstream/t/data/welcomelists/lp.org [new file with mode: 0644]
upstream/t/data/welcomelists/media_unspun [new file with mode: 0644]
upstream/t/data/welcomelists/mlist_mailman_message [new file with mode: 0644]
upstream/t/data/welcomelists/mlist_yahoo_groups_message [new file with mode: 0644]
upstream/t/data/welcomelists/mypoints [new file with mode: 0644]
upstream/t/data/welcomelists/neat_net_tricks [new file with mode: 0644]
upstream/t/data/welcomelists/netcenter-direct_de [new file with mode: 0644]
upstream/t/data/welcomelists/netsol_renewal [new file with mode: 0644]
upstream/t/data/welcomelists/networkworld [new file with mode: 0644]
upstream/t/data/welcomelists/oracle_net_techblast [new file with mode: 0644]
upstream/t/data/welcomelists/orbitz.com [new file with mode: 0644]
upstream/t/data/welcomelists/paypal.com [new file with mode: 0644]
upstream/t/data/welcomelists/register.com_password [new file with mode: 0644]
upstream/t/data/welcomelists/ryanairmail.com [new file with mode: 0644]
upstream/t/data/welcomelists/sf.net [new file with mode: 0644]
upstream/t/data/welcomelists/winxpnews.com [new file with mode: 0644]
upstream/t/data/welcomelists/yahoo-inc.com [new file with mode: 0644]
upstream/t/data/whitelists/action.eff.org [deleted file]
upstream/t/data/whitelists/amazon_co_uk_ship [deleted file]
upstream/t/data/whitelists/amazon_com_ship [deleted file]
upstream/t/data/whitelists/cert.org [deleted file]
upstream/t/data/whitelists/debian_bts_reassign [deleted file]
upstream/t/data/whitelists/ibm_enews_de [deleted file]
upstream/t/data/whitelists/infoworld [deleted file]
upstream/t/data/whitelists/linuxplanet [deleted file]
upstream/t/data/whitelists/lp.org [deleted file]
upstream/t/data/whitelists/media_unspun [deleted file]
upstream/t/data/whitelists/mlist_mailman_message [deleted file]
upstream/t/data/whitelists/mlist_yahoo_groups_message [deleted file]
upstream/t/data/whitelists/mypoints [deleted file]
upstream/t/data/whitelists/neat_net_tricks [deleted file]
upstream/t/data/whitelists/netcenter-direct_de [deleted file]
upstream/t/data/whitelists/netsol_renewal [deleted file]
upstream/t/data/whitelists/networkworld [deleted file]
upstream/t/data/whitelists/oracle_net_techblast [deleted file]
upstream/t/data/whitelists/orbitz.com [deleted file]
upstream/t/data/whitelists/paypal.com [deleted file]
upstream/t/data/whitelists/register.com_password [deleted file]
upstream/t/data/whitelists/ryanairmail.com [deleted file]
upstream/t/data/whitelists/sf.net [deleted file]
upstream/t/data/whitelists/winxpnews.com [deleted file]
upstream/t/data/whitelists/yahoo-inc.com [deleted file]
upstream/t/date.t
upstream/t/db_awl_path.t
upstream/t/db_awl_path_welcome_block.t [new file with mode: 0755]
upstream/t/db_awl_perms.t
upstream/t/db_awl_perms_welcome_block.t [new file with mode: 0755]
upstream/t/db_based_welcomelist.t [new file with mode: 0755]
upstream/t/db_based_welcomelist_ips.t [new file with mode: 0755]
upstream/t/db_based_whitelist.t
upstream/t/db_based_whitelist_ips.t
upstream/t/dcc.t
upstream/t/debug.t
upstream/t/decodeshorturl.t [new file with mode: 0755]
upstream/t/desc_wrap.t
upstream/t/dkim.t
upstream/t/dmarc.t [new file with mode: 0755]
upstream/t/dnsbl.t
upstream/t/dnsbl_sc_meta.t
upstream/t/dnsbl_subtests.t
upstream/t/duplicates.t [deleted file]
upstream/t/enable_compat.t [new file with mode: 0755]
upstream/t/extracttext.t [new file with mode: 0755]
upstream/t/freemail.t
upstream/t/freemail_welcome_block.t [new file with mode: 0755]
upstream/t/fromnamespoof.t [new file with mode: 0755]
upstream/t/get_all_headers.t
upstream/t/get_headers.t
upstream/t/gtube.t
upstream/t/hashbl.t [new file with mode: 0755]
upstream/t/hashcash.t [deleted file]
upstream/t/header.t [new file with mode: 0755]
upstream/t/header_utf8.t [new file with mode: 0755]
upstream/t/html_colors.t
upstream/t/html_obfu.t
upstream/t/html_utf8.t
upstream/t/idn_dots.t
upstream/t/if_can.t [changed mode: 0644->0755]
upstream/t/if_else.t [new file with mode: 0755]
upstream/t/ifversion.t
upstream/t/ip_addrs.t
upstream/t/lang_lint.t
upstream/t/lang_pl_tests.t
upstream/t/line_endings.t
upstream/t/lint_nocreate_prefs.t
upstream/t/local_tests_only.t [new file with mode: 0755]
upstream/t/memory_cycles.t
upstream/t/metadata.t
upstream/t/mimeheader.t
upstream/t/mimeparse.t
upstream/t/missing_hb_separator.t
upstream/t/mkrules.t [new file with mode: 0755]
upstream/t/mkrules_else.t [new file with mode: 0755]
upstream/t/nonspam.t
upstream/t/olevbmacro.t
upstream/t/originating_ip_hdr.t [changed mode: 0644->0755]
upstream/t/pdfinfo.t [new file with mode: 0755]
upstream/t/perlcritic.pl [new file with mode: 0755]
upstream/t/perlcritic.t [new file with mode: 0644]
upstream/t/phishing.t [changed mode: 0644->0755]
upstream/t/plugin.t
upstream/t/plugin_file.t
upstream/t/plugin_priorities.t
upstream/t/podchecker.t [new file with mode: 0755]
upstream/t/prefs_include.t
upstream/t/priorities.t [changed mode: 0644->0755]
upstream/t/priorities_welcome_block.t [new file with mode: 0755]
upstream/t/pyzor.t [new file with mode: 0755]
upstream/t/razor2.t
upstream/t/rcvd_parser.t
upstream/t/re_base_extraction.t
upstream/t/recips.t
upstream/t/recreate.t
upstream/t/recursion.t
upstream/t/regexp_named_capture.t [new file with mode: 0755]
upstream/t/regexp_valid.t
upstream/t/relative_scores.t
upstream/t/relaycountry.t [new file with mode: 0755]
upstream/t/relaycountry_fast.t [deleted file]
upstream/t/relaycountry_geoip.t [deleted file]
upstream/t/relaycountry_geoip2.t [deleted file]
upstream/t/report_safe.t
upstream/t/reportheader.t
upstream/t/reportheader_8bit.t
upstream/t/reuse.t [changed mode: 0644->0755]
upstream/t/root_spamd.t [changed mode: 0644->0755]
upstream/t/root_spamd_tell.t [changed mode: 0644->0755]
upstream/t/root_spamd_tell_paranoid.t [changed mode: 0644->0755]
upstream/t/root_spamd_tell_x.t [changed mode: 0644->0755]
upstream/t/root_spamd_tell_x_paranoid.t [changed mode: 0644->0755]
upstream/t/root_spamd_u.t
upstream/t/root_spamd_u_dcc.t
upstream/t/root_spamd_virtual.t
upstream/t/root_spamd_x.t [changed mode: 0644->0755]
upstream/t/root_spamd_x_paranoid.t [changed mode: 0644->0755]
upstream/t/root_spamd_x_u.t
upstream/t/rule_multiple.t
upstream/t/rule_names.t
upstream/t/rule_types.t
upstream/t/sa_awl.t
upstream/t/sa_awl_welcome_block.t [new file with mode: 0755]
upstream/t/sa_check_spamd.t
upstream/t/sa_compile.t [changed mode: 0644->0755]
upstream/t/sha1.t
upstream/t/shortcircuit.t [changed mode: 0644->0755]
upstream/t/shortcircuit_before_dns.t [new file with mode: 0755]
upstream/t/spam.t
upstream/t/spamc.t
upstream/t/spamc_H.t [new file with mode: 0755]
upstream/t/spamc_bug6176.t
upstream/t/spamc_cf.t
upstream/t/spamc_headers.t
upstream/t/spamc_l.t
upstream/t/spamc_optC.t
upstream/t/spamc_optL.t
upstream/t/spamc_x_E_R.t
upstream/t/spamc_x_e.t
upstream/t/spamc_y.t
upstream/t/spamc_z.t [changed mode: 0644->0755]
upstream/t/spamd.t
upstream/t/spamd_allow_user_rules.t
upstream/t/spamd_client.t
upstream/t/spamd_hup.t
upstream/t/spamd_kill_restart.t
upstream/t/spamd_kill_restart_rr.t
upstream/t/spamd_ldap.t
upstream/t/spamd_maxchildren.t
upstream/t/spamd_maxsize.t
upstream/t/spamd_parallel.t
upstream/t/spamd_plugin.t
upstream/t/spamd_port.t
upstream/t/spamd_prefork_stress.t
upstream/t/spamd_prefork_stress_2.t
upstream/t/spamd_prefork_stress_3.t
upstream/t/spamd_prefork_stress_4.t
upstream/t/spamd_protocol_10.t
upstream/t/spamd_report.t
upstream/t/spamd_report_ifspam.t
upstream/t/spamd_sql_prefs.t
upstream/t/spamd_ssl.t
upstream/t/spamd_ssl_accept_fail.t
upstream/t/spamd_ssl_z.t [new file with mode: 0755]
upstream/t/spamd_stop.t
upstream/t/spamd_symbols.t
upstream/t/spamd_syslog.t
upstream/t/spamd_unix.t
upstream/t/spamd_unix_and_tcp.t
upstream/t/spamd_user_rules_leak.t
upstream/t/spamd_utf8.t
upstream/t/spamd_welcomelist_leak.t [new file with mode: 0755]
upstream/t/spamd_whitelist_leak.t
upstream/t/spf.t
upstream/t/spf_welcome_block.t [new file with mode: 0755]
upstream/t/sql_based_welcomelist.t [new file with mode: 0755]
upstream/t/sql_based_whitelist.t
upstream/t/stop_always_matching_regexps.t [changed mode: 0644->0755]
upstream/t/strip2.t
upstream/t/strip_no_subject.t
upstream/t/stripmarkup.t
upstream/t/tainted_msg.t
upstream/t/testrules.yml [new file with mode: 0644]
upstream/t/text_bad_ctype.t
upstream/t/timeout.t
upstream/t/trust_path.t
upstream/t/uri.t
upstream/t/uri_html.t
upstream/t/uri_list.t [changed mode: 0644->0755]
upstream/t/uri_saferedirect.t
upstream/t/uri_text.t
upstream/t/uribl.t
upstream/t/uribl_all_types.t [changed mode: 0644->0755]
upstream/t/uribl_domains_only.t [changed mode: 0644->0755]
upstream/t/uribl_ips_only.t [changed mode: 0644->0755]
upstream/t/urilocalbl.t [new file with mode: 0755]
upstream/t/urilocalbl_geoip.t [deleted file]
upstream/t/utf8.t
upstream/t/util_wrap.t
upstream/t/welcomelist_addrs.t [new file with mode: 0755]
upstream/t/welcomelist_from.t [new file with mode: 0755]
upstream/t/welcomelist_subject.t [new file with mode: 0755]
upstream/t/welcomelist_to.t [new file with mode: 0755]
upstream/t/whitelist_addrs.t
upstream/t/whitelist_from.t
upstream/t/whitelist_subject.t
upstream/t/whitelist_to.t
upstream/t/wlbl_uri.t [new file with mode: 0755]
upstream/t/zz_cleanup.t [deleted file]

index 7c41d29a1142c13ce3da9386dcfe841de5f14df6..80e8fbd35e2570657d00dc7c4a71bcf679a7a114 100644 (file)
@@ -1,4 +1,4 @@
-Copyright (C) 2021 The Apache Software Foundation
+Copyright (C) 2022 The Apache Software Foundation
 
 Project Management Committee (PMC):
 
index a0c994023998397c174ed9a55c6a205c02c3b9c5..fe1cb60ce6dc3f604df4f9d0a6dcc3a668a1056d 100644 (file)
 ------------------------------------------------------------------------
-r1888513 | hege | 2021-04-08 10:29:27 +0000 (Thu, 08 Apr 2021) | 2 lines
+r1905917 | sidney | 2022-12-11 19:21:41 +0000 (Sun, 11 Dec 2022) | 1 line
  
- Bug 7892 - T_KAM_HTML_FONT_INVALID false positive for "<color> 
-!important"
+ Bug 7826 - Fix a couple missed blacklist to blocklist edits in comments
+------------------------------------------------------------------------
+r1905889 | gbechis | 2022-12-09 15:59:31 +0000 (Fri, 09 Dec 2022) | 2 
+lines
+ unbreak when Test::Pod is not installed
  
 ------------------------------------------------------------------------
-r1888498 | sidney | 2021-04-08 05:13:34 +0000 (Thu, 08 Apr 2021) | 1 line
+r1905879 | gbechis | 2022-12-09 09:09:55 +0000 (Fri, 09 Dec 2022) | 3 
+lines
+ remove "nice" tflags where not needed
+ bz #8085
  
- Update PMC list
 ------------------------------------------------------------------------
-r1888492 | sidney | 2021-04-08 04:01:39 +0000 (Thu, 08 Apr 2021) | 1 line
+r1905870 | sidney | 2022-12-08 21:42:40 +0000 (Thu, 08 Dec 2022) | 1 line
  
- preparing to release 3.4.6-rc1
+ Bug 8088 - Add -T and remove use strict and use warnings - copied 
+initial file from wrong template
 ------------------------------------------------------------------------
-r1888435 | hege | 2021-04-06 13:08:42 +0000 (Tue, 06 Apr 2021) | 2 lines
+r1905869 | sidney | 2022-12-08 21:31:13 +0000 (Thu, 08 Dec 2022) | 1 line
  
- Revert Revision 1878575,1878574,1878572 (Bugs 7822, 7822), remove any 
-traces of undocumented check_cleanup from 3.4, metas do not work 
-correctly with it. URIDNSBL/HashBL revert to logging only first hit, any 
-improvements will be in 4.0 only.
+ Bug 8088 - Fix typo in POD documentation and add podchecker.t to 
+regression tests
+------------------------------------------------------------------------
+r1905867 | sidney | 2022-12-08 18:53:56 +0000 (Thu, 08 Dec 2022) | 1 line
  
+ Bug 8087 - Fix bug that showed up in DMARC with some subdomains
 ------------------------------------------------------------------------
-r1888433 | hege | 2021-04-06 13:06:49 +0000 (Tue, 06 Apr 2021) | 2 lines
+r1905853 | hege | 2022-12-08 10:24:26 +0000 (Thu, 08 Dec 2022) | 2 lines
  
- Bug 7897 - add test case for meta net-rules
+ Bug 8016 - Remove uridnsbl_skip_domain(s)
  
 ------------------------------------------------------------------------
-r1888042 | sidney | 2021-03-24 23:49:14 +0000 (Wed, 24 Mar 2021) | 1 line
+r1905834 | sidney | 2022-12-07 09:07:23 +0000 (Wed, 07 Dec 2022) | 1 line
  
- post-release of 3.4.5 ready new development version 3.4.6 in case there 
-is any more development required in this branch
+ remove caution against using versions of gpg that are now 15-20 years 
+past their stated end of life date
 ------------------------------------------------------------------------
-r1888010 | sidney | 2021-03-24 14:27:23 +0000 (Wed, 24 Mar 2021) | 1 line
+r1905829 | sidney | 2022-12-07 07:00:49 +0000 (Wed, 07 Dec 2022) | 1 line
  
- update 3.4.5 announcement file with actual checksums
+ Bug 8086 - remove obsolete options and commands from build script. Also 
+some old comments
 ------------------------------------------------------------------------
-r1887843 | sidney | 2021-03-20 09:14:03 +0000 (Sat, 20 Mar 2021) | 1 line
+r1905828 | sidney | 2022-12-07 06:59:04 +0000 (Wed, 07 Dec 2022) | 1 line
  
- preparing to release 3.4.5 (supersedes r1887620)
+ remove some garbage characters in the announcement file
 ------------------------------------------------------------------------
-r1887653 | sidney | 2021-03-14 21:37:10 +0000 (Sun, 14 Mar 2021) | 1 line
+r1905823 | sidney | 2022-12-07 03:03:09 +0000 (Wed, 07 Dec 2022) | 1 line
  
- preparing to release 3.4.5-rc2
+ 4.0.0-rc4 RELEASED
 ------------------------------------------------------------------------
-r1887620 | sidney | 2021-03-14 08:16:27 +0000 (Sun, 14 Mar 2021) | 1 line
+r1905819 | sidney | 2022-12-06 22:44:26 +0000 (Tue, 06 Dec 2022) | 1 line
  
- preparing to release 3.4.5
+ preparing to release 4.0.0-rc4
 ------------------------------------------------------------------------
-r1887306 | hege | 2021-03-07 21:56:45 +0000 (Sun, 07 Mar 2021) | 2 lines
+r1905818 | sidney | 2022-12-06 21:53:22 +0000 (Tue, 06 Dec 2022) | 1 line
  
- Fix previous commit, need to allow multiple AskDNS hits
+ Bug 8056 - fix yet another typo in documentation line in earlier commit
+------------------------------------------------------------------------
+r1905817 | sidney | 2022-12-06 21:49:03 +0000 (Tue, 06 Dec 2022) | 1 line
  
+ Bug 8056 - fix typos in documentation lines in previous commit
 ------------------------------------------------------------------------
-r1887305 | hege | 2021-03-07 21:51:31 +0000 (Sun, 07 Mar 2021) | 2 lines
+r1905814 | sidney | 2022-12-06 20:56:14 +0000 (Tue, 06 Dec 2022) | 1 line
  
- AskDNS cleanups and fixes for Bug 7777 & Bug 7875 (Multiple DNS 
-responses)
+ Minor edit to release build instructions documentation
+------------------------------------------------------------------------
+r1905811 | sidney | 2022-12-06 20:19:59 +0000 (Tue, 06 Dec 2022) | 1 line
  
+ Bug 8056 - Add .gitattributes to MANIFEST.SKIP
 ------------------------------------------------------------------------
-r1886188 | gbechis | 2021-02-04 08:02:07 +0000 (Thu, 04 Feb 2021) | 3 
-lines
+r1905809 | sidney | 2022-12-06 19:48:27 +0000 (Tue, 06 Dec 2022) | 1 line
  
- do not consider oleobject1.bin files as bad,
- they could also be images
+ Bug 8056 - add .gitattributes required for Windows test Github Actions 
+to work
+------------------------------------------------------------------------
+r1905808 | sidney | 2022-12-06 19:35:16 +0000 (Tue, 06 Dec 2022) | 1 line
  
+ Bug 8045 - Add warning to tests about directory permissions now required 
+for tests to pass
 ------------------------------------------------------------------------
-r1885637 | kmcgrail | 2021-01-18 05:37:09 +0000 (Mon, 18 Jan 2021) | 1 
-line
+r1905790 | sidney | 2022-12-06 11:05:53 +0000 (Tue, 06 Dec 2022) | 1 line
  
- preparing to release 3.4.5-rc1
+ Bug 8056 - commit Github Actions that can be used in a fork of our 
+Github mirror to run regression tests on Github action runners
 ------------------------------------------------------------------------
-r1885636 | kmcgrail | 2021-01-18 05:36:12 +0000 (Mon, 18 Jan 2021) | 1 
-line
+r1905787 | sidney | 2022-12-06 08:31:13 +0000 (Tue, 06 Dec 2022) | 1 line
  
- adding spam that hits razor for testing
+ Bug 8084 - exclude new perlcritic policy for bareword dir handles
 ------------------------------------------------------------------------
-r1885345 | kb | 2021-01-11 02:51:19 +0000 (Mon, 11 Jan 2021) | 1 line
+r1905783 | sidney | 2022-12-06 01:07:53 +0000 (Tue, 06 Dec 2022) | 1 line
  
- BodyEval: plaintext_body_sig_ratio eval rules, bug 7879
+ Bug 8083 - exclude Bangs::ProhibitDebuggingModules from perlcritic tests
 ------------------------------------------------------------------------
-r1885234 | gbechis | 2021-01-07 07:47:53 +0000 (Thu, 07 Jan 2021) | 2 
-lines
+r1905778 | hege | 2022-12-05 19:15:26 +0000 (Mon, 05 Dec 2022) | 2 lines
  
- pod fixes
+ Bug 8078 - Shortcircuiting does not work as expected
  
 ------------------------------------------------------------------------
-r1885233 | gbechis | 2021-01-07 07:31:13 +0000 (Thu, 07 Jan 2021) | 2 
+r1905769 | gbechis | 2022-12-05 15:47:59 +0000 (Mon, 05 Dec 2022) | 2 
 lines
  
- clarify man page
-------------------------------------------------------------------------
-r1885214 | kb | 2021-01-06 21:08:43 +0000 (Wed, 06 Jan 2021) | 1 line
+ "num" hashbl config regression tests
  
- plaintext_body_sig_ratio: eval() rules for the (first text/plain MIME 
-part's) body and signature lengths and ratio
 ------------------------------------------------------------------------
-r1884879 | gbechis | 2020-12-28 15:00:10 +0000 (Mon, 28 Dec 2020) | 2 
+r1905766 | gbechis | 2022-12-05 15:23:04 +0000 (Mon, 05 Dec 2022) | 2 
 lines
  
- update [meta]cpan url
+ add a "num" option to check_hashbl_bodyre that removes the chars from 
+the match that are not numbers
  
 ------------------------------------------------------------------------
-r1884876 | gbechis | 2020-12-28 14:38:35 +0000 (Mon, 28 Dec 2020) | 2 
-lines
+r1905737 | hege | 2022-12-04 14:04:52 +0000 (Sun, 04 Dec 2022) | 2 lines
  
- Mention some changes in 3.4.5
+ Fix EMPTY_MESSAGE description for nosubject
  
 ------------------------------------------------------------------------
-r1884872 | kmcgrail | 2020-12-28 13:56:49 +0000 (Mon, 28 Dec 2020) | 1 
-line
+r1905736 | hege | 2022-12-04 13:59:57 +0000 (Sun, 04 Dec 2022) | 2 lines
  
- More MANIFEST cleanup
-------------------------------------------------------------------------
-r1884871 | kmcgrail | 2020-12-28 13:51:17 +0000 (Mon, 28 Dec 2020) | 1 
-line
+ Use nosubject for __NONEMPTY_BODY
  
- MANIFEST clean-up
 ------------------------------------------------------------------------
-r1884870 | kmcgrail | 2020-12-28 13:48:59 +0000 (Mon, 28 Dec 2020) | 1 
-line
+r1905735 | hege | 2022-12-04 13:59:35 +0000 (Sun, 04 Dec 2022) | 2 lines
+ Clear out old tests
  
- Fixing Copyright on CREDITS file
 ------------------------------------------------------------------------
-r1883660 | gbechis | 2020-11-20 07:33:00 +0000 (Fri, 20 Nov 2020) | 2 
-lines
+r1905734 | hege | 2022-12-04 13:47:03 +0000 (Sun, 04 Dec 2022) | 2 lines
  
- fix GeoIP open_type call, bz #7871
+ Bug 8078 - Shortcircuiting does not work as expected
  
 ------------------------------------------------------------------------
-r1883643 | gbechis | 2020-11-19 15:37:20 +0000 (Thu, 19 Nov 2020) | 2 
+r1905576 | gbechis | 2022-11-28 16:13:19 +0000 (Mon, 28 Nov 2022) | 3 
 lines
  
- typo
+ include the appropriate headers to avoid spurious test failures
+ when "-Wimplicit-function-declaration" compiler option is used
  
 ------------------------------------------------------------------------
-r1883642 | gbechis | 2020-11-19 15:35:33 +0000 (Thu, 19 Nov 2020) | 2 
+r1905566 | gbechis | 2022-11-28 08:34:04 +0000 (Mon, 28 Nov 2022) | 2 
 lines
  
- specify in debug message that not all rule types are compatible
+ update url shortener as well, spotted by hege@ thanks
  
 ------------------------------------------------------------------------
-r1883069 | gbechis | 2020-11-02 18:14:47 +0000 (Mon, 02 Nov 2020) | 3 
+r1905564 | gbechis | 2022-11-28 07:59:10 +0000 (Mon, 28 Nov 2022) | 2 
 lines
  
- backport TextCat improvements from trunk
- fix bz #7866
+ add s.free.fr shortener
  
 ------------------------------------------------------------------------
-r1882297 | gbechis | 2020-10-07 08:28:05 +0000 (Wed, 07 Oct 2020) | 2 
+r1905524 | gbechis | 2022-11-25 10:04:38 +0000 (Fri, 25 Nov 2022) | 2 
 lines
  
- Missing files from previous commit, bz #7860
+ catch more samples
  
 ------------------------------------------------------------------------
-r1882269 | gbechis | 2020-10-06 10:20:40 +0000 (Tue, 06 Oct 2020) | 4 
+r1905523 | gbechis | 2022-11-25 09:07:36 +0000 (Fri, 25 Nov 2022) | 2 
 lines
  
- Make it possible to run the Spamassassin test suite against the installed
- SpamAssassin files (rather than those in the source directory)
- bz #7860
+ catch another uri
  
 ------------------------------------------------------------------------
-r1881912 | jhardin | 2020-09-21 18:43:37 +0000 (Mon, 21 Sep 2020) | 1 line
+r1905518 | sidney | 2022-11-25 04:03:09 +0000 (Fri, 25 Nov 2022) | 1 line
  
- Bug 7857: merge Revision 1881911 from trunk
+ test from .fun and .site TLDs
 ------------------------------------------------------------------------
-r1881784 | gbechis | 2020-09-17 07:17:40 +0000 (Thu, 17 Sep 2020) | 2 
+r1905425 | gbechis | 2022-11-21 09:06:27 +0000 (Mon, 21 Nov 2022) | 2 
 lines
  
- exit if reallyallowplugin option is not specified
-------------------------------------------------------------------------
-r1881066 | billcole | 2020-08-21 19:29:57 +0000 (Fri, 21 Aug 2020) | 1 
-line
+ catch more uris
  
- Understand deprecated charset=ascii correctly. BZ#7851
 ------------------------------------------------------------------------
-r1880999 | billcole | 2020-08-19 17:31:48 +0000 (Wed, 19 Aug 2020) | 1 
-line
+r1905377 | gbechis | 2022-11-18 11:40:26 +0000 (Fri, 18 Nov 2022) | 2 
+lines
  
- Fix duplicated-word typos in documentation BZ#7850
-------------------------------------------------------------------------
-r1880998 | billcole | 2020-08-19 17:26:15 +0000 (Wed, 19 Aug 2020) | 1 
-line
+ match more uris
  
- Add man page generation for sa-check_spamd BZ#7849
 ------------------------------------------------------------------------
-r1879979 | hege | 2020-07-17 05:03:52 +0000 (Fri, 17 Jul 2020) | 2 lines
+r1905376 | gbechis | 2022-11-18 10:59:09 +0000 (Fri, 18 Nov 2022) | 2 
+lines
  
- Bug 7810 - gmail has an extra dot in rDNS
+ fix regexp
  
 ------------------------------------------------------------------------
-r1879806 | hege | 2020-07-12 10:18:32 +0000 (Sun, 12 Jul 2020) | 2 lines
+r1905227 | billcole | 2022-11-10 22:04:16 +0000 (Thu, 10 Nov 2022) | 3 
+lines
  
- Bug 7817 - Pyzor.pm - Show traceback in log
+ Forcing publish of 2 extremely safe rules to see if that works to get 
+around promotion blockage
  
-------------------------------------------------------------------------
-r1879731 | kmcgrail | 2020-07-10 04:18:33 +0000 (Fri, 10 Jul 2020) | 1 
-line
  
- Fixing powered by Apache SpamAssassin logo to the correct version 2.0
 ------------------------------------------------------------------------
-r1879727 | billcole | 2020-07-09 21:59:24 +0000 (Thu, 09 Jul 2020) | 1 
-line
+r1905214 | hege | 2022-11-10 07:28:04 +0000 (Thu, 10 Nov 2022) | 2 lines
  
- Don't assume versions are simple numbers
-------------------------------------------------------------------------
-r1879726 | billcole | 2020-07-09 20:42:56 +0000 (Thu, 09 Jul 2020) | 1 
-line
+ Bug 7735 - Meta rules need to handle missing/unrun dependencies
  
- Don't treat versions like simple numbers
 ------------------------------------------------------------------------
-r1879700 | hege | 2020-07-09 10:47:48 +0000 (Thu, 09 Jul 2020) | 2 lines
+r1905160 | hege | 2022-11-08 16:08:50 +0000 (Tue, 08 Nov 2022) | 2 lines
  
- Backport EnvelopeFrom fixes from trunk (Revision 1844628,1864383) (Bug 
-7834)
+ Bug 7735 - Meta rules need to handle missing/unrun dependencies
  
 ------------------------------------------------------------------------
-r1879123 | billcole | 2020-06-23 18:20:55 +0000 (Tue, 23 Jun 2020) | 4 
-lines
- Fix Bug #7830: non-numeric version comparison. 
+r1904981 | hege | 2022-11-01 20:11:58 +0000 (Tue, 01 Nov 2022) | 3 lines
  
+ Bug 7735 - Meta rules need to handle missing/unrun dependencies
+ - revert to old logic
  
 ------------------------------------------------------------------------
-r1879052 | kmcgrail | 2020-06-21 02:48:32 +0000 (Sun, 21 Jun 2020) | 1 
-line
+r1904928 | hege | 2022-10-30 08:07:30 +0000 (Sun, 30 Oct 2022) | 2 lines
+ Remove accidentally committed unneeded debug line
  
- preparing to release 3.4.5-pre1
 ------------------------------------------------------------------------
-r1878990 | hege | 2020-06-19 14:11:26 +0000 (Fri, 19 Jun 2020) | 2 lines
+r1904893 | hege | 2022-10-28 05:35:52 +0000 (Fri, 28 Oct 2022) | 2 lines
  
- Bug 7828 - uri_detail lacks support for key type "host"
+ Bug 8070 - PDFInfo pdf_is_empty_body() destroys body array
  
 ------------------------------------------------------------------------
-r1878575 | hege | 2020-06-08 05:18:37 +0000 (Mon, 08 Jun 2020) | 2 lines
+r1904865 | hege | 2022-10-27 06:46:28 +0000 (Thu, 27 Oct 2022) | 2 lines
  
- Log all URIBL hit domains in report
+ Adjust priority of GMD_PDF_EMPTY_BODY to work around Bug 8070
  
 ------------------------------------------------------------------------
-r1878574 | hege | 2020-06-08 04:44:27 +0000 (Mon, 08 Jun 2020) | 2 lines
+r1904837 | sidney | 2022-10-26 03:35:50 +0000 (Wed, 26 Oct 2022) | 1 line
  
- Bug 7822: HashBL not examining all addresses in a message
+ bug 8069 - Use shortened links that are under our control so test 
+remains stable
+------------------------------------------------------------------------
+r1904818 | sidney | 2022-10-24 22:20:56 +0000 (Mon, 24 Oct 2022) | 1 line
  
+ bug 8068 - Add a delay in the test to allow for some slow test systems
 ------------------------------------------------------------------------
-r1878572 | hege | 2020-06-08 04:18:44 +0000 (Mon, 08 Jun 2020) | 2 lines
+r1904811 | hege | 2022-10-24 14:03:19 +0000 (Mon, 24 Oct 2022) | 2 lines
  
- Backport check_cleanup callback from trunk for internal use, not 
-documenting since it will only be in 3.4.5
+ Bug 8062 - no URL makes uridnsbl rules "unrun"
  
 ------------------------------------------------------------------------
-r1878568 | hege | 2020-06-07 16:34:50 +0000 (Sun, 07 Jun 2020) | 2 lines
+r1904798 | hege | 2022-10-24 08:10:13 +0000 (Mon, 24 Oct 2022) | 2 lines
  
- Clarify some HashBL docs
+ Some extra test code slipped through in last commit, revert
  
 ------------------------------------------------------------------------
-r1878559 | hege | 2020-06-07 10:41:22 +0000 (Sun, 07 Jun 2020) | 2 lines
+r1904797 | hege | 2022-10-24 07:59:32 +0000 (Mon, 24 Oct 2022) | 2 lines
  
- Bug 7822: HashBL not examining all addresses in a message
+ Bug 8061 - Fix meta handling for $suppl_attrib->{rule_hits}
  
 ------------------------------------------------------------------------
-r1877459 | gbechis | 2020-05-06 22:36:46 +0000 (Wed, 06 May 2020) | 2 
-lines
- always pass the rulename to bgsend_and_start_lookup
+r1904779 | sidney | 2022-10-22 12:46:14 +0000 (Sat, 22 Oct 2022) | 1 line
  
+ Bug 8066 - remove unnecessary svn:eol-style property from some files in 
+svn repository
 ------------------------------------------------------------------------
-r1877139 | gbechis | 2020-04-28 19:21:04 +0000 (Tue, 28 Apr 2020) | 3 
-lines
+r1904751 | sidney | 2022-10-20 23:11:06 +0000 (Thu, 20 Oct 2022) | 1 line
  
- fix warnings that happens when From: is not a proper email address
- bz 7811
+ Bug 8067 - Work around error thrown by Cwd::realpath on older Windows 
+perl when path does not exist
+------------------------------------------------------------------------
+r1904709 | billcole | 2022-10-20 01:27:03 +0000 (Thu, 20 Oct 2022) | 1 
+line
  
+ Yet another hashbust pattern
 ------------------------------------------------------------------------
-r1877124 | gbechis | 2020-04-28 09:50:37 +0000 (Tue, 28 Apr 2020) | 3 
+r1904696 | gbechis | 2022-10-19 14:02:15 +0000 (Wed, 19 Oct 2022) | 2 
 lines
  
- fix txrep tags, "_" is not an allowed char in tag names
- fixes bz 7749
+ add a Drupal uri check
  
 ------------------------------------------------------------------------
-r1876821 | hege | 2020-04-22 10:00:36 +0000 (Wed, 22 Apr 2020) | 2 lines
+r1904678 | hege | 2022-10-18 09:29:30 +0000 (Tue, 18 Oct 2022) | 2 lines
  
- Allow undefined suppl_attrib just in case
+ Update tlds
  
 ------------------------------------------------------------------------
-r1876795 | hege | 2020-04-21 12:28:07 +0000 (Tue, 21 Apr 2020) | 2 lines
+r1904676 | hege | 2022-10-18 08:24:19 +0000 (Tue, 18 Oct 2022) | 2 lines
  
- Add some suppl_attrib debugging
+ Bug 8063 - uri not detected if two text/html parts exist
  
 ------------------------------------------------------------------------
-r1876780 | gbechis | 2020-04-21 09:20:23 +0000 (Tue, 21 Apr 2020) | 2 
-lines
+r1904671 | sidney | 2022-10-18 06:05:42 +0000 (Tue, 18 Oct 2022) | 1 line
  
- silence a possible warning
+ Bug 8066 - remove unnecessary svn:eol-style property from some files in 
+svn repository
+------------------------------------------------------------------------
+r1904667 | billcole | 2022-10-17 21:41:36 +0000 (Mon, 17 Oct 2022) | 1 
+line
  
+ adjusting test rules
 ------------------------------------------------------------------------
-r1876711 | hege | 2020-04-19 06:25:48 +0000 (Sun, 19 Apr 2020) | 2 lines
+r1904598 | hege | 2022-10-15 12:19:43 +0000 (Sat, 15 Oct 2022) | 3 lines
  
- Mention Bug 7803
+ Add missing debug logging for all rules in blocked DNS query, previously 
+only first rule was logged.
+ (consider this safe and trivial enough to commit without voting)
  
 ------------------------------------------------------------------------
-r1876710 | hege | 2020-04-19 06:18:25 +0000 (Sun, 19 Apr 2020) | 2 lines
+r1904597 | hege | 2022-10-15 12:06:15 +0000 (Sat, 15 Oct 2022) | 2 lines
  
Bug 7809 - unwhitelist broken
Fix meta documentation
  
 ------------------------------------------------------------------------
-r1876561 | hege | 2020-04-15 15:03:58 +0000 (Wed, 15 Apr 2020) | 2 lines
+r1904529 | hege | 2022-10-11 17:40:55 +0000 (Tue, 11 Oct 2022) | 2 lines
  
DNSEval cleanups, validate hostnames
Bug 8060 - Fix meta handling for metas without dependencies
  
 ------------------------------------------------------------------------
-r1876556 | hege | 2020-04-15 13:59:34 +0000 (Wed, 15 Apr 2020) | 2 lines
+r1904528 | hege | 2022-10-11 17:39:03 +0000 (Tue, 11 Oct 2022) | 2 lines
  
- Bug 7808 - Fix check_rbl_headers with multiple same headers
+ Bug 8059 - Fix meta handling for URIDNSBL NS/A lookups
  
 ------------------------------------------------------------------------
-r1876381 | hege | 2020-04-10 20:38:45 +0000 (Fri, 10 Apr 2020) | 2 lines
+r1904484 | hege | 2022-10-10 09:29:48 +0000 (Mon, 10 Oct 2022) | 2 lines
  
- Fix header rule parsing
+ Bug 8058 - DMARC makes DNS queries with local_tests_only
  
 ------------------------------------------------------------------------
-r1876367 | hege | 2020-04-10 14:49:20 +0000 (Fri, 10 Apr 2020) | 2 lines
+r1904481 | gbechis | 2022-10-10 06:43:47 +0000 (Mon, 10 Oct 2022) | 3 
+lines
  
- Bug 7750 - _DKIMSELECTOR_ template tag is not substituted, when mail is 
-not DKIM signed
+ check for Office 2003 markers only when needed
+ bz #8055
  
 ------------------------------------------------------------------------
-r1876350 | hege | 2020-04-10 08:22:55 +0000 (Fri, 10 Apr 2020) | 2 lines
+r1904466 | sidney | 2022-10-09 03:10:56 +0000 (Sun, 09 Oct 2022) | 1 line
  
- Bug 7790 - Allow = character in pyzor_options
+ bug 8027 - skip extracttext tests if executable found in path with space 
+to avoid test failure
+------------------------------------------------------------------------
+r1904448 | billcole | 2022-10-08 03:27:30 +0000 (Sat, 08 Oct 2022) | 1 
+line
  
+ de-testing some rules
 ------------------------------------------------------------------------
-r1876348 | hege | 2020-04-10 07:51:51 +0000 (Fri, 10 Apr 2020) | 2 lines
+r1904368 | gbechis | 2022-10-03 06:30:32 +0000 (Mon, 03 Oct 2022) | 2 
+lines
  
- Bug 7803 - SQL schema of userpref table, value too short
+ add snip.ly
  
 ------------------------------------------------------------------------
-r1876347 | hege | 2020-04-10 07:47:37 +0000 (Fri, 10 Apr 2020) | 2 lines
- Bug 7807 - t/spamd_ssl.t fails due to small key size
+r1904337 | billcole | 2022-09-29 18:25:59 +0000 (Thu, 29 Sep 2022) | 1 
+line
  
+ Intuit reported as spamming on Users ML
 ------------------------------------------------------------------------
-r1876346 | hege | 2020-04-10 07:44:37 +0000 (Fri, 10 Apr 2020) | 2 lines
+r1904315 | billcole | 2022-09-28 02:49:06 +0000 (Wed, 28 Sep 2022) | 1 
+line
  
- Bug 7763 - ssl tests must be run as root
+ wrap  mimeheader rules in ifplugin.
+------------------------------------------------------------------------
+r1904311 | sidney | 2022-09-27 22:39:36 +0000 (Tue, 27 Sep 2022) | 1 line
  
+ Bug 8054 - Fix tests detection of existence of ipv4 and ipv6 local ip 
+addresses
 ------------------------------------------------------------------------
-r1876320 | hege | 2020-04-09 12:40:52 +0000 (Thu, 09 Apr 2020) | 2 lines
+r1904286 | billcole | 2022-09-26 19:35:06 +0000 (Mon, 26 Sep 2022) | 1 
+line
  
- Bug 7806 - Tainting through concatenation with $^X does not taint
+ It only looks like a boundary string.
+------------------------------------------------------------------------
+r1904253 | billcole | 2022-09-25 19:49:34 +0000 (Sun, 25 Sep 2022) | 1 
+line
  
+ Various new test rules: hashbust texts and MIME bogosity
 ------------------------------------------------------------------------
-r1876218 | gbechis | 2020-04-07 08:20:15 +0000 (Tue, 07 Apr 2020) | 2 
-lines
+r1904221 | sidney | 2022-09-22 22:36:12 +0000 (Thu, 22 Sep 2022) | 1 line
  
- match few more received lines
+ 4.0.0-rc3 RELEASED
+------------------------------------------------------------------------
+r1904209 | sidney | 2022-09-22 12:52:26 +0000 (Thu, 22 Sep 2022) | 1 line
  
+ preparing to release 4.0.0-rc3
 ------------------------------------------------------------------------
-r1875134 | gbechis | 2020-03-12 18:32:40 +0000 (Thu, 12 Mar 2020) | 5 
-lines
+r1904206 | sidney | 2022-09-22 09:53:19 +0000 (Thu, 22 Sep 2022) | 1 line
  
- sync OLEVBMacro plugin with trunk
- - check for undef before reading mime part
- - add a new rule to check if on the doc file there is an url that 
- triggers a download to an external malicious file
+ Bug 8050 - Fix global_state_dir on Windows
+------------------------------------------------------------------------
+r1904201 | sidney | 2022-09-21 20:38:43 +0000 (Wed, 21 Sep 2022) | 1 line
  
+ Bug 8043 - Don't try and fail to setgid to drop privs when spamd started 
+with a supplemental group without privs
 ------------------------------------------------------------------------
-r1874343 | gbechis | 2020-02-21 23:04:46 +0000 (Fri, 21 Feb 2020) | 4 
-lines
+r1904166 | billcole | 2022-09-20 12:55:16 +0000 (Tue, 20 Sep 2022) | 1 
+line
  
- put [raw]body_part_scan_size documentation in the right
- section of man page
- fix bz 7796
+ adjusting to slight fingerprint change
+------------------------------------------------------------------------
+r1904165 | billcole | 2022-09-20 12:46:28 +0000 (Tue, 20 Sep 2022) | 1 
+line
  
+ adjusting to slight fingerprint change
 ------------------------------------------------------------------------
-r1874012 | gbechis | 2020-02-14 10:57:25 +0000 (Fri, 14 Feb 2020) | 2 
-lines
+r1904155 | hege | 2022-09-20 05:19:30 +0000 (Tue, 20 Sep 2022) | 2 lines
  
- another couple of too chatty info messages converted to dbg
+ Deprecate HeaderEval check_for_unique_subject_id(), 
+word_is_in_dictionary() functions (Bug 8051)
  
 ------------------------------------------------------------------------
-r1874010 | gbechis | 2020-02-14 10:35:39 +0000 (Fri, 14 Feb 2020) | 2 
-lines
- switch a too chatty info into a dbg statement
+r1904147 | sidney | 2022-09-19 12:57:56 +0000 (Mon, 19 Sep 2022) | 1 line
  
+ Bug 8045 - Drop privileges for the one-time initialization of plugins at 
+start of spamd
 ------------------------------------------------------------------------
-r1873859 | gbechis | 2020-02-10 14:33:45 +0000 (Mon, 10 Feb 2020) | 2 
-lines
+r1904140 | sidney | 2022-09-18 23:52:24 +0000 (Sun, 18 Sep 2022) | 1 line
  
- one more OLEMacro marker
+ Bug 8048 - Make default for pyzor and raxor2 fork options 0 on Windows
+------------------------------------------------------------------------
+r1904139 | sidney | 2022-09-18 23:48:01 +0000 (Sun, 18 Sep 2022) | 1 line
  
+ Bug 8047 - work around MSG_DONTWAIT not existing on Windows
 ------------------------------------------------------------------------
-r1873752 | gbechis | 2020-02-07 18:37:10 +0000 (Fri, 07 Feb 2020) | 2 
-lines
+r1904059 | hege | 2022-09-14 04:53:19 +0000 (Wed, 14 Sep 2022) | 2 lines
  
- sync OLEVBMacro plugin with trunk
+ Trivial debug line typo fix
  
 ------------------------------------------------------------------------
-r1873340 | hege | 2020-01-29 21:38:08 +0000 (Wed, 29 Jan 2020) | 2 lines
- Clarify mimepart limit
+r1903986 | sidney | 2022-09-10 20:10:55 +0000 (Sat, 10 Sep 2022) | 1 line
  
+ 4.0.0-rc2 RELEASED
 ------------------------------------------------------------------------
-r1873200 | hege | 2020-01-27 09:43:17 +0000 (Mon, 27 Jan 2020) | 2 lines
+r1903975 | sidney | 2022-09-10 15:04:50 +0000 (Sat, 10 Sep 2022) | 1 line
  
- More DKIM test files for different CRLF/LF cases
+ preparing to release 4.0.0-rc2
+------------------------------------------------------------------------
+r1903966 | sidney | 2022-09-10 12:21:30 +0000 (Sat, 10 Sep 2022) | 1 line
  
+ Minor edit to release build instructions documentation
 ------------------------------------------------------------------------
-r1873123 | kmcgrail | 2020-01-25 02:49:19 +0000 (Sat, 25 Jan 2020) | 1 
-line
+r1903962 | sidney | 2022-09-10 11:28:35 +0000 (Sat, 10 Sep 2022) | 1 line
  
- preparing to release 3.4.4 (post rc-1)
+ Bug 8038 - work around quirk of newer Extutils::MakeMaker on Windows 
+with dmake
 ------------------------------------------------------------------------
-r1873122 | kmcgrail | 2020-01-25 02:04:07 +0000 (Sat, 25 Jan 2020) | 1 
-line
+r1903921 | sidney | 2022-09-08 03:11:07 +0000 (Thu, 08 Sep 2022) | 1 line
  
- preparing announcement for 3.4.4
+ Bug 8040 - Add note to test that has a very rare failure due to race 
+condition
 ------------------------------------------------------------------------
-r1872966 | kmcgrail | 2020-01-19 00:30:44 +0000 (Sun, 19 Jan 2020) | 1 
-line
+r1903917 | sidney | 2022-09-07 20:51:34 +0000 (Wed, 07 Sep 2022) | 1 line
  
- Preparing to release 3.4.4
+ Bug 8033 - Add PRAGMA to SQLite test to speed test without unreliable 
+use of /dev/shm
 ------------------------------------------------------------------------
-r1872942 | hege | 2020-01-18 08:44:49 +0000 (Sat, 18 Jan 2020) | 2 lines
+r1903904 | sidney | 2022-09-06 21:31:50 +0000 (Tue, 06 Sep 2022) | 1 line
  
- Revert DKIM change from Revision 1864870 (Bug 7785)
+ bug 8036 - set -zsh so ps -C spamd works on linux
+------------------------------------------------------------------------
+r1903878 | sidney | 2022-09-05 10:39:38 +0000 (Mon, 05 Sep 2022) | 1 line
  
+ Fix typo in pod doc
 ------------------------------------------------------------------------
-r1872936 | gbechis | 2020-01-17 23:30:50 +0000 (Fri, 17 Jan 2020) | 2 
-lines
+r1903870 | sidney | 2022-09-05 05:54:46 +0000 (Mon, 05 Sep 2022) | 1 line
  
- catch some more Microsoft Office encrypted documents
+ bug 8033 - Remove use of /dev/shm to speed up test because that causes 
+test failure on some machines. Label test as long running
+------------------------------------------------------------------------
+r1903850 | sidney | 2022-09-03 20:18:01 +0000 (Sat, 03 Sep 2022) | 1 line
  
+ bug 8039 - Remove no longer used code accidentally left in Makefile.PL
 ------------------------------------------------------------------------
-r1872935 | gbechis | 2020-01-17 23:24:35 +0000 (Fri, 17 Jan 2020) | 2 
-lines
+r1903795 | sidney | 2022-09-01 00:29:49 +0000 (Thu, 01 Sep 2022) | 1 line
  
- make SpamAssassin compatible with Perl 5.8.x again
+ Bug 8034 Fix test failure when Net::DNS::Nameserver is not installed
+------------------------------------------------------------------------
+r1903794 | billcole | 2022-08-31 19:37:48 +0000 (Wed, 31 Aug 2022) | 1 
+line
  
+ Bug #8037
 ------------------------------------------------------------------------
-r1872912 | gbechis | 2020-01-17 10:31:08 +0000 (Fri, 17 Jan 2020) | 2 
+r1903782 | gbechis | 2022-08-30 20:42:01 +0000 (Tue, 30 Aug 2022) | 3 
 lines
  
- Increase fns_extrachars default value to 50
+ Mail::SpamAssassin::SubProcBackChannel is needed
+ fix bz #8035
  
 ------------------------------------------------------------------------
-r1872864 | hege | 2020-01-16 07:40:02 +0000 (Thu, 16 Jan 2020) | 2 lines
+r1903693 | hege | 2022-08-26 06:00:47 +0000 (Fri, 26 Aug 2022) | 2 lines
  
Add missing is_admin to (raw)body_part_scan_size
Bug 8032 - DCC meta failure
  
 ------------------------------------------------------------------------
-r1872863 | hege | 2020-01-16 07:31:23 +0000 (Thu, 16 Jan 2020) | 2 lines
+r1903659 | sidney | 2022-08-24 10:32:02 +0000 (Wed, 24 Aug 2022) | 1 line
  
- Sync CREDITS from trunk
+ 4.0.0-rc1 RELEASED
+------------------------------------------------------------------------
+r1903655 | sidney | 2022-08-24 09:11:42 +0000 (Wed, 24 Aug 2022) | 1 line
  
+ preparing to release 4.0.0-rc1
 ------------------------------------------------------------------------
-r1872862 | hege | 2020-01-16 07:17:34 +0000 (Thu, 16 Jan 2020) | 2 lines
+r1903650 | sidney | 2022-08-24 02:23:32 +0000 (Wed, 24 Aug 2022) | 1 line
  
- Check priority values
+ bug 7981 - Update UPGRADE file for 4.0.0 replease. Re-wrap 4.0.0 
+announcements file from 72 to 70 columns
+------------------------------------------------------------------------
+r1903649 | sidney | 2022-08-24 01:58:14 +0000 (Wed, 24 Aug 2022) | 1 line
  
+ bug 8030 - Have spamd save incoming @INC to pass as -I options when it 
+does a SIGHUP restart of itself
 ------------------------------------------------------------------------
-r1872861 | hege | 2020-01-16 07:14:23 +0000 (Thu, 16 Jan 2020) | 2 lines
+r1903647 | gbechis | 2022-08-23 21:42:53 +0000 (Tue, 23 Aug 2022) | 2 
+lines
  
Use compiled patterns
match more custom uris
  
 ------------------------------------------------------------------------
-r1872800 | kmcgrail | 2020-01-15 02:29:58 +0000 (Wed, 15 Jan 2020) | 1 
-line
+r1903607 | sidney | 2022-08-21 05:14:57 +0000 (Sun, 21 Aug 2022) | 1 line
  
-  FromNameSpoof.pm requires 5.10.1+ so clarifying the docs on 3.4 EOL 
-branch
+ Correct typo force-mirror -> forcemirror
 ------------------------------------------------------------------------
-r1872785 | hege | 2020-01-14 15:59:37 +0000 (Tue, 14 Jan 2020) | 2 lines
+r1903603 | sidney | 2022-08-20 23:26:52 +0000 (Sat, 20 Aug 2022) | 1 line
  
- Improve SUBJ_ALL_CAPS
+ Announcement file rewritten for 4.0.0, word wrapped at 72 (Thunderbird's 
+default for plain text), placeholder for file hashes
+------------------------------------------------------------------------
+r1903602 | sidney | 2022-08-20 20:17:24 +0000 (Sat, 20 Aug 2022) | 1 line
  
+ bug 6439 - Add new test file from previous commit to MANIFEST
 ------------------------------------------------------------------------
-r1872772 | hege | 2020-01-14 11:55:35 +0000 (Tue, 14 Jan 2020) | 2 lines
+r1903595 | sidney | 2022-08-20 11:39:17 +0000 (Sat, 20 Aug 2022) | 1 line
  
- Fix nosubject and maxhits tflags when sa-compile is used
+ bug 8025 - Add a comment referencing this issue to the fix already 
+committed
+------------------------------------------------------------------------
+r1903581 | sidney | 2022-08-20 00:19:41 +0000 (Sat, 20 Aug 2022) | 1 line
  
+ bug 8029 - Change tests that use a spamd pid file to make use of the one 
+already set up in SATest.pm
 ------------------------------------------------------------------------
-r1872755 | hege | 2020-01-14 06:12:47 +0000 (Tue, 14 Jan 2020) | 2 lines
+r1903556 | gbechis | 2022-08-19 08:16:08 +0000 (Fri, 19 Aug 2022) | 2 
+lines
  
- Fix debug test
+ pubish rules
  
 ------------------------------------------------------------------------
-r1871709 | hege | 2019-12-17 21:42:32 +0000 (Tue, 17 Dec 2019) | 2 lines
+r1903543 | sidney | 2022-08-18 23:36:56 +0000 (Thu, 18 Aug 2022) | 1 line
  
- Don't canonicalize stuff like #abcdef ?foobar /image.gif as http://
+ bug 6439 - Add test case to t/extracttext.t to demonstrate using cat to 
+handle text disguised as octet/stream
+------------------------------------------------------------------------
+r1903528 | sidney | 2022-08-18 16:49:59 +0000 (Thu, 18 Aug 2022) | 1 line
  
+ Add DBD::SQLite min version requirement to some tests that didn't check 
+for it. Cosmetic correction where it said 1.59
 ------------------------------------------------------------------------
-r1871708 | hege | 2019-12-17 20:40:03 +0000 (Tue, 17 Dec 2019) | 2 lines
+r1903510 | sidney | 2022-08-18 04:32:14 +0000 (Thu, 18 Aug 2022) | 1 line
  
- Bug 7776 - Limit Bayes parsed token count
+ bug 8028 - Fix tests that failed when run in perl built with 
+uselongdouble that was not a SpamAssassin bug
+------------------------------------------------------------------------
+r1903469 | sidney | 2022-08-17 00:01:47 +0000 (Wed, 17 Aug 2022) | 1 line
  
+ bug 8028 - SQLite now handles upsert using same syntax as pgsql, fix an 
+error message
 ------------------------------------------------------------------------
-r1871698 | hege | 2019-12-17 14:28:28 +0000 (Tue, 17 Dec 2019) | 2 lines
+r1903460 | gbechis | 2022-08-16 13:14:59 +0000 (Tue, 16 Aug 2022) | 2 
+lines
  
- Trim whitespace properly
+ test for some html links
  
 ------------------------------------------------------------------------
-r1871697 | hege | 2019-12-17 14:10:37 +0000 (Tue, 17 Dec 2019) | 2 lines
+r1903454 | sidney | 2022-08-16 08:33:56 +0000 (Tue, 16 Aug 2022) | 1 line
  
- Bug 7778 - T_KAM_HTML_FONT_INVALID false positive for "inherit"
+ Bug 8002 - Exclude another set of PerlCritic policies found on a CPAN 
+test machine
+------------------------------------------------------------------------
+r1903420 | sidney | 2022-08-15 05:06:36 +0000 (Mon, 15 Aug 2022) | 1 line
  
+ More complete fix for taint than in previous commit, using the code 
+already in sa_t_init()
 ------------------------------------------------------------------------
-r1871204 | kmcgrail | 2019-12-11 22:44:50 +0000 (Wed, 11 Dec 2019) | 1 
-line
+r1903411 | sidney | 2022-08-14 11:22:37 +0000 (Sun, 14 Aug 2022) | 1 line
  
- more tweaks to build process for clarity and syncing 3.4 and trunk
+ bug 8026 - Update extracttest.t with test data that works with more 
+versions of tesseract
 ------------------------------------------------------------------------
-r1871200 | kmcgrail | 2019-12-11 22:06:34 +0000 (Wed, 11 Dec 2019) | 1 
-line
+r1903388 | gbechis | 2022-08-13 09:07:22 +0000 (Sat, 13 Aug 2022) | 2 
+lines
+ Google storage cloud abuse rule
  
- Updating Build Docs to be clearer
 ------------------------------------------------------------------------
-r1871194 | kmcgrail | 2019-12-11 21:17:29 +0000 (Wed, 11 Dec 2019) | 1 
-line
+r1903383 | sidney | 2022-08-13 00:50:00 +0000 (Sat, 13 Aug 2022) | 1 line
  
- 3.4.3 RELEASED
+ bug 8025 - Use better untaint pattern for Windows file paths than the 
+incomplete fix for bug 8010
 ------------------------------------------------------------------------
-r1871193 | kmcgrail | 2019-12-11 21:14:24 +0000 (Wed, 11 Dec 2019) | 1 
-line
+r1903375 | sidney | 2022-08-12 15:48:45 +0000 (Fri, 12 Aug 2022) | 1 line
  
- Fixing copyright on CREDITS
+ bug 7666 - Make declared module dependencies more accurate. Reduce noise 
+in make_install.t, sa_compile.t on macOS
 ------------------------------------------------------------------------
-r1871192 | kmcgrail | 2019-12-11 21:08:12 +0000 (Wed, 11 Dec 2019) | 1 
-line
+r1903374 | sidney | 2022-08-12 15:38:44 +0000 (Fri, 12 Aug 2022) | 1 line
  
- final 3.4.3 announcement with new hashes
+ bug 7666 - Fix tests running in taint mode that invoke spamassassin when 
+PERL5LIB is used to pass in module paths
 ------------------------------------------------------------------------
-r1871189 | kmcgrail | 2019-12-11 20:53:22 +0000 (Wed, 11 Dec 2019) | 1 
+r1903372 | mmartinec | 2022-08-12 14:26:43 +0000 (Fri, 12 Aug 2022) | 1 
 line
  
- Preparing to release 3.4.3 with a few small updates
+ AskDNS.pm: documentation clarification
 ------------------------------------------------------------------------
-r1871188 | kmcgrail | 2019-12-11 20:45:11 +0000 (Wed, 11 Dec 2019) | 1 
-line
+r1903371 | sidney | 2022-08-12 13:19:19 +0000 (Fri, 12 Aug 2022) | 1 line
  
- update of the announcement text prepping for 3.4.3 release
+ Fix typo in previous commit
 ------------------------------------------------------------------------
-r1871122 | hege | 2019-12-10 07:53:03 +0000 (Tue, 10 Dec 2019) | 2 lines
+r1903369 | sidney | 2022-08-12 11:19:32 +0000 (Fri, 12 Aug 2022) | 1 line
  
- Some missing OLEMacro -> OLEVBMacro renames
+ Skip part of test if running in perl linked with too old libdb for this 
+test's db file
+------------------------------------------------------------------------
+r1903365 | sidney | 2022-08-12 02:50:04 +0000 (Fri, 12 Aug 2022) | 1 line
  
+ Fix taint error in test when run in shell that sets 
+/Users/sidney/.bashrc in environment, such as FreeBSD
 ------------------------------------------------------------------------
-r1871075 | billcole | 2019-12-09 07:40:37 +0000 (Mon, 09 Dec 2019) | 3 
+r1903359 | gbechis | 2022-08-11 13:24:23 +0000 (Thu, 11 Aug 2022) | 2 
 lines
  
- Flesh out "Notable changes," and fix some wrapping
+ avoid a lint warning in named capture code
  
 ------------------------------------------------------------------------
-r1871074 | gbechis | 2019-12-09 07:30:42 +0000 (Mon, 09 Dec 2019) | 2 
-lines
+r1903351 | hege | 2022-08-11 11:11:13 +0000 (Thu, 11 Aug 2022) | 2 lines
  
- mention _SUBTESTSCOLLAPSED(,)_ template tag
+ Test that escaping %{} works
  
 ------------------------------------------------------------------------
-r1871035 | gbechis | 2019-12-08 10:12:35 +0000 (Sun, 08 Dec 2019) | 3 
-lines
+r1903347 | hege | 2022-08-11 11:00:08 +0000 (Thu, 11 Aug 2022) | 2 lines
  
- Describe changes to DNSEval and HashBL plugins.
- Add info about new subjprefix keyword
+ Catch regexp warnings
  
 ------------------------------------------------------------------------
-r1870963 | gbechis | 2019-12-07 08:31:50 +0000 (Sat, 07 Dec 2019) | 2 
-lines
- OLEMacro plugin has been renamed to OLEVBMacro
+r1903269 | sidney | 2022-08-07 12:21:13 +0000 (Sun, 07 Aug 2022) | 1 line
  
+ reorder checks for whether test can be run to avoid a spurious message 
+when there is no spamc built
 ------------------------------------------------------------------------
-r1870943 | kmcgrail | 2019-12-07 01:07:41 +0000 (Sat, 07 Dec 2019) | 1 
+r1903240 | mmartinec | 2022-08-05 14:22:30 +0000 (Fri, 05 Aug 2022) | 1 
 line
  
- 1st pass at 3.4.3 announcement
+ util: idn_to_ascii logging to include the affected string
 ------------------------------------------------------------------------
-r1870940 | kmcgrail | 2019-12-06 23:58:14 +0000 (Fri, 06 Dec 2019) | 1 
-line
+r1903224 | sidney | 2022-08-04 11:08:11 +0000 (Thu, 04 Aug 2022) | 1 line
  
- preparing to release 3.4.3
+ Add defined check for a value that can end up undefined
 ------------------------------------------------------------------------
-r1870809 | gbechis | 2019-12-04 07:53:41 +0000 (Wed, 04 Dec 2019) | 2 
+r1903198 | gbechis | 2022-08-02 15:50:25 +0000 (Tue, 02 Aug 2022) | 2 
 lines
  
- better regexp
+ fix man page
  
 ------------------------------------------------------------------------
-r1870806 | hege | 2019-12-04 07:41:25 +0000 (Wed, 04 Dec 2019) | 2 lines
+r1903194 | sidney | 2022-08-02 10:37:01 +0000 (Tue, 02 Aug 2022) | 1 line
  
- Don't capture $1 for no reason
+ bug 7666 - Fix module dependency checks in Makefile.PL so CPAN tests can 
+install missing modules and continue running
+------------------------------------------------------------------------
+r1903193 | sidney | 2022-08-02 10:32:07 +0000 (Tue, 02 Aug 2022) | 1 line
  
+ bug 7666 - Fix tests that run spamassassin in taint mode not passing 
+through PERL5LIB path
 ------------------------------------------------------------------------
-r1870805 | gbechis | 2019-12-04 07:36:57 +0000 (Wed, 04 Dec 2019) | 3 
-lines
+r1903176 | sidney | 2022-08-02 01:16:56 +0000 (Tue, 02 Aug 2022) | 1 line
  
- change some default values to catch more macros
- seen on the wild
+ Remove unnecessary info line about SQL tests when SQL tests are skipped
+------------------------------------------------------------------------
+r1903131 | sidney | 2022-07-31 05:02:23 +0000 (Sun, 31 Jul 2022) | 1 line
+ bug 8020 - Make failed NetAddr::IP dependency not fatal when checking 
+dependencies
+------------------------------------------------------------------------
+r1903087 | sidney | 2022-07-29 04:04:40 +0000 (Fri, 29 Jul 2022) | 1 line
  
+ Bug 8002 - Exclude another PerlCritic policy found on a CPAN test 
+machine, add required modules for test
 ------------------------------------------------------------------------
-r1870804 | gbechis | 2019-12-04 07:29:19 +0000 (Wed, 04 Dec 2019) | 3 
+r1903079 | gbechis | 2022-07-28 16:12:23 +0000 (Thu, 28 Jul 2022) | 3 
 lines
  
- add more rtf markers to catch dangerous ole objects
in rtf files
+ some .xls files are erroneously detected as encrypted,
look for a marker not present on encrypted files
  
 ------------------------------------------------------------------------
-r1870554 | hege | 2019-11-28 10:28:21 +0000 (Thu, 28 Nov 2019) | 2 lines
+r1903078 | jhardin | 2022-07-28 14:08:07 +0000 (Thu, 28 Jul 2022) | 1 line
  
- AskDNS should use parsed_metadata instead of extract_metadata
+ Add "page.link" as 2TLD for URIBL checks - e.g.: academia.page.link
+------------------------------------------------------------------------
+r1903070 | sidney | 2022-07-28 01:57:22 +0000 (Thu, 28 Jul 2022) | 1 line
  
+ Bug 8019 - Fix make_install.t so it can be run using prove -T
 ------------------------------------------------------------------------
-r1870552 | hege | 2019-11-28 10:04:41 +0000 (Thu, 28 Nov 2019) | 2 lines
+r1903063 | mmartinec | 2022-07-27 17:35:40 +0000 (Wed, 27 Jul 2022) | 1 
+line
  
- Fix LASTEXTERNAL* tag usage affecting askdns and action_depends_on_tags
+ fix t/root_spamd_*.t tests, they were expecting an extra blank before 
+the result message line from spamc
+------------------------------------------------------------------------
+r1903061 | mmartinec | 2022-07-27 16:04:02 +0000 (Wed, 27 Jul 2022) | 1 
+line
  
+ t/perlcritic.pl: remove exemption for Perlsecret "Baby Cart", deal with 
+the only case of its use in ExtractText.pm (the @{[]} hack is no longer 
+needed around split() in scalar context since perl5.11, we require 5.14 
+in SpamAssassin.pm)
 ------------------------------------------------------------------------
-r1870501 | hege | 2019-11-27 12:35:58 +0000 (Wed, 27 Nov 2019) | 2 lines
+r1903050 | sidney | 2022-07-27 09:20:50 +0000 (Wed, 27 Jul 2022) | 1 line
  
- Fix various Received parsings
+ Add test dependencies to ensure that CPAN test bots know about them
+------------------------------------------------------------------------
+r1903039 | sidney | 2022-07-26 22:19:21 +0000 (Tue, 26 Jul 2022) | 1 line
  
+ bug 8003 - fix path syntax when in Windows to let mkrule tests work
 ------------------------------------------------------------------------
-r1870497 | hege | 2019-11-27 10:05:04 +0000 (Wed, 27 Nov 2019) | 2 lines
+r1903033 | mmartinec | 2022-07-26 18:00:47 +0000 (Tue, 26 Jul 2022) | 1 
+line
  
- Bug 5646 - Postfix with set mail_name option doesn't recognize 
-authentication
+ t/perlcritic.pl: remove exemption for Perlsecret Goatse, deal with the 
+only two cases of its use in MIMEEval.pm, reduce perlcritic verbosity 
+from 10 to 9
+------------------------------------------------------------------------
+r1903032 | sidney | 2022-07-26 15:50:07 +0000 (Tue, 26 Jul 2022) | 1 line
  
+ bug 8003 - fix extra noise in test on Windows platform
 ------------------------------------------------------------------------
-r1870353 | kmcgrail | 2019-11-25 03:18:21 +0000 (Mon, 25 Nov 2019) | 1 
+r1903030 | mmartinec | 2022-07-26 13:35:35 +0000 (Tue, 26 Jul 2022) | 1 
 line
  
- preparing to release 3.4.3-rc7
+ perlcritic does not appreciate a !! operator
 ------------------------------------------------------------------------
-r1870344 | kmcgrail | 2019-11-24 20:31:12 +0000 (Sun, 24 Nov 2019) | 1 
+r1903010 | mmartinec | 2022-07-25 15:24:41 +0000 (Mon, 25 Jul 2022) | 1 
 line
  
- More polish on the collapsed subtests work
+ document ARC, cosmetics/style
 ------------------------------------------------------------------------
-r1870343 | gbechis | 2019-11-24 19:41:30 +0000 (Sun, 24 Nov 2019) | 2 
-lines
+r1903009 | mmartinec | 2022-07-25 15:21:20 +0000 (Mon, 25 Jul 2022) | 1 
+line
  
- fix a warning
+ MS::Plugin::DKIM : must not treat a selector "0" as missing! (also fixes 
+warnings: call method "result_detail" on an undefined value)
+------------------------------------------------------------------------
+r1902917 | mmartinec | 2022-07-21 18:04:30 +0000 (Thu, 21 Jul 2022) | 1 
+line
  
+ spelling in doc
 ------------------------------------------------------------------------
-r1870328 | gbechis | 2019-11-24 18:22:17 +0000 (Sun, 24 Nov 2019) | 4 
-lines
+r1902916 | mmartinec | 2022-07-21 17:55:37 +0000 (Thu, 21 Jul 2022) | 1 
+line
  
- Add a new SUBTESTSCOLLAPSED template tag
- with subtests collapsed similar to what printed
- in log file
+ documentation: match the list of recognized RR types to a regexp in code
+------------------------------------------------------------------------
+r1902912 | mmartinec | 2022-07-21 14:23:36 +0000 (Thu, 21 Jul 2022) | 1 
+line
  
+ set_tag() documentation small fix
 ------------------------------------------------------------------------
-r1870083 | gbechis | 2019-11-21 12:00:48 +0000 (Thu, 21 Nov 2019) | 2 
-lines
+r1902889 | hege | 2022-07-20 19:15:25 +0000 (Wed, 20 Jul 2022) | 2 lines
  
- put olevbmacro regression tests into MANIFEST file
+ Bug 8016 - Remove uridnsbl_skip_domain(s)
  
 ------------------------------------------------------------------------
-r1870058 | gbechis | 2019-11-20 21:16:01 +0000 (Wed, 20 Nov 2019) | 3 
+r1902865 | gbechis | 2022-07-19 21:11:55 +0000 (Tue, 19 Jul 2022) | 2 
 lines
  
- prevent a warning from filling logs with packet dumps
- useful only for debugging purposes
+ improve check for forged Hotmail headers due to Microsoft changes
  
 ------------------------------------------------------------------------
-r1870054 | gbechis | 2019-11-20 18:19:42 +0000 (Wed, 20 Nov 2019) | 2 
+r1902838 | gbechis | 2022-07-18 13:07:03 +0000 (Mon, 18 Jul 2022) | 2 
 lines
  
OLEVBMacro plugin regression tests
Google translate is used to obfuscate uris
  
 ------------------------------------------------------------------------
-r1869872 | gbechis | 2019-11-15 18:21:16 +0000 (Fri, 15 Nov 2019) | 3 
-lines
+r1902744 | kb | 2022-07-15 18:09:59 +0000 (Fri, 15 Jul 2022) | 7 lines
  
- silence some warnings if Archive::Zip
- is not installed
+ Bug 7980, plaintext_body_sig_ratio performance:  Replaced the one-shot, 
+prone
+ to backtrack signature identifying regex.  Now doing a fast single-pass 
+over
+ the entire string, using a minimal regex to identify signature 
+delimiters.
  
-------------------------------------------------------------------------
-r1869855 | gbechis | 2019-11-15 14:45:11 +0000 (Fri, 15 Nov 2019) | 3 
-lines
+ Also ignore decoy markers at the end.
  
- explain better that Archive::Zip and IO::String Perl
- modules are needed for OLEVBMacro plugin
  
 ------------------------------------------------------------------------
-r1869761 | gbechis | 2019-11-13 17:44:00 +0000 (Wed, 13 Nov 2019) | 3 
-lines
- fix SRS uri parser
- bz #6089
+r1902710 | sidney | 2022-07-14 04:24:51 +0000 (Thu, 14 Jul 2022) | 1 line
  
+ bug 8015 - Remove test for blocked bitly link. Bitly has no permanent 
+link to test with
 ------------------------------------------------------------------------
-r1869726 | gbechis | 2019-11-13 08:25:09 +0000 (Wed, 13 Nov 2019) | 2 
+r1902571 | gbechis | 2022-07-08 13:44:54 +0000 (Fri, 08 Jul 2022) | 2 
 lines
  
Add another debug message
use DKIM from $suppl_attrib if available
  
 ------------------------------------------------------------------------
-r1869721 | hege | 2019-11-13 06:07:03 +0000 (Wed, 13 Nov 2019) | 2 lines
- Fix pod warnings (Bug 7773)
+r1902513 | billcole | 2022-07-06 20:34:44 +0000 (Wed, 06 Jul 2022) | 1 
+line
  
+ New spam-for-hire seen
 ------------------------------------------------------------------------
-r1869700 | gbechis | 2019-11-12 14:07:29 +0000 (Tue, 12 Nov 2019) | 2 
+r1902486 | gbechis | 2022-07-05 13:43:27 +0000 (Tue, 05 Jul 2022) | 2 
 lines
  
- explain that olemacro_extended_scan is needed to run 
-check_olemacro_renamed
+ adds proper if can() sub
  
 ------------------------------------------------------------------------
-r1869683 | gbechis | 2019-11-12 08:47:07 +0000 (Tue, 12 Nov 2019) | 2 
+r1902484 | gbechis | 2022-07-05 13:37:53 +0000 (Tue, 05 Jul 2022) | 3 
 lines
  
- Add additional debug message
+ Checks if SPF checks have been skipped because EnvelopeFrom cannot be 
+determined,
+ to be used in meta-rules
  
 ------------------------------------------------------------------------
-r1869642 | billcole | 2019-11-11 05:28:07 +0000 (Mon, 11 Nov 2019) | 1 
-line
+r1902425 | sidney | 2022-07-03 10:22:54 +0000 (Sun, 03 Jul 2022) | 1 line
  
- Spelling
+ bug 8003 - after changes made for other tests, re_base_extraction.t now 
+works on Windows
 ------------------------------------------------------------------------
-r1869639 | kmcgrail | 2019-11-11 04:09:44 +0000 (Mon, 11 Nov 2019) | 1 
-line
+r1902424 | sidney | 2022-07-03 09:30:08 +0000 (Sun, 03 Jul 2022) | 1 line
  
- Fixing misspellings noted in bz7772
+ Bug 8003 - mass_check.t requires masscheck which is not written to run 
+on Windows
 ------------------------------------------------------------------------
-r1869595 | kmcgrail | 2019-11-09 06:08:55 +0000 (Sat, 09 Nov 2019) | 1 
-line
+r1902423 | sidney | 2022-07-03 09:23:00 +0000 (Sun, 03 Jul 2022) | 1 line
  
- preparing to release 3.4.3-rc6
+ Bug 8003 - reuse.t requires masscheck which is not written to run on 
+Windows
 ------------------------------------------------------------------------
-r1869333 | gbechis | 2019-11-03 15:13:03 +0000 (Sun, 03 Nov 2019) | 2 
+r1902385 | gbechis | 2022-07-01 07:26:04 +0000 (Fri, 01 Jul 2022) | 2 
 lines
  
- Rename OLEMacro plugin to OLEVBMacro to be more clear
+ add ARC rules
  
 ------------------------------------------------------------------------
-r1869331 | gbechis | 2019-11-03 14:59:44 +0000 (Sun, 03 Nov 2019) | 2 
+r1902301 | gbechis | 2022-06-28 07:28:36 +0000 (Tue, 28 Jun 2022) | 3 
 lines
  
- sync with trunk, check .xltx files as well
+ fix sql schema on MariaDB 10.1
+ bz #8012
  
 ------------------------------------------------------------------------
-r1869065 | gbechis | 2019-10-28 07:21:12 +0000 (Mon, 28 Oct 2019) | 2 
+r1902276 | gbechis | 2022-06-27 10:01:33 +0000 (Mon, 27 Jun 2022) | 2 
 lines
  
- Add more info to subjprefix keyword documentation
+ unbreak DKIM when $suppl_attrib are used (amavisd-new for example)
  
 ------------------------------------------------------------------------
-r1868828 | kmcgrail | 2019-10-24 01:29:33 +0000 (Thu, 24 Oct 2019) | 1 
-line
+r1902245 | jhardin | 2022-06-25 18:30:57 +0000 (Sat, 25 Jun 2022) | 1 line
  
- preparing to release 3.4.3-rc5
+ Add exclusion for myimages and myphotos to __URI_TRY_3LD
 ------------------------------------------------------------------------
-r1868693 | hege | 2019-10-21 10:58:45 +0000 (Mon, 21 Oct 2019) | 2 lines
- Remove unused unties
+r1902055 | sidney | 2022-06-19 04:48:21 +0000 (Sun, 19 Jun 2022) | 1 line
  
+ bug 8003 - Reduce noise of warnings in Windows lock file code to make 
+some tests practical in Windows
 ------------------------------------------------------------------------
-r1868685 | gbechis | 2019-10-21 09:34:51 +0000 (Mon, 21 Oct 2019) | 16 
-lines
- Add a new subjprefix keyword.
- This keyword will add a prefix in emails Subject if a rule is matched.
- To enable this option "rewrite_header Subject" config option must be 
-enabled
- as well.
- The check "if can(Mail::SpamAssassin::Conf::feature_subjprefix)"
- should be used to silence warnings in previous SpamAssassin
- versions.
- This feature could not work out-of-the box if the glue
- software that calls SpamAssassin (MimeDefang, Amavisd-new, ...)
- uses the original email instead of the one produced by SA.
- Some improvements to those softwares may be needed before enabling
- this feature.
+r1902053 | sidney | 2022-06-19 04:10:57 +0000 (Sun, 19 Jun 2022) | 1 line
  
+ bug 8003 - Fix bayesbdb.t not closing db files during test, now works on 
+Windows
 ------------------------------------------------------------------------
-r1868631 | gbechis | 2019-10-19 15:33:18 +0000 (Sat, 19 Oct 2019) | 2 
-lines
- fix sought header rules generation
+r1901958 | sidney | 2022-06-16 03:44:43 +0000 (Thu, 16 Jun 2022) | 1 line
  
+ bug 8003 - Remove debugging flag accidentally left in last commit
 ------------------------------------------------------------------------
-r1868412 | hege | 2019-10-13 19:49:26 +0000 (Sun, 13 Oct 2019) | 2 lines
- Add test for check_rbl() negative subtest
+r1901956 | sidney | 2022-06-15 23:10:44 +0000 (Wed, 15 Jun 2022) | 1 line
  
+ bug 8011 - Fix Pyzor and Razor tests and various code that supports them 
+for use in Windows
 ------------------------------------------------------------------------
-r1867881 | hege | 2019-10-02 10:25:18 +0000 (Wed, 02 Oct 2019) | 2 lines
- Add uri test for http://foo/ Firefix like rewrite
+r1901955 | sidney | 2022-06-15 22:57:24 +0000 (Wed, 15 Jun 2022) | 1 line
  
+ bug 8010 - remove lines obsoleted by other untaint fixes
 ------------------------------------------------------------------------
-r1867230 | hege | 2019-09-20 14:13:18 +0000 (Fri, 20 Sep 2019) | 2 lines
- Small fix for escaped quotes
+r1901954 | sidney | 2022-06-15 22:53:05 +0000 (Wed, 15 Jun 2022) | 1 line
  
+ bug 8003 - Skip tests or portions that cannot run in Windows, change 
+other non-portable things in tests to portable equivalents
 ------------------------------------------------------------------------
-r1867225 | hege | 2019-09-20 13:15:30 +0000 (Fri, 20 Sep 2019) | 2 lines
+r1901953 | sidney | 2022-06-15 22:45:51 +0000 (Wed, 15 Jun 2022) | 1 line
  
- Improve :name :addr parser (Bug 7753)
+ bug 8003 - Change ip address used in test from one that Windows is too 
+strict with
+------------------------------------------------------------------------
+r1901952 | sidney | 2022-06-15 22:23:22 +0000 (Wed, 15 Jun 2022) | 1 line
  
+ bug 8010 - Fix untaint pattern in File::Find in Windows
 ------------------------------------------------------------------------
-r1867159 | gbechis | 2019-09-19 06:29:14 +0000 (Thu, 19 Sep 2019) | 2 
-lines
+r1901951 | sidney | 2022-06-15 21:57:36 +0000 (Wed, 15 Jun 2022) | 1 line
  
- better ipv6 regexp
+ bug 8003 - disable these tests i Windows since umask is a no-op there
+------------------------------------------------------------------------
+r1901899 | sidney | 2022-06-14 09:15:28 +0000 (Tue, 14 Jun 2022) | 1 line
  
+ Bug 8009 - Delete anti-pattern that matches when some optional modules 
+are missing, and not real errors
 ------------------------------------------------------------------------
-r1867055 | hege | 2019-09-17 12:35:39 +0000 (Tue, 17 Sep 2019) | 2 lines
+r1901887 | billcole | 2022-06-13 18:26:12 +0000 (Mon, 13 Jun 2022) | 1 
+line
  
- Use cleaned list for check_hashbl_uris
+ typo in prior comment
+------------------------------------------------------------------------
+r1901885 | billcole | 2022-06-13 18:10:09 +0000 (Mon, 13 Jun 2022) | 1 
+line
  
+ New MID pattern rule, tests very well on private B2B system.
 ------------------------------------------------------------------------
-r1866389 | hege | 2019-09-04 13:49:07 +0000 (Wed, 04 Sep 2019) | 2 lines
+r1901879 | gbechis | 2022-06-13 14:03:50 +0000 (Mon, 13 Jun 2022) | 2 
+lines
  
- Avoid warning: Use of uninitialized value $dom in pattern match (m//) at 
-.../RegistryBoundaries.pm
+ mention Authentication-Results header in man page
  
 ------------------------------------------------------------------------
-r1866203 | hege | 2019-08-31 11:47:09 +0000 (Sat, 31 Aug 2019) | 2 lines
- Fix DUPMIN back to default 10.. duh.
+r1901875 | sidney | 2022-06-13 10:09:28 +0000 (Mon, 13 Jun 2022) | 1 line
  
+ bug 8007 - POSIX::_exit in forked child on Windows terminates parent, 
+use exit() instead if on Windows
 ------------------------------------------------------------------------
-r1866202 | hege | 2019-08-31 11:43:17 +0000 (Sat, 31 Aug 2019) | 2 lines
+r1901764 | sidney | 2022-06-09 02:12:54 +0000 (Thu, 09 Jun 2022) | 1 line
  
- Fix loglevel for duplicate logline suppressor
+ Bug 8003 - Fix compile time error in Windows in test that is supposed to 
+be skipped on Windows
+------------------------------------------------------------------------
+r1901738 | sidney | 2022-06-08 02:26:05 +0000 (Wed, 08 Jun 2022) | 1 line
  
+ bug 8005 - sleep() required in test in Windows where select() is needed 
+in other OS
 ------------------------------------------------------------------------
-r1866198 | gbechis | 2019-08-31 09:42:33 +0000 (Sat, 31 Aug 2019) | 2 
+r1901719 | gbechis | 2022-06-07 08:41:50 +0000 (Tue, 07 Jun 2022) | 3 
 lines
  
- Install v343.pre as well
+ Add check_arc_signed() and check_arc_valid() subs to verify ARC 
+signatures.
+ bz #7935
  
 ------------------------------------------------------------------------
-r1866181 | kmcgrail | 2019-08-31 04:33:43 +0000 (Sat, 31 Aug 2019) | 1 
-line
+r1901667 | sidney | 2022-06-05 12:50:11 +0000 (Sun, 05 Jun 2022) | 1 line
  
- preparing to release 3.4.3-rc4
+ Bug 8003 - Fix determining when to skip spamc/spamd tests in Windows
 ------------------------------------------------------------------------
-r1866128 | hege | 2019-08-30 07:49:30 +0000 (Fri, 30 Aug 2019) | 2 lines
+r1901657 | hege | 2022-06-05 08:24:49 +0000 (Sun, 05 Jun 2022) | 2 lines
  
- Bug 7747 - Limit checked mime parts
+ Fix revision 1901651
  
 ------------------------------------------------------------------------
-r1865616 | hege | 2019-08-21 10:53:07 +0000 (Wed, 21 Aug 2019) | 2 lines
+r1901656 | hege | 2022-06-05 08:03:13 +0000 (Sun, 05 Jun 2022) | 2 lines
  
- Skip more misparsed uri garbage
+ Remove superfluous return
  
 ------------------------------------------------------------------------
-r1865612 | hege | 2019-08-21 09:19:39 +0000 (Wed, 21 Aug 2019) | 2 lines
- Improve schemeless uri parser start boundary
+r1901651 | sidney | 2022-06-05 03:43:52 +0000 (Sun, 05 Jun 2022) | 1 line
  
+ bug 8003 - skip tests that fail in Windows that need further 
+investigation to determine if they can be fixed
 ------------------------------------------------------------------------
-r1865609 | hege | 2019-08-21 08:40:41 +0000 (Wed, 21 Aug 2019) | 2 lines
- Make uri parser find longer uris (up to 2k) which are common these days
+r1901649 | sidney | 2022-06-05 02:41:19 +0000 (Sun, 05 Jun 2022) | 1 line
  
+ bug 8003 - Remove check for sudo when in Windows
 ------------------------------------------------------------------------
-r1865409 | hege | 2019-08-19 04:19:58 +0000 (Mon, 19 Aug 2019) | 2 lines
+r1901581 | hege | 2022-06-03 05:46:32 +0000 (Fri, 03 Jun 2022) | 2 lines
  
- DNS name max length is actually 253 chars. Quote % for uniformity.
+ Minor got_hit/rule_ready cleanups (Bug 7999)
  
 ------------------------------------------------------------------------
-r1865107 | hege | 2019-08-14 11:35:47 +0000 (Wed, 14 Aug 2019) | 2 lines
+r1901580 | hege | 2022-06-03 05:46:17 +0000 (Fri, 03 Jun 2022) | 2 lines
  
- More uri email parser tweaks
+ Add missing semicolon, cosmetic
  
 ------------------------------------------------------------------------
-r1865102 | hege | 2019-08-14 09:37:00 +0000 (Wed, 14 Aug 2019) | 2 lines
+r1901579 | hege | 2022-06-03 05:12:35 +0000 (Fri, 03 Jun 2022) | 2 lines
  
- Commit all uri parser changes from trunk to 3.4
+ Minor rule_ready optimization
  
 ------------------------------------------------------------------------
-r1865095 | hege | 2019-08-14 08:34:58 +0000 (Wed, 14 Aug 2019) | 2 lines
+r1901578 | hege | 2022-06-03 05:02:42 +0000 (Fri, 03 Jun 2022) | 2 lines
  
- More email uri parser tweaks
+ Clean up plugin, don't call unnecessary got_hit() (Bug 7999)
  
 ------------------------------------------------------------------------
-r1865086 | hege | 2019-08-14 05:17:00 +0000 (Wed, 14 Aug 2019) | 2 lines
+r1901577 | hege | 2022-06-03 04:59:29 +0000 (Fri, 03 Jun 2022) | 2 lines
  
- Update html render docs
+ Check lint_rules correctly
  
 ------------------------------------------------------------------------
-r1865051 | hege | 2019-08-13 17:09:47 +0000 (Tue, 13 Aug 2019) | 2 lines
- More uri parser cleanups
+r1901573 | sidney | 2022-06-02 22:41:38 +0000 (Thu, 02 Jun 2022) | 1 line
  
+ bug 8003 - Untaint PATH in Windows
 ------------------------------------------------------------------------
-r1865044 | hege | 2019-08-13 13:54:37 +0000 (Tue, 13 Aug 2019) | 2 lines
+r1901534 | hege | 2022-06-02 05:40:27 +0000 (Thu, 02 Jun 2022) | 2 lines
  
- Remove accidental /g
+ Bug 8003 - Many test failures in Windows due to various platform 
+dependent things
  
 ------------------------------------------------------------------------
-r1865043 | hege | 2019-08-13 13:53:12 +0000 (Tue, 13 Aug 2019) | 2 lines
+r1901533 | hege | 2022-06-02 05:32:04 +0000 (Thu, 02 Jun 2022) | 2 lines
  
- Strip common schemeless skype: email: prefixes from mails
+ Use find_executable_in_env_path for better Windows support, clean up code
  
 ------------------------------------------------------------------------
-r1865041 | gbechis | 2019-08-13 13:41:52 +0000 (Tue, 13 Aug 2019) | 2 
-lines
+r1901532 | hege | 2022-06-02 05:31:47 +0000 (Thu, 02 Jun 2022) | 2 lines
  
- improve debug message
+ find_executable_in_env_path: search .exe files on Windows
  
 ------------------------------------------------------------------------
-r1865039 | hege | 2019-08-13 13:15:38 +0000 (Tue, 13 Aug 2019) | 2 lines
- Schemeless uri parser improvements
+r1901489 | sidney | 2022-06-01 11:21:27 +0000 (Wed, 01 Jun 2022) | 1 line
  
+ Bug 8002 - Exclude more PerlCritic policies that are checked on CPAN 
+test machines
 ------------------------------------------------------------------------
-r1865030 | hege | 2019-08-13 11:58:14 +0000 (Tue, 13 Aug 2019) | 2 lines
+r1901451 | hege | 2022-05-31 13:32:39 +0000 (Tue, 31 May 2022) | 2 lines
  
Further email parsing and canonicalizing fixes
Skip dcc test on windows, I don't think a native cdcc.exe exists
  
 ------------------------------------------------------------------------
-r1865025 | hege | 2019-08-13 11:09:53 +0000 (Tue, 13 Aug 2019) | 2 lines
+r1901450 | hege | 2022-05-31 13:28:28 +0000 (Tue, 31 May 2022) | 2 lines
  
- Ignore schemeless emails without valid tld
+ Bug 8001 - extracttext.t test failure
  
 ------------------------------------------------------------------------
-r1865018 | hege | 2019-08-13 09:10:33 +0000 (Tue, 13 Aug 2019) | 2 lines
+r1901439 | sidney | 2022-05-31 06:37:39 +0000 (Tue, 31 May 2022) | 1 line
  
- Ignore empty uris from stripped body
+ bug 7986 - Cleanup of fix that was in previous commit
+------------------------------------------------------------------------
+r1901434 | sidney | 2022-05-31 03:10:28 +0000 (Tue, 31 May 2022) | 1 line
  
+ bug 7986 - Fix by using File::Temp::tempdir() for socketpath in tests
 ------------------------------------------------------------------------
-r1865015 | hege | 2019-08-13 08:31:18 +0000 (Tue, 13 Aug 2019) | 2 lines
+r1901426 | sidney | 2022-05-30 22:49:16 +0000 (Mon, 30 May 2022) | 1 line
  
- Skip invalid cid: "emails" in schemeless parser
+ 4.0.0-pre2 released
+------------------------------------------------------------------------
+r1901424 | sidney | 2022-05-30 21:27:07 +0000 (Mon, 30 May 2022) | 1 line
  
+ preparing to release 4.0.0-pr2
 ------------------------------------------------------------------------
-r1864941 | hege | 2019-08-12 07:30:28 +0000 (Mon, 12 Aug 2019) | 2 lines
+r1901421 | gbechis | 2022-05-30 16:15:13 +0000 (Mon, 30 May 2022) | 2 
+lines
  
- Fix duplicate supressor logic to escape duplicated message properly
+ spam from freshdesk.com domain has been reported
  
 ------------------------------------------------------------------------
-r1864890 | hege | 2019-08-10 16:45:56 +0000 (Sat, 10 Aug 2019) | 2 lines
+r1901419 | hege | 2022-05-30 14:12:23 +0000 (Mon, 30 May 2022) | 4 lines
  
- Let URIDNSBL set URIDOMAINS/URIHOSTS tag even if empty
+ - hashbl_email_domain_alias
+ - warn of undefined acl
+ - lc base32 for better cosmetics
  
 ------------------------------------------------------------------------
-r1864886 | hege | 2019-08-10 16:08:10 +0000 (Sat, 10 Aug 2019) | 2 lines
+r1901416 | hege | 2022-05-30 12:49:39 +0000 (Mon, 30 May 2022) | 2 lines
  
- Fail more gracefully if missing Net::CIDR::Lite
+ Bug 6995 - specify user to fall back for spamd instead of nobody
  
 ------------------------------------------------------------------------
-r1864880 | hege | 2019-08-10 15:48:37 +0000 (Sat, 10 Aug 2019) | 2 lines
+r1901405 | hege | 2022-05-30 09:21:09 +0000 (Mon, 30 May 2022) | 2 lines
  
- Don't load OLEMacro, floods unnecessary warnings if Archive::Zip not 
-installed..
+ Document "return undef" for eval-functions
  
 ------------------------------------------------------------------------
-r1864877 | hege | 2019-08-10 15:20:39 +0000 (Sat, 10 Aug 2019) | 2 lines
+r1901403 | hege | 2022-05-30 08:57:52 +0000 (Mon, 30 May 2022) | 2 lines
  
- Bug 7729 - body rules to match body only, not including the Subject (new 
-tflag nosubject)
+ Fix eval functions returning unintended "undef"
  
 ------------------------------------------------------------------------
-r1864875 | hege | 2019-08-10 13:22:28 +0000 (Sat, 10 Aug 2019) | 2 lines
- Improve logic in tflags multiple
+r1901399 | sidney | 2022-05-30 07:42:02 +0000 (Mon, 30 May 2022) | 1 line
  
+ bug 7998 Add two files to make clean that were dropped from distribution 
+some time ago
 ------------------------------------------------------------------------
-r1864870 | hege | 2019-08-10 10:54:28 +0000 (Sat, 10 Aug 2019) | 2 lines
+r1901397 | hege | 2022-05-30 05:58:31 +0000 (Mon, 30 May 2022) | 2 lines
  
- Use fixed string for Message::get_pristine(), save lots of memory
+ Minor cleaning up, ignore disabled metas (score 0), make unrun meta 
+reporting foolproof
  
 ------------------------------------------------------------------------
-r1864819 | hege | 2019-08-09 15:43:02 +0000 (Fri, 09 Aug 2019) | 2 lines
+r1901378 | sidney | 2022-05-29 04:53:44 +0000 (Sun, 29 May 2022) | 1 line
  
- Fix some tests, test more non-default modules too
+ fix irrelevant spf warning in test case
+------------------------------------------------------------------------
+r1901358 | sidney | 2022-05-28 15:06:49 +0000 (Sat, 28 May 2022) | 1 line
  
+ bug 7997 move non-rule settings from 01_test_rules.cf to 
+01_test_rules.pre
 ------------------------------------------------------------------------
-r1864805 | hege | 2019-08-09 13:57:25 +0000 (Fri, 09 Aug 2019) | 2 lines
+r1901350 | hege | 2022-05-28 11:45:22 +0000 (Sat, 28 May 2022) | 2 lines
  
- More Bug 7740 fixes
+ Revert skipping last priority do_meta_tests, fixes some issues, but 
+metas still need a bit more tweaking
  
 ------------------------------------------------------------------------
-r1864760 | hege | 2019-08-09 05:55:28 +0000 (Fri, 09 Aug 2019) | 2 lines
+r1901349 | hege | 2022-05-28 11:43:13 +0000 (Sat, 28 May 2022) | 2 lines
  
- Fix phishing test
+ Make some tests run with and without extra rules to catch bugs
  
 ------------------------------------------------------------------------
-r1864730 | hege | 2019-08-08 19:34:39 +0000 (Thu, 08 Aug 2019) | 2 lines
+r1901348 | hege | 2022-05-28 11:36:17 +0000 (Sat, 28 May 2022) | 2 lines
  
- Fix html tests from bug 7743 changes
+ Don't clear any tstprefs() or tstlocalrules() settings with 
+clear_localrules()
  
 ------------------------------------------------------------------------
-r1864713 | hege | 2019-08-08 15:14:13 +0000 (Thu, 08 Aug 2019) | 2 lines
+r1901347 | hege | 2022-05-28 11:00:42 +0000 (Sat, 28 May 2022) | 2 lines
  
- Update comments too..
+ Fix Unescaped left brace for %{FOO} templates (Bug 7992)
  
 ------------------------------------------------------------------------
-r1864712 | hege | 2019-08-08 15:12:20 +0000 (Thu, 08 Aug 2019) | 2 lines
+r1901346 | hege | 2022-05-28 10:38:25 +0000 (Sat, 28 May 2022) | 16 lines
  
- Bug 7743 - Remove legacy HTML parsing
+ Test cleanups and fixes.
  
-------------------------------------------------------------------------
-r1864686 | hege | 2019-08-08 08:11:36 +0000 (Thu, 08 Aug 2019) | 2 lines
+ Note that %patterns has now two exact patterns styles:
  
- Bug 7670 - Documentation about rawbody rules should be changed
+ - Literal strings match exactly the string.  Whitespace is no longer 
+ignored
+   (any leading and trailing whitelist must match), but consecutive
+   whitespace is normalized:
  
-------------------------------------------------------------------------
-r1864685 | hege | 2019-08-08 07:28:25 +0000 (Thu, 08 Aug 2019) | 2 lines
+   q{ FOO } => ''
+   ' FOO ' => ''
  
- TMPDIR fix from trunk
+ - Regular expressions, defined with standard qr// operator:
  
-------------------------------------------------------------------------
-r1864621 | hege | 2019-08-07 13:24:20 +0000 (Wed, 07 Aug 2019) | 2 lines
+   qr/ FOO / => ''
  
- Cleanup body_part_scan_size, split_into_array_of_short_paragraphs, chunk 
-size handling. Rawbody splitting did not even work properly previously, 
-sometimes outputting huge parts. Added new t/body_str.t test for splits.
  
 ------------------------------------------------------------------------
-r1864595 | hege | 2019-08-07 06:03:44 +0000 (Wed, 07 Aug 2019) | 2 lines
+r1901345 | hege | 2022-05-28 10:25:23 +0000 (Sat, 28 May 2022) | 2 lines
  
- Optimize split_into_array_of_short_paragraphs
+ Remove redundant if
  
 ------------------------------------------------------------------------
-r1864510 | hege | 2019-08-06 11:24:29 +0000 (Tue, 06 Aug 2019) | 2 lines
+r1901344 | hege | 2022-05-28 10:24:55 +0000 (Sat, 28 May 2022) | 2 lines
  
- Sigh, final fix, finish_parsing_end does not have $pms..
+ Fix tflags multiple handling for full rules
  
 ------------------------------------------------------------------------
-r1864489 | hege | 2019-08-06 10:09:01 +0000 (Tue, 06 Aug 2019) | 2 lines
- Fix some dns availability checks
+r1901318 | sidney | 2022-05-27 09:59:23 +0000 (Fri, 27 May 2022) | 1 line
  
+ Bug 7989 Remove three more references in tests to deleted plugin Esp.pm
 ------------------------------------------------------------------------
-r1864461 | hege | 2019-08-06 06:44:42 +0000 (Tue, 06 Aug 2019) | 2 lines
+r1901311 | hege | 2022-05-27 06:06:52 +0000 (Fri, 27 May 2022) | 2 lines
  
- Sync FreeMail from trunk
+ Enable HashBL plugin by default per devlist discussion
  
 ------------------------------------------------------------------------
-r1864424 | hege | 2019-08-05 09:26:17 +0000 (Mon, 05 Aug 2019) | 2 lines
+r1901297 | gbechis | 2022-05-26 17:14:35 +0000 (Thu, 26 May 2022) | 2 
+lines
  
- Add some unicode dot normalizations to uri_list_canonicalize
+ fix cache where CamelCase configuration options are used
  
 ------------------------------------------------------------------------
-r1864418 | hege | 2019-08-05 08:28:40 +0000 (Mon, 05 Aug 2019) | 2 lines
+r1901270 | hege | 2022-05-26 06:27:34 +0000 (Thu, 26 May 2022) | 2 lines
  
- Set User-Agent for wget/curl/fetch
+ user/host/domain options for check_hashbl_emails() and some cleaning up
  
 ------------------------------------------------------------------------
-r1864417 | hege | 2019-08-05 07:37:08 +0000 (Mon, 05 Aug 2019) | 2 lines
+r1901268 | hege | 2022-05-26 05:24:05 +0000 (Thu, 26 May 2022) | 2 lines
  
- Rollback Bug 6802, was buggy and needs some more throught
+ Use uridnsbl_skip_domains for HashBL lookups
  
 ------------------------------------------------------------------------
-r1864416 | hege | 2019-08-05 06:47:21 +0000 (Mon, 05 Aug 2019) | 2 lines
+r1901255 | hege | 2022-05-25 19:25:54 +0000 (Wed, 25 May 2022) | 2 lines
  
- 5% overall speedup from Check.pm regex //o, add IS_RULENAME constant
+ Why is stuff like USER_IN_DKIM_WHITELIST in sandbox 10_force_active.cf? 
+Add WELCOME/BLOCK alternatives. Should clean all of non-sandbox rules 
+away if it's not necessary.
  
 ------------------------------------------------------------------------
-r1864377 | hege | 2019-08-04 11:43:10 +0000 (Sun, 04 Aug 2019) | 2 lines
+r1901254 | hege | 2022-05-25 19:22:50 +0000 (Wed, 25 May 2022) | 2 lines
  
- Better logging of charset decoding warnings, Bug 7520 related
+ USER_IN_SPF_WELCOMELIST and USER_IN_DKIM_WELCOMELIST ended up in 
+72_scores.cf as 0.001? Try to fix?
  
 ------------------------------------------------------------------------
-r1864341 | hege | 2019-08-03 15:08:46 +0000 (Sat, 03 Aug 2019) | 2 lines
+r1901249 | hege | 2022-05-25 15:48:43 +0000 (Wed, 25 May 2022) | 2 lines
  
- Bug 7039 - sa-compile notes inability to write in home dir even though 
-it successfully uses a /tmp dir
+ Make DMARC rules async to properly wait for SPF and DKIM results
  
 ------------------------------------------------------------------------
-r1864340 | hege | 2019-08-03 14:40:38 +0000 (Sat, 03 Aug 2019) | 2 lines
+r1901241 | hege | 2022-05-25 13:46:02 +0000 (Wed, 25 May 2022) | 4 lines
  
- Fix _URIDOMAINS_ duplicates (Bug 6966)
+ Unify __URL_SHORTENER usage:
+ - Replace sandbox __URL_SHORTENER with rules/25_url_shortener.cf
+ - Migrate __PDS_URISHORTENER list into __URL_SHORTENER
  
 ------------------------------------------------------------------------
-r1864337 | hege | 2019-08-03 14:10:07 +0000 (Sat, 03 Aug 2019) | 2 lines
+r1901240 | hege | 2022-05-25 13:36:17 +0000 (Wed, 25 May 2022) | 2 lines
  
- Remove hashbl sha256 support, since DNS can't hand 64 character label, 
-duh..
+ Allow "max_short_urls 0" to disable all HTTP requests, enabling usage of 
+short_url() as a list lookup only.
  
 ------------------------------------------------------------------------
-r1864336 | hege | 2019-08-03 13:55:00 +0000 (Sat, 03 Aug 2019) | 12 lines
+r1901228 | hege | 2022-05-25 09:57:47 +0000 (Wed, 25 May 2022) | 2 lines
  
- 3.4 & trunk:
- - new Util::is_fqdn_valid() function to validate hostname (DNS name) 
-format (Bug 7736).  To check if a name contains valid TLD, it's still 
-needed to additionally use RegistryBoundaries::is_domain_valid().
- - uri_list_canonicalize fixes: fragments, logins, ports (strip :80 
-:443), firefox like canon http://foobar -> http://www.foobar.com (Bug 
-6596)
- - reduce DNS errors from warn to info
- trunk only:
- - new $pms->add_uri_detail_list function
- - improve get_uri_detail_list, documentation
- - new uri_detail_list types: unlinked, schemeless
- - split_domain, trim_domain, is_domain_valid: new $is_ascii arg skips 
-idn_to_ascii() conversion to save redundant calls
- - improve get() :host :domain
+ Make sure checks are done in case of strange rule priorities vs 
+check_dnsbl
  
 ------------------------------------------------------------------------
-r1864328 | hege | 2019-08-03 12:17:37 +0000 (Sat, 03 Aug 2019) | 2 lines
+r1901227 | hege | 2022-05-25 09:46:02 +0000 (Wed, 25 May 2022) | 3 lines
  
- Fix dkim test
+ - Add short_url_redir() function to check if a valid redirection was 
+found
+ - short_url() will result in hit as long as url_shortener matching URL 
+was found, no HTTP request required (fixes local tests only or missing 
+LWP module)
  
 ------------------------------------------------------------------------
-r1864157 | hege | 2019-08-01 14:54:18 +0000 (Thu, 01 Aug 2019) | 2 lines
+r1901166 | hege | 2022-05-23 12:55:35 +0000 (Mon, 23 May 2022) | 2 lines
  
- Some uri parser enhancements/fixes
+ Optimize short url parsing
  
 ------------------------------------------------------------------------
-r1864152 | hege | 2019-08-01 13:01:43 +0000 (Thu, 01 Aug 2019) | 2 lines
+r1901164 | hege | 2022-05-23 12:31:55 +0000 (Mon, 23 May 2022) | 2 lines
  
- Don't croak on empty selector
+ Improve documentation
  
 ------------------------------------------------------------------------
-r1864149 | hege | 2019-08-01 12:28:38 +0000 (Thu, 01 Aug 2019) | 2 lines
+r1901157 | hege | 2022-05-23 09:20:21 +0000 (Mon, 23 May 2022) | 2 lines
  
- Bug 5971 - M:SA:Conf::get_rule_value('rbl_evals') tries to coerce array 
-to hash
+ Add current tinyurl block example. Remove deprecated go.to.
  
 ------------------------------------------------------------------------
-r1864140 | hege | 2019-08-01 11:15:10 +0000 (Thu, 01 Aug 2019) | 2 lines
+r1901155 | hege | 2022-05-23 09:09:08 +0000 (Mon, 23 May 2022) | 8 lines
  
- Recommend Redis for Bayes
+ DecodeShortURLs:
+ - Add url_shortener_get (GET requests)
+ - Add clear_url_shortener
+ - Add url_shortener_timeout
+ - Add max_short_url_redirections
+ - Detect and warn about legacy short_url_tests() usage
+ - Improve docs and tests
  
 ------------------------------------------------------------------------
-r1864132 | hege | 2019-08-01 08:33:48 +0000 (Thu, 01 Aug 2019) | 2 lines
+r1901154 | hege | 2022-05-23 08:32:12 +0000 (Mon, 23 May 2022) | 2 lines
  
- Bug 6030 - whitelist_bounce_relays documentation enhancement
+ Use $pms->get_uri_list() as do_uri_tests() argument, otherwise any 
+add_uri_detail_list additions are not available for uri rules.
  
 ------------------------------------------------------------------------
-r1864120 | gbechis | 2019-08-01 07:45:09 +0000 (Thu, 01 Aug 2019) | 2 
+r1901152 | gbechis | 2022-05-23 08:15:56 +0000 (Mon, 23 May 2022) | 2 
 lines
  
- fix sought body rules generation
+ publish btc rbl
  
 ------------------------------------------------------------------------
-r1864044 | hege | 2019-07-31 11:11:02 +0000 (Wed, 31 Jul 2019) | 2 lines
+r1901136 | hege | 2022-05-23 04:41:50 +0000 (Mon, 23 May 2022) | 2 lines
  
- Fix ignoring @@ in mailto
+ Update docs
  
 ------------------------------------------------------------------------
-r1864043 | hege | 2019-07-31 10:43:49 +0000 (Wed, 31 Jul 2019) | 2 lines
+r1901135 | hege | 2022-05-23 04:38:50 +0000 (Mon, 23 May 2022) | 2 lines
  
- uri_to_domain - ignore cid:, fix mailto: parameter handling
+ Add url_shortener_user_agent (default Chrome) so request is not blocked 
+by some services
  
 ------------------------------------------------------------------------
-r1864032 | hege | 2019-07-31 05:04:11 +0000 (Wed, 31 Jul 2019) | 2 lines
+r1901118 | hege | 2022-05-22 09:21:31 +0000 (Sun, 22 May 2022) | 2 lines
  
- Bug 6233 - What values are valid/recommended for SYMBOLIC_TEST_NAME?
+ Improve tests
  
 ------------------------------------------------------------------------
-r1864015 | hege | 2019-07-30 17:46:25 +0000 (Tue, 30 Jul 2019) | 2 lines
+r1901117 | hege | 2022-05-22 09:21:22 +0000 (Sun, 22 May 2022) | 2 lines
  
- Bug 5619 - auto-generated spamassassin(1) man page repetition
+ Add some debug logging for named captures
  
 ------------------------------------------------------------------------
-r1864014 | hege | 2019-07-30 17:15:34 +0000 (Tue, 30 Jul 2019) | 2 lines
+r1901116 | hege | 2022-05-22 09:20:58 +0000 (Sun, 22 May 2022) | 2 lines
  
- Bug 7383 - auto_whitelist_path from config not used
+ Forgot to escape capture name in regex
  
 ------------------------------------------------------------------------
-r1863985 | hege | 2019-07-30 10:10:16 +0000 (Tue, 30 Jul 2019) | 2 lines
+r1901115 | hege | 2022-05-22 09:13:08 +0000 (Sun, 22 May 2022) | 2 lines
  
- Fix timers when running spamassassin against a folder of files
+ Fix renamed hash check
  
 ------------------------------------------------------------------------
-r1863981 | hege | 2019-07-30 07:50:22 +0000 (Tue, 30 Jul 2019) | 2 lines
+r1901114 | hege | 2022-05-22 08:44:07 +0000 (Sun, 22 May 2022) | 6 lines
  
- Bug 5620 - missing item and raw HTML on man pages
+ Bug 7992 - Capturing and reusing strings for matching across rules
+ - Now uses %{TAGNAME} template format for regex matching
+ - If any regex rule depends on undefined tag, consider the rule unrun
+ - Allow tag names to contain underscores
+ - Add documentation
  
 ------------------------------------------------------------------------
-r1863980 | hege | 2019-07-30 07:28:04 +0000 (Tue, 30 Jul 2019) | 2 lines
+r1901112 | hege | 2022-05-22 08:39:51 +0000 (Sun, 22 May 2022) | 2 lines
  
- Update TextCat documentation a bit
+ Clear out some ancient Perl 5.6 checks
  
 ------------------------------------------------------------------------
-r1863788 | hege | 2019-07-26 09:20:57 +0000 (Fri, 26 Jul 2019) | 2 lines
+r1901096 | hege | 2022-05-21 08:51:57 +0000 (Sat, 21 May 2022) | 3 lines
  
- Bug 6802 - force regex ascii semantics
+ - Named capture cleanups, add tests, new PMS/set_captures, 
+Parser/parse_captures functions (Bug 7992)
+ - MIMEHeader: support named regex captures, add tflags multiple support, 
+improve tests
  
 ------------------------------------------------------------------------
-r1863776 | hege | 2019-07-26 07:27:39 +0000 (Fri, 26 Jul 2019) | 2 lines
+r1901093 | hege | 2022-05-21 06:21:56 +0000 (Sat, 21 May 2022) | 5 lines
  
- Bug 7741 - Support City database now properly
+ Bug 7992 - Capturing and reusing strings for matching across rules
+ - Check %- right after regex matching, to prevent got_hit or anything 
+else potentially messing with it in the future
+ - Save all matches on tflags multiple rules
+ - Remove duplicate values from matches/tags
  
 ------------------------------------------------------------------------
-r1863742 | hege | 2019-07-25 15:56:36 +0000 (Thu, 25 Jul 2019) | 2 lines
+r1901085 | gbechis | 2022-05-20 13:52:25 +0000 (Fri, 20 May 2022) | 3 
+lines
  
- Revert Bug 7741
+ better limit on regexp, it cannot work with longer strings because of dns
+ labels limits.
  
 ------------------------------------------------------------------------
-r1863531 | hege | 2019-07-21 17:12:07 +0000 (Sun, 21 Jul 2019) | 2 lines
+r1901082 | hege | 2022-05-20 08:52:33 +0000 (Fri, 20 May 2022) | 2 lines
  
- Check for GeoIP2 City.mmdb also
+ Bug 7994 - Plugin ASN.pm, AskDNS.pm: return early if $pkt is undefined
  
 ------------------------------------------------------------------------
-r1863527 | hege | 2019-07-21 15:49:38 +0000 (Sun, 21 Jul 2019) | 2 lines
+r1901080 | hege | 2022-05-20 07:59:04 +0000 (Fri, 20 May 2022) | 2 lines
  
- Simplify settings tags a bit
+ Add missing header rule logging
  
 ------------------------------------------------------------------------
-r1863526 | hege | 2019-07-21 15:08:35 +0000 (Sun, 21 Jul 2019) | 2 lines
+r1901068 | hege | 2022-05-19 15:48:50 +0000 (Thu, 19 May 2022) | 2 lines
  
- Bug 7741 - Invalid database type 0 error when enabling URILocalBL
+ Better validation for rulenames
  
 ------------------------------------------------------------------------
-r1863525 | hege | 2019-07-21 13:53:39 +0000 (Sun, 21 Jul 2019) | 2 lines
+r1901067 | hege | 2022-05-19 15:43:41 +0000 (Thu, 19 May 2022) | 2 lines
  
- Missed on regex fix, also clarify documentation about case-insensitivity
+ Automatically adjust priority -100 for tflags net rules
  
 ------------------------------------------------------------------------
-r1863524 | hege | 2019-07-21 13:48:27 +0000 (Sun, 21 Jul 2019) | 2 lines
+r1901063 | hege | 2022-05-19 13:23:35 +0000 (Thu, 19 May 2022) | 2 lines
  
- Bug 7740 - Cannot set OLEMacro regex options, and other small regex 
-cleanups
+ Add tflags net
  
 ------------------------------------------------------------------------
-r1862889 | hege | 2019-07-10 17:10:34 +0000 (Wed, 10 Jul 2019) | 2 lines
+r1901060 | hege | 2022-05-19 09:47:40 +0000 (Thu, 19 May 2022) | 5 lines
  
- HTML_FONT_FACE_BAD fixes from Bug 5956, 7312
+ Some meta cleanups and optimizations (Bug 7987)
+ - Use rule_ready() everywhere instead of direct tests_already_hit modify
+ - Simple tracking of meta dependency hits, run do_meta_tests only when 
+needed
+ - Do not run do_meta_tests on last priority, as finish_meta_tests will 
+run anyway
  
 ------------------------------------------------------------------------
-r1862748 | hege | 2019-07-08 13:32:37 +0000 (Mon, 08 Jul 2019) | 2 lines
+r1901042 | gbechis | 2022-05-18 17:59:54 +0000 (Wed, 18 May 2022) | 2 
+lines
  
- Add Bug 7725 fix to AskDNS too
+ silence a warning
  
 ------------------------------------------------------------------------
-r1862718 | hege | 2019-07-08 07:30:39 +0000 (Mon, 08 Jul 2019) | 2 lines
+r1901033 | hege | 2022-05-18 12:40:40 +0000 (Wed, 18 May 2022) | 2 lines
  
- Add some has_* features just in case
+ HashBL: add check_hashbl_attachments. Improve documentation.
  
 ------------------------------------------------------------------------
-r1862690 | hege | 2019-07-07 11:25:00 +0000 (Sun, 07 Jul 2019) | 2 lines
+r1900984 | hege | 2022-05-17 07:52:27 +0000 (Tue, 17 May 2022) | 2 lines
  
- Add HashBL changes
+ Revert get_async_pending_rules from do_meta_tests one more time. It's 
+really not needed, as rule_ready() in run_eval_tests is enough.
  
 ------------------------------------------------------------------------
-r1862689 | hege | 2019-07-07 11:12:36 +0000 (Sun, 07 Jul 2019) | 2 lines
+r1900983 | hege | 2022-05-17 07:48:20 +0000 (Tue, 17 May 2022) | 2 lines
  
- Clarify documentation
+ Remove outdated comment
  
 ------------------------------------------------------------------------
-r1862686 | hege | 2019-07-07 10:53:50 +0000 (Sun, 07 Jul 2019) | 2 lines
+r1900981 | hege | 2022-05-17 06:03:11 +0000 (Tue, 17 May 2022) | 2 lines
  
- Add missing register_async_rule_finish
+ Add HashBL things
  
 ------------------------------------------------------------------------
-r1862685 | hege | 2019-07-07 10:50:05 +0000 (Sun, 07 Jul 2019) | 2 lines
+r1900979 | hege | 2022-05-17 05:58:09 +0000 (Tue, 17 May 2022) | 2 lines
  
- Sync with trunk version (check_hashbl_uris, hashbl_ignore), use 
-compile_regexp, fix max=x truncating, logging cleanup
+ Add local($1) just in case
  
 ------------------------------------------------------------------------
-r1862683 | hege | 2019-07-07 09:44:35 +0000 (Sun, 07 Jul 2019) | 2 lines
+r1900978 | hege | 2022-05-17 05:48:11 +0000 (Tue, 17 May 2022) | 2 lines
  
- Few more parameter whitespace fixes
+ Forgot has_hashbl_sha256
  
 ------------------------------------------------------------------------
-r1862682 | hege | 2019-07-07 09:34:49 +0000 (Sun, 07 Jul 2019) | 2 lines
+r1900977 | hege | 2022-05-17 05:43:57 +0000 (Tue, 17 May 2022) | 2 lines
  
- Few more parameter whitespace fixes
+ Add sha256 option to HashBL (Bug 7993)
  
 ------------------------------------------------------------------------
-r1862681 | hege | 2019-07-07 09:31:49 +0000 (Sun, 07 Jul 2019) | 2 lines
+r1900976 | hege | 2022-05-17 05:40:33 +0000 (Tue, 17 May 2022) | 2 lines
  
- Tighten up addrlist parameter checks
+ Add very simple Util/base32_encode function for HashBL
  
 ------------------------------------------------------------------------
-r1862678 | hege | 2019-07-07 08:13:38 +0000 (Sun, 07 Jul 2019) | 2 lines
+r1900974 | hege | 2022-05-17 04:02:38 +0000 (Tue, 17 May 2022) | 5 lines
  
- Fix regex case sensitive
+ Bug 7987 - DNSEval.pm,HashBL.pm,URILocalBL.pm: unnecessary use of 
+rule_pending and rule_ready
+ For backwards compatibility:
+ - Use rule_ready() in run_eval_tests to allow async even for "return 0"
+ - Bring back async pending check in do_meta_tests
  
 ------------------------------------------------------------------------
-r1862625 | gbechis | 2019-07-05 17:40:10 +0000 (Fri, 05 Jul 2019) | 2 
-lines
+r1900961 | hege | 2022-05-16 15:51:19 +0000 (Mon, 16 May 2022) | 4 lines
  
- sync dependencies check with reality
+ Bug 7987 - DNSEval.pm,HashBL.pm,URILocalBL.pm: unnecessary use of 
+rule_pending and rule_ready
+ - Remove $pms->rule_pending(), $pms->{tests_pending} to make things much 
+simpler
+ - Async eval-functions must now "return undef"
  
 ------------------------------------------------------------------------
-r1862624 | gbechis | 2019-07-05 17:26:47 +0000 (Fri, 05 Jul 2019) | 3 
+r1900942 | gbechis | 2022-05-16 07:46:47 +0000 (Mon, 16 May 2022) | 2 
 lines
  
- Add OLEMacro plugin to 3.4.3 and rename rules/v*.pre
- accordingly
+ Remove Esp plugin
  
 ------------------------------------------------------------------------
-r1862622 | hege | 2019-07-05 16:33:55 +0000 (Fri, 05 Jul 2019) | 2 lines
+r1900932 | hege | 2022-05-15 17:42:47 +0000 (Sun, 15 May 2022) | 2 lines
  
- Small X-Relay-Countries-Auth documentation add
+ Add missing t/data/spam/hashbl
  
 ------------------------------------------------------------------------
-r1862620 | hege | 2019-07-05 15:08:59 +0000 (Fri, 05 Jul 2019) | 2 lines
+r1900929 | hege | 2022-05-15 16:07:26 +0000 (Sun, 15 May 2022) | 4 lines
  
- More Bug 7731 tweaks, rename MUA to X-Relay-Countries-Auth
+ - Add options to check_hashbl_tag, ip/ipv4/ipv6/revip/fqdn/tld/trim
+ - Cleanup HashBL code
+ - Add basic HashBL tests
  
 ------------------------------------------------------------------------
-r1862608 | hege | 2019-07-05 12:07:13 +0000 (Fri, 05 Jul 2019) | 2 lines
+r1900928 | hege | 2022-05-15 15:31:51 +0000 (Sun, 15 May 2022) | 2 lines
  
- Fix handling when geoip not loaded
+ Add $current_checkfile variable to get current log output file
  
 ------------------------------------------------------------------------
-r1862607 | hege | 2019-07-05 12:00:21 +0000 (Fri, 05 Jul 2019) | 2 lines
+r1900927 | hege | 2022-05-15 13:29:53 +0000 (Sun, 15 May 2022) | 2 lines
  
- Bug 7731 - Add external and msa metadata to RelayCountry
+ Skip empty regex captures
  
 ------------------------------------------------------------------------
-r1862111 | hege | 2019-06-26 08:49:22 +0000 (Wed, 26 Jun 2019) | 2 lines
+r1900917 | hege | 2022-05-15 09:05:12 +0000 (Sun, 15 May 2022) | 2 lines
  
- Bug 5639 - document multiple header matching better
+ Add check_hashbl_tag eval
  
 ------------------------------------------------------------------------
-r1862107 | hege | 2019-06-26 08:05:59 +0000 (Wed, 26 Jun 2019) | 2 lines
+r1900911 | hege | 2022-05-15 05:31:19 +0000 (Sun, 15 May 2022) | 2 lines
  
- Remove use bytes from mass-check (Bug 7613)
+ Do not check if captured_rules exists, as all values are now potentially 
+used as tags
  
 ------------------------------------------------------------------------
-r1862103 | hege | 2019-06-26 06:53:33 +0000 (Wed, 26 Jun 2019) | 2 lines
+r1900910 | hege | 2022-05-15 05:12:44 +0000 (Sun, 15 May 2022) | 3 lines
  
- Fix previous commit logic..
+ Bug 7992 - Capturing and reusing strings for matching across rules
+ - Set captured value(s) as a tag
  
 ------------------------------------------------------------------------
-r1862102 | hege | 2019-06-26 06:49:51 +0000 (Wed, 26 Jun 2019) | 2 lines
+r1900880 | hege | 2022-05-14 12:38:56 +0000 (Sat, 14 May 2022) | 2 lines
  
- Handle SHA signatures a bit more carefully
+ No regex capture for header exists: test
  
 ------------------------------------------------------------------------
-r1862101 | gbechis | 2019-06-26 06:27:31 +0000 (Wed, 26 Jun 2019) | 3 
-lines
+r1900879 | hege | 2022-05-14 12:18:41 +0000 (Sat, 14 May 2022) | 2 lines
  
- skip regression test if sudo(8) is not installed
- fix bz #6665
+ Bug 7992 - Capturing and reusing strings for matching across rules
  
 ------------------------------------------------------------------------
-r1862057 | hege | 2019-06-25 12:51:45 +0000 (Tue, 25 Jun 2019) | 2 lines
+r1900876 | gbechis | 2022-05-14 09:36:03 +0000 (Sat, 14 May 2022) | 2 
+lines
  
- Also parse image/jpg (commonly used even if not standard)
+ fix Esp regression tests, X-MC-User is a 25 chars string
  
 ------------------------------------------------------------------------
-r1862009 | hege | 2019-06-24 14:46:44 +0000 (Mon, 24 Jun 2019) | 2 lines
+r1900873 | hege | 2022-05-14 06:58:57 +0000 (Sat, 14 May 2022) | 2 lines
  
- Bug 6582: Implement body_part_scan_size / rawbody_part_scan_size limits
+ Small code cleanup
  
 ------------------------------------------------------------------------
-r1861977 | hege | 2019-06-24 06:32:24 +0000 (Mon, 24 Jun 2019) | 2 lines
+r1900871 | hege | 2022-05-14 06:30:45 +0000 (Sat, 14 May 2022) | 2 lines
  
- Fix 20_saw_ampersand.t
+ Do not leak options when redefining a header test. Add some actual basic 
+header tests.
  
 ------------------------------------------------------------------------
-r1861976 | hege | 2019-06-24 06:24:57 +0000 (Mon, 24 Jun 2019) | 2 lines
- Fix 20_saw_ampersand.t
+r1900857 | gbechis | 2022-05-13 13:27:05 +0000 (Fri, 13 May 2022) | 4 
+lines
  
-------------------------------------------------------------------------
-r1861961 | kmcgrail | 2019-06-24 00:34:10 +0000 (Mon, 24 Jun 2019) | 1 
-line
+ Official ASF channel should be loaded first in
+ order to be able to override scores by using custom channels
+ bz #7991
  
- preparing to release 3.4.3-rc3
 ------------------------------------------------------------------------
-r1861944 | hege | 2019-06-23 18:24:45 +0000 (Sun, 23 Jun 2019) | 2 lines
+r1900849 | hege | 2022-05-13 06:06:33 +0000 (Fri, 13 May 2022) | 8 lines
  
- Update skipped files
+ - Bug 7987
+ - fix body rules considered unrun when using sa-compile
+ - fix check_rbl_sub rules considered unrun and other DNSEval cleanups
+ - improve rule_pending/rule_ready/got_hit() logic
+ - rename $pms->get_pending_lookups to get_async_pending_rules
+ - other minor async cleanups
+ - test and documentation improvements
  
 ------------------------------------------------------------------------
-r1861942 | hege | 2019-06-23 16:20:48 +0000 (Sun, 23 Jun 2019) | 2 lines
+r1900839 | gbechis | 2022-05-12 14:25:12 +0000 (Thu, 12 May 2022) | 2 
+lines
  
- Remove unneeded t/mkrules*.t from 3.4
+ set DMARC_PASS and DMARC_MISSING rules as immutable
  
 ------------------------------------------------------------------------
-r1861937 | hege | 2019-06-23 14:37:54 +0000 (Sun, 23 Jun 2019) | 2 lines
+r1900834 | hege | 2022-05-12 11:34:54 +0000 (Thu, 12 May 2022) | 2 lines
  
- Some taint fixes
+ Limit fixing net rule priorities to -100
  
 ------------------------------------------------------------------------
-r1861932 | hege | 2019-06-23 13:51:31 +0000 (Sun, 23 Jun 2019) | 2 lines
+r1900832 | hege | 2022-05-12 09:39:34 +0000 (Thu, 12 May 2022) | 2 lines
  
- Apparently make tardist doesn't always output "Created xyz.tar.gz", try 
-to find latest tarfile with ls -tr instead
+ Auto adjust priority to -100
  
 ------------------------------------------------------------------------
-r1861926 | hege | 2019-06-23 13:10:00 +0000 (Sun, 23 Jun 2019) | 2 lines
+r1900829 | hege | 2022-05-12 09:29:35 +0000 (Thu, 12 May 2022) | 2 lines
  
- Fix URILocalBL requiring Net::CIDR::Lite
+ Cleanup ASN, add support for tag name in check_asn()
  
 ------------------------------------------------------------------------
-r1861909 | hege | 2019-06-23 09:47:18 +0000 (Sun, 23 Jun 2019) | 2 lines
+r1900813 | hege | 2022-05-11 15:24:34 +0000 (Wed, 11 May 2022) | 2 lines
  
- Remove exponential sleeps, they don't make much sense, basically 
-check_mirror_af is the one that croaks if our network is down. There's 
-already bunch of retries also on external wget/curl commands. Just sleep 
-few seconds between tries, should be enough.
+ Prettier failure pattern logging
  
 ------------------------------------------------------------------------
-r1861908 | hege | 2019-06-23 09:26:37 +0000 (Sun, 23 Jun 2019) | 2 lines
+r1900812 | hege | 2022-05-11 15:12:25 +0000 (Wed, 11 May 2022) | 2 lines
  
- Few trivial ipv4/ipv6 fixes, handle forcing better
+ Don't override existing priority unless it's default 0
  
 ------------------------------------------------------------------------
-r1861891 | hege | 2019-06-22 18:00:35 +0000 (Sat, 22 Jun 2019) | 2 lines
+r1900811 | hege | 2022-05-11 14:59:25 +0000 (Wed, 11 May 2022) | 2 lines
  
- Skip left brace regexp tests which depend on Perl version
+ Small Shortcircuit cleanup. Mention network lookups at -100 priority.
  
 ------------------------------------------------------------------------
-r1861889 | hege | 2019-06-22 17:53:31 +0000 (Sat, 22 Jun 2019) | 2 lines
+r1900800 | jhardin | 2022-05-11 03:28:05 +0000 (Wed, 11 May 2022) | 1 line
  
- Trivial change, don't fail lint on description for non-existent rule 
-(similar to bug 5514)
+ Add rule for eval
+------------------------------------------------------------------------
+r1900798 | sidney | 2022-05-11 01:56:18 +0000 (Wed, 11 May 2022) | 1 line
  
+ use prove for the rule tests too for a better release tester experience
 ------------------------------------------------------------------------
-r1861877 | hege | 2019-06-22 16:00:42 +0000 (Sat, 22 Jun 2019) | 2 lines
+r1900796 | sidney | 2022-05-11 00:28:26 +0000 (Wed, 11 May 2022) | 1 line
  
- Bug 7726 - Enable taint for all tests
+ update script that runs release tests for change in the perlcritic test
+------------------------------------------------------------------------
+r1900794 | sidney | 2022-05-10 23:23:31 +0000 (Tue, 10 May 2022) | 1 line
  
+ move percritic test code from xt directory which is not in MANIFEST
 ------------------------------------------------------------------------
-r1861762 | hege | 2019-06-21 08:35:21 +0000 (Fri, 21 Jun 2019) | 2 lines
+r1900793 | gbechis | 2022-05-10 23:11:43 +0000 (Tue, 10 May 2022) | 3 
+lines
  
- Bug 7725 - Perl taint bug with URIDNSBL netmask calculations
+ refactor some code
+ improvements on Mailup and Sendinblue matches
  
 ------------------------------------------------------------------------
-r1861758 | hege | 2019-06-21 08:23:02 +0000 (Fri, 21 Jun 2019) | 2 lines
+r1900789 | hege | 2022-05-10 16:55:26 +0000 (Tue, 10 May 2022) | 2 lines
  
- Some trivial fixes, always latest tardist file, reset sa-compile cache
+ Add t/perlcritic.t in MANIFEST
  
 ------------------------------------------------------------------------
-r1861744 | hege | 2019-06-21 06:26:00 +0000 (Fri, 21 Jun 2019) | 2 lines
+r1900788 | hege | 2022-05-10 16:53:03 +0000 (Tue, 10 May 2022) | 2 lines
  
- Fix t/all_modules.t
+ Add t/perlcritic.t per dev-list discussion
  
 ------------------------------------------------------------------------
-r1861634 | hege | 2019-06-19 15:43:30 +0000 (Wed, 19 Jun 2019) | 2 lines
+r1900771 | sidney | 2022-05-10 03:31:25 +0000 (Tue, 10 May 2022) | 1 line
  
- Bug 7723 - FromNameSpoof warnings with missing To-header
+ corrected fix to  perlcritic error
+------------------------------------------------------------------------
+r1900770 | sidney | 2022-05-10 03:22:40 +0000 (Tue, 10 May 2022) | 1 line
  
+ make a map non-destructive fixes perlcritic error and makes it not 
+destroy the list
 ------------------------------------------------------------------------
-r1861633 | hege | 2019-06-19 15:41:21 +0000 (Wed, 19 Jun 2019) | 2 lines
+r1900768 | sidney | 2022-05-10 02:19:01 +0000 (Tue, 10 May 2022) | 1 line
  
- Bug 7724 - MIMEEval state not checked properly
+ Updated build/release instructions - some content moved to wiki
+------------------------------------------------------------------------
+r1900764 | sidney | 2022-05-10 00:41:11 +0000 (Tue, 10 May 2022) | 1 line
  
+ Fix texcat languages filename not defined warning in t/reuse.t test
 ------------------------------------------------------------------------
-r1861513 | hege | 2019-06-17 14:28:24 +0000 (Mon, 17 Jun 2019) | 2 lines
+r1900741 | hege | 2022-05-09 12:52:20 +0000 (Mon, 09 May 2022) | 2 lines
  
- Add --reallyallowplugins in upgrade notes
+ Remove non-existing check_rbl_results_for eval
  
 ------------------------------------------------------------------------
-r1861431 | hege | 2019-06-15 19:34:46 +0000 (Sat, 15 Jun 2019) | 2 lines
+r1900740 | hege | 2022-05-09 12:51:18 +0000 (Mon, 09 May 2022) | 2 lines
  
- Tighten up --allowplugins allowed settings
+ Adjust priority of all eval rules..
  
 ------------------------------------------------------------------------
-r1861429 | hege | 2019-06-15 19:13:30 +0000 (Sat, 15 Jun 2019) | 2 lines
+r1900738 | hege | 2022-05-09 11:46:21 +0000 (Mon, 09 May 2022) | 2 lines
  
- Print warning about --allowplugins usage, only allow it with 
---reallyallowplugins
+ Automatically adjust check_rbl* rules to -100 for early async launch
  
 ------------------------------------------------------------------------
-r1861424 | hege | 2019-06-15 18:42:17 +0000 (Sat, 15 Jun 2019) | 2 lines
+r1900732 | gbechis | 2022-05-09 11:08:33 +0000 (Mon, 09 May 2022) | 2 
+lines
  
- Bug 6944 - t/dcc.t fails to check if dcc is installed or not before 
-testing
+ add "info" sub
  
 ------------------------------------------------------------------------
-r1861423 | hege | 2019-06-15 18:34:48 +0000 (Sat, 15 Jun 2019) | 2 lines
+r1900725 | hege | 2022-05-09 09:11:43 +0000 (Mon, 09 May 2022) | 2 lines
  
- Retry even if sha/asc download fails, sleep a bit between mirror retries
+ Improve logging
  
 ------------------------------------------------------------------------
-r1861404 | hege | 2019-06-15 15:29:54 +0000 (Sat, 15 Jun 2019) | 2 lines
+r1900723 | hege | 2022-05-09 08:33:22 +0000 (Mon, 09 May 2022) | 2 lines
  
- Skip downloading sha256 file needlessly if already having sha512
+ Use $hitsptr for consistency
  
 ------------------------------------------------------------------------
-r1861402 | hege | 2019-06-15 14:52:03 +0000 (Sat, 15 Jun 2019) | 2 lines
+r1900719 | hege | 2022-05-09 05:27:43 +0000 (Mon, 09 May 2022) | 2 lines
  
- Bug 7089 - add domains_only function to DNSEval.pm
+ Small code cleanup, improve logging. Ignore $ent->{key} as documented.
  
 ------------------------------------------------------------------------
-r1861377 | hege | 2019-06-15 12:01:00 +0000 (Sat, 15 Jun 2019) | 2 lines
+r1900688 | hege | 2022-05-08 12:17:21 +0000 (Sun, 08 May 2022) | 2 lines
  
- Bug 5258 - implement rules_matching() meta expression
+ Improve tests
  
 ------------------------------------------------------------------------
-r1861375 | hege | 2019-06-15 11:55:02 +0000 (Sat, 15 Jun 2019) | 2 lines
+r1900680 | hege | 2022-05-08 06:40:12 +0000 (Sun, 08 May 2022) | 2 lines
  
- Add t/add_modules.t
+ Improve rule_pending() documentation
  
 ------------------------------------------------------------------------
-r1861357 | hege | 2019-06-14 16:28:44 +0000 (Fri, 14 Jun 2019) | 2 lines
+r1900678 | hege | 2022-05-08 06:04:55 +0000 (Sun, 08 May 2022) | 2 lines
  
- Add Finnish VS: reply prefix
+ Remove redundant $would_log_rules_all check
  
 ------------------------------------------------------------------------
-r1861317 | gbechis | 2019-06-14 07:57:14 +0000 (Fri, 14 Jun 2019) | 3 
-lines
+r1900676 | sidney | 2022-05-08 05:40:03 +0000 (Sun, 08 May 2022) | 8 lines
+ bug 7988 Fixes and updates to regression tests
+ - All tests now use common initialization in SATest.pm
+ - Use absolute pathname in @INC to fix breakage caused by chdir
+ - Some wording changes in test warnings
+ - Revamp xt tests to use one shell script that calls t/*.t and another 3 
+test scripts
+ - Fix problems in saw-ampersand test and update for newer SpamAssassin 
+code
  
- Revert part of commit r1831073 that sneak in by fault
- fixes #7657, thanks to hege@ for debugging this
  
 ------------------------------------------------------------------------
-r1861265 | hege | 2019-06-13 15:03:40 +0000 (Thu, 13 Jun 2019) | 2 lines
+r1900675 | hege | 2022-05-08 05:15:50 +0000 (Sun, 08 May 2022) | 2 lines
  
- Bug 7374 - Some e-mails create "Complex regular subexpression recursion 
-limit (32766) exceeded" warning
+ Remove outdated/superfluous suggestion to run prove command, all tests 
+should be run the way general documentation suggests. (Note: "prove -T" 
+should always be used, if used..)
  
 ------------------------------------------------------------------------
-r1861259 | hege | 2019-06-13 13:57:59 +0000 (Thu, 13 Jun 2019) | 2 lines
+r1900674 | kmcgrail | 2022-05-08 04:23:09 +0000 (Sun, 08 May 2022) | 1 
+line
  
- Bug 7681 - Use standard SEE ALSOs
+ BZ 7981 working on release UPGRADE and Announcement files in Google Docs
+------------------------------------------------------------------------
+r1900670 | sidney | 2022-05-08 00:24:17 +0000 (Sun, 08 May 2022) | 1 line
  
+ Add missing declaration and fix an undefined var error uncovered in 
+testing that it revealed
 ------------------------------------------------------------------------
-r1861237 | hege | 2019-06-13 08:05:08 +0000 (Thu, 13 Jun 2019) | 2 lines
+r1900667 | hege | 2022-05-07 20:34:59 +0000 (Sat, 07 May 2022) | 2 lines
  
- Fix harmless hash assignment warnings in relaycountry tests
+ Add a some more Bug 7735 comments/documentation
  
 ------------------------------------------------------------------------
-r1861236 | hege | 2019-06-13 07:59:37 +0000 (Thu, 13 Jun 2019) | 2 lines
+r1900666 | hege | 2022-05-07 20:27:28 +0000 (Sat, 07 May 2022) | 2 lines
  
- Fix harmless warning for test if Geo::IP not available
+ Fix SA breaking typo, sorry
  
 ------------------------------------------------------------------------
-r1861234 | hege | 2019-06-13 07:53:26 +0000 (Thu, 13 Jun 2019) | 2 lines
+r1900664 | hege | 2022-05-07 19:03:30 +0000 (Sat, 07 May 2022) | 2 lines
  
- Fix qr_to_string for Perl <5.14
+ Fix comment/documentation
  
 ------------------------------------------------------------------------
-r1861222 | hege | 2019-06-13 06:22:31 +0000 (Thu, 13 Jun 2019) | 2 lines
+r1900658 | hege | 2022-05-07 14:41:14 +0000 (Sat, 07 May 2022) | 2 lines
  
- Remove t/spamc_H.t from manifest since Bug 7046 is not fixed (and 
-probably wont for 3.4 branch)
+ Add few more tests
  
 ------------------------------------------------------------------------
-r1861221 | hege | 2019-06-13 06:19:19 +0000 (Thu, 13 Jun 2019) | 2 lines
+r1900653 | hege | 2022-05-07 14:00:49 +0000 (Sat, 07 May 2022) | 2 lines
  
- Fix possible t/dnsbl.t failure
+ Add some more tests. Seems NetAddr::IP has some bug handling stuff like 
+127.0.0.1/31 (I don't think it should match 127.0.0.0).
  
 ------------------------------------------------------------------------
-r1861220 | hege | 2019-06-13 06:10:14 +0000 (Thu, 13 Jun 2019) | 2 lines
+r1900651 | hege | 2022-05-07 13:16:03 +0000 (Sat, 07 May 2022) | 2 lines
  
- Define DKIM_INVALID for tests
+ Installing Net::CIDR::Lite allows to use dash separated IP range format 
+(e.g. 192.168.1.1-192.168.255.255) for NetSet tables (internal_networks, 
+trusted_networks, msa_networks, uri_local_cidr)
  
 ------------------------------------------------------------------------
-r1861219 | hege | 2019-06-13 06:09:44 +0000 (Thu, 13 Jun 2019) | 2 lines
+r1900648 | hege | 2022-05-07 09:21:33 +0000 (Sat, 07 May 2022) | 2 lines
  
- Add t/all_modules.t to manifest
+ No point mapping bayes_ignore_header constantly from array to lc hash, 
+just make it lc hash from the start. Also make it more standards 
+conforming, no point having differently named hash from the command.
  
 ------------------------------------------------------------------------
-r1861214 | kmcgrail | 2019-06-13 02:49:23 +0000 (Thu, 13 Jun 2019) | 1 
-line
+r1900646 | hege | 2022-05-07 08:13:29 +0000 (Sat, 07 May 2022) | 2 lines
+ More DKIM-Signature like headers to "mark presence only"
  
- Preparing to release 3.4.3
 ------------------------------------------------------------------------
-r1861181 | hege | 2019-06-12 18:33:01 +0000 (Wed, 12 Jun 2019) | 2 lines
+r1900642 | hege | 2022-05-07 06:01:02 +0000 (Sat, 07 May 2022) | 2 lines
  
- Fix 60_perlcritic.t warnings
+ Remove superfluous version check, it's not possible to be false
  
 ------------------------------------------------------------------------
-r1861142 | hege | 2019-06-12 15:06:34 +0000 (Wed, 12 Jun 2019) | 2 lines
+r1900630 | hege | 2022-05-06 15:03:13 +0000 (Fri, 06 May 2022) | 2 lines
  
- Fix makedist, no external rules required
+ Use primary key for MySQL bayes_expire to make it potentially Galera 
+compatible
  
 ------------------------------------------------------------------------
-r1861141 | hege | 2019-06-12 15:00:18 +0000 (Wed, 12 Jun 2019) | 2 lines
+r1900622 | gbechis | 2022-05-06 10:45:31 +0000 (Fri, 06 May 2022) | 2 
+lines
  
- Define rules internally so make disttest also works without external 
-rules
+ better match on X-Mailer
  
 ------------------------------------------------------------------------
-r1861131 | kmcgrail | 2019-06-12 13:51:47 +0000 (Wed, 12 Jun 2019) | 1 
-line
+r1900614 | hege | 2022-05-06 05:53:16 +0000 (Fri, 06 May 2022) | 2 lines
  
- Preparing to release 3.4.3
-------------------------------------------------------------------------
-r1860921 | kmcgrail | 2019-06-10 01:27:42 +0000 (Mon, 10 Jun 2019) | 1 
-line
+ Make if logic a little more straightforward
  
- updating razor2 spam test file
 ------------------------------------------------------------------------
-r1860903 | hege | 2019-06-09 13:13:59 +0000 (Sun, 09 Jun 2019) | 2 lines
+r1900613 | hege | 2022-05-06 05:40:14 +0000 (Fri, 06 May 2022) | 2 lines
  
- Bug 7037 - RelayCountry is leaking file descriptors
+ Act as soon as DKIMDOMAIN is ready
  
 ------------------------------------------------------------------------
-r1860896 | hege | 2019-06-09 11:42:11 +0000 (Sun, 09 Jun 2019) | 2 lines
+r1900607 | hege | 2022-05-06 04:14:21 +0000 (Fri, 06 May 2022) | 2 lines
  
- Bug 7689 - reduce lint time from quadratic to linear
+ Only mark rule_pending when needed
  
 ------------------------------------------------------------------------
-r1860891 | hege | 2019-06-09 10:16:29 +0000 (Sun, 09 Jun 2019) | 2 lines
+r1900599 | hege | 2022-05-05 17:58:25 +0000 (Thu, 05 May 2022) | 2 lines
  
- Bug 7658 - Pyzor error: Use of uninitialized value $response[0] in 
-pattern match (m//)
+ Ok fix properly. Apparently checkfile() is only for saving filenames 
+when error (Output can be examined in..). Fix the path.
  
 ------------------------------------------------------------------------
-r1860889 | hege | 2019-06-09 09:54:05 +0000 (Sun, 09 Jun 2019) | 2 lines
+r1900597 | hege | 2022-05-05 17:48:29 +0000 (Thu, 05 May 2022) | 2 lines
  
- New option --httputil to force used download utility
+ Fix spurious cannot open mkrules_else.0 warnings
  
 ------------------------------------------------------------------------
-r1860877 | hege | 2019-06-09 08:27:37 +0000 (Sun, 09 Jun 2019) | 2 lines
+r1900596 | hege | 2022-05-05 17:39:36 +0000 (Thu, 05 May 2022) | 2 lines
  
- Clarify --allowplugins dangerousness
+ Fix HAVE_ZLIB
  
 ------------------------------------------------------------------------
-r1860874 | hege | 2019-06-09 08:09:44 +0000 (Sun, 09 Jun 2019) | 2 lines
+r1900595 | hege | 2022-05-05 17:31:13 +0000 (Thu, 05 May 2022) | 2 lines
  
- Bug 7703 - sa-update aborts unnecessarily on IPv6-only hosts with valid 
-proxy
+ Fix: "my" variable $dbh masks earlier declaration in same scope
  
 ------------------------------------------------------------------------
-r1860873 | hege | 2019-06-09 08:05:38 +0000 (Sun, 09 Jun 2019) | 2 lines
+r1900594 | hege | 2022-05-05 17:31:00 +0000 (Thu, 05 May 2022) | 2 lines
  
- Fix unuinitialized errors when no subrules hit
+ Fix: Name "main::libidn_done" used only once: possible typo
  
 ------------------------------------------------------------------------
-r1860806 | hege | 2019-06-08 06:59:21 +0000 (Sat, 08 Jun 2019) | 2 lines
+r1900586 | sidney | 2022-05-05 13:15:00 +0000 (Thu, 05 May 2022) | 1 line
  
- Commit log suppressor from trunk
+ bug 7986 Partial fix lets  tests run when directory path up to 80 long. 
+Use workdir, remove now obsolete mk_safe_tmpdir()
+------------------------------------------------------------------------
+r1900583 | sidney | 2022-05-05 12:16:01 +0000 (Thu, 05 May 2022) | 1 line
  
+ 4.0.0-pre1 released
 ------------------------------------------------------------------------
-r1860766 | kmcgrail | 2019-06-07 15:09:53 +0000 (Fri, 07 Jun 2019) | 1 
-line
+r1900572 | sidney | 2022-05-05 02:46:37 +0000 (Thu, 05 May 2022) | 1 line
  
- Improving Debug output for subtest rule hits
+ preparing to release 4.0.0-pr1
 ------------------------------------------------------------------------
-r1859366 | gbechis | 2019-05-16 10:57:45 +0000 (Thu, 16 May 2019) | 2 
+r1900556 | gbechis | 2022-05-04 17:09:39 +0000 (Wed, 04 May 2022) | 2 
 lines
  
- remove last dot in hostname if present
+ basic FromNameSpoof tests
  
 ------------------------------------------------------------------------
-r1859210 | gbechis | 2019-05-14 07:11:22 +0000 (Tue, 14 May 2019) | 2 
+r1900555 | gbechis | 2022-05-04 16:39:55 +0000 (Wed, 04 May 2022) | 2 
 lines
  
- fix regexp
-------------------------------------------------------------------------
-r1859129 | kmcgrail | 2019-05-12 05:27:31 +0000 (Sun, 12 May 2019) | 1 
-line
+ fix man page formatting
  
- fixed some whitespace issues thanks to Kevin Golding
 ------------------------------------------------------------------------
-r1859116 | hege | 2019-05-11 15:01:08 +0000 (Sat, 11 May 2019) | 2 lines
- Fix 3.4 async semantics
+r1900553 | hege | 2022-05-04 13:43:43 +0000 (Wed, 04 May 2022) | 2 lines
  
-------------------------------------------------------------------------
-r1859114 | kmcgrail | 2019-05-11 13:24:27 +0000 (Sat, 11 May 2019) | 1 
-line
+ Document parallel testing, also as reminder for everyone..
  
- Fixing MANIFEST files
 ------------------------------------------------------------------------
-r1858971 | gbechis | 2019-05-09 07:40:13 +0000 (Thu, 09 May 2019) | 2 
-lines
+r1900552 | hege | 2022-05-04 13:39:36 +0000 (Wed, 04 May 2022) | 2 lines
  
- info(...) is not defined, use the proper version
+ Add FromNameSpoof
  
 ------------------------------------------------------------------------
-r1858690 | gbechis | 2019-05-05 14:13:19 +0000 (Sun, 05 May 2019) | 2 
-lines
+r1900549 | hege | 2022-05-04 12:51:24 +0000 (Wed, 04 May 2022) | 2 lines
  
- warn about "please rerun with debug enabled" only if debug is not enabled
+ Clean up code, properly wait for DKIM results, improve docs
  
 ------------------------------------------------------------------------
-r1858681 | gbechis | 2019-05-05 12:29:04 +0000 (Sun, 05 May 2019) | 2 
-lines
- silence a warning in a corner-case code path
+r1900547 | sidney | 2022-05-04 11:06:22 +0000 (Wed, 04 May 2022) | 1 line
  
+ bug 7982 fix tests failing when run from release tarball, removing 
+dependency on rules that are in trunk
 ------------------------------------------------------------------------
-r1858680 | gbechis | 2019-05-05 12:04:24 +0000 (Sun, 05 May 2019) | 3 
-lines
- check also urls that are only on plain/text part
- fix #bz 7086
+r1900546 | sidney | 2022-05-04 11:01:48 +0000 (Wed, 04 May 2022) | 1 line
  
+ Back to development version until we are ready to build the next 
+pre-release
 ------------------------------------------------------------------------
-r1858605 | gbechis | 2019-05-04 15:45:34 +0000 (Sat, 04 May 2019) | 2 
-lines
+r1900541 | hege | 2022-05-04 06:56:15 +0000 (Wed, 04 May 2022) | 2 lines
  
- Add more checks to check_rbl_rcvd
+ Fix typo
  
 ------------------------------------------------------------------------
-r1857623 | gbechis | 2019-04-16 06:30:43 +0000 (Tue, 16 Apr 2019) | 2 
+r1900538 | gbechis | 2022-05-04 06:43:17 +0000 (Wed, 04 May 2022) | 2 
 lines
  
Add more improvements recently developed
Match html files stored on Fleek cloud
  
 ------------------------------------------------------------------------
-r1857557 | hege | 2019-04-15 10:16:13 +0000 (Mon, 15 Apr 2019) | 2 lines
+r1900536 | hege | 2022-05-04 04:38:59 +0000 (Wed, 04 May 2022) | 2 lines
  
- Don't add X-ASN-Route metadata, it's just duplicate Bayes data for X-ASN
+ Don't add listname itself to a list
  
 ------------------------------------------------------------------------
-r1857549 | hege | 2019-04-15 06:45:15 +0000 (Mon, 15 Apr 2019) | 2 lines
+r1900531 | hege | 2022-05-04 02:52:21 +0000 (Wed, 04 May 2022) | 2 lines
  
- Bug 7211 - Support IPv6 ASN lookups with asn_lookup_ipv6
+ Add missing to MANIFEST
  
 ------------------------------------------------------------------------
-r1857048 | gbechis | 2019-04-06 07:46:52 +0000 (Sat, 06 Apr 2019) | 2 
+r1900515 | gbechis | 2022-05-03 15:02:40 +0000 (Tue, 03 May 2022) | 2 
 lines
  
- check authority values in dns answer
+ revert r1900506, not correct for general use
  
 ------------------------------------------------------------------------
-r1856933 | gbechis | 2019-04-04 13:34:49 +0000 (Thu, 04 Apr 2019) | 2 
+r1900514 | gbechis | 2022-05-03 14:56:35 +0000 (Tue, 03 May 2022) | 3 
 lines
  
- convert check_rbl_ns_from to async lookups
+ silence a warning if uri_to_domain fails.
+ bz #7984
  
 ------------------------------------------------------------------------
-r1856896 | gbechis | 2019-04-03 18:27:49 +0000 (Wed, 03 Apr 2019) | 2 
-lines
+r1900513 | hege | 2022-05-03 14:40:18 +0000 (Tue, 03 May 2022) | 4 lines
  
- copy check_hashbl_bodyre from trunk (r1848553)
+ - Add http code caching
+ - Add short_url_code just in case, to check any non-redirect http code
+ - Check register_eval_rule type
  
 ------------------------------------------------------------------------
-r1856894 | gbechis | 2019-04-03 18:18:17 +0000 (Wed, 03 Apr 2019) | 4 
-lines
+r1900512 | hege | 2022-05-03 13:50:53 +0000 (Tue, 03 May 2022) | 3 lines
  
- Add check_hashbl_emails from trunk
- Add the possibility to specify an acl to be able
- to check only some domains against an hashbl rbl
+ - Add clear_localrules() test function to use only rules defined in *.t 
+/ tstprefs().
+ - Convert sql_based_welcomelist.t to clear_localrules
  
 ------------------------------------------------------------------------
-r1856892 | gbechis | 2019-04-03 17:57:12 +0000 (Wed, 03 Apr 2019) | 2 
-lines
- enable check_rbl_rcvd
+r1900511 | sidney | 2022-05-03 13:48:24 +0000 (Tue, 03 May 2022) | 1 line
  
+ Cosmetic fix that does not affect the test but I could not unsee it once 
+I noticed it
 ------------------------------------------------------------------------
-r1856890 | gbechis | 2019-04-03 17:32:37 +0000 (Wed, 03 Apr 2019) | 3 
+r1900508 | gbechis | 2022-05-03 12:41:02 +0000 (Tue, 03 May 2022) | 2 
 lines
  
- Add check_rbl_rcvd
- to check all received headers domains or ip addresses against a specific 
-rbl.
+ missed in previous
  
 ------------------------------------------------------------------------
-r1856888 | gbechis | 2019-04-03 17:27:06 +0000 (Wed, 03 Apr 2019) | 4 
+r1900507 | gbechis | 2022-05-03 12:39:54 +0000 (Tue, 03 May 2022) | 2 
 lines
  
- Add check_rbl_headers to check specific headers in rbl
- Headers to be checked can be specified for all rbl
- or for a specific rbl
+ test autocleanup
  
 ------------------------------------------------------------------------
-r1856885 | gbechis | 2019-04-03 17:12:10 +0000 (Wed, 03 Apr 2019) | 3 
+r1900506 | gbechis | 2022-05-03 12:39:04 +0000 (Tue, 03 May 2022) | 3 
 lines
  
- add check_rbl_ns_from
- This checks in a rbl the dns server of the from addrs domain name.
+ cleanup database by checking "modified" field so that frequently checked
+ urls are always in hot cache
  
 ------------------------------------------------------------------------
-r1856026 | hege | 2019-03-22 05:02:57 +0000 (Fri, 22 Mar 2019) | 2 lines
- fix check_rbl_from_host from bug 7024
+r1900494 | sidney | 2022-05-02 23:53:07 +0000 (Mon, 02 May 2022) | 1 line
  
+ change a name used in test to make it clearer that a warning message is 
+expected and can be ignored
 ------------------------------------------------------------------------
-r1854814 | gbechis | 2019-03-05 07:29:05 +0000 (Tue, 05 Mar 2019) | 2 
+r1900485 | gbechis | 2022-05-02 16:12:44 +0000 (Mon, 02 May 2022) | 2 
 lines
  
- Net::CIDR::Lite is needed to run urilocalbl code
+ DecodeShortURLs cache test
  
 ------------------------------------------------------------------------
-r1854666 | hege | 2019-03-02 19:27:07 +0000 (Sat, 02 Mar 2019) | 2 lines
+r1900483 | hege | 2022-05-02 15:23:50 +0000 (Mon, 02 May 2022) | 2 lines
  
- Fix long string header wrapping (bug 7672)
+ Make TTL handling foolproof, do a cheap delete before select. Tidy 
+things up a bit.
  
 ------------------------------------------------------------------------
-r1854476 | gbechis | 2019-02-27 18:07:28 +0000 (Wed, 27 Feb 2019) | 3 
-lines
+r1900481 | hege | 2022-05-02 14:56:24 +0000 (Mon, 02 May 2022) | 2 lines
  
- Switch to https and fix some 404 errors
- bz #7652
+ Fix logic: Compare TTL to created field, otherwise entry might not never 
+expire and update itself.
  
 ------------------------------------------------------------------------
-r1854354 | gbechis | 2019-02-26 07:39:34 +0000 (Tue, 26 Feb 2019) | 3 
-lines
+r1900479 | hege | 2022-05-02 14:21:38 +0000 (Mon, 02 May 2022) | 2 lines
  
- fix make_install regression test on *BSD,
- still passes on Linux
+ Revert back to unix timestamps (int)
  
 ------------------------------------------------------------------------
-r1854347 | billcole | 2019-02-26 00:13:11 +0000 (Tue, 26 Feb 2019) | 2 
+r1900477 | gbechis | 2022-05-02 12:56:54 +0000 (Mon, 02 May 2022) | 2 
 lines
  
- Fixing bug 7302 without causing bug 7692
+ more tweaks to Paypal rule
  
 ------------------------------------------------------------------------
-r1854341 | gbechis | 2019-02-25 22:26:38 +0000 (Mon, 25 Feb 2019) | 2 
+r1900474 | gbechis | 2022-05-02 10:33:01 +0000 (Mon, 02 May 2022) | 2 
 lines
  
- fix regression test
+ another white tentacle
  
 ------------------------------------------------------------------------
-r1853301 | gbechis | 2019-02-10 08:54:17 +0000 (Sun, 10 Feb 2019) | 2 
+r1900468 | gbechis | 2022-05-01 22:31:52 +0000 (Sun, 01 May 2022) | 2 
 lines
  
Phishing.pm regression tests
more Paypal images
  
 ------------------------------------------------------------------------
-r1852885 | gbechis | 2019-02-04 09:55:45 +0000 (Mon, 04 Feb 2019) | 3 
+r1900467 | gbechis | 2022-05-01 21:40:15 +0000 (Sun, 01 May 2022) | 2 
 lines
  
- do not try to use Geo::IP constants if GeoIP2 is present
- fix #7687
+ Check emails with Paypal hosted image but message not from Paypal
  
 ------------------------------------------------------------------------
-r1852805 | gbechis | 2019-02-02 23:42:59 +0000 (Sat, 02 Feb 2019) | 2 
-lines
+r1900464 | hege | 2022-05-01 18:13:16 +0000 (Sun, 01 May 2022) | 2 lines
  
- fix msgcount type for txrep in Postgresql sql file
+ Improve docs
  
 ------------------------------------------------------------------------
-r1851889 | gbechis | 2019-01-23 07:48:46 +0000 (Wed, 23 Jan 2019) | 2 
-lines
+r1900462 | hege | 2022-05-01 17:51:21 +0000 (Sun, 01 May 2022) | 2 lines
  
- more speed improvements
+ Clean up DecodeShortURLs code. Add MySQL/Postgres support.
  
 ------------------------------------------------------------------------
-r1851418 | gbechis | 2019-01-16 07:41:34 +0000 (Wed, 16 Jan 2019) | 3 
-lines
+r1900458 | hege | 2022-05-01 14:10:07 +0000 (Sun, 01 May 2022) | 2 lines
  
- Fix pod errors
- bz #7682
+ Fix invalid tr//
  
 ------------------------------------------------------------------------
-r1851367 | billcole | 2019-01-15 14:32:48 +0000 (Tue, 15 Jan 2019) | 1 
-line
+r1900446 | hege | 2022-05-01 09:25:11 +0000 (Sun, 01 May 2022) | 2 lines
+ Fix perlcritic
  
- Fixing command-line example formatting. Bug #7679 
 ------------------------------------------------------------------------
-r1851021 | hege | 2019-01-11 08:52:30 +0000 (Fri, 11 Jan 2019) | 2 lines
+r1900443 | hege | 2022-05-01 09:08:00 +0000 (Sun, 01 May 2022) | 2 lines
  
- Fix RDNS_NONE when rdns=[1.2.3.4] (f.e. amavisd-milter)
+ run_long_tests is already enabled by default, remove unneeded duplicates 
+from xt/
  
 ------------------------------------------------------------------------
-r1851018 | gbechis | 2019-01-11 08:15:08 +0000 (Fri, 11 Jan 2019) | 2 
-lines
+r1900437 | hege | 2022-05-01 06:56:11 +0000 (Sun, 01 May 2022) | 2 lines
  
- Some speed improvements
+ Bug 7983 - t/all_modules.t (OLEVBMacro) fails without 
+Archive::Zip/IO::String
  
 ------------------------------------------------------------------------
-r1849822 | billcole | 2018-12-27 23:46:24 +0000 (Thu, 27 Dec 2018) | 1 
-line
+r1900420 | sidney | 2022-04-30 09:39:17 +0000 (Sat, 30 Apr 2022) | 1 line
  
- correcting URLs to https
+ preparing to release 4.0.0-rc1
 ------------------------------------------------------------------------
-r1849747 | gbechis | 2018-12-26 09:49:30 +0000 (Wed, 26 Dec 2018) | 3 
-lines
+r1900413 | hege | 2022-04-30 06:01:09 +0000 (Sat, 30 Apr 2022) | 2 lines
  
- As per Shevek's srs paper, srs scheme should be case insensitive
- bz #7673
+ Some last missing welcome/block changes
  
 ------------------------------------------------------------------------
-r1849441 | billcole | 2018-12-20 21:43:37 +0000 (Thu, 20 Dec 2018) | 1 
-line
+r1900393 | hege | 2022-04-29 18:26:21 +0000 (Fri, 29 Apr 2022) | 2 lines
  
- Failed lint should fail for real.
-------------------------------------------------------------------------
-r1848970 | kmcgrail | 2018-12-14 22:22:49 +0000 (Fri, 14 Dec 2018) | 1 
-line
+ Use catfile just to be pedantic
  
- Optimize extract of body rules during sa-compile - Bug 7665
 ------------------------------------------------------------------------
-r1848969 | kmcgrail | 2018-12-14 21:05:01 +0000 (Fri, 14 Dec 2018) | 1 
-line
+r1900390 | hege | 2022-04-29 16:26:57 +0000 (Fri, 29 Apr 2022) | 2 lines
+ Purge write testfiles only sometimes, remember to use catdir
  
- Work on improving evaluation rules and preparing for 3.4.3
 ------------------------------------------------------------------------
-r1848827 | gbechis | 2018-12-13 07:44:12 +0000 (Thu, 13 Dec 2018) | 3 
-lines
+r1900389 | hege | 2022-04-29 15:59:29 +0000 (Fri, 29 Apr 2022) | 2 lines
  
- Add sqlite database definitions for txrep
- fix bz #7668
+ Fix race condition generated warning of trying to -M a disappeared file 
+after readdir()
  
 ------------------------------------------------------------------------
-r1848550 | hege | 2018-12-10 06:03:10 +0000 (Mon, 10 Dec 2018) | 2 lines
+r1900388 | hege | 2022-04-29 15:20:40 +0000 (Fri, 29 Apr 2022) | 2 lines
  
- Fix RB warnings
+ Not sure what the previous commit was about, revert
  
 ------------------------------------------------------------------------
-r1848549 | hege | 2018-12-10 05:32:41 +0000 (Mon, 10 Dec 2018) | 2 lines
+r1900387 | hege | 2022-04-29 15:01:54 +0000 (Fri, 29 Apr 2022) | 2 lines
  
- Mention RegistryBoundaries 20_aux_tlds.cf fix (commit 1845096)
+ Apparently if has() isn't supported on SA 3.3... let's just use if 
+can(), because we are nice..
  
 ------------------------------------------------------------------------
-r1848548 | hege | 2018-12-10 05:22:27 +0000 (Mon, 10 Dec 2018) | 2 lines
+r1900386 | gbechis | 2022-04-29 13:47:54 +0000 (Fri, 29 Apr 2022) | 2 
+lines
  
- Fix hash warns
+ check only directories to avoid a warning
  
 ------------------------------------------------------------------------
-r1847473 | hege | 2018-11-26 14:23:41 +0000 (Mon, 26 Nov 2018) | 2 lines
+r1900371 | hege | 2022-04-29 03:54:35 +0000 (Fri, 29 Apr 2022) | 2 lines
  
- Document ALL-* pseudo-headers
+ Make sure mirrors fetches are randomized
  
 ------------------------------------------------------------------------
-r1846805 | hege | 2018-11-17 14:40:10 +0000 (Sat, 17 Nov 2018) | 2 lines
+r1900368 | hege | 2022-04-28 19:02:15 +0000 (Thu, 28 Apr 2022) | 2 lines
  
- Fix Windows-1252 autodetection with normalize_charset (Bug 7656)
+ Improve docs and --install errors
  
 ------------------------------------------------------------------------
-r1846293 | hege | 2018-11-10 10:37:56 +0000 (Sat, 10 Nov 2018) | 2 lines
+r1900365 | hege | 2022-04-28 18:32:59 +0000 (Thu, 28 Apr 2022) | 2 lines
  
- Bug 7655 - '/etc/mail/spamassassin/sa-update-keys/': No such file or 
-directory
+ It's really pointless to download SHA512/256 checksums if GPG is used, 
+so don't waste the mirrors with that.
  
 ------------------------------------------------------------------------
-r1845932 | hege | 2018-11-06 16:08:20 +0000 (Tue, 06 Nov 2018) | 2 lines
+r1900364 | hege | 2022-04-28 17:38:37 +0000 (Thu, 28 Apr 2022) | 2 lines
  
- Mention parse_dkim_uris in URIDNSBL docs too
+ Use zopfli for better compression, clean up paths from hashfiles
  
 ------------------------------------------------------------------------
-r1845736 | hege | 2018-11-04 13:36:22 +0000 (Sun, 04 Nov 2018) | 2 lines
+r1900308 | hege | 2022-04-27 07:19:16 +0000 (Wed, 27 Apr 2022) | 2 lines
  
Skip duplicate lookups
Re-enable automatic updates
  
 ------------------------------------------------------------------------
-r1845723 | hege | 2018-11-04 11:16:11 +0000 (Sun, 04 Nov 2018) | 2 lines
+r1900305 | hege | 2022-04-27 06:15:56 +0000 (Wed, 27 Apr 2022) | 2 lines
  
- Bug 7242 - URIBL_SBL and URIBL_SBL_A doing each other's lookups
+ Bug 7980 - plaintext_body_sig_ratio performance
  
 ------------------------------------------------------------------------
-r1845197 | hege | 2018-10-30 06:26:56 +0000 (Tue, 30 Oct 2018) | 2 lines
+r1900294 | hege | 2022-04-26 17:35:35 +0000 (Tue, 26 Apr 2022) | 2 lines
  
- Small re fix, don't warn with sa-update lint
+ Minor fixes
  
 ------------------------------------------------------------------------
-r1845107 | hege | 2018-10-29 12:03:00 +0000 (Mon, 29 Oct 2018) | 2 lines
+r1900291 | hege | 2022-04-26 14:19:48 +0000 (Tue, 26 Apr 2022) | 2 lines
  
- Fix RB tests and case-i
+ Only mark presence of Autocrypt header
  
 ------------------------------------------------------------------------
-r1845096 | hege | 2018-10-29 10:29:15 +0000 (Mon, 29 Oct 2018) | 2 lines
+r1900273 | hege | 2022-04-25 17:17:04 +0000 (Mon, 25 Apr 2022) | 2 lines
  
- Make RegistryBoundaries actually use 20_aux_tlds.cf, initialize it only 
-after configuration is parsed. Fix plugins to handle valid_tlds_re at 
-finish_parsing_end. Remove old hardcoded list, only sa-update is now 
-supported.
+ Forgot reuse
  
 ------------------------------------------------------------------------
-r1845067 | hege | 2018-10-28 22:16:50 +0000 (Sun, 28 Oct 2018) | 2 lines
+r1900272 | hege | 2022-04-25 17:00:36 +0000 (Mon, 25 Apr 2022) | 2 lines
  
- Remove unused Data::Dumper
+ Add DMARC stock rules
  
 ------------------------------------------------------------------------
-r1844916 | hege | 2018-10-26 16:55:46 +0000 (Fri, 26 Oct 2018) | 2 lines
+r1900271 | hege | 2022-04-25 16:21:08 +0000 (Mon, 25 Apr 2022) | 2 lines
  
- fix dbg facilities
+ Fix typo
  
 ------------------------------------------------------------------------
-r1844901 | hege | 2018-10-26 12:35:00 +0000 (Fri, 26 Oct 2018) | 2 lines
+r1900270 | hege | 2022-04-25 16:16:16 +0000 (Mon, 25 Apr 2022) | 2 lines
  
- Duh, it's "dns_server"
+ Minor cleanup
  
 ------------------------------------------------------------------------
-r1844900 | hege | 2018-10-26 12:33:14 +0000 (Fri, 26 Oct 2018) | 2 lines
+r1900269 | hege | 2022-04-25 16:09:24 +0000 (Mon, 25 Apr 2022) | 2 lines
  
- Ignore dns_servers in sa-update files, paranoid check
+ Clean up *.pre files
  
 ------------------------------------------------------------------------
-r1844813 | hege | 2018-10-25 08:32:57 +0000 (Thu, 25 Oct 2018) | 2 lines
+r1900268 | hege | 2022-04-25 16:05:58 +0000 (Mon, 25 Apr 2022) | 3 lines
  
- Call test_log instead of got_hit description suffix hackery
+ - Use DMARC by default like SPF/DKIM.
+ - Lazy load Mail::DMARC::PurePerl and only dbg() failure if it's missing 
+(like SPF/DKIM).
  
 ------------------------------------------------------------------------
-r1844811 | hege | 2018-10-25 07:39:45 +0000 (Thu, 25 Oct 2018) | 2 lines
+r1900267 | hege | 2022-04-25 15:47:57 +0000 (Mon, 25 Apr 2022) | 2 lines
  
- Do not resolve things unless is_dns_available()
+ Fix ifplugin Dmarc/WhiteListSubject backwards compatibility
  
 ------------------------------------------------------------------------
-r1844808 | hege | 2018-10-25 06:07:23 +0000 (Thu, 25 Oct 2018) | 2 lines
+r1900253 | hege | 2022-04-25 08:10:56 +0000 (Mon, 25 Apr 2022) | 2 lines
  
- Bug 6360 - "negative match" on a "0" string
+ Update docs
  
 ------------------------------------------------------------------------
-r1844620 | hege | 2018-10-23 07:07:53 +0000 (Tue, 23 Oct 2018) | 2 lines
+r1900249 | hege | 2022-04-25 05:41:18 +0000 (Mon, 25 Apr 2022) | 2 lines
  
- Small ident fix
+ Bug 7979 - tests fail if Mail::DMARC is not installed
  
 ------------------------------------------------------------------------
-r1844618 | hege | 2018-10-23 06:09:01 +0000 (Tue, 23 Oct 2018) | 2 lines
+r1900247 | hege | 2022-04-25 05:08:21 +0000 (Mon, 25 Apr 2022) | 2 lines
  
- Fix t/get_all_headers.t
+ Fix mkupdates
  
 ------------------------------------------------------------------------
-r1844485 | gbechis | 2018-10-21 12:10:40 +0000 (Sun, 21 Oct 2018) | 3 
-lines
+r1900225 | hege | 2022-04-23 17:17:49 +0000 (Sat, 23 Apr 2022) | 2 lines
  
- Add last_hit to awl table as well
- bz #7631
+ utf8 tweak
  
 ------------------------------------------------------------------------
-r1844387 | hege | 2018-10-20 03:19:42 +0000 (Sat, 20 Oct 2018) | 2 lines
+r1900221 | hege | 2022-04-23 12:46:04 +0000 (Sat, 23 Apr 2022) | 2 lines
  
Fix check_illegal_chars ALL:raw usage
Update doc, Mail::DMARC::PurePerl is part of Mail::DMARC package
  
 ------------------------------------------------------------------------
-r1844385 | hege | 2018-10-20 03:05:08 +0000 (Sat, 20 Oct 2018) | 2 lines
+r1900216 | hege | 2022-04-23 12:19:33 +0000 (Sat, 23 Apr 2022) | 2 lines
  
- Sync with trunk, ALL fixes
+ Disable rule updates temporarily for welcomelists testing (Bug 7826)
  
 ------------------------------------------------------------------------
-r1844384 | hege | 2018-10-20 02:57:00 +0000 (Sat, 20 Oct 2018) | 2 lines
+r1900215 | hege | 2022-04-23 12:18:23 +0000 (Sat, 23 Apr 2022) | 2 lines
  
- Fix typo..
+ Merge trunk-welcomelist to trunk (Bug 7826)
  
 ------------------------------------------------------------------------
-r1844383 | hege | 2018-10-20 02:54:21 +0000 (Sat, 20 Oct 2018) | 2 lines
+r1900212 | hege | 2022-04-23 12:02:32 +0000 (Sat, 23 Apr 2022) | 2 lines
  
- Make ALL pseudo-header return decoded headers, so it's usage is 
-consistent with normal header usage
+ Add Net::LibIDN2 support
  
 ------------------------------------------------------------------------
-r1844334 | hege | 2018-10-19 12:49:51 +0000 (Fri, 19 Oct 2018) | 2 lines
+r1900201 | hege | 2022-04-23 08:59:38 +0000 (Sat, 23 Apr 2022) | 2 lines
  
- Bug 7224 - fix get_all_hdrs_in_rcvd_index_range, get(ALL[-*]) should 
-return unfolded header lines unless :raw called
+ Update INSTALL
  
 ------------------------------------------------------------------------
-r1844306 | gbechis | 2018-10-19 06:36:47 +0000 (Fri, 19 Oct 2018) | 4 
-lines
+r1900196 | hege | 2022-04-23 08:32:08 +0000 (Sat, 23 Apr 2022) | 2 lines
  
- Starting from SQL-92 "count" is a reserved word
- Renamed field count to msgcount, follow UPGRADE notes to update your 
-database
- fixes bz #7578
+ Clean up DependencyInfo
  
 ------------------------------------------------------------------------
-r1843623 | gbechis | 2018-10-12 06:38:56 +0000 (Fri, 12 Oct 2018) | 3 
-lines
+r1900193 | hege | 2022-04-23 08:29:46 +0000 (Sat, 23 Apr 2022) | 2 lines
  
- Change an info message into a debug message, not useful for the average 
-user
- bz #7632
+ Add IO::String dep
  
 ------------------------------------------------------------------------
-r1843622 | gbechis | 2018-10-12 06:14:11 +0000 (Fri, 12 Oct 2018) | 3 
-lines
+r1900192 | hege | 2022-04-23 08:20:56 +0000 (Sat, 23 Apr 2022) | 2 lines
  
- Fix txrep_ipv{4,6}_mask_len option
- bz #7640
+ Update INSTALL documentation
  
 ------------------------------------------------------------------------
-r1843574 | hege | 2018-10-11 17:03:36 +0000 (Thu, 11 Oct 2018) | 2 lines
+r1900187 | hege | 2022-04-23 07:41:02 +0000 (Sat, 23 Apr 2022) | 2 lines
  
- Bug 7641 - FromNameSpoof plugin comments still reference dns_check
+ Remove HTTP::Date dependency
  
 ------------------------------------------------------------------------
-r1843047 | hege | 2018-10-07 07:43:12 +0000 (Sun, 07 Oct 2018) | 2 lines
+r1900186 | hege | 2022-04-23 06:45:17 +0000 (Sat, 23 Apr 2022) | 2 lines
  
- Deprecate ancient TieOneStringHash usage, it's an absolute performance 
-pig
+ Clean up version requirements
  
 ------------------------------------------------------------------------
-r1843010 | gbechis | 2018-10-06 10:45:59 +0000 (Sat, 06 Oct 2018) | 3 
-lines
+r1900161 | hege | 2022-04-22 17:15:57 +0000 (Fri, 22 Apr 2022) | 2 lines
  
- do not consider Sympa headers in Bayes as we have done
- for other mailing lists softwares
+ DMARC plugin cleanup and rename Dmarc.pm -> DMARC.pm
  
 ------------------------------------------------------------------------
-r1842773 | hege | 2018-10-04 04:49:18 +0000 (Thu, 04 Oct 2018) | 2 lines
+r1900138 | hege | 2022-04-22 06:46:06 +0000 (Fri, 22 Apr 2022) | 4 lines
  
- Bug 7589 - Tag optional modules in debug_diagnostics
+ - Tokenize From/To/Cc names (Bug 6319)
+ - Fix *MI *Ad *UA parsing, only last found header value was used, duh
+ - Improve logging
  
 ------------------------------------------------------------------------
-r1842645 | gbechis | 2018-10-02 17:40:43 +0000 (Tue, 02 Oct 2018) | 2 
-lines
+r1900137 | hege | 2022-04-22 06:40:42 +0000 (Fri, 22 Apr 2022) | 2 lines
  
- fix spamc file leak, bz #7638
+ Bug 7674 - sa-learn learns all messages as ham even if --spam is 
+specified
  
 ------------------------------------------------------------------------
-r1842597 | gbechis | 2018-10-02 06:35:44 +0000 (Tue, 02 Oct 2018) | 2 
-lines
+r1900131 | hege | 2022-04-22 04:44:26 +0000 (Fri, 22 Apr 2022) | 2 lines
  
- typo
+ More consistent dbg
  
 ------------------------------------------------------------------------
-r1842593 | hege | 2018-10-02 04:56:57 +0000 (Tue, 02 Oct 2018) | 2 lines
+r1900130 | hege | 2022-04-22 04:40:45 +0000 (Fri, 22 Apr 2022) | 2 lines
  
Allow decimal number in meta token (Bug 7557)
Remove some unnecessary "warning:" from dbg (Bug 7788)
  
 ------------------------------------------------------------------------
-r1842467 | hege | 2018-10-01 10:44:52 +0000 (Mon, 01 Oct 2018) | 2 lines
+r1900116 | hege | 2022-04-21 17:54:17 +0000 (Thu, 21 Apr 2022) | 2 lines
  
- Fix doc typo
+ Update compressed extensions
  
 ------------------------------------------------------------------------
-r1842427 | gbechis | 2018-10-01 06:21:12 +0000 (Mon, 01 Oct 2018) | 3 
-lines
+r1900115 | hege | 2022-04-21 17:48:18 +0000 (Thu, 21 Apr 2022) | 2 lines
  
- fix a typo and unbreak
- bz #7636
+ Bug 7977 - sa-learn --mbox broken in trunk
  
 ------------------------------------------------------------------------
-r1842426 | hege | 2018-10-01 05:02:34 +0000 (Mon, 01 Oct 2018) | 2 lines
+r1900092 | hege | 2022-04-21 04:15:50 +0000 (Thu, 21 Apr 2022) | 2 lines
  
- Actually fastest this way
+ Sigh typo
  
 ------------------------------------------------------------------------
-r1842425 | hege | 2018-10-01 04:47:21 +0000 (Mon, 01 Oct 2018) | 2 lines
+r1900091 | hege | 2022-04-21 04:15:00 +0000 (Thu, 21 Apr 2022) | 2 lines
  
- Fix very bad optimization
+ cat is not portable
  
 ------------------------------------------------------------------------
-r1842403 | hege | 2018-09-30 18:24:47 +0000 (Sun, 30 Sep 2018) | 2 lines
+r1900090 | hege | 2022-04-21 03:51:14 +0000 (Thu, 21 Apr 2022) | 2 lines
  
- Actually tell which meta rules token is considered strange
+ Add enable_compat dbg
  
 ------------------------------------------------------------------------
-r1842326 | hege | 2018-09-29 12:10:15 +0000 (Sat, 29 Sep 2018) | 2 lines
- Fix bug 7418 changes, next mirror retry works again. Few cosmetic 
-updates.
+r1900080 | billcole | 2022-04-20 18:02:27 +0000 (Wed, 20 Apr 2022) | 1 
+line
  
+ remove .space from TLD lists and remove test rule which demo'd the issue 
+(BZ#7953)
 ------------------------------------------------------------------------
-r1842321 | hege | 2018-09-29 10:20:26 +0000 (Sat, 29 Sep 2018) | 2 lines
+r1900062 | hege | 2022-04-20 06:02:40 +0000 (Wed, 20 Apr 2022) | 2 lines
  
- Bug 7623 - sa-update files with mirrors containing paths (or ports)
+ Fix version
  
 ------------------------------------------------------------------------
-r1842303 | hege | 2018-09-29 09:41:24 +0000 (Sat, 29 Sep 2018) | 2 lines
+r1900060 | hege | 2022-04-20 05:54:52 +0000 (Wed, 20 Apr 2022) | 2 lines
  
- Bug 7623 - sa-update files with mirrors containing paths (or ports)
+ Bug 7973 - PerMsgStatus.pm: sub finish_tests never called
  
 ------------------------------------------------------------------------
-r1842074 | hege | 2018-09-27 08:04:21 +0000 (Thu, 27 Sep 2018) | 2 lines
+r1900058 | hege | 2022-04-20 05:20:16 +0000 (Wed, 20 Apr 2022) | 2 lines
  
Add touch_file() to utils
Fix tests
  
 ------------------------------------------------------------------------
-r1842029 | hege | 2018-09-26 14:21:12 +0000 (Wed, 26 Sep 2018) | 2 lines
+r1900057 | hege | 2022-04-20 05:05:54 +0000 (Wed, 20 Apr 2022) | 2 lines
  
- Bug 7624 - fix fns_ignore_dkim etc cleanup
+ Add missing $spamtest->finish(); when --linting
  
 ------------------------------------------------------------------------
-r1842026 | hege | 2018-09-26 13:57:29 +0000 (Wed, 26 Sep 2018) | 2 lines
+r1900054 | hege | 2022-04-20 03:57:17 +0000 (Wed, 20 Apr 2022) | 2 lines
  
- HashBL did lookups with only local tests enabled :-(
+ Fix make
  
 ------------------------------------------------------------------------
-r1841938 | hege | 2018-09-25 14:29:14 +0000 (Tue, 25 Sep 2018) | 2 lines
+r1900053 | hege | 2022-04-20 03:56:59 +0000 (Wed, 20 Apr 2022) | 2 lines
  
- Remove anti-optimization (remember to benchmark these things..)
+ Fix make
  
 ------------------------------------------------------------------------
-r1841937 | hege | 2018-09-25 14:28:23 +0000 (Tue, 25 Sep 2018) | 2 lines
+r1900050 | hege | 2022-04-20 03:33:17 +0000 (Wed, 20 Apr 2022) | 2 lines
  
- Fix indentation
+ Bug 7974 - SpamAssassin.pm, wrong order of calls in sub finish
  
 ------------------------------------------------------------------------
-r1841821 | hege | 2018-09-24 09:53:55 +0000 (Mon, 24 Sep 2018) | 2 lines
+r1900049 | hege | 2022-04-20 03:23:48 +0000 (Wed, 20 Apr 2022) | 2 lines
  
- Bug 7610 - Fix and move DKIM_INVALID to official rules
+ Bug 7976 - Check.pm wrong pointer
  
 ------------------------------------------------------------------------
-r1841820 | hege | 2018-09-24 09:52:33 +0000 (Mon, 24 Sep 2018) | 2 lines
+r1900048 | hege | 2022-04-20 03:16:17 +0000 (Wed, 20 Apr 2022) | 2 lines
  
- Add missing t/freemail.t
+ Fix make
  
 ------------------------------------------------------------------------
-r1841804 | hege | 2018-09-24 08:07:48 +0000 (Mon, 24 Sep 2018) | 2 lines
+r1900046 | hege | 2022-04-20 02:55:05 +0000 (Wed, 20 Apr 2022) | 2 lines
  
- Add freemail_import_whitelist_auth, freemail_import_def_whitelist_auth 
-(Bug 6451)
+ Fix make
  
 ------------------------------------------------------------------------
-r1841802 | hege | 2018-09-24 06:55:34 +0000 (Mon, 24 Sep 2018) | 2 lines
+r1900045 | hege | 2022-04-20 02:49:42 +0000 (Wed, 20 Apr 2022) | 2 lines
  
- Perldocified and added t/freemail.t test
+ Bug 7975 - Util.pm, sub domain_to_search_list, code reordering
  
 ------------------------------------------------------------------------
-r1841540 | gbechis | 2018-09-21 06:55:32 +0000 (Fri, 21 Sep 2018) | 3 
-lines
+r1900021 | hege | 2022-04-19 08:38:39 +0000 (Tue, 19 Apr 2022) | 4 lines
  
- fix fp FORGED_YAHOO_RCVD
- bz# 7625
+ - Disable possible run_nightly tarball creation, mkupdate-with-scores 
+already does it more reliably
+ - Update tarball lint test much succeed for ALL versions (3.4.1-3.4.6 
+currently tested)
+ - Code fixes and cleanups
  
 ------------------------------------------------------------------------
-r1841433 | gbechis | 2018-09-20 07:18:53 +0000 (Thu, 20 Sep 2018) | 4 
+r1900016 | gbechis | 2022-04-19 06:45:27 +0000 (Tue, 19 Apr 2022) | 2 
 lines
  
- revert r1838778, fixing a possible use-after-free,
- opt can be used later.
- bz #7633
+ if a restartable signal is caught, retry select(2) 3 times before 
+aborting
  
 ------------------------------------------------------------------------
-r1841427 | hege | 2018-09-20 06:25:02 +0000 (Thu, 20 Sep 2018) | 2 lines
+r1900011 | hege | 2022-04-19 05:46:22 +0000 (Tue, 19 Apr 2022) | 2 lines
  
- MANIFEST missing t/relaycountry_geoip2.t
+ Error check cd, fix regexp
  
 ------------------------------------------------------------------------
-r1841423 | hege | 2018-09-20 05:24:08 +0000 (Thu, 20 Sep 2018) | 2 lines
- Add /var/lib/GeoIP to search path
+r1900005 | sidney | 2022-04-19 01:23:31 +0000 (Tue, 19 Apr 2022) | 1 line
  
+ bug 7358 Accomodate certain mailformed nested MIME that some MUAs accept
 ------------------------------------------------------------------------
-r1841422 | hege | 2018-09-20 05:10:45 +0000 (Thu, 20 Sep 2018) | 2 lines
+r1899984 | hege | 2022-04-18 15:16:23 +0000 (Mon, 18 Apr 2022) | 2 lines
  
- Make GeoIP2 default paths configurable, add ubuntu /var/lib/GeoIP, clean 
-up a bit
+ enable_compat feature (Bug 7972)
  
 ------------------------------------------------------------------------
-r1841385 | hege | 2018-09-19 20:35:55 +0000 (Wed, 19 Sep 2018) | 2 lines
+r1899954 | hege | 2022-04-17 16:33:29 +0000 (Sun, 17 Apr 2022) | 2 lines
  
- Duh, can add -L arg too
+ Add "config: parsing file foo.cf" debug output, to see the actual 
+parsing order (vs "read file" which is just physical reads not in 
+"include" order)
  
 ------------------------------------------------------------------------
-r1841384 | hege | 2018-09-19 20:26:54 +0000 (Wed, 19 Sep 2018) | 2 lines
+r1899923 | hege | 2022-04-17 05:21:13 +0000 (Sun, 17 Apr 2022) | 2 lines
  
- Add relaycountry_geoip2 test, fix all relaycountry tests not requiring 
-net
+ Apply Bug 5771 to TxRep too
  
 ------------------------------------------------------------------------
-r1841378 | hege | 2018-09-19 20:07:27 +0000 (Wed, 19 Sep 2018) | 2 lines
+r1899918 | sidney | 2022-04-16 23:09:01 +0000 (Sat, 16 Apr 2022) | 1 line
  
- Try default database locations for GeoIP2
+ Clarify usage and perldoc documentation for -D option
+------------------------------------------------------------------------
+r1899917 | sidney | 2022-04-16 23:00:37 +0000 (Sat, 16 Apr 2022) | 1 line
  
+ bug 7674 make --ham or --spam not optional for first path in command 
+line, improve documentation
 ------------------------------------------------------------------------
-r1841359 | hege | 2018-09-19 17:58:01 +0000 (Wed, 19 Sep 2018) | 2 lines
+r1899900 | hege | 2022-04-16 07:06:20 +0000 (Sat, 16 Apr 2022) | 2 lines
  
- Reorganize code for simplicity/readability, handle GeoIP2 errors 
-gracefully
+ Bug 7646 - spamd running with virtual-config-dir mkdir error
  
 ------------------------------------------------------------------------
-r1841346 | hege | 2018-09-19 14:24:48 +0000 (Wed, 19 Sep 2018) | 2 lines
+r1899898 | hege | 2022-04-16 05:57:18 +0000 (Sat, 16 Apr 2022) | 2 lines
  
- Fix few badly parsed IPs
+ Bug 5771 - umask issue in UnixNFSSafe.pm
  
 ------------------------------------------------------------------------
-r1841313 | hege | 2018-09-19 10:44:43 +0000 (Wed, 19 Sep 2018) | 2 lines
+r1899897 | hege | 2022-04-16 05:18:35 +0000 (Sat, 16 Apr 2022) | 2 lines
  
- Bug 7622: fix IP matching..
+ Allow disabling stopwords processing with "bayes_stopword_languages 
+disable"
  
 ------------------------------------------------------------------------
-r1841309 | gbechis | 2018-09-19 10:18:01 +0000 (Wed, 19 Sep 2018) | 4 
-lines
+r1899896 | hege | 2022-04-16 05:11:33 +0000 (Sat, 16 Apr 2022) | 2 lines
  
- Prevent URILocalBL plugin from using dns in regression tests
- and iff there is an ip in uri.
- bz #7622
+ Don't try to change uid/gid if not needed
  
 ------------------------------------------------------------------------
-r1841192 | gbechis | 2018-09-18 06:33:48 +0000 (Tue, 18 Sep 2018) | 3 
-lines
+r1899889 | sidney | 2022-04-15 12:18:08 +0000 (Fri, 15 Apr 2022) | 1 line
  
- geoip regression tests needs network because of
- dns
+ bug 5740 spamd tries to bayes learn when reporting even when bayes is 
+disabled
+------------------------------------------------------------------------
+r1899876 | sidney | 2022-04-15 05:00:51 +0000 (Fri, 15 Apr 2022) | 1 line
  
+ fix typo in a pkg name
 ------------------------------------------------------------------------
-r1841067 | kmcgrail | 2018-09-17 11:21:22 +0000 (Mon, 17 Sep 2018) | 1 
+r1899866 | billcole | 2022-04-14 18:58:50 +0000 (Thu, 14 Apr 2022) | 1 
 line
  
- Refining the process for announcing new versions - Bug 7620
+ See bug 7971. Limited score on DOS_RCVD_IP_TWICE_B
 ------------------------------------------------------------------------
-r1841065 | kmcgrail | 2018-09-17 11:14:16 +0000 (Mon, 17 Sep 2018) | 1 
-line
+r1899850 | hege | 2022-04-14 12:01:32 +0000 (Thu, 14 Apr 2022) | 2 lines
+ Update ArchiveIterator note
  
- spamc fixes to compile for windows - bug 7617
 ------------------------------------------------------------------------
-r1841063 | kmcgrail | 2018-09-17 11:11:19 +0000 (Mon, 17 Sep 2018) | 1 
-line
+r1899849 | hege | 2022-04-14 11:58:05 +0000 (Thu, 14 Apr 2022) | 2 lines
+ Update outdated message size clause
  
- dmake install failure on windows - bug 7255
 ------------------------------------------------------------------------
-r1841022 | kmcgrail | 2018-09-16 16:04:35 +0000 (Sun, 16 Sep 2018) | 1 
-line
+r1899848 | hege | 2022-04-14 11:50:31 +0000 (Thu, 14 Apr 2022) | 2 lines
+ Further ArchiveIterator improvements, all of gzip/bzip2/xz/lz4/lzip/lzo 
+are now detected and uncompressed automatically.
  
- more tweaks to the build process
 ------------------------------------------------------------------------
-r1841018 | kmcgrail | 2018-09-16 14:38:05 +0000 (Sun, 16 Sep 2018) | 1 
-line
+r1899844 | gbechis | 2022-04-14 11:09:46 +0000 (Thu, 14 Apr 2022) | 2 
+lines
+ add support for 3rd tld url shorteners that creates a random 3rd level 
+subdomain.
  
- more cleanup on the build process
 ------------------------------------------------------------------------
-r1841016 | kmcgrail | 2018-09-16 14:12:15 +0000 (Sun, 16 Sep 2018) | 1 
-line
+r1899843 | hege | 2022-04-14 11:03:16 +0000 (Thu, 14 Apr 2022) | 4 lines
+ ArchiveIterator cleanups
+ - Uncompress gzip regardless of extension (Bug 7598)
+ - Add .xz support
  
-  more tweaks
 ------------------------------------------------------------------------
-r1841010 | kmcgrail | 2018-09-16 13:42:55 +0000 (Sun, 16 Sep 2018) | 1 
-line
+r1899837 | hege | 2022-04-14 07:18:24 +0000 (Thu, 14 Apr 2022) | 2 lines
+ Remove unneeded Compress::Zlib mention, has been in Perl core since 5.10
  
- tweaks for updating the website docs
 ------------------------------------------------------------------------
-r1841005 | kmcgrail | 2018-09-16 13:25:13 +0000 (Sun, 16 Sep 2018) | 1 
-line
+r1899836 | hege | 2022-04-14 06:56:53 +0000 (Thu, 14 Apr 2022) | 2 lines
+ ArchiveIterator: skip disappeared files gracefully (Bug 7934)
  
- small tweak on announcement
 ------------------------------------------------------------------------
-r1840976 | kmcgrail | 2018-09-15 19:17:32 +0000 (Sat, 15 Sep 2018) | 1 
-line
+r1899804 | gbechis | 2022-04-13 10:29:00 +0000 (Wed, 13 Apr 2022) | 2 
+lines
+ fix error message handling
  
- Adding another step for release
 ------------------------------------------------------------------------
-r1840966 | kmcgrail | 2018-09-15 06:25:34 +0000 (Sat, 15 Sep 2018) | 1 
-line
+r1899803 | hege | 2022-04-13 09:40:24 +0000 (Wed, 13 Apr 2022) | 2 lines
+ Bug 7267 - no way to set SSL_VERIFY_PEER in spamd
  
- continue to document the release process
 ------------------------------------------------------------------------
-r1840957 | gbechis | 2018-09-14 22:14:29 +0000 (Fri, 14 Sep 2018) | 2 
-lines
+r1899775 | hege | 2022-04-12 10:55:47 +0000 (Tue, 12 Apr 2022) | 2 lines
  
- switch all ASF web sites uri to https
+ Bug 7183 - Spamc/Spamd very slow with -z compression and ssl
  
 ------------------------------------------------------------------------
-r1840872 | kmcgrail | 2018-09-14 01:31:55 +0000 (Fri, 14 Sep 2018) | 1 
-line
+r1899770 | hege | 2022-04-12 08:39:24 +0000 (Tue, 12 Apr 2022) | 2 lines
+ Improve accept error handling
  
- build process clean-up and 3.4.2 announcement updates
 ------------------------------------------------------------------------
-r1840870 | kmcgrail | 2018-09-14 01:25:10 +0000 (Fri, 14 Sep 2018) | 1 
-line
+r1899762 | hege | 2022-04-12 06:26:37 +0000 (Tue, 12 Apr 2022) | 2 lines
+ Log SSL version/cipher
  
- preparing to release 3.4.2
 ------------------------------------------------------------------------
-r1840662 | sidney | 2018-09-12 11:35:41 +0000 (Wed, 12 Sep 2018) | 1 line
+r1899744 | hege | 2022-04-11 11:38:39 +0000 (Mon, 11 Apr 2022) | 2 lines
+ Bug 7941 - sql/txrep_sqlite.sql: typo in UPDATE trigger column name 
+breaks txrep DB
  
- Add Paul Stead as committer
 ------------------------------------------------------------------------
-r1840385 | kmcgrail | 2018-09-08 21:37:32 +0000 (Sat, 08 Sep 2018) | 1 
-line
+r1899743 | gbechis | 2022-04-11 11:01:02 +0000 (Mon, 11 Apr 2022) | 2 
+lines
+ add max_size support
  
- more build updates and new rc1 sums for announcement
 ------------------------------------------------------------------------
-r1840380 | kmcgrail | 2018-09-08 21:08:05 +0000 (Sat, 08 Sep 2018) | 1 
-line
+r1899742 | gbechis | 2022-04-11 10:28:47 +0000 (Mon, 11 Apr 2022) | 2 
+lines
+ add support for REPORT and REPORT_IFSPAM commands
  
- preparing to release 3.4.2-rc1 again with new sa-update
 ------------------------------------------------------------------------
-r1840377 | kmcgrail | 2018-09-08 20:40:12 +0000 (Sat, 08 Sep 2018) | 1 
-line
+r1899741 | hege | 2022-04-11 09:47:45 +0000 (Mon, 11 Apr 2022) | 2 lines
+ Revert 1899730 msgcount change due to unforeseen dependencies, cheers 
+Paul
  
- Removing sha-1 sig support from sa-update - bug 7614
 ------------------------------------------------------------------------
-r1840330 | kmcgrail | 2018-09-08 01:05:14 +0000 (Sat, 08 Sep 2018) | 1 
-line
+r1899740 | hege | 2022-04-11 09:39:37 +0000 (Mon, 11 Apr 2022) | 2 lines
+ Some SQL documentation updates
  
- changing to 3.3.3 to 3.3.2
 ------------------------------------------------------------------------
-r1840329 | kmcgrail | 2018-09-08 01:03:42 +0000 (Sat, 08 Sep 2018) | 1 
-line
+r1899739 | hege | 2022-04-11 09:22:36 +0000 (Mon, 11 Apr 2022) | 2 lines
+ Use DBD::SQLite for Bayes tests when available
  
- fixing a small typo in the announcement
 ------------------------------------------------------------------------
-r1840233 | kmcgrail | 2018-09-06 16:07:14 +0000 (Thu, 06 Sep 2018) | 1 
-line
+r1899738 | hege | 2022-04-11 09:20:59 +0000 (Mon, 11 Apr 2022) | 2 lines
+ Fix _token_select_string as SQLite compatible
  
- updating the hash sigs for the announcement
 ------------------------------------------------------------------------
-r1840230 | kmcgrail | 2018-09-06 15:47:47 +0000 (Thu, 06 Sep 2018) | 1 
-line
+r1899737 | pds | 2022-04-11 08:36:15 +0000 (Mon, 11 Apr 2022) | 1 line
  
- preparing to release 3.4.2-rc1
+ Add last_hit to schema
 ------------------------------------------------------------------------
-r1840219 | kmcgrail | 2018-09-06 13:02:56 +0000 (Thu, 06 Sep 2018) | 1 
-line
+r1899734 | hege | 2022-04-11 08:27:40 +0000 (Mon, 11 Apr 2022) | 2 lines
+ Use RPAD only in MySQL _token_select_string as originally intended. 
+SQLite does not have RPAD.
  
- more cleanup of branding and build process
 ------------------------------------------------------------------------
-r1840213 | kmcgrail | 2018-09-06 12:04:10 +0000 (Thu, 06 Sep 2018) | 1 
-line
+r1899731 | hege | 2022-04-11 07:09:25 +0000 (Mon, 11 Apr 2022) | 2 lines
+ Use DBD::SQLite for AWL tests when available
  
- fix for Util wrap pre Perl 5.14 - bug 7616
 ------------------------------------------------------------------------
-r1840170 | kmcgrail | 2018-09-05 23:46:20 +0000 (Wed, 05 Sep 2018) | 1 
-line
+r1899730 | hege | 2022-04-11 07:05:08 +0000 (Mon, 11 Apr 2022) | 2 lines
+ Increment msgcount in SQL for consistency
  
- updating the readme and announcement text
 ------------------------------------------------------------------------
-r1840128 | kmcgrail | 2018-09-05 12:15:57 +0000 (Wed, 05 Sep 2018) | 1 
-line
+r1899728 | hege | 2022-04-11 06:26:28 +0000 (Mon, 11 Apr 2022) | 2 lines
+ Improve test a bit more
  
- sa-update version work - bug 7006
 ------------------------------------------------------------------------
-r1840072 | billcole | 2018-09-04 22:27:55 +0000 (Tue, 04 Sep 2018) | 1 
-line
+r1899727 | hege | 2022-04-11 06:14:08 +0000 (Mon, 11 Apr 2022) | 2 lines
+ Bug 7965 - SQL storage backend miscalculates mean score for AWL
  
- Make leading space/zero for one-digit dates in mbox separator optional 
-Bug 7445
 ------------------------------------------------------------------------
-r1840053 | kmcgrail | 2018-09-04 17:32:36 +0000 (Tue, 04 Sep 2018) | 1 
-line
+r1899715 | hege | 2022-04-10 19:23:45 +0000 (Sun, 10 Apr 2022) | 2 lines
+ Properly bind token as SQL_BINARY, allowing DBD::MariaDB driver to work 
+also
+------------------------------------------------------------------------
+r1899713 | hege | 2022-04-10 19:02:14 +0000 (Sun, 10 Apr 2022) | 2 lines
+ Allow DBI:MariaDB usage
  
- Fixing the docs bug 7042
 ------------------------------------------------------------------------
-r1840050 | billcole | 2018-09-04 16:39:43 +0000 (Tue, 04 Sep 2018) | 1 
+r1899711 | billcole | 2022-04-10 18:16:37 +0000 (Sun, 10 Apr 2022) | 1 
 line
  
- document %x token foe Exim-like virtual config dirs
+ adding some distinctive strings from CAN-SPAM 'compliance' boilerplate
 ------------------------------------------------------------------------
-r1839962 | hege | 2018-09-03 13:21:42 +0000 (Mon, 03 Sep 2018) | 2 lines
+r1899707 | hege | 2022-04-10 16:05:41 +0000 (Sun, 10 Apr 2022) | 2 lines
  
- Optimize loop, run hits only once
+ Fix validuserplugin.pm load path
  
 ------------------------------------------------------------------------
-r1839883 | hege | 2018-09-02 13:50:12 +0000 (Sun, 02 Sep 2018) | 2 lines
+r1899706 | hege | 2022-04-10 16:05:14 +0000 (Sun, 10 Apr 2022) | 2 lines
  
- Fix SHA512 verification
+ Fix run_awl_sql_tests
  
 ------------------------------------------------------------------------
-r1839865 | billcole | 2018-09-02 00:44:43 +0000 (Sun, 02 Sep 2018) | 1 
-line
+r1899653 | hege | 2022-04-07 15:08:03 +0000 (Thu, 07 Apr 2022) | 2 lines
+ Bug 7969 - Parser.pm, sub finish_parsing, small code reorder
  
- Add SHA512 support to build/mkupdates/* scripts and sa-update
 ------------------------------------------------------------------------
-r1839854 | kmcgrail | 2018-09-01 21:23:41 +0000 (Sat, 01 Sep 2018) | 1 
+r1899617 | billcole | 2022-04-06 14:42:37 +0000 (Wed, 06 Apr 2022) | 1 
 line
  
- More SHA256/512issues identified
+ pegging a zero-FP rule to a higher score, remove old commented line
 ------------------------------------------------------------------------
-r1839851 | kmcgrail | 2018-09-01 21:11:42 +0000 (Sat, 01 Sep 2018) | 1 
-line
+r1899585 | hege | 2022-04-05 15:42:38 +0000 (Tue, 05 Apr 2022) | 2 lines
+ Fix UTF-16 detection
  
- preparing to release 3.4.2-pre5
 ------------------------------------------------------------------------
-r1839848 | kmcgrail | 2018-09-01 21:05:17 +0000 (Sat, 01 Sep 2018) | 1 
+r1899571 | billcole | 2022-04-04 20:50:32 +0000 (Mon, 04 Apr 2022) | 1 
 line
  
- Preparing 3.4.2-pre4
+ Fixed 'aliases' per Bug #7968
 ------------------------------------------------------------------------
-r1839835 | kmcgrail | 2018-09-01 18:03:57 +0000 (Sat, 01 Sep 2018) | 1 
-line
+r1899551 | hege | 2022-04-04 10:37:27 +0000 (Mon, 04 Apr 2022) | 2 lines
+ Add standard license boilerplate
  
- Minor MANIFEST fix
 ------------------------------------------------------------------------
-r1839834 | kmcgrail | 2018-09-01 18:01:46 +0000 (Sat, 01 Sep 2018) | 1 
-line
+r1899545 | gbechis | 2022-04-04 06:15:29 +0000 (Mon, 04 Apr 2022) | 2 
+lines
+ remove rule that depends on a non existent rule
  
- Streamlining the build process, Updating the build process for new 
-infrastructure and switching to sha256/512 - bug 7596
 ------------------------------------------------------------------------
-r1839832 | billcole | 2018-09-01 17:46:01 +0000 (Sat, 01 Sep 2018) | 1 
-line
+r1899531 | hege | 2022-04-03 09:39:09 +0000 (Sun, 03 Apr 2022) | 2 lines
+ Fix and sslify some documentation urls
  
- remove pointless and incompatible  modifier from recent patch
 ------------------------------------------------------------------------
-r1839826 | kmcgrail | 2018-09-01 14:55:44 +0000 (Sat, 01 Sep 2018) | 1 
-line
+r1899530 | hege | 2022-04-03 09:14:29 +0000 (Sun, 03 Apr 2022) | 2 lines
+ Bug 7870 - Mail::SpamAssassin::Conf "body" documentation clarification
  
- Cleanup on README file
 ------------------------------------------------------------------------
-r1839824 | kmcgrail | 2018-09-01 14:21:36 +0000 (Sat, 01 Sep 2018) | 1 
-line
+r1899529 | hege | 2022-04-03 09:03:12 +0000 (Sun, 03 Apr 2022) | 2 lines
+ Add autolearn_body to dcc/pyzor/razor rules (Bug 7904)
  
- Placeholder for Upgrade info
 ------------------------------------------------------------------------
-r1839807 | kmcgrail | 2018-09-01 05:39:30 +0000 (Sat, 01 Sep 2018) | 1 
-line
+r1899528 | hege | 2022-04-03 08:45:07 +0000 (Sun, 03 Apr 2022) | 2 lines
+ Update tlds
  
- Preparing to release 3.4.2-pre4
 ------------------------------------------------------------------------
-r1839806 | kmcgrail | 2018-09-01 05:37:42 +0000 (Sat, 01 Sep 2018) | 1 
-line
+r1899526 | hege | 2022-04-03 08:19:41 +0000 (Sun, 03 Apr 2022) | 2 lines
+ Add tflags autolearn_header/autolearn_body (Bug 7907)
  
- Fixing minor logic issue on HAS_DSA
 ------------------------------------------------------------------------
-r1839797 | billcole | 2018-08-31 23:43:25 +0000 (Fri, 31 Aug 2018) | 1 
-line
+r1899525 | hege | 2022-04-03 07:34:21 +0000 (Sun, 03 Apr 2022) | 2 lines
+ Bug 7905/7906: Rewrote autolearn logic. Meta points are now split 
+between head/body, according to how many head/body rules it depends on 
+(not recursive, just first deps are checked). If there are no head/body 
+deps, nothing is added. No discrimination of network rules anymore.
  
- skip unparseable Cyrus LMTPA over unix socket Received header 
 ------------------------------------------------------------------------
-r1839792 | billcole | 2018-08-31 22:04:32 +0000 (Fri, 31 Aug 2018) | 1 
-line
+r1899511 | hege | 2022-04-02 12:04:25 +0000 (Sat, 02 Apr 2022) | 2 lines
+ Improve TextCat. Add new utf8 lms. Add tools for maintaining languages.
  
- Actually implementing use_bayes_rules distinct from use_bayes. Bug #7110
 ------------------------------------------------------------------------
-r1839684 | kmcgrail | 2018-08-30 15:26:34 +0000 (Thu, 30 Aug 2018) | 1 
-line
+r1899507 | hege | 2022-04-02 08:04:26 +0000 (Sat, 02 Apr 2022) | 2 lines
+ Bug 7950 - sa-learn documentation broken link
  
- Fix warnings on Windows platform in 3.4 - bug 7259
 ------------------------------------------------------------------------
-r1839641 | gbechis | 2018-08-30 07:32:41 +0000 (Thu, 30 Aug 2018) | 2 
-lines
+r1899506 | hege | 2022-04-02 07:28:58 +0000 (Sat, 02 Apr 2022) | 2 lines
  
- typo in man page
+ Optimize domain_to_search_list
  
 ------------------------------------------------------------------------
-r1839639 | gbechis | 2018-08-30 07:30:54 +0000 (Thu, 30 Aug 2018) | 2 
+r1899446 | gbechis | 2022-03-31 15:57:33 +0000 (Thu, 31 Mar 2022) | 2 
 lines
  
- Phishing plugin
+ use rule only if needed plugin is loaded
  
 ------------------------------------------------------------------------
-r1839638 | gbechis | 2018-08-30 07:27:29 +0000 (Thu, 30 Aug 2018) | 6 
+r1899445 | gbechis | 2022-03-31 15:55:03 +0000 (Thu, 31 Mar 2022) | 2 
 lines
  
- Add  Mail::SpamAssassin::Plugin::Phishing
- This phishing plugin finds uris used in phishing campaigns detected by
- OpenPhish or PhishTank feeds.
- bz 7564
+ match a recurring spam pattern
  
 ------------------------------------------------------------------------
-r1839529 | kmcgrail | 2018-08-29 01:29:54 +0000 (Wed, 29 Aug 2018) | 1 
-line
+r1899407 | jhardin | 2022-03-31 01:21:17 +0000 (Thu, 31 Mar 2022) | 1 line
  
- Fixing small perlcritic issue
+ Broaden UNSUB_GOOG_FORM a bit
 ------------------------------------------------------------------------
-r1839517 | kmcgrail | 2018-08-29 00:27:22 +0000 (Wed, 29 Aug 2018) | 1 
-line
+r1899164 | hege | 2022-03-24 05:16:31 +0000 (Thu, 24 Mar 2022) | 2 lines
+ Bug 7958 - Allow '#' in paths when untainting
  
- small spelling error
 ------------------------------------------------------------------------
-r1839515 | billcole | 2018-08-28 23:55:29 +0000 (Tue, 28 Aug 2018) | 1 
-line
+r1898895 | hege | 2022-03-13 08:42:41 +0000 (Sun, 13 Mar 2022) | 4 lines
+ - Support ALL pseudoheader (has_all_header) (Bug 5582)
+ - Support tflags range (has_tflags_range)
+ - Support tflags concat (has_tflags_concat)
  
- Detect UTF-16 flavor
 ------------------------------------------------------------------------
-r1839514 | billcole | 2018-08-28 23:44:51 +0000 (Tue, 28 Aug 2018) | 1 
-line
+r1898892 | hege | 2022-03-13 07:13:13 +0000 (Sun, 13 Mar 2022) | 3 lines
+ - Header :first :last modifiers did not work at all before 
+(feature_header_first_last)
+ - Allow matching all :addr :name etc modifier results 
+(feature_header_match_many)
  
- switch default for parse_dkim_uris
 ------------------------------------------------------------------------
-r1839511 | billcole | 2018-08-28 23:12:05 +0000 (Tue, 28 Aug 2018) | 1 
-line
+r1898891 | hege | 2022-03-13 06:24:24 +0000 (Sun, 13 Mar 2022) | 2 lines
+ Not supposed to add t/header.t yet..
  
- Fixing t/util_wrap.t for new tab=>8 spaces accounting
 ------------------------------------------------------------------------
-r1839487 | billcole | 2018-08-28 17:16:00 +0000 (Tue, 28 Aug 2018) | 1 
-line
+r1898890 | hege | 2022-03-13 06:17:05 +0000 (Sun, 13 Mar 2022) | 2 lines
+ Add missing t/data/spam/unicode1
  
- Making allowance for tabs in M::SA::Util=>wrap(), tweaking default wrap 
-width
 ------------------------------------------------------------------------
-r1839410 | gbechis | 2018-08-28 07:45:52 +0000 (Tue, 28 Aug 2018) | 4 
-lines
+r1898791 | hege | 2022-03-09 14:34:25 +0000 (Wed, 09 Mar 2022) | 11 lines
+ Fix sa-compile with UTF-8 rules, in many cases rules might not hit at 
+all.
+ Perlapi says:
+ "SvPVutf8 is like SvPV, but converts sv to UTF-8 first if not already 
+UTF-8."
  
- Fix indented rules to be rescored
- Give a chance to RCVD_IN_MSPIKE rules.
- bz #6400
+ So change XS code to use SvPV, since SA body is supposed to be in bytes, 
+*duh*.
+ Add some more tests.
+ Also backport to 3.4.
  
 ------------------------------------------------------------------------
-r1839409 | gbechis | 2018-08-28 07:35:13 +0000 (Tue, 28 Aug 2018) | 2 
-lines
+r1898789 | hege | 2022-03-09 14:15:20 +0000 (Wed, 09 Mar 2022) | 2 lines
  
- Mention 'report_wrap_width' new option
+ Add some utf8 body tests
  
 ------------------------------------------------------------------------
-r1839390 | kmcgrail | 2018-08-28 02:48:28 +0000 (Tue, 28 Aug 2018) | 1 
-line
+r1898788 | hege | 2022-03-09 14:13:23 +0000 (Wed, 09 Mar 2022) | 2 lines
+ Fix debug print
  
- Adding more features to WLBLEval - Bug 7354
 ------------------------------------------------------------------------
-r1839388 | kmcgrail | 2018-08-28 02:39:26 +0000 (Tue, 28 Aug 2018) | 1 
-line
+r1898781 | hege | 2022-03-09 13:20:18 +0000 (Wed, 09 Mar 2022) | 2 lines
+ Use catdir
  
- Adding FromNameSpoof plugin - bug 7606
 ------------------------------------------------------------------------
-r1839367 | billcole | 2018-08-27 19:18:16 +0000 (Mon, 27 Aug 2018) | 1 
-line
+r1898780 | hege | 2022-03-09 13:17:24 +0000 (Wed, 09 Mar 2022) | 2 lines
+ Purge old .sawritetest files automatically
  
- Adding configurable wrap width for X-Spam-Report header. Bug #6104
 ------------------------------------------------------------------------
-r1839294 | gbechis | 2018-08-27 10:41:59 +0000 (Mon, 27 Aug 2018) | 2 
-lines
+r1898776 | hege | 2022-03-09 10:03:59 +0000 (Wed, 09 Mar 2022) | 2 lines
  
detect Sympa mailinglists, bz #7523
Bug 7645 - Wide character in print at /usr/bin/sa-compile line 433
  
 ------------------------------------------------------------------------
-r1839260 | kmcgrail | 2018-08-26 21:55:00 +0000 (Sun, 26 Aug 2018) | 1 
-line
+r1898724 | hege | 2022-03-08 07:31:44 +0000 (Tue, 08 Mar 2022) | 2 lines
+ Fix typo+https
  
- build_spamc & build_spamd are options for win32 only - bug 7376
 ------------------------------------------------------------------------
-r1839147 | kmcgrail | 2018-08-25 23:31:00 +0000 (Sat, 25 Aug 2018) | 1 
-line
+r1898688 | hege | 2022-03-07 13:42:46 +0000 (Mon, 07 Mar 2022) | 2 lines
+ Enable pyzor_fork, razor_fork by default
  
- Addig tag for LASTEXTERNALIP - Bug 7334
 ------------------------------------------------------------------------
-r1839143 | kmcgrail | 2018-08-25 23:17:51 +0000 (Sat, 25 Aug 2018) | 1 
-line
+r1898687 | hege | 2022-03-07 13:41:22 +0000 (Mon, 07 Mar 2022) | 2 lines
+ Documentation cleanups
  
- allow font names in tickmarks - bug 7312
 ------------------------------------------------------------------------
-r1839141 | kmcgrail | 2018-08-25 23:11:53 +0000 (Sat, 25 Aug 2018) | 1 
-line
+r1898684 | hege | 2022-03-07 13:20:37 +0000 (Mon, 07 Mar 2022) | 2 lines
+ Add missing t/data/spam/olevbmacro/target_uri.eml
  
- changing socket handling for spamd - bug 7274
 ------------------------------------------------------------------------
-r1839140 | kmcgrail | 2018-08-25 23:04:42 +0000 (Sat, 25 Aug 2018) | 1 
-line
+r1898682 | hege | 2022-03-07 13:19:15 +0000 (Mon, 07 Mar 2022) | 2 lines
+ Major code cleanup and logic fixes
  
- Improving razor2 test
 ------------------------------------------------------------------------
-r1839137 | kmcgrail | 2018-08-25 22:49:01 +0000 (Sat, 25 Aug 2018) | 1 
-line
+r1898679 | hege | 2022-03-07 12:47:46 +0000 (Mon, 07 Mar 2022) | 2 lines
+ Strip also non-breaking whitespace (\xA0) from HTML URIs
  
- changing make to $Config{make} for sa-compile - bug 7294
 ------------------------------------------------------------------------
-r1839132 | kmcgrail | 2018-08-25 22:35:14 +0000 (Sat, 25 Aug 2018) | 1 
-line
+r1898676 | hege | 2022-03-07 11:56:11 +0000 (Mon, 07 Mar 2022) | 2 lines
+ Test cleanup
  
- Add references to plugins - bug 7280
 ------------------------------------------------------------------------
-r1839127 | kmcgrail | 2018-08-25 22:08:33 +0000 (Sat, 25 Aug 2018) | 1 
-line
+r1898675 | hege | 2022-03-07 11:55:14 +0000 (Mon, 07 Mar 2022) | 2 lines
+ Add missing https_http_mismatch
  
- Adding information rule updates and sha1 to announcement
 ------------------------------------------------------------------------
-r1839085 | gbechis | 2018-08-25 17:20:14 +0000 (Sat, 25 Aug 2018) | 2 
-lines
+r1898674 | hege | 2022-03-07 11:54:14 +0000 (Mon, 07 Mar 2022) | 2 lines
  
- revert r1826179, fixes bz #7602
+ Fix URL whitespace parsing
  
 ------------------------------------------------------------------------
-r1839015 | billcole | 2018-08-25 05:15:19 +0000 (Sat, 25 Aug 2018) | 1 
-line
+r1898665 | hege | 2022-03-07 08:11:46 +0000 (Mon, 07 Mar 2022) | 2 lines
+ Unify dbg() usage
  
- Really skip Devel::SawAmpersand test when it's unneeded
 ------------------------------------------------------------------------
-r1839005 | kmcgrail | 2018-08-25 01:44:30 +0000 (Sat, 25 Aug 2018) | 1 
-line
+r1898654 | hege | 2022-03-06 13:42:39 +0000 (Sun, 06 Mar 2022) | 2 lines
+ Remove deprecated --auth-ident from spamd (Bug 7599)
  
- adding a description of why the change exists
 ------------------------------------------------------------------------
-r1839002 | kmcgrail | 2018-08-25 01:22:03 +0000 (Sat, 25 Aug 2018) | 1 
-line
+r1898649 | hege | 2022-03-06 11:49:43 +0000 (Sun, 06 Mar 2022) | 2 lines
+ Bug 7923 - RFE: Making HashBL email_whitelist a configurable feature
  
- Adding more cases for user_prefs.template to be found - bug 7298
 ------------------------------------------------------------------------
-r1838999 | kmcgrail | 2018-08-25 00:47:02 +0000 (Sat, 25 Aug 2018) | 1 
-line
+r1898645 | hege | 2022-03-06 10:35:45 +0000 (Sun, 06 Mar 2022) | 2 lines
+ Clarify that tag names must be alphanumeric (Bug 6162)
  
- Small fix for new6 bug - reported by ToddR, cPanel
 ------------------------------------------------------------------------
-r1838992 | kmcgrail | 2018-08-24 23:58:13 +0000 (Fri, 24 Aug 2018) | 1 
-line
+r1898622 | hege | 2022-03-05 13:51:29 +0000 (Sat, 05 Mar 2022) | 2 lines
+ Add some string/tag and uri size limits, improve uri parsing
  
- fixing Use of uninitialized value $file in File::Spec->catpath bug 7272
 ------------------------------------------------------------------------
-r1838856 | gbechis | 2018-08-24 13:39:02 +0000 (Fri, 24 Aug 2018) | 2 
-lines
+r1898621 | hege | 2022-03-05 12:54:08 +0000 (Sat, 05 Mar 2022) | 2 lines
  
- typo in optional module
+ Major code cleanups, improve parsing and matching, add basic unit test
  
 ------------------------------------------------------------------------
-r1838854 | gbechis | 2018-08-24 13:29:27 +0000 (Fri, 24 Aug 2018) | 2 
-lines
+r1898557 | hege | 2022-03-03 08:39:19 +0000 (Thu, 03 Mar 2022) | 2 lines
  
- Mention Mail::SpamAssassin::Plugin::ResourceLimit
+ Bug 7960 - PDFInfo misses valid metadata
  
 ------------------------------------------------------------------------
-r1838779 | kmcgrail | 2018-08-24 01:53:14 +0000 (Fri, 24 Aug 2018) | 1 
+r1898546 | billcole | 2022-03-03 04:25:28 +0000 (Thu, 03 Mar 2022) | 1 
 line
  
- fixing an opt not freed. bug 7509
+ Assumption about high-bit characters no longer valid. BZ#7960
 ------------------------------------------------------------------------
-r1838777 | kmcgrail | 2018-08-24 01:45:57 +0000 (Fri, 24 Aug 2018) | 1 
-line
+r1898503 | gbechis | 2022-03-01 08:44:20 +0000 (Tue, 01 Mar 2022) | 2 
+lines
+ add support for Mailgun and Mdirector esp
  
- Reverting previous comment of return - bug 7191 comment 18
 ------------------------------------------------------------------------
-r1838775 | kmcgrail | 2018-08-24 01:35:46 +0000 (Fri, 24 Aug 2018) | 1 
+r1898279 | billcole | 2022-02-21 16:41:15 +0000 (Mon, 21 Feb 2022) | 1 
 line
  
- logic switch on spamd to fix the unlimited timeout option.  bug 6748
+ no need to limit something with 0 FPs
 ------------------------------------------------------------------------
-r1838771 | kmcgrail | 2018-08-24 00:45:27 +0000 (Fri, 24 Aug 2018) | 1 
-line
+r1898241 | jhardin | 2022-02-19 23:44:44 +0000 (Sat, 19 Feb 2022) | 1 line
  
- Adding ResourceLimits.pm plugin and dependency test for BSD::Resources
+ FP avoidance tuning URI_TRY_3LD
 ------------------------------------------------------------------------
-r1838645 | billcole | 2018-08-22 15:24:51 +0000 (Wed, 22 Aug 2018) | 1 
+r1898197 | billcole | 2022-02-18 22:20:32 +0000 (Fri, 18 Feb 2022) | 1 
 line
  
- Restoring required -D flag so that the patterns & antipatterns can 
-actually work
+ I think this is abnormal, seen only in malware spam
 ------------------------------------------------------------------------
-r1838604 | kmcgrail | 2018-08-22 04:41:03 +0000 (Wed, 22 Aug 2018) | 1 
-line
+r1898196 | jhardin | 2022-02-18 22:03:15 +0000 (Fri, 18 Feb 2022) | 2 
+lines
  
- Minor version check robustness bug 7095
+ Convert lookbehind assertion to lookahead to avoid variable-length 
+issues with unicode semantics for "ss"/"st"
+ Bug#7956
 ------------------------------------------------------------------------
-r1838601 | kmcgrail | 2018-08-22 04:15:31 +0000 (Wed, 22 Aug 2018) | 1 
-line
+r1898151 | axb | 2022-02-17 12:40:26 +0000 (Thu, 17 Feb 2022) | 1 line
  
- Adding info about rules being in root to manifest
+ removed  alinto.com 
 ------------------------------------------------------------------------
-r1838598 | kmcgrail | 2018-08-22 04:06:35 +0000 (Wed, 22 Aug 2018) | 1 
-line
+r1898139 | jhardin | 2022-02-17 03:48:13 +0000 (Thu, 17 Feb 2022) | 1 line
  
- Commenting a change accidentally committed for Bug 7095
+ FP avoidance tuning URI_TRY_3LD
 ------------------------------------------------------------------------
-r1838597 | kmcgrail | 2018-08-22 03:56:32 +0000 (Wed, 22 Aug 2018) | 1 
+r1898129 | billcole | 2022-02-16 15:16:30 +0000 (Wed, 16 Feb 2022) | 1 
 line
  
- rules, rulesrc and t.rules are only in trunk now
+ Suspect URI host, maybe good TLDs in bad list
 ------------------------------------------------------------------------
-r1838596 | kmcgrail | 2018-08-22 03:55:49 +0000 (Wed, 22 Aug 2018) | 1 
+r1898109 | billcole | 2022-02-15 17:56:01 +0000 (Tue, 15 Feb 2022) | 1 
 line
  
- prepping for 3.4.2 release
+ Wellframe is a well-behaved coordinated care provider whose mail hits 
+some harsh KAM rules.
 ------------------------------------------------------------------------
-r1838594 | kmcgrail | 2018-08-22 02:27:45 +0000 (Wed, 22 Aug 2018) | 1 
+r1898106 | billcole | 2022-02-15 13:32:13 +0000 (Tue, 15 Feb 2022) | 1 
 line
  
- Removing 3 experimental/devel plugins
+ de-test a solid but rare rule
 ------------------------------------------------------------------------
-r1838591 | kmcgrail | 2018-08-21 23:53:30 +0000 (Tue, 21 Aug 2018) | 1 
-line
+r1898041 | jhardin | 2022-02-13 21:20:18 +0000 (Sun, 13 Feb 2022) | 1 line
  
- Remove pretty command line in ps so pkill can work
+ FP avoidance tuning CONTENT_AFTER_HTML
 ------------------------------------------------------------------------
-r1838588 | kmcgrail | 2018-08-21 23:22:01 +0000 (Tue, 21 Aug 2018) | 1 
-line
+r1897942 | hege | 2022-02-10 13:38:49 +0000 (Thu, 10 Feb 2022) | 2 lines
+ Test some empty body variations
+------------------------------------------------------------------------
+r1897706 | kb | 2022-02-03 02:16:28 +0000 (Thu, 03 Feb 2022) | 1 line
  
- Reminder not to leave -D
+ plaintext_body_sig_ratio: signature delimiter space optional, spammers 
+do not adhere strictly to the standard
 ------------------------------------------------------------------------
-r1838586 | billcole | 2018-08-21 21:34:03 +0000 (Tue, 21 Aug 2018) | 1 
+r1897569 | billcole | 2022-01-28 23:15:45 +0000 (Fri, 28 Jan 2022) | 1 
 line
  
- tighten up patterns in t/dnsbl.t
+ de-testing
 ------------------------------------------------------------------------
-r1838522 | gbechis | 2018-08-21 07:51:57 +0000 (Tue, 21 Aug 2018) | 2 
+r1897537 | gbechis | 2022-01-27 08:26:00 +0000 (Thu, 27 Jan 2022) | 2 
 lines
  
- Describe some of the code developed and to be released in 3.4.2
+ warning fix
  
 ------------------------------------------------------------------------
-r1838511 | billcole | 2018-08-20 23:48:03 +0000 (Mon, 20 Aug 2018) | 1 
-line
+r1897535 | gbechis | 2022-01-27 08:03:55 +0000 (Thu, 27 Jan 2022) | 2 
+lines
  
- Check for rules before using them in test
-------------------------------------------------------------------------
-r1838509 | kmcgrail | 2018-08-20 23:32:03 +0000 (Mon, 20 Aug 2018) | 1 
-line
+ Make additional olemacro download marker configurable
  
- Updated committer and pmc list
 ------------------------------------------------------------------------
-r1838499 | billcole | 2018-08-20 21:45:37 +0000 (Mon, 20 Aug 2018) | 1 
+r1897529 | billcole | 2022-01-27 03:58:29 +0000 (Thu, 27 Jan 2022) | 1 
 line
  
backport trunk sa-compile and t/sa_compile.t fixes
adding some nice tflags for subrules
 ------------------------------------------------------------------------
-r1838491 | kmcgrail | 2018-08-20 20:52:44 +0000 (Mon, 20 Aug 2018) | 1 
+r1897511 | billcole | 2022-01-26 15:48:57 +0000 (Wed, 26 Jan 2022) | 1 
 line
  
- rewrite of razor2 test and a sample email for testing
+  remove T_ from good rules
 ------------------------------------------------------------------------
-r1838489 | kmcgrail | 2018-08-20 20:52:11 +0000 (Mon, 20 Aug 2018) | 1 
+r1897510 | billcole | 2022-01-26 15:40:48 +0000 (Wed, 26 Jan 2022) | 1 
 line
  
- rewrite of razor2 test and a sample email for testing
+ Chronic spammer fingerprint
 ------------------------------------------------------------------------
-r1838485 | kmcgrail | 2018-08-20 20:07:49 +0000 (Mon, 20 Aug 2018) | 1 
-line
+r1897359 | jhardin | 2022-01-23 00:09:57 +0000 (Sun, 23 Jan 2022) | 1 line
  
- removing prototype on bgread for PerlCritic
+ Add XM_RANDOM FP exclusion for "Qi Mail Connector"
 ------------------------------------------------------------------------
-r1838443 | kmcgrail | 2018-08-20 17:39:25 +0000 (Mon, 20 Aug 2018) | 1 
-line
+r1897186 | jhardin | 2022-01-19 03:09:13 +0000 (Wed, 19 Jan 2022) | 1 line
  
- moved rules and rules-extra to trunk-only for 3.4 and continue 
-streamlining build process
+ Expose subrule as rule for scoring and publication
 ------------------------------------------------------------------------
-r1838429 | kmcgrail | 2018-08-20 14:20:05 +0000 (Mon, 20 Aug 2018) | 1 
-line
+r1897134 | jhardin | 2022-01-16 19:19:38 +0000 (Sun, 16 Jan 2022) | 1 line
  
- framework for 3.4.2 announcement
+ Add subrule for "unsubscribe via this Google Docs form:" for evaluation
 ------------------------------------------------------------------------
-r1838390 | kmcgrail | 2018-08-19 16:14:03 +0000 (Sun, 19 Aug 2018) | 1 
-line
+r1897097 | jhardin | 2022-01-15 18:35:44 +0000 (Sat, 15 Jan 2022) | 1 line
  
- Fixing the MANIFEST
+ Recognize "shtml" as HTML file attachment extension
 ------------------------------------------------------------------------
-r1838387 | kmcgrail | 2018-08-19 16:13:01 +0000 (Sun, 19 Aug 2018) | 1 
-line
+r1896876 | gbechis | 2022-01-10 09:07:45 +0000 (Mon, 10 Jan 2022) | 2 
+lines
+ use From:domain if EnvelopeFrom:host cannot be found
  
-  Bug 7591 not using this faster untaint
 ------------------------------------------------------------------------
-r1838374 | gbechis | 2018-08-19 10:10:16 +0000 (Sun, 19 Aug 2018) | 2 
+r1896875 | gbechis | 2022-01-10 09:06:48 +0000 (Mon, 10 Jan 2022) | 2 
 lines
  
- refactor some "require" code
+ make the test fail on spf as well
  
 ------------------------------------------------------------------------
-r1838365 | gbechis | 2018-08-19 08:54:59 +0000 (Sun, 19 Aug 2018) | 2 
+r1896791 | gbechis | 2022-01-07 12:00:49 +0000 (Fri, 07 Jan 2022) | 2 
 lines
  
- skip tests if GeoIP is installed but there are no databases available
+ do not try to cache urls longer then permitted
  
 ------------------------------------------------------------------------
-r1838364 | gbechis | 2018-08-19 08:52:11 +0000 (Sun, 19 Aug 2018) | 2 
+r1896786 | gbechis | 2022-01-07 10:48:55 +0000 (Fri, 07 Jan 2022) | 2 
 lines
  
better detection of GeoIP installed modules
Add a sub to check for exploitable documents
  
 ------------------------------------------------------------------------
-r1837877 | gbechis | 2018-08-11 18:33:18 +0000 (Sat, 11 Aug 2018) | 2 
-lines
+r1896315 | gbechis | 2021-12-23 13:43:24 +0000 (Thu, 23 Dec 2021) | 1 line
  
  typo
+------------------------------------------------------------------------
+r1896197 | billcole | 2021-12-20 19:09:36 +0000 (Mon, 20 Dec 2021) | 1 
+line
+ Interesting hashbusting trick...
+------------------------------------------------------------------------
+r1896096 | pds | 2021-12-17 10:57:38 +0000 (Fri, 17 Dec 2021) | 1 line
  
+ subrule typo
 ------------------------------------------------------------------------
-r1837876 | gbechis | 2018-08-11 18:23:51 +0000 (Sat, 11 Aug 2018) | 3 
+r1896078 | gbechis | 2021-12-16 22:28:45 +0000 (Thu, 16 Dec 2021) | 2 
 lines
  
- close file descriptors when they are no more needed
- probably only partial fix for #7587
+ return undef if the EnvelopeFrom:host cannot be found
  
 ------------------------------------------------------------------------
-r1837466 | gbechis | 2018-08-05 13:39:41 +0000 (Sun, 05 Aug 2018) | 4 
-lines
+r1896052 | pds | 2021-12-16 13:06:13 +0000 (Thu, 16 Dec 2021) | 1 line
  
- Starting from 04/01/2018 GeoLite Legacy databases have been 
-discontinued.      
- Add optional support to new Maxmind database type (GeoIP2).
- fixes bz #7529
+ cPanel metas
+------------------------------------------------------------------------
+r1895737 | kb | 2021-12-10 01:27:46 +0000 (Fri, 10 Dec 2021) | 1 line
  
+ plaintext_body_sig_ratio: optional carriage return in line breaks
 ------------------------------------------------------------------------
-r1837465 | gbechis | 2018-08-05 13:38:31 +0000 (Sun, 05 Aug 2018) | 8 
+r1895485 | gbechis | 2021-12-02 08:39:35 +0000 (Thu, 02 Dec 2021) | 3 
 lines
  
- Starting from 04/01/2018 GeoLite Legacy databases have been 
-discontinued.      
- Add optional support to new Maxmind database type (GeoIP2).
- In addiction to that add support also to IP::Country::DB_File database;
- IP::Country::DB_File database is created from official 
-Ripe/Arin/Afrinic/... 
- data, it's faster than IP::Country::Fast on updating a database and it 
-supports ipv6.
+ Add a sub check_olertfobject to check if a document has
+ a potencially malicious rtf document embedded
  
- fixes bz #7529
+------------------------------------------------------------------------
+r1895479 | billcole | 2021-12-01 23:00:11 +0000 (Wed, 01 Dec 2021) | 1 
+line
  
+ typo
 ------------------------------------------------------------------------
-r1836883 | gbechis | 2018-07-28 09:38:39 +0000 (Sat, 28 Jul 2018) | 3 
+r1895473 | gbechis | 2021-12-01 18:08:14 +0000 (Wed, 01 Dec 2021) | 2 
 lines
  
- Add possibility to match multiple rules
- for a single uri, bz #7595
+ Match every file under xl/embeddings directory
  
 ------------------------------------------------------------------------
-r1836855 | gbechis | 2018-07-27 18:03:13 +0000 (Fri, 27 Jul 2018) | 2 
+r1895420 | gbechis | 2021-11-30 07:22:58 +0000 (Tue, 30 Nov 2021) | 2 
 lines
  
- improve tests
+ Don't trim spf domain in 'mfrom' scope
  
 ------------------------------------------------------------------------
-r1836516 | gbechis | 2018-07-23 21:23:37 +0000 (Mon, 23 Jul 2018) | 2 
+r1895389 | gbechis | 2021-11-28 10:46:36 +0000 (Sun, 28 Nov 2021) | 2 
 lines
  
- Add Mail::SpamAssassin::Plugin::URILocalBL regression tests
+ Fix domain source of SPF with `mfrom` scope
  
 ------------------------------------------------------------------------
-r1836275 | gbechis | 2018-07-19 14:19:48 +0000 (Thu, 19 Jul 2018) | 2 
+r1895271 | gbechis | 2021-11-23 15:47:31 +0000 (Tue, 23 Nov 2021) | 2 
 lines
  
- Mail::SpamAssassin::Plugin::RelayCountry regression tests
+ improve logging and add regression tests for 
+check_olemacro_redirect_uri()
  
 ------------------------------------------------------------------------
-r1835030 | gbechis | 2018-07-03 22:20:19 +0000 (Tue, 03 Jul 2018) | 2 
-lines
+r1895162 | jhardin | 2021-11-18 21:45:20 +0000 (Thu, 18 Nov 2021) | 1 line
  
- make it work even if SA is not installed
+ Also check envelope from header and HELO to try to identify emails from 
+Shopify
+------------------------------------------------------------------------
+r1895057 | jhardin | 2021-11-15 15:48:32 +0000 (Mon, 15 Nov 2021) | 1 line
  
+ FP Avoidance tuning
 ------------------------------------------------------------------------
-r1834725 | gbechis | 2018-06-30 07:01:43 +0000 (Sat, 30 Jun 2018) | 2 
+r1894685 | gbechis | 2021-11-02 14:29:25 +0000 (Tue, 02 Nov 2021) | 3 
 lines
  
- typo in man page
+ recognize Arc-Authentication-Results,
+ first step in supporting Arc headers
  
 ------------------------------------------------------------------------
-r1834723 | gbechis | 2018-06-30 06:37:15 +0000 (Sat, 30 Jun 2018) | 3 
-lines
+r1894312 | hege | 2021-10-17 10:23:49 +0000 (Sun, 17 Oct 2021) | 2 lines
  
- correct syntax for GRANT with PostgreSQL
- bz 7281
+ Lower required spam count 100k -> 80k
  
 ------------------------------------------------------------------------
-r1834722 | gbechis | 2018-06-30 06:12:21 +0000 (Sat, 30 Jun 2018) | 5 
-lines
+r1894308 | hege | 2021-10-17 07:17:32 +0000 (Sun, 17 Oct 2021) | 2 lines
  
- remove an extra blank line put on the MIME-parts
- array. That way the resultant email analized
- by SA was a bit different from the original one.
- bz 6708
+ Bug 7931 - Undefined subroutine &Scalar::Util::tainted
  
 ------------------------------------------------------------------------
-r1834452 | billcole | 2018-06-26 17:37:23 +0000 (Tue, 26 Jun 2018) | 1 
-line
+r1894307 | hege | 2021-10-17 07:11:26 +0000 (Sun, 17 Oct 2021) | 2 lines
+ Remove Bug 7842 testing leftovers
  
- Test for bug 7591
 ------------------------------------------------------------------------
-r1834327 | billcole | 2018-06-25 13:34:44 +0000 (Mon, 25 Jun 2018) | 1 
-line
+r1893711 | gbechis | 2021-09-28 16:22:20 +0000 (Tue, 28 Sep 2021) | 2 
+lines
+ fix pyzor tests by adding an updated spample email
  
- REALLY revert whitewash fix of t/idn_dots.t
 ------------------------------------------------------------------------
-r1834325 | billcole | 2018-06-25 13:30:17 +0000 (Mon, 25 Jun 2018) | 1 
+r1893694 | billcole | 2021-09-27 15:51:34 +0000 (Mon, 27 Sep 2021) | 1 
 line
  
- Revert whitewash fix of t/idn_dots.t
+ spam reported from whitelisted domain, BZ#7930
 ------------------------------------------------------------------------
-r1834218 | billcole | 2018-06-23 17:21:42 +0000 (Sat, 23 Jun 2018) | 1 
-line
+r1893631 | jhardin | 2021-09-25 22:03:40 +0000 (Sat, 25 Sep 2021) | 1 line
  
- add 'use utf8' for older Perl
+ FP avoidance tuning FSL_BULK_SIG
 ------------------------------------------------------------------------
-r1834151 | kmcgrail | 2018-06-22 18:09:19 +0000 (Fri, 22 Jun 2018) | 1 
+r1893523 | billcole | 2021-09-22 21:11:13 +0000 (Wed, 22 Sep 2021) | 1 
 line
  
- Working on idn_dots.t test failures for RC4
+ Bug 7913: correct description of SUBJECT_NEEDS_ENCODING
 ------------------------------------------------------------------------
-r1833929 | gbechis | 2018-06-20 17:16:33 +0000 (Wed, 20 Jun 2018) | 3 
-lines
- silence a warning if GeoIP v6 database is not installed
- but a v6 address is on relay headers
+r1893522 | billcole | 2021-09-22 20:49:07 +0000 (Wed, 22 Sep 2021) | 1 
+line
  
+ Bug  7921
 ------------------------------------------------------------------------
-r1833660 | gbechis | 2018-06-17 09:41:02 +0000 (Sun, 17 Jun 2018) | 11 
-lines
- partial fix for bz 7529
- starting from 04/01/2018, Geolite legacy databases has been
- discontinued and they will be no more updates.
- Add a "country_db_type" option that will let the user choose
- between GeoIP and IP::Country::Fast databases.
- By default GeoIP is enabled and there is still a fallback
- on IP::Country::Fast as in previuos implementation.
- IP::Country::Fast has no ipv6 support, so a better api
- should be adopted sooner or later.
+r1893514 | mmartinec | 2021-09-22 14:59:53 +0000 (Wed, 22 Sep 2021) | 1 
+line
  
+ Plugin/PDFInfo.pm: fix the "no such facility warn", triping the t/debug.t
 ------------------------------------------------------------------------
-r1833617 | billcole | 2018-06-15 17:33:15 +0000 (Fri, 15 Jun 2018) | 1 
+r1893513 | mmartinec | 2021-09-22 14:43:28 +0000 (Wed, 22 Sep 2021) | 1 
 line
  
- Reverting prematurely-committed changes
+ t/all_modules.t: patterns must use distinct names, otherwise the report 
+is wrong
 ------------------------------------------------------------------------
-r1833615 | billcole | 2018-06-15 17:23:05 +0000 (Fri, 15 Jun 2018) | 1 
+r1893496 | mmartinec | 2021-09-21 12:35:10 +0000 (Tue, 21 Sep 2021) | 1 
 line
  
- Corrected link to Pyzor documentation site, replacing OTHER dead SF link.
+ Documentation mistake in Conf.pm
 ------------------------------------------------------------------------
-r1832678 | gbechis | 2018-06-01 11:15:23 +0000 (Fri, 01 Jun 2018) | 2 
+r1892962 | gbechis | 2021-09-06 07:16:18 +0000 (Mon, 06 Sep 2021) | 2 
 lines
  
fix custom headers length, fix another fp via Google Groups
Check for url shorteners in webforms
  
 ------------------------------------------------------------------------
-r1831955 | gbechis | 2018-05-21 06:24:55 +0000 (Mon, 21 May 2018) | 2 
+r1892749 | gbechis | 2021-08-31 07:31:43 +0000 (Tue, 31 Aug 2021) | 2 
 lines
  
- more generic regexp to match ipv6
+ add a sub has_short_url to differentiate from unofficial plugin
  
 ------------------------------------------------------------------------
-r1831837 | gbechis | 2018-05-18 09:04:10 +0000 (Fri, 18 May 2018) | 2 
+r1892748 | gbechis | 2021-08-31 06:47:18 +0000 (Tue, 31 Aug 2021) | 3 
 lines
  
- Unbreak FORGED_GMAIL_RCVD
+ read URIs from pdf files and check them against dnsbl
+ bz #7579
  
 ------------------------------------------------------------------------
-r1831826 | gbechis | 2018-05-18 07:13:02 +0000 (Fri, 18 May 2018) | 2 
+r1892724 | gbechis | 2021-08-30 09:29:01 +0000 (Mon, 30 Aug 2021) | 2 
 lines
  
- Fix another fp on FORGED_GMAIL_RCVD rule
+ reduce some fp
  
 ------------------------------------------------------------------------
-r1831443 | gbechis | 2018-05-11 19:44:30 +0000 (Fri, 11 May 2018) | 2 
+r1892560 | gbechis | 2021-08-24 07:46:25 +0000 (Tue, 24 Aug 2021) | 2 
 lines
  
fix fp for FORGED_GMAIL_RCVD rule
add a debug message
  
 ------------------------------------------------------------------------
-r1831329 | billcole | 2018-05-10 12:08:55 +0000 (Thu, 10 May 2018) | 1 
-line
+r1892554 | jhardin | 2021-08-24 01:24:00 +0000 (Tue, 24 Aug 2021) | 1 line
  
- revert r1823175
+ Check whether <font size="-1"> is worthwhile.
 ------------------------------------------------------------------------
-r1831273 | billcole | 2018-05-09 17:37:07 +0000 (Wed, 09 May 2018) | 1 
-line
+r1892540 | hege | 2021-08-23 08:49:51 +0000 (Mon, 23 Aug 2021) | 2 lines
+ More parameter sanitatation
  
- Improve spamd PID detection with a fixed pidfile
 ------------------------------------------------------------------------
-r1831272 | billcole | 2018-05-09 17:35:07 +0000 (Wed, 09 May 2018) | 1 
-line
+r1892529 | jhardin | 2021-08-22 17:15:24 +0000 (Sun, 22 Aug 2021) | 1 line
  
- Decouple mass-check from "base" perl
+ More low-contrast tuning
 ------------------------------------------------------------------------
-r1831073 | gbechis | 2018-05-07 06:37:50 +0000 (Mon, 07 May 2018) | 3 
-lines
+r1892498 | jhardin | 2021-08-21 17:52:32 +0000 (Sat, 21 Aug 2021) | 1 line
  
- Enforce a C locale when logging to stder
- bz #7305
+ FP Avoidance tuning
+------------------------------------------------------------------------
+r1892485 | jhardin | 2021-08-21 02:50:31 +0000 (Sat, 21 Aug 2021) | 1 line
  
+ Recognize font tag with negative size as tiny. Lots of low-contrast ham 
+in the masscheck corpora now, retire some poor metas and add some new 
+ones.
 ------------------------------------------------------------------------
-r1829671 | gbechis | 2018-04-20 17:45:03 +0000 (Fri, 20 Apr 2018) | 2 
+r1892404 | gbechis | 2021-08-17 22:27:15 +0000 (Tue, 17 Aug 2021) | 2 
 lines
  
- Test spamc also with --option=value case
+ Extract uris from Office files, uris can then be accessed by URIDNSBL 
+and other plugins
  
 ------------------------------------------------------------------------
-r1829628 | gbechis | 2018-04-20 06:48:21 +0000 (Fri, 20 Apr 2018) | 3 
+r1892255 | gbechis | 2021-08-12 06:26:27 +0000 (Thu, 12 Aug 2021) | 2 
 lines
  
- too much free(3) will kill --reporttype=option handling
- problem spotted by Reio Remma, thanks
+ make the check work even if Dkim is not available
  
 ------------------------------------------------------------------------
-r1829033 | gbechis | 2018-04-13 06:45:35 +0000 (Fri, 13 Apr 2018) | 5 
+r1892254 | gbechis | 2021-08-12 06:25:20 +0000 (Thu, 12 Aug 2021) | 2 
 lines
  
- Add an option to score uris per continent.
- Possible continent codes are:
- af, as, eu, na, oc, sa for Africa, Asia, Europe, North America, 
- Oceania  and South America.
+ fix Dmarc check with new Mail::DMARC versions
  
 ------------------------------------------------------------------------
-r1828218 | kmcgrail | 2018-04-03 11:28:11 +0000 (Tue, 03 Apr 2018) | 1 
-line
+r1892125 | jhardin | 2021-08-09 03:40:08 +0000 (Mon, 09 Aug 2021) | 1 line
  
- Adding Manifest items fo3 3.42
+ FP Avoidance tuning
 ------------------------------------------------------------------------
-r1826916 | billcole | 2018-03-16 03:15:19 +0000 (Fri, 16 Mar 2018) | 1 
-line
+r1892087 | jhardin | 2021-08-07 17:49:13 +0000 (Sat, 07 Aug 2021) | 1 line
  
- added optional support for SHA256 in addition to or instead of SHA1 
-validation
+ More image hosting sites being abused by spammers
 ------------------------------------------------------------------------
-r1826822 | gbechis | 2018-03-15 14:27:09 +0000 (Thu, 15 Mar 2018) | 2 
-lines
+r1892060 | hege | 2021-08-07 09:05:03 +0000 (Sat, 07 Aug 2021) | 2 lines
  
- fix for perl older than 5.24
+ Bug 7919, fix some more if-if-else bugs
  
 ------------------------------------------------------------------------
-r1826771 | gbechis | 2018-03-15 07:33:00 +0000 (Thu, 15 Mar 2018) | 4 
-lines
- If there are rules present in score but not in .cf files a warning is 
-printed,
- shut up the warning.
- bz 7535
+r1892029 | jhardin | 2021-08-06 02:05:38 +0000 (Fri, 06 Aug 2021) | 1 line
  
+ Add webp image format, it's starting to show up. Add more free image 
+hosting sites. More new-product spam tuning.
 ------------------------------------------------------------------------
-r1826742 | gbechis | 2018-03-14 17:36:30 +0000 (Wed, 14 Mar 2018) | 3 
-lines
+r1892008 | hege | 2021-08-04 06:47:35 +0000 (Wed, 04 Aug 2021) | 2 lines
  
- detect more http[s] url mismatches
- bz 6977
+ Bug 7917, fix bad if-if-else
  
 ------------------------------------------------------------------------
-r1826740 | gbechis | 2018-03-14 17:26:02 +0000 (Wed, 14 Mar 2018) | 2 
-lines
+r1892003 | jhardin | 2021-08-04 03:06:12 +0000 (Wed, 04 Aug 2021) | 1 line
  
- fix utf8 mode
-------------------------------------------------------------------------
-r1826582 | billcole | 2018-03-12 17:49:59 +0000 (Mon, 12 Mar 2018) | 1 
-line
- Update documentation of 'eval' rule method source, sanity-check method 
-calls. Fixes Bug #7558
-------------------------------------------------------------------------
-r1826356 | billcole | 2018-03-09 16:02:43 +0000 (Fri, 09 Mar 2018) | 1 
-line
- Partial fix for bug 7558
+ More image hosting sites being abused by spammers
 ------------------------------------------------------------------------
-r1826202 | gbechis | 2018-03-08 10:48:04 +0000 (Thu, 08 Mar 2018) | 3 
+r1891997 | gbechis | 2021-08-03 21:00:26 +0000 (Tue, 03 Aug 2021) | 2 
 lines
  
- add homedir parameter in dccproc call
- RedHat bz 1532139 
+ move sub has_olemacro_redirect_uri to the correct place
  
 ------------------------------------------------------------------------
-r1826187 | gbechis | 2018-03-08 08:17:53 +0000 (Thu, 08 Mar 2018) | 3 
-lines
- fix utf8 decoding in some corner cases
- bz 7520
+r1891986 | gbechis | 2021-08-03 16:54:30 +0000 (Tue, 03 Aug 2021) | 1 line
  
+ Add a sub 'feature' for new OLEMacro redirect_uri sub
 ------------------------------------------------------------------------
-r1826179 | billcole | 2018-03-08 06:41:57 +0000 (Thu, 08 Mar 2018) | 1 
-line
+r1891977 | hege | 2021-08-03 09:35:51 +0000 (Tue, 03 Aug 2021) | 2 lines
  
- Fix for Bug #7557
-------------------------------------------------------------------------
-r1826177 | billcole | 2018-03-08 05:33:13 +0000 (Thu, 08 Mar 2018) | 1 
-line
+ Lint rule updates with 3.4.4 too, instead of just trunk
  
- Fix for bug #7556
 ------------------------------------------------------------------------
-r1825725 | gbechis | 2018-03-02 13:57:33 +0000 (Fri, 02 Mar 2018) | 2 
+r1891970 | gbechis | 2021-08-03 06:44:16 +0000 (Tue, 03 Aug 2021) | 3 
 lines
  
- Add HashBL (Email Blocklist (EBL), http://msbl.org/ebl.html) plugin, bz 
-#7548
+ Add a new "check_olemacro_redirect_uri" sub that checks
+ for Office files that redirects to potentially malicious uris
  
 ------------------------------------------------------------------------
-r1825185 | gbechis | 2018-02-24 00:37:46 +0000 (Sat, 24 Feb 2018) | 4 
-lines
- As per rfc 5322 the time zone is a required field,
- so a date without time zone should be considered as invalid
- bz #6894
+r1891951 | jhardin | 2021-08-01 20:18:07 +0000 (Sun, 01 Aug 2021) | 1 line
  
+ __URI_LONG_REPEAT hit on shorter repeat host+domain parts, spammers are 
+using shorter ones now
 ------------------------------------------------------------------------
-r1825177 | gbechis | 2018-02-23 22:50:32 +0000 (Fri, 23 Feb 2018) | 3 
+r1891877 | gbechis | 2021-07-29 17:15:37 +0000 (Thu, 29 Jul 2021) | 2 
 lines
  
- document when --mbox or --mbx parameters are needed
- bz #6857
+ unbreak linter on older version
  
 ------------------------------------------------------------------------
-r1825175 | gbechis | 2018-02-23 22:44:45 +0000 (Fri, 23 Feb 2018) | 4 
+r1891861 | gbechis | 2021-07-28 19:28:23 +0000 (Wed, 28 Jul 2021) | 2 
 lines
  
- In OpenBSD /usr/sbin/sysctl is a symlink to /sbin/sysctl
- fix path, no functional change
- bz #7545
+ typo: alias -> aliases
  
 ------------------------------------------------------------------------
-r1825157 | gbechis | 2018-02-23 18:25:25 +0000 (Fri, 23 Feb 2018) | 5 
-lines
- Change a couple of die calls into warnings,
- this way  pyzor throws a python error, 
- all other async lookups are not aborted.
- bz #7026
+r1891833 | jhardin | 2021-07-27 14:56:42 +0000 (Tue, 27 Jul 2021) | 1 line
  
+ FP Avoidance tuning
 ------------------------------------------------------------------------
-r1825154 | gbechis | 2018-02-23 18:17:29 +0000 (Fri, 23 Feb 2018) | 3 
+r1891820 | gbechis | 2021-07-27 07:05:54 +0000 (Tue, 27 Jul 2021) | 2 
 lines
  
- check for freemail for all emails in a Reply-To header
- bz #6664
+ Add [welcome,block]list_from_dkim and [welcome,block]list_from_uri_host
  
 ------------------------------------------------------------------------
-r1825032 | gbechis | 2018-02-22 08:20:37 +0000 (Thu, 22 Feb 2018) | 3 
-lines
- Check if $socket is defined and print error accordingly
- bz 7380
+r1891798 | jhardin | 2021-07-26 00:40:37 +0000 (Mon, 26 Jul 2021) | 1 line
  
+ More "new product" spam tuning, including more hosted image sites; 
+convert meta dependency to subrule; adjust SUBJ_BRKN_WORDNUMS
 ------------------------------------------------------------------------
-r1825018 | billcole | 2018-02-21 23:46:08 +0000 (Wed, 21 Feb 2018) | 1 
-line
+r1891797 | jhardin | 2021-07-26 00:38:12 +0000 (Mon, 26 Jul 2021) | 1 line
  
- Group switching code for bugs #7554 and #7555
+ Split FORGED_RELAY_MUA_TO_MX to subrule for metas and scored rule; if 
+only scored rule behaves too poorly to publish, the metas break
 ------------------------------------------------------------------------
-r1824931 | gbechis | 2018-02-21 07:33:02 +0000 (Wed, 21 Feb 2018) | 2 
-lines
- Add an example of a rule that matches an ASN, bz 6929
+r1891616 | jhardin | 2021-07-17 17:18:59 +0000 (Sat, 17 Jul 2021) | 1 line
  
+ subrule performance pretty good, expose scored FACEBOOK_IMG_NOT_RCVD_FB 
+with some FP Avoidance exclusions
 ------------------------------------------------------------------------
-r1824688 | gbechis | 2018-02-18 18:35:40 +0000 (Sun, 18 Feb 2018) | 2 
-lines
- fix all pod errors spotted in bz 7168 and many more
+r1891602 | jhardin | 2021-07-17 02:05:24 +0000 (Sat, 17 Jul 2021) | 1 line
  
+ More new-product spammer tuning
 ------------------------------------------------------------------------
-r1824577 | gbechis | 2018-02-17 09:47:43 +0000 (Sat, 17 Feb 2018) | 2 
+r1891584 | gbechis | 2021-07-16 12:51:44 +0000 (Fri, 16 Jul 2021) | 2 
 lines
  
- Fix some regression tests on OpenBSD, bz 7499
+ check for an undefined value
  
 ------------------------------------------------------------------------
-r1823276 | kmcgrail | 2018-02-06 06:05:37 +0000 (Tue, 06 Feb 2018) | 1 
-line
+r1891560 | jhardin | 2021-07-15 02:52:36 +0000 (Thu, 15 Jul 2021) | 1 line
  
- Bug 7418 - sa-update change to handle cross platform newline better
+ more new-product spam tuning
 ------------------------------------------------------------------------
-r1823274 | kmcgrail | 2018-02-06 05:10:42 +0000 (Tue, 06 Feb 2018) | 1 
-line
+r1891460 | jhardin | 2021-07-11 21:28:27 +0000 (Sun, 11 Jul 2021) | 1 line
  
- Bug 7496 - speed up startup code
+ Add mime type subrules that may help detect Zloader
 ------------------------------------------------------------------------
-r1823205 | kmcgrail | 2018-02-05 16:13:03 +0000 (Mon, 05 Feb 2018) | 1 
-line
+r1891436 | jhardin | 2021-07-10 19:25:00 +0000 (Sat, 10 Jul 2021) | 1 line
  
- Clean-up of unmaintained tools and files that are only maintained in 
-trunk - see trunk-only/
+ More new product spammer tuning
 ------------------------------------------------------------------------
-r1823175 | kmcgrail | 2018-02-05 14:10:22 +0000 (Mon, 05 Feb 2018) | 1 
-line
+r1891390 | jhardin | 2021-07-09 03:21:32 +0000 (Fri, 09 Jul 2021) | 1 line
  
- Bug 7492 - switch from use vars to our cleanup
+ Push TAGSTAT_IMG_NOT_RCVD_TGST. More new-product-spam tuning.
 ------------------------------------------------------------------------
-r1823171 | davej | 2018-02-05 13:34:29 +0000 (Mon, 05 Feb 2018) | 1 line
+r1891371 | jhardin | 2021-07-08 01:13:03 +0000 (Thu, 08 Jul 2021) | 1 line
  
- Bug 7417
+ Add tagstat.com image hosting. More product spam tuning.
 ------------------------------------------------------------------------
-r1823142 | kmcgrail | 2018-02-05 09:10:12 +0000 (Mon, 05 Feb 2018) | 1 
-line
+r1891340 | pds | 2021-07-07 08:10:26 +0000 (Wed, 07 Jul 2021) | 1 line
  
- Bug 7491 switch test framework to Test::More
+ FP tweak
 ------------------------------------------------------------------------
-r1823126 | kmcgrail | 2018-02-05 06:20:06 +0000 (Mon, 05 Feb 2018) | 1 
-line
+r1891288 | jhardin | 2021-07-05 21:32:05 +0000 (Mon, 05 Jul 2021) | 1 line
  
- Bug 7481 - Adding build time specification of re2c binary
+ Add Tumblr-image-not-from-tumblr rule, spammers using tumblr-hosted 
+images. Fix copy-paste error in HOSTED_IMG_MULTI. minor rules and score 
+tuning.
 ------------------------------------------------------------------------
-r1822650 | davej | 2018-01-30 14:19:37 +0000 (Tue, 30 Jan 2018) | 1 line
+r1891283 | hege | 2021-07-05 12:47:06 +0000 (Mon, 05 Jul 2021) | 2 lines
  
- Bug 6222
-------------------------------------------------------------------------
-r1822649 | davej | 2018-01-30 14:17:16 +0000 (Tue, 30 Jan 2018) | 1 line
+ Sanitize parameters
  
- Bug 7540
 ------------------------------------------------------------------------
-r1822483 | davej | 2018-01-28 22:40:16 +0000 (Sun, 28 Jan 2018) | 1 line
+r1891234 | jhardin | 2021-07-03 17:41:58 +0000 (Sat, 03 Jul 2021) | 1 line
  
- Bug 7534
+ adding/tunning Alibaba spammer rules
 ------------------------------------------------------------------------
-r1822467 | davej | 2018-01-28 16:03:13 +0000 (Sun, 28 Jan 2018) | 1 line
+r1891186 | jhardin | 2021-07-01 19:57:35 +0000 (Thu, 01 Jul 2021) | 1 line
  
- Bug 6946.
+ Push publication of a rule
 ------------------------------------------------------------------------
-r1821749 | davej | 2018-01-20 15:26:02 +0000 (Sat, 20 Jan 2018) | 1 line
+r1891162 | jhardin | 2021-06-30 15:02:14 +0000 (Wed, 30 Jun 2021) | 1 line
  
- Bug 6946
+ FP avoidance tuning
 ------------------------------------------------------------------------
-r1819502 | davej | 2017-12-29 18:37:34 +0000 (Fri, 29 Dec 2017) | 1 line
+r1891151 | jhardin | 2021-06-30 02:59:21 +0000 (Wed, 30 Jun 2021) | 1 line
  
- Bug 6420
+ Revive GB's __LINKED_IMG_NOT_RCVD_LINK with new URI pattern, in active 
+use; freshen some stale rules.
 ------------------------------------------------------------------------
-r1819497 | kmcgrail | 2017-12-29 15:20:04 +0000 (Fri, 29 Dec 2017) | 1 
-line
+r1891047 | pds | 2021-06-25 21:28:43 +0000 (Fri, 25 Jun 2021) | 1 line
  
- Bug 7525 - missing includes declarations in spamc
+ Adjust to use meta
 ------------------------------------------------------------------------
-r1819449 | kmcgrail | 2017-12-28 23:14:24 +0000 (Thu, 28 Dec 2017) | 1 
-line
+r1891034 | jhardin | 2021-06-25 03:05:08 +0000 (Fri, 25 Jun 2021) | 1 line
  
- bug 7524 logic patch for getoptlong issues in spamc
+ FP Avoidance tuning
 ------------------------------------------------------------------------
-r1819447 | kmcgrail | 2017-12-28 22:49:03 +0000 (Thu, 28 Dec 2017) | 1 
-line
+r1890951 | gbechis | 2021-06-21 21:21:58 +0000 (Mon, 21 Jun 2021) | 1 line
  
- Bug 6970 - adding t.co url shortener
+ Mail::DMARC::PurePerl is needed for Dmarc plugin to work
 ------------------------------------------------------------------------
-r1819442 | kmcgrail | 2017-12-28 22:20:16 +0000 (Thu, 28 Dec 2017) | 1 
-line
+r1890950 | gbechis | 2021-06-21 21:17:48 +0000 (Mon, 21 Jun 2021) | 2 
+lines
  
- bug 7524 - opt cant be freed here or getoptlong fails
-------------------------------------------------------------------------
-r1816710 | kmcgrail | 2017-11-30 12:46:21 +0000 (Thu, 30 Nov 2017) | 1 
-line
+ missing file in MANIFEST
  
- Bug 7509 - free for spamc opt
 ------------------------------------------------------------------------
-r1815854 | jhardin | 2017-11-20 20:54:17 +0000 (Mon, 20 Nov 2017) | 1 line
+r1890848 | jhardin | 2021-06-17 00:47:51 +0000 (Thu, 17 Jun 2021) | 1 line
  
- Bug 7437 - fix issues with parsing a message having an unclosed HTML 
-<style> and <script> tag (e.g. due to spamc size limits)
+ Add subrule for eval, may help reduce FPs
 ------------------------------------------------------------------------
-r1815828 | jhardin | 2017-11-20 18:21:15 +0000 (Mon, 20 Nov 2017) | 1 line
+r1890825 | gbechis | 2021-06-15 22:39:15 +0000 (Tue, 15 Jun 2021) | 2 
+lines
+ man page format fixes
  
- Bug 7437 - fix issues with parsing a message having an unclosed HTML 
-<style> tag (e.g. due to spamc size limits)
 ------------------------------------------------------------------------
-r1815773 | billcole | 2017-11-20 05:09:20 +0000 (Mon, 20 Nov 2017) | 1 
-line
+r1890811 | gbechis | 2021-06-15 15:20:54 +0000 (Tue, 15 Jun 2021) | 2 
+lines
+ add a Dmarc.pm plugin to check for DMARC compliance
  
- Prevent BodyRuleBaseExtractor from orphaning files in sa-compile runs
 ------------------------------------------------------------------------
-r1814251 | kmcgrail | 2017-11-04 04:13:10 +0000 (Sat, 04 Nov 2017) | 1 
-line
+r1890810 | gbechis | 2021-06-15 12:56:13 +0000 (Tue, 15 Jun 2021) | 2 
+lines
+ allow needed dns queries
  
- Remove META.yml file from MANIFEST. It is added with make dist 
-automatically.  Added .gitignore and build/pga dir to MANIFEST.SKIP.  
-Removed META.yml from svn, it is created from make dist.  Requiring 
-MakeMaker v6.17 to make.  Cleaned up some meta file information.  Set the 
-minimum version for IO::Socket::SSL to 1.76.
 ------------------------------------------------------------------------
-r1814016 | billcole | 2017-11-01 22:40:24 +0000 (Wed, 01 Nov 2017) | 1 
-line
+r1890669 | gbechis | 2021-06-10 09:20:08 +0000 (Thu, 10 Jun 2021) | 2 
+lines
+ fix regression tests when BSD::Resource is not installed
  
- Recognize Horde HTTPS protocol in Received header
 ------------------------------------------------------------------------
-r1813995 | kmcgrail | 2017-11-01 20:46:34 +0000 (Wed, 01 Nov 2017) | 1 
-line
+r1890481 | jhardin | 2021-06-05 02:28:17 +0000 (Sat, 05 Jun 2021) | 1 line
  
- Adding .gitignore file
+ Spammers abusing another Amazon domain for hosting images
 ------------------------------------------------------------------------
-r1812595 | kb | 2017-10-18 23:19:59 +0000 (Wed, 18 Oct 2017) | 6 lines
+r1890326 | gbechis | 2021-05-30 17:51:46 +0000 (Sun, 30 May 2021) | 2 
+lines
  
- Bug 7256, using a header rule with an eval() function does not work the 
-way
- this was intended.
+ use pms to store flags
  
- Remove HEADER_HOST_IN_BLACKLIST and *_WHITELIST rules.
+------------------------------------------------------------------------
+r1890324 | hege | 2021-05-30 09:53:08 +0000 (Sun, 30 May 2021) | 2 lines
  
+ Do not hide error messages. Warn visibly if specifically requested 
+module failed to load.
  
 ------------------------------------------------------------------------
-r1812589 | kb | 2017-10-18 22:48:31 +0000 (Wed, 18 Oct 2017) | 3 lines
- clarify (URI|HEADER)_HOST_IN_(BLACK|WHITE)LIST descriptions
+r1890323 | gbechis | 2021-05-30 09:45:17 +0000 (Sun, 30 May 2021) | 2 
+lines
  
+ fix sql syntax
  
 ------------------------------------------------------------------------
-r1808962 | kb | 2017-09-20 00:15:45 +0000 (Wed, 20 Sep 2017) | 1 line
+r1890322 | gbechis | 2021-05-30 08:49:17 +0000 (Sun, 30 May 2021) | 3 
+lines
+ do not print backtrace if we cannot load an optional
+ module, re-enable now working test
  
- bug 7472: Fix POD errors with perl >= 5.18, wrap exit code items in C<> 
-to avoid parser complaints
 ------------------------------------------------------------------------
-r1808358 | billcole | 2017-09-14 15:23:26 +0000 (Thu, 14 Sep 2017) | 1 
-line
+r1890317 | hege | 2021-05-30 05:14:19 +0000 (Sun, 30 May 2021) | 2 lines
+ Enable normalize_charset by default (Bug 7656)
  
- Corrected alphabetization of patch contributor credits
 ------------------------------------------------------------------------
-r1808350 | billcole | 2017-09-14 15:01:42 +0000 (Thu, 14 Sep 2017) | 2 
+r1890313 | gbechis | 2021-05-29 19:51:18 +0000 (Sat, 29 May 2021) | 3 
 lines
  
- Corrected Dianne Skoll's name in CREDITS
+ disable a test for the moment,
+ regression test fails if GeoIP2 module cannot be initialized.
  
 ------------------------------------------------------------------------
-r1806880 | kb | 2017-09-01 00:10:12 +0000 (Fri, 01 Sep 2017) | 6 lines
+r1890312 | gbechis | 2021-05-29 19:48:39 +0000 (Sat, 29 May 2021) | 2 
+lines
  
- RFC 2231 section 3: Parameter Value Continuations
+ load DecodeShortURLs plugin as well
  
- Support MIME parameter value continuations for the filename value, which 
-is
- actually used by plugins and rules.
+------------------------------------------------------------------------
+r1890282 | gbechis | 2021-05-28 13:17:24 +0000 (Fri, 28 May 2021) | 2 
+lines
  
+ Cope with spammer changes, add a simpler rule for testing
  
 ------------------------------------------------------------------------
-r1806756 | kb | 2017-08-31 01:41:06 +0000 (Thu, 31 Aug 2017) | 3 lines
+r1890274 | hege | 2021-05-28 09:40:09 +0000 (Fri, 28 May 2021) | 15 lines
  
- bug 7466: decode Content-* header
+ Bug 7735 - Meta rules need to handle missing/unrun dependencies
  
+ - Meta rules no longer use priority values, they are evaluated 
+dynamically
+   when the rules they depend on are finished.
  
-------------------------------------------------------------------------
-r1806555 | kb | 2017-08-29 10:45:10 +0000 (Tue, 29 Aug 2017) | 3 lines
+ - API: New $pms->rule_pending() and $pms->rule_ready() functions.
+   $pms->rule_pending($rulename) must be called from rules eval-function, 
+if
+   the result can arrive later than when exiting the function (async
+   lookups).  $pms->rule_ready($rulename) or $pms->got_hit(...) must be
+   called when the result has arrived.  If these are not used, it can 
+break
+   depending meta rule evaluation.
  
- bug 7361: Allow building against OpenSSL 1.1.0
+ - API: Deprecated $pms->harvest_until_rule_completes, 
+$pms->is_rule_complete
  
  
 ------------------------------------------------------------------------
-r1806518 | kb | 2017-08-29 00:41:13 +0000 (Tue, 29 Aug 2017) | 3 lines
- bug 7453: Fix "use of uninitialized value in pattern match" warning.
+r1890266 | gbechis | 2021-05-28 08:04:45 +0000 (Fri, 28 May 2021) | 2 
+lines
  
+ remove legacy "dynamic" rules and use proper 'eval' rules
  
 ------------------------------------------------------------------------
-r1806114 | kb | 2017-08-24 23:50:59 +0000 (Thu, 24 Aug 2017) | 3 lines
- bug 7462: "use lib '.'" before "use SATest" for Perl 5.26 compatibility
+r1890263 | hege | 2021-05-28 07:00:33 +0000 (Fri, 28 May 2021) | 2 lines
  
+ Some quick cleanups, check_dnsbl should be used instead of 
+parsed_metadata
  
 ------------------------------------------------------------------------
-r1806023 | kb | 2017-08-24 10:52:44 +0000 (Thu, 24 Aug 2017) | 3 lines
+r1890261 | hege | 2021-05-28 04:56:38 +0000 (Fri, 28 May 2021) | 2 lines
  
- bug 7441: handle Received-SPF temperror and permerror
+ Fix typo
  
+------------------------------------------------------------------------
+r1890258 | jhardin | 2021-05-28 01:38:46 +0000 (Fri, 28 May 2021) | 1 line
  
+ fix __RCVD_DOTGOV_EXT and __RCVD_DOTEDU_EXT
 ------------------------------------------------------------------------
-r1806020 | kb | 2017-08-24 10:32:59 +0000 (Thu, 24 Aug 2017) | 3 lines
+r1890257 | jhardin | 2021-05-28 01:34:32 +0000 (Fri, 28 May 2021) | 1 line
  
- bug 7443: handle inline style attributes in table and anchor tags
+ adjust score limit of RCVD_DOTEDU_SHORT and exclude ALL_TRUSTED
+------------------------------------------------------------------------
+r1890251 | gbechis | 2021-05-27 17:35:21 +0000 (Thu, 27 May 2021) | 2 
+lines
  
+ Add DecodeShortURLs plugin to check urls hidden behind url shorteners
  
 ------------------------------------------------------------------------
-r1805349 | kb | 2017-08-17 23:24:36 +0000 (Thu, 17 Aug 2017) | 3 lines
+r1890024 | gbechis | 2021-05-19 09:23:41 +0000 (Wed, 19 May 2021) | 2 
+lines
  
- bug 7340: remove expire flag after token expiration is done
+ remove rule with too many fp
  
+------------------------------------------------------------------------
+r1890021 | gbechis | 2021-05-19 07:18:39 +0000 (Wed, 19 May 2021) | 2 
+lines
+ avoid a division by zero error
  
 ------------------------------------------------------------------------
-r1804611 | kmcgrail | 2017-08-09 21:51:29 +0000 (Wed, 09 Aug 2017) | 1 
-line
+r1889988 | hege | 2021-05-18 07:43:10 +0000 (Tue, 18 May 2021) | 2 lines
+ Minor optimizations and cleanups
  
- Bug 7416 patch from Dave Jones
 ------------------------------------------------------------------------
-r1804346 | kb | 2017-08-07 15:57:21 +0000 (Mon, 07 Aug 2017) | 3 lines
+r1889987 | hege | 2021-05-18 07:35:35 +0000 (Tue, 18 May 2021) | 2 lines
  
- bug 7296: sa-learn --folders option: default to type "detect", if not 
-explicitely specified
+ Minor optimization
  
+------------------------------------------------------------------------
+r1889986 | hege | 2021-05-18 07:32:52 +0000 (Tue, 18 May 2021) | 2 lines
+ Minor optimizations
  
 ------------------------------------------------------------------------
-r1804338 | kb | 2017-08-07 14:13:56 +0000 (Mon, 07 Aug 2017) | 3 lines
+r1889964 | gbechis | 2021-05-17 16:54:58 +0000 (Mon, 17 May 2021) | 2 
+lines
  
- bug 7447: Delete parse_queue in Message::finish() to prevent memory leak.
+ check for fake mailing lists headers
  
+------------------------------------------------------------------------
+r1889951 | hege | 2021-05-17 10:20:41 +0000 (Mon, 17 May 2021) | 2 lines
+ Minor optimizations
  
 ------------------------------------------------------------------------
-r1804327 | kb | 2017-08-07 12:43:37 +0000 (Mon, 07 Aug 2017) | 3 lines
+r1889937 | hege | 2021-05-16 18:21:39 +0000 (Sun, 16 May 2021) | 2 lines
  
- bug 7304, replace memcmp with strncmp
+ Try to clean up some of the setuid/setgid code
+------------------------------------------------------------------------
+r1889935 | hege | 2021-05-16 16:19:57 +0000 (Sun, 16 May 2021) | 2 lines
  
+ Clean up get_user_groups(), tighten up group matching
  
 ------------------------------------------------------------------------
-r1804260 | kb | 2017-08-06 19:20:50 +0000 (Sun, 06 Aug 2017) | 3 lines
+r1889917 | hege | 2021-05-15 17:04:47 +0000 (Sat, 15 May 2021) | 2 lines
  
- fix typo
+ Clean up get_user_groups(), tighten up group matching
+------------------------------------------------------------------------
+r1889752 | gbechis | 2021-05-11 10:03:17 +0000 (Tue, 11 May 2021) | 2 
+lines
  
+ another obfuscation tecnique using Google search spotted in the wild
  
 ------------------------------------------------------------------------
-r1803384 | kb | 2017-07-29 17:56:31 +0000 (Sat, 29 Jul 2017) | 3 lines
+r1889744 | gbechis | 2021-05-11 06:42:44 +0000 (Tue, 11 May 2021) | 2 
+lines
+ protect with ifplugin
  
- bug 7155: Fix spamc longoptions prefix-substring handling.
+------------------------------------------------------------------------
+r1889731 | hege | 2021-05-10 18:20:22 +0000 (Mon, 10 May 2021) | 2 lines
  
+ Disable duplicate rule merging per 
+https://bz.apache.org/SpamAssassin/show_bug.cgi?id=7735#c12
  
 ------------------------------------------------------------------------
-r1803335 | kb | 2017-07-28 20:22:06 +0000 (Fri, 28 Jul 2017) | 3 lines
+r1889728 | hege | 2021-05-10 17:38:12 +0000 (Mon, 10 May 2021) | 2 lines
  
- bug 7442: SQL grants, add missing UPDATE privilege
+ Fix some anti_pattern logic
  
+------------------------------------------------------------------------
+r1889714 | hege | 2021-05-10 04:41:31 +0000 (Mon, 10 May 2021) | 2 lines
+ Hashing functions expect bytes
  
 ------------------------------------------------------------------------
-r1803310 | kb | 2017-07-28 17:33:10 +0000 (Fri, 28 Jul 2017) | 3 lines
+r1889706 | hege | 2021-05-09 16:07:07 +0000 (Sun, 09 May 2021) | 2 lines
+ Shave another 50ms from parsing
  
- bug 7303, fix "uninitialized value" warning
+------------------------------------------------------------------------
+r1889705 | hege | 2021-05-09 13:55:32 +0000 (Sun, 09 May 2021) | 2 lines
  
+ Shave off 50ms parsing time
  
 ------------------------------------------------------------------------
-r1796723 | jhardin | 2017-05-30 01:32:39 +0000 (Tue, 30 May 2017) | 3 
-lines
+r1889704 | hege | 2021-05-09 13:47:33 +0000 (Sun, 09 May 2021) | 2 lines
+ Shave off 50ms parsing time
  
- Update __MOZILLA_MSGID per users list discussion, tbird using new format 
-- 8-4-4-4-12 lc hex, was 8.8 UC hex. Recognize both.
- N.B.: 8-4-4-4-12 UC hex is Apple Mail MUA.
- Copy from trunk
 ------------------------------------------------------------------------
-r1795107 | kmcgrail | 2017-05-14 14:37:05 +0000 (Sun, 14 May 2017) | 1 
-line
+r1889682 | jhardin | 2021-05-08 16:31:44 +0000 (Sat, 08 May 2021) | 1 line
  
- More prep work on the build with infrastructure for 3.4.2
+ Expose RATWARE_MS_HASH and RATWARE_OUTLOOK_NONAME to ruleqa due to 
+reported FPs using the fixed scoring and apparent rule staleness.
 ------------------------------------------------------------------------
-r1793220 | davej | 2017-04-29 17:00:44 +0000 (Sat, 29 Apr 2017) | 1 line
+r1889669 | hege | 2021-05-08 10:00:28 +0000 (Sat, 08 May 2021) | 2 lines
+ Apply dns_query_restriction to SPF/DKIM queries
  
- Corrected ordering by last name of committers list
 ------------------------------------------------------------------------
-r1793217 | davej | 2017-04-29 16:58:22 +0000 (Sat, 29 Apr 2017) | 1 line
+r1889668 | hege | 2021-05-08 09:59:52 +0000 (Sat, 08 May 2021) | 2 lines
+ No reason for domain_to_search_list to return empty last value
  
- Added Dave Jones to committers list
 ------------------------------------------------------------------------
-r1792371 | kmcgrail | 2017-04-23 15:19:46 +0000 (Sun, 23 Apr 2017) | 1 
-line
+r1889667 | hege | 2021-05-08 09:35:41 +0000 (Sat, 08 May 2021) | 2 lines
+ Move domain_to_search_list to Util
  
- Continued tweaks on the build process
 ------------------------------------------------------------------------
-r1792309 | kmcgrail | 2017-04-22 17:42:58 +0000 (Sat, 22 Apr 2017) | 1 
-line
+r1889666 | hege | 2021-05-08 09:34:33 +0000 (Sat, 08 May 2021) | 2 lines
+ Catch more errors
  
- preparing to release 3.4.2 pre3
 ------------------------------------------------------------------------
-r1792301 | kmcgrail | 2017-04-22 16:37:41 +0000 (Sat, 22 Apr 2017) | 1 
+r1889566 | billcole | 2021-05-06 13:55:00 +0000 (Thu, 06 May 2021) | 1 
 line
  
- Bug 7411 FORGED_MUA_MOZILLA
+ remove unican.es. Bug #7903
 ------------------------------------------------------------------------
-r1792295 | kmcgrail | 2017-04-22 15:43:11 +0000 (Sat, 22 Apr 2017) | 1 
-line
+r1889518 | hege | 2021-05-05 13:22:46 +0000 (Wed, 05 May 2021) | 2 lines
+ Fix list-bad-rules warning
  
- working through bugs in the release process
 ------------------------------------------------------------------------
-r1792266 | kmcgrail | 2017-04-22 06:08:20 +0000 (Sat, 22 Apr 2017) | 1 
-line
+r1889417 | jhardin | 2021-05-02 16:12:09 +0000 (Sun, 02 May 2021) | 1 line
  
- preparing to release 3.4.2-pre1
+ tuning GOOG_STO_EMAIL_PHISH
 ------------------------------------------------------------------------
-r1792260 | kmcgrail | 2017-04-22 05:20:27 +0000 (Sat, 22 Apr 2017) | 1 
-line
+r1889399 | hege | 2021-05-02 10:35:25 +0000 (Sun, 02 May 2021) | 2 lines
+ Stop complaining about missing 'fetch' if not on FreeBSD
  
- Unifying small rule changes between 3.4 and trunk
 ------------------------------------------------------------------------
-r1791591 | sidney | 2017-04-16 10:19:00 +0000 (Sun, 16 Apr 2017) | 2 lines
+r1889398 | hege | 2021-05-02 10:31:31 +0000 (Sun, 02 May 2021) | 2 lines
+ Up Net::DNS requirement to 0.69
  
- Bug 7225: A regexp for parsing an IPv4 address inconsistently 
-allows/disallows a leading zero
- Fix problem in  port from trunk
 ------------------------------------------------------------------------
-r1791583 | sidney | 2017-04-16 09:36:02 +0000 (Sun, 16 Apr 2017) | 1 line
+r1889397 | hege | 2021-05-02 10:29:05 +0000 (Sun, 02 May 2021) | 2 lines
+ Clean out some historic Net::DNS stuff
  
- fix typo in comment
 ------------------------------------------------------------------------
-r1791582 | sidney | 2017-04-16 09:26:14 +0000 (Sun, 16 Apr 2017) | 1 line
+r1889396 | hege | 2021-05-02 10:16:59 +0000 (Sun, 02 May 2021) | 2 lines
+ Fix test count
  
- Merged from trunk - bug 7198: Let whitelist_from_rcvd also accept CIDR 
-notation and IPv6 address [from revision 1681350]
 ------------------------------------------------------------------------
-r1791580 | sidney | 2017-04-16 09:06:27 +0000 (Sun, 16 Apr 2017) | 1 line
+r1889395 | hege | 2021-05-02 10:14:08 +0000 (Sun, 02 May 2021) | 2 lines
+ Allow --lint --net to test network
  
- Merged from trunk - Bug 7212: code compaction (was: Warning of 
-uninitialized value) [from revision 1685857]
 ------------------------------------------------------------------------
-r1791577 | sidney | 2017-04-16 08:32:11 +0000 (Sun, 16 Apr 2017) | 1 line
+r1889394 | hege | 2021-05-02 09:19:19 +0000 (Sun, 02 May 2021) | 2 lines
+ Fix binaries check
  
- bug 7410 - Port to 3.4 branch some cleanup that was committed to trunk 
-in Plugin/HeaderEval
 ------------------------------------------------------------------------
-r1791576 | sidney | 2017-04-16 07:47:45 +0000 (Sun, 16 Apr 2017) | 1 line
+r1889393 | hege | 2021-05-02 09:04:05 +0000 (Sun, 02 May 2021) | 2 lines
+ Tidy up ResourceLimits
  
- Merged from trunk - bug 7409 - get plugins FreeMail, TextCat and VBounce 
-ready to deal with perl characters if they happen to reach them in a mail 
-body [from revision 1707595]
 ------------------------------------------------------------------------
-r1791573 | sidney | 2017-04-16 07:28:59 +0000 (Sun, 16 Apr 2017) | 2 lines
+r1889387 | hege | 2021-05-02 07:44:06 +0000 (Sun, 02 May 2021) | 2 lines
+ Improve logging
  
- Bug 7231 - Net::DNS 1.01 returns answers formatted differently, breaks SA
- Port more of the changes that were committed to trunk for this as part 
-of syncing 3.4 branch with trunk before 3.4.2 release
 ------------------------------------------------------------------------
-r1791571 | sidney | 2017-04-16 06:18:27 +0000 (Sun, 16 Apr 2017) | 1 line
+r1889376 | jhardin | 2021-05-01 14:26:07 +0000 (Sat, 01 May 2021) | 1 line
  
- Merged from trunk - Bug 7285: Make the DKIM selector a tag [from 
-revision 1749299]
+ Tune phishing rules; FP Avoidance tuning for HAS_X_OUTGOING_SPAM_STAT
 ------------------------------------------------------------------------
-r1791569 | sidney | 2017-04-16 06:00:55 +0000 (Sun, 16 Apr 2017) | 1 line
+r1889370 | gbechis | 2021-05-01 09:46:20 +0000 (Sat, 01 May 2021) | 2 
+lines
+ better Mailup check
  
- Merge from trunk - Bug 7265 - DNS resolving breaks with Net::DNS 1.03
 ------------------------------------------------------------------------
-r1791555 | sidney | 2017-04-16 02:16:05 +0000 (Sun, 16 Apr 2017) | 1 line
+r1889364 | gbechis | 2021-05-01 09:41:28 +0000 (Sat, 01 May 2021) | 2 
+lines
+ cope with recent MailUP changes
  
- Merged from trunk - Bug 7408 - Change some methods in Node.pm from 
-methods to ordinary subroutines [from revision 1707588]
 ------------------------------------------------------------------------
-r1791487 | sidney | 2017-04-15 10:07:38 +0000 (Sat, 15 Apr 2017) | 1 line
+r1889347 | jhardin | 2021-05-01 00:37:54 +0000 (Sat, 01 May 2021) | 1 line
  
- Merged from trunk - Bug 6608: Make thinks that Digest::SHA is optional 
-AND required - Make Digest::SHA required and Digest::SHA1 as optional 
-[from revision 1749931]
+ Prefer __HAS_xxx to be header rule, any context-sensitive aliases should 
+be the meta
 ------------------------------------------------------------------------
-r1791474 | sidney | 2017-04-15 08:55:53 +0000 (Sat, 15 Apr 2017) | 1 line
+r1889338 | hege | 2021-04-30 18:28:50 +0000 (Fri, 30 Apr 2021) | 2 lines
+ Fix bogus mail parsing
  
- Merged from trunk - Bug 7225: A regexp for parsing an IPv4 address 
-inconsistently allows/disallows a leading zero [from revision 1692208]
 ------------------------------------------------------------------------
-r1791469 | sidney | 2017-04-15 08:19:48 +0000 (Sat, 15 Apr 2017) | 1 line
+r1889337 | hege | 2021-04-30 18:17:51 +0000 (Fri, 30 Apr 2021) | 14 lines
+ - Improved internal header address (From/To/Cc) parser, now also handles
+   multiple addresses.  Optional support for external Email::Address::XS
+   parser, which can handle nested comments and other oddities.
+ - Header :addr :name modifiers now returns all addresses.  :first :last
+   select only first (topmost) or last header to process, when there are
+   multiple headers with the same name (:addr and :name may still return
+   multiple values from a single header).
+ - API: $pms->get() can and should now be called in list context.  Scalar
+   context continues to return multiple values newline separated, but this
+   should be considered deprecated.
  
- Sync with trunk before 3.4.2 release - port update of version number to 
-3.4.2
 ------------------------------------------------------------------------
-r1791456 | sidney | 2017-04-15 07:01:26 +0000 (Sat, 15 Apr 2017) | 1 line
+r1889334 | hege | 2021-04-30 18:00:36 +0000 (Fri, 30 Apr 2021) | 2 lines
+ Add "use version"
  
- Bug 7406 - Update branch 3.4 version of 20_aux_tlds.cf with TLD changes 
-previously committed to trunk
 ------------------------------------------------------------------------
-r1791448 | sidney | 2017-04-15 04:38:56 +0000 (Sat, 15 Apr 2017) | 1 line
+r1889318 | hege | 2021-04-30 06:47:20 +0000 (Fri, 30 Apr 2021) | 2 lines
+ Fix unicode quote handling in HTML parsing
  
- Merged from trunk - Bug 7192 moving MILLION_USD, NA_DOLLARS & US_DOLLARS 
-to sandbox for ruleqa/promotion, etc. [from revision 1679253]
 ------------------------------------------------------------------------
-r1791428 | sidney | 2017-04-14 23:21:40 +0000 (Fri, 14 Apr 2017) | 1 line
+r1889311 | billcole | 2021-04-30 02:27:15 +0000 (Fri, 30 Apr 2021) | 1 
+line
  
- Merged from trunk - Clarifying Copyright - bug 7263 [from revision 
-1714592]
+ syntax fix. 
 ------------------------------------------------------------------------
-r1791426 | sidney | 2017-04-14 22:35:30 +0000 (Fri, 14 Apr 2017) | 2 lines
+r1889308 | billcole | 2021-04-30 01:37:58 +0000 (Fri, 30 Apr 2021) | 1 
+line
  
- Bug 7405 - Error in commit of new option in seek-phrases-in-log was 
-fixed in trunk after 3.4 was branched
- This makes 3.4 branch same as what is in trunk
+ Cosmetics of MAILING_LIST_MULTI and HAS/ML# duplicate, sandbox test rule 
+housekeeping.
 ------------------------------------------------------------------------
-r1791197 | billcole | 2017-04-13 01:22:08 +0000 (Thu, 13 Apr 2017) | 3 
-lines
+r1889305 | hege | 2021-04-29 18:42:10 +0000 (Thu, 29 Apr 2021) | 2 lines
+ Remove all traces of pdftohtml. HTML output can't be used as 
+set_rendered() expects already rendered body. It makes no sense to add 
+any extra functionality to re-render something, since pdftotext can as 
+easily be used instead. Duh.
  
- Fixed to use sitebin instead of hardcoded '/bin'
+------------------------------------------------------------------------
+r1889303 | hege | 2021-04-29 17:42:02 +0000 (Thu, 29 Apr 2021) | 2 lines
  
+ Do not use buggy nested if/if/else..
  
 ------------------------------------------------------------------------
-r1791044 | billcole | 2017-04-11 22:03:38 +0000 (Tue, 11 Apr 2017) | 3 
-lines
+r1889300 | hege | 2021-04-29 17:22:26 +0000 (Thu, 29 Apr 2021) | 2 lines
  
- Assuring that the test doesn't mess with /{etc,var}/opt/ if PREFIX is 
-/opt/$DIR
+ Bug 7901 - Direct usage of UTF-8 in subject triggering 
+SUBJ_ILLEGAL_CHARS and SUBJECT_NEEDS_ENCODING rules
  
+------------------------------------------------------------------------
+r1889299 | jhardin | 2021-04-29 14:46:42 +0000 (Thu, 29 Apr 2021) | 1 line
  
+ Push google phishing rule for publication; FP Avoidance tuning; add rule 
+for evaluation
 ------------------------------------------------------------------------
-r1791013 | mmartinec | 2017-04-11 18:56:16 +0000 (Tue, 11 Apr 2017) | 1 
-line
+r1889298 | hege | 2021-04-29 14:45:43 +0000 (Thu, 29 Apr 2021) | 2 lines
+ Tune nested comment
  
- Bug 7404: Bad regexp (and logic) in MS::PerMsgStatus::get_content_preview
 ------------------------------------------------------------------------
-r1790998 | kmcgrail | 2017-04-11 16:31:20 +0000 (Tue, 11 Apr 2017) | 1 
-line
+r1889253 | hege | 2021-04-28 04:51:44 +0000 (Wed, 28 Apr 2021) | 2 lines
+ Let nightly score HK_RANDOM_ENVFROM
  
- fix for 7181 on 3.4 - same as revision 1790984.
 ------------------------------------------------------------------------
-r1790926 | kmcgrail | 2017-04-11 05:54:45 +0000 (Tue, 11 Apr 2017) | 5 
-lines
+r1889252 | hege | 2021-04-28 04:41:11 +0000 (Wed, 28 Apr 2021) | 2 lines
  
- KG: Syncing Trunk to 3.4: 
+ Whitelist @marketplace.amazon.de for HK_RANDOM
  
- Revision 1707582 "random changes, cosmetic or trivial - 
+------------------------------------------------------------------------
+r1889247 | jhardin | 2021-04-28 03:21:16 +0000 (Wed, 28 Apr 2021) | 1 line
  
- Revision 1707583 "Plugin/Bayes.pm: add missing $tokprefix to u8: and 8: 
-tokens, shorten also tokens in Content-Disposition and 
-Content-Transfer-Encoding"
+ Add google storage phishing rule for evaluation
 ------------------------------------------------------------------------
-r1790920 | kmcgrail | 2017-04-11 05:05:28 +0000 (Tue, 11 Apr 2017) | 1 
-line
+r1889241 | hege | 2021-04-27 16:40:04 +0000 (Tue, 27 Apr 2021) | 2 lines
+ Some more header oddities
  
- KG: Syncing Trunk to 3.4: RFC 4408 -> 7208 comment
 ------------------------------------------------------------------------
-r1790919 | kmcgrail | 2017-04-11 04:58:54 +0000 (Tue, 11 Apr 2017) | 1 
-line
+r1889239 | hege | 2021-04-27 16:19:07 +0000 (Tue, 27 Apr 2021) | 2 lines
+ Masscheck some header oddities
  
- KG: Syncing Trunk to 3.4: missed a use bytes
 ------------------------------------------------------------------------
-r1790918 | kmcgrail | 2017-04-11 04:56:50 +0000 (Tue, 11 Apr 2017) | 1 
-line
+r1889198 | jhardin | 2021-04-26 14:27:41 +0000 (Mon, 26 Apr 2021) | 1 line
  
- Removing deprecated RegistrarBoundaries.pm and related routines/MANIFEST 
-entries per bug 7170 and added rules-extras/
+ Add some more X and XM header subrules, FP Avoidance tuning and score 
+limit reduction for XM_RANDOM and HAS_X_OUTGOING_SPAM_STAT
 ------------------------------------------------------------------------
-r1790913 | kmcgrail | 2017-04-11 03:59:33 +0000 (Tue, 11 Apr 2017) | 1 
-line
+r1889191 | gbechis | 2021-04-26 08:08:37 +0000 (Mon, 26 Apr 2021) | 2 
+lines
+ fix 'nolog' templates
  
- KG: Syncing Trunk to 3.4: sync TxRep.pm (imports comments and reformats 
-a little whitespace)
 ------------------------------------------------------------------------
-r1790912 | kmcgrail | 2017-04-11 03:54:45 +0000 (Tue, 11 Apr 2017) | 1 
-line
+r1889172 | hege | 2021-04-25 13:37:38 +0000 (Sun, 25 Apr 2021) | 2 lines
+ Fix "use version" usage
  
- KG: Syncing Trunk to 3.4: 
-https://bz.apache.org/SpamAssassin/show_bug.cgi?id=7232 removing use bytes
 ------------------------------------------------------------------------
-r1790909 | kmcgrail | 2017-04-11 03:17:35 +0000 (Tue, 11 Apr 2017) | 1 
-line
+r1889171 | hege | 2021-04-25 13:14:24 +0000 (Sun, 25 Apr 2021) | 2 lines
+ Fix @INC that last code cleanup removed. Enable exact pattern matching 
+with q{'foobar'}.
  
- KG: Syncing Trunk to 3.4: 
-https://bz.apache.org/SpamAssassin/show_bug.cgi?id=7305
 ------------------------------------------------------------------------
-r1790908 | kmcgrail | 2017-04-11 03:15:10 +0000 (Tue, 11 Apr 2017) | 1 
-line
+r1889169 | hege | 2021-04-25 10:42:12 +0000 (Sun, 25 Apr 2021) | 2 lines
+ Update test order
  
- KG: Syncing Trunk to 3.4: A small change to the redis bayes config
 ------------------------------------------------------------------------
-r1790907 | kmcgrail | 2017-04-11 02:07:56 +0000 (Tue, 11 Apr 2017) | 1 
-line
+r1889161 | hege | 2021-04-25 06:00:04 +0000 (Sun, 25 Apr 2021) | 2 lines
+ warn about missing Net::LibIDN
  
- Working with Kevin Golding to sync trunk & 3.4 branch: First sweep is a 
-small one, it just merges in release details and metadata type files.
 ------------------------------------------------------------------------
-r1790906 | kmcgrail | 2017-04-11 02:04:24 +0000 (Tue, 11 Apr 2017) | 1 
-line
+r1889153 | jhardin | 2021-04-24 18:42:00 +0000 (Sat, 24 Apr 2021) | 1 line
  
- Small whitespace cleanup for readability
+ Push possible-phishing rules for publication, spank the 
+AdultDatingCompany joe-jobbers
 ------------------------------------------------------------------------
-r1790769 | kmcgrail | 2017-04-10 02:12:14 +0000 (Mon, 10 Apr 2017) | 1 
-line
+r1889141 | hege | 2021-04-24 09:33:13 +0000 (Sat, 24 Apr 2021) | 2 lines
+ Fix get :domain
  
- Minor patch to check for re2c binary
 ------------------------------------------------------------------------
-r1782717 | jhardin | 2017-02-13 02:16:44 +0000 (Mon, 13 Feb 2017) | 1 line
+r1889138 | hege | 2021-04-24 07:20:25 +0000 (Sat, 24 Apr 2021) | 2 lines
+ Validate addresses in find_all_addrs_in_line
  
- Fix bug#7367: Don't assume cwd (".") is in @INC, it may be removed for 
-security reasons - see CVE-2016-1238
 ------------------------------------------------------------------------
-r1782715 | jhardin | 2017-02-13 01:15:03 +0000 (Mon, 13 Feb 2017) | 3 
-lines
+r1889125 | hege | 2021-04-23 11:35:12 +0000 (Fri, 23 Apr 2021) | 2 lines
+ Override Logger output escaping with SA_LOGGER_ESCAPE environment 
+variable
  
- Merge revision 1782713 from trunk:
- Fix $JOBS (thx Tom Hendrikx)
- Add log file symlinks (thx Kevin A. McGrail)
 ------------------------------------------------------------------------
-r1750443 | sidney | 2016-06-28 04:41:16 +0000 (Tue, 28 Jun 2016) | 1 line
+r1889120 | axb | 2021-04-23 08:05:35 +0000 (Fri, 23 Apr 2021) | 1 line
  
- New PMC member, new committer
+ added ct.sendgrid.net
 ------------------------------------------------------------------------
-r1749347 | mmartinec | 2016-06-20 14:37:18 +0000 (Mon, 20 Jun 2016) | 1 
-line
+r1889093 | hege | 2021-04-22 06:44:25 +0000 (Thu, 22 Apr 2021) | 2 lines
+ Check full hostname from uridnsbl_skip_domain also
  
- Bad SSL/TLS Version Default - applied Proposed Patch v2: support for 
-SSLv3 removed - removed t/spamd_ssl_tls.t and t/spamd_ssl_v3.t
 ------------------------------------------------------------------------
-r1749346 | mmartinec | 2016-06-20 14:35:01 +0000 (Mon, 20 Jun 2016) | 1 
-line
+r1889056 | hege | 2021-04-21 12:59:00 +0000 (Wed, 21 Apr 2021) | 2 lines
+ Add --forcemirror parameter
  
- Bad SSL/TLS Version Default - applied Proposed Patch v2: support for 
-SSLv3 removed, removed spamd option --ssl-version, removed spamc option 
---ssl=sslv3
 ------------------------------------------------------------------------
-r1749230 | mmartinec | 2016-06-19 23:15:55 +0000 (Sun, 19 Jun 2016) | 1 
-line
+r1889050 | hege | 2021-04-21 10:35:25 +0000 (Wed, 21 Apr 2021) | 2 lines
+ Bug 7505 - build/mkupdates/listpromotable deprecated goto
  
- Bug 6461 - whatis parse fails for some man pages - adding missing NAME, 
-SYNOPSIS, DESCRIPTION
 ------------------------------------------------------------------------
-r1749190 | mmartinec | 2016-06-19 17:44:26 +0000 (Sun, 19 Jun 2016) | 1 
-line
+r1888999 | hege | 2021-04-20 07:35:51 +0000 (Tue, 20 Apr 2021) | 2 lines
+ Support compacted/deduplicated RULE(hitcount) format for mass-check logs
  
- Bug 6461 - whatis parse fails for some man pages - fixing POD
 ------------------------------------------------------------------------
-r1748642 | mmartinec | 2016-06-15 23:10:23 +0000 (Wed, 15 Jun 2016) | 1 
-line
+r1888996 | hege | 2021-04-20 05:28:49 +0000 (Tue, 20 Apr 2021) | 2 lines
+ Limit show_mclog to 100 matches to limit resource usage
  
- Bug 7321: impossible to disable ipv6 in spamc - document options -4 and 
--6 in spamc.pod
 ------------------------------------------------------------------------
-r1748623 | mmartinec | 2016-06-15 19:20:50 +0000 (Wed, 15 Jun 2016) | 1 
+r1888971 | billcole | 2021-04-19 16:50:20 +0000 (Mon, 19 Apr 2021) | 1 
 line
  
- Bug 7326: Add log info about revoke report to Razor2 - log "spam 
-revoked" at the same log level as "spam reported" for consistency
+ Make ALL_INTERNAL depend on the message having at least one relay.
 ------------------------------------------------------------------------
-r1726002 | mmartinec | 2016-01-21 16:17:13 +0000 (Thu, 21 Jan 2016) | 1 
+r1888965 | billcole | 2021-04-19 16:17:02 +0000 (Mon, 19 Apr 2021) | 1 
 line
  
- added a comment that a bug 99755 in HTML::Parser was fixed in 3.72
+ fixed syntax on ALL_INTERNAL
 ------------------------------------------------------------------------
-r1722535 | kmcgrail | 2016-01-01 19:01:04 +0000 (Fri, 01 Jan 2016) | 1 
+r1888964 | billcole | 2021-04-19 14:32:40 +0000 (Mon, 19 Apr 2021) | 1 
 line
  
- Fix Pod error - 7283
+ Add ALL_INTERNAL, publish T_SCC_SPECIAL_GUID, which has a perfect but 
+low-volume history.
 ------------------------------------------------------------------------
-r1721238 | kmcgrail | 2015-12-21 19:25:57 +0000 (Mon, 21 Dec 2015) | 1 
-line
+r1888905 | hege | 2021-04-18 15:01:33 +0000 (Sun, 18 Apr 2021) | 2 lines
+ Document sa-learn -L a bit more.
  
- Change #1 from bug 7279 for SURBL list changes for 3.4
 ------------------------------------------------------------------------
-r1720874 | mmartinec | 2015-12-19 01:24:12 +0000 (Sat, 19 Dec 2015) | 1 
-line
+r1888900 | hege | 2021-04-18 14:03:03 +0000 (Sun, 18 Apr 2021) | 2 lines
+ Bug 4435 - Progress meter doesn't handle time left when it's in the hours
  
- updated a comment
 ------------------------------------------------------------------------
-r1720872 | mmartinec | 2015-12-19 00:46:26 +0000 (Sat, 19 Dec 2015) | 1 
-line
+r1888898 | hege | 2021-04-18 13:01:09 +0000 (Sun, 18 Apr 2021) | 2 lines
+ Add new TextCat languages es.utf8 fr.utf8 it.utf8 ru.utf8 zh.utf8 (Bug 
+6364)
  
- Bug 7278: redirector_pattern - reverse order so hardcoded check done last
 ------------------------------------------------------------------------
-r1720454 | jquinn | 2015-12-16 21:06:45 +0000 (Wed, 16 Dec 2015) | 1 line
+r1888873 | jhardin | 2021-04-17 22:07:21 +0000 (Sat, 17 Apr 2021) | 1 line
  
- new Received authentication methods for CommuniGate
+ Expose gmail phisher rule for scoring and publication
 ------------------------------------------------------------------------
-r1720441 | jquinn | 2015-12-16 20:23:15 +0000 (Wed, 16 Dec 2015) | 1 line
+r1888872 | jhardin | 2021-04-17 20:23:17 +0000 (Sat, 17 Apr 2021) | 1 line
  
- Updated TxRep documentation
+ Update generated ruleset, add rules for emails in the body (not all 419s 
+use reply-to)
 ------------------------------------------------------------------------
-r1720216 | jquinn | 2015-12-15 18:25:27 +0000 (Tue, 15 Dec 2015) | 1 line
+r1888867 | jhardin | 2021-04-17 18:40:55 +0000 (Sat, 17 Apr 2021) | 1 line
  
- fix for username inconsistencies in bug 7191
+ Fixes to new phishing rules; Amazon occasionally doesn't have rDNS on an 
+MTA; remove some references to missing rules;
 ------------------------------------------------------------------------
-r1716143 | mmartinec | 2015-11-24 14:16:16 +0000 (Tue, 24 Nov 2015) | 1 
-line
+r1888843 | hege | 2021-04-17 06:42:38 +0000 (Sat, 17 Apr 2021) | 2 lines
+ Reduce locale dependency for tests
  
- Bug 7266: scheme name is case insensitive, digits 1-8 are allowed too
 ------------------------------------------------------------------------
-r1716140 | mmartinec | 2015-11-24 14:00:54 +0000 (Tue, 24 Nov 2015) | 1 
-line
+r1888837 | jhardin | 2021-04-17 02:16:01 +0000 (Sat, 17 Apr 2021) | 1 line
  
- Bug 7266 - no IPv6 address on sa-update.secnap.net - @af -> @my_af
+ Add some more FROM phishing rules for eval
 ------------------------------------------------------------------------
-r1716132 | mmartinec | 2015-11-24 13:51:03 +0000 (Tue, 24 Nov 2015) | 1 
-line
+r1888834 | jhardin | 2021-04-16 22:00:20 +0000 (Fri, 16 Apr 2021) | 1 line
  
- Bug 7266 - no IPv6 address on sa-update.secnap.net - missing semicolon, 
-tabs->space, aestetics
+ Add some rules for eval
 ------------------------------------------------------------------------
-r1715936 | jquinn | 2015-11-23 19:58:50 +0000 (Mon, 23 Nov 2015) | 1 line
+r1888828 | hege | 2021-04-16 15:12:36 +0000 (Fri, 16 Apr 2021) | 2 lines
+ Config parsing cleanups, also keep track of line numbers
  
- sa-update tries both ipv6 and ipv4
 ------------------------------------------------------------------------
-r1715248 | mmartinec | 2015-11-19 19:22:25 +0000 (Thu, 19 Nov 2015) | 1 
-line
+r1888808 | jhardin | 2021-04-16 02:22:10 +0000 (Fri, 16 Apr 2021) | 1 line
  
- Bug 7265: DNS resolving breaks with Net::DNS 1.03 - fixing Plugin/DKIM.pm
+ More Alibaba spammer tuning; add some rules for eval
 ------------------------------------------------------------------------
-r1715197 | mmartinec | 2015-11-19 15:31:49 +0000 (Thu, 19 Nov 2015) | 1 
-line
+r1888798 | hege | 2021-04-15 17:12:18 +0000 (Thu, 15 Apr 2021) | 2 lines
+ Bug 7848 - Rule parser doesn't support nested if/ifplugins
  
- Bug 7265: DNS resolving breaks with Net::DNS 1.03
 ------------------------------------------------------------------------
-r1714589 | kmcgrail | 2015-11-16 14:14:51 +0000 (Mon, 16 Nov 2015) | 1 
-line
+r1888787 | hege | 2021-04-15 11:10:55 +0000 (Thu, 15 Apr 2021) | 2 lines
+ Oops, normalize_charset was not supposed to change yet :-)
  
- Clarifying Copyright - bug 7263
 ------------------------------------------------------------------------
-r1714143 | mmartinec | 2015-11-12 23:59:41 +0000 (Thu, 12 Nov 2015) | 1 
-line
+r1888786 | hege | 2021-04-15 11:10:00 +0000 (Thu, 15 Apr 2021) | 2 lines
+ Document notrim flag
  
- Bug 7264 - Allow '(' and ')' in paths when untainting
 ------------------------------------------------------------------------
-r1713710 | jquinn | 2015-11-10 18:20:36 +0000 (Tue, 10 Nov 2015) | 1 line
+r1888778 | hege | 2021-04-14 20:16:46 +0000 (Wed, 14 Apr 2021) | 2 lines
+ Better pdftotext defaults
  
- Wrong SA version in readme
 ------------------------------------------------------------------------
-r1713709 | jquinn | 2015-11-10 18:15:48 +0000 (Tue, 10 Nov 2015) | 1 line
+r1888775 | hege | 2021-04-14 19:56:39 +0000 (Wed, 14 Apr 2021) | 2 lines
+ More ExtractText cleanups. Stop using double backslashes in 
+configuration.
  
- Windows option to enable building spamd
 ------------------------------------------------------------------------
-r1711889 | kmcgrail | 2015-11-02 03:27:24 +0000 (Mon, 02 Nov 2015) | 1 
-line
+r1888763 | hege | 2021-04-14 15:00:34 +0000 (Wed, 14 Apr 2021) | 2 lines
+ Update extracttext test
  
- Fix Credits File for a few international names
 ------------------------------------------------------------------------
-r1710612 | jquinn | 2015-10-26 15:03:14 +0000 (Mon, 26 Oct 2015) | 1 line
+r1888762 | hege | 2021-04-14 14:59:56 +0000 (Wed, 14 Apr 2021) | 2 lines
+ Cleanups, support envvars, fix tesseract hanging with 
+OMP_THREAD_LIMIT=1, add -nodrm for pdftohtml
  
- better handling of newlines in debug output
 ------------------------------------------------------------------------
-r1710602 | jquinn | 2015-10-26 14:05:56 +0000 (Mon, 26 Oct 2015) | 1 line
+r1888761 | hege | 2021-04-14 14:55:08 +0000 (Wed, 14 Apr 2021) | 2 lines
+ Update allowplugins list
  
- makefile that is nicer for windows
 ------------------------------------------------------------------------
-r1708487 | sidney | 2015-10-13 19:11:23 +0000 (Tue, 13 Oct 2015) | 1 line
+r1888760 | hege | 2021-04-14 14:03:27 +0000 (Wed, 14 Apr 2021) | 2 lines
+ Skip reading already read config files (often happens with the 
+--siteconfigpath/configpath/prefspath mess)
  
- bug 7251: merge from trunk - temp dir creation all using 
-Util::secure_tmpdir() instead of duplicating code and possibly 
-introducing bugs
 ------------------------------------------------------------------------
-r1706851 | jquinn | 2015-10-05 15:21:26 +0000 (Mon, 05 Oct 2015) | 1 line
+r1888759 | hege | 2021-04-14 13:51:55 +0000 (Wed, 14 Apr 2021) | 2 lines
+ Fix root tests
  
- decode MIME attachment names for better rule matching
 ------------------------------------------------------------------------
-r1698172 | jquinn | 2015-08-27 14:43:31 +0000 (Thu, 27 Aug 2015) | 1 line
+r1888757 | hege | 2021-04-14 08:51:33 +0000 (Wed, 14 Apr 2021) | 2 lines
+ 10FCV fixes
  
- Ugly fix for TxRep data being updated incorrectly
 ------------------------------------------------------------------------
-r1694126 | mmartinec | 2015-08-04 23:16:38 +0000 (Tue, 04 Aug 2015) | 1 
-line
+r1888754 | hege | 2021-04-14 07:29:41 +0000 (Wed, 14 Apr 2021) | 2 lines
+ Improve nfssafe locker performance with less wait, remove unneeded diag 
+message
  
- Bug 7231: Net::DNS 1.01 returns answers formatted differently, breaks SA
 ------------------------------------------------------------------------
-r1693640 | mmartinec | 2015-07-31 19:03:30 +0000 (Fri, 31 Jul 2015) | 1 
-line
+r1888720 | hege | 2021-04-13 10:42:01 +0000 (Tue, 13 Apr 2021) | 2 lines
+ Fix previous fix
  
- Plugin::DKIM warning: Redundant argument in printf
 ------------------------------------------------------------------------
-r1693414 | mmartinec | 2015-07-30 11:45:48 +0000 (Thu, 30 Jul 2015) | 1 
-line
+r1888719 | hege | 2021-04-13 10:27:19 +0000 (Tue, 13 Apr 2021) | 2 lines
+ Stopword fixes and cleanups
  
- Bug 7226: Enhance whitelist_from_dkim to let it accept signing subdomains
 ------------------------------------------------------------------------
-r1691992 | mmartinec | 2015-07-20 18:24:48 +0000 (Mon, 20 Jul 2015) | 1 
-line
+r1888718 | hege | 2021-04-13 10:22:18 +0000 (Tue, 13 Apr 2021) | 2 lines
+ Remove pointless message
  
- Bug 7223: Net::DNS 1.01 breaks DnsResolver
 ------------------------------------------------------------------------
-r1688247 | jquinn | 2015-06-29 15:03:27 +0000 (Mon, 29 Jun 2015) | 1 line
+r1888690 | jhardin | 2021-04-13 02:17:38 +0000 (Tue, 13 Apr 2021) | 1 line
  
- anchored txrep relay helo check for extra safety
+ Remove RCVD_IN_RP_* transition rules; add references to old rule names 
+to RCVD_IN_VALIDITY_* "reuse" tag
 ------------------------------------------------------------------------
-r1688201 | jquinn | 2015-06-29 13:11:21 +0000 (Mon, 29 Jun 2015) | 1 line
+r1888675 | hege | 2021-04-12 13:32:05 +0000 (Mon, 12 Apr 2021) | 2 lines
+ Add spamcop_relayhost option (Bug 5402)
  
- fix for txrep sql that is not valid postgres
 ------------------------------------------------------------------------
-r1687548 | hege | 2015-06-25 15:14:03 +0000 (Thu, 25 Jun 2015) | 2 lines
+r1888670 | hege | 2021-04-12 12:18:13 +0000 (Mon, 12 Apr 2021) | 2 lines
  
- Bug 7216: TextCat documentation enhancement and _TEXTCATRESULTS_ tag
+ Prevent flooding on setuid error
  
 ------------------------------------------------------------------------
-r1686458 | mmartinec | 2015-06-19 17:11:38 +0000 (Fri, 19 Jun 2015) | 1 
-line
+r1888666 | hege | 2021-04-12 11:28:42 +0000 (Mon, 12 Apr 2021) | 2 lines
+ Remove BUG6152_INVALID_DATE_TZ_ABSURD (Bug 7812)
  
- Bug 7213: UNPARSEABLE_RELAY false positive on valid 'LHLO ... LMTP' 
-header
 ------------------------------------------------------------------------
-r1685843 | kmcgrail | 2015-06-16 14:17:18 +0000 (Tue, 16 Jun 2015) | 1 
-line
+r1888665 | hege | 2021-04-12 11:26:48 +0000 (Mon, 12 Apr 2021) | 2 lines
+ Remove BUG6152_INVALID_DATE_TZ_ABSURD (Bug 7812)
  
- Fixed uninitialized error with $line - Thanks to Franz Schwartau bug 
-7212 - 3.4
 ------------------------------------------------------------------------
-r1684652 | kmcgrail | 2015-06-10 12:15:22 +0000 (Wed, 10 Jun 2015) | 1 
-line
+r1888663 | hege | 2021-04-12 11:14:14 +0000 (Mon, 12 Apr 2021) | 2 lines
+ Bug 7835 - enable notrim for DBL and SURBL uribls
  
- reverting accidental commit
 ------------------------------------------------------------------------
-r1684648 | kmcgrail | 2015-06-10 12:03:35 +0000 (Wed, 10 Jun 2015) | 1 
-line
+r1888657 | sidney | 2021-04-12 10:14:26 +0000 (Mon, 12 Apr 2021) | 1 line
  
- referencing SF account in build/README for consideration at next release
+ 3.4.6 announcement file with actual checksums
 ------------------------------------------------------------------------
-r1684226 | jquinn | 2015-06-08 17:11:20 +0000 (Mon, 08 Jun 2015) | 1 line
+r1888650 | hege | 2021-04-12 07:47:55 +0000 (Mon, 12 Apr 2021) | 2 lines
+ Remove executable prop
  
- Typo in TxRep.pm
 ------------------------------------------------------------------------
-r1681230 | kmcgrail | 2015-05-22 20:18:31 +0000 (Fri, 22 May 2015) | 1 
-line
+r1888649 | hege | 2021-04-12 07:43:12 +0000 (Mon, 12 Apr 2021) | 2 lines
+ Major test cleanups. Parallel testing supported (HARNESS_OPTIONS=j8 make 
+test), configuration t/testrules.yml.
  
- Cleanup of spamd-apache2 for httpd 2.4 - bug 7197 - 3.4
 ------------------------------------------------------------------------
-r1681095 | kmcgrail | 2015-05-22 12:45:48 +0000 (Fri, 22 May 2015) | 1 
-line
+r1888632 | hege | 2021-04-11 18:07:11 +0000 (Sun, 11 Apr 2021) | 2 lines
+ dcc/pyzor/razor2 settings should be is_admin
  
- Cleaning up the copyright/author/etc. in apache-spamd to be ASF compliant
 ------------------------------------------------------------------------
-r1679656 | mmartinec | 2015-05-15 23:01:12 +0000 (Fri, 15 May 2015) | 1 
-line
+r1888615 | hege | 2021-04-11 10:25:12 +0000 (Sun, 11 Apr 2021) | 2 lines
+ Check that search paths are directories
  
- Bug 7195: Util warnings from trim_domain()
 ------------------------------------------------------------------------
-r1679653 | mmartinec | 2015-05-15 22:30:39 +0000 (Fri, 15 May 2015) | 1 
-line
+r1888612 | hege | 2021-04-11 09:50:35 +0000 (Sun, 11 Apr 2021) | 2 lines
+ Fix TextCat languages-file search paths
  
- Bug 7196: PerMsgStatus Warning
 ------------------------------------------------------------------------
-r1679280 | kmcgrail | 2015-05-13 21:28:52 +0000 (Wed, 13 May 2015) | 1 
-line
+r1888607 | hege | 2021-04-11 07:51:37 +0000 (Sun, 11 Apr 2021) | 2 lines
+ Special wildcard "dns_query_restriction deny *" is supported to block 
+all queries except allowed ones.
  
- adds --timing parm for spamd for 3.4 branch - bug 7194
 ------------------------------------------------------------------------
-r1678016 | kmcgrail | 2015-05-06 14:59:03 +0000 (Wed, 06 May 2015) | 1 
-line
+r1888604 | hege | 2021-04-11 06:45:03 +0000 (Sun, 11 Apr 2021) | 2 lines
+ Update long_running_tests
  
- bug 7164 - 3.4 commit - small clean up on whitespace/logic for clarity 
-and added logic for msgscore undefined in Txrep.pm
 ------------------------------------------------------------------------
-r1677162 | kmcgrail | 2015-05-01 14:53:15 +0000 (Fri, 01 May 2015) | 1 
-line
+r1888591 | hege | 2021-04-10 16:20:35 +0000 (Sat, 10 Apr 2021) | 2 lines
+ Sync 3.4.5 UPGRADE notes
  
- 3.4 Commit for bug 7186 lowering TVD_PH_BODY_ACCOUNTS_PRE scores to 0.001
 ------------------------------------------------------------------------
-r1676888 | kmcgrail | 2015-04-30 03:03:36 +0000 (Thu, 30 Apr 2015) | 1 
-line
+r1888587 | hege | 2021-04-10 14:46:01 +0000 (Sat, 10 Apr 2021) | 2 lines
+ ExtractText code cleanups and fixes
  
- 3.4 commit - adding compress header debug statement - bug 7183
 ------------------------------------------------------------------------
-r1676800 | kmcgrail | 2015-04-29 17:19:13 +0000 (Wed, 29 Apr 2015) | 1 
-line
+r1888586 | hege | 2021-04-10 14:42:02 +0000 (Sat, 10 Apr 2021) | 2 lines
+ Support custom STDERR redirect in helper_app_pipe_open()
  
- unifying 3.4 branch build readme with trunk
 ------------------------------------------------------------------------
-r1676787 | kmcgrail | 2015-04-29 16:36:50 +0000 (Wed, 29 Apr 2015) | 1 
-line
+r1888576 | hege | 2021-04-10 09:40:38 +0000 (Sat, 10 Apr 2021) | 2 lines
+ Use ASN metadata for Bayes correctly (Bug 5655)
  
- 3.4 branch Commit of Bug 7182 for raise from 15 to 20
 ------------------------------------------------------------------------
-r1676620 | kmcgrail | 2015-04-28 20:38:08 +0000 (Tue, 28 Apr 2015) | 1 
-line
+r1888573 | hege | 2021-04-10 08:25:07 +0000 (Sat, 10 Apr 2021) | 2 lines
+ Change ArchiveIterator/sa-learn maximum message size 256->500 KB to 
+match more modern spamc default
  
- 3.4.2 devel cycle started
 ------------------------------------------------------------------------
-r1676616 | kmcgrail | 2015-04-28 20:36:05 +0000 (Tue, 28 Apr 2015) | 1 
-line
+r1888569 | jhardin | 2021-04-10 02:03:13 +0000 (Sat, 10 Apr 2021) | 1 line
  
- Creating 3.4 branch based on 3.4.1 so that trunk can go to 4.0
+ More Alibaba spammer jousting
 ------------------------------------------------------------------------
index eeba4f3a9465294cbd946997838d431ccb08b95a..8a3e74b593ae7b16a5bc2dc1c92cc37ce34c8f00 100644 (file)
@@ -11,16 +11,16 @@ Installing or Upgrading SpamAssassin
 
 Using CPAN via CPAN.pm:
 
-       perl -MCPAN -e shell                    [as root]
+       perl -MCPAN -e shell  # as root
        o conf prerequisites_policy ask
        install Mail::SpamAssassin
        quit
 
 Using Linux:
 
-       Debian unstable: apt-get install spamassassin
+       Debian/Ubuntu: apt-get install spamassassin
        Gentoo: emerge mail-filter/spamassassin
-       Fedora: yum install spamassassin
+       Fedora/CentOS/RedHat: yum install spamassassin
 
 Alternatively download the tarfile, zipfile, and/or build your own RPM
 from https://spamassassin.apache.org/.  Building from tar/zip file is
@@ -28,10 +28,9 @@ usually as simple as:
 
        [unzip/untar the archive]
        cd Mail-SpamAssassin-*
-       perl Makefile.PL
-       [option: add -DSPAMC_SSL to $CFLAGS to build an SSL-enabled spamc]
+       perl Makefile.PL  # add ENABLE_SSL=yes for SSL support
        make
-       make install                            [as root]
+       make install  # as root
 
 After installing SpamAssassin, you need to download and install the
 SpamAssassin ruleset using "sa-update".  See the "Installing Rules"
@@ -42,16 +41,10 @@ the prerequisite information further down.
 
 To install as non-root, see the directions below.
 
-If you are running AFS, you may also need to specify INSTALLSITELIB and
-SITELIBEXP.
-
 Note that you can upgrade SpamAssassin using these instructions, as long
 as you take care to read the caveats in the file UPGRADE.   Upgrading
 will not delete your learnt Bayes data or local rule modifications.
 
-If you're using SunOS 4.1.x, see
-http://wiki.spamassassin.org/w/BuildingOnSunOS4 for build tips.
-
 
 Installing SpamAssassin for Personal Use (Not System-Wide)
 ----------------------------------------------------------
@@ -62,7 +55,7 @@ These steps assume the following, so substitute as necessary:
   - The location of the procmail executable is /usr/bin/procmail
 
 Many more details of this process are at
-http://wiki.apache.org/spamassassin/SingleUserUnixInstall
+https://wiki.apache.org/spamassassin/SingleUserUnixInstall
 
 1. Uncompress and extract the SpamAssassin archive, using "unzip" or
    "tar xvfz", in a temporary directory.
@@ -119,7 +112,7 @@ http://wiki.apache.org/spamassassin/SingleUserUnixInstall
 caughtspam
 
 Also, see the file procmailrc.example and
-http://wiki.apache.org/spamassassin/UsedViaProcmail
+https://wiki.apache.org/spamassassin/UsedViaProcmail
 
 8. Now, you should be ready to send some test emails and ensure everything
    works as expected.  First, send yourself a test email that doesn't
@@ -136,7 +129,7 @@ http://wiki.apache.org/spamassassin/UsedViaProcmail
    don't lose incoming email.
 
    Note: one possible cause for this is the use of smrsh on the MTA system;
-   see http://wiki.spamassassin.org/w/ProcmailVsSmrsh for details.
+   see https://wiki.apache.org/spamassassin/ProcmailVsSmrsh for details.
 
 9. You can now customize SpamAssassin.  See README for more information.
 
@@ -153,18 +146,25 @@ Installing rules from network is done with a single command:
 
         sa-update
 
-This is normally run as root.
+For security reasons, it should not be run as root, but as the user normally
+running SpamAssassin.  You can run the initial setup once as root, to create
+necessary directories etc.  Then you need to change ownership of
+LOCAL_STATE_DIR to that user (usually: chown -R user:user
+/var/lib/spamassassin), you can find out the default directory with
+sa-update --help (look for --updatedir).  Same needs to be done for
+LOCAL_RULES_DIR/sa-update-keys (usually: chown -R user:user
+/etc/mail/spamassassin/sa-update-keys), the directory can be found with
+spamassassin --help (look for --siteconfigpath).
 
 If you wish to install rules from downloaded files, rather than "live" from
 the latest online ruleset, here is how to do it.
 
-Obtain all the following files:
+Obtain all the following files from https://spamassassin.apache.org/downloads.cgi:
 
     Mail-SpamAssassin-rules-xxx.tgz
     Mail-SpamAssassin-rules-xxx.tgz.asc
-    Mail-SpamAssassin-rules-xxx.tgz.md5
-    Mail-SpamAssassin-rules-xxx.tgz.sha1
-      (where xxx may look something like '3.3.0-rc1.r893295')
+    Mail-SpamAssassin-rules-xxx.tgz.sha512
+      (where xxx may look something like '4.0.0.r1900144')
 
 Save them all to the current directory.
 Obtain a rules-signing public key:
@@ -180,7 +180,7 @@ Install rules from a compressed tar archive:
 
     sa-update --install Mail-SpamAssassin-rules-xxx.tgz
 
-Note that the ".tgz.asc", ".tgz.md5" and ".tgz.sha1" files all need to
+Note that the ".tgz", ".tgz.asc" and ".tgz.sha512" files all need to
 be in the same directory, otherwise sa-update will fail.
 
 
@@ -197,7 +197,7 @@ CPAN
 ----
 
 Most of the modules listed below are available via the Comprehensive Perl
-Archive Network (CPAN, see http://www.cpan.org/ for more information).
+Archive Network (CPAN, see https://www.cpan.org/ for more information).
 While each module is different, most can be installed via a few simple
 commands such as:
 
@@ -222,11 +222,7 @@ through those mechanisms, too, if you prefer.
 Required Perl Interpreter
 -------------------------
 
-Perl 5.8.1 or a later version is required.
-Preferred versions are 5.8.8, or 5.10.1 or later.
-
-Most of the functions might still work with Perl 5.6.1 or 5.6.2,
-but 5.6.* is no longer a supported version.
+Perl 5.14.0 or a later version is required.
 
 
 Required Perl Modules
@@ -243,81 +239,41 @@ SpamAssassin build directory.
 The list of required modules that do not ship with Perl and must be
 installed:
 
-  - Digest::SHA1 (from CPAN),
-    or the newer Digest::SHA which is a perl base module since Perl 5.10.0
+  - Digest::SHA (from CPAN)
 
-    The Digest::SHA1 module is used as a cryptographic hash for some
-    tests and the Bayes subsystem if Digest::SHA module is not available.
-
-    An external perl module razor-agents-2.84 as used by a Razor2 plugin
-    seems to be the only remaining component depending on Digest::SHA1
-    (note that a packager may ship a patched version of razor-agents which
-    can use Digest::SHA instead)
-
-    Debian: apt-get install libdigest-sha1-perl
-    Gentoo: emerge dev-perl/Digest-SHA1
-    Fedora: yum install perl-Digest-SHA1
+    Used as a cryptographic hash for some tests and the Bayes subsystem.
+    It is also required by the DKIM plugin.
 
   - HTML::Parser >= 3.43 (from CPAN)
 
     HTML is used for an ever-increasing amount of email so this dependency
     is unavoidable.  Run "perldoc -q html" for additional information.
 
-    Debian: apt-get install libhtml-parser-perl
-    Gentoo: emerge dev-perl/HTML-Parser
-    Fedora: yum install perl-HTML-Parser
-
-  - Net::DNS (from CPAN)
+  - Net::DNS >= 0.69 (from CPAN)
 
     Used for all DNS-based tests (SBL, XBL, SpamCop, DSBL, etc.),
     perform MX checks, used when manually reporting spam to SpamCop,
     and used by sa-update to gather version information.
 
-    You need to make sure the Net::DNS version is sufficiently up-to-date:
-
-      - version 0.34 or higher on Unix systems
-      - version 0.46 or higher on Windows systems
-
-    Debian/Ubuntu: apt-get install libnet-dns-perl
-    Fedora: yum install perl-Net-DNS
-
-  - NetAddr::IP (from CPAN)
-
-    Used to parse IP addresses and IP address ranges for
-    "trusted_networks".
-
-    Debian/Ubuntu: apt-get install libnetaddr-ip-perl
-    Fedora: yum install perl-NetAddr-IP
-
-  - Time::HiRes (from CPAN)
-
-    Used by asynchronous DNS lookups to operate timeouts with subsecond
-    precision and to report processing times accurately.
+  - NetAddr::IP >= 4.010 (from CPAN)
 
-  - LWP (aka libwww-perl) (from CPAN)
+    Used to parse IP addresses and IP address ranges for "trusted_networks". 
+    Used in determining which DNS tests are to be done for each of the
+    header's received fields.  Used by AWL plugin for extracting network
+    addresses.  Used by DNSxL rules for assembling DNS queries.
 
-    This set of modules will include both the LWP::UserAgent and
-    HTTP::Date modules, used by sa-update to retrieve update archives.
+    Avoid buggy versions 4.034-4.035 and 4.045-4.054.
 
-    Fedora: yum install perl-libwww-perl
+Examples of installing required modules on popular distributions:
 
-  - HTTP::Date (from CPAN)
+ Debian/Ubuntu:
+  apt-get install libdigest-sha-perl libhtml-parser-perl libnet-dns-perl libnetaddr-ip-perl
 
-    Used by sa-update to deal with certain Date requests.
+ Gentoo:
+  emerge dev-perl/Digest-SHA dev-perl/HTML-Parser dev-perl/Net-DNS dev-perl/NetAddr-IP
 
-  - IO::Zlib (from CPAN)
-
-    Used by sa-update to uncompress update archives.
-    Version 1.04 or later is required.
-
-    Fedora: yum install perl-IO-Zlib
-
-  - Archive::Tar (from CPAN)
-
-    Used by sa-update to expand update archives.
-    Version 1.23 or later is required.
-
-    Fedora: yum install perl-Archive-Tar
+ Fedora/CentOS/RedHat:
+  yum install perl-Digest-SHA perl-HTML-Parser perl-Net-DNS perl-NetAddr-IP
 
 
 Optional Modules
@@ -329,140 +285,161 @@ their version is too low, SpamAssassin will still work, just not as
 effectively because some of the spam-detection tests will have to be
 skipped.
 
-Note: SpamAssassin will not warn you if these are installed, but the
-version is too low for them to be used.
+Note: SpamAssassin may not warn you if these are installed, but the version
+is too low for them to be used.
 
-  - MIME::Base64
+  - MIME::Base64 (from CPAN)
 
     This module is highly recommended to increase the speed with which
     Base64 encoded messages/mail parts are decoded.
 
+  - Encode::Detect::Detector (from CPAN)
 
-  - DB_File (from CPAN, included in many distributions)
+    For proper detection of charsets and converting them into Unicode, you
+    will need to install this module.
 
-    Used to store data on-disk, for the Bayes-style logic, TxRep, and
-    auto-whitelist.  *Much* more efficient than the other standard Perl
-    database packages.  Strongly recommended.
+  - Net::LibIDN2 (from CPAN)
+  - Net::LibIDN (from CPAN)
 
-    There seems to be a bug in libdb 4.1.25, which is
-    distributed by default on some versions of Linux.  See
-    http://wiki.apache.org/spamassassin/DbFileSleepBug for details.
+    Provides mapping between Internationalized Domain Names (IDN) in Unicode
+    and ASCII-compatible encoding (ACE) for use in DNS and comparisions. 
+    The module is optional, but without it Unicode IDN names found in mail
+    will not be suitable for DNS queries and welcome/blocklisting.
 
+    Either module should work fine, but newer Net::LibIDN2 might not be
+    available in all distributions.
 
-  - Net::SMTP (from CPAN)
+  - Email::Address::XS
 
-    Used when manually reporting spam to SpamCop.
+    Used to parse email addresses from header fields like To/From/cc, per
+    RFC 5322.  If installed, it may additionally be used by internal parser
+    to process complex lists.
 
+  - Mail::DKIM (from CPAN)
 
+    If this module is installed, and you enable the DKIM plugin,
+    SpamAssassin will perform DKIM lookups when a DKIM-Signature header is
+    present in the message headers.  Current versions of Mail::DKIM (0.20 or
+    later) also perform Domain Key lookups on DomainKey-Signature headers,
+    without requiring the Mail::DomainKeys module, which is now obsolete. 
+    Version 0.37 or later is preferred, the absolute minimal version is
+    0.31.
+    
   - Mail::SPF (from CPAN)
 
     Used to check DNS Sender Policy Framework (SPF) records to fight email
-    address forgery and make it easier to identify spams.  This module
-    makes Mail::SPF::Query obsolete.
-
-    Net::DNS version 0.58 or higher is required.
+    address forgery and make it easier to identify spams.
 
-    Note that NetAddr::IP (required by Mail::SPF) versions up to and
-    including version 4.006 include a bug that will slow down the entire
-    perl interpreter.  NetAddr::IP version 4.007 or later fixes this.
+  - MaxMind::DB::Reader::XS (GeoIP2) (from CPAN)
+  - MaxMind::DB::Reader (GeoIP2) (from CPAN)
+  - IP::Country::DB_File (from CPAN)
+  - Geo::IP (old deprecated GeoIP) (from CPAN)
+  - IP::Country::Fast (deprecated) (from CPAN)
 
-
-  - Geo::IP (from CPAN)
+    Geolocation modules, choose one from the list (in recommended order).
 
     Used by the RelayCountry plugin (not enabled by default) to determine
-    the domain country codes of each relay in the path of an email.
-
-    IP::Country::Fast is used as alternative if Geo::IP is not installed.
-    This is not recommended as it's obsolete.
+    the domain country codes of each relay in the path of an email.  Also
+    used by the URILocalBL plugin (not enabled by default) to provide ISP
+    and Country code based filtering.
 
+    See: https://wiki.apache.org/spamassassin/RelayCountryPlugin
 
-  - Net::Ident (from CPAN)
+  - Mail::DMARC
 
-    If you plan to use the --auth-ident option to spamd, you will need
-    to install this module.
+    Used by the optional DMARC check plugin, which itself requires DKIM and
+    SPF features working.
 
+  - DB_File (from CPAN)
 
-  - IO::Socket::INET6 (from CPAN)
+    Used to store data on-disk, for the Bayes-style logic, TxRep, and
+    auto-welcomelist.  *Much* more efficient than the other standard Perl
+    database packages.  Strongly recommended.
 
-    This is required if the first nameserver listed in your IP
-    configuration or /etc/resolv.conf file is available only via an IPv6
-    address.
+    There seems to be a bug in libdb 4.1.25, which is
+    distributed by default on some versions of Linux.  See
+    https://wiki.apache.org/spamassassin/DbFileSleepBug for details.
 
-    Fedora: yum install perl-IO-Socket-INET6
+  - IO::Socket::IP (from CPAN)
+  - IO::Socket::INET6 (from CPAN)
 
+    Installing IO::Socket::IP is recommended if spamd is to listen on IPv6
+    sockets or if DNS queries should go to an IPv6 name server.  If
+    IO::Socket::IP is not available, using a deprecated module
+    IO::Socket::INET6 will be attempted, and in its absence the support for
+    IPv6 will not be available.  Some plugins and underlying modules may
+    also prefer IO::Socket::IP over IO::Socket::INET6.
 
   - IO::Socket::SSL (from CPAN)
 
-    If you wish to use SSL encryption to communicate between spamc and
-    spamd (the --ssl option to spamd), you need to install this
-    module. (You will need the OpenSSL libraries and use the
-    ENABLE_SSL="yes" argument to Makefile.PL to build and run an SSL
-    compatible spamc.)
-
-    Fedora: yum install perl-IO-Socket-SSL
-
-
-  - Compress::Zlib (from CPAN)
-
-    If you wish to use the optional zlib compression for communication
-    between spamc and spamd (the -z option to spamc), useful for
-    long-distance use of spamc over the internet, you need to install
-    this module.
+    If you wish to use SSL encryption to communicate between spamc and spamd
+    (the --ssl option to spamd), you need to install this module.  (You will
+    need the OpenSSL libraries and use the ENABLE_SSL=yes argument to
+    Makefile.PL to build and run an SSL compatible spamc.)
 
-    Fedora: yum install perl-Compress-Zlib
-
-
-  - Mail::DKIM (from CPAN)
-
-    If this module is installed, and you enable the DKIM plugin,
-    SpamAssassin will perform DKIM lookups when a DKIM-Signature header is
-    present in the message headers.  Current versions of Mail::DKIM (0.20
-    or later) also perform Domain Key lookups on DomainKey-Signature headers,
-    without requiring the Mail::DomainKeys module, which is now obsolete.
-    Version 0.37 or later is preferred, the absolute minimal version is 0.31.
-    
-    Note that the Mail::DKIM module in turn requires the Digest::SHA module
-    and OpenSSL libraries.
+  - Net::Patricia
 
+    If this module is available, it will be used for IP address lookups in
+    tables internal_networks, trusted_networks, and msa_networks. 
+    Recommended when a number of entries in these tables is hundred or more. 
+    However, in case of overlapping (or conflicting) networks in these
+    tables, lookup results may differ as Net::Patricia finds a
+    tightest-matching entry, while a sequential NetAddr::IP search finds a
+    first-matching entry.  So when overlapping network ranges are given,
+    specifying more specific subnets (longest netmask) first, followed by
+    wider subnets ensures predictable results.
 
   - DBI *and* DBD driver/modules for your database (from CPAN)
 
-    If you intend to use SpamAssassin with an SQL database backend for
-    user configuration data, Bayes storage, or other storage, you will need
-    to have these installed; both the basic DBI module and the driver for
-    your database.
+    If you intend to use SpamAssassin with an SQL database backend for user
+    configuration data, Bayes storage, or other storage, you will need to
+    have these installed; both the basic DBI module and the driver for your
+    database (for example DBD::MariaDB, DBD::mysql or DBD::Pg).
 
+  - Archive::Zip
+  - IO::String
 
-  - Encode::Detect (from CPAN)
+    Required by the optional OLEVBMacro plugin.
 
-    If you plan to use the normalize_charset config setting to detect
-    charsets and convert them into Unicode, you will need to install
-    this module.
+  - Razor2
+
+    If you plan to use Vipul's Razor, note that versions up to and including
+    version 2.82 include a bug that will slow down the entire perl
+    interpreter.  Version 2.83 or later fixes this.
 
+    If you do not plan to use this plugin, be sure to comment out its
+    loadplugin line in "/etc/mail/spamassassin/v310.pre".
 
-  - Apache::Test (from CPAN)
+  - Digest::SHA1 (from CPAN)
 
-    If you plan to run the Apache2 version of spamd in the
-    "spamd-apache2" directory, you will need to install this
-    module.
+    An external perl module razor-agents-2.84 as used by a Razor2 plugin
+    seems to be the only remaining component depending on Digest::SHA1 (note
+    that a packager may ship a patched version of razor-agents which can use
+    Digest::SHA instead)
 
+  - LWP::UserAgent (aka libwww-perl) (from CPAN)
 
-  - Apache 2 and mod_perl
+    Can be used by sa-update to retrieve update archives, as alternative to
+    curl/wget/fetch.
 
-    If you plan to run the Apache2 version of spamd in the "spamd-apache2"
-    directory, you will need to ensure these are installed.
+  - Net::SMTP (from CPAN)
 
-    Ubuntu: sudo apt-get install apache2 libapache2-mod-perl2
+    Used when manually reporting spam to SpamCop.
 
+Examples of installing most recommended modules on popular distributions:
 
-  - Razor2
+ Debian/Ubuntu:
+  apt-get install libencode-detect-perl libnet-libidn-perl \
+    libemail-address-xs-perl libmail-dkim-perl libmail-spf-perl \
+    libio-socket-ip-perl
 
-    If you plan to use Vipul's Razor, note that versions up to and
-    including version 2.82 include a bug that will slow down the entire
-    perl interpreter.  Version 2.83 or later fixes this.
+ Gentoo:
+  emerge dev-perl/Encode-Detect dev-perl/Net-LibIDN \
+    dev-perl/Email-Address-XS dev-perl/Mail-DKIM dev-perl/Mail-SPF
 
-    If you do not plan to use this plugin, be sure to comment out
-    its loadplugin line in "/etc/mail/spamassassin/v310.pre".
+ Fedora/CentOS/RedHat:
+  yum install perl-MIME-Base64 perl-Encode-Detect perl-Net-LibIDN \
+    perl-Email-Address-XS perl-Mail-DKIM perl-Mail-SPF perl-IO-Socket-IP
 
 
 What Next?
@@ -471,7 +448,7 @@ What Next?
 Take a look at the USAGE document for more information on how to use
 SpamAssassin.
 
-The SpamAssassin Wiki <http://wiki.spamassassin.org/> contains
+The SpamAssassin Wiki <https://wiki.apache.org/spamassassin/> contains
 information on custom plugins, extensions, and other optional modules
 included with SpamAssassin.
 
index 534b7794ad28752f244709cb168d3a05bb02c707..c18b0a3903f7cb3d264002d90500d71547df4d35 100644 (file)
@@ -30,6 +30,6 @@ Notes on building SpamAssassin on VMS
 
   - bug 1099 in the SA Bugzilla is being used to track progress.
 
-        http://issues.apache.org/SpamAssassin/show_bug.cgi?id=1099
+        https://issues.apache.org/SpamAssassin/show_bug.cgi?id=1099
 
 
index 7b1bab221ce906e626ece6dc2e6860f9ddbd3978..673ec4bf8f1eaa429655ced640e26c6138519812 100644 (file)
@@ -27,7 +27,7 @@ lib/Mail/SpamAssassin.pm
 lib/Mail/SpamAssassin/AICache.pm
 lib/Mail/SpamAssassin/ArchiveIterator.pm
 lib/Mail/SpamAssassin/AsyncLoop.pm
-lib/Mail/SpamAssassin/AutoWhitelist.pm
+lib/Mail/SpamAssassin/AutoWelcomelist.pm
 lib/Mail/SpamAssassin/Bayes.pm
 lib/Mail/SpamAssassin/Bayes/CombineChi.pm
 lib/Mail/SpamAssassin/Bayes/CombineNaiveBayes.pm
@@ -49,6 +49,7 @@ lib/Mail/SpamAssassin/Constants.pm
 lib/Mail/SpamAssassin/DBBasedAddrList.pm
 lib/Mail/SpamAssassin/Dns.pm
 lib/Mail/SpamAssassin/DnsResolver.pm
+lib/Mail/SpamAssassin/GeoDB.pm
 lib/Mail/SpamAssassin/HTML.pm
 lib/Mail/SpamAssassin/Locales.pm
 lib/Mail/SpamAssassin/Locker.pm
@@ -74,20 +75,23 @@ lib/Mail/SpamAssassin/Plugin/ASN.pm
 lib/Mail/SpamAssassin/Plugin/AWL.pm
 lib/Mail/SpamAssassin/Plugin/AccessDB.pm
 lib/Mail/SpamAssassin/Plugin/AntiVirus.pm
+lib/Mail/SpamAssassin/Plugin/AuthRes.pm
 lib/Mail/SpamAssassin/Plugin/AutoLearnThreshold.pm
 lib/Mail/SpamAssassin/Plugin/Bayes.pm
 lib/Mail/SpamAssassin/Plugin/BodyEval.pm
 lib/Mail/SpamAssassin/Plugin/BodyRuleBaseExtractor.pm
 lib/Mail/SpamAssassin/Plugin/Check.pm
 lib/Mail/SpamAssassin/Plugin/DCC.pm
+lib/Mail/SpamAssassin/Plugin/DecodeShortURLs.pm
 lib/Mail/SpamAssassin/Plugin/DKIM.pm
+lib/Mail/SpamAssassin/Plugin/DMARC.pm
 lib/Mail/SpamAssassin/Plugin/DNSEval.pm
+lib/Mail/SpamAssassin/Plugin/ExtractText.pm
 lib/Mail/SpamAssassin/Plugin/FreeMail.pm
 lib/Mail/SpamAssassin/Plugin/FromNameSpoof.pm
+lib/Mail/SpamAssassin/Plugin/HashBL.pm
 lib/Mail/SpamAssassin/Plugin/HTMLEval.pm
 lib/Mail/SpamAssassin/Plugin/HTTPSMismatch.pm
-lib/Mail/SpamAssassin/Plugin/Hashcash.pm
-lib/Mail/SpamAssassin/Plugin/HashBL.pm
 lib/Mail/SpamAssassin/Plugin/HeaderEval.pm
 lib/Mail/SpamAssassin/Plugin/ImageInfo.pm
 lib/Mail/SpamAssassin/Plugin/MIMEEval.pm
@@ -116,7 +120,7 @@ lib/Mail/SpamAssassin/Plugin/URIDetail.pm
 lib/Mail/SpamAssassin/Plugin/URIEval.pm
 lib/Mail/SpamAssassin/Plugin/VBounce.pm
 lib/Mail/SpamAssassin/Plugin/WLBLEval.pm
-lib/Mail/SpamAssassin/Plugin/WhiteListSubject.pm
+lib/Mail/SpamAssassin/Plugin/WelcomeListSubject.pm
 lib/Mail/SpamAssassin/PluginHandler.pm
 lib/Mail/SpamAssassin/Plugin/URILocalBL.pm
 lib/Mail/SpamAssassin/RegistryBoundaries.pm
@@ -132,7 +136,6 @@ lib/Mail/SpamAssassin/Util/ScopedTimer.pm
 lib/Mail/SpamAssassin/Util/TieOneStringHash.pm
 lib/spamassassin-run.pod
 procmailrc.example
-rules.README
 rules/active.list
 rules/init.pre
 rules/languages
@@ -148,7 +151,10 @@ rules/v340.pre
 rules/v341.pre
 rules/v342.pre
 rules/v343.pre
+rules/v400.pre
 rules/20_aux_tlds.cf
+rules-extras/README.txt
+rules-extras/10_uridnsbl_skip_financial.cf
 sa-awl.raw
 sa-check_spamd.raw
 sa-compile.raw
@@ -222,6 +228,9 @@ sql/awl_mysql.sql
 sql/awl_pg.sql
 sql/bayes_mysql.sql
 sql/bayes_pg.sql
+sql/decodeshorturl_mysql.sql
+sql/decodeshorturl_pg.sql
+sql/decodeshorturl_sqlite.sql
 sql/userpref_mysql.sql
 sql/userpref_pg.sql
 sql/txrep_mysql.sql
@@ -231,10 +240,13 @@ t/README
 t/SATest.pl
 t/SATest.pm
 t/all_modules.t
+t/askdns.t
+t/authres.t
 t/autolearn.t
 t/autolearn_force.t
 t/autolearn_force_fail.t
 t/basic_lint.t
+t/basic_lint_net.t
 t/basic_lint_without_sandbox.t
 t/basic_meta.t
 t/basic_meta2.t
@@ -246,6 +258,7 @@ t/bayessdbm.t
 t/bayessdbm_seen_delete.t
 t/bayessql.t
 t/blacklist_autolearn.t
+t/blocklist_autolearn.t
 t/body_mod.t
 t/body_str.t
 t/check_implemented.t
@@ -256,10 +269,13 @@ t/config_text.t
 t/config_tree_recurse.t
 t/cpp_comments_in_spamc.t
 t/cross_user_config_leak.t
-t/olevbmacro.t
+t/dmarc.t
+t/arc.t
 t/data/01_test_rules.cf
 t/data/01_test_rules.pre
 t/data/Dumpheaders.pm
+t/data/dkim/arc/ok01.eml
+t/data/dkim/arc/ko01.eml
 t/data/dkim/test-adsp-11.msg
 t/data/dkim/test-adsp-12.msg
 t/data/dkim/test-adsp-13.msg
@@ -307,6 +323,15 @@ t/data/dkim/test-pass-23.msg
 t/data/etc/hello.txt
 t/data/etc/testhost.cert
 t/data/etc/testhost.key
+t/data/geodb/GeoIP2-City.mmdb
+t/data/geodb/GeoIP2-Country.mmdb
+t/data/geodb/GeoIP2-ISP.mmdb
+t/data/geodb/GeoIPCity.dat
+t/data/geodb/GeoIPISP.dat
+t/data/geodb/create_GeoIPCity.README
+t/data/geodb/create_GeoIPISP.README
+t/data/geodb/create_ipcc.sh
+t/data/geodb/ipcc.db
 t/data/mime-subject.txt
 t/data/nice/001
 t/data/nice/002
@@ -324,6 +349,7 @@ t/data/nice/013
 t/data/nice/014
 t/data/nice/015
 t/data/nice/016
+t/data/nice/authres
 t/data/nice/base64.txt
 t/data/nice/crlf-endings
 t/data/nice/dkim/AddedVtag_07
@@ -336,6 +362,10 @@ t/data/nice/dkim/MultipleSig_06
 t/data/nice/dkim/NonExistingHeader_09
 t/data/nice/dkim/Nowsp_03
 t/data/nice/dkim/Simple_02
+t/data/nice/dmarc/noneok.eml
+t/data/nice/dmarc/quarok.eml
+t/data/nice/dmarc/rejectok.eml
+t/data/nice/dmarc/strictrejectok.eml
 t/data/nice/mailman_message.txt
 t/data/nice/mime1
 t/data/nice/mime2
@@ -353,6 +383,14 @@ t/data/nice/spf1
 t/data/nice/spf2
 t/data/nice/spf3
 t/data/nice/spf3-received-spf
+t/data/nice/spf4-received-spf-nofold
+t/data/nice/spf5-received-spf-crlf
+t/data/nice/spf6-received-spf-crlf2
+t/data/nice/unicode1
+t/data/nice/unicode2
+t/data/nice.mbox
+t/data/phishing/openphish-feed.txt
+t/data/phishing/phishtank-feed.csv
 t/data/reporterplugin.pm
 t/data/spam/001
 t/data/spam/002
@@ -380,107 +418,149 @@ t/data/spam/badmime3.txt
 t/data/spam/base64.txt
 t/data/spam/bsmtp
 t/data/spam/bsmtpnull
+t/data/spam/decodeshorturl/base.eml
+t/data/spam/decodeshorturl/base2.eml
+t/data/spam/decodeshorturl/chain.eml
+t/data/spam/dmarc/nodmarc.eml
+t/data/spam/dmarc/noneko.eml
+t/data/spam/dmarc/quarko.eml
+t/data/spam/dmarc/rejectko.eml
+t/data/spam/dmarc/strictrejectko.eml
 t/data/spam/dnsbl.eml
+t/data/spam/dnsbl_domsonly.eml
+t/data/spam/dnsbl_ipsonly.eml
+t/data/spam/extracttext/gtube_png.eml
+t/data/spam/extracttext/gtube_pdf.eml
+t/data/spam/extracttext/gtube_b64_oct.eml
+t/data/spam/freemail1
+t/data/spam/freemail2
+t/data/spam/freemail3
+t/data/spam/fromnamespoof/spoof1
 t/data/spam/gtube.eml
 t/data/spam/gtubedcc.eml
+t/data/spam/gtubedcc_crlf.eml
+t/data/spam/hashbl
 t/data/spam/olevbmacro/encrypted.eml
 t/data/spam/olevbmacro/goodcsv.eml
 t/data/spam/olevbmacro/macro.eml
 t/data/spam/olevbmacro/malicemacro.eml
 t/data/spam/olevbmacro/nomacro.eml
 t/data/spam/olevbmacro/renamedmacro.eml
+t/data/spam/olevbmacro/target_uri.eml
 t/data/spam/olevbmacro/zippwmacro.eml
+t/data/spam/phishing_openphish.eml
+t/data/spam/phishing_phishtank.eml
+t/data/spam/pyzor
 t/data/spam/razor2
 t/data/spam/relayUS.eml
 t/data/spam/spf1
 t/data/spam/spf2
 t/data/spam/spf3
+t/data/spam/unicode1
+t/data/spam/urilocalbl_net.eml
 t/data/spamc_blank.cf
 t/data/taintcheckplugin.pm
 t/data/testplugin.pm
 t/data/testplugin2.pm
 t/data/validuserplugin.pm
-t/data/whitelists/action.eff.org
-t/data/whitelists/amazon_co_uk_ship
-t/data/whitelists/amazon_com_ship
-t/data/whitelists/cert.org
-t/data/whitelists/debian_bts_reassign
-t/data/whitelists/ibm_enews_de
-t/data/whitelists/infoworld
-t/data/whitelists/linuxplanet
-t/data/whitelists/lp.org
-t/data/whitelists/media_unspun
-t/data/whitelists/mlist_mailman_message
-t/data/whitelists/mlist_yahoo_groups_message
-t/data/whitelists/mypoints
-t/data/whitelists/neat_net_tricks
-t/data/whitelists/netcenter-direct_de
-t/data/whitelists/netsol_renewal
-t/data/whitelists/networkworld
-t/data/whitelists/oracle_net_techblast
-t/data/whitelists/orbitz.com
-t/data/whitelists/paypal.com
-t/data/whitelists/register.com_password
-t/data/whitelists/ryanairmail.com
-t/data/whitelists/sf.net
-t/data/whitelists/winxpnews.com
-t/data/whitelists/yahoo-inc.com
-t/data/phishing/openphish-feed.txt
-t/data/phishing/phishtank-feed.csv
-t/data/spam/phishing_openphish.eml
-t/data/spam/phishing_phishtank.eml
-t/phishing.t
+t/data/welcomelists/action.eff.org
+t/data/welcomelists/amazon_co_uk_ship
+t/data/welcomelists/amazon_com_ship
+t/data/welcomelists/cert.org
+t/data/welcomelists/debian_bts_reassign
+t/data/welcomelists/ibm_enews_de
+t/data/welcomelists/infoworld
+t/data/welcomelists/linuxplanet
+t/data/welcomelists/lp.org
+t/data/welcomelists/media_unspun
+t/data/welcomelists/mlist_mailman_message
+t/data/welcomelists/mlist_yahoo_groups_message
+t/data/welcomelists/mypoints
+t/data/welcomelists/neat_net_tricks
+t/data/welcomelists/netcenter-direct_de
+t/data/welcomelists/netsol_renewal
+t/data/welcomelists/networkworld
+t/data/welcomelists/oracle_net_techblast
+t/data/welcomelists/orbitz.com
+t/data/welcomelists/paypal.com
+t/data/welcomelists/register.com_password
+t/data/welcomelists/ryanairmail.com
+t/data/welcomelists/sf.net
+t/data/welcomelists/winxpnews.com
+t/data/welcomelists/yahoo-inc.com
 t/date.t
 t/db_awl_path.t
+t/db_awl_path_welcome_block.t
 t/db_awl_perms.t
+t/db_awl_perms_welcome_block.t
+t/db_based_welcomelist.t
+t/db_based_welcomelist_ips.t
 t/db_based_whitelist.t
 t/db_based_whitelist_ips.t
 t/dcc.t
 t/debug.t
+t/decodeshorturl.t
 t/desc_wrap.t
 t/dkim.t
 t/dnsbl.t
 t/dnsbl_sc_meta.t
-t/duplicates.t
+t/dnsbl_subtests.t
+t/enable_compat.t
+t/extracttext.t
 t/freemail.t
+t/freemail_welcome_block.t
+t/fromnamespoof.t
 t/get_all_headers.t
 t/get_headers.t
 t/gtube.t
-t/hashcash.t
+t/header.t
+t/header_utf8.t
+t/hashbl.t
 t/html_colors.t
 t/html_obfu.t
 t/html_utf8.t
 t/idn_dots.t
 t/if_can.t
+t/if_else.t
 t/ifversion.t
 t/ip_addrs.t
 t/lang_lint.t
 t/lang_pl_tests.t
 t/line_endings.t
 t/lint_nocreate_prefs.t
+t/local_tests_only.t
 t/memory_cycles.t
 t/metadata.t
 t/mimeheader.t
 t/mimeparse.t
 t/missing_hb_separator.t
+t/mkrules.t
+t/mkrules_else.t
 t/nonspam.t
+t/olevbmacro.t
 t/originating_ip_hdr.t
+t/pdfinfo.t
+t/phishing.t
 t/plugin.t
 t/plugin_file.t
 t/plugin_priorities.t
 t/prefs_include.t
 t/priorities.t
+t/priorities_welcome_block.t
+t/perlcritic.t
+t/perlcritic.pl
+t/podchecker.t
+t/pyzor.t
 t/razor2.t
 t/rcvd_parser.t
 t/re_base_extraction.t
 t/recips.t
 t/recreate.t
 t/recursion.t
+t/regexp_named_capture.t
 t/regexp_valid.t
-t/relaycountry_fast.t
-t/relaycountry_geoip.t
-t/relaycountry_geoip2.t
 t/relative_scores.t
+t/relaycountry.t
 t/report_safe.t
 t/reportheader.t
 t/reportheader_8bit.t
@@ -500,14 +580,18 @@ t/rule_multiple.t
 t/rule_names.t
 t/rule_types.t
 t/sa_awl.t
+t/sa_awl_welcome_block.t
 t/sa_check_spamd.t
 t/sa_compile.t
 t/sha1.t
 t/shortcircuit.t
+t/shortcircuit_before_dns.t
 t/spam.t
 t/spamc.t
 t/spamc_B.t
 t/spamc_E.t
+t/spamc_H.t
+t/spamc_bug6176.t
 t/spamc_c.t
 t/spamc_c_stdout_closed.t
 t/spamc_cf.t
@@ -540,6 +624,7 @@ t/spamd_report.t
 t/spamd_report_ifspam.t
 t/spamd_sql_prefs.t
 t/spamd_ssl.t
+t/spamd_ssl_z.t
 t/spamd_ssl_accept_fail.t
 t/spamd_stop.t
 t/spamd_symbols.t
@@ -548,8 +633,11 @@ t/spamd_unix.t
 t/spamd_unix_and_tcp.t
 t/spamd_user_rules_leak.t
 t/spamd_utf8.t
+t/spamd_welcomelist_leak.t
 t/spamd_whitelist_leak.t
 t/spf.t
+t/spf_welcome_block.t
+t/sql_based_welcomelist.t
 t/sql_based_whitelist.t
 t/stop_always_matching_regexps.t
 t/strip2.t
@@ -557,30 +645,31 @@ t/strip_no_subject.t
 t/stripmarkup.t
 t/tainted_msg.t
 t/test_dir
+t/testrules.yml
 t/text_bad_ctype.t
 t/timeout.t
 t/trust_path.t
 t/uri.t
 t/uri_html.t
+t/uri_list.t
+t/uri_saferedirect.t
 t/uri_text.t
 t/uribl.t
-t/urilocalbl_geoip.t
-t/uri_saferedirect.t
+t/uribl_all_types.t
+t/uribl_domains_only.t
+t/uribl_ips_only.t
+t/urilocalbl.t
 t/utf8.t
 t/util_wrap.t
+t/welcomelist_addrs.t
+t/welcomelist_from.t
+t/welcomelist_subject.t
+t/welcomelist_to.t
+t/wlbl_uri.t
 t/whitelist_addrs.t
 t/whitelist_from.t
 t/whitelist_subject.t
 t/whitelist_to.t
-t/zz_cleanup.t
-t/spamc_bug6176.t
-t/data/spam/dnsbl_domsonly.eml
-t/uribl_domains_only.t
-t/data/spam/dnsbl_ipsonly.eml
-t/uribl_all_types.t
-t/uribl_ips_only.t
-t/uri_list.t
-t/dnsbl_subtests.t
 powered_by/128-powered-by-spamassassin.png
 powered_by/256-powered-by-spamassassin.png
 powered_by/512-powered-by-spamassassin.png
index caadcb68306d56746759c33d8450c0f1d4dc1d5d..0d463806b05a0f193934cb649c5d2df3fae1dcde 100644 (file)
@@ -10,6 +10,7 @@
 \.pid$
 \.so$
 \.svn/
+\.gitattributes$
 \.gitignore$
 \.swp$
 \.tmp$
@@ -22,6 +23,7 @@
 \btmon\.out$
 \b[Oo][Ll][Dd]$
 \b[Oo][Uu][Tt]$
+^.github/
 ^blib/
 ^blibdirs$
 ^build/3\.\d\.\d_change_summary$
@@ -63,7 +65,7 @@
 ^t/bayessql\.cf$
 ^t/config$
 ^t/data/nice/cjk/
-^t/data/whitelists/
+^t/data/welcomelists/
 ^t/do_net$
 ^t/log/
 ^t/rule_tests\.t$
 ^build/rebuild_xt$
 ^build/repackage_latest_update_rules$
 ^MYMETA.(json|yml)$
-^trunk-only.*
-^t/mkrules\.t$
-^t/mkrules_else\.t$
-^t/spamc_H\.t$
+^textcat/
index 1ad75bfd917f312a914acc287d92a19ec4aad065..f8c6c626de1a1b4ca4feeed6f327f60a1a8cd1c0 100644 (file)
@@ -4,13 +4,14 @@
       "The Apache SpamAssassin Project <dev@spamassassin.apache.org>"
    ],
    "dynamic_config" : 1,
-   "generated_by" : "ExtUtils::MakeMaker version 6.68, CPAN::Meta::Converter version 2.120921",
+   "generated_by" : "ExtUtils::MakeMaker version 7.62, CPAN::Meta::Converter version 2.150010",
    "license" : [
+      "unknown",
       "apache_2_0"
    ],
    "meta-spec" : {
       "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec",
-      "version" : "2"
+      "version" : 2
    },
    "name" : "Mail-SpamAssassin",
    "no_index" : {
    "prereqs" : {
       "build" : {
          "requires" : {
-            "ExtUtils::MakeMaker" : "0"
+            "ExtUtils::MakeMaker" : "6.64"
+         }
+      },
+      "configure" : {
+         "requires" : {
+            "ExtUtils::MakeMaker" : "6.64"
          }
       },
       "runtime" : {
+         "recommends" : {
+            "Archive::Zip" : "0",
+            "BSD::Resource" : "0",
+            "Compress::Zlib" : "0",
+            "DBD::SQLite" : "1.5901",
+            "DBI" : "0",
+            "DB_File" : "0",
+            "Email::Address::XS" : "0",
+            "Encode::Detect::Detector" : "0",
+            "Geo::IP" : "0",
+            "IO::Socket::INET6" : "0",
+            "IO::Socket::IP" : "0.09",
+            "IO::Socket::SSL" : "1.76",
+            "IO::String" : "0",
+            "IP::Country::DB_File" : "0",
+            "IP::Country::Fast" : "0",
+            "LWP::UserAgent" : "0",
+            "MIME::Base64" : "0",
+            "Mail::DKIM" : "0.37",
+            "Mail::DMARC" : "0",
+            "Mail::SPF" : "0",
+            "MaxMind::DB::Reader" : "0",
+            "MaxMind::DB::Reader::XS" : "0",
+            "Net::CIDR::Lite" : "0",
+            "Net::LibIDN" : "0",
+            "Net::LibIDN2" : "0",
+            "Net::Patricia" : "1.16",
+            "Net::SMTP" : "0",
+            "Razor2::Client::Agent" : "2.61"
+         },
          "requires" : {
             "Archive::Tar" : "1.23",
-            "Digest::SHA1" : "0",
             "Errno" : "0",
             "File::Copy" : "2.02",
             "File::Spec" : "0.8",
             "HTML::Parser" : "3.43",
             "IO::Zlib" : "1.04",
             "Mail::DKIM" : "0.31",
-            "Net::DNS" : "0.34",
+            "Net::DNS" : "0.69",
             "NetAddr::IP" : "4.01",
             "Pod::Usage" : "1.1",
             "Sys::Hostname" : "0",
-            "Test::More" : "0",
             "Time::HiRes" : "0",
-            "Time::Local" : "0"
+            "Time::Local" : "0",
+            "perl" : "5.014"
+         }
+      },
+      "test" : {
+         "recommends" : {
+            "Net::DNS::Nameserver" : "0"
+         },
+         "requires" : {
+            "Devel::Cycle" : "0",
+            "Perl::Critic::Policy::Perlsecret" : "0",
+            "Perl::Critic::Policy::TestingAndDebugging::ProhibitNoStrict" : "0",
+            "Test::More" : "0",
+            "Text::Diff" : "0"
          }
       }
    },
       },
       "x_MailingList" : "http://wiki.apache.org/spamassassin/MailingLists"
    },
-   "version" : "3.004006"
+   "version" : "4.000000",
+   "x_serialization_backend" : "JSON::PP version 4.06"
 }
index ddfda8810ea8ae39a3a2ddf11e1d9daaad5d24c4..2162e475a17debbccdef18d6d973db5101983de6 100644 (file)
@@ -3,37 +3,73 @@ abstract: 'Apache SpamAssassin is an extensible email filter which is used to id
 author:
   - 'The Apache SpamAssassin Project <dev@spamassassin.apache.org>'
 build_requires:
-  ExtUtils::MakeMaker: 0
+  Devel::Cycle: '0'
+  ExtUtils::MakeMaker: '6.64'
+  Perl::Critic::Policy::Perlsecret: '0'
+  Perl::Critic::Policy::TestingAndDebugging::ProhibitNoStrict: '0'
+  Test::More: '0'
+  Text::Diff: '0'
+configure_requires:
+  ExtUtils::MakeMaker: '6.64'
 dynamic_config: 1
-generated_by: 'ExtUtils::MakeMaker version 6.68, CPAN::Meta::Converter version 2.120921'
-license: apache
+generated_by: 'ExtUtils::MakeMaker version 7.62, CPAN::Meta::Converter version 2.150010'
+license: unknown
 meta-spec:
   url: http://module-build.sourceforge.net/META-spec-v1.4.html
-  version: 1.4
+  version: '1.4'
 name: Mail-SpamAssassin
 no_index:
   directory:
     - t
     - inc
+recommends:
+  Archive::Zip: '0'
+  BSD::Resource: '0'
+  Compress::Zlib: '0'
+  DBD::SQLite: '1.5901'
+  DBI: '0'
+  DB_File: '0'
+  Email::Address::XS: '0'
+  Encode::Detect::Detector: '0'
+  Geo::IP: '0'
+  IO::Socket::INET6: '0'
+  IO::Socket::IP: '0.09'
+  IO::Socket::SSL: '1.76'
+  IO::String: '0'
+  IP::Country::DB_File: '0'
+  IP::Country::Fast: '0'
+  LWP::UserAgent: '0'
+  MIME::Base64: '0'
+  Mail::DKIM: '0.37'
+  Mail::DMARC: '0'
+  Mail::SPF: '0'
+  MaxMind::DB::Reader: '0'
+  MaxMind::DB::Reader::XS: '0'
+  Net::CIDR::Lite: '0'
+  Net::LibIDN: '0'
+  Net::LibIDN2: '0'
+  Net::Patricia: '1.16'
+  Net::SMTP: '0'
+  Razor2::Client::Agent: '2.61'
 requires:
-  Archive::Tar: 1.23
-  Digest::SHA1: 0
-  Errno: 0
-  File::Copy: 2.02
-  File::Spec: 0.8
-  HTML::Parser: 3.43
-  IO::Zlib: 1.04
-  Mail::DKIM: 0.31
-  Net::DNS: 0.34
-  NetAddr::IP: 4.01
-  Pod::Usage: 1.1
-  Sys::Hostname: 0
-  Test::More: 0
-  Time::HiRes: 0
-  Time::Local: 0
+  Archive::Tar: '1.23'
+  Errno: '0'
+  File::Copy: '2.02'
+  File::Spec: '0.8'
+  HTML::Parser: '3.43'
+  IO::Zlib: '1.04'
+  Mail::DKIM: '0.31'
+  Net::DNS: '0.69'
+  NetAddr::IP: '4.01'
+  Pod::Usage: '1.1'
+  Sys::Hostname: '0'
+  Time::HiRes: '0'
+  Time::Local: '0'
+  perl: '5.014'
 resources:
+  MailingList: http://wiki.apache.org/spamassassin/MailingLists
   homepage: https://spamassassin.apache.org/
   license: http://www.apache.org/licenses/LICENSE-2.0.html
   repository: http://svn.apache.org/repos/asf/spamassassin/
-  x_MailingList: http://wiki.apache.org/spamassassin/MailingLists
-version: 3.004006
+version: '4.000000'
+x_serialization_backend: 'CPAN::Meta::YAML version 0.018'
index f476698c81d544eea190c85ed0213221825a6e36..3e946034f6468d5a78c3ef7bc1ad3ef93ed3e37f 100644 (file)
@@ -1,19 +1,14 @@
 #!/usr/bin/perl
-require 5.6.1;
+require v5.14.0;
 
 use strict;
 use warnings;
 use Config;
 
-use ExtUtils::MakeMaker;
+use ExtUtils::MakeMaker 6.64;
 
-# avoid stupid 'Argument "6.30_01" isn't numeric in numeric ge (>=)' warnings;
-my $mm_version = eval $ExtUtils::MakeMaker::VERSION;
-
-# raising the version of makemaker to 6.46 per bug 6598 & 6131
-if ($mm_version < 6.46) {
-  die "Apache SpamAssassin Makefile.PL requires at least ExtUtils::MakeMaker v6.46";
-}
+# raising the version of makemaker to 6.64 to use TEST_REQUIRES
+use constant MIN_MAKEMAKER_VERSION => 6.64;
 
 use constant RUNNING_ON_WINDOWS => ($^O =~ /^(mswin|dos|os2)/oi);
 use constant HAS_DBI => eval { require DBI; };
@@ -167,7 +162,7 @@ my %makefile = (
     'PMLIBDIRS' => [ 'lib' ],
 
     'PM_FILTER' => '$(PREPROCESS) -Mconditional -Mvars -DVERSION="$(VERSION)" \
-       -DPREFIX="$(I_PREFIX)" \
+       -DPREFIX="$(I_PREFIX)" \
        -DDEF_RULES_DIR="$(I_DATADIR)" \
        -DLOCAL_RULES_DIR="$(I_CONFDIR)" \
        -DLOCAL_STATE_DIR="$(I_LOCALSTATEDIR)"',
@@ -177,9 +172,7 @@ my %makefile = (
     },
 
     # be quite explicit about this; afaik CPAN.pm is sensible using this
-    # also see CURRENT_PM below
     'PREREQ_PM' => {
-        'Digest::SHA1'         => 0,             # 2.0 is oldest tested version
         'File::Spec'           => 0.8,           # older versions lack some routines we need
         'File::Copy'           => 2.02,          # this version is shipped with 5.005_03, the oldest version known to work
         'Pod::Usage'           => 1.10,          # all versions prior to this do seem to be buggy
@@ -187,18 +180,37 @@ my %makefile = (
         'Archive::Tar'         => 1.23,          # for sa-update
         'IO::Zlib'             => 1.04,          # for sa-update
         'Mail::DKIM'           => 0.31,
-        'Net::DNS'             => (RUNNING_ON_WINDOWS ? 0.46 : 0.34), # bugs in older revs
+        'Net::DNS'             => 0.69,
         'NetAddr::IP'          => 4.010,
         'Sys::Hostname'        => 0,
-        'Test::More'           => 0,
         'Time::HiRes'          => 0,
         'Time::Local'          => 0,
         'Errno'                => 0,
     },
 
+    # In case MIN_MAKEMAKER_VERSION is greater than the version bundled in the core of MIN_PERL_VERSION
+    # use this to ensure CPAN will automatically upgrade MakeMaker if needed
+    'BUILD_REQUIRES' => {
+        'ExtUtils::MakeMaker' => MIN_MAKEMAKER_VERSION,
+    },
+
+    'CONFIGURE_REQUIRES' => {
+        'ExtUtils::MakeMaker' => MIN_MAKEMAKER_VERSION,
+    },
+
+    # The modules that are not core that are used in default tests
+    'TEST_REQUIRES' => {
+        'Devel::Cycle' => 0,
+        'Test::More'   => 0,
+        'Text::Diff' => 0,
+        'Perl::Critic::Policy::TestingAndDebugging::ProhibitNoStrict' => 0,
+        'Perl::Critic::Policy::Perlsecret' => 0,
+    },
+
     'dist' => {
         COMPRESS => 'gzip -9f',
-        SUFFIX => 'gz',
+        SUFFIX => '.gz',
+        TARFLAGS => 'cf',
         DIST_DEFAULT => 'tardist',
 
         CI => 'svn commit',
@@ -228,12 +240,8 @@ my %makefile = (
 
         'rules/*.pm',
 
-        # don't remove these. they are built from 'rulesrc' in SVN, but
-        # in a distribution tarball, they're not
-        # 'rules/70_sandbox.cf',
-        # 'rules/72_active.cf',
-
-        # this file is no longer built, or used
+        'rules/70_sandbox.cf',
+        'rules/72_active.cf',
         'rules/70_inactive.cf',
 
       )
@@ -246,6 +254,7 @@ my %makefile = (
     # We have only this Makefile.PL and this option keeps MakeMaker from
     # asking all questions twice after a 'make dist*'.
     'NORECURS' => 1,
+    'MIN_PERL_VERSION'=> 5.014000,
 );
 
 # rules/72_active.cf is built from "rulesrc", but *must* exist before
@@ -254,14 +263,6 @@ my @FILES_THAT_MUST_EXIST = qw(
         rules/72_active.cf
     );
 
-# make sure certain optional modules are up-to-date if they are installed
-# also see PREREQ_PM above
-my %CURRENT_PM = (
-    'Net::DNS' => (RUNNING_ON_WINDOWS ? 0.46 : 0.34),
-    'Razor2::Client::Agent' => 2.40,
-);
-
-
 # All the $(*MAN1*) stuff is empty/zero if Perl was Configured with -Dman1dir=none;
 # however, support site/vendor man1 dirs (bug 5338)
 unless($Config{installman1dir}
@@ -324,11 +325,13 @@ See that file if you wish to customize what tests are run, and how.
 
 # check optional module versions
 use lib 'lib';
-use Mail::SpamAssassin::Util::DependencyInfo;
-if (Mail::SpamAssassin::Util::DependencyInfo::long_diagnostics() != 0) {
-  # missing required module?  die!
-  # bug 5908: http://cpantest.grango.org/wiki/CPANAuthorNotes says
-  # we should exit with a status of 0, but without creating Makefile
+require Mail::SpamAssassin::Util::DependencyInfo;
+if (Mail::SpamAssassin::Util::DependencyInfo::long_diagnostics(1) != 0) {
+  # This prints a full report of missing required and optional modules and binaries
+  # but only exit 0 without creating Makefile if there are missing required binaries.
+  # See http://cpantest.grango.org/wiki/CPANAuthorNotes
+  # Continuing when there are missing required CPAN modules allows cpan to install them
+  # before it runs make on the Makefile
   exit 0;
 }
 
@@ -362,26 +365,45 @@ $makefile{META_MERGE} = {
     MailingList => 'http://wiki.apache.org/spamassassin/MailingLists',
   },
 
-  recommends => {
-    'MIME::Base64' =>              0,
-    'DB_File' =>                   0,
-    'Net::SMTP' =>                 0,
-    'Mail::SPF' =>                 0,
-    'Geo::IP' =>                   0,
-    'Razor2::Client::Agent' =>     2.61,
-    'Net::Ident' =>                0,
-    'IO::Socket::INET6' =>         0,
-    'IO::Socket::SSL' =>           1.76,
-    'Compress::Zlib' =>            0,
-    'Mail::DKIM' =>                0.37,
-    'DBI' =>                       0,
-    'Getopt::Long' =>              2.32,
-    'LWP::UserAgent' =>            0,
-    'HTTP::Date' =>                0,
-    'Archive::Tar' =>              1.23,
-    'IO::Zlib' =>                  1.04,
-    'Encode::Detect' =>            0
-  }
+  prereqs => {
+    runtime => {
+      recommends => {
+        'MIME::Base64' =>              0,
+        'DB_File' =>                   0,
+        'Net::SMTP' =>                 0,
+        'Net::LibIDN2' =>              0,
+        'Net::LibIDN' =>               0,
+        'Mail::SPF' =>                 0,
+        'MaxMind::DB::Reader' =>       0,
+        'MaxMind::DB::Reader::XS' =>   0,
+        'Geo::IP' =>                   0,
+        'IP::Country::DB_File' =>      0,
+        'IP::Country::Fast' =>         0,
+        'Razor2::Client::Agent' =>     2.61,
+        'IO::Socket::IP' =>            0.09,
+        'IO::Socket::INET6' =>         0,
+        'IO::Socket::SSL' =>           1.76,
+        'Compress::Zlib' =>            0,
+        'Mail::DKIM' =>                0.37,
+        'DBI' =>                       0,
+        'DBD::SQLite' =>               1.59_01,
+        'LWP::UserAgent' =>            0,
+        'Encode::Detect::Detector' =>  0,
+        'Net::Patricia' =>             1.16,
+        'Net::CIDR::Lite' =>           0,
+        'BSD::Resource' =>             0,
+        'Archive::Zip' =>              0,
+        'IO::String' =>                0,
+        'Email::Address::XS' =>        0,
+        'Mail::DMARC' =>               0,
+      },
+    },
+    test => {
+      recommends => {
+        'Net::DNS::Nameserver' =>      0,
+      },
+    },
+  },
 };
 
 #######################################################################
@@ -390,7 +412,7 @@ $makefile{META_MERGE} = {
 $makefile{EXE_FILES} = [ values %{$makefile{EXE_FILES}} ];
 $makefile{AUTHOR} =~ s/(<.+) at (.+>)/$1\@$2/;
 WriteMakefile(%makefile);
-print "Makefile written by ExtUtils::MakeMaker $mm_version\n";
+print "Makefile written by ExtUtils::MakeMaker $ExtUtils::MakeMaker::VERSION\n";
 
 #######################################################################
 
@@ -795,7 +817,14 @@ sub _set_macro_PERL_TAINT {
 sub _set_macro_PREPROCESS {
 
   return if get_macro('PREPROCESS');
-  set_macro('PREPROCESS', join(' ', macro_ref('PERL_BIN'), qq{build/preprocessor}));
+  # Bug 8038 - work around quirk of newer Extutils::MakeMaker on Windows with dmake
+  my $perl_bin = get_expanded_macro('FULLPERL');
+  if ($RUNNING_ON_WINDOWS and ($::Config{make} eq 'dmake') and ($perl_bin =~ /^([a-zA-Z]:)?\\"(.*)$/)) {
+    $perl_bin = "\"$1\\$2";
+  } else {
+    $perl_bin = macro_ref('PERL_BIN');
+  }
+  set_macro('PREPROCESS', join(' ', $perl_bin, qq{build/preprocessor}));
 }
 
 # This routine sets the value for CONFIGURE (spamc only).
@@ -1131,6 +1160,7 @@ conf__install:
        $(PERL) -MFile::Copy -e "copy(q[rules/v341.pre], q[$(B_CONFDIR)/v341.pre]) unless -f q[$(B_CONFDIR)/v341.pre]"
        $(PERL) -MFile::Copy -e "copy(q[rules/v342.pre], q[$(B_CONFDIR)/v342.pre]) unless -f q[$(B_CONFDIR)/v342.pre]"
        $(PERL) -MFile::Copy -e "copy(q[rules/v343.pre], q[$(B_CONFDIR)/v343.pre]) unless -f q[$(B_CONFDIR)/v343.pre]"
+       $(PERL) -MFile::Copy -e "copy(q[rules/v400.pre], q[$(B_CONFDIR)/v400.pre]) unless -f q[$(B_CONFDIR)/v400.pre]"
 
 data__install:
        -$(MKPATH) $(B_DATADIR)
index 617c4306e3ea034fd2cd4f44ff9e12c5b35fa9ec..94ce367384da5ce98f0782632f40f16a07d1ef08 100644 (file)
@@ -289,25 +289,25 @@ Resources
 ---------
 
 [BUGZILLA] SpamAssassin bug database:
-  <http://issues.apache.org/SpamAssassin/>
+  <https://issues.apache.org/SpamAssassin/>
 
 [DEBPERL] Debian Perl Policy, Chapter 3: Packaged Modules:
-  <http://www.debian.org/doc/packaging-manuals/perl-policy/ch-module_packages.html>
+  <https://www.debian.org/doc/packaging-manuals/perl-policy/ch-module_packages.html>
 
 [GNUMAKECMD] GNU make manual: Make Conventions: Variables for Specifying
   Commands
-  <http://www.gnu.org/manual/make-3.79.1/html_chapter/make_14.html#SEC119>
+  <https://www.gnu.org/software/make/manual/html_node/Command-Variables.html#Command-Variables>
 
 [MANEUMM616] The man page for ExtUtils::MakeMaker 6.16:
-  <http://search.cpan.org/author/MSCHWERN/ExtUtils-MakeMaker-6.16/lib/ExtUtils/MakeMaker.pm#Default_Makefile_Behaviour>
+  <https://search.cpan.org/author/MSCHWERN/ExtUtils-MakeMaker-6.16/lib/ExtUtils/MakeMaker.pm#Default_Makefile_Behaviour>
 
 [MM00779] makemaker-at-perl-dot-org: Michael G Schwern: "Re: MakeMaker
   problems with relocation" (PREFIX was broken):
-  <http://www.mail-archive.com/makemaker@perl.org/msg00779.html>
+  <https://www.mail-archive.com/makemaker@perl.org/msg00779.html>
 
 [P5P94113] perl5-porters: Michael G Schwern: "Re: OS X's vendorlib default
   seems wrong" (description of different repositoreis):
-  <http://archive.develooper.com/perl5-porters@perl.org/msg94113.html>
+  <https://archive.develooper.com/perl5-porters@perl.org/msg94113.html>
 
 [RHBUG78053] Red Hat bug 78053: "incompatible changes in behavior of
   MakeMaker; affects rpm build process" (introduction of DESTDIR):
index 2d5cebf89dbe8ae02824cb48553b2e5f329dc4ed..26bbd0009740fe7f53f90651eb9ae71229b93433 100644 (file)
@@ -14,7 +14,7 @@ filtering, DNS blocklists, and collaborative filtering databases.
 Apache SpamAssassin is a project of the Apache Software Foundation (ASF).
 
 
-What Apache SpamAssassin is Not
+What Apache SpamAssassin Is Not
 -------------------------------
 
 Apache SpamAssassin is not a program to delete spam, route spam and ham to
@@ -71,9 +71,9 @@ spamassassin-users mailing list[2]. If you've found a bug (and you're
 sure it's a bug after checking the Wiki), please file a report in our
 Bugzilla[3].
 
-       [1]: http://wiki.apache.org/spamassassin/
-       [2]: http://wiki.apache.org/spamassassin/MailingLists
-       [3]: http://issues.apache.org/SpamAssassin/
+       [1]: https://wiki.apache.org/spamassassin/
+       [2]: https://wiki.apache.org/spamassassin/MailingLists
+       [3]: https://issues.apache.org/SpamAssassin/
 
 Please also be sure to read the man pages.
 
@@ -151,7 +151,7 @@ default locations that Apache SpamAssassin will look at the end.
   - $USER_HOME/.spamassassin:
 
        User state directory.  Used to hold spamassassin state, such
-       as a per-user automatic whitelist, and the user's preferences
+       as a per-user automatic welcomelist, and the user's preferences
        file.
 
   - $USER_HOME/.spamassassin/user_prefs:
@@ -298,7 +298,7 @@ these translations, so that they can be added to the
 distribution. Please file a bug in our Bugzilla[4], and attach your
 translations. You will, of course, be credited for this work!
 
-       [4]: http://issues.apache.org/SpamAssassin/
+       [4]: https://issues.apache.org/SpamAssassin/
 
 
 Disabled code
index 5fd0b43d705b116b41e7e998999cd252bb1bf840..63cb97c85a724c6c9ff873dc2728288cb9ec367f 100644 (file)
-Note for Users Upgrading to SpamAssassin 3.4.5
+Note for Users Upgrading to SpamAssassin 4.0.0
 ----------------------------------------------
 
-- Spamassassin test suite can now run against the installed
-  SpamAssassin files (rather than those in the source directory)
+Apache SpamAssassin 4.0.0 represents years of work by the project with
+numerous improvements, new rule types, and internal native handling
+of messages in international languages. We highly recommend looking
+through this file and all of the .pre files to evaluate your
+configuration thoroughly. Plugins have been added, removed, and
+improved throughout.
+
+- All rules, functions, command line options and modules that contain
+  "whitelist" or "blacklist" have been renamed to contain more
+  racially neutral "welcomelist" and "blocklist" terms. This allows
+  acronyms like WL and BL to remain the same. Previous options will
+  continue work at least until version 4.1.0 is released. If you have
+  local settings including scores or meta rules referring to old rule
+  names, these should be changed and "enable_compat
+  welcomelist_blocklist" added in init.pre. See:
+  https://wiki.apache.org/spamassassin/WelcomelistBlocklist (Bug 7826)
+
+- Meta rules no longer use priority values, they are evaluated
+  dynamically when the rules they depend on are finished. (Bug 7735)
+
+- API: New $pms->rule_ready() function. Any asynchronous eval-function
+  must now return undef (instead of 0 or 1), if rule result is not
+  ready when exiting the function. $pms->rule_ready($rulename) or
+  $pms->got_hit(...) must be called when the result has arrived. If
+  these are not used, it can break depending meta rule evaluation.
 
-- unwhitelist_auth now also removes def_whitelist_auth entries
+- Setting normalize_charset is now enabled by default. Note that rules
+  should not expect specific non-UTF8 or UTF8 encoding in
+  body. Matching is done against the raw data which may vary depending
+  on normalize_charset setting and whether decoding to UTF8 was
+  successful. See:
+  https://wiki.apache.org/spamassassin/WritingRulesAdvanced
 
-- SPF: add unwhitelist_from_spf to remove both whitelist_from_spf and
-  def_whitelist_from_spf entries
+- DKIM plugin has added support for ARC signature verification
+
+- The DecodeShortURL plugin has been added and decodes URIs from URL
+  shorteners that may be used to evade scanning
 
-- Default SQL schema for userpref.value changed from varchar(100) to
-  varchar(255), no need to modify unless you hit the limit. (Bug 7803)
+- Strings can now be captured from rules and later reused using the
+     special %{TAGNAME} syntax
+
+- The Bayes stopwords, or noise words, are now configurable in order
+  to optimize Bayes usage for non-English languages. Stopwords for 16
+  foreign languages have been included. See 60_bayes_stopwords.cf in
+  the rules files. See Mail::SpamAssassin::Plugin::Bayes and the
+  bayes_stopword_languages option if you wish to use a different
+  stopword list. This is highly recommended if you are using Bayes and
+  you are processing messages in languages other than English.
+
+- The OLEVBMacro plugin has been improved to identify more macros
+  while also extracting uris from the attachments for automatic
+  inclusion in RBL lookups
+
+- Internationalized domain name (IDN) support has been added and
+  requires Net::LibIDN2 or Net::LibIDN module with a new
+  Util::idn_to_ascii() function. (Bug 7215)
+
+- Improved internal header address (From/To/Cc) parser, now also
+  handles multiple addresses and includes optional support for
+  external Email::Address::XS parser, which can handle nested comments
+  and other oddities.
+
+- Header :addr :name modifiers now return all addresses. Options of
+  :first :last select only first (topmost) or last header to process
+  when there are multiple headers with the same name. :addr and :name
+  may still return multiple values from a single header.
+
+- API: $pms->get() can and should now be called in list
+  context. Scalar context continues to return multiple values newline
+  separated, but this should be considered deprecated.
+
+- New ExtractText plugin that extracts text from documents or images
+  to feed the data into SpamAssassin for standard processing with
+  existing rules, URIs extracted from documents will fall into normal
+  RBL lookups.
+
+- New "nolog" tflag added to hide info coming from rules in
+  SpamAssassin reports
+
+- All log output (stderr, file, syslog) is now escaped properly for \r
+  \n \t \\, control chars, DEL, and UTF-8 sequences presented as
+  \x{XX}.  Whitespace is not normalized anymore like in versions prior
+  to 4.0.0.
+
+- API: Logger::add() has new optional 'escape' parameter.  New
+  Logger::escape_str() function.
+
+- API: New $pms->add_uri_detail_list() function. Also new
+  uri_detail_list types: unlinked, schemeless
+
+- Util::split_domain, trim_domain, and is_domain_valid functions have
+  a new optional argument ($is_ascii)
+
+- Header names support new :host :domain :ip :revip modifiers
+
+- AskDNS: tag HEADER(hdrname) supported to query any header content
+  similarly to header rules
+
+- The HashCash module and support has been removed completely, as it
+  has been long since deprecated
+
+- URILocalBL: uri_block_cc/uri_block_cont now support negation (Bug
+  7528)
+
+- URILocalBL: IPv6 lookups for hosts is now support, if provided by
+  your database
+
+- DNS and other asynchronous lookups such as Pyzor and DCC are now
+  only launched when priority -100 is reached. This allows short
+  circuiting at a lower priority without sending unneeded DNS queries
+  and starting process forms. (Bug 5930)
+
+- API: New plugin method callback method check_dnsbl added to launch
+  network lookups at priority -100 and check_post_dnsbl to harvest own
+  network lookups
+
+- API: New plugin callback method check_cleanup for cleaning up
+  things...
+
+- FreeMail: new options freemail_import_welcomelist_auth and
+  freemail_import_def_welcomelist_auth added (Bug 6451)
+
+- New internal Mail::SpamAssassin::GeoDB module that provides a
+  unified interface to modules MaxMind::DB::Reader (GeoIP2), Geo::IP,
+  IP::Country::DB_File, and IP::Country::Fast.
+
+  This is utilized by RelayCountry and URILocalBL with settings
+  geodb_module, geodb_options, and geodb_search_path.
+
+  Deprecated settings still work such as country_db_type,
+  country_db_path, uri_country_db_path, and uri_country_db_isp_path
+  but will print a warning to migrate to geodb_module/options.
+
+- Razor2 razor_fork option added to create separate Razor2 processes
+  and read in the results later asynchronously, increasing throughput,
+  and automatically adjusting rule priorities to -100.
+
+- DCC checks are now done asynchronously if using dccifd, improving
+  throughput.  With dccifd, rule priorities are automatically adjusted
+  to -100.  Commercial reputation rules can be ignored with the option
+  "use_dcc_rep 0" to save a few CPU cycles.
+
+- Pyzor pyzor_fork option added to create separate Pyzor processes and
+  read in the results later asynchronously, increasing throughput, and
+  automatically adjusting rule priorities to -100. Renamed pyzor_max
+  setting to pyzor_count_min. Added pyzor_welcomelist_min and
+  pyzor_welcomelist_factor setting. Also try to improve false
+  positives by ignoring "empty body" messages.
+
+- API: deprecated $pms->register_async_rule_start() and
+  $pms->register_async_rule_finish() calls though left in for
+  backwards compatibility. Plugins should only use
+  $pms->bgsend_and_start_lookup(), which handles required things
+  Automatically. Direct calls to bgsend or start_lookup should not be
+  used.  $pms->bgsend_and_start_lookup() should always contain
+  $ent->{rulename} for correct meta dependency handling. Deprecated
+  start_lookup, get_lookup, lookup_ns, harvest_until_rule_completes,
+  and is_rule_complete.
 
-- URIDetail can now match full hostname with "host" key
+- SPF: Mail::SPF is now the only supported perl module and
+  Mail::SPF::Query is deprecated along with the settings
+  do_not_use_mail_spf, and do_not_use_mail_spf_query. SPF lookups are
+  not done asynchronously so using an MTA filter such as pypolicyd-spf
+  or spf-engine can generate Received-SPF for SpamAssassin to parse.
 
-- BodyEval: plaintext_body_sig_ratio: eval rules for the (first text/plain
-  MIME part's) body and signature lengths and ratio
+- "ALL" pseudo-header now returns decoded headers, so it's usage is
+  consistent with single header matching. Using the :raw option mimics
+  the previous behavior of with undecoded and folded headers.
 
-Note for Users Upgrading to SpamAssassin 3.4.4
-----------------------------------------------
-
-- FromNameSpoof: fns_extrachars parameter default value has been increased to 50
-
-- nosubject and maxhits tflags now work correctly with sa-compile
-
-Note for Users Upgrading to SpamAssassin 3.4.3
-----------------------------------------------
-
-- New subjprefix keyword added, this can be used to add a prefix to
-  email Subject if the original email matches a particular rule
-
-- New Util::is_fqdn_valid() function to validate hostname (DNS name) format
-  (Bug 7736).  To check if a name contains valid TLD, it's still needed to
-  additionally use RegistryBoundaries::is_domain_valid()
-
-- New OLEVBMacro plugin to detect OLE Macro inside documents attached to emails,
-  this plugin requires Archive::Zip and IO::String Perl modules to work.
-
-- Due to the dangerous nature of sa-update --allowplugins option, it
-  now prints a warning that --reallyallowplugins is required to use it.
-  This is to make sure all the legacy installations and wiki guides etc
-  still using it needlessly get fixed.
-
-- TxRep and Awl plugins has been modified to be compatible 
-  with latest Postgresql versions.
-  You should upgrade your sql database running the following command:
-  MySQL:
-  "ALTER TABLE `txrep` CHANGE `count` `msgcount` INT(11) NOT NULL DEFAULT '0';"
-  "ALTER TABLE `awl` CHANGE `count` `msgcount` INT(11) NOT NULL DEFAULT '0';"
-  "ALTER TABLE `awl` ADD last_hit timestamp NOT NULL default CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;"
-  PostgreSQL:
-  "ALTER TABLE txrep RENAME COLUMN count TO msgcount;"
-  "ALTER TABLE awl RENAME COLUMN count TO msgcount;"
-  "ALTER TABLE awl ADD last_hit timestamp NOT NULL default CURRENT_TIMESTAMP;"
-
-- body_part_scan_size 50000, rawbody_part_scan_size 500000 defaults added (Bug 6582)
-  These enable safer and faster scanning of large emails.
-
-- ALL pseudo-header now returns decoded headers, so it's usage is consistent
-  with single header matching (:raw returns undecoded and folded like before).
-
-- RegistryBoundaries did not load 20_aux_tlds.cf properly in older versions. 
-  Old hardcoded list is now removed and RB will print "no tlds defined, need
-  to run sa-update" unless it can find list from config files.
-
-- Deprecated functions: Parser::is_delimited_regexp_valid(),
-  Parser::is_regexp_valid(), Util::regexp_remove_delimiters(),
-  Util::make_qr().  These all are combined into new Util::compile_regexp().
-
-- DNSEval: add check_rbl_headers to check specific headers in rbl
-
-- DNSEval: add check_rbl_ns_from to check against an rbl for dns servers
-
-- HashBL: Add check_hashbl_bodyre, check_hashbl_emails, check_hashbl_uris,
-  hashbl_ignore
-
-- ASN: Support IPv6 with asn_lookup_ipv6 (Bug 7211)
-
-- sa-update: New option --httputil to force used download utility
-
-- Add rules_matching() expression to meta rules
-
-- Add tflags domains_only/ips_only to DNSEval.pm functions
-
-- RelayCountry: Added new metadata: X-Spam-Countries-External (_RELAYCOUNTRYEXT_),
-  X-Spam-Countries-Auth (_RELAYCOUNTRYAUTH_), X-Spam-Countries-All (_RELAYCOUNTRYALL_)
-
-- New tflag "nosubject" for 'body' rules, to stop matching the Subject
-  header which is part of the body text.
-
-Note for Users Upgrading to SpamAssassin 3.4.2
-----------------------------------------------
-
-- We now support SHA-512 and SHA-256 signatures for our rules updates.
-
-- We may stop producing SHA-1 signatures in the near future so upgrading
-to 3.4.2 is important.  sa-update no longer uses these signatures.
-
-See https://bz.apache.org/SpamAssassin/show_bug.cgi?id=7614
-
-- freemail_import_whitelist_auth, freemail_import_def_whitelist_auth added (Bug 6451)
-
-
-New plugins
------------
+- New dns_block_rule option handles blocked DNSBLs (Bug 6728)
 
-There are four new plugins added with this release:
+- ASN: Support GeoDB for ASN lookups (asn_use_geodb, asn_prefer_geodb,
+  asn_use_dns).
 
-  Mail::SpamAssassin::Plugin::HashBL
+- ASN: Default sa-update ruleset doesn't make ASN lookups or add
+  headers anymore. Configure desired methods, asn_use_geodb or
+  asn_use_dns, and add_header clauses manually as described in the
+  plugin documentation. Usage of asn_use_geodb without DNS is
+  recommended unless ASNCIDR is needed. Do not use rules that check
+  metadata X-ASN header! Only the new eval function check_asn()
+  described in plugin manual works reliably.
 
-The HashBL plugin is the interface to The Email Blocklist (EBL).
-The EBL is intended to filter spam that is sent from IP addresses 
-and domains that cannot be blocked without causing significant 
-numbers of false positives.
+- sa-update: New --score-multiplier, --score-limit, and --forcemirror
+  options added.
+    #1 forcemirror: forces sa-update to use a specific mirror server,
+    #2 score-multiplier: adjust all scores from update channel by a
+     given multiplier to quickly level set scores to match your
+     preferred threshold
+    #3 score-limit adjusts all scores from update channel over a
+     specified limit to a new limit
 
-  Mail::SpamAssassin::Plugin::ResourceLimits
+- New dns_options "nov4" and "nov6" added.  IMPORTANT:; You must set
+  nov6 if your DNS resolver is filtering IPv6 AAAA replies.
 
-This plugin leverages BSD::Resource to assure your spamd child processes
-do not exceed specified CPU or memory limit. If this happens, the child
-process will die. See the BSD::Resource for more details.
+- API: Added Message::get_pristine_body_digest(),
+  Message::get_msgid(), and Message::generate_msgid()
+  functions. Removed deprecated private Plugin::Bayes::get_msgid()
+  function.
+
+- Bayes and TxRep seen Message-ID tracking hashing method changed.  No
+  actions are required. If re-learning some old messages, they might
+  be learned twice but old IDs should expire automatically.
+
+- report_charset defaults now to UTF-8.
+
+- Meta rules inherit net tflag setting from dependencies (Bug 7735)
+
+- BodyEval: Added plaintext_body_sig_ratio eval rules for the first
+  text/plain MIME part's body and signature length ratio.
+
+- API: Now supports multiple calls of $pms->test_log() for
+  rules. Added $pms->check_cleanup() to finalize tags, reports,
+  etc. Deprecated internal $pms->{test_log_msgs}, renamed to
+  $pms->{test_logs}. Deprecated $pms->clear_test_state() as it is not
+  needed anymore. $pms->test_log() now accepts $rulename as second
+  argument.
+
+- URIDNSBL: urirhsbl/urirhssub rules support "notrim" tflag to force
+  querying the full hostname instead of just the domain. This works
+  best if the specific uribl supports this mode. (Bug 7835)
+
+- Removed deprecated --auth-ident and --ident-timeout options from
+  spamd
+
+- MIMEHeader: support matching ALL header, tflags range, and tflags
+  concat
+
+- Autolearn: add new tflags autolearn_header/autolearn_body. These can
+  force a rule to count as header or body points accordingly. (Bug
+  7907)
+
+- SSL client certificate support for spamc/spamd is now easier. New
+  spamc options --ssl-cert, --ssl-key, --ssl-ca-file, and
+  --ssl-ca-path. New spamd options --ssl-verify, --ssl-ca-file, and
+  --ssl-ca-path (Bug 7267)
+
+- ArchiveIterator now automatically uncompressed all gzip, bzip2, xz,
+  lz4, lzip, and lzo-compressed files (Bug 7598). These apply to
+  spamassassin and sa-learn commands also.
+
+- New DMARC policy check plugin.
+
+- New project maintained DecodeShortURLs plugin which may not be
+  directly compatible with rules from other third party plugins. See
+  The plugin documentation for configuration and rule format.
+
+- Installing module Net::CIDR::Lite allows the use of dash-separated
+  IP range format (e.g. 192.168.1.1-192.168.255.255) for NetSet tables
+  including internal_networks, trusted_networks, msa_networks, and
+  uri_local_cidr.
+
+- The HashBL plugin in 342.pre is now enabled by default.
 
-  Mail::SpamAssassin::Plugin::FromNameSpoof
-
-This plugin allows for detection of the From:name field being used to mislead 
-recipients into thinking an email is from another address.  The man page 
-includes examples and we expect to put test rules for this plugin into 
-rulesrc soon!
-
-  Mail::SpamAssassin::Plugin::Phishing
-
-This plugin finds uris used in phishing campaigns detected by
-OpenPhish (https://openphish.com) or PhishTank (https://phishtank.com) feeds.
-
-These plugins are disabled by default. To enable, uncomment
-the loadplugin configuration options in file v342.pre, or add it to
-some local .pre file such as local.pre .
-
-Notable changes
----------------
-
-For security reasons SSLv3 support has been removed from spamc(1).
-
-GeoIP2 support has been added to RelayCountry and URILocalBL plugins due
-to GeoIP legacy api deprecations.
-
-New configuration options
--------------------------
-
-A new template tag _DKIMSELECTOR_ that maps to the DKIM selector (the 's' tag) 
-from valid signatures has been added.
-
-A 'uri_block_cont' option to URILocalBL plugin to score uris per continent has been added.
-Possible continent codes are:
-af, as, eu, na, oc, sa for Africa, Asia, Europe, North America, 
-Oceania and South America.
-
-The 'country_db_type' and 'country_db_path' options has been added to be able 
-to choose in RelayCountry plugin between GeoIP legacy 
-(discontinued from 04/01/2018), GeoIP2, IP::Country::Fast 
-and IP::Country::DB_File.
-GeoIP legacy is still the default option but it will be deprecated 
-in future releases.
-
-A config option 'uri_country_db_path' has been added to be able to choose 
-in URILocalBL plugin between GeoIP legacy and new GeoIP2 api.
-
-A config option 'resource_limit_cpu' (default: 0 or no limit) has been added
-to configure how many cpu cycles are allowed on a child process before it dies.
-
-A config option 'resource_limit_mem' (default: 0 or no limit) has been added
-to configure the maximum number of bytes of memory allowed both for 
-(virtual) address space bytes and resident set size.
-
-A new config option 'report_wrap_width' (default: 70) has been added
-to set the wrap width for description lines in the X-Spam-Report header.
-
-Notable Internal changes
-------------------------
-
-SpamAssassin can cope with new Net::DNS module versions.
-
-The "bytes" pragma has been remove from both core modules and plugins for
-better utf-8 compatibility, there has been also some other utf-8 related fixes.
-
-The spamc(1) client can now be build against OpenSSL 1.1.0.
-
-The test framework has been switched to Test::More module.
-
-Other updates
--------------
-
-A list of top-level domains in registrar boundaries was updated.
-
-
-Note for Users Upgrading to SpamAssassin 3.4.1
-----------------------------------------------
-
-- The TxRep plugin is now included and disabled by default for new installs.
-  To replace an existing AWL configuration with TxRep, follow the steps below:
-  - Disable AWL
-  - Enable TxRep
-  - Set txrep_factor equal to your previous AWL factor
-  - Set use_txrep to 1
-
-  For more detailed information and more configuration options, consult the
-  documentation in Mail::SpamAssassin::Plugin::TxRep.
-
-- The $VALID_TLDS_RE global in registrar boundaries is deprecated but kept for 
-  third-party plugin compatibility.  It will become increasingly out of date
-  and may be removed in a future release. 
-
-  See lib/Mail/SpamAssassin/Plugin/FreeMail.pm for an example of the new way
-  to obtain a valid list of TLDs, i.e. 
-
-    $self->{main}->{registryboundaries}->{valid_tlds_re} 
-
-- Mail::SpamAssassin::Util::RegistrarBoundaries is being replaced by 
-  Mail::SpamAssassin::RegistryBoundaries so that new TLDs can be updated via 
-  20_aux_tlds.cf delivered via sa-update.
-
-  ***3rd Party Plugin Authors, Please Note***
-  
-  The following functions will be removed in the next release after 3.4.1 
-  excepting any emergency break/fix releases immediately after 3.4.1:
-  
-  Mail::SpamAssassin::Util::RegistrarBoundaries::is_domain_valid 
-  Mail::SpamAssassin::Util::RegistrarBoundaries::trim_domain
-  Mail::SpamAssassin::Util::RegistrarBoundaries::split_domain 
-  Mail::SpamAssassin::Util::uri_to_domain 
-  
-  And the following variables will also be removed:
-  
-  Mail::SpamAssassin::Util::RegistrarBoundaries::US_STATES
-  Mail::SpamAssassin::Util::RegistrarBoundaries::THREE_LEVEL_DOMAINS
-  Mail::SpamAssassin::Util::RegistrarBoundaries::TWO_LEVEL_DOMAINS
-  Mail::SpamAssassin::Util::RegistrarBoundaries::VALID_TLDS_RE
-  Mail::SpamAssassin::Util::RegistrarBoundaries::VALID_TLDS
-  
-  
-  This change should only affect 3rd party plugin authors who will need to 
-  update their code to utilize Mail::SpamAssassin::RegistryBoundaries 
-  instead of the functions and variables in 
-  Mail::SpamAssassin::Util::RegistrarBoundaries and the function 
-  Mail::SpamAssassin::Util::uri_to_domain which are deprecated and will be 
-  removed.
-
-  For example, the $VALID_TLDS_RE global in registrar boundaries is 
-  deprecated but kept for third-party plugin compatibility.  It will become 
-  increasingly out of date and may be removed in a future release.
-
-  See lib/Mail/SpamAssassin/Plugin/FreeMail.pm for an example of the new way
-  to obtain a valid list of TLDs, i.e.
-
-    $self->{main}->{registryboundaries}->{valid_tlds_re}
-
-- It is now recommended that users uncomment "normalize_charset 1" in
-  local.cf. It will break rules that depend on messages being in non-UTF8
-  encodings, but going forward this will enable more robust unicode rules and
-  will eventually become the default.
-
-
-
-Note for Users Upgrading to SpamAssassin 3.4.0
-----------------------------------------------
-
-- When Bayes classification is in use and messages are 'learned' as spam
-  or ham and stored in a database, the Bayes plugin generates internal
-  message IDs of learned messages and stores them in a 'seen' database to
-  avoid re-learning duplicates and accidental un-learning messages that
-  were not previously learned. With changes in bug 5185, the calculation
-  of message IDs in a bayes 'seen' database has changed, so new code can
-  no longer associate new messages with those learned before the change.
-
-- Note that this change does not affect recognition of old tokens and the
-  classification algorithm, only duplicate detection and unlearning of old
-  messages is affected.
-
-- Because of this change, if you use Bayes and you are upgrading from a
-  version prior to 3.4.0, you may consider wiping your Bayes database
-  and starting fresh.
-
-- There is a new optional dependency on Net::Patricia to speed up lookups
-  on internal_networks, trusted_networks or msa_networks when these lists
-  contain a larger number of entries. Consider installing this module to
-  speed up classification.
-
-- The minimal required version of NetAddr::IP was bumped to 4.010
-
-- In addition to existing backends, the 3.4.0 introduces support for keeping
-  a Bayes database on a Redis server, either running locally, or accessed
-  over network. Similar to SQL backends, the database may be concurrently
-  used by several hosts running SpamAssassin.
-
-- For more detail on these and other changes, please see the Announcement
-  file at:
-   http://svn.apache.org/repos/asf/spamassassin/branches/3.4/build/announcements/3.4.0.txt
-
-Note for Users Upgrading to SpamAssassin 3.3.0
------------------------------------------------
-
-- Rules are no longer included with SpamAssassin "out of the box".  You will
-  need to immediately run "sa-update", or download the additional rules .tgz
-  package and run "sa-update --install" with it, to get a ruleset.
-
-- The BETA label has been taken off of the SpamAssassin SQL support.  Please
-  be aware that occasional changes may still be made to this area of the
-  code.  You should be sure to read this upgrade document each time you
-  install a new version to determine if any SQL updates need to be made to
-  your local installation.
-
-- The DKIM plugin is now enabled by default for new installs, if the perl
-  module Mail::DKIM is installed.  However, installation of SpamAssassin
-  will not overwrite existing .pre configuration files, so to use DKIM when
-  upgrading from a previous release that did not use DKIM, a directive:
-
-    loadplugin Mail::SpamAssassin::Plugin::DKIM
-
-  will need to be uncommented in file "v312.pre", or added to some
-  other .pre file, such as local.pre.
-
-
-Note for Users Upgrading to SpamAssassin 3.2.0
------------------------------------------------
-
-- The "127/8" network, including 127.0.0.1, is now always implicitly part of
-  "trusted_networks" and "internal_networks".  It's impossible to remove these
-  from the trusted/internal sets, since if you cannot trust the host where
-  SpamAssassin is running, you cannot trust SpamAssassin itself.  If you
-  previously had "trusted_networks" and "internal_networks" lines listing those
-  hosts, you should now remove them, otherwise a minor (non-lint-error) warning
-  will be output.
-
-- For ISPs -- version 3.2.0 now includes a new way to specify Mail Submission
-  Agents, relay hosts which accept mail from your own users and authenticates
-  them appropriately.  See the Mail::SpamAssassin::Conf manual page for the
-  "msa_networks" setting.
-
-
-Note for Users Upgrading to SpamAssassin 3.1.0
------------------------------------------------
-
-- A significant amount of core functionality has been moved into
-  plugins.  These include, AWL (auto-whitelist), DCC, Pyzor, Razor2,
-  SpamCop reporting and TextCat.  For information on configuring these
-  plugins please refer to their individual documentation:
-  perldoc Mail::SpamAssassin::Plugin::* (ie AWL, DCC, etc)
-
-- There are now multiple files read to enable plugins in the
-  /etc/mail/spamassassin directory; previously only one, "init.pre" was
-  read.  Now both "init.pre", "v310.pre", and any other files ending
-  in ".pre" will be read.  As future releases are made, new plugins
-  will be added to new files named according to the release they're
-  added in.
-
-- Due to license restrictions the DCC and Razor2 plugins are disabled
-  by default.  We encourage you to read the appropriate license
-  yourself and decide if you are able to re-enable the plugins for
-  your site.
-
-- The use_auto_whitelist config option has been moved to a user config
-  option, this allows individual users to turn on or off whitelisting
-  regardless of what is set in the system config.  If you would like to
-  disable AWL (auto-whitelist) on a site-wide basis, then you can comment
-  out the plugin in "v310.pre".
-
-- The bayes_auto_learn_threshold_* config options for bayes have moved
-  to a plugin.  In general the default should work just fine however
-  if you are interested in changing these values you should see:
-  perldoc Mail::SpamAssassin::Plugin::AutoLearnThreshold
-
-- The AWL support for NDBM_File databases has been dropped, due to a
-  bug in that package which renders it useless (bug 4353).  It appears
-  that SDBM_File, the package which will be used instead, is fully
-  compatible with NDBM however, so this should be unnoticeable.
-
-- The prefork algorithm for spamd has been changed.  In this version spamd
-  will attempt to keep a small number of "hot" child processes as busy as
-  possible, and keep any others as idle as possible, using something
-  similar to the Apache httpd server scaling algorithm. This reduces
-  memory usage and swapping. You can use the --round-robin switch for
-  spamd to disable this scaling algorithm, and the behaviour seen in the
-  3.0.x versions will be used instead, where all processes receive an
-  equal load and no scaling takes place.
-
-- As of 3.1.0, in addition to the generic BayesSQL support (via
-  Mail::SpamAssassin::BayesStore::SQL) usable by multiple database
-  drivers there is now specific support for MySQL 4.1+ and
-  PostgreSQL.  This support is based on non-standard features present
-  in both database servers that allow for various performance boosts.
-
-  If you were using the previous BayesSQL support with MySQL, and
-  already have MySQL 4.1+ installed you can begin using the new module
-  immediately by replacing the bayes_store_module line in your
-  configuration with:  Mail::SpamAssassin::BayesStore::MySQL
-
-  We do however recommend that you switch your MySQL tables over to
-  InnoDB for better data integrity and multi user support.  You can
-  most often do this via a simple ALTER TABLE command, refer to the
-  MySQL documentation for more information.
-
-  If you were previously using PostgreSQL for your bayes database then
-  we STRONGLY recommend switching to the PostgreSQL specific backend:
-  Mail::SpamAssassin::BayesStore::PgSQL
-  To switch to this backend you should first run sa-learn --backup to
-  make a backup of your existing data and then drop and recreate the
-  database following the instructions in sql/README.bayes.  Then you
-  can restore the database with sa-learn --restore.  If you have
-  multiple users then you will have to run --backup and --restore for
-  each user to fully restore the database.
-
-- See http://wiki.apache.org/spamassassin/UpgradeTo310 for a
-  supplementary list of upgrade notes.  It will be updated with any
-  upgrade notes not captured in this document.
-
-- Finally, this document is likely not complete.  Other configuration
-  options/arguments may have changed from older versions, etc.  It would
-  be good to double-check any custom configuration options to make sure
-  they're still valid.  This could be as simple as running "spamassassin
-  --lint", or more complex, as required by the environment.
-
-
-Note for Users Upgrading to SpamAssassin 3.0.x
-----------------------------------------------
-
-- The SpamAssassin 2.6x release series was the last set of releases to
-  officially support perl versions earlier than perl 5.6.1.  If you are
-  using an earlier version of perl, you will need to upgrade before you
-  can use the 3.0.0 version of SpamAssassin.  You will also want to make
-  sure that you have the appropriate versions of required and optional
-  modules as they may have changed from old versions.  The INSTALL
-  document has the modules and version requirements listed.
-
-- See http://wiki.apache.org/spamassassin/UpgradeTo300 for a
-  supplementary list of upgrade notes.  It will be updated with any
-  upgrade notes not captured in this document.
-
-- SpamAssassin 3.0.0 has a significantly different API (Application Program
-  Interface) from the 2.x series of code.  This means that if you use
-  SpamAssassin through a third-party utility (milter, etc,) you need to make
-  sure you have an updated version which supports 3.0.0.  See
-  http://wiki.apache.org/spamassassin/UpgradeTo300 for information about
-  third-party software.
-
-- The --auto-whitelist, --whitelist and -a options for "spamd" and
-  "spamassassin" to turn on the auto-whitelist have been removed and
-  replaced by the "use_auto_whitelist" configuration option which is
-  also now turned on by default.
-
-- The --virtual-config switch for spamd had to be dropped, due to licensing
-  issues.  It is replaced by the --virtual-config-dir switch.
-
-- The "rewrite_subject" and "subject_tag" configuration options were
-  deprecated and are now removed. Instead, using "rewrite_header Subject
-  [your desired setting]".  e.g.
-
-    rewrite_subject 1
-    subject_tag ****SPAM(_SCORE_)****
-
-  becomes
-
-    rewrite_header Subject ****SPAM(_SCORE_)****
-
-- The "sa-learn --rebuild" command has been deprecated; please use
-  "sa-learn --sync" instead.  The --rebuild option will remain temporarily
-  for backward compatibility.
-
-- The Bayesian storage modules have been completely re-written and now
-  include Berkeley DB (DBM) storage as well as SQL based storage (see
-  sql/README.bayes for more information).  In addition, a new format
-  has been introduced for the bayes database that stores tokens in fixed
-  length hashes (Bayes v3).  All DBM databases should be automatically
-  converted to this new format the first time they are opened for write.
-  You can manually perform the upgrade by running "sa-learn --sync"
-  from the command line.
-
-  Due to the database format change, you will want to do something like
-  this when upgrading:
-
-  - stop running spamassassin/spamd (ie: you don't want it to be running
-    during the upgrade)
-  - run "sa-learn --rebuild", this will sync your journal.  if you skip
-    this step, any data from the journal will be lost when the DB is
-    upgraded.
-  - upgrade SA to 3.0.0
-  - run "sa-learn --sync", which will cause the db format to be upgraded.
-    if you want to see what is going on, you can add the "-D" option.
-  - test the new database by running some sample mails through
-    SpamAssassin, and/or at least running "sa-learn --dump" to make sure
-    the data looks valid.
-  - start running spamassassin/spamd again
-
-  If, instead of uprading your Bayes database, you want to wipe it and
-  start fresh, you can run "sa-learn --clear" to safely remove your
-  Bayes database files.  If the --clear command issues an error then
-  you can simply delete the Bayes database files ("bayes_*") while SA
-  is not running; SpamAssassin will recreate them in the current
-  format when it runs.
-
-- "spamd" now has a default max-children setting of 5; no more than 5
-  child scanner processes will be run in parallel.  Previously, there was
-  no default limit unless you specified the "-m" switch when starting
-  spamd.
-
-- If you are using a UNIX machine with all database files on local disks,
-  and no sharing of those databases across NFS filesystems, you can use a
-  more efficient, but non-NFS-safe, locking mechanism.   Do this by adding
-  the line "lock_method flock" to the /etc/mail/spamassassin/local.cf
-  file. This is strongly recommended if you're not using NFS, as it is
-  much faster than the NFS-safe locker.
-
-- Please note that the use of the following commandline parameters for
-  spamassassin and spamd have been deprecated and may be removed in
-  upcoming versions of SpamAssassin.  Please discontinue usage of these
-  options:
-
-    in the 2.6x series:                --add-from, --pipe, -F, --stop-at-threshold,
-                               -S, -P (spamassassin only)
-    in the 3.0.x series:       --auto-whitelist, -a, --whitelist-factory, -M,
-                               --warning-from, -w, --log-to-mbox, -l
-
-- user_scores_sql_table is no longer supported.  If you need to use a table
-  name, other than the default, create a custom query using the
-  user_scores_sql_custom_query config option.
-
-- SpamAssassin runs in "taint mode" by default for improved security.
-  Certain third-party modules may be incompatible with taint mode.
-
-- 2.6x deprecated the use of the "check_bayes_db" script, and it
-  has been removed in 3.0.0.  Please see the sa-learn man/pod
-  documentation for more info.
-
-- Finally, this document is likely not complete.  Other configuration
-  options/arguments may have changed from older versions, etc.  It would
-  be good to double-check any custom configuration options to make sure
-  they're still valid.  This could be as simple as running "spamassassin
-  --lint", or more complex, as required by the environment.
-
-  An example: "require_version <version>" hasn't changed itself, but the
-  internal version representation is now "x.yyyzzz" instead of "x.yz"
-  which could cause issues if "require_version 3.00" is expected to work
-  (it won't, it needs to be "require_version 3.000000").
-
-
-Note for Users Upgrading from SpamAssassin 2.5x
------------------------------------------------
-
-- Due to major reliability shortcomings in the database support libraries
-  other than DB_File, we now require that the DB_File module be installed
-  to use SpamAssassin's Bayes rules.
-
-  SpamAssassin will still work without DB_File installed, but the Bayes
-  support will be disabled.
-
-  If you install DB_File and wish to import old Bayes database data, each
-  user with a Bayes db should run "sa-learn --import" to copy old entries
-  from the other formats into a new DB_File file.
-
-  Due to the database library change, and the change to the database
-  format itself, you will want to do something like this when upgrading:
-
-  - stop running spamassassin/spamd (ie: you don't want it to be running
-    during the upgrade)
-  - run "sa-learn --rebuild", this will sync your journal.  if you skip
-    this step, any data from the journal will be lost when the DB is
-    upgraded.
-  - install DB_File module if necessary
-  - upgrade SA to 3.0.0
-  - if you were using another database module previously, run "sa-learn
-    --import" to migrate the data into new DB_File files
-  - run "sa-learn --sync", which will cause the db format to be upgraded.
-    if you want to see what is going on, you can add the "-D" option.
-  - test the new database by running some sample mails through
-    SpamAssassin, and/or at least running "sa-learn --dump" to make sure
-    the data looks valid.
-  - start running spamassassin/spamd again
-
-  Obviously the steps will be different depending on your environment, but
-  you get the idea. :)
-
-
-Note For Users Upgrading From SpamAssassin 2.3x or 2.4x
--------------------------------------------------------
-
-- SpamAssassin no longer includes code to handle local mail delivery, as
-  it was not reliable enough, compared to procmail.  So now, if you relied
-  on spamassassin to write the mail into your mail folder, you'll have to
-  change your setup to use procmail as detailed below.  If you used
-  spamassassin to filter your mail and then something else wrote it into a
-  folder for you, then you should be fine.
-
-- Support for versions of the optional Mail::Audit module is no longer
-  included.
-
-- The default mode of tagging (which used to be ***SPAM*** in the subject
-  line) no longer takes place.  Instead the message is rewritten. If an
-  incoming message is tagged as spam, instead of modifying the original
-  message, SpamAssassin will create a new report message and attach the
-  original message as a message/rfc822 MIME part (ensuring the original
-  message is completely preserved and easier to recover).  If you do not
-  want to modify the body of incoming spam, use the "report_safe" option.
-  The "report_header" and "defang_mime" options have been removed as a
-  result.
+- HeaderEval check_for_unique_subject_id() function is deprecated.
 
 (end of UPGRADE)
-
-//vim:tw=74:
index 5c0905039486c1096d0d86abb12e72e360fa8704..f212af309a35032e379a9e50b1955fa317551975 100644 (file)
@@ -84,7 +84,7 @@ If you use procmail, or haven't decided on any of the above examples:
 If you want to use SpamAssassin site-wide:
 
   - take a look at the notes on the Wiki website, currently at
-    <http://wiki.apache.org/spamassassin/UsingSiteWide>.  You will probably
+    <https://wiki.apache.org/spamassassin/UsingSiteWide>.  You will probably
     want to use 'spamd' (see below).   You may want to investigate the
     new Apache mod_perl module, in the 'spamd-apache2' directory, too.
 
@@ -101,11 +101,11 @@ If you want to use SpamAssassin site-wide:
     add the line 'DROPPRIVS=yes' at the top of the file.
 
 
-The Auto-Whitelist
+The Auto-Welcomelist
 ------------------
 
-The auto-whitelist is enabled using the 'use_auto_whitelist' option.
-(See http://wiki.apache.org/spamassassin/AutoWhitelist for details on
+The auto-welcomelist is enabled using the 'use_auto_welcomelist' option.
+(See https://wiki.apache.org/spamassassin/AutoWelcomelist for details on
 how it works, if you're curious.)
 
 
@@ -113,15 +113,6 @@ Other Installation Notes
 ------------------------
 
   
-  - Hashcash is a useful system; it requires that senders exercise a
-    CPU-intensive task before they can send mail to you, so we give that
-    some bonus points.  However, it requires that you list what addresses
-    you expect to receive mail for, by adding 'hashcash_accept' lines to
-    your ~/.spamassassin/user_prefs or /etc/mail/spamassassin/local.cf
-    files.  See the Mail::SpamAssassin::Plugin::Hashcash manual page for
-    details on how to specify these.
-
-
   - SpamAssassin now uses a temporary file in /tmp (or $TMPDIR, if that's
     set in the environment) for Pyzor and DCC checks.  Make sure that this
     directory is either (a) not writable by other users, or (b) not shared
@@ -161,7 +152,7 @@ Other Installation Notes
   - A very handy new feature is SPF support, which allows you to check
     that the message sender is permitted by their domain to send from the
     IP address used.  This has the potential to greatly cut down on mail
-    forgery.  (see http://spf.pobox.com/ for more details.)
+    forgery.
 
 
   - MDaemon users should add this line to their "local.cf" file:
@@ -219,7 +210,7 @@ Other Installation Notes
     to do this, take a look here [1] for a simple forwarding-based
     alternative.
 
-      [1]: http://wiki.apache.org/spamassassin/SpamTrapping
+      [1]: https://wiki.apache.org/spamassassin/SpamTrapping
 
 
   - Scores and other user preferences can now be loaded from, and Bayes
@@ -242,7 +233,7 @@ Other Installation Notes
 
 
   - Lots more ways to integrate SpamAssassin can be read at
-    http://wiki.SpamAssassin.org/ .
+    https://wiki.apache.org/spamassassin/ .
 
 
 (end of USAGE)
index 52c64ed519177b181a27c092a9a29647cd75a912..f25f5ef74cbf2df126dbf3f11fdd4d810c4a81a4 100755 (executable)
@@ -239,6 +239,7 @@ sub lint_rule_text {
   my $pretext = q{
     loadplugin Mail::SpamAssassin::Plugin::Check
     loadplugin Mail::SpamAssassin::Plugin::URIDNSBL
+    util_rb_tld com # skip "need to run sa-update" warn
     use_bayes 0
   };
 
index 44869de693443213e4d8ec0e7f4bb03c39b0e9e3..bb17325ca6bf95cbf20b65eebd13d907742f0a4b 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/perl
 
 BEGIN {
-  require Digest::SHA; import Digest::SHA qw(sha256_hex sha512_hex);
+  require Digest::SHA; Digest::SHA->import(qw(sha256_hex sha512_hex));
 }
 
 $/=undef;
index d06a5a5c2e6cfb01a66986f197762534a669d6a6..e24b1714918045f5596d53d62ae72134e404102b 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/perl
 
 BEGIN {
-  require Digest::SHA; import Digest::SHA qw(sha256_hex sha512_hex);
+  require Digest::SHA; Digest::SHA->import(qw(sha256_hex sha512_hex));
 }
 
 $/=undef;
index 64583d90ddd1ecce888208adedd8e3adfab3848a..8f6e995cdeb29d99c30f0aecaad3f9e25e983626 100644 (file)
@@ -6,9 +6,9 @@ SpamAssassin can now load users' score files from an LDAP server.  The concept
 here is to have a web application (PHP/perl/ASP/etc.) that will allow users to
 be able to update their local preferences on how SpamAssassin will filter their
 e-mail.  The most common use for a system like this would be for users to be
-able to update the white list of addresses (whitelist_from) without the need
-for them to update their $HOME/.spamassassin/user_prefs file.  It is also quite
-common for users listed in /etc/passwd to not have a home directory,
+able to update the white list of addresses (welcomelist_from, previously whitelist_from) 
+without the need for them to update their $HOME/.spamassassin/user_prefs file.  
+It is also quite common for users listed in /etc/passwd to not have a home directory,
 therefore, the only way to have their own local settings would be through a
 database or LDAP server.
 
@@ -38,8 +38,8 @@ Examples:
 
 If the user_scores_dsn option does not exist, SpamAssassin will not attempt
 to use an LDAP server for retrieving users' preferences. Note that this will
-NOT look for test rules, only local scores, whitelist_from(s), and
-required_score.
+NOT look for test rules, only local scores, welcomelist_from(s) (previously whitelist_from), 
+and required_score.
 
 Requirements
 ------------
@@ -68,7 +68,7 @@ our inetOrgPerson subclass.
 Here's an example for openldap's /etc/openldap/schema/inetorgperson.schema :
 
   # SpamAssassin
-  # see http://SpamAssassin.org/ .
+  # see https://SpamAssassin.org/ .
   attributetype ( 2.16.840.1.113730.3.1.217
           NAME 'spamassassin'
           DESC 'SpamAssassin user preferences settings'
@@ -97,9 +97,6 @@ If you do not see the above text, then the LDAP query was not successful, and
 you should see any error messages reported. <username> should be the user
 that was passed to spamd and is usually the user executing spamc.
 
-If you need to set up LDAP, a good guide is here:
-http://yolinux.com/TUTORIALS/LinuxTutorialLDAP.html
-
 To test LDAP support using the SpamAssassin test suite, you need to
 perform a little bit of manual configuration first.  See the file
 "ldap/README.testing" for details.
@@ -111,6 +108,6 @@ operation of LDAP support may change at any time with future releases of SA.
 ******
 
 Please send any comments to <kris at koehntopp.de> and file bugs via
-<http://issues.apache.org/SpamAssassin/>.
+<https://issues.apache.org/SpamAssassin/>.
 
 Kristian Köhntopp
index 7f4d571b3bc67f8c6b6c63b1448b8f0e53c1c4d7..d9ce4fd4627b9c14ef07c09381ab93e92122e9fb 100644 (file)
@@ -40,7 +40,7 @@ Mail::SpamAssassin - Spam detector and markup engine
 =head1 DESCRIPTION
 
 Mail::SpamAssassin is a module to identify spam using several methods
-including text analysis, internet-based realtime blacklists, statistical
+including text analysis, internet-based realtime blocklists, statistical
 analysis, and internet-based hashing algorithms.
 
 Using its rule base, it uses a wide range of heuristic tests on mail
@@ -64,7 +64,7 @@ use warnings;
 # use bytes;
 use re 'taint';
 
-require 5.006_001;
+require v5.14.0;
 
 use Mail::SpamAssassin::Logger;
 use Mail::SpamAssassin::Constants;
@@ -87,11 +87,10 @@ use Time::HiRes qw(time);
 use Cwd;
 use Config;
 
-our $VERSION = "3.004006";      # update after release (same format as perl $])
+our $VERSION = "4.000000";      # update after release (same format as perl $])
 #our $IS_DEVEL_BUILD = 1;        # 1 for devel build 
 our $IS_DEVEL_BUILD = 0;        # 0 for release versions including rc & pre releases
 
-
 # Used during the prerelease/release-candidate part of the official release
 # process. If you hacked up your SA, you should add a version_tag to your .cf
 # files; this variable should not be modified.
@@ -101,18 +100,18 @@ our @ISA = qw();
 
 # SUB_VERSION is now just <yyyy>-<mm>-<dd>
 our $SUB_VERSION = 'svnunknown';
-if ('$LastChangedDate: 2021-04-09 19:54:52 +1200 (Fri, 09 Apr 2021) $' =~ ':') {
-  # Subversion keyword "$LastChangedDate: 2021-04-09 19:54:52 +1200 (Fri, 09 Apr 2021) $" has been successfully expanded.
+if ('$LastChangedDate: 2022-12-14 02:29:30 +0000 (Wed, 14 Dec 2022) $' =~ ':') {
+  # Subversion keyword "$LastChangedDate: 2022-12-14 02:29:30 +0000 (Wed, 14 Dec 2022) $" has been successfully expanded.
   # Doesn't happen with automated launchpad builds:
   # https://bugs.launchpad.net/launchpad/+bug/780916
-  $SUB_VERSION = (split(/\s+/,'$LastChangedDate: 2021-04-09 19:54:52 +1200 (Fri, 09 Apr 2021) $ updated by SVN'))[1];
+  $SUB_VERSION = (split(/\s+/,'$LastChangedDate: 2022-12-14 02:29:30 +0000 (Wed, 14 Dec 2022) $ updated by SVN'))[1];
 }
 
 
 if (defined $IS_DEVEL_BUILD && $IS_DEVEL_BUILD) {
-  if ('$LastChangedRevision: 1888548 $' =~ ':') {
-    # Subversion keyword "$LastChangedRevision: 1888548 $" has been successfully expanded.
-    push(@EXTRA_VERSION, ('r' . qw{$LastChangedRevision: 1888548 $ updated by SVN}[1]));
+  if ('$LastChangedRevision: 1905971 $' =~ ':') {
+    # Subversion keyword "$LastChangedRevision: 1905971 $" has been successfully expanded.
+    push(@EXTRA_VERSION, ('r' . qw{$LastChangedRevision: 1905971 $ updated by SVN}[1]));
   } else {
     push(@EXTRA_VERSION, ('r' . 'svnunknown'));
   }
@@ -428,8 +427,8 @@ sub new {
     $self->timer_enable();
   }
 
-  $self->{conf} ||= new Mail::SpamAssassin::Conf ($self);
-  $self->{plugins} = Mail::SpamAssassin::PluginHandler->new ($self);
+  $self->{conf} ||= Mail::SpamAssassin::Conf->new($self);
+  $self->{plugins} = Mail::SpamAssassin::PluginHandler->new($self);
 
   $self->{save_pattern_hits} ||= 0;
 
@@ -469,7 +468,7 @@ sub create_locker {
   # for slow but safe, by keeping in quotes
   eval '
     use Mail::SpamAssassin::Locker::'.$class.';
-    $self->{locker} = new Mail::SpamAssassin::Locker::'.$class.' ($self);
+    $self->{locker} = Mail::SpamAssassin::Locker::'.$class.'->new($self);
     1;
   ' or do {
     my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
@@ -728,6 +727,9 @@ sub init_learner {
     if (exists $opts->{$k}) { $self->{$v} = $opts->{$k}; }
   }
 
+  # Set flag which can be checked from plugins etc
+  $self->{learning} = 1;
+
   return \%ret;
 }
 
@@ -757,6 +759,7 @@ Finish learning.
 sub finish_learner {
   my $self = shift;
   $self->{bayes_scanner}->force_close(1) if $self->{bayes_scanner};
+  delete $self->{learning};
   1;
 }
 
@@ -832,7 +835,7 @@ sub signal_user_changed {
   $self->{bayes_scanner}->finish() if $self->{bayes_scanner};
   if ($self->{conf}->{use_bayes}) {
       require Mail::SpamAssassin::Bayes;
-      $self->{bayes_scanner} = new Mail::SpamAssassin::Bayes ($self);
+      $self->{bayes_scanner} = Mail::SpamAssassin::Bayes->new($self);
   } else {
       delete $self->{bayes_scanner} if $self->{bayes_scanner};
   }
@@ -949,73 +952,84 @@ sub revoke_as_spam {
 
 ###########################################################################
 
-=item $f->add_address_to_whitelist ($addr, $cli_p)
+=item $f->add_address_to_welcomelist ($addr, $cli_p)
+
+Previously add_address_to_whitelist which will work interchangeably until 4.1.
 
 Given a string containing an email address, add it to the automatic
-whitelist database.
+welcomelist database.
 
 If $cli_p is set then underlying plugin may give visual feedback on additions/failures.
 
 =cut
 
-sub add_address_to_whitelist {
+sub add_address_to_welcomelist {
   my ($self, $addr, $cli_p) = @_;
 
-  $self->call_plugins("whitelist_address", { address => $addr,
+  $self->call_plugins("welcomelist_address", { address => $addr,
                                              cli_p => $cli_p });
 }
+*add_address_to_whitelist = \&add_address_to_welcomelist; # removed in 4.1
 
 ###########################################################################
 
-=item $f->add_all_addresses_to_whitelist ($mail, $cli_p)
+=item $f->add_all_addresses_to_welcomelist ($mail, $cli_p)
+
+Previously add_all_addresses_to_whitelist which will work interchangeably until 4.1.
 
 Given a mail message, find as many addresses in the usual headers (To, Cc, From
-etc.), and the message body, and add them to the automatic whitelist database.
+etc.), and the message body, and add them to the automatic welcomelist database.
 
 If $cli_p is set then underlying plugin may give visual feedback on additions/failures.
 
 =cut
 
-sub add_all_addresses_to_whitelist {
+sub add_all_addresses_to_welcomelist {
   my ($self, $mail_obj, $cli_p) = @_;
 
   foreach my $addr ($self->find_all_addrs_in_mail ($mail_obj)) {
-    $self->call_plugins("whitelist_address", { address => $addr,
+    $self->call_plugins("welcomelist_address", { address => $addr,
                                                cli_p => $cli_p });
   }
 }
+*add_all_addresses_to_whitelist = \&add_all_addresses_to_welcomelist; # removed in 4.1
 
 ###########################################################################
 
-=item $f->remove_address_from_whitelist ($addr, $cli_p)
+=item $f->remove_address_from_welcomelist ($addr, $cli_p)
+
+Previously remove_address_from_whitelist which will work interchangeably until 4.1.
 
 Given a string containing an email address, remove it from the automatic
-whitelist database.
+welcomelist database.
 
 If $cli_p is set then underlying plugin may give visual feedback on additions/failures.
 
 =cut
 
-sub remove_address_from_whitelist {
+sub remove_address_from_welcomelist {
   my ($self, $addr, $cli_p) = @_;
 
   $self->call_plugins("remove_address", { address => $addr,
                                           cli_p => $cli_p });
 }
+*remove_address_from_whitelist = \&remove_address_from_welcomelist; # removed in 4.1
 
 ###########################################################################
 
-=item $f->remove_all_addresses_from_whitelist ($mail, $cli_p)
+=item $f->remove_all_addresses_from_welcomelist ($mail, $cli_p)
+
+Previously remove_all_addresses_from_whitelist which will work interchangeably until 4.1.
 
 Given a mail message, find as many addresses in the usual headers (To, Cc, From
-etc.), and the message body, and remove them from the automatic whitelist
+etc.), and the message body, and remove them from the automatic welcomelist
 database.
 
 If $cli_p is set then underlying plugin may give visual feedback on additions/failures.
 
 =cut
 
-sub remove_all_addresses_from_whitelist {
+sub remove_all_addresses_from_welcomelist {
   my ($self, $mail_obj, $cli_p) = @_;
 
   foreach my $addr ($self->find_all_addrs_in_mail ($mail_obj)) {
@@ -1023,30 +1037,36 @@ sub remove_all_addresses_from_whitelist {
                                             cli_p => $cli_p });
   }
 }
+*remove_all_addresses_from_whitelist = \&remove_all_addresses_from_welcomelist; # removed in 4.1
 
 ###########################################################################
 
-=item $f->add_address_to_blacklist ($addr, $cli_p)
+=item $f->add_address_to_blocklist ($addr, $cli_p)
+
+Previously add_address_to_blacklist which will work interchangeably until 4.1.
 
 Given a string containing an email address, add it to the automatic
-whitelist database with a high score, effectively blacklisting them.
+welcomelist database with a high score, effectively blocklisting them.
 
 If $cli_p is set then underlying plugin may give visual feedback on additions/failures.
 
 =cut
 
-sub add_address_to_blacklist {
+sub add_address_to_blocklist {
   my ($self, $addr, $cli_p) = @_;
-  $self->call_plugins("blacklist_address", { address => $addr,
+  $self->call_plugins("blocklist_address", { address => $addr,
                                              cli_p => $cli_p });
 }
+*add_address_to_blacklist = \&add_address_to_blocklist; # removed in 4.1
 
 ###########################################################################
 
-=item $f->add_all_addresses_to_blacklist ($mail, $cli_p)
+=item $f->add_all_addresses_to_blocklist ($mail, $cli_p)
+
+Previously add_all_addresses_to_blacklist which will work interchangeably until 4.1.
 
 Given a mail message, find addresses in the From headers and add them to the
-automatic whitelist database with a high score, effectively blacklisting them.
+automatic welcomelist database with a high score, effectively blocklisting them.
 
 Note that To and Cc addresses are not used.
 
@@ -1054,23 +1074,26 @@ If $cli_p is set then underlying plugin may give visual feedback on additions/fa
 
 =cut
 
-sub add_all_addresses_to_blacklist {
+sub add_all_addresses_to_blocklist {
   my ($self, $mail_obj, $cli_p) = @_;
 
   $self->init(1);
 
   my @addrlist;
   my @hdrs = $mail_obj->get_header('From');
-  if ($#hdrs >= 0) {
-    push (@addrlist, $self->find_all_addrs_in_line (join (" ", @hdrs)));
+  foreach my $hdr (@hdrs) {
+    my @addrs = Mail::SpamAssassin::Util::parse_header_addresses($hdr);
+    foreach my $addr (@addrs) {
+      push @addrlist, $addr->{address} if defined $addr->{address};
+    }
   }
 
   foreach my $addr (@addrlist) {
-    $self->call_plugins("blacklist_address", { address => $addr,
+    $self->call_plugins("blocklist_address", { address => $addr,
                                                cli_p => $cli_p });
   }
-
 }
+*add_all_addresses_to_blacklist = \&add_all_addresses_to_blocklist; # removed in 4.1
 
 ###########################################################################
 
@@ -1158,13 +1181,12 @@ sub remove_spamassassin_markup {
   my $hdrs = $mail_obj->get_pristine_header();
   my $body = $mail_obj->get_pristine_body();
 
-  # remove DOS line endings
-  $hdrs =~ s/\r//gs;
+  # force \n for line-ending processing temporarily
+  $hdrs =~ s/\015?\012/\n/gs;
+  $body =~ s/\015?\012/\n/gs;
 
   # unfold SA added headers, but not X-Spam-Prev headers ...
-  $hdrs = "\n".$hdrs;   # simplifies regexp below
-  1 while $hdrs =~ s/(\nX-Spam-(?!Prev).+?)\n[ \t]+(\S.*\n)/$1 $2/g;
-  $hdrs =~ s/^\n//;
+  1 while $hdrs =~ s/((?:^|\n)X-Spam-(?!Prev).+?)\n[ \t]+(\S.*\n)/$1 $2/g;
 
 ###########################################################################
   # Backward Compatibility, pre 3.0.x.
@@ -1215,14 +1237,11 @@ sub remove_spamassassin_markup {
   }
 
   # remove any other X-Spam headers we added, will be unfolded
-  $hdrs = "\n".$hdrs;   # simplifies regexp below
-  1 while $hdrs =~ s/\nX-Spam-.*\n/\n/g;
-  $hdrs =~ s/^\n//;
+  1 while $hdrs =~ s/(^|\n)X-Spam-.*\n/$1/g;
 
-  # re-add DOS line endings
-  if ($mail_obj->{line_ending} ne "\n") {
-    $hdrs =~ s/\r?\n/$mail_obj->{line_ending}/gs;
-  }
+  # force original message line endings
+  $hdrs =~ s/\n/$mail_obj->{line_ending}/gs;
+  $body =~ s/\n/$mail_obj->{line_ending}/gs;
 
   # Put the whole thing back together ...
   return join ('', $mbox, $hdrs, $body);
@@ -1235,8 +1254,8 @@ sub remove_spamassassin_markup {
 Read a configuration file and parse user preferences from it.
 
 User preferences are as defined in the C<Mail::SpamAssassin::Conf> manual page.
-In other words, they include scoring options, scores, whitelists and
-blacklists, and so on, but do not include rule definitions, privileged
+In other words, they include scoring options, scores, welcomelists and
+blocklists, and so on, but do not include rule definitions, privileged
 settings, etc. unless C<allow_user_rules> is enabled; and they never include
 the administrator settings.
 
@@ -1261,7 +1280,8 @@ sub read_scoreonly_config {
 
   $text = "file start $filename\n" . $text;
   # add an extra \n in case file did not end in one.
-  $text .= "\nfile end $filename\n";
+  $text .= "\n" unless $text =~ /\n\z/;
+  $text .= "file end $filename\n";
 
   $self->{conf}->{main} = $self;
   $self->{conf}->parse_scores_only ($text);
@@ -1326,7 +1346,7 @@ sub load_scoreonly_ldap {
 =item $f->set_persistent_address_list_factory ($factoryobj)
 
 Set the persistent address list factory, used to create objects for the
-automatic whitelist algorithm's persistent-storage back-end.  See
+automatic welcomelist algorithm's persistent-storage back-end.  See
 C<Mail::SpamAssassin::PersistentAddrList> for the API these factory objects
 must implement, and the API the objects they produce must implement.
 
@@ -1499,7 +1519,7 @@ sub lint_rules {
   $self->{dont_copy_prefs} = $olddcp;       # revert back to previous
 
   # bug 5048: override settings to ensure a faster lint
-  $self->{'conf'}->{'use_auto_whitelist'} = 0;
+  $self->{'conf'}->{'use_auto_welcomelist'} = 0;
   $self->{'conf'}->{'bayes_auto_learn'} = 0;
 
   my $mail = $self->parse(\@testmsg, 1, { master_deadline => undef });
@@ -1531,7 +1551,6 @@ sub finish {
   $self->call_plugins("finish_tests", { conf => $self->{conf},
                                         main => $self });
 
-  $self->{conf}->finish(); delete $self->{conf};
   $self->{plugins}->finish(); delete $self->{plugins};
 
   if ($self->{bayes_scanner}) {
@@ -1541,6 +1560,8 @@ sub finish {
 
   $self->{resolver}->finish()  if $self->{resolver};
 
+  $self->{conf}->finish(); delete $self->{conf};
+
   $self->timer_end("finish");
   %{$self} = ();
 }
@@ -1662,6 +1683,11 @@ sub init {
   # Note that this PID has run init()
   $self->{_initted} = $$;
 
+  # if spamd or other forking, wait for spamd_child_init
+  if (!$self->{skip_prng_reseeding}) {
+    $self->set_global_state_dir();
+  }
+
   #fix spamd reading root prefs file
   if (!defined $use_user_pref) {
     $use_user_pref = 1;
@@ -1737,10 +1763,18 @@ sub init {
   }
 
   if ($self->{pre_config_text}) {
-    $self->{config_text} = $self->{pre_config_text} . $self->{config_text};
+    $self->{pre_config_text} .= "\n" unless $self->{pre_config_text} =~ /\n\z/;
+    $self->{config_text} = "file start (pre_config_text)\n".
+                           $self->{pre_config_text}.
+                           "file end (pre_config_text)\n".
+                           $self->{config_text};
   }
   if ($self->{post_config_text}) {
-    $self->{config_text} .= $self->{post_config_text};
+    $self->{post_config_text} .= "\n" unless $self->{post_config_text} =~ /\n\z/;
+    $self->{config_text} .= "\n" unless $self->{config_text} =~ /\n\z/;
+    $self->{config_text} .= "file start (post_config_text)\n".
+                            $self->{post_config_text}.
+                            "file end (post_config_text)\n";
   }
 
   if ($self->{config_text} !~ /\S/) {
@@ -1771,7 +1805,7 @@ sub init {
   # Initialize the Bayes subsystem
   if ($self->{conf}->{use_bayes}) {
       require Mail::SpamAssassin::Bayes;
-      $self->{bayes_scanner} = new Mail::SpamAssassin::Bayes ($self);
+      $self->{bayes_scanner} = Mail::SpamAssassin::Bayes->new($self);
   }
   $self->{'learn_to_journal'} = $self->{conf}->{bayes_learn_to_journal};
 
@@ -1796,6 +1830,21 @@ sub init {
   # should be called only after configuration has been parsed
   $self->{resolver} = Mail::SpamAssassin::DnsResolver->new($self);
 
+  # load GeoDB if some plugin wants it
+  if ($self->{geodb_wanted}) {
+    eval '
+      use Mail::SpamAssassin::GeoDB;
+      $self->{geodb} = Mail::SpamAssassin::GeoDB->new({
+        conf => $self->{conf}->{geodb},
+        wanted => $self->{geodb_wanted},
+      });
+      1;
+    ';
+    if ($@ || !$self->{geodb}) {
+      dbg("config: GeoDB disabled: $@");
+    }
+  }
+
   # TODO -- open DNS cache etc. if necessary
 }
 
@@ -1826,10 +1875,10 @@ sub _read_cf_pre {
       dbg("config: file or directory $path not accessible: $!");
     } elsif (-d _) {
       foreach my $file ($self->$filelistmethod($path)) {
-        $txt .= read_cf_file($file);
+        $txt .= $self->read_cf_file($file);
       }
     } elsif (-f _ && -s _ && -r _) {
-      $txt .= read_cf_file($path);
+      $txt .= $self->read_cf_file($path);
     }
   }
 
@@ -1838,9 +1887,14 @@ sub _read_cf_pre {
 
 
 sub read_cf_file {
-  my($path) = @_;
+  my($self, $path) = @_;
   my $txt = '';
 
+  if ($self->{cf_files_read}->{$path}++) {
+    dbg("config: skipping already read file: $path");
+    return $txt;
+  }
+
   local *IN;
   if (open (IN, "<".$path)) {
 
@@ -1852,7 +1906,8 @@ sub read_cf_file {
 
     $txt = "file start $path\n" . $txt;
     # add an extra \n in case file did not end in one.
-    $txt .= "\nfile end $path\n";
+    $txt .= "\n" unless $txt =~ /\n\z/;
+    $txt .= "file end $path\n";
 
     dbg("config: read file $path");
   }
@@ -1900,7 +1955,7 @@ sub get_and_create_userstate_dir {
     dbg("config: error accessing $fname: $!");
   } else {  # does not exist, create it
     eval {
-      mkpath($fname, 0, 0700);  1;
+      mkpath(Mail::SpamAssassin::Util::untaint_file_path($fname), 0, 0700);  1;
     } or do {
       my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
       dbg("config: mkdir $fname failed: $eval_stat");
@@ -1910,6 +1965,58 @@ sub get_and_create_userstate_dir {
   $fname;
 }
 
+# find the most global writable state dir
+# used by dns_block_rule state files etc
+sub set_global_state_dir {
+  my ($self) = @_;
+  # try home_dir_for_helpers
+  my $helper_dir = $self->{home_dir_for_helpers} || '';
+  if ($helper_dir) {
+    my $dir = File::Spec->catdir($helper_dir, ".spamassassin");
+    return if $self->test_global_state_dir($dir);
+  }
+  # try user home (if different from helper home)
+  my $home;
+  if (am_running_on_windows()) {
+    # Windows has a special folder for common appdata (Bug 8050)
+    $home = Mail::SpamAssassin::Util::common_application_data_directory();
+  } else {
+    $home = (Mail::SpamAssassin::Util::portable_getpwuid ($>))[7];
+  }
+  if ($home && $home ne $helper_dir) {
+    my $dir = File::Spec->catdir($home, ".spamassassin");
+    return if $self->test_global_state_dir($dir);
+  }
+  # try LOCAL_STATE_DIR
+  return if $self->test_global_state_dir($self->{LOCAL_STATE_DIR});
+  # fallback to userstate
+  $self->{global_state_dir} = $self->get_and_create_userstate_dir();
+  dbg("config: global_state_dir set to userstate_dir: $self->{global_state_dir}");
+}
+
+sub test_global_state_dir {
+    my ($self, $dir) = @_;
+    eval { mkpath($dir, 0, 0700); }; # just a single stat if exists already
+    # Purge stale test files (enough to do only some times randomly)
+    if (rand() < 0.2 && opendir(WT_DIR, $dir)) {
+      foreach (grep {index($_, '.sawritetest') == 0 &&
+                 (-M File::Spec->catfile($dir, $_)||0) > 0.0001} readdir(WT_DIR)) {
+        unlink(Mail::SpamAssassin::Util::untaint_file_path(File::Spec->catfile($dir, $_)));
+      }
+      closedir WT_DIR;
+    }
+    my $n = ".sawritetest$$".Mail::SpamAssassin::Util::pseudo_random_string(6);
+    my $file = File::Spec->catfile($dir, $n);
+    if (Mail::SpamAssassin::Util::touch_file($file, { create_exclusive => 1 })) {
+      dbg("config: global_state_dir set to $dir");
+      $self->{global_state_dir} = $dir;
+      unlink($file);
+      return 1;
+    }
+    unlink($file); # just in case?
+    return 0;
+}
+
 =item $fullpath = $f->find_rule_support_file ($filename)
 
 Find a rule-support file, such as C<languages> or C<triplets.txt>,
@@ -1923,8 +2030,22 @@ it exists, or undef if it doesn't exist.
 sub find_rule_support_file {
   my ($self, $filename) = @_;
 
+  my @paths;
+  # search custom directories first
+  if ($self->{site_rules_filename}) {
+    foreach my $path (split("\000", $self->{site_rules_filename})) {
+      push @paths, $path  if -d $path;
+    }
+  }
+  if ($self->{rules_filename} && -d $self->{rules_filename}) {
+    push @paths, $self->{rules_filename}
+  }
+  # updates sub-directory missing from @default_rules_path
+  push @paths, '__local_state_dir__/__version__/updates_spamassassin_org';
+  push @paths, @default_rules_path;
+
   return $self->first_existing_path(
-    map { my $p = $_; $p =~ s{$}{/$filename}; $p } @default_rules_path );
+    map { my $p = $_; $p =~ s{$}{/$filename}; $p } @paths );
 }
 
 =item $f->create_default_prefs ($filename, $username [ , $userdir ] )
@@ -2006,15 +2127,15 @@ sub expand_name {
   if (am_running_on_windows()) {
     my $userprofile = $ENV{USERPROFILE} || '';
 
-    return $userprofile if ($userprofile && $userprofile =~ m/^[a-z]\:[\/\\]/oi);
-    return $userprofile if ($userprofile =~ m/^\\\\/o);
+    return $userprofile if ($userprofile && $userprofile =~ m/^[a-z]\:[\/\\]/i);
+    return $userprofile if ($userprofile =~ m/^\\\\/);
 
-    return $home if ($home && $home =~ m/^[a-z]\:[\/\\]/oi);
-    return $home if ($home =~ m/^\\\\/o);
+    return $home if ($home && $home =~ m/^[a-z]\:[\/\\]/i);
+    return $home if ($home =~ m/^\\\\/);
 
     return '';
   } else {
-    return $home if ($home && $home =~ /\//o);
+    return $home if ($home && index($home, '/') != -1);
     return (getpwnam($name))[7] if ($name ne '');
     return (getpwuid($>))[7];
   }
@@ -2028,6 +2149,9 @@ sub sed_path {
     return $self->{conf}->{sed_path_cache}->{$path};
   }
 
+  # <4.0 compatibility check, to be removed in 4.1
+  my $check_compat = $path eq '__userstate__/auto-welcomelist';
+
   my $orig_path = $path;
 
   $path =~ s/__local_rules_dir__/$self->{LOCAL_RULES_DIR} || ''/ges;
@@ -2035,10 +2159,21 @@ sub sed_path {
   $path =~ s/__def_rules_dir__/$self->{DEF_RULES_DIR} || ''/ges;
   $path =~ s{__prefix__}{$self->{PREFIX} || $Config{prefix} || '/usr'}ges;
   $path =~ s{__userstate__}{$self->get_and_create_userstate_dir() || ''}ges;
+  $path =~ s/__global_state_dir__/$self->{global_state_dir} || ''/ges;
   $path =~ s{__perl_major_ver__}{$self->get_perl_major_version()}ges;
   $path =~ s/__version__/${VERSION}/gs;
   $path =~ s/^\~([^\/]*)/$self->expand_name($1)/es;
 
+  # <4.0 compatibility check, to be removed in 4.1
+  if ($check_compat) {
+    if ($path =~ m{^(.+)/(.+)$}) {
+      # Use auto-whitelist if found
+      if (!-e $path && -e "$1/auto-whitelist") {
+        $path = "$1/auto-whitelist";
+      }
+    }
+  }
+
   $path = Mail::SpamAssassin::Util::untaint_file_path ($path);
   $self->{conf}->{sed_path_cache}->{$orig_path} = $path;
   return $path;
@@ -2077,16 +2212,32 @@ sub get_pre_files_in_dir {
   return $self->_get_cf_pre_files_in_dir($dir, 'pre');
 }
 
+sub _reorder_dir {
+  # Official ASF channel should be loaded first in
+  # order to be able to override scores by using custom channels
+  # bz 7991
+  if($a eq 'updates_spamassassin_org.cf') {
+    return -1;
+  } elsif ($b eq 'updates_spamassassin_org.cf') {
+    return 1;
+  }
+  return $a cmp $b;
+}
+
 sub _get_cf_pre_files_in_dir {
   my ($self, $dir, $type) = @_;
 
   if ($self->{config_tree_recurse}) {
     my @cfs;
-
+    # copied from Mail::SpamAssassin::Util::untaint_file_path
+    # fix bugs 8010 and 8025 by using an untaint pattern that is better on Windows than File::Find's default
+    my $chars = '-_A-Za-z0-9.#%=+,/:()\\@\\xA0-\\xFF\\\\';
+    my $re = qr{^\s*([$chars][${chars}~ ]*)\z};
     # use "eval" to avoid loading File::Find unless this is specified
     eval ' use File::Find qw();
       File::Find::find(
         { untaint => 1,
+          am_running_on_windows() ? (untaint_pattern => $re) : (),
           follow => 1,
           wanted =>
             sub { push(@cfs, $File::Find::name) if /\.\Q$type\E$/i && -f $_ }
@@ -2095,7 +2246,7 @@ sub _get_cf_pre_files_in_dir {
       my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
       die "_get_cf_pre_files_in_dir error: $eval_stat";
     };
-    @cfs = sort { $a cmp $b } @cfs;
+    @cfs = sort { _reorder_dir($a, $b) } @cfs;
     return @cfs;
   }
   else {
@@ -2104,7 +2255,7 @@ sub _get_cf_pre_files_in_dir {
                      /\.${type}$/i && -f "$dir/$_" } readdir(SA_CF_DIR);
     closedir SA_CF_DIR;
 
-    return map { "$dir/$_" } sort { $a cmp $b } @cfs;
+    return map { "$dir/$_" } sort { _reorder_dir($a, $b) } @cfs;
   }
 }
 
@@ -2127,11 +2278,18 @@ sub call_plugins {
   return unless $self->{plugins};
 
   # Use some calls ourself too
-  if ($subname eq 'finish_parsing_end') {
+  if ($subname eq 'spamd_child_init') {
+    # set global dir now if spamd
+    $self->set_global_state_dir();
+  } elsif ($subname eq 'finish_parsing_end') {
     # Initialize RegistryBoundaries, now that util_rb_tld etc from config is
     # read.  Plugins can also now use {valid_tlds_re} to one time compile
     # regexes in finish_parsing_end.
     $self->{registryboundaries} = Mail::SpamAssassin::RegistryBoundaries->new ($self);
+  } elsif ($subname eq 'whitelist_address' || $subname eq 'blacklist_address') {
+    # Warn about backwards compatibility, removed in 4.1
+    # Third party usage should be rare event, so do not translate function names
+    warn "config: Deprecated $subname called from call_plugins, use welcomelist_address or blocklist_address\n";
   }
 
   # safety net in case some plugin changes global settings, Bug 6218
@@ -2152,8 +2310,12 @@ sub find_all_addrs_in_mail {
                                Errors-To Mail-Followup-To))
   {
     my @hdrs = $mail_obj->get_header($header);
-    if ($#hdrs < 0) { next; }
-    push (@addrlist, $self->find_all_addrs_in_line(join (" ", @hdrs)));
+    foreach my $hdr (@hdrs) {
+      my @addrs = Mail::SpamAssassin::Util::parse_header_addresses($hdr);
+      foreach my $addr (@addrs) {
+        push @addrlist, $addr->{address} if defined $addr->{address};
+      }
+    }
   }
 
   # find addrs in body, too
@@ -2176,17 +2338,22 @@ sub find_all_addrs_in_mail {
 sub find_all_addrs_in_line {
   my ($self, $line) = @_;
 
+  return () unless defined $line;
+
   # a more permissive pattern based on "dot-atom" as per RFC2822
-  my $ID_PATTERN   = '[-a-z0-9_\+\:\=\!\#\$\%\&\*\^\?\{\}\|\~\/\.]+';
-  my $HOST_PATTERN = '[-a-z0-9_\+\:\/]+';
+  my $ID_PATTERN   = qr/[-a-zA-Z0-9_\+\:\=\!\#\$\%\&\*\^\?\{\}\|\~\/\.]+/;
+  my $HOST_PATTERN = qr/[-a-zA-Z0-9_\+\:\/]+/;
 
   my @addrs;
   my %seen;
   while ($line =~ s/(?:mailto:)?\s*
              ($ID_PATTERN \@
-             $HOST_PATTERN(?:\.$HOST_PATTERN)+)//oix) 
+             ($HOST_PATTERN(?:\.$HOST_PATTERN)+))//oix) 
   {
     my $addr = $1;
+    my $host = $2;
+    next unless Mail::SpamAssassin::Util::is_fqdn_valid($host);
+    next unless $self->{registryboundaries}->is_domain_valid($host);
     $addr =~ s/^mailto://;
     next if (defined ($seen{$addr})); $seen{$addr} = 1;
     push (@addrs, $addr);
index 5c4314e1b23b55fa928fbc0a78727b6ae501ad0d..f10faa7137ff218ebb0667367076485edbeb0213 100644 (file)
@@ -25,13 +25,13 @@ use warnings;
 use re 'taint';
 
 use Errno qw(ENOENT EACCES EBADF);
-use Mail::SpamAssassin::Util;
+use Mail::SpamAssassin::Util qw(compile_regexp);
 use Mail::SpamAssassin::Constants qw(:sa);
 use Mail::SpamAssassin::Logger;
 use Mail::SpamAssassin::AICache;
 
-# 256 KiB is a big email, unless stated otherwise
-use constant BIG_BYTES => 256*1024;
+# 500 KiB is a big email, unless stated otherwise
+use constant BIG_BYTES => 500*1024;
 
 our ( $MESSAGES, $AICache, %class_opts );
 
@@ -43,9 +43,9 @@ Mail::SpamAssassin::ArchiveIterator - find and process messages one at a time
 
 =head1 SYNOPSIS
 
-  my $iter = new Mail::SpamAssassin::ArchiveIterator(
+  my $iter = Mail::SpamAssassin::ArchiveIterator->new(
     { 
-      'opt_max_size' => 256 * 1024,  # 0 implies no limit
+      'opt_max_size' => 500 * 1024,  # 0 implies no limit
       'opt_cache' => 1,
     }
   );
@@ -77,7 +77,7 @@ and C<result_sub> functions appropriately per message.
 
 ###########################################################################
 
-=item $item = new Mail::SpamAssassin::ArchiveIterator( [ { opt => val, ... } ] )
+=item $item = Mail::SpamAssassin::ArchiveIterator->new( [ { opt => val, ... } ] )
 
 Constructs a new C<Mail::SpamAssassin::ArchiveIterator> object.  You may
 pass the following attribute-value pairs to the constructor.  The pairs are
@@ -91,7 +91,7 @@ A value of option I<opt_max_size> determines a limit (number of bytes)
 beyond which a message is considered large and is skipped by ArchiveIterator.
 
 A value 0 implies no size limit, all messages are examined. An undefined
-value implies a default limit of 256 KiB.
+value implies a default limit of 500 KiB.
 
 =item opt_all
 
@@ -205,8 +205,6 @@ sub new {
   $self->{s} = [ ];            # spam, of course
   $self->{h} = [ ];            # ham, as if you couldn't guess
 
-  $self->{access_problem} = 0;
-
   if ($self->{opt_all}) {
     $self->{opt_max_size} = 0;
   } elsif (!defined $self->{opt_max_size}) {
@@ -235,10 +233,14 @@ sub set_functions {
 
 =item run ( @target_paths )
 
-Generates the list of messages to process, then runs each message through the
-configured wanted subroutine.  Files which have a name ending in C<.gz> or
-C<.bz2> will be properly uncompressed via call to C<gzip -dc> and C<bzip2 -dc>
-respectively.
+Generates the list of messages to process, then runs each message through
+the configured wanted subroutine.
+
+Compressed files are detected and uncompressed automatically regardless of
+file extension.  Supported formats are C<gzip>, C<bzip2>, C<xz>, C<lz4>,
+C<lzip>, C<lzo>.  Gzip is uncompressed via IO::Zlib module, others use their
+specific command line tool (bzip2/xz/lz4/lzip/lzop).  Compressed
+mailbox/mbox files are not supported.
 
 The target_paths array is expected to be either one element per path in the
 following format: C<class:format:raw_location>, or a hash reference containing
@@ -292,6 +294,11 @@ sub run {
     return 0;
   }
 
+  # Find some uncompressors (gzip is handled with IO::Zlib)
+  foreach ('bzip2','xz','lz4','lzip','lzop') {
+    $self->{$_.'_path'} = Mail::SpamAssassin::Util::find_executable_in_env_path($_);
+  }
+
   # scan the targets and get the number and list of messages
   $self->_scan_targets(\@targets,
     sub {
@@ -316,11 +323,16 @@ sub run {
 sub _run {
   my ($self, $messages) = @_;
 
+  my $messages_run = 0;
   while (my $message = shift @{$messages}) {
     my($class, undef, $date, undef, $result) = $self->_run_message($message);
-    &{$self->{result_sub}}($class, $result, $date) if $result;
+    if ($result) {
+      $messages_run++;
+      &{$self->{result_sub}}($class, $result, $date);
+    }
   }
-  return ! $self->{access_problem};
+  # Return success if atleast some files were processed through
+  return $messages_run > 0;
 }
 
 ############################################################################
@@ -346,24 +358,8 @@ sub _run_message {
 sub _run_file {
   my ($self, $class, $format, $where, $date) = @_;
 
-  if (!_mail_open($where)) {
-    $self->{access_problem} = 1;
-    return;
-  }
-
-  my $stat_errn = stat(INPUT) ? 0 : 0+$!;
-  if ($stat_errn == ENOENT) {
-    dbg("archive-iterator: no such input ($where)");
-    return;
-  }
-  elsif ($stat_errn != 0) {
-    warn "archive-iterator: no access to input ($where): $!";
-    return;
-  }
-  elsif (!-f _ && !-c _ && !-p _) {
-    warn "archive-iterator: not a plain file (or char.spec. or pipe) ($where)";
-    return;
-  }
+  my $fh = $self->_mail_open($where, 1);
+  return unless $fh;
 
   my $opt_max_size = $self->{opt_max_size};
   if (!$opt_max_size) {
@@ -375,7 +371,7 @@ sub _run_file {
     # note that -s can only deal with files, it returns 0 on char.spec. STDIN
     info("archive-iterator: skipping large message: ".
          "file size %d, limit %d bytes", -s _, $opt_max_size);
-    close INPUT  or die "error closing input file: $!";
+    close $fh  or die "error closing input file: $!";
     return;
   }
 
@@ -384,12 +380,12 @@ sub _run_file {
   my $len = 0;
   my $str = '';
   my($inbuf,$nread);
-  while ( $nread=read(INPUT,$inbuf,16384) ) {
+  while ( $nread=read($fh,$inbuf,16384) ) {
     $len += $nread;
     if ($opt_max_size && $len > $opt_max_size) {
       info("archive-iterator: skipping large message: read %d, limit %d bytes",
            $len, $opt_max_size);
-      close INPUT  or die "error closing input file: $!";
+      close $fh  or die "error closing input file: $!";
       return;
     }
     $str .= $inbuf;
@@ -400,7 +396,7 @@ sub _run_file {
   for my $j (0..$#msg) {
     if ($msg[$j] =~ /^\015?$/) { $header = $j; last }
   }
-  close INPUT  or die "error closing input file: $!";
+  close $fh  or die "error closing input file: $!";
 
   if ($date == AI_TIME_UNKNOWN && $self->{determine_receive_date}) {
     $date = Mail::SpamAssassin::Util::receive_date(join('', splice(@msg, 0, $header)));
@@ -418,21 +414,20 @@ sub _run_mailbox {
   }
   my @msg;
   my $header;
-  if (!_mail_open($file)) {
-    $self->{access_problem} = 1;
-    return;
-  }
+
+  my $fh = $self->_mail_open($file, 1);
+  return unless $fh;
 
   my $opt_max_size = $self->{opt_max_size};
   dbg("archive-iterator: _run_mailbox %s, ofs %d, limit %d",
       $file, $offset, $opt_max_size||0);
 
-  seek(INPUT,$offset,0)  or die "cannot reposition file to $offset: $!";
+  seek($fh,$offset,0)  or die "cannot reposition file to $offset: $!";
 
   my $size = 0;
-  for ($!=0; <INPUT>; $!=0) {
+  for ($!=0; <$fh>; $!=0) {
     #Changed Regex to use option Per bug 6703
-    last if (substr($_,0,5) eq "From " && @msg && /$self->{opt_from_regex}/o);
+    last if (/^From / && @msg && $_ =~ $self->{opt_from_regex});
     $size += length($_);
     push (@msg, $_);
 
@@ -441,7 +436,7 @@ sub _run_mailbox {
       info("archive-iterator: skipping large message: ".
            "%d lines, %d bytes, limit %d bytes",
            scalar @msg, $size, $opt_max_size);
-      close INPUT  or die "error closing input file: $!";
+      close $fh  or die "error closing input file: $!";
       return;
     }
 
@@ -452,7 +447,7 @@ sub _run_mailbox {
   defined $_ || $!==0  or
     $!==EBADF ? dbg("archive-iterator: error reading: $!")
               : die "error reading: $!";
-  close INPUT  or die "error closing input file: $!";
+  close $fh  or die "error closing input file: $!";
 
   if ($date == AI_TIME_UNKNOWN && $self->{determine_receive_date}) {
     $date = Mail::SpamAssassin::Util::receive_date(join('', splice(@msg, 0, $header)));
@@ -464,23 +459,24 @@ sub _run_mailbox {
 sub _run_mbx {
   my ($self, $class, $format, $where, $date) = @_;
 
-  my ($file, $offset) = ($where =~ m/(.*)\.(\d+)$/);
+  my ($file, $offset);
+  { local($1,$2);  # Bug 7140 (avoids perl bug [perl #123880])
+    ($file, $offset) = ($where =~ m/(.*)\.(\d+)$/);
+  }
   my @msg;
   my $header;
 
-  if (!_mail_open($file)) {
-    $self->{access_problem} = 1;
-    return;
-  }
+  my $fh = $self->_mail_open($file, 1);
+  return unless $fh;
 
   my $opt_max_size = $self->{opt_max_size};
   dbg("archive-iterator: _run_mbx %s, ofs %d, limit %d",
       $file, $offset, $opt_max_size||0);
 
-  seek(INPUT,$offset,0)  or die "cannot reposition file to $offset: $!";
+  seek($fh,$offset,0)  or die "cannot reposition file to $offset: $!";
     
   my $size = 0;
-  for ($!=0; <INPUT>; $!=0) {
+  for ($!=0; <$fh>; $!=0) {
     last if ($_ =~ MBX_SEPARATOR);
     $size += length($_);
     push (@msg, $_);
@@ -490,7 +486,7 @@ sub _run_mbx {
       info("archive-iterator: skipping large message: ".
            "%d lines, %d bytes, limit %d bytes",
            scalar @msg, $size, $opt_max_size);
-      close INPUT  or die "error closing input file: $!";
+      close $fh  or die "error closing input file: $!";
       return;
     }
 
@@ -501,7 +497,7 @@ sub _run_mbx {
   defined $_ || $!==0  or
     $!==EBADF ? dbg("archive-iterator: error reading: $!")
               : die "error reading: $!";
-  close INPUT  or die "error closing input file: $!";
+  close $fh  or die "error closing input file: $!";
 
   if ($date == AI_TIME_UNKNOWN && $self->{determine_receive_date}) {
     $date = Mail::SpamAssassin::Util::receive_date(join('', splice(@msg, 0, $header)));
@@ -579,12 +575,9 @@ sub _scan_targets {
       if ($format eq 'detect') {
        # detect the format
         my $stat_errn = stat($location) ? 0 : 0+$!;
-        if ($stat_errn == ENOENT) {
-          $thisformat = 'file';  # actually, no file - to be detected later
-        }
-        elsif ($stat_errn != 0) {
-          warn "archive-iterator: no access to $location: $!";
-          $thisformat = 'file';
+        if ($stat_errn != 0) {
+          warn "archive-iterator: no access to $location: $!\n";
+          next;
         }
         elsif (-d _) {
          # it's a directory
@@ -623,37 +616,121 @@ sub _scan_targets {
 }
 
 sub _mail_open {
-  my ($file) = @_;
+  my ($self, $file, $ignore_missing) = @_;
+  my $fh;
 
+  # Go ahead and try to open the file
   # bug 5288: the "magic" version of open will strip leading and trailing
   # whitespace from the expression.  switch to the three-argument version
   # of open which does not strip whitespace.  see "perldoc -f open" and
   # "perldoc perlipc" for more information.
-
-  # Assume that the file by default is just a plain file
-  my @expr = ( $file );
-  my $mode = '<';
-
-  # Handle different types of compressed files
-  if ($file =~ /\.gz$/) {
-    $mode = '-|';
-    unshift @expr, 'gunzip', '-cd';
-  }
-  elsif ($file =~ /\.bz2$/) {
-    $mode = '-|';
-    unshift @expr, 'bzip2', '-cd';
-  }
-
-  # Go ahead and try to open the file
-  if (!open (INPUT, $mode, @expr)) {
-    warn "archive-iterator: unable to open $file: $!\n";
-    return 0;
+  if (!open($fh, '<', $file)) {
+    # Don't warn about disappeared files
+    if ($ignore_missing && $! == ENOENT) {
+      dbg("archive-iterator: no access to $file: $!");
+    } else {
+      warn "archive-iterator: no access to $file: $!\n"
+    }
+    return;
   }
 
   # bug 5249: mail could have 8-bit data, need this on some platforms
-  binmode INPUT  or die "cannot set input file to binmode: $!";
+  binmode $fh  or die "cannot set input file to binmode: $!";
+
+  # Detect compressed data (only from files, can't reopen pipe)
+  if (-f $file && read($fh, my $magic, 6)) {
+    # GZIP
+    if ($magic =~ /^\x1F\x8B/) {
+      dbg("archive-iterator: detected gzip file $file, reopening with IO::Zlib");
+      close $fh  or die "error closing input file: $!";
+      eval { require IO::Zlib; };
+      if ($@) { warn "archive-iterator: IO::Zlib required for $file: $@\n"; return; }
+      $fh = IO::Zlib->new($file, "rb");
+      if (!$fh) {
+        if ($ignore_missing && $! == ENOENT) {
+          dbg("archive-iterator: no access to $file: $!");
+        } else {
+          warn "archive-iterator: no access to $file: $!\n";
+        }
+        return;
+      }
+    }
+    # BZIP2
+    elsif ($magic =~ /^\x42\x5A(?:\x68|\x30)/) {
+      dbg("archive-iterator: detected bzip2 file $file, reopening with bzip2");
+      close $fh  or die "error closing input file: $!";
+      if (!$self->{bzip2_path}) {
+        warn "archive-iterator: bzip2 executable required for $file\n";
+        return;
+      }
+      if (!open($fh, '-|', $self->{bzip2_path}, '-cd', $file)) {
+        warn "archive-iterator: no access to $file: $!\n";
+        return;
+      }
+      binmode $fh  or die "cannot set input file to binmode: $!";
+    }
+    # XZ
+    elsif ($magic =~ /^\xFD\x37\x7A\x58\x5A\x00/) {
+      dbg("archive-iterator: detected xz file $file, reopening with xz");
+      close $fh  or die "error closing input file: $!";
+      if (!$self->{xz_path}) {
+        warn "archive-iterator: xz executable required for $file\n";
+        return;
+      }
+      if (!open($fh, '-|', $self->{xz_path}, '-cd', $file)) {
+        warn "archive-iterator: no access to $file: $!\n";
+        return;
+      }
+      binmode $fh  or die "cannot set input file to binmode: $!";
+    }
+    # LZ4
+    elsif ($magic =~ /^\x04\x22\x4D\x18/) {
+      dbg("archive-iterator: detected lz4 file $file, reopening with lz4");
+      close $fh  or die "error closing input file: $!";
+      if (!$self->{lz4_path}) {
+        warn "archive-iterator: lz4 executable required for $file\n";
+        return;
+      }
+      if (!open($fh, '-|', $self->{lz4_path}, '-cd', $file)) {
+        warn "archive-iterator: no access to $file: $!\n";
+        return;
+      }
+      binmode $fh  or die "cannot set input file to binmode: $!";
+    }
+    # LZIP
+    elsif ($magic =~ /^\x4C\x5A\x49\x50/) {
+      dbg("archive-iterator: detected lzip file $file, reopening with lzip");
+      close $fh  or die "error closing input file: $!";
+      if (!$self->{lzip_path}) {
+        warn "archive-iterator: lzip executable required for $file\n";
+        return;
+      }
+      if (!open($fh, '-|', $self->{lzip_path}, '-cd', $file)) {
+        warn "archive-iterator: no access to $file: $!\n";
+        return;
+      }
+      binmode $fh  or die "cannot set input file to binmode: $!";
+    }
+    # LZO
+    elsif ($magic =~ /^\x89\x4C\x5A\x4F\x00\x0D/) {
+      dbg("archive-iterator: detected lzo file $file, reopening with lzop");
+      close $fh  or die "error closing input file: $!";
+      if (!$self->{lzop_path}) {
+        warn "archive-iterator: lzop executable required for $file\n";
+        return;
+      }
+      if (!open($fh, '-|', $self->{lzop_path}, '-cd', $file)) {
+        warn "archive-iterator: no access to $file: $!\n";
+        return;
+      }
+      binmode $fh  or die "cannot set input file to binmode: $!";
+    } else {
+      # Reset position
+      seek($fh,0,0);
+    }
+  }
 
-  return 1;
+  return $fh;
 }
 
 sub _set_default_message_selection_opts {
@@ -663,11 +740,15 @@ sub _set_default_message_selection_opts {
   $self->{opt_want_date} = 1 unless (defined $self->{opt_want_date});
   $self->{opt_cache} = 0 unless (defined $self->{opt_cache});
   #Changed Regex to include boundaries for Communigate Pro versions (5.2.x and later). per Bug 6413
-  $self->{opt_from_regex} = '^From \S+  ?(\S\S\S \S\S\S .?\d .?\d:\d\d:\d\d \d{4}|.?\d-\d\d-\d{4}_\d\d:\d\d:\d\d_)' unless (defined $self->{opt_from_regex});
-
-  #STRIP LEADING AND TRAILING / FROM REGEX FOR OPTION
-  $self->{opt_from_regex} =~ s/^\///;
-  $self->{opt_from_regex} =~ s/\/$//;
+  if (!defined $self->{opt_from_regex}) {
+    $self->{opt_from_regex} = qr/^From \S+  ?(\S\S\S \S\S\S .?\d .?\d:\d\d:\d\d \d{4}|.?\d-\d\d-\d{4}_\d\d:\d\d:\d\d_)/;
+  } elsif (ref($self->{opt_from_regex}) ne 'Regexp') {
+    my ($rec, $err) = compile_regexp($self->{opt_from_regex}, 1);
+    if (!$rec) {
+      die "fatal: invalid mbox_format_from_regex '$self->{opt_from_regex}': $err\n";
+    }
+    $self->{opt_from_regex} = $rec;
+  }
 
   dbg("archive-iterator: _set_default_message_selection_opts After: Scanprob[$self->{opt_scanprob}], want_date[$self->{opt_want_date}], cache[$self->{opt_cache}], from_regex[$self->{opt_from_regex}]");
 
@@ -750,7 +831,7 @@ sub _scan_directory {
     # Maildir format: bug 3003
     for my $sub ("new", "cur") {
       opendir (DIR, "$folder/$sub")
-            or die "Can't open '$folder/$sub' dir: $!\n";
+            or die "archive-iterator: can't open '$folder/$sub' dir: $!\n";
       # Don't learn from messages marked as deleted
       # Or files starting with a leading dot
       push @files, map { "$sub/$_" } grep { !/^\.|:2,.*T/ } readdir(DIR);
@@ -790,9 +871,10 @@ sub _scan_directory {
     my $stat_errn = stat($file) ? 0 : 0+$!;
     if ($stat_errn == ENOENT) {
       # no longer there?
+      dbg("archive-iterator: no access to $file: $!");
     }
     elsif ($stat_errn != 0) {
-      warn "archive-iterator: no access to $file: $!";
+      warn "archive-iterator: no access to $file: $!\n";
     }
     elsif (-f _ || -c _ || -p _) {
       $self->_scan_file($class, $file, $bkfunc);
@@ -801,7 +883,7 @@ sub _scan_directory {
       push(@subdirs, $file);
     }
     else {
-      warn "archive-iterator: $file is not a plain file or directory: $!";
+      warn "archive-iterator: $file is not a plain file or directory\n";
     }
   }
   undef @files;  # release storage
@@ -844,18 +926,17 @@ sub _scan_file {
       }
 
       my $header = '';
-      if (!_mail_open($mail)) {
-        $self->{access_problem} = 1;
-        return;
-      }
-      for ($!=0; <INPUT>; $!=0) {
+      my $fh = $self->_mail_open($mail);
+      return unless $fh;
+
+      for ($!=0; <$fh>; $!=0) {
         last if /^\015?$/s;
         $header .= $_;
       }
       defined $_ || $!==0  or
         $!==EBADF ? dbg("archive-iterator: error reading: $!")
                   : die "error reading: $!";
-      close INPUT  or die "error closing input file: $!";
+      close $fh  or die "error closing input file: $!";
 
       return if ($self->{opt_skip_empty_messages} && $header eq '');
 
@@ -896,7 +977,6 @@ sub _scan_mailbox {
     $folder =~ s/\/\s*$//; #Remove trailing slash, if there
     if (!opendir(DIR, $folder)) {
       warn "archive-iterator: can't open '$folder' dir: $!\n";
-      $self->{access_problem} = 1;
       return;
     }
     while ($_ = readdir(DIR)) {
@@ -921,9 +1001,8 @@ sub _scan_mailbox {
 
   foreach my $file (@files) {
     $self->_bump_scan_progress();
-    if ($file =~ /\.(?:gz|bz2)$/) {
+    if ($file =~ /\.(?:gz|bz2|xz|lz[o4]?)$/i) {
       warn "archive-iterator: compressed mbox folders are not supported at this time\n";
-      $self->{access_problem} = 1;
       next;
     }
 
@@ -943,20 +1022,18 @@ sub _scan_mailbox {
     }
 
     unless ($count) {
-      if (!_mail_open($file)) {
-        $self->{access_problem} = 1;
-       next;
-      }
+      my $fh = $self->_mail_open($file);
+      next unless $fh;
 
       my $start = 0;           # start of a message
       my $where = 0;           # current byte offset
       my $first = '';          # first line of message
       my $header = '';         # header text
       my $in_header = 0;       # are in we a header?
-      while (!eof INPUT) {
+      while (!eof $fh) {
         my $offset = $start;   # byte offset of this message
         my $header = $first;   # remember first line
-        for ($!=0; <INPUT>; $!=0) {
+        for ($!=0; <$fh>; $!=0) {
          if ($in_header) {
             if (/^\015?$/s) {
              $in_header = 0;
@@ -966,15 +1043,15 @@ sub _scan_mailbox {
            }
          }
           #Changed Regex to use option Per bug 6703
-         if (substr($_,0,5) eq "From " && /$self->{opt_from_regex}/o) {
+         if (/^From / && $_ =~ $self->{opt_from_regex}) {
            $in_header = 1;
            $first = $_;
            $start = $where;
-           $where = tell INPUT;
+           $where = tell $fh;
             $where >= 0  or die "cannot obtain file position: $!";
            last;
          }
-         $where = tell INPUT;
+         $where = tell $fh;
           $where >= 0  or die "cannot obtain file position: $!";
         }
         defined $_ || $!==0  or
@@ -986,7 +1063,7 @@ sub _scan_mailbox {
          $info->{$offset} = Mail::SpamAssassin::Util::receive_date($header);
        }
       }
-      close INPUT  or die "error closing input file: $!";
+      close $fh  or die "error closing input file: $!";
     }
 
     while(my($k,$v) = each %{$info}) {
@@ -1027,7 +1104,6 @@ sub _scan_mbx {
     $folder =~ s/\/\s*$//; # remove trailing slash, if there is one
     if (!opendir(DIR, $folder)) {
       warn "archive-iterator: can't open '$folder' dir: $!\n";
-      $self->{access_problem} = 1;
       return;
     }
     while ($_ = readdir(DIR)) {
@@ -1053,9 +1129,8 @@ sub _scan_mbx {
   foreach my $file (@files) {
     $self->_bump_scan_progress();
 
-    if ($folder =~ /\.(?:gz|bz2)$/) {
+    if ($folder =~ /\.(?:gz|bz2|xz|lz[o4]?)$/i) {
       warn "archive-iterator: compressed mbx folders are not supported at this time\n";
-      $self->{access_problem} = 1;
       next;
     }
 
@@ -1075,13 +1150,11 @@ sub _scan_mbx {
     }
 
     unless ($count) {
-      if (!_mail_open($file)) {
-       $self->{access_problem} = 1;
-        next;
-      }
+      my $fh = $self->_mail_open($file);
+      next unless $fh;
 
       # check the mailbox is in mbx format
-      $! = 0; $fp = <INPUT>;
+      $! = 0; $fp = <$fh>;
       defined $fp || $!==0  or
         $!==EBADF ? dbg("archive-iterator: error reading: $!")
                   : die "error reading: $!";
@@ -1092,18 +1165,17 @@ sub _scan_mbx {
       }
 
       # skip mbx headers to the first email...
-      seek(INPUT,2048,0)  or die "cannot reposition file to 2048: $!";
-      my $sep = MBX_SEPARATOR;
+      seek($fh,2048,0)  or die "cannot reposition file to 2048: $!";
 
-      for ($!=0; <INPUT>; $!=0) {
-        if ($_ =~ /$sep/) {
-         my $offset = tell INPUT;
+      for ($!=0; <$fh>; $!=0) {
+        if ($_ =~ MBX_SEPARATOR) {
+         my $offset = tell $fh;
           $offset >= 0  or die "cannot obtain file position: $!";
          my $size = $2;
 
          # gather up the headers...
          my $header = '';
-          for ($!=0; <INPUT>; $!=0) {
+          for ($!=0; <$fh>; $!=0) {
             last if (/^\015?$/s);
            $header .= $_;
          }
@@ -1116,7 +1188,7 @@ sub _scan_mbx {
           }
 
          # go onto the next message
-         seek(INPUT, $offset + $size, 0)
+         seek($fh, $offset + $size, 0)
             or die "cannot reposition file to $offset + $size: $!";
        }
         else {
@@ -1126,7 +1198,7 @@ sub _scan_mbx {
       defined $_ || $!==0  or
         $!==EBADF ? dbg("archive-iterator: error reading: $!")
                   : die "error reading: $!";
-      close INPUT  or die "error closing input file: $!";
+      close $fh  or die "error closing input file: $!";
     }
 
     while(my($k,$v) = each %{$info}) {
index 0f7e067c6efcec9cdc156a1411eb09f9e27eb2b9..8cf4b34e3d28d6cbf840e90bfbd11e324da71f97 100644 (file)
@@ -42,6 +42,7 @@ use Time::HiRes qw(time);
 
 use Mail::SpamAssassin;
 use Mail::SpamAssassin::Logger;
+use Mail::SpamAssassin::Util qw(idn_to_ascii domain_to_search_list);
 
 our @ISA = qw();
 
@@ -71,9 +72,9 @@ sub new {
     main                => $main,
     queries_started     => 0,
     queries_completed   => 0,
-    total_queries_started   => 0,
-    total_queries_completed => 0,
     pending_lookups     => { },
+    pending_rules      => { },  # maintain pending rules list for meta evaluation
+    rules_for_key      => { },  # record all rules used by a key for logging
     timing_by_query     => { },
     all_lookups         => { },  # keyed by "rr_type/domain"
   };
@@ -82,66 +83,54 @@ sub new {
   $self;
 }
 
-# Given a domain name, produces a listref of successively stripped down
-# parent domains, e.g. a domain '2.10.Example.COM' would produce a list:
-# '2.10.example.com', '10.example.com', 'example.com', 'com', ''
-#
-sub domain_to_search_list {
-  my ($domain) = @_;
-  $domain =~ s/^\.+//; $domain =~ s/\.+\z//;  # strip leading and trailing dots
-  my @search_keys;
-  if ($domain =~ /\[/) {  # don't split address literals
-    @search_keys = ( $domain, '' );  # presumably an address literal
-  } else {
-    local $1;
-    $domain = lc $domain;
-    for (;;) {
-      push(@search_keys, $domain);
-      last  if $domain eq '';
-      # strip one level
-      $domain = ($domain =~ /^ (?: [^.]* ) \. (.*) \z/xs) ? $1 : '';
-    }
-    if (@search_keys > 20) {  # enforce some sanity limit
-      @search_keys = @search_keys[$#search_keys-19 .. $#search_keys];
-    }
-  }
-  return \@search_keys;
-}
-
 # ---------------------------------------------------------------------------
 
-=item $ent = $async->start_lookup($ent, $master_deadline)
+=item $ent = $async->bgsend_and_start_lookup($name, $type, $class, $ent, $cb, %options)
+
+Launch async DNS lookups.  This is the only official method supported for
+plugins since version 4.0.0.  Do not use bgsend and start_lookup separately.
 
-Register the start of a long-running asynchronous lookup operation.
-C<$ent> is a hash reference containing the following items:
+Merges duplicate queries automatically, only launches one and calls all
+related callbacks on answer.
 
 =over 4
 
-=item key (required)
+=item $name (required)
+
+Name to query.
 
-A key string, unique to this lookup.  This is what is reported in
-debug messages, used as the key for C<get_lookup()>, etc.
+=item $type (required)
 
-=item id (required)
+Type to query, A, TXT, NS, etc.
 
-An ID string, also unique to this lookup.  Typically, this is the DNS packet ID
-as returned by DnsResolver's C<bgsend> method.  Sadly, the Net::DNS
-architecture forces us to keep a separate ID string for this task instead of
-reusing C<key> -- if you are not using DNS lookups through DnsResolver, it
-should be OK to just reuse C<key>.
+=item $class (required/deprecated)
 
-=item type (required)
+Deprecated, ignored, set as undef.
+
+=item C<$ent> is a required hash reference containing the following items:
+
+=over 4
+
+=item $ent->{rulename} (required)
+
+The rulename that started and/or depends on this query.  Required for rule
+dependencies to work correctly.  Can be a single rulename, or array of
+multiple rulenames.
+
+=item $ent->{type} (optional)
 
 A string, typically one word, used to describe the type of lookup in log
-messages, such as C<DNSBL>, C<MX>, C<TXT>.
+messages, such as C<DNSBL>, C<URIBL-A>.  If not defined, default is value of
+$type.
 
-=item zone (optional)
+=item $ent->{zone} (optional)
 
-A zone specification (typically a DNS zone name - e.g. host, domain, or RBL)
-which may be used as a key to look up per-zone settings. No semantics on this
-parameter is imposed by this module. Currently used to fetch by-zone timeouts.
+A zone specification (typically a DNS zone name - e.g.  host, domain, or
+RBL) which may be used as a key to look up per-zone settings.  No semantics
+on this parameter is imposed by this module.  Currently used to fetch
+by-zone timeouts (from rbl_timeout setting).  Defaults to $name.
 
-=item timeout_initial (optional)
+=item $ent->{timeout_initial} (optional)
 
 An initial value of elapsed time for which we are willing to wait for a
 response (time in seconds, floating point value is allowed). When elapsed
@@ -158,121 +147,122 @@ variable rbl_timeout.
 If a value of the timeout_initial parameter is below timeout_min, the initial
 timeout is set to timeout_min.
 
-=item timeout_min (optional)
+=item $ent->{timeout_min} (optional)
 
 A lower bound (in seconds) to which the actual timeout approaches as the
 number of queries completed approaches the number of all queries started.
 Defaults to 0.2 * timeout_initial.
 
-=back
+=item $ent->{key}, $ent->{id} (deprecated)
 
-C<$ent> is returned by this method, with its contents augmented by additional
-information.
+Deprecated, ignored, automatically generated since 4.0.0.
 
-=cut
+=item $ent->{YOUR_OWN_ITEM}
 
-sub start_lookup {
-  my ($self, $ent, $master_deadline) = @_;
+Any other custom values/objects that you want to pass on to the answer
+callback.
 
-  my $id  = $ent->{id};
-  my $key = $ent->{key};
-  defined $id && $id ne ''  or die "oops, no id";
-  $key                      or die "oops, no key";
-  $ent->{type}              or die "oops, no type";
+=back
 
-  my $now = time;
-  $ent->{start_time} = $now  if !defined $ent->{start_time};
+=item $cb (required)
 
-  # are there any applicable per-zone settings?
-  my $zone = $ent->{zone};
-  my $settings;  # a ref to a by-zone or to global settings
-  my $conf_by_zone = $self->{main}->{conf}->{by_zone};
-  if (defined $zone && $conf_by_zone) {
-  # dbg("async: searching for by_zone settings for $zone");
-    $zone =~ s/^\.//;  $zone =~ s/\.\z//;  # strip leading and trailing dot
-    for (;;) {  # 2.10.example.com, 10.example.com, example.com, com, ''
-      if (exists $conf_by_zone->{$zone}) {
-        $settings = $conf_by_zone->{$zone};
-        last;
-      } elsif ($zone eq '') {
-        last;
-      } else {  # strip one level, careful with address literals
-        $zone = ($zone =~ /^( (?: [^.] | \[ (?: \\. | [^\]\\] )* \] )* )
-                            \. (.*) \z/xs) ? $2 : '';
-      }
-    }
-  }
+Callback function for answer, called as $cb->($ent, $pkt).  C<$ent> is the
+same object that bgsend_and_start_lookup was called with.  C<$pkt> is the
+packet object for the response, Net::DNS:RR objects can be found from
+$pkt->answer.
 
-  dbg("async: applying by_zone settings for %s", $zone)  if $settings;
+=item %options (required)
 
-  my $t_init = $ent->{timeout_initial};  # application-specified has precedence
-  $t_init = $settings->{rbl_timeout}  if $settings && !defined $t_init;
-  $t_init = $self->{main}->{conf}->{rbl_timeout}  if !defined $t_init;
-  $t_init = 0  if !defined $t_init;      # last-resort default, just in case
+Hash of options. Only supported and required option is master_deadline:
 
-  my $t_end = $ent->{timeout_min};       # application-specified has precedence
-  $t_end = $settings->{rbl_timeout_min}  if $settings && !defined $t_end;
-  $t_end = $self->{main}->{conf}->{rbl_timeout_min}  if !defined $t_end; # added for bug 7070
-  $t_end = 0.2 * $t_init  if !defined $t_end;
-  $t_end = 0  if $t_end < 0;  # just in case
-  $t_init = $t_end  if $t_init < $t_end;
+  master_deadline => $pms->{master_deadline}
 
-  my $clipped_by_master_deadline = 0;
-  if (defined $master_deadline) {
-    my $time_avail = $master_deadline - time;
-    $time_avail = 0.5  if $time_avail < 0.5;  # give some slack
-    if ($t_init > $time_avail) {
-      $t_init = $time_avail; $clipped_by_master_deadline = 1;
-      $t_end  = $time_avail  if $t_end > $time_avail;
-    }
-  }
-  $ent->{timeout_initial} = $t_init;
-  $ent->{timeout_min} = $t_end;
+=back
 
-  $ent->{display_id} =  # identifies entry in debug logging and similar
-    join(", ", grep { defined }
-               map { ref $ent->{$_} ? @{$ent->{$_}} : $ent->{$_} }
-               qw(sets rules rulename type key) );
+=cut
 
-  $self->{pending_lookups}->{$key} = $ent;
+sub start_queue {
+  my($self) = @_;
 
-  $self->{queries_started}++;
-  $self->{total_queries_started}++;
-  dbg("async: starting: %s (timeout %.1fs, min %.1fs)%s",
-      $ent->{display_id}, $ent->{timeout_initial}, $ent->{timeout_min},
-      !$clipped_by_master_deadline ? '' : ', capped by time limit');
+  $self->{wait_queue} = 1;
+}
 
-  $ent;
+sub launch_queue {
+  my($self) = @_;
+
+  delete $self->{wait_queue};
+
+  if ($self->{bgsend_queue}) {
+    dbg("async: launching queued lookups");
+    foreach (@{$self->{bgsend_queue}}) {
+      $self->bgsend_and_start_lookup(@$_);
+    }
+    delete $self->{bgsend_queue};
+  }
 }
 
-# ---------------------------------------------------------------------------
+sub bgsend_and_start_lookup {
+  my $self = shift;
+  my($domain, $type, $class, $ent, $cb, %options) = @_;
 
-=item $ent = $async->bgsend_and_start_lookup($domain, $type, $class, $ent, $cb, %options)
+  return if $self->{main}->{resolver}->{no_resolver};
 
-A common idiom: calls C<bgsend>, followed by a call to C<start_lookup>,
-returning the argument $ent object as modified by C<start_lookup> and
-filled-in with a query ID.
+  # Waiting for priority -100 to launch?
+  if ($self->{wait_queue}) {
+    push @{$self->{bgsend_queue}}, [@_];
+    dbg("async: DNS priority not reached, queueing lookup: $domain/$type");
+    return $ent;
+  }
 
-=cut
+  if (!defined $ent->{rulename} && !$self->{rulename_warned}++) {
+    my($package, $filename, $line) = caller;
+    warn "async: bgsend_and_start_lookup called without rulename, ".
+         "from $package ($filename) line $line. You are likely using ".
+         "a plugin that is not compatible with SpamAssasin 4.0.0.";
+  }
 
-sub bgsend_and_start_lookup {
-  my($self, $domain, $type, $class, $ent, $cb, %options) = @_;
-  $ent = {}  if !$ent;
   $domain =~ s/\.+\z//s;  # strip trailing dots, these sometimes still sneak in
+  $domain = idn_to_ascii($domain);
+
+  # At this point the $domain should already be encoded to UTF-8 and
+  # IDN converted to ASCII-compatible encoding (ACE).  Make sure this is
+  # really the case in order to be able to catch any leftover omissions.
+  if (utf8::is_utf8($domain)) {
+    utf8::encode($domain);
+    my($package, $filename, $line) = caller;
+    info("bgsend_and_start_lookup: Unicode domain name, expected octets: %s, ".
+         "called from %s line %d", $domain, $package, $line);
+  } elsif ($domain =~ tr/\x00-\x7F//c) {  # is not all-ASCII
+    my($package, $filename, $line) = caller;
+    info("bgsend_and_start_lookup: non-ASCII domain name: %s, ".
+         "called from %s line %d", $domain, $package, $line);
+  }
+
+  my $dnskey = uc($type).'/'.lc($domain);
+  my $dns_query_info = $self->{all_lookups}{$dnskey};
+
+  $ent = {}  if !$ent;
   $ent->{id} = undef;
+  my $key = $ent->{key} = $dnskey;
   $ent->{query_type} = $type;
   $ent->{query_domain} = $domain;
   $ent->{type} = $type  if !exists $ent->{type};
+  $ent->{zone} = $domain  if !exists $ent->{zone};
   $cb = $ent->{completed_callback}  if !$cb;  # compatibility with SA < 3.4
 
-  my $key = $ent->{key} || '';
+  my @rulenames = grep { defined } (ref $ent->{rulename} ?
+                    @{$ent->{rulename}} : $ent->{rulename});
 
-  my $dnskey = uc($type) . '/' . lc($domain);
-  my $dns_query_info = $self->{all_lookups}{$dnskey};
+  $self->{rules_for_key}->{$key}{$_} = 1 foreach (@rulenames);
 
   if ($dns_query_info) {  # DNS query already underway or completed
+    if ($dns_query_info->{blocked}) {
+      dbg("async: blocked by %s: %s, rules: %s", $dns_query_info->{blocked},
+          $dnskey, join(", ", @rulenames));
+      return;
+    }
     my $id = $ent->{id} = $dns_query_info->{id};  # re-use existing query
-    return if !defined $id;  # presumably blocked, or other fatal failure
+    return if !defined $id;  # presumably some fatal failure
     my $id_tail = $id; $id_tail =~ s{^\d+/IN/}{};
     lc($id_tail) eq lc($dnskey)
       or info("async: unmatched id %s, key=%s", $id, $dnskey);
@@ -281,24 +271,27 @@ sub bgsend_and_start_lookup {
     if (!$pkt) {  # DNS query underway, still waiting for results
       # just add our query to the existing one
       push(@{$dns_query_info->{applicants}}, [$ent,$cb]);
-      dbg("async: query %s already underway, adding no.%d %s",
+      $self->{pending_rules}->{$_}{$key} = 1 foreach (@rulenames);
+      dbg("async: query %s already underway, adding no.%d, rules: %s",
           $id, scalar @{$dns_query_info->{applicants}},
-          $ent->{rulename} || $key);
+          join(", ", @rulenames));
 
     } else {  # DNS query already completed, re-use results
       # answer already known, just do the callback and be done with it
+      delete $self->{pending_rules}->{$_}{$key} foreach (@rulenames);
       if (!$cb) {
-        dbg("async: query %s already done, re-using for %s", $id, $key);
+        dbg("async: query %s already done, re-using for %s, rules: %s",
+            $id, $key, join(", ", @rulenames));
       } else {
-        dbg("async: query %s already done, re-using for %s, callback",
-            $id, $key);
+        dbg("async: query %s already done, re-using for %s, callback, rules: %s",
+            $id, $key, join(", ", @rulenames));
         eval {
           $cb->($ent, $pkt); 1;
         } or do {
           chomp $@;
           # resignal if alarm went off
           die "async: (1) $@\n"  if $@ =~ /__alarm__ignore__\(.*\)/s;
-          warn sprintf("query %s completed, callback %s failed: %s\n",
+          warn sprintf("async: query %s completed, callback %s failed: %s\n",
                        $id, $key, $@);
         };
       }
@@ -307,23 +300,48 @@ sub bgsend_and_start_lookup {
 
   else {  # no existing query, open a new DNS query
     $dns_query_info = $self->{all_lookups}{$dnskey} = {};  # new query needed
-    my($id, $blocked);
+    my($id, $blocked, $check_dbrdom);
+    # dns_query_restriction
+    my $blocked_by = 'dns_query_restriction';
     my $dns_query_blockages = $self->{main}->{conf}->{dns_query_blocked};
-    if ($dns_query_blockages) {
+    # dns_block_rule
+    my $dns_block_domains = $self->{main}->{conf}->{dns_block_rule_domains};
+    if ($dns_query_blockages || $dns_block_domains) {
       my $search_list = domain_to_search_list($domain);
-      foreach my $parent_domain (@$search_list) {
-        $blocked = $dns_query_blockages->{$parent_domain};
-        last if defined $blocked; # stop at first defined, can be true or false
+      foreach my $parent_domain ((@$search_list, '*')) {
+        if ($dns_query_blockages) {
+          $blocked = $dns_query_blockages->{$parent_domain};
+          last if defined $blocked; # stop at first defined, can be true or false
+        }
+        if ($parent_domain ne '*' && exists $dns_block_domains->{$parent_domain}) {
+          # save for later check.. ps. untainted already
+          $check_dbrdom = $dns_block_domains->{$parent_domain};
+        }
+      }
+    }
+    if (!$blocked && $check_dbrdom) {
+      my $blockfile =
+        $self->{main}->sed_path("__global_state_dir__/dnsblock_${check_dbrdom}");
+      if (my $mtime = (stat($blockfile))[9]) {
+        if (time - $mtime <= $self->{main}->{conf}->{dns_block_time}) {
+          $blocked = 1;
+          $blocked_by = 'dns_block_rule';
+        } else {
+          dbg("async: dns_block_rule removing expired $blockfile");
+          unlink($blockfile);
+        }
       }
     }
     if ($blocked) {
-      dbg("async: blocked by dns_query_restriction: %s", $dnskey);
+      dbg("async: blocked by %s: %s, rules: %s", $blocked_by, $dnskey,
+          join(", ", @rulenames));
+      $dns_query_info->{blocked} = $blocked_by;
     } else {
-      dbg("async: launching %s for %s", $dnskey, $key);
+      dbg("async: launching %s, rules: %s", $dnskey, join(", ", @rulenames));
       $id = $self->{main}->{resolver}->bgsend($domain, $type, $class, sub {
           my($pkt, $pkt_id, $timestamp) = @_;
           # this callback sub is called from DnsResolver::poll_responses()
-        # dbg("async: in a bgsend_and_start_lookup callback, id %s", $pkt_id);
+          # dbg("async: in a bgsend_and_start_lookup callback, id %s", $pkt_id);
           if ($pkt_id ne $id) {
             warn "async: mismatched dns id: got $pkt_id, expected $id\n";
             return;
@@ -333,10 +351,14 @@ sub bgsend_and_start_lookup {
           my $cb_count = 0;
           foreach my $tuple (@{$dns_query_info->{applicants}}) {
             my($appl_ent, $appl_cb) = @$tuple;
+            my @rulenames = grep { defined } (ref $appl_ent->{rulename} ?
+                      @{$appl_ent->{rulename}} : $appl_ent->{rulename});
+            foreach (@rulenames) {
+              delete $self->{pending_rules}->{$_}{$appl_ent->{key}};
+            }
             if ($appl_cb) {
-              dbg("async: calling callback on key %s%s", $key,
-                  !defined $appl_ent->{rulename} ? ''
-                    : ", rule ".$appl_ent->{rulename});
+              dbg("async: calling callback on key %s, rules: %s",
+                  $key, join(", ", @rulenames));
               $cb_count++;
               eval {
                 $appl_cb->($appl_ent, $pkt); 1;
@@ -344,7 +366,7 @@ sub bgsend_and_start_lookup {
                 chomp $@;
                 # resignal if alarm went off
                 die "async: (2) $@\n"  if $@ =~ /__alarm__ignore__\(.*\)/s;
-                warn sprintf("query %s completed, callback %s failed: %s\n",
+                warn sprintf("async: query %s completed, callback %s failed: %s\n",
                              $id, $appl_ent->{key}, $@);
               };
             }
@@ -356,26 +378,120 @@ sub bgsend_and_start_lookup {
     return if !defined $id;
     $dns_query_info->{id} = $ent->{id} = $id;
     push(@{$dns_query_info->{applicants}}, [$ent,$cb]);
-    $self->start_lookup($ent, $options{master_deadline});
+    $self->{pending_rules}->{$_}{$key} = 1 foreach (@rulenames);
+    $self->_start_lookup($ent, $options{master_deadline});
   }
   return $ent;
 }
 
 # ---------------------------------------------------------------------------
 
-=item $ent = $async->get_lookup($key)
+=item $ent = $async->start_lookup($ent, $master_deadline)
+
+DIRECT USE DEPRECATED since 4.0.0, please use bgsend_and_start_lookup.
+
+=cut
 
-Retrieve the pending-lookup object for the given key C<$key>.
+sub start_lookup {
+  my $self = shift;
+
+  if (!$self->{start_lookup_warned}++) {
+    my($package, $filename, $line) = caller;
+    warn "async: deprecated start_lookup called, ".
+         "from $package ($filename) line $line. You are likely using ".
+         "a plugin that is not compatible with SpamAssasin 4.0.0.";
+  }
+
+  return if $self->{main}->{resolver}->{no_resolver};
+  $self->_start_lookup(@_);
+}
+
+# Internal use not deprecated. :-)
+sub _start_lookup {
+  my ($self, $ent, $master_deadline) = @_;
+
+  my $id  = $ent->{id};
+  my $key = $ent->{key};
+  defined $id && $id ne ''  or die "oops, no id";
+  $key                      or die "oops, no key";
+  $ent->{type}              or die "oops, no type";
 
-If the lookup is complete, this will return C<undef>.
+  my $now = time;
+  $ent->{start_time} = $now  if !defined $ent->{start_time};
 
-Note that a lookup is still considered "pending" until C<complete_lookups()> is
-called, even if it has been reported as complete via C<set_response_packet()>.
+  # are there any applicable per-zone settings?
+  my $zone = $ent->{zone};
+  my $settings;  # a ref to a by-zone or to global settings
+  my $conf_by_zone = $self->{main}->{conf}->{by_zone};
+  if (defined $zone && $conf_by_zone) {
+  # dbg("async: searching for by_zone settings for $zone");
+    $zone =~ s/^\.//;  $zone =~ s/\.\z//;  # strip leading and trailing dot
+    for (;;) {  # 2.10.example.com, 10.example.com, example.com, com, ''
+      if (exists $conf_by_zone->{$zone}) {
+        $settings = $conf_by_zone->{$zone};
+        last;
+      } elsif ($zone eq '') {
+        last;
+      } else {  # strip one level, careful with address literals
+        $zone = ($zone =~ /^( (?: [^.] | \[ (?: \\. | [^\]\\] )* \] )* )
+                            \. (.*) \z/xs) ? $2 : '';
+      }
+    }
+  }
+
+  dbg("async: applying by_zone settings for %s", $zone)  if $settings;
+
+  my $t_init = $ent->{timeout_initial};  # application-specified has precedence
+  $t_init = $settings->{rbl_timeout}  if $settings && !defined $t_init;
+  $t_init = $self->{main}->{conf}->{rbl_timeout}  if !defined $t_init;
+  $t_init = 0  if !defined $t_init;      # last-resort default, just in case
+
+  my $t_end = $ent->{timeout_min};       # application-specified has precedence
+  $t_end = $settings->{rbl_timeout_min}  if $settings && !defined $t_end;
+  $t_end = $self->{main}->{conf}->{rbl_timeout_min}  if !defined $t_end; # added for bug 7070
+  $t_end = 0.2 * $t_init  if !defined $t_end;
+  $t_end = 0  if $t_end < 0;  # just in case
+  $t_init = $t_end  if $t_init < $t_end;
+
+  my $clipped_by_master_deadline = 0;
+  if (defined $master_deadline) {
+    my $time_avail = $master_deadline - time;
+    $time_avail = 0.5  if $time_avail < 0.5;  # give some slack
+    if ($t_init > $time_avail) {
+      $t_init = $time_avail; $clipped_by_master_deadline = 1;
+      $t_end  = $time_avail  if $t_end > $time_avail;
+    }
+  }
+  $ent->{timeout_initial} = $t_init;
+  $ent->{timeout_min} = $t_end;
+
+  my @rulenames = grep { defined } (ref $ent->{rulename} ?
+                    @{$ent->{rulename}} : $ent->{rulename});
+  $ent->{display_id} =  # identifies entry in debug logging and similar
+    join(", ", grep { defined } map { $ent->{$_} } qw(type key));
+
+  $self->{pending_lookups}->{$key} = $ent;
+
+  $self->{queries_started}++;
+  dbg("async: starting: %s%s (timeout %.1fs, min %.1fs)%s",
+      @rulenames ? join(", ", @rulenames).", " : '',
+      $ent->{display_id}, $ent->{timeout_initial}, $ent->{timeout_min},
+      !$clipped_by_master_deadline ? '' : ', capped by time limit');
+
+  $ent;
+}
+
+# ---------------------------------------------------------------------------
+
+=item $ent = $async->get_lookup($key)
+
+DEPRECATED since 4.0.0. Do not use.
 
 =cut
 
 sub get_lookup {
   my ($self, $key) = @_;
+  warn("async: deprecated get_lookup function used\n");
   return $self->{pending_lookups}->{$key};
 }
 
@@ -415,18 +531,16 @@ sub complete_lookups {
   my %typecount;
 
   my $pending = $self->{pending_lookups};
-  $self->{queries_started} = 0;
-  $self->{queries_completed} = 0;
 
   my $now = time;
 
   if (defined $timeout && $timeout > 0 &&
-      %$pending && $self->{total_queries_started} > 0)
+      %$pending && $self->{queries_started} > 0)
   {
     # shrink a 'select' timeout if a caller specified unnecessarily long
     # value beyond the latest deadline of any outstanding request;
     # can save needless wait time (up to 1 second in harvest_dnsbl_queries)
-    my $r = $self->{total_queries_completed} / $self->{total_queries_started};
+    my $r = $self->{queries_completed} / $self->{queries_started};
     my $r2 = $r * $r;  # 0..1
     my $max_deadline;
     while (my($key,$ent) = each %$pending) {
@@ -457,9 +571,9 @@ sub complete_lookups {
 
     if (%$pending) {  # any outstanding requests still?
       $self->{last_poll_responses_time} = $now;
-      my $nfound = $self->{main}->{resolver}->poll_responses($timeout);
-      dbg("async: select found %s responses ready (t.o.=%.1f)",
-          !$nfound ? 'no' : $nfound,  $timeout);
+      my ($nfound, $ncb) = $self->{main}->{resolver}->poll_responses($timeout);
+      dbg("async: select found %d responses ready (t.o.=%.1f), did %d callbacks",
+          $nfound, $timeout, $ncb);
     }
     $now = time;  # capture new timestamp, after possible sleep in 'select'
 
@@ -475,18 +589,19 @@ sub complete_lookups {
         $anydone = 1;
         $ent->{finish_time} = $now  if !defined $ent->{finish_time};
         my $elapsed = $ent->{finish_time} - $ent->{start_time};
-        dbg("async: completed in %.3f s: %s", $elapsed, $ent->{display_id});
-        $self->{timing_by_query}->{". $key"} += $elapsed;
+        my @rulenames = keys %{$self->{rules_for_key}->{$key}};
+        dbg("async: completed in %.3f s: %s, rules: %s",
+            $elapsed, $ent->{display_id}, join(", ", @rulenames));
+        $self->{timing_by_query}->{". $key ($ent->{type})"} += $elapsed;
         $self->{queries_completed}++;
-        $self->{total_queries_completed}++;
         delete $pending->{$key};
       }
     }
 
     if (%$pending) {  # still any requests outstanding? are they expired?
       my $r =
-        !$allow_aborting_of_expired || !$self->{total_queries_started} ? 1.0
-        : $self->{total_queries_completed} / $self->{total_queries_started};
+        !$allow_aborting_of_expired || !$self->{queries_started} ? 1.0
+        : $self->{queries_completed} / $self->{queries_started};
       my $r2 = $r * $r;  # 0..1
       while (my($key,$ent) = each %$pending) {
         $typecount{$ent->{type}}++;
@@ -496,8 +611,6 @@ sub complete_lookups {
         $dt = 1 + int $dt  if $timer_resolution == 1 && $dt > int $dt;
         $allexpired = 0  if $now <= $ent->{start_time} + $dt;
       }
-      dbg("async: queries completed: %d, started: %d",
-          $self->{queries_completed}, $self->{queries_started});
     }
 
     # ensure we don't get stuck if a request gets lost in the ether.
@@ -511,9 +624,9 @@ sub complete_lookups {
       $alldone = 1;
     }
     else {
-      dbg("async: queries active: %s%s at %s",
+      dbg("async: queries still pending: %s%s",
           join (' ', map { "$_=$typecount{$_}" } sort keys %typecount),
-          $allexpired ? ', all expired' : '', scalar(localtime(time)));
+          $allexpired ? ', all expired' : '');
       $alldone = 0;
     }
     1;
@@ -544,15 +657,20 @@ sub abort_remaining_lookups {
   my $foundcnt = 0;
   my $now = time;
 
+  $self->{pending_rules} = {};
+
   while (my($key,$ent) = each %$pending) {
-    dbg("async: aborting after %.3f s, %s: %s",
-        $now - $ent->{start_time},
+    my $dur = $now - $ent->{start_time};
+    my @rulenames = keys %{$self->{rules_for_key}->{$key}};
+    my $msg = sprintf( "async: aborting after %.3f s, %s: %s, rules: %s",
+        $dur,
         (defined $ent->{timeout_initial} &&
          $now > $ent->{start_time} + $ent->{timeout_initial}
            ? 'past original deadline' : 'deadline shrunk'),
-        $ent->{display_id} );
+        $ent->{display_id}, join(", ", @rulenames) );
+    $dur > 1 ? info($msg) : dbg($msg);
     $foundcnt++;
-    $self->{timing_by_query}->{"X $key"} = $now - $ent->{start_time};
+    $self->{timing_by_query}->{"X $key"} = $dur;
     $ent->{finish_time} = $now  if !defined $ent->{finish_time};
     delete $pending->{$key};
   }
@@ -566,8 +684,10 @@ sub abort_remaining_lookups {
     foreach my $tuple (@{$dns_query_info->{applicants}}) {
       my($ent, $cb) = @$tuple;
       if ($cb) {
-        dbg("async: calling callback/abort on key %s%s", $dnskey,
-            !defined $ent->{rulename} ? '' : ", rule ".$ent->{rulename});
+        my @rulenames = grep { defined } (ref $ent->{rulename} ?
+                  @{$ent->{rulename}} : $ent->{rulename});
+        dbg("async: calling callback/abort on key %s, rules: %s", $dnskey,
+            join(", ", @rulenames));
         $cb_count++;
         eval {
           $cb->($ent, undef); 1;
@@ -575,7 +695,7 @@ sub abort_remaining_lookups {
           chomp $@;
           # resignal if alarm went off
           die "async: (2) $@\n"  if $@ =~ /__alarm__ignore__\(.*\)/s;
-          warn sprintf("query %s aborted, callback %s failed: %s\n",
+          warn sprintf("async: query %s aborted, callback %s failed: %s\n",
                        $dnskey, $ent->{key}, $@);
         };
       }
@@ -594,6 +714,8 @@ sub abort_remaining_lookups {
 
 =item $async->set_response_packet($id, $pkt, $key, $timestamp)
 
+For internal use, do not call from plugins.
+
 Register a "response packet" for a given query.  C<$id> is the ID for the
 query, and must match the C<id> supplied in C<start_lookup()>. C<$pkt> is the
 packet object for the response. A parameter C<$key> identifies an entry in a
@@ -645,6 +767,8 @@ sub set_response_packet {
 
 =item $async->report_id_complete($id,$key,$key,$timestamp)
 
+DEPRECATED since 4.0.0. Do not use.
+
 Legacy. Equivalent to $self->set_response_packet($id,undef,$key,$timestamp),
 i.e. providing undef as a response packet. Register that a query has
 completed and is no longer "pending". C<$id> is the ID for the query,
diff --git a/upstream/lib/Mail/SpamAssassin/AutoWelcomelist.pm b/upstream/lib/Mail/SpamAssassin/AutoWelcomelist.pm
new file mode 100644 (file)
index 0000000..db77947
--- /dev/null
@@ -0,0 +1,353 @@
+# <@LICENSE>
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to you under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at:
+# 
+#     http://www.apache.org/licenses/LICENSE-2.0
+# 
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# </@LICENSE>
+
+=head1 NAME
+
+Mail::SpamAssassin::AutoWelcomelist - auto-welcomelist handler for SpamAssassin
+
+=head1 SYNOPSIS
+
+  (see Mail::SpamAssassin)
+
+
+=head1 DESCRIPTION
+
+Mail::SpamAssassin is a module to identify spam using text analysis and
+several internet-based realtime blocklists.
+
+This class is used internally by SpamAssassin to manage the automatic
+welcomelisting functionality.  Please refer to the C<Mail::SpamAssassin>
+documentation for public interfaces.
+
+=head1 METHODS
+
+=over 4
+
+=cut
+
+package Mail::SpamAssassin::AutoWelcomelist;
+
+use strict;
+use warnings;
+# use bytes;
+use re 'taint';
+
+use NetAddr::IP 4.000;
+
+use Mail::SpamAssassin;
+use Mail::SpamAssassin::Logger;
+use Mail::SpamAssassin::Util qw(untaint_var);
+
+our @ISA = qw();
+
+###########################################################################
+
+sub new {
+  my $class = shift;
+  $class = ref($class) || $class;
+  my ($main, $msg) = @_;
+
+  my $conf = $main->{conf};
+  my $self = {
+    main          => $main,
+    factor        => $conf->{auto_welcomelist_factor},
+    ipv4_mask_len => $conf->{auto_welcomelist_ipv4_mask_len},
+    ipv6_mask_len => $conf->{auto_welcomelist_ipv6_mask_len},
+  };
+
+  my $factory;
+  if ($main->{pers_addr_list_factory}) {
+    $factory = $main->{pers_addr_list_factory};
+  }
+  else {
+    my $type = $conf->{auto_welcomelist_factory};
+    if ($type =~ /^([_A-Za-z0-9:]+)$/) {
+      $type = untaint_var($type);
+      eval '
+       require '.$type.';
+        $factory = '.$type.'->new();
+        1;
+      ' or do {
+       my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
+       warn "auto-welcomelist: $eval_stat\n";
+       undef $factory;
+      };
+      $main->set_persistent_address_list_factory($factory) if $factory;
+    }
+    else {
+      warn "auto-welcomelist: illegal auto_welcomelist_factory setting\n";
+    }
+  }
+
+  if (!defined $factory) {
+    $self->{checker} = undef;
+  } else {
+    $self->{checker} = $factory->new_checker($self->{main});
+  }
+
+  bless ($self, $class);
+  $self;
+}
+
+###########################################################################
+
+=item $meanscore = awl->check_address($addr, $originating_ip, $signedby);
+
+This method will return the mean score of all messages associated with the
+given address, or undef if the address hasn't been seen before.
+
+If B<$originating_ip> is supplied, it will be used in the lookup.
+
+=cut
+
+sub check_address {
+  my ($self, $addr, $origip, $signedby) = @_;
+
+  if (!defined $self->{checker}) {
+    return;            # no factory defined; we can't check
+  }
+
+  $self->{entry} = undef;
+
+  my $fulladdr = $self->pack_addr ($addr, $origip);
+  my $entry = $self->{checker}->get_addr_entry ($fulladdr, $signedby);
+  $self->{entry} = $entry;
+
+  if (!$entry->{msgcount}) {
+    # no entry found
+    if (defined $origip) {
+      # try upgrading a default entry (probably from "add-addr-to-foo")
+      my $noipaddr = $self->pack_addr ($addr, undef);
+      my $noipent = $self->{checker}->get_addr_entry ($noipaddr, undef);
+
+      if (defined $noipent->{msgcount} && $noipent->{msgcount} > 0) {
+       dbg("auto-welcomelist: found entry w/o IP address for $addr: replacing with $origip");
+       $self->{checker}->remove_entry($noipent);
+        # Now assign proper entry the count and totscore values of the
+        # no-IP entry instead of assigning the whole value to avoid
+        # wiping out any information added to the previous entry.
+       $entry->{msgcount} = $noipent->{msgcount};
+       $entry->{totscore} = $noipent->{totscore};
+      }
+    }
+  }
+
+  if ($entry->{msgcount} < 0 ||
+      $entry->{msgcount} != $entry->{msgcount} ||  # test for NaN
+      $entry->{totscore} != $entry->{totscore})
+  {
+    warn "auto-welcomelist: resetting bad data for ($addr, $origip), ".
+         "count: $entry->{msgcount}, totscore: $entry->{totscore}\n";
+    $entry->{msgcount} = $entry->{totscore} = 0;
+  }
+
+  return !$entry->{msgcount} ? undef : $entry->{totscore} / $entry->{msgcount};
+}
+
+###########################################################################
+
+=item awl->count();
+
+This method will return the count of messages used in determining the
+welcomelist correction.
+
+=cut
+
+sub count {
+  my $self = shift;
+  return $self->{entry}->{msgcount};
+}
+
+
+###########################################################################
+
+=item awl->add_score($score);
+
+This method will add half the score to the current entry.  Half the
+score is used, so that repeated use of the same From and IP address
+combination will gradually reduce the score.
+
+=cut
+
+sub add_score {
+  my ($self,$score) = @_;
+
+  if (!defined $self->{checker}) {
+    return;            # no factory defined; we can't check
+  }
+  if ($score != $score) {
+    warn "auto-welcomelist: attempt to add a $score to AWL entry ignored\n";
+    return;            # don't try to add a NaN
+  }
+
+  $self->{entry}->{msgcount} ||= 0;
+  $self->{checker}->add_score($self->{entry}, $score);
+}
+
+###########################################################################
+
+=item awl->add_known_good_address($addr);
+
+This method will add a score of -100 to the given address -- effectively
+"bootstrapping" the address as being one that should be welcomelisted.
+
+=cut
+
+sub add_known_good_address {
+  my ($self, $addr, $signedby) = @_;
+
+  return $self->modify_address($addr, -100, $signedby);
+}
+
+
+###########################################################################
+
+=item awl->add_known_bad_address($addr);
+
+This method will add a score of 100 to the given address -- effectively
+"bootstrapping" the address as being one that should be blocklisted.
+
+=cut
+
+sub add_known_bad_address {
+  my ($self, $addr, $signedby) = @_;
+
+  return $self->modify_address($addr, 100, $signedby);
+}
+
+###########################################################################
+
+sub remove_address {
+  my ($self, $addr, $signedby) = @_;
+
+  return $self->modify_address($addr, undef, $signedby);
+}
+
+###########################################################################
+
+sub modify_address {
+  my ($self, $addr, $score, $signedby) = @_;
+
+  if (!defined $self->{checker}) {
+    return;            # no factory defined; we can't check
+  }
+
+  my $fulladdr = $self->pack_addr ($addr, undef);
+  my $entry = $self->{checker}->get_addr_entry ($fulladdr, $signedby);
+
+  # remove any old entries (will remove per-ip entries as well)
+  # always call this regardless, as the current entry may have 0
+  # scores, but the per-ip one may have more
+  $self->{checker}->remove_entry($entry);
+
+  # remove address only, no new score to add
+  if (!defined $score)  { return 1; }
+  if ($score != $score) { return 1; }  # don't try to add a NaN
+
+  # else add score. get a new entry first
+  $entry = $self->{checker}->get_addr_entry ($fulladdr, $signedby);
+  $self->{checker}->add_score($entry, $score);
+
+  return 1;
+}
+
+###########################################################################
+
+sub finish {
+  my $self = shift;
+
+  return  if !defined $self->{checker};
+  $self->{checker}->finish();
+}
+
+###########################################################################
+
+sub ip_to_awl_key {
+  my ($self, $origip) = @_;
+
+  my $result;
+  local $1;
+  if (!defined $origip) {
+    # could not find an IP address to use
+  } elsif ($origip =~ /^ (\d{1,3} \. \d{1,3}) \. \d{1,3} \. \d{1,3} $/xs) {
+    my $mask_len = $self->{ipv4_mask_len};
+    $mask_len = 16  if !defined $mask_len;
+    # handle the default and easy cases manually
+    if ($mask_len == 32) {
+      $result = $origip;
+    } elsif ($mask_len == 16) {
+      $result = $1;
+    } else {
+      my $origip_obj = NetAddr::IP->new($origip . '/' . $mask_len);
+      if (!defined $origip_obj) {  # invalid IPv4 address
+        dbg("auto-welcomelist: bad IPv4 address $origip");
+      } else {
+        $result = $origip_obj->network->addr;
+        $result =~s/(\.0){1,3}\z//;  # truncate zero tail
+      }
+    }
+  } elsif (index($origip, ':') >= 0 &&  # triage
+           $origip =~
+           /^ [0-9a-f]{0,4} (?: : [0-9a-f]{0,4} | \. [0-9]{1,3} ){2,9} $/xsi) {
+    # looks like an IPv6 address
+    my $mask_len = $self->{ipv6_mask_len};
+    $mask_len = 48  if !defined $mask_len;
+    my $origip_obj = NetAddr::IP->new6($origip . '/' . $mask_len);
+    if (!defined $origip_obj) {  # invalid IPv6 address
+      dbg("auto-welcomelist: bad IPv6 address $origip");
+    } elsif (NetAddr::IP->can('full6')) {  # since NetAddr::IP 4.010
+      $result = $origip_obj->network->full6;  # string in a canonical form
+      $result =~ s/(:0000){1,7}\z/::/;        # compress zero tail
+    }
+  } else {
+    dbg("auto-welcomelist: bad IP address $origip");
+  }
+  if (defined $result && length($result) > 39) {  # just in case, keep under
+    $result = substr($result,0,39);               # the awl.ip field size
+  }
+  if (defined $result) {
+    dbg("auto-welcomelist: IP masking %s -> %s", $origip,$result);
+  }
+  return $result;
+}
+
+###########################################################################
+
+sub pack_addr {
+  my ($self, $addr, $origip) = @_;
+
+  $addr = lc $addr;
+  $addr =~ s/[\000\;\'\"\!\|]/_/gs;    # paranoia
+
+  if (defined $origip) {
+    $origip = $self->ip_to_awl_key($origip);
+  }
+  if (!defined $origip) {
+    # could not find an IP address to use, could be localhost mail
+    # or from the user running "add-addr-to-*".
+    $origip = 'none';
+  }
+  return $addr . "|ip=" . $origip;
+}
+
+###########################################################################
+
+1;
+
+=back
+
+=cut
diff --git a/upstream/lib/Mail/SpamAssassin/AutoWhitelist.pm b/upstream/lib/Mail/SpamAssassin/AutoWhitelist.pm
deleted file mode 100644 (file)
index 627e249..0000000
+++ /dev/null
@@ -1,354 +0,0 @@
-# <@LICENSE>
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements.  See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to you under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License.  You may obtain a copy of the License at:
-# 
-#     http://www.apache.org/licenses/LICENSE-2.0
-# 
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-# </@LICENSE>
-
-=head1 NAME
-
-Mail::SpamAssassin::AutoWhitelist - auto-whitelist handler for SpamAssassin
-
-=head1 SYNOPSIS
-
-  (see Mail::SpamAssassin)
-
-
-=head1 DESCRIPTION
-
-Mail::SpamAssassin is a module to identify spam using text analysis and
-several internet-based realtime blacklists.
-
-This class is used internally by SpamAssassin to manage the automatic
-whitelisting functionality.  Please refer to the C<Mail::SpamAssassin>
-documentation for public interfaces.
-
-=head1 METHODS
-
-=over 4
-
-=cut
-
-package Mail::SpamAssassin::AutoWhitelist;
-
-use strict;
-use warnings;
-# use bytes;
-use re 'taint';
-
-use NetAddr::IP 4.000;
-
-use Mail::SpamAssassin;
-use Mail::SpamAssassin::Logger;
-use Mail::SpamAssassin::Util qw(untaint_var);
-
-our @ISA = qw();
-
-###########################################################################
-
-sub new {
-  my $class = shift;
-  $class = ref($class) || $class;
-  my ($main, $msg) = @_;
-
-  my $conf = $main->{conf};
-  my $self = {
-    main          => $main,
-    factor        => $conf->{auto_whitelist_factor},
-    ipv4_mask_len => $conf->{auto_whitelist_ipv4_mask_len},
-    ipv6_mask_len => $conf->{auto_whitelist_ipv6_mask_len},
-  };
-
-  my $factory;
-  if ($main->{pers_addr_list_factory}) {
-    $factory = $main->{pers_addr_list_factory};
-  }
-  else {
-    my $type = $conf->{auto_whitelist_factory};
-    if ($type =~ /^([_A-Za-z0-9:]+)$/) {
-      $type = untaint_var($type);
-      eval '
-           require '.$type.';
-            $factory = '.$type.'->new();
-            1;
-           '
-      or do {
-       my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
-       warn "auto-whitelist: $eval_stat\n";
-       undef $factory;
-      };
-      $main->set_persistent_address_list_factory($factory) if $factory;
-    }
-    else {
-      warn "auto-whitelist: illegal auto_whitelist_factory setting\n";
-    }
-  }
-
-  if (!defined $factory) {
-    $self->{checker} = undef;
-  } else {
-    $self->{checker} = $factory->new_checker($self->{main});
-  }
-
-  bless ($self, $class);
-  $self;
-}
-
-###########################################################################
-
-=item $meanscore = awl->check_address($addr, $originating_ip, $signedby);
-
-This method will return the mean score of all messages associated with the
-given address, or undef if the address hasn't been seen before.
-
-If B<$originating_ip> is supplied, it will be used in the lookup.
-
-=cut
-
-sub check_address {
-  my ($self, $addr, $origip, $signedby) = @_;
-
-  if (!defined $self->{checker}) {
-    return;            # no factory defined; we can't check
-  }
-
-  $self->{entry} = undef;
-
-  my $fulladdr = $self->pack_addr ($addr, $origip);
-  my $entry = $self->{checker}->get_addr_entry ($fulladdr, $signedby);
-  $self->{entry} = $entry;
-
-  if (!$entry->{msgcount}) {
-    # no entry found
-    if (defined $origip) {
-      # try upgrading a default entry (probably from "add-addr-to-foo")
-      my $noipaddr = $self->pack_addr ($addr, undef);
-      my $noipent = $self->{checker}->get_addr_entry ($noipaddr, undef);
-
-      if (defined $noipent->{msgcount} && $noipent->{msgcount} > 0) {
-       dbg("auto-whitelist: found entry w/o IP address for $addr: replacing with $origip");
-       $self->{checker}->remove_entry($noipent);
-        # Now assign proper entry the count and totscore values of the
-        # no-IP entry instead of assigning the whole value to avoid
-        # wiping out any information added to the previous entry.
-       $entry->{msgcount} = $noipent->{msgcount};
-       $entry->{totscore} = $noipent->{totscore};
-      }
-    }
-  }
-
-  if ($entry->{msgcount} < 0 ||
-      $entry->{msgcount} != $entry->{msgcount} ||  # test for NaN
-      $entry->{totscore} != $entry->{totscore})
-  {
-    warn "auto-whitelist: resetting bad data for ($addr, $origip), ".
-         "count: $entry->{msgcount}, totscore: $entry->{totscore}\n";
-    $entry->{msgcount} = $entry->{totscore} = 0;
-  }
-
-  return !$entry->{msgcount} ? undef : $entry->{totscore} / $entry->{msgcount};
-}
-
-###########################################################################
-
-=item awl->count();
-
-This method will return the count of messages used in determining the
-whitelist correction.
-
-=cut
-
-sub count {
-  my $self = shift;
-  return $self->{entry}->{msgcount};
-}
-
-
-###########################################################################
-
-=item awl->add_score($score);
-
-This method will add half the score to the current entry.  Half the
-score is used, so that repeated use of the same From and IP address
-combination will gradually reduce the score.
-
-=cut
-
-sub add_score {
-  my ($self,$score) = @_;
-
-  if (!defined $self->{checker}) {
-    return;            # no factory defined; we can't check
-  }
-  if ($score != $score) {
-    warn "auto-whitelist: attempt to add a $score to AWL entry ignored\n";
-    return;            # don't try to add a NaN
-  }
-
-  $self->{entry}->{msgcount} ||= 0;
-  $self->{checker}->add_score($self->{entry}, $score);
-}
-
-###########################################################################
-
-=item awl->add_known_good_address($addr);
-
-This method will add a score of -100 to the given address -- effectively
-"bootstrapping" the address as being one that should be whitelisted.
-
-=cut
-
-sub add_known_good_address {
-  my ($self, $addr, $signedby) = @_;
-
-  return $self->modify_address($addr, -100, $signedby);
-}
-
-
-###########################################################################
-
-=item awl->add_known_bad_address($addr);
-
-This method will add a score of 100 to the given address -- effectively
-"bootstrapping" the address as being one that should be blacklisted.
-
-=cut
-
-sub add_known_bad_address {
-  my ($self, $addr, $signedby) = @_;
-
-  return $self->modify_address($addr, 100, $signedby);
-}
-
-###########################################################################
-
-sub remove_address {
-  my ($self, $addr, $signedby) = @_;
-
-  return $self->modify_address($addr, undef, $signedby);
-}
-
-###########################################################################
-
-sub modify_address {
-  my ($self, $addr, $score, $signedby) = @_;
-
-  if (!defined $self->{checker}) {
-    return;            # no factory defined; we can't check
-  }
-
-  my $fulladdr = $self->pack_addr ($addr, undef);
-  my $entry = $self->{checker}->get_addr_entry ($fulladdr, $signedby);
-
-  # remove any old entries (will remove per-ip entries as well)
-  # always call this regardless, as the current entry may have 0
-  # scores, but the per-ip one may have more
-  $self->{checker}->remove_entry($entry);
-
-  # remove address only, no new score to add
-  if (!defined $score)  { return 1; }
-  if ($score != $score) { return 1; }  # don't try to add a NaN
-
-  # else add score. get a new entry first
-  $entry = $self->{checker}->get_addr_entry ($fulladdr, $signedby);
-  $self->{checker}->add_score($entry, $score);
-
-  return 1;
-}
-
-###########################################################################
-
-sub finish {
-  my $self = shift;
-
-  return  if !defined $self->{checker};
-  $self->{checker}->finish();
-}
-
-###########################################################################
-
-sub ip_to_awl_key {
-  my ($self, $origip) = @_;
-
-  my $result;
-  local $1;
-  if (!defined $origip) {
-    # could not find an IP address to use
-  } elsif ($origip =~ /^ (\d{1,3} \. \d{1,3}) \. \d{1,3} \. \d{1,3} $/xs) {
-    my $mask_len = $self->{ipv4_mask_len};
-    $mask_len = 16  if !defined $mask_len;
-    # handle the default and easy cases manually
-    if ($mask_len == 32) {
-      $result = $origip;
-    } elsif ($mask_len == 16) {
-      $result = $1;
-    } else {
-      my $origip_obj = NetAddr::IP->new($origip . '/' . $mask_len);
-      if (!defined $origip_obj) {  # invalid IPv4 address
-        dbg("auto-whitelist: bad IPv4 address $origip");
-      } else {
-        $result = $origip_obj->network->addr;
-        $result =~s/(\.0){1,3}\z//;  # truncate zero tail
-      }
-    }
-  } elsif ($origip =~ /:/ &&  # triage
-           $origip =~
-           /^ [0-9a-f]{0,4} (?: : [0-9a-f]{0,4} | \. [0-9]{1,3} ){2,9} $/xsi) {
-    # looks like an IPv6 address
-    my $mask_len = $self->{ipv6_mask_len};
-    $mask_len = 48  if !defined $mask_len;
-    my $origip_obj = NetAddr::IP->new6($origip . '/' . $mask_len);
-    if (!defined $origip_obj) {  # invalid IPv6 address
-      dbg("auto-whitelist: bad IPv6 address $origip");
-    } elsif (NetAddr::IP->can('full6')) {  # since NetAddr::IP 4.010
-      $result = $origip_obj->network->full6;  # string in a canonical form
-      $result =~ s/(:0000){1,7}\z/::/;        # compress zero tail
-    }
-  } else {
-    dbg("auto-whitelist: bad IP address $origip");
-  }
-  if (defined $result && length($result) > 39) {  # just in case, keep under
-    $result = substr($result,0,39);               # the awl.ip field size
-  }
-  if (defined $result) {
-    dbg("auto-whitelist: IP masking %s -> %s", $origip,$result);
-  }
-  return $result;
-}
-
-###########################################################################
-
-sub pack_addr {
-  my ($self, $addr, $origip) = @_;
-
-  $addr = lc $addr;
-  $addr =~ s/[\000\;\'\"\!\|]/_/gs;    # paranoia
-
-  if (defined $origip) {
-    $origip = $self->ip_to_awl_key($origip);
-  }
-  if (!defined $origip) {
-    # could not find an IP address to use, could be localhost mail
-    # or from the user running "add-addr-to-*".
-    $origip = 'none';
-  }
-  return $addr . "|ip=" . $origip;
-}
-
-###########################################################################
-
-1;
-
-=back
-
-=cut
index 32dd9097d1af215407bdaa3347d371993524fa52..f7e1bb48983165b33f9e182f33d4f84b474bb007 100644 (file)
@@ -111,7 +111,7 @@ sub learn {
   {
     # DMK, koppel@ece.lsu.edu:  Hoping that the ultimate fix to bug 2263 will
     # make it unnecessary to construct a PerMsgStatus here.
-    my $PMS = new Mail::SpamAssassin::PerMsgStatus $self->{main}, $msg;
+    my $PMS = Mail::SpamAssassin::PerMsgStatus->new($self->{main}, $msg);
     my $ignore = $self->ignore_message($PMS);
     $PMS->finish();
     return 0 if $ignore;
index 41d9fc24aff6af1e298af4066e20209679517a1f..d1164f0003e8ea8e4c66303571ed0a91a9f58d39 100644 (file)
@@ -35,14 +35,11 @@ use Errno qw(EBADF);
 #use Data::Dumper;
 use File::Basename;
 use File::Path;
-
-BEGIN {
-  eval { require Digest::SHA; import Digest::SHA qw(sha1); 1 }
-  or do { require Digest::SHA1; import Digest::SHA1 qw(sha1) }
-}
+use Digest::SHA qw(sha1);
 
 use Mail::SpamAssassin::BayesStore;
 use Mail::SpamAssassin::Logger;
+use Mail::SpamAssassin::Util qw(compile_regexp);
 
 our @ISA = qw( Mail::SpamAssassin::BayesStore );
 
@@ -77,6 +74,7 @@ sub new {
 
 sub DESTROY {
   my $self = shift;
+
   $self->_close_db;
 }
 
@@ -650,6 +648,14 @@ sub dump_tokens {
   my($self, $template, $regex, @vars) = @_;
   dbg("bayes: dump_tokens starting");
 
+  if (defined $regex) {
+    my ($rec, $err) = compile_regexp($regex, 2);
+    if (!$rec) {
+      die "Invalid dump_tokens regex '$regex': $err\n";
+    }
+    $regex = $rec;
+  }
+
   my $cursor = $self->{handles}->{tokens}->db_cursor;
   $cursor or die "Couldn't get cursor: $BerkeleyDB::Error";
   my ($token, $value) = ("", "");
index 4bb2fbe8c52b6ec04358e0c0ca2f157f53295212..ace15bcc736b66daf9b88b8ee9c39b8132381854 100644 (file)
@@ -27,14 +27,10 @@ use Errno qw(EBADF);
 use File::Basename;
 use File::Spec;
 use File::Path;
-
-BEGIN {
-  eval { require Digest::SHA; import Digest::SHA qw(sha1); 1 }
-  or do { require Digest::SHA1; import Digest::SHA1 qw(sha1) }
-}
+use Digest::SHA qw(sha1);
 
 use Mail::SpamAssassin;
-use Mail::SpamAssassin::Util qw(untaint_var am_running_on_windows);
+use Mail::SpamAssassin::Util qw(untaint_var am_running_on_windows compile_regexp);
 use Mail::SpamAssassin::BayesStore;
 use Mail::SpamAssassin::Logger;
 
@@ -992,9 +988,17 @@ sub get_storage_variables {
 sub dump_db_toks {
   my ($self, $template, $regex, @vars) = @_;
 
+  if (defined $regex) {
+    my ($rec, $err) = compile_regexp($regex, 2);
+    if (!$rec) {
+      die "Invalid dump_tokens regex '$regex': $err\n";
+    }
+    $regex = $rec;
+  }
+
   while (my ($tok, $tokvalue) = each %{$self->{db_toks}}) {
     next if ($tok =~ MAGIC_RE); # skip magic tokens
-    next if (defined $regex && ($tok !~ /$regex/o));
+    next if (defined $regex && $tok !~ /$regex/o);
 
     # We have the value already, so just unpack it.
     my ($ts, $th, $atime) = $self->tok_unpack ($tokvalue);
index c4bfb920c9fea3655a3839bed4081926e91d169f..df3ed817f5e4152430ee70a4023051ad408b9dd1 100644 (file)
@@ -30,6 +30,9 @@ In addition, this module will support rollback on error, if you are
 using the InnoDB database table type in MySQL.  For more information
 please review the instructions in sql/README.bayes.
 
+This module is also compatible with MariaDB and DBD::MariaDB can be used
+instead of DBD::mysql driver.
+
 =cut
 
 package Mail::SpamAssassin::BayesStore::MySQL;
@@ -292,7 +295,8 @@ sub set_running_expire_tok {
 
   return 0 unless (defined($self->{_dbh}));
 
-  my $sql = "INSERT INTO bayes_expire (id,runtime) VALUES (?,?)";
+  my $sql = "INSERT INTO bayes_expire (id,runtime) VALUES (?,?)
+             ON DUPLICATE KEY UPDATE runtime=VALUES(runtime)";
 
   my $time = time();
 
@@ -339,6 +343,147 @@ sub remove_running_expire_tok {
   return 1;
 }
 
+=head2 tok_get
+
+public instance (Integer, Integer, Integer) tok_get (String $token)
+
+Description:
+This method retrieves a specified token (C<$token>) from the database
+and returns it's spam_count, ham_count and last access time.
+
+=cut
+
+sub tok_get {
+  my ($self, $token) = @_;
+
+  return (0,0,0) unless (defined($self->{_dbh}));
+
+  my $sql = "SELECT spam_count, ham_count, atime
+               FROM bayes_token
+              WHERE id = ?
+                AND token = ?";
+
+  my $sth = $self->{_dbh}->prepare_cached($sql);
+
+  unless (defined($sth)) {
+    dbg("bayes: tok_get: SQL error: ".$self->{_dbh}->errstr());
+    $self->{_dbh}->rollback();
+    return (0,0,0);
+  }
+
+  $sth->bind_param(1, $self->{_userid});
+  $sth->bind_param(2, $token, DBI::SQL_BINARY);
+
+  my $rc = $sth->execute();
+
+  unless ($rc) {
+    dbg("bayes: tok_get: SQL error: ".$self->{_dbh}->errstr());
+    $self->{_dbh}->rollback();
+    return (0,0,0);
+  }
+
+  my ($spam_count, $ham_count, $atime) = $sth->fetchrow_array();
+
+  $sth->finish();
+
+  $spam_count = 0 if (!$spam_count || $spam_count < 0);
+  $ham_count = 0 if (!$ham_count || $ham_count < 0);
+  $atime = 0 if (!$atime);
+
+  return ($spam_count, $ham_count, $atime)
+}
+
+=head2 tok_get_all
+
+public instance (\@) tok_get (@ $tokens)
+
+Description:
+This method retrieves the specified tokens (C<$tokens>) from storage and returns
+an array ref of arrays spam count, ham count and last access time.
+
+=cut
+
+sub tok_get_all {
+  my ($self, @tokens) = @_;
+
+  return [] unless (defined($self->{_dbh}));
+
+  my $token_list_size = scalar(@tokens);
+  dbg("bayes: tok_get_all: token count: $token_list_size");
+  my @tok_results;
+
+  my $search_index = 0;
+  my $results_index = 0;
+  my $bunch_end;
+
+  my $token_select = $self->_token_select_string();
+
+  my $multi_sql = "SELECT $token_select, spam_count, ham_count, atime
+                     FROM bayes_token
+                    WHERE id = ?
+                      AND token IN ";
+
+  # fetch tokens in bunches of 100 until there are <= 100 left, then just fetch the rest
+  while ($token_list_size > $search_index) {
+    my $bunch_size;
+    if ($token_list_size - $search_index > 100) {
+      $bunch_size = 100;
+    }
+    else {
+      $bunch_size = $token_list_size - $search_index;
+    }
+    while ($token_list_size - $search_index >= $bunch_size) {
+      my @tok;
+      my $in_str = '(';
+
+      $bunch_end = $search_index + $bunch_size;
+      for ( ; $search_index < $bunch_end; $search_index++) {
+       $in_str .= '?,';
+       push(@tok, $tokens[$search_index]);
+      }
+      chop $in_str;
+      $in_str .= ')';
+
+      my $dynamic_sql = $multi_sql . $in_str;
+
+      my $sth = $self->{_dbh}->prepare($dynamic_sql);
+
+      unless (defined($sth)) {
+       dbg("bayes: tok_get_all: SQL error: ".$self->{_dbh}->errstr());
+       $self->{_dbh}->rollback();
+       return [];
+      }
+
+      my $idx = 0;
+      $sth->bind_param(++$idx, $self->{_userid});
+      $sth->bind_param(++$idx, $_, DBI::SQL_BINARY) foreach (@tok);
+
+      my $rc = $sth->execute();
+
+      unless ($rc) {
+       dbg("bayes: tok_get_all: SQL error: ".$self->{_dbh}->errstr());
+       $self->{_dbh}->rollback();
+       return [];
+      }
+
+      my $results = $sth->fetchall_arrayref();
+
+      $sth->finish();
+
+      foreach my $result (@{$results}) {
+       # Make sure that spam_count and ham_count are not negative
+       $result->[1] = 0 if (!$result->[1] || $result->[1] < 0);
+       $result->[2] = 0 if (!$result->[2] || $result->[2] < 0);
+       # Make sure that atime has a value
+       $result->[3] = 0 if (!$result->[3]);
+       $tok_results[$results_index++] = $result;
+      }
+    }
+  }
+
+  return \@tok_results;
+}
+
 =head2 nspam_nham_change
 
 public instance (Boolean) nspam_nham_change (Integer $num_spam,
@@ -421,10 +566,22 @@ sub tok_touch {
                 AND token = ?
                 AND atime < ?";
 
-  my $rows = $self->{_dbh}->do($sql, undef, $atime, $self->{_userid},
-                              $token, $atime);
+  my $sth = $self->{_dbh}->prepare_cached($sql);
 
-  unless (defined($rows)) {
+  unless (defined($sth)) {
+    dbg("bayes: tok_touch: SQL error: ".$self->{_dbh}->errstr());
+    $self->{_dbh}->rollback();
+    return 0;
+  }
+
+  $sth->bind_param(1, $atime);
+  $sth->bind_param(2, $self->{_userid});
+  $sth->bind_param(3, $token, DBI::SQL_BINARY);
+  $sth->bind_param(4, $atime);
+
+  my $rows = $sth->execute();
+
+  unless ($rows) {
     dbg("bayes: tok_touch: SQL error: ".$self->{_dbh}->errstr());
     $self->{_dbh}->rollback();
     return 0;
@@ -478,20 +635,29 @@ sub tok_touch_all {
   return 1 unless (scalar(@{$tokens}));
 
   my $sql = "UPDATE bayes_token SET atime = ? WHERE id = ? AND token IN (";
-
-  my @bindings = ($atime, $self->{_userid});
-  foreach my $token (@{$tokens}) {
+  foreach (@{$tokens}) {
     $sql .= "?,";
-    push(@bindings, $token);
   }
   chop($sql); # get rid of trailing ,
-
   $sql .= ") AND atime < ?";
-  push(@bindings, $atime);
 
-  my $rows = $self->{_dbh}->do($sql, undef, @bindings);
+  my $sth = $self->{_dbh}->prepare($sql);
 
-  unless (defined($rows)) {
+  unless (defined($sth)) {
+    dbg("bayes: tok_touch_all: SQL error: ".$self->{_dbh}->errstr());
+    $self->{_dbh}->rollback();
+    return [];
+  }
+
+  my $idx = 0;
+  $sth->bind_param(++$idx, $atime);
+  $sth->bind_param(++$idx, $self->{_userid});
+  $sth->bind_param(++$idx, $_, DBI::SQL_BINARY) foreach (@{$tokens});
+  $sth->bind_param(++$idx, $atime);
+
+  my $rows = $sth->execute();
+
+  unless ($rows) {
     dbg("bayes: tok_touch_all: SQL error: ".$self->{_dbh}->errstr());
     $self->{_dbh}->rollback();
     return 0;
@@ -735,7 +901,8 @@ sub _initialize_db {
     return 0;
   }
 
-  $id = $self->{_dbh}->{'mysql_insertid'};
+  $id = $self->{_dsn} =~ /^DBI:MariaDB/i ?
+    $self->{_dbh}->{'mariadb_insertid'} : $self->{_dbh}->{'mysql_insertid'};
 
   $self->{_dbh}->commit();
 
@@ -797,10 +964,12 @@ sub _put_token {
       return 0;
     }
 
-    my $rc = $sth->execute($spam_count,
-                          $ham_count,
-                          $self->{_userid},
-                          $token);
+    $sth->bind_param(1, $spam_count);
+    $sth->bind_param(2, $ham_count);
+    $sth->bind_param(3, $self->{_userid});
+    $sth->bind_param(4, $token, DBI::SQL_BINARY);
+
+    my $rc = $sth->execute();
 
     unless ($rc) {
       dbg("bayes: _put_token: SQL error: ".$self->{_dbh}->errstr());
@@ -824,14 +993,16 @@ sub _put_token {
       return 0;
     }
 
-    my $rc = $sth->execute($self->{_userid},
-                          $token,
-                          $spam_count,
-                          $ham_count,
-                          $atime,
-                          $spam_count,
-                          $ham_count,
-                          $atime);
+    $sth->bind_param(1, $self->{_userid});
+    $sth->bind_param(2, $token, DBI::SQL_BINARY);
+    $sth->bind_param(3, $spam_count);
+    $sth->bind_param(4, $ham_count);
+    $sth->bind_param(5, $atime);
+    $sth->bind_param(6, $spam_count);
+    $sth->bind_param(7, $ham_count);
+    $sth->bind_param(8, $atime);
+
+    my $rc = $sth->execute();
 
     unless ($rc) {
       dbg("bayes: _put_token: SQL error: ".$self->{_dbh}->errstr());
@@ -948,12 +1119,15 @@ sub _put_tokens {
       return 0;
     }
 
+    $sth->bind_param(1, $spam_count);
+    $sth->bind_param(2, $ham_count);
+    $sth->bind_param(3, $self->{_userid});
+    # 4, update token in foreach loop
+
     my $error_p = 0;
     foreach my $token (keys %{$tokens}) {
-      my $rc = $sth->execute($spam_count,
-                            $ham_count,
-                            $self->{_userid},
-                            $token);
+      $sth->bind_param(4, $token, DBI::SQL_BINARY);
+      my $rc = $sth->execute();
 
       unless ($rc) {
        dbg("bayes: _put_tokens: SQL error: ".$self->{_dbh}->errstr());
@@ -984,18 +1158,21 @@ sub _put_tokens {
       return 0;
     }
 
+    $sth->bind_param(1, $self->{_userid});
+    # 2, update token in foreach loop
+    $sth->bind_param(3, $spam_count);
+    $sth->bind_param(4, $ham_count);
+    $sth->bind_param(5, $atime);
+    $sth->bind_param(6, $spam_count);
+    $sth->bind_param(7, $ham_count);
+    $sth->bind_param(8, $atime);
+
     my $error_p = 0;
     my $new_tokens = 0;
     my $need_atime_update_p = 0;
     foreach my $token (keys %{$tokens}) {
-      my $rc = $sth->execute($self->{_userid},
-                            $token,
-                            $spam_count,
-                            $ham_count,
-                            $atime,
-                            $spam_count,
-                            $ham_count,
-                            $atime);
+      $sth->bind_param(2, $token, DBI::SQL_BINARY);
+      my $rc = $sth->execute();
 
       if (!$rc) {
        dbg("bayes: _put_tokens: SQL error: ".$self->{_dbh}->errstr());
@@ -1070,6 +1247,22 @@ sub _put_tokens {
   return 1;
 }
 
+=head2 _token_select_string
+
+private instance (String) _token_select_string
+
+Description:
+This method returns the string to be used in SELECT statements to represent
+the token column.
+
+The default is to use the RPAD function to pad the token out to 5 characters.
+
+=cut
+
+sub _token_select_string {
+  return "RPAD(token, 5, ' ')";
+}
+
 sub sa_die { Mail::SpamAssassin::sa_die(@_); }
 
 1;
index 06553e41725760f2463bf1e3af27b8d86b55a549..cd02928fd215293c65193ae8fc229045467a076d 100644 (file)
@@ -51,7 +51,7 @@ use constant HAS_DBI => eval { require DBI; };
 
 # We need this so we can import the pg_types, since this is a DBD::Pg specific module it should be ok
 # YUCK! This little require/import trick is required for the rpm stuff
-BEGIN { require DBD::Pg; import DBD::Pg qw(:pg_types); }
+BEGIN { require DBD::Pg; DBD::Pg->import(qw(:pg_types)); }
 
 =head1 METHODS
 
index f01b1ac49be89d054a2f24c2f61a1e5b9a9e1009..d18c521b43d8d2baccd30d0ff42465199347e785 100644 (file)
@@ -119,16 +119,12 @@ use warnings;
 # use bytes;
 use re 'taint';
 use Errno qw(EBADF);
-use Mail::SpamAssassin::Util qw(untaint_var);
-use Mail::SpamAssassin::Timeout;
-
-BEGIN {
-  eval { require Digest::SHA; import Digest::SHA qw(sha1); 1 }
-  or do { require Digest::SHA1; import Digest::SHA1 qw(sha1) }
-}
+use Digest::SHA qw(sha1);
 
-use Mail::SpamAssassin::Logger;
 use Mail::SpamAssassin::BayesStore;
+use Mail::SpamAssassin::Logger;
+use Mail::SpamAssassin::Timeout;
+use Mail::SpamAssassin::Util qw(untaint_var);
 use Mail::SpamAssassin::Util::TinyRedis;
 
 our $VERSION = 0.09;
index 7b737fd2c78ed5e50d6d87c789c746555c58647d..16e4e787d51fbd69262f1569d6b386436c7f6cf6 100644 (file)
@@ -21,7 +21,11 @@ Mail::SpamAssassin::BayesStore::SQL - SQL Bayesian Storage Module Implementation
 
 =head1 DESCRIPTION
 
-This module implements a SQL based bayesian storage module.
+This module implements a SQL based bayesian storage module.  It's compatible
+with SQLite and possibly other standard SQL servers.
+
+Do not use this for MySQL/MariaDB or PgSQL, they have their own specific
+modules.
 
 =cut
 
@@ -32,11 +36,7 @@ use warnings;
 # use bytes;
 use re 'taint';
 use Errno qw(EBADF);
-
-BEGIN {
-  eval { require Digest::SHA; import Digest::SHA qw(sha1); 1 }
-  or do { require Digest::SHA1; import Digest::SHA1 qw(sha1) }
-}
+use Digest::SHA qw(sha1);
 
 use Mail::SpamAssassin::BayesStore;
 use Mail::SpamAssassin::Logger;
@@ -1676,6 +1676,13 @@ sub _connect_db {
     dbg("bayes: database connection established");
   }
 
+  # SQLite PRAGMA attributes - here for tests, see bug 8033
+  if ($self->{_dsn} =~ /^dbi:SQLite:.*?;(.+)/i) {
+    foreach my $attr (split(/;/, $1)) {
+      $dbh->do("PRAGMA $attr");
+    }
+  }
+  
   $self->{_dbh} = $dbh;
 
  return 1;
@@ -2348,12 +2355,13 @@ Description:
 This method returns the string to be used in SELECT statements to represent
 the token column.
 
-The default is to use the RPAD function to pad the token out to 5 characters.
+The default is to use the SUBSTR function to pad the token out to 5 characters.
 
 =cut
 
 sub _token_select_string {
-  return "RPAD(token, 5, ' ')";
+  # Use SQLite compatible RPAD alternative
+  return "SUBSTR(token || '     ', 1, 5)";
 }
 
 sub sa_die { Mail::SpamAssassin::sa_die(@_); }
index aa931470ce15fe6f60d0c43fb8ab803647456340..dbe7d2a660f5e7396b8b2fdebd92bdbdbf567cb7 100644 (file)
@@ -106,6 +106,10 @@ sub new {
     $self->{username} = $args->{username};
   }
 
+  if ($args->{max_size}) {
+    $self->{max_size} = $args->{max_size};
+  }
+
   if ($args->{timeout}) {
     $self->{timeout} = $args->{timeout} || 30;
   }
@@ -135,6 +139,8 @@ threshold
 
 message
 
+report
+
 =cut
 
 sub process {
@@ -150,6 +156,41 @@ sub process {
   return $self->_filter($msg, $command);
 }
 
+=head2 spam_report
+
+public instance (\%) spam_report (String $msg)
+
+Description:
+The method implements the report call.
+
+See the process method for the return value.
+
+=cut
+
+sub spam_report {
+  my ($self, $msg) = @_;
+
+  return $self->_filter($msg, 'REPORT');
+}
+
+=head2 spam_report_ifspam
+
+public instance (\%) spam_report_ifspam (String $msg)
+
+Description:
+The method implements the report_ifspam call.
+A report will be returned only if the message is spam.
+
+See the process method for the return value.
+
+=cut
+
+sub spam_report_ifspam {
+  my ($self, $msg) = @_;
+
+  return $self->_filter($msg, 'REPORT_IFSPAM');
+}
+
 =head2 check
 
 public instance (\%) check (String $msg)
@@ -270,10 +311,10 @@ sub learn {
   close $remote  or die "error closing socket: $!";
 
   if ($learntype == 0 || $learntype == 1) {
-    return $did_set =~ /local/;
+    return index($did_set, 'local') >= 0;
   }
   else { #safe since we've already checked the $learntype values
-    return $did_remove =~ /local/;
+    return index($did_remove, 'local') >= 0;
   }
 }
 
@@ -534,12 +575,15 @@ threshold
 
 message (if available)
 
+report (if available)
+
 =cut
 
 sub _filter {
   my ($self, $msg, $command) = @_;
 
   my %data;
+  my $msgsize;
 
   $self->_clear_errors();
 
@@ -547,7 +591,10 @@ sub _filter {
 
   return 0 unless ($remote);
 
-  my $msgsize = length($msg.$EOL);
+  if(defined $self->{max_size}) {
+    $msg = substr($msg,0,$self->{max_size});
+  }
+  $msgsize = length($msg.$EOL);
 
   print $remote "$command $PROTOVERSION$EOL";
   print $remote "Content-length: $msgsize$EOL";
@@ -595,7 +642,11 @@ sub _filter {
     $!==EBADF ? dbg("error reading from spamd (10): $!")
               : die "error reading from spamd (10): $!";
 
-  $data{message} = $return_msg if ($return_msg);
+  if($command =~ /^REPORT/) {
+    $data{report} = $return_msg if ($return_msg);
+  } else {
+    $data{message} = $return_msg if ($return_msg);
+  }
 
   close $remote  or die "error closing socket: $!";
 
index 3948f46750601e1291071950f95012ccd26331e5..9cea74a52ce077578aa8d1deb1271e0348dab39b 100644 (file)
@@ -46,7 +46,7 @@ directories.
 The following web page lists the most important configuration settings
 used to configure SpamAssassin; novices are encouraged to read it first:
 
-  http://wiki.apache.org/spamassassin/ImportantInitialConfigItems
+  https://wiki.apache.org/spamassassin/ImportantInitialConfigItems
 
 =head1 FILE FORMAT
 
@@ -89,7 +89,7 @@ use Mail::SpamAssassin::NetSet;
 use Mail::SpamAssassin::Constants qw(:sa :ip);
 use Mail::SpamAssassin::Conf::Parser;
 use Mail::SpamAssassin::Logger;
-use Mail::SpamAssassin::Util qw(untaint_var compile_regexp);
+use Mail::SpamAssassin::Util qw(untaint_var idn_to_ascii compile_regexp);
 use File::Spec;
 
 our @ISA = qw();
@@ -117,6 +117,23 @@ my @rule_types = ("body_tests", "uri_tests", "uri_evals",
                   "full_evals", "rawbody_tests", "rawbody_evals",
                  "rbl_evals", "meta_tests");
 
+# Map internal ruletype to descriptive ruletype string
+our %TYPE_AS_STRING = (
+  $TYPE_HEAD_TESTS => 'header',
+  $TYPE_HEAD_EVALS => 'header',
+  $TYPE_BODY_TESTS => 'body',
+  $TYPE_BODY_EVALS => 'body',
+  $TYPE_FULL_TESTS => 'full',
+  $TYPE_FULL_EVALS => 'full',
+  $TYPE_RAWBODY_TESTS => 'rawbody',
+  $TYPE_RAWBODY_EVALS => 'rawbody',
+  $TYPE_URI_TESTS => 'uri',
+  $TYPE_URI_EVALS => 'uri',
+  $TYPE_META_TESTS => 'meta',
+  $TYPE_RBL_EVALS => 'header',
+  $TYPE_EMPTY_TESTS => 'empty',
+);
+
 #Removed $VERSION per BUG 6422
 #$VERSION = 'bogus';     # avoid CPAN.pm picking up version strings later
 
@@ -218,7 +235,6 @@ it from running.
 
   push (@cmds, {
     setting => 'score',
-    is_frequent => 1,
     code => sub {
       my ($self, $key, $value, $line) = @_;
       my($rule, @scores) = split(/\s+/, $value);
@@ -268,30 +284,32 @@ it from running.
 
 =back
 
-=head2 WHITELIST AND BLACKLIST OPTIONS
+=head2 WELCOMELIST AND BLOCKLIST OPTIONS
 
 =over 4
 
-=item whitelist_from user@example.com
+=item welcomelist_from user@example.com
 
-Used to whitelist sender addresses which send mail that is often tagged
+Previously whitelist_from which will work interchangeably until 4.1.
+
+Used to welcomelist sender addresses which send mail that is often tagged
 (incorrectly) as spam.
 
 Use of this setting is not recommended, since it blindly trusts the message,
 which is routinely and easily forged by spammers and phish senders. The
-recommended solution is to instead use C<whitelist_auth> or other authenticated
-whitelisting methods, or C<whitelist_from_rcvd>.
+recommended solution is to instead use C<welcomelist_auth> or other authenticated
+welcomelisting methods, or C<welcomelist_from_rcvd>.
 
-Whitelist and blacklist addresses are now file-glob-style patterns, so
+Welcomelist and blocklist addresses are now file-glob-style patterns, so
 C<friend@somewhere.com>, C<*@isp.com>, or C<*.domain.net> will all work.
 Specifically, C<*> and C<?> are allowed, but all other metacharacters
 are not. Regular expressions are not used for security reasons.
 Matching is case-insensitive.
 
 Multiple addresses per line, separated by spaces, is OK.  Multiple
-C<whitelist_from> lines are also OK.
+C<welcomelist_from> lines are also OK.
 
-The headers checked for whitelist addresses are as follows: if C<Resent-From>
+The headers checked for welcomelist addresses are as follows: if C<Resent-From>
 is set, use that; otherwise check all addresses taken from the following
 set of headers:
 
@@ -305,45 +323,51 @@ where this is available, is looked up.  See C<envelope_sender_header>.
 
 e.g.
 
-  whitelist_from joe@example.com fred@example.com
-  whitelist_from *@example.com
+  welcomelist_from joe@example.com fred@example.com
+  welcomelist_from *@example.com
 
 =cut
 
   push (@cmds, {
-    setting => 'whitelist_from',
+    setting => 'welcomelist_from',
+    aliases => ['whitelist_from'], # backward compatible - to be removed for 4.1
     type => $CONF_TYPE_ADDRLIST,
   });
 
-=item unwhitelist_from user@example.com
+=item unwelcomelist_from user@example.com
+
+Previously unwelcomelist_from which will work interchangeably until 4.1.
 
-Used to remove a default whitelist_from entry, so for example a distribution
-whitelist_from can be overridden in a local.cf file, or an individual user can
-override a whitelist_from entry in their own C<user_prefs> file.
+Used to remove a default welcomelist_from entry, so for example a distribution
+welcomelist_from can be overridden in a local.cf file, or an individual user can
+override a welcomelist_from entry in their own C<user_prefs> file.
 The specified email address has to match exactly (although case-insensitively)
-the address previously used in a whitelist_from line, which implies that a
+the address previously used in a welcomelist_from line, which implies that a
 wildcard only matches literally the same wildcard (not 'any' address).
 
 e.g.
 
-  unwhitelist_from joe@example.com fred@example.com
-  unwhitelist_from *@example.com
+  unwelcomelist_from joe@example.com fred@example.com
+  unwelcomelist_from *@example.com
 
 =cut
 
   push (@cmds, {
-    command => 'unwhitelist_from',
-    setting => 'whitelist_from',
+    command => 'unwelcomelist_from',
+    aliases => ['unwhitelist_from'], # backward compatible - to be removed for 4.1
+    setting => 'welcomelist_from',
     type => $CONF_TYPE_ADDRLIST,
     code => \&Mail::SpamAssassin::Conf::Parser::remove_addrlist_value
   });
 
-=item whitelist_from_rcvd addr@lists.sourceforge.net sourceforge.net
+=item welcomelist_from_rcvd addr@lists.sourceforge.net sourceforge.net
 
-Works similarly to whitelist_from, except that in addition to matching
+Previously whitelist_from_rcvd which will work interchangeably until 4.1.
+
+Works similarly to welcomelist_from, except that in addition to matching
 a sender address, a relay's rDNS name or its IP address must match too
-for the whitelisting rule to fire. The first parameter is a sender's e-mail
-address to whitelist, and the second is a string to match the relay's rDNS,
+for the welcomelisting rule to fire. The first parameter is a sender's e-mail
+address to welcomelist, and the second is a string to match the relay's rDNS,
 or its IP address. Matching is case-insensitive.
 
 This second parameter is matched against a TCP-info information field as
@@ -375,24 +399,27 @@ result in the generated Received header field according to RFC 5321.
 
 e.g.
 
-  whitelist_from_rcvd joe@example.com  example.com
-  whitelist_from_rcvd *@*              mail.example.org
-  whitelist_from_rcvd *@axkit.org      [192.0.2.123]
-  whitelist_from_rcvd *@axkit.org      [192.0.2.0/24]
-  whitelist_from_rcvd *@axkit.org      [192.0.2.0]/24
-  whitelist_from_rcvd *@axkit.org      [2001:db8:1234::/48]
-  whitelist_from_rcvd *@axkit.org      [2001:db8:1234::]/48
+  welcomelist_from_rcvd joe@example.com  example.com
+  welcomelist_from_rcvd *@*              mail.example.org
+  welcomelist_from_rcvd *@axkit.org      [192.0.2.123]
+  welcomelist_from_rcvd *@axkit.org      [192.0.2.0/24]
+  welcomelist_from_rcvd *@axkit.org      [192.0.2.0]/24
+  welcomelist_from_rcvd *@axkit.org      [2001:db8:1234::/48]
+  welcomelist_from_rcvd *@axkit.org      [2001:db8:1234::]/48
+
+=item def_welcomelist_from_rcvd addr@lists.sourceforge.net sourceforge.net
 
-=item def_whitelist_from_rcvd addr@lists.sourceforge.net sourceforge.net
+Previously def_whitelist_from_rcvd which will work interchangeably until 4.1.
 
-Same as C<whitelist_from_rcvd>, but used for the default whitelist entries
-in the SpamAssassin distribution.  The whitelist score is lower, because
+Same as C<welcomelist_from_rcvd>, but used for the default welcomelist entries
+in the SpamAssassin distribution.  The welcomelist score is lower, because
 these are often targets for spammer spoofing.
 
 =cut
 
   push (@cmds, {
-    setting => 'whitelist_from_rcvd',
+    setting => 'welcomelist_from_rcvd',
+    aliases => ['whitelist_from_rcvd'], # backward compatible - to be removed for 4.1
     type => $CONF_TYPE_ADDRLIST,
     code => sub {
       my ($self, $key, $value, $line) = @_;
@@ -402,13 +429,14 @@ these are often targets for spammer spoofing.
       unless ($value =~ /^\S+\s+\S+$/) {
        return $INVALID_VALUE;
       }
-      $self->{parser}->add_to_addrlist_rcvd ('whitelist_from_rcvd',
+      $self->{parser}->add_to_addrlist_rcvd ('welcomelist_from_rcvd',
                                         split(/\s+/, $value));
     }
   });
 
   push (@cmds, {
-    setting => 'def_whitelist_from_rcvd',
+    setting => 'def_welcomelist_from_rcvd',
+    aliases => ['def_whitelist_from_rcvd'], # backward compatible - to be removed for 4.1
     type => $CONF_TYPE_ADDRLIST,
     code => sub {
       my ($self, $key, $value, $line) = @_;
@@ -418,63 +446,69 @@ these are often targets for spammer spoofing.
       unless ($value =~ /^\S+\s+\S+$/) {
        return $INVALID_VALUE;
       }
-      $self->{parser}->add_to_addrlist_rcvd ('def_whitelist_from_rcvd',
+      $self->{parser}->add_to_addrlist_rcvd ('def_welcomelist_from_rcvd',
                                         split(/\s+/, $value));
     }
   });
 
-=item whitelist_allows_relays user@example.com
+=item welcomelist_allows_relays user@example.com
 
-Specify addresses which are in C<whitelist_from_rcvd> that sometimes
+Previously whitelist_allows_relays which will work interchangeably until 4.1.
+
+Specify addresses which are in C<welcomelist_from_rcvd> that sometimes
 send through a mail relay other than the listed ones. By default mail
-with a From address that is in C<whitelist_from_rcvd> that does not match
+with a From address that is in C<welcomelist_from_rcvd> that does not match
 the relay will trigger a forgery rule. Including the address in
-C<whitelist_allows_relay> prevents that.
+C<welcomelist_allows_relay> prevents that.
 
-Whitelist and blacklist addresses are now file-glob-style patterns, so
+Welcomelist and blocklist addresses are now file-glob-style patterns, so
 C<friend@somewhere.com>, C<*@isp.com>, or C<*.domain.net> will all work.
 Specifically, C<*> and C<?> are allowed, but all other metacharacters
 are not. Regular expressions are not used for security reasons.
 Matching is case-insensitive.
 
 Multiple addresses per line, separated by spaces, is OK.  Multiple
-C<whitelist_allows_relays> lines are also OK.
+C<welcomelist_allows_relays> lines are also OK.
 
 The specified email address does not have to match exactly the address
-previously used in a whitelist_from_rcvd line as it is compared to the
+previously used in a welcomelist_from_rcvd line as it is compared to the
 address in the header.
 
 e.g.
 
-  whitelist_allows_relays joe@example.com fred@example.com
-  whitelist_allows_relays *@example.com
+  welcomelist_allows_relays joe@example.com fred@example.com
+  welcomelist_allows_relays *@example.com
 
 =cut
 
   push (@cmds, {
-    setting => 'whitelist_allows_relays',
+    setting => 'welcomelist_allows_relays',
+    aliases => ['whitelist_allows_relays'], # backward compatible - to be removed for 4.1
     type => $CONF_TYPE_ADDRLIST,
   });
 
-=item unwhitelist_from_rcvd user@example.com
+=item unwelcomelist_from_rcvd user@example.com
+
+Previously unwhitelist_from_rcvd which will work interchangeably until 4.1.
 
-Used to remove a default whitelist_from_rcvd or def_whitelist_from_rcvd
-entry, so for example a distribution whitelist_from_rcvd can be overridden
-in a local.cf file, or an individual user can override a whitelist_from_rcvd
+Used to remove a default welcomelist_from_rcvd or def_welcomelist_from_rcvd
+entry, so for example a distribution welcomelist_from_rcvd can be overridden
+in a local.cf file, or an individual user can override a welcomelist_from_rcvd
 entry in their own C<user_prefs> file.
 
 The specified email address has to match exactly the address previously
-used in a whitelist_from_rcvd line.
+used in a welcomelist_from_rcvd line.
 
 e.g.
 
-  unwhitelist_from_rcvd joe@example.com fred@example.com
-  unwhitelist_from_rcvd *@axkit.org
+  unwelcomelist_from_rcvd joe@example.com fred@example.com
+  unwelcomelist_from_rcvd *@axkit.org
 
 =cut
 
   push (@cmds, {
-    setting => 'unwhitelist_from_rcvd',
+    setting => 'unwelcomelist_from_rcvd',
+    aliases => ['unwhitelist_from_rcvd'], # backward compatible - to be removed for 4.1
     type => $CONF_TYPE_ADDRLIST,
     code => sub {
       my ($self, $key, $value, $line) = @_;
@@ -484,63 +518,69 @@ e.g.
       unless ($value =~ /^(?:\S+(?:\s+\S+)*)$/) {
        return $INVALID_VALUE;
       }
-      $self->{parser}->remove_from_addrlist_rcvd('whitelist_from_rcvd',
+      $self->{parser}->remove_from_addrlist_rcvd('welcomelist_from_rcvd',
                                         split (/\s+/, $value));
-      $self->{parser}->remove_from_addrlist_rcvd('def_whitelist_from_rcvd',
+      $self->{parser}->remove_from_addrlist_rcvd('def_welcomelist_from_rcvd',
                                         split (/\s+/, $value));
     }
   });
 
-=item blacklist_from user@example.com
+=item blocklist_from user@example.com
 
 Used to specify addresses which send mail that is often tagged (incorrectly) as
-non-spam, but which the user doesn't want.  Same format as C<whitelist_from>.
+non-spam, but which the user doesn't want.  Same format as C<welcomelist_from>.
 
 =cut
 
   push (@cmds, {
-    setting => 'blacklist_from',
+    setting => 'blocklist_from',
+    aliases => ['blacklist_from'], # backward compatible - to be removed for 4.1
     type => $CONF_TYPE_ADDRLIST,
   });
 
-=item unblacklist_from user@example.com
+=item unblocklist_from user@example.com
 
-Used to remove a default blacklist_from entry, so for example a
-distribution blacklist_from can be overridden in a local.cf file, or
-an individual user can override a blacklist_from entry in their own
+Previously unblacklist_from which will work interchangeably until 4.1.
+
+Used to remove a default blocklist_from entry, so for example a
+distribution blocklist_from can be overridden in a local.cf file, or
+an individual user can override a blocklist_from entry in their own
 C<user_prefs> file. The specified email address has to match exactly
-the address previously used in a blacklist_from line.
+the address previously used in a blocklist_from line.
 
 
 e.g.
 
-  unblacklist_from joe@example.com fred@example.com
-  unblacklist_from *@spammer.com
+  unblocklist_from joe@example.com fred@example.com
+  unblocklist_from *@spammer.com
 
 =cut
 
 
   push (@cmds, {
-    command => 'unblacklist_from',
-    setting => 'blacklist_from',
+    command => 'unblocklist_from',
+    aliases => ['unblacklist_from'], # backward compatible - to be removed for 4.1
+    setting => 'blocklist_from',
     type => $CONF_TYPE_ADDRLIST,
     code => \&Mail::SpamAssassin::Conf::Parser::remove_addrlist_value
   });
 
 
-=item whitelist_to user@example.com
+=item welcomelist_to user@example.com
+
+Previously whitelist_to which will work interchangeably until 4.1.
 
 If the given address appears as a recipient in the message headers
 (Resent-To, To, Cc, obvious envelope recipient, etc.) the mail will
-be whitelisted.  Useful if you're deploying SpamAssassin system-wide,
+be listed as allowed.  Useful if you're deploying SpamAssassin system-wide,
 and don't want some users to have their mail filtered.  Same format
-as C<whitelist_from>.
+as C<welcomelist_from>.
 
-There are three levels of To-whitelisting, C<whitelist_to>, C<more_spam_to>
+There are three levels of To-welcomelisting, C<welcomelist_to>, C<more_spam_to>
 and C<all_spam_to>.  Users in the first level may still get some spammish
 mails blocked, but users in C<all_spam_to> should never get mail blocked.
 
-The headers checked for whitelist addresses are as follows: if C<Resent-To> or
+The headers checked for welcomelist addresses are as follows: if C<Resent-To> or
 C<Resent-Cc> are set, use those; otherwise check all addresses taken from the
 following set of headers:
 
@@ -568,7 +608,8 @@ See above.
 =cut
 
   push (@cmds, {
-    setting => 'whitelist_to',
+    setting => 'welcomelist_to',
+    aliases => ['whitelist_to'], # backward compatible - to be removed for 4.1
     type => $CONF_TYPE_ADDRLIST,
   });
   push (@cmds, {
@@ -580,72 +621,84 @@ See above.
     type => $CONF_TYPE_ADDRLIST,
   });
 
-=item blacklist_to user@example.com
+=item blocklist_to user@example.com
+
+Previously blacklist_auth which will work interchangeably until 4.1.
 
 If the given address appears as a recipient in the message headers
 (Resent-To, To, Cc, obvious envelope recipient, etc.) the mail will
-be blacklisted.  Same format as C<blacklist_from>.
+be blocklisted.  Same format as C<blocklist_from>.
 
 =cut
 
   push (@cmds, {
-    setting => 'blacklist_to',
+    setting => 'blocklist_to',
+    aliases => ['blacklist_to'], # backward compatible - to be removed for 4.1
     type => $CONF_TYPE_ADDRLIST,
   });
 
-=item whitelist_auth user@example.com
+=item welcomelist_auth user@example.com
+
+Previously whitelist_auth which will work interchangeably until 4.1.
 
 Used to specify addresses which send mail that is often tagged (incorrectly) as
-spam.  This is different from C<whitelist_from> and C<whitelist_from_rcvd> in
+spam.  This is different from C<welcomelist_from> and C<welcomelist_from_rcvd> in
 that it first verifies that the message was sent by an authorized sender for
-the address, before whitelisting.
+the address, before welcomelisting.
 
 Authorization is performed using one of the installed sender-authorization
 schemes: SPF (using C<Mail::SpamAssassin::Plugin::SPF>), or DKIM (using
 C<Mail::SpamAssassin::Plugin::DKIM>).  Note that those plugins must be active,
 and working, for this to operate.
 
-Using C<whitelist_auth> is roughly equivalent to specifying duplicate
-C<whitelist_from_spf>, C<whitelist_from_dk>, and C<whitelist_from_dkim> lines
+Using C<welcomelist_auth> is roughly equivalent to specifying duplicate
+C<welcomelist_from_spf>, C<welcomelist_from_dk>, and C<welcomelist_from_dkim> lines
 for each of the addresses specified.
 
 e.g.
 
-  whitelist_auth joe@example.com fred@example.com
-  whitelist_auth *@example.com
+  welcomelist_auth joe@example.com fred@example.com
+  welcomelist_auth *@example.com
 
-=item def_whitelist_auth user@example.com
+=item def_welcomelist_auth user@example.com
 
-Same as C<whitelist_auth>, but used for the default whitelist entries
-in the SpamAssassin distribution.  The whitelist score is lower, because
+Previously def_whitelist_auth which will work interchangeably until 4.1.
+
+Same as C<welcomelist_auth>, but used for the default welcomelist entries
+in the SpamAssassin distribution.  The welcomelist score is lower, because
 these are often targets for spammer spoofing.
 
 =cut
 
   push (@cmds, {
-    setting => 'whitelist_auth',
+    setting => 'welcomelist_auth',
+    aliases => ['whitelist_auth'], # backward compatible - to be removed for 4.1
     type => $CONF_TYPE_ADDRLIST,
   });
 
   push (@cmds, {
-    setting => 'def_whitelist_auth',
+    setting => 'def_welcomelist_auth',
+    aliases => ['def_whitelist_auth'], # backward compatible - to be removed for 4.1
     type => $CONF_TYPE_ADDRLIST,
   });
 
-=item unwhitelist_auth user@example.com
+=item unwelcomelist_auth user@example.com
+
+Previously unwhitelist_auth which will work interchangeably until 4.1.
 
-Used to remove a C<whitelist_auth> or C<def_whitelist_auth> entry. The
+Used to remove a C<welcomelist_auth> or C<def_welcomelist_auth> entry. The
 specified email address has to match exactly the address previously used.
 
 e.g.
 
-  unwhitelist_auth joe@example.com fred@example.com
-  unwhitelist_auth *@example.com
+  unwelcomelist_auth joe@example.com fred@example.com
+  unwelcomelist_auth *@example.com
 
 =cut
 
   push (@cmds, {
-    setting => 'unwhitelist_auth',
+    setting => 'unwelcomelist_auth',
+    aliases => ['unwhitelist_auth'], # backward compatible - to be removed for 4.1
     type => $CONF_TYPE_ADDRLIST,
     code => sub {
       my ($self, $key, $value, $line) = @_;
@@ -655,9 +708,9 @@ e.g.
       unless ($value =~ /^(?:\S+(?:\s+\S+)*)$/) {
         return $INVALID_VALUE;
       }
-      $self->{parser}->remove_from_addrlist('whitelist_auth',
+      $self->{parser}->remove_from_addrlist('welcomelist_auth',
                                         split (/\s+/, $value));
-      $self->{parser}->remove_from_addrlist('def_whitelist_auth',
+      $self->{parser}->remove_from_addrlist('def_welcomelist_auth',
                                         split (/\s+/, $value));
     }
   });
@@ -690,16 +743,16 @@ in order to match.
 Use the delist_uri_host directive to neutralize previous enlist_uri_host
 settings.
 
-Enlisting to lists named 'BLACK' and 'WHITE' have their shorthand directives
-blacklist_uri_host and whitelist_uri_host and corresponding default rules,
-but the names 'BLACK' and 'WHITE' are otherwise not special or reserved.
+Enlisting to lists named 'BLOCK' and 'WELCOME' have their shorthand directives
+blocklist_uri_host and welcomelist_uri_host and corresponding default rules,
+but the names 'BLOCK' and 'WELCOME' are otherwise not special or reserved.
 
 =cut
 
   push (@cmds, {
     command => 'enlist_uri_host',
     setting => 'uri_host_lists',
-    type => $CONF_TYPE_ADDRLIST,
+    type => $CONF_TYPE_HASH_KEY_VALUE,
     code => sub {
       my($conf, $key, $value, $line) = @_;
       local($1,$2);
@@ -737,7 +790,7 @@ name and has no meaning here.
   push (@cmds, {
     command => 'delist_uri_host',
     setting => 'uri_host_lists',
-    type => $CONF_TYPE_ADDRLIST,
+    type => $CONF_TYPE_HASH_KEY_VALUE,
     code => sub {
       my($conf, $key, $value, $line) = @_;
       local($1,$2);
@@ -772,17 +825,17 @@ Matching is case-insensitive.
 Multiple addresses per line, separated by spaces, is OK.  Multiple
 C<enlist_addrlist> lines are also OK.
 
-Enlisting an address to the list named blacklist_to is synonymous to using the
-directive blacklist_to 
+Enlisting an address to the list named blocklist_to is synonymous to using
+the directive blocklist_to.
 
-Enlisting an address to the list named blacklist_from is synonymous to using the
-directive blacklist_from
+Enlisting an address to the list named blocklist_from is synonymous to using
+the directive blocklist_from.
 
-Enlisting an address to the list named whitelist_to is synonymous to using the
-directive whitelist_to 
+Enlisting an address to the list named welcomelist_to is synonymous to using
+the directive welcomelist_to.
 
-Enlisting an address to the list named whitelist_from is synonymous to using the
-directive whitelist_from
+Enlisting an address to the list named welcomelist_from is synonymous to
+using the directive welcomelist_from.
 
 e.g.
 
@@ -803,48 +856,54 @@ e.g.
       my $listname = $1;  # corresponds to arg in check_uri_host_in_wblist()
       # note: must not factor out dereferencing, as otherwise
       # subhashes would spring up in a copy and be lost
-      $conf->{parser}->add_to_addrlist ($listname, split(/\s+/, $value));
+      $conf->{parser}->add_to_addrlist ($listname, split(/\s+/, $2));
     }
   });
 
-=item blacklist_uri_host host-or-domain ...
+=item blocklist_uri_host host-or-domain ...
 
-Is a shorthand for a directive:  enlist_uri_host (BLACK) host ...
+Previously blacklist_uri_host which will work interchangeably until 4.1.
+
+Is a shorthand for a directive:  enlist_uri_host (BLOCK) host ...
 
 Please see directives enlist_uri_host and delist_uri_host for details.
 
 =cut
 
   push (@cmds, {
-    command => 'blacklist_uri_host',
+    command => 'blocklist_uri_host',
+    aliases => ['blacklist_uri_host'], # backward compatible - to be removed for 4.1
     setting => 'uri_host_lists',
-    type => $CONF_TYPE_ADDRLIST,
+    type => $CONF_TYPE_HASH_KEY_VALUE,
     code => sub {
       my($conf, $key, $value, $line) = @_;
       foreach my $host ( split(/\s+/, lc $value) ) {
         my $v = $host =~ s/^!// ? 0 : 1;
-        $conf->{uri_host_lists}{'BLACK'}{$host} = $v;
+        $conf->{uri_host_lists}{'BLOCK'}{$host} = $v;
       }
     }
   });
 
-=item whitelist_uri_host host-or-domain ...
+=item welcomelist_uri_host host-or-domain ...
+
+Previously whitelist_uri_host which will work interchangeably until 4.1.
 
-Is a shorthand for a directive:  enlist_uri_host (BLACK) host ...
+Is a shorthand for a directive:  enlist_uri_host (WELCOME) host ...
 
 Please see directives enlist_uri_host and delist_uri_host for details.
 
 =cut
 
   push (@cmds, {
-    command => 'whitelist_uri_host',
+    command => 'welcomelist_uri_host',
+    aliases => ['whitelist_uri_host'], # backward compatible - to be removed for 4.1
     setting => 'uri_host_lists',
-    type => $CONF_TYPE_ADDRLIST,
+    type => $CONF_TYPE_HASH_KEY_VALUE,
     code => sub {
       my($conf, $key, $value, $line) = @_;
       foreach my $host ( split(/\s+/, lc $value) ) {
         my $v = $host =~ s/^!// ? 0 : 1;
-        $conf->{uri_host_lists}{'WHITE'}{$host} = $v;
+        $conf->{uri_host_lists}{'WELCOME'}{$host} = $v;
       }
     }
   });
@@ -927,14 +986,14 @@ rewrites the email content.
 
 Here is an example on how to use this feature:
 
-        rewrite_header Subject *****SPAM*****
-        add_header all Subjprefix _SUBJPREFIX_
-        body     OLEMACRO_MALICE eval:check_olemacro_malice()
-        describe OLEMACRO_MALICE Dangerous Office Macro
-        score    OLEMACRO_MALICE 5.0
-        if can(Mail::SpamAssassin::Conf::feature_subjprefix)
-          subjprefix OLEMACRO_MALICE [VIRUS]
-        endif
+       rewrite_header Subject *****SPAM*****
+       add_header all Subjprefix _SUBJPREFIX_
+       body     OLEMACRO_MALICE eval:check_olemacro_malice()
+       describe OLEMACRO_MALICE Dangerous Office Macro
+       score    OLEMACRO_MALICE 5.0
+       if can(Mail::SpamAssassin::Conf::feature_subjprefix)
+         subjprefix OLEMACRO_MALICE [VIRUS]
+       endif
 
 =cut
 
@@ -1141,16 +1200,16 @@ the original mail into tagged messages.
     }
   });
 
-=item report_wrap_width (default: 70
+=item report_wrap_width (default: 75
 
-This option sets the wrap width for description lines in the X-Spam-Report 
+This option sets the wrap width for description lines in the X-Spam-Report
 header, not accounting for tab width. 
 
 =cut
 
   push (@cmds, {
     setting => 'report_wrap_width',
-    default => '70',
+    default => '75',
     type => $CONF_TYPE_NUMERIC,
   });
 
@@ -1210,7 +1269,7 @@ Select the locales to allow from the list below:
     type => $CONF_TYPE_STRING,
   });
 
-=item normalize_charset ( 0 | 1)        (default: 0)
+=item normalize_charset ( 0 | 1 )        (default: 1)
 
 Whether to decode non- UTF-8 and non-ASCII textual parts and recode them
 to UTF-8 before the text is given over to rules processing. The character
@@ -1232,7 +1291,7 @@ it will be used if it is available.
 
   push (@cmds, {
     setting => 'normalize_charset',
-    default => 0,
+    default => 1,
     type => $CONF_TYPE_BOOL,
     code => sub {
        my ($self, $key, $value, $line) = @_;
@@ -1257,11 +1316,6 @@ it will be used if it is available.
            $self->{normalize_charset} = 0;
            return $INVALID_VALUE;
        }
-       unless (eval 'require Encode') {
-           $self->{parser}->lint_warn("config: normalize_charset requires Encode");
-           $self->{normalize_charset} = 0;
-           return $INVALID_VALUE;
-       }
     }
   });
 
@@ -1277,9 +1331,9 @@ What networks or hosts are 'trusted' in your setup.  B<Trusted> in this case
 means that relay hosts on these networks are considered to not be potentially
 operated by spammers, open relays, or open proxies.  A trusted host could
 conceivably relay spam, but will not originate it, and will not forge header
-data. DNS blacklist checks will never query for hosts on these networks. 
+data. DNS blocklist checks will never query for hosts on these networks. 
 
-See C<http://wiki.apache.org/spamassassin/TrustPath> for more information.
+See C<https://wiki.apache.org/spamassassin/TrustPath> for more information.
 
 MXes for your domain(s) and internal relays should B<also> be specified using
 the C<internal_networks> setting. When there are 'trusted' hosts that
@@ -1299,6 +1353,9 @@ If masklen is not specified, and there is not trailing dot, then just a single
 IP address specified is used, as if the masklen were C</32> with an IPv4
 address, or C</128> in case of an IPv6 address.
 
+If module Net::CIDR::Lite is installed, it's also possible to use dash
+separated IP range format (e.g. 192.168.1.1-192.168.255.255).
+
 If a network or host address is prefaced by a C<!> the matching network or
 host will be excluded from the list even if a less specific (shorter netmask
 length) subnet is later specified in the list. This allows a subset of
@@ -1552,7 +1609,7 @@ more trusted relays.  See also C<envelope_sender_header>.
 =item skip_rbl_checks ( 0 | 1 )   (default: 0)
 
 Turning on the skip_rbl_checks setting will disable the DNSEval plugin,
-which implements Real-time Block List (or: Blackhole List) (RBL) lookups.
+which implements Real-time Block List (or: Blockhole List) (RBL) lookups.
 
 By default, SpamAssassin will run RBL checks. Individual blocklists may
 be disabled selectively by setting a score of a corresponding rule to 0.
@@ -1661,8 +1718,7 @@ documentation for details.
       }
       my $scope = '';  # scoped IP address?
       $scope = $1  if $address =~ s/ ( % [A-Z0-9._~-]* ) \z//xsi;
-      my $IP_ADDRESS = IP_ADDRESS;  # IP_ADDRESS regexp does not handle scope
-      if ($address =~ /$IP_ADDRESS/ && $port >= 1 && $port <= 65535) {
+      if ($address =~ IS_IP_ADDRESS && $port >= 1 && $port <= 65535) {
         $self->{dns_servers} = []  if !$self->{dns_servers};
         # checked, untainted, stored in a normalized form
         push(@{$self->{dns_servers}}, untaint_var("[$address$scope]:$port"));
@@ -1831,16 +1887,22 @@ indicating seconds (default), minutes, hours, days, weeks).
     type => $CONF_TYPE_DURATION,
   });
 
-=item dns_options opts   (default: norotate, nodns0x20, edns=4096)
+=item dns_options opts   (default: v4, v6, norotate, nodns0x20, edns=4096)
 
-Provides a (whitespace or comma -separated) list of options applying
-to DNS resolving. Available options are: I<rotate>, I<dns0x20> and
-I<edns> (or I<edns0>). Option name may be negated by prepending a I<no>
-(e.g. I<norotate>, I<NoEDNS>) to counteract a previously enabled option.
-Option names are not case-sensitive. The I<dns_options> directive may
+Provides a (whitespace or comma -separated) list of options applying to DNS
+resolving.  Available options are: I<v4>, I<v6>, I<rotate>, I<dns0x20> and
+I<edns> (or I<edns0>).  Option name may be negated by prepending a I<no>
+(e.g.  I<norotate>, I<NoEDNS>) to counteract a previously enabled option. 
+Option names are not case-sensitive.  The I<dns_options> directive may
 appear in configuration files multiple times, the last setting prevails.
 
-Option I<edns> (or I<edsn0>) may take a value which specifies a requestor's
+Option I<v4> declares resolver capable of returning IPv4 (A) records. 
+Option I<v6> declares resolver capable of returning IPv6 (AAAA) records. 
+One would set I<nov6> if the resolver is filtering AAAA responses.  NOTE:
+these options only refer to I<resolving capabilies>, there is no other
+meaning like whether the IP address of resolver itself is IPv4 or IPv6.
+
+Option I<edns> (or I<edns0>) may take a value which specifies a requestor's
 acceptable UDP payload size according to EDNS0 specifications (RFC 6891,
 ex RFC 2671) e.g. I<edns=4096>. When EDNS0 is off (I<noedns> or I<edns=512>)
 a traditional implied UDP payload size is 512 bytes, which is also a minimum
@@ -1881,15 +1943,19 @@ do not work for no apparent reason.
   push (@cmds, {
     setting => 'dns_options',
     type => $CONF_TYPE_HASH_KEY_VALUE,
+    # RFC 6891: A good compromise may be the use of an EDNS maximum payload size
+    # of 4096 octets as a starting point.
+    default => { 'v4' => 1, 'v6' => 1,
+                 'rotate' => 0, 'dns0x20' => 0, 'edns' => 4096 },
     code => sub {
       my ($self, $key, $value, $line) = @_;
       foreach my $option (split (/[\s,]+/, lc $value)) {
         local($1,$2);
-        if ($option =~ /^no(rotate|dns0x20)\z/) {
+        if ($option =~ /^no(rotate|dns0x20|v4|v6)\z/) {
           $self->{dns_options}->{$1} = 0;
         } elsif ($option =~ /^no(edns)0?\z/) {
           $self->{dns_options}->{$1} = 0;
-        } elsif ($option =~ /^(rotate|dns0x20)\z/) {
+        } elsif ($option =~ /^(rotate|dns0x20|v4|v6)\z/) {
           $self->{dns_options}->{$1} = 1;
         } elsif ($option =~ /^(edns)0? (?: = (\d+) )? \z/x) {
           # RFC 6891 (ex RFC 2671) - EDNS0, value is a requestor's UDP payload
@@ -1939,6 +2005,9 @@ a score of corresponding DNSBL and URIBL rules to zero, and can be a handy
 alternative to hunting for such rules when a site policy does not allow
 certain DNS block lists to be queried.
 
+Special wildcard "dns_query_restriction deny *" is supported to block all
+queries except allowed ones.
+
 Example:
   dns_query_restriction deny  dnswl.org surbl.org
   dns_query_restriction allow zen.spamhaus.org
@@ -1980,6 +2049,60 @@ options, leaving the list empty, i.e. allowing DNS queries for any domain
     }
   });
 
+=item dns_block_rule RULE domain
+
+If rule named RULE is hit, DNS queries to specified domain are
+I<temporarily> blocked. Intended to be used with rules that check
+RBL return codes for specific blocked status.  For example:
+
+  urirhssub URIBL_BLOCKED multi.uribl.com. A 1
+  dns_block_rule URIBL_BLOCKED multi.uribl.com
+
+Block status is maintained across all processes by empty statefile named
+"dnsblock_multi.uribl.com" in global state dir:
+home_dir_for_helpers/.spamassassin, $HOME/.spamassassin,
+/var/lib/spamassassin (localstate), depending which is found and writable.
+
+=cut
+
+  push (@cmds, {
+    setting => 'dns_block_rule',
+    is_admin => 1,
+    type => $CONF_TYPE_HASH_KEY_VALUE,
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      local($1,$2);
+      defined $value && $value =~ /^(\S+)\s+(.+)$/
+        or return $INVALID_VALUE;
+      my $rule = $1;
+      foreach my $domain (split(/\s+/, lc($2))) {
+        $domain =~ s/^\.//; $domain =~ s/\.\z//;  # strip dots
+        if ($domain !~ /^[a-z0-9.-]+$/) {
+          return $INVALID_VALUE;
+        }
+        # will end up in filename, do not allow / etc in above regex!
+        $domain = untaint_var($domain);
+        # Check.pm check_main() uses this
+        $self->{dns_block_rule}{$rule}{$domain} = 1;
+        # bgsend_and_start_lookup() uses this
+        $self->{dns_block_rule_domains}{$domain} = $domain;
+      }
+    }
+  });
+
+=item dns_block_time   (default: 300)
+
+dns_block_rule query blockage will last this many seconds.
+
+=cut
+
+  push (@cmds, {
+    setting => 'dns_block_time',
+    is_admin => 1,
+    default => 300,
+    type => $CONF_TYPE_NUMERIC,
+  });
+
 =back
 
 =head2 LEARNING OPTIONS
@@ -2123,7 +2246,7 @@ a spam-filtering ISP or mailing list, and that service adds
 new headers (as most of them do), these headers may provide
 inappropriate cues to the Bayesian classifier, allowing it
 to take a "short cut". To avoid this, list the headers using this
-setting.  Example:
+setting. Header matching is case-insensitive.  Example:
 
         bayes_ignore_header X-Upstream-Spamfilter
         bayes_ignore_header X-Upstream-SomethingElse
@@ -2132,14 +2255,15 @@ setting.  Example:
 
   push (@cmds, {
     setting => 'bayes_ignore_header',
-    default => [],
-    type => $CONF_TYPE_STRINGLIST,
+    type => $CONF_TYPE_HASH_KEY_VALUE,
     code => sub {
       my ($self, $key, $value, $line) = @_;
       if ($value eq '') {
         return $MISSING_REQUIRED_VALUE;
       }
-      push (@{$self->{bayes_ignore_headers}}, split(/\s+/, $value));
+      foreach (split(/\s+/, $value)) {
+        $self->{bayes_ignore_header}->{lc $_} = 1;
+      }
     }
   });
 
@@ -2148,7 +2272,7 @@ setting.  Example:
 Bayesian classification and autolearning will not be performed on mail
 from the listed addresses.  Program C<sa-learn> will also ignore the
 listed addresses if it is invoked using the C<--use-ignores> option.
-One or more addresses can be listed, see C<whitelist_from>.
+One or more addresses can be listed, see C<welcomelist_from>.
 
 Spam messages from certain senders may contain many words that
 frequently occur in ham.  For example, one might read messages from a
@@ -2396,7 +2520,7 @@ as implemented by a Shortcircuit plugin. A synthetic hit on a rule named
 TIME_LIMIT_EXCEEDED with a near-zero default score is generated, so that
 the report will reflect the event. A score for TIME_LIMIT_EXCEEDED may
 be provided explicitly in a configuration file, for example to achieve
-whitelisting or blacklisting effect for messages with long processing times.
+welcomelisting or blocklisting effect for messages with long processing times.
 
 The C<time_limit> option is a useful protection against excessive processing
 time on certain degenerate or unusually long or complex mail messages, as well
@@ -2539,7 +2663,7 @@ default heuristics.
 
 (Note for MTA developers: we would prefer if the use of a single header be
 avoided in future, since that precludes 'downstream' spam scanning.
-C<http://wiki.apache.org/spamassassin/EnvelopeSenderInReceived> details a
+C<https://wiki.apache.org/spamassassin/EnvelopeSenderInReceived> details a
 better proposal, storing the envelope sender at each hop in the C<Received>
 header.)
 
@@ -2570,11 +2694,10 @@ length to no more than 50 characters.
   push (@cmds, {
     command => 'describe',
     setting => 'descriptions',
-    is_frequent => 1,
     type => $CONF_TYPE_HASH_KEY_VALUE,
   });
 
-=item report_charset CHARSET           (default: unset)
+=item report_charset CHARSET           (default: UTF-8)
 
 Set the MIME Content-Type charset used for the text/plain report which
 is attached to spam mail messages.
@@ -2583,7 +2706,7 @@ is attached to spam mail messages.
 
   push (@cmds, {
     setting => 'report_charset',
-    default => '',
+    default => 'UTF-8',
     type => $CONF_TYPE_STRING,
   });
 
@@ -2783,18 +2906,15 @@ Example: http://chkpt.zdnet.com/chkpt/whatever/spammer.domain/yo/dude
     type => $CONF_TYPE_STRINGLIST,
     code => sub {
       my ($self, $key, $value, $line) = @_;
-
       $value =~ s/^\s+//;
       if ($value eq '') {
        return $MISSING_REQUIRED_VALUE;
       }
-
       my ($rec, $err) = compile_regexp($value, 1);
       if (!$rec) {
         dbg("config: invalid redirector_pattern '$value': $err");
        return $INVALID_VALUE;
       }
-
       push @{$self->{main}->{conf}->{redirector_patterns}}, $rec;
     }
   });
@@ -2896,7 +3016,7 @@ by newlines.
 C<X-Spam-Relays-Internal> and C<X-Spam-Relays-External> represent a portable,
 pre-parsed representation of the message's network path, as recorded in the
 Received headers, divided into 'trusted' vs 'untrusted' and 'internal' vs
-'external' sets.  See C<http://wiki.apache.org/spamassassin/TrustedRelays> for
+'external' sets.  See C<https://wiki.apache.org/spamassassin/TrustedRelays> for
 more details.
 
 =back
@@ -2944,7 +3064,7 @@ C<arguments> are optional arguments to the function call.
 
 =item header SYMBOLIC_TEST_NAME eval:check_rbl('set', 'zone' [, 'sub-test'])
 
-Check a DNSBL (a DNS blacklist or whitelist).  This will retrieve Received:
+Check a DNSBL (a DNS blocklist or welcomelist).  This will retrieve Received:
 headers from the message, extract the IP addresses, select which ones are
 'untrusted' based on the C<trusted_networks> logic, and query that DNSBL
 zone.  There's a few things to note:
@@ -2955,9 +3075,7 @@ zone.  There's a few things to note:
 
 Duplicated IPs are only queried once and reserved IPs are not queried.
 Private IPs are those listed in
-C<https://www.iana.org/assignments/ipv4-address-space>,
-C<http://duxcw.com/faq/network/privip.htm>,
-C<http://duxcw.com/faq/network/autoip.htm>, or
+C<https://www.iana.org/assignments/ipv4-address-space>, or
 C<https://tools.ietf.org/html/rfc5735> as private.
 
 =item the 'set' argument
@@ -2994,7 +3112,7 @@ sending directly to your MX (mail exchange).
 
 =item selecting IPs by whether they are trusted
 
-When checking a 'nice' DNSBL (a DNS whitelist), you cannot trust the IP
+When checking a 'nice' DNSBL (a DNS welcomelist), you cannot trust the IP
 addresses in Received headers that were not added by trusted relays.  To
 test the first IP address that can be trusted, place '-firsttrusted' at the
 end of the set name.  That should test the IP address of the relay that
@@ -3010,7 +3128,7 @@ IP address from the most recent 'untrusted line', as used in '-firsttrusted'
 above.  That's because we're talking about the trustworthiness of the
 IP address data, not the source header line, here; and in the case of 
 the most recent header (the 'firsttrusted'), that data can be trusted.
-See the Wiki page at C<http://wiki.apache.org/spamassassin/TrustedRelays>
+See the Wiki page at C<https://wiki.apache.org/spamassassin/TrustedRelays>
 for more information on this.
 
 =item Selecting just the last external IP
@@ -3044,7 +3162,6 @@ name.
 
   push (@cmds, {
     setting => 'header',
-    is_frequent => 1,
     is_priv => 1,
     code => sub {
       my ($self, $key, $value, $line) = @_;
@@ -3086,8 +3203,14 @@ non-text MIME parts are stripped, and the message decoded from
 Quoted-Printable or Base-64-encoded format if necessary.  Parts declared as
 text/html will be rendered from HTML to text.
 
+Body is processed as a raw byte string, which means Unicode-specific regex
+features like \p{} can NOT be used for matching.  The normalize_charset
+setting will also affect how raw bytes are presented.  Rules in .cf files
+should be written portably - to match "a with umlaut" character, look for
+both LATIN1 and UTF8 raw byte variants: /(?:\xE4|\xC3\xA4)/
+
 All body paragraphs (double-newline-separated blocks text) are turned into a
-line breaks removed, whitespace normalized single line.  Any lines longer
+linebreaks-removed, whitespace-normalized, single line.  Any lines longer
 than 2kB are split into shorter separate lines (from a boundary when
 possible), this may unexpectedly prevent pattern from matching.  Patterns
 are matched independently against each of these lines.
@@ -3096,6 +3219,9 @@ Note that by default the message Subject header is considered part of the
 body and becomes the first line when running the rules. If you don't want
 to match Subject along with body text, use "tflags RULENAME nosubject".
 
+See C<https://wiki.apache.org/SpamAssassin/WritingRules> for more
+information.
+
 =item body SYMBOLIC_TEST_NAME eval:name_of_eval_method([args])
 
 Define a body eval test.  See above.
@@ -3104,7 +3230,6 @@ Define a body eval test.  See above.
 
   push (@cmds, {
     setting => 'body',
-    is_frequent => 1,
     is_priv => 1,
     code => sub {
       my ($self, $key, $value, $line) = @_;
@@ -3187,7 +3312,6 @@ Define a raw-body eval test.  See above.
 
   push (@cmds, {
     setting => 'rawbody',
-    is_frequent => 1,
     is_priv => 1,
     code => sub {
       my ($self, $key, $value, $line) = @_;
@@ -3221,6 +3345,10 @@ The full message is the pristine message headers plus the pristine message
 body, including all MIME data such as images, other attachments, MIME
 boundaries, etc.
 
+Note that CRLF/LF line endings are matched as the original message has them.
+For any full rules that match newlines, it's recommended to use \r?$ instead
+of plain $, so it works on all systems.
+
 =item full SYMBOLIC_TEST_NAME eval:name_of_eval_method([args])
 
 Define a full message eval test.  See above.
@@ -3306,7 +3434,6 @@ Which would be the same as:
 
   push (@cmds, {
     setting => 'meta',
-    is_frequent => 1,
     is_priv => 1,
     code => sub {
       my ($self, $key, $value, $line) = @_;
@@ -3405,7 +3532,7 @@ source of the points.
 
 This flag is specific when using AWL plugin.
 
-Normally, AWL plugin normalizes scores via auto-whitelist. In some scenarios
+Normally, AWL plugin normalizes scores via auto-welcomelist. In some scenarios
 it works against the system administrator when trying to add some rules to
 correct miss-classified email. When AWL plugin searches the email and finds 
 the noawl flag it will exit without normalizing the score nor storing the
@@ -3462,13 +3589,21 @@ it is documented there.
 This flag is specific to rules invoking an URIDNSBL plugin,
 it is documented there.
 
+=item  notrim
+
+This flag is specific to rules invoking an URIDNSBL plugin,
+it is documented there.
+
+=item nolog
+
+This flag will hide (sensitive) rule informations from reports
+
 =back
 
 =cut
 
   push (@cmds, {
     setting => 'tflags',
-    is_frequent => 1,
     is_priv => 1,
     type => $CONF_TYPE_HASH_KEY_VALUE,
   });
@@ -3507,6 +3642,45 @@ internally, and should not be used.
 
 =back
 
+=head2 CAPTURING TAGS USING REGEX NAMED CAPTURE GROUPS
+
+SpamAssassin 4.0 supports capturing template tags from regex rules.  The
+captured tags, along with other standard template tags, can be used in other
+rules as a matching string.  See B<TEMPLATE TAGS> section for more info on
+tags.
+
+Capturing can be done in any body/rawbody/header/uri/full rule that uses a
+regex for matching (not eval rules).  Standard Perl named capture group
+format C<(?E<lt>NAMEE<gt>pattern)> must be used, as described in
+L<https://perldoc.perl.org/perlre#(?%3CNAME%3Epattern)>.
+
+Example, capturing a tag named C<BODY_HELLO_NAME>:
+
+ body __HELLO_NAME /\bHello, (?<BODY_HELLO_NAME>\w+)\b/
+
+The tag can then be used in another rule for matching, using a %{TAGNAME}
+template.  This would search the captured name in From-header:
+
+ header HELLO_NAME_IN_FROM From =~ /\b%{BODY_HELLO_NAME}\b/i
+
+If any tag that a rule depends on is not found, then the rule is not run at
+all.  To prevent a literal %{NAME} string from being parsed as a template,
+it can be escaped with a backslash: \%{NAME}.
+
+Captured tags can also be used in reports and in other plugins like AskDNS,
+with the standard C<_BODY_HELLO_NAME_> notation.
+
+Note that at this time there is no automatic dependency tracking for rule
+running order.  All rules that use named capture groups are automatically
+set to priority -10000, so that the tags should always be ready for any
+normal rules to use.  When rule depends on a tag that might be set at later
+stage by a plugin for example, it's priority should be set manually to a
+higher value.
+
+=over 4
+
+=back
+
 =head1 ADMINISTRATOR SETTINGS
 
 These settings differ from the ones above, in that they are considered 'more
@@ -3674,8 +3848,13 @@ subdomain of the specified zone.
 
 =item util_rb_tld tld1 tld2 ...
 
-This option maintains list of valid TLDs in the RegistryBoundaries code. 
-TLDs include things like com, net, org, etc.
+=encoding utf8
+
+This option maintains a list of valid TLDs in the RegistryBoundaries code. 
+Top level domains (TLD) include things like com, net, org, xn--p1ai, рф, ...
+International domain names may be specified in ASCII-compatible encoding (ACE),
+e.g. xn--p1ai, xn--qxam, or with Unicode labels encoded as UTF-8 octets,
+e.g. рф, ελ.
 
 =cut
 
@@ -3691,7 +3870,7 @@ TLDs include things like com, net, org, etc.
        return $INVALID_VALUE;
       }
       foreach (split(/\s+/, $value)) {
-        $self->{valid_tlds}{lc $_} = 1;
+        $self->{valid_tlds}{idn_to_ascii($_)} = 1;
       }
     }
   });
@@ -3699,7 +3878,9 @@ TLDs include things like com, net, org, etc.
 =item util_rb_2tld 2tld-1.tld 2tld-2.tld ...
 
 This option maintains list of valid 2nd-level TLDs in the RegistryBoundaries
-code.  2TLDs include things like co.uk, fed.us, etc.
+code.  2TLDs include things like co.uk, fed.us, etc.  International domain
+names may be specified in ASCII-compatible encoding (ACE), or with Unicode
+labels encoded as UTF-8 octets.
 
 =cut
 
@@ -3715,7 +3896,7 @@ code.  2TLDs include things like co.uk, fed.us, etc.
        return $INVALID_VALUE;
       }
       foreach (split(/\s+/, $value)) {
-        $self->{two_level_domains}{lc $_} = 1;
+        $self->{two_level_domains}{idn_to_ascii($_)} = 1;
       }
     }
   });
@@ -3723,7 +3904,9 @@ code.  2TLDs include things like co.uk, fed.us, etc.
 =item util_rb_3tld 3tld1.some.tld 3tld2.other.tld ...
 
 This option maintains list of valid 3rd-level TLDs in the RegistryBoundaries
-code.  3TLDs include things like demon.co.uk, plc.co.im, etc.
+code.  3TLDs include things like demon.co.uk, plc.co.im, etc.  International
+domain names may be specified in ASCII-compatible encoding (ACE), or with
+Unicode labels encoded as UTF-8 octets.
 
 =cut
 
@@ -3739,7 +3922,7 @@ code.  3TLDs include things like demon.co.uk, plc.co.im, etc.
        return $INVALID_VALUE;
       }
       foreach (split(/\s+/, $value)) {
-        $self->{three_level_domains}{lc $_} = 1;
+        $self->{three_level_domains}{idn_to_ascii($_)} = 1;
       }
     }
   });
@@ -3760,9 +3943,9 @@ standard lists supplied by sa-update.
       unless (!defined $value || $value eq '') {
         return $INVALID_VALUE;
       }
-      $self->{valid_tlds} = ();
-      $self->{two_level_domains} = ();
-      $self->{three_level_domains} = ();
+      undef $self->{valid_tlds};
+      undef $self->{two_level_domains};
+      undef $self->{three_level_domains};
       dbg("config: cleared tld lists");
     }
   });
@@ -3816,7 +3999,8 @@ string of octal digits, it is converted to a numeric value internally.
     type => $CONF_TYPE_NUMERIC,
     code => sub {
       my ($self, $key, $value, $line) = @_;
-      if ($value !~ /^0?[0-7]{3}$/) { return $INVALID_VALUE }
+      if ($value !~ /^0?[0-7]{3}$/) { return $INVALID_VALUE; }
+      $value = '0'.$value if length($value) == 3; # Bug 5771
       $self->{bayes_file_mode} = untaint_var($value);
     }
   });
@@ -4103,6 +4287,16 @@ See C<Mail::SpamAssassin::Plugin> for more details on writing plugins.
       } else {
        return $INVALID_VALUE;
       }
+      # trunk Dmarc.pm was renamed to DMARC.pm
+      # (same check also in Conf/Parser.pm handle_conditional)
+      if ($package eq 'Mail::SpamAssassin::Plugin::Dmarc') {
+        $package = 'Mail::SpamAssassin::Plugin::DMARC';
+      }
+      # backwards compatible - removed in 4.1
+      # (same check also in Conf/Parser.pm handle_conditional)
+      elsif ($package eq 'Mail::SpamAssassin::Plugin::WhiteListSubject') {
+        $package = 'Mail::SpamAssassin::Plugin::WelcomeListSubject';
+      }
       $self->load_plugin ($package, $path);
     }
   });
@@ -4148,6 +4342,196 @@ end with a '|'.  Also ignore rules with C<some> combinatorial explosions.
     type     => $CONF_TYPE_BOOL,
   });
 
+=item geodb_module STRING
+
+This option tells SpamAssassin which geolocation module to use. 
+If not specified, all supported ones are tried in this order:
+
+Plugins can override this internally if required.
+
+ MaxMind::DB::Reader  (same as GeoIP2::Database::Reader)
+ Geo::IP
+ IP::Country::DB_File  (not used unless geodb_options path set)
+ IP::Country::Fast
+
+=cut
+
+  push (@cmds, {
+    setting => 'geodb_module',
+    is_admin => 1,
+    default => undef,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      $value = lc $value;
+      if ($value eq 'maxmind::db::reader' ||
+            $value eq 'geoip2::database::reader' || $value eq 'geoip2') {
+        $self->{geodb}->{module} = 'geoip2';
+      } elsif ($value eq 'geo::ip' || $value eq 'geoip') {
+        $self->{geodb}->{module} = 'geoip';
+      } elsif ($value eq 'ip::country::db_file' || $value eq 'db_file') {
+        $self->{geodb}->{module} = 'dbfile';
+      } elsif ($value eq 'ip::country::fast' || $value eq 'fast') {
+        $self->{geodb}->{module} = 'fast';
+      } else {
+        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+      }
+    }
+  });
+
+  # support deprecated RelayCountry setting
+  push (@cmds, {
+    setting => 'country_db_type',
+    is_admin => 1,
+    default => undef,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      warn("config: deprecated setting used, change country_db_type to geodb_module\n");
+      if ($value =~ /GeoIP2/i) {
+        $self->{geodb}->{module} = 'geoip2';
+      } elsif ($value =~ /Geo/i) {
+        $self->{geodb}->{module} = 'geoip';
+      } elsif ($value =~ /Fast/i) {
+        $self->{geodb}->{module} = 'fast';
+      } else {
+        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+      }
+    }
+  });
+
+=item geodb_options dbtype:/path/to/db ...
+
+Supported dbtypes:
+
+I<city> - use City database
+I<country> - use Country database
+I<isp> - try loading ISP database
+I<asn> - try loading ASN database
+
+Append full database path with colon, for example:
+I<isp:/opt/geoip/isp.mmdb>
+
+Plugins can internally request all types they require, geodb_options is only
+needed if the default location search (described below) does not work.
+
+GeoIP/GeoIP2 searches these files/directories:
+
+ country:
+   GeoIP2-Country.mmdb, GeoLite2-Country.mmdb
+   GeoIP.dat (and v6 version)
+ city:
+   GeoIP2-City.mmdb, GeoLite2-City.mmdb
+   GeoIPCity.dat, GeoLiteCity.dat (and v6 versions)
+ isp:
+   GeoIP2-ISP.mmdb
+   GeoIPISP.dat, GeoLiteISP.dat (and v6 versions)
+ directories:
+   /usr/local/share/GeoIP
+   /usr/share/GeoIP
+   /var/lib/GeoIP
+   /opt/share/GeoIP
+
+=cut
+
+  push (@cmds, {
+    setting => 'geodb_options',
+    is_admin => 1,
+    type => $CONF_TYPE_HASH_KEY_VALUE,
+    default => {},
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      foreach my $option (split (/\s+/, $value)) {
+        my ($option, $db) = split(/:/, $option, 2);
+        $option = lc($option);
+        if ($option eq 'reset') {
+          $self->{geodb}->{options} = {};
+        } elsif ($option eq 'country') {
+          $self->{geodb}->{options}->{country} = $db || undef;
+        } elsif ($option eq 'city') {
+          $self->{geodb}->{options}->{city} = $db || undef;
+        } elsif ($option eq 'isp') {
+          $self->{geodb}->{options}->{isp} = $db || undef;
+        } else {
+          return $INVALID_VALUE;
+        }
+      }
+    }
+  });
+
+=item geodb_search_path /path/to/GeoIP ...
+
+Alternative to geodb_options. Overrides the default list of directories to
+search for default filenames.
+
+=cut
+
+  push (@cmds, {
+    setting => 'geodb_search_path',
+    is_admin => 1,
+    default => [],
+    type => $CONF_TYPE_STRINGLIST,
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      if ($value eq 'reset') {
+        $self->{geodb}->{geodb_search_path} = [];
+      } elsif ($value eq '') {
+        return $MISSING_REQUIRED_VALUE;
+      } else {
+        push(@{$self->{geodb}->{geodb_search_path}}, split(/\s+/, $value));
+      }
+    }
+  });
+
+  # support deprecated RelayCountry setting
+  push (@cmds, {
+    setting => 'country_db_path',
+    is_admin => 1,
+    default => undef,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      warn("config: deprecated setting used, change country_db_path to geodb_options\n");
+      if ($value ne '') {
+        $self->{geodb}->{options}->{country} = $value;
+      } else {
+        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+      }
+    }
+  });
+  # support deprecated URILocalBL setting
+  push (@cmds, {
+    setting => 'uri_country_db_path',
+    is_admin => 1,
+    default => undef,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      warn("config: deprecated setting used, change uri_country_db_path to geodb_options\n");
+      if ($value ne '') {
+        $self->{geodb}->{options}->{country} = $value;
+      } else {
+        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+      }
+    }
+  });
+  # support deprecated URILocalBL setting
+  push (@cmds, {
+    setting => 'uri_country_db_isp_path',
+    is_admin => 1,
+    default => undef,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      warn("config: deprecated setting used, change uri_country_db_isp_path to geodb_options\n");
+      if ($value ne '') {
+        $self->{geodb}->{options}->{isp} = $value;
+      } else {
+        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+      }
+    }
+  });
+
 =back
 
 =head1 PREPROCESSING OPTIONS
@@ -4286,6 +4670,41 @@ version.  So 3.0.0 is C<3.000000>, and 3.4.80 is C<3.004080>.
     }
   });
 
+=item enable_compat xxxxxx
+
+Define a version compatibility flag.
+
+This creates a function named C<Mail::SpamAssassin::Conf::compat_xxxxxx>,
+which returns true.  It can be used for example in cf-files, similarly as existing
+C<feature_> checks:
+
+  if can(Mail::SpamAssassin::Conf::compat_xxxxxx)
+
+Name can only consist of [a-zA-Z0-9_] characters.
+
+Mainly used by SpamAssassin distribution to handle backwards compatibility
+issues.
+
+=cut
+
+  push (@cmds, {
+    setting => 'enable_compat',
+    is_admin => 1,
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      if ($value eq '') {
+        return $MISSING_REQUIRED_VALUE;
+      } elsif ($value !~ /^[a-zA-Z0-9_]{1,128}$/) {
+        return $INVALID_VALUE;
+      }
+      dbg("config: enabling compatibility flag $value");
+      # Inject compat method
+      { no strict 'refs';
+        *{"Mail::SpamAssassin::Conf::compat_$value"} = sub { 1 };
+      }
+    }
+  });
+
 =back
 
 =head1 TEMPLATE TAGS
@@ -4342,8 +4761,14 @@ optional, and the default is shown below.
                    'X-Spam-Relays-Internal' pseudo-header)
  _RELAYSEXTERNAL_  relays used and deemed to be external (see the 
                    'X-Spam-Relays-External' pseudo-header)
+ _FIRSTTRUSTEDIP_  IP address of first trusted client (see RELAYSTRUSTED)
+ _FIRSTTRUSTEDREVIP_  IP address of first trusted client (in reversed
+                   format suitable for RBL queries)
  _LASTEXTERNALIP_  IP address of client in the external-to-internal
                    SMTP handover
+ _LASTEXTERNALREVIP_  IP address of client in the external-to-internal
+                   SMTP handover (in reversed format suitable for RBL
+                   queries)
  _LASTEXTERNALRDNS_ reverse-DNS of client in the external-to-internal
                    SMTP handover
  _LASTEXTERNALHELO_ HELO string used by client in the external-to-internal
@@ -4378,7 +4803,9 @@ optional, and the default is shown below.
 If a tag reference uses the name of a tag which is not in this list or defined
 by a loaded plugin, the reference will be left intact and not replaced by any
 value.
-All template tag names should be restricted to the character set [A-Za-z0-9(,)].
+
+All template tag names must consist of only uppercase character set
+[A-Z0-9_] and not contain consecutive underscores (__).
 
 Additional, plugin specific, template tags can be found in the documentation for
 the following plugins:
@@ -4499,7 +4926,7 @@ sub new {
   # keep descriptions in a slow but space-efficient single-string
   # data structure
   # NOTE: Deprecated usage of TieOneStringHash as of 10/2018, it's an
-  # absolute pig, doubling config parse time, while benchmarks indicate
+  # absolute pig, doubling config parsing time, while benchmarks indicate
   # no difference in resident memory size!
   $self->{descriptions} = { };
   #tie %{$self->{descriptions}}, 'Mail::SpamAssassin::Util::TieOneStringHash'
@@ -4521,7 +4948,19 @@ sub new {
   $self->{rawbody_evals} = { };
   $self->{meta_tests} = { };
   $self->{eval_plugins} = { };
-  $self->{duplicate_rules} = { };
+  $self->{eval_plugins_types} = { };
+
+  # meta dependencies
+  $self->{meta_dependencies} = {};
+  $self->{meta_deprules} = {};
+  $self->{meta_nodeps} = {};
+
+  # map eval function names to rulenames
+  $self->{eval_to_rule} = {};
+
+  # regex capture template rules
+  $self->{capture_rules} = {};
+  $self->{capture_template_rules} = {};
 
   # testing stuff
   $self->{regression_tests} = { };
@@ -4532,20 +4971,19 @@ sub new {
   $self->{headers_spam} = [ ];
   $self->{headers_ham} = [ ];
 
-  $self->{bayes_ignore_headers} = [ ];
+  $self->{bayes_ignore_header} = { };
   $self->{bayes_ignore_from} = { };
   $self->{bayes_ignore_to} = { };
 
-  $self->{whitelist_auth} = { };
-  $self->{def_whitelist_auth} = { };
-  $self->{whitelist_from} = { };
-  $self->{whitelist_allows_relays} = { };
-  $self->{blacklist_from} = { };
-  $self->{whitelist_from_rcvd} = { };
-  $self->{def_whitelist_from_rcvd} = { };
+  $self->{welcomelist_auth} = { };
+  $self->{def_welcomelist_auth} = { };
+  $self->{welcomelist_from} = { };
+  $self->{welcomelist_allows_relays} = { };
+  $self->{welcomelist_from_rcvd} = { };
+  $self->{def_welcomelist_from_rcvd} = { };
 
-  $self->{blacklist_to} = { };
-  $self->{whitelist_to} = { };
+  $self->{blocklist_to} = { };
+  $self->{welcomelist_to} = { };
   $self->{more_spam_to} = { };
   $self->{all_spam_to} = { };
 
@@ -4562,10 +5000,6 @@ sub new {
     push(@{$self->{headers_ham}},  $r);
   }
 
-  # RFC 6891: A good compromise may be the use of an EDNS maximum payload size
-  # of 4096 octets as a starting point.
-  $self->{dns_options}->{edns} = 4096;
-
   # these should potentially be settable by end-users
   # perhaps via plugin?
   $self->{num_check_received} = 9;
@@ -4698,13 +5132,13 @@ sub trim_rules {
     push(@all_rules, $self->get_rule_keys($rule_type));
   }
 
-  my @rules_to_keep = grep(/$rec/, @all_rules);
+  my @rules_to_keep = grep(/$rec/o, @all_rules);
 
   if (@rules_to_keep == 0) {
     die "config: trim_rules: all rules excluded, nothing to test\n";
   }
 
-  my @meta_tests    = grep(/$rec/, $self->get_rule_keys('meta_tests'));
+  my @meta_tests    = grep(/$rec/o, $self->get_rule_keys('meta_tests'));
   foreach my $meta (@meta_tests) {
     push(@rules_to_keep, $self->add_meta_depends($meta))
   }
@@ -4863,59 +5297,9 @@ sub get_description_for_rule {
 
 ###########################################################################
 
-sub maybe_header_only {
-  my($self,$rulename) = @_;
-  my $type = $self->{test_types}->{$rulename};
-
-  if ($rulename =~ /AUTOLEARNTEST/i) {
-    dbg("config: auto-learn: $rulename - Test type is $self->{test_types}->{$rulename}.");
-  }
-  return 0 if (!defined ($type));
-
-  if (($type == $TYPE_HEAD_TESTS) || ($type == $TYPE_HEAD_EVALS)) {
-    return 1;
-
-  } elsif ($type == $TYPE_META_TESTS) {
-    my $tflags = $self->{tflags}->{$rulename}; 
-    $tflags ||= '';
-    if ($tflags =~ m/\bnet\b/i) {
-      return 0;
-    } else {
-      return 1;
-    }
-  }
-
-  return 0;
-}
-
-sub maybe_body_only {
-  my($self,$rulename) = @_;
-  my $type = $self->{test_types}->{$rulename};
-
-  if ($rulename =~ /AUTOLEARNTEST/i) {
-    dbg("config: auto-learn: $rulename - Test type is $self->{test_types}->{$rulename}.");
-  }
-
-  return 0 if (!defined ($type));
-
-  if (($type == $TYPE_BODY_TESTS) || ($type == $TYPE_BODY_EVALS)
-        || ($type == $TYPE_URI_TESTS) || ($type == $TYPE_URI_EVALS))
-  {
-    # some rawbody go off of headers...
-    return 1;
-
-  } elsif ($type == $TYPE_META_TESTS) {
-    my $tflags = $self->{tflags}->{$rulename}; $tflags ||= '';
-    if ($tflags =~ m/\bnet\b/i) {
-      return 0;
-    } else {
-      return 1;
-    }
-  }
-
-  return 0;
-}
+# Deprecated since Bug 7905/7906
+sub maybe_header_only { warn "Deprecated Conf::maybe_header_only() called"; }
+sub maybe_body_only { warn "Deprecated Conf::maybe_body_only() called"; }
 
 ###########################################################################
 
@@ -4930,8 +5314,18 @@ sub load_plugin_succeeded {
 }
 
 sub register_eval_rule {
-  my ($self, $pluginobj, $nameofsub) = @_;
+  my ($self, $pluginobj, $nameofsub, $ruletype) = @_;
+  if (exists $self->{eval_plugins}->{$nameofsub}) {
+    warn("config: eval function '$nameofsub' already exists, overwriting\n");
+  }
   $self->{eval_plugins}->{$nameofsub} = $pluginobj;
+  if (defined $ruletype) {
+    if (defined $TYPE_AS_STRING{$ruletype}) {
+      $self->{eval_plugins_types}->{$nameofsub} = $ruletype;
+    } else {
+      $self->{parser}->lint_warn("config: invalid ruletype for eval $nameofsub");
+    }
+  }
 }
 
 ###########################################################################
@@ -4953,8 +5347,8 @@ sub clone {
   # is defined, its method will be recompiled for future scans in
   # order to *remove* the generated method calls
   my @NON_COPIED_KEYS = qw(
-    main eval_plugins plugins_loaded registered_commands sed_path_cache parser
-    scoreset scores want_rebuild_for_type
+    main eval_plugins eval_plugins_types plugins_loaded registered_commands
+    sed_path_cache parser scoreset scores want_rebuild_for_type
   );
 
   # special cases.  first, skip anything that cannot be changed
@@ -5063,9 +5457,8 @@ sub free_uncompiled_rule_source {
   if (!$self->{main}->{keep_config_parsing_metadata} &&
         !$self->{allow_user_rules})
   {
-    delete $self->{if_stack};
+    #delete $self->{if_stack}; # it's Parser not Conf?
     #delete $self->{source_file};
-    #delete $self->{meta_dependencies};
   }
 }
 
@@ -5105,10 +5498,22 @@ sub feature_bug6558_free { 1 }
 sub feature_edns { 1 }  # supports 'dns_options edns' config option
 sub feature_dns_query_restriction { 1 }  # supported config option
 sub feature_registryboundaries { 1 } # replaces deprecated registrarboundaries
+sub feature_geodb { 1 } # if needed for some reason
+sub feature_dns_block_rule { 1 } # supports 'dns_block_rule' config option
 sub feature_compile_regexp { 1 } # Util::compile_regexp
 sub feature_meta_rules_matching { 1 } # meta rules_matching() expression
 sub feature_subjprefix { 1 } # add subject prefixes rule option
+sub feature_bayes_stopwords { 1 } # multi language stopwords in Bayes
+sub feature_get_host { 1 } # $pms->get() :host :domain :ip :revip # was implemented together with AskDNS::has_tag_header # Bug 7734
+sub feature_blocklist_welcomelist { 1 } # bz 7826 - do not use, for backwards compatibility
+sub feature_welcomelist_blocklist { 1 } # bz 7826 - this is the actual feature_ to use, everything is renamed at this point
+sub feature_header_address_parser { 1 } # improved header address parsing using Email::Address::XS, $pms->get() list context
+sub feature_local_tests_only { 1 } # Config parser supports "if (local_tests_only)"
+sub feature_header_first_last { 1 } # Can actually use :first :last modifiers in rules
+sub feature_header_match_many { 1 } # Can actually match all :addr :name etc results, before only first one was used
+sub feature_capture_rules { 1 } # Can capture and use tags with regex in body/rawbody/full/uri/header rules # Bug 7992
 sub has_tflags_nosubject { 1 } # tflags nosubject
+sub has_tflags_nolog { 1 } # tflags nolog
 sub perl_min_version_5010000 { return $] >= 5.010000 }  # perl version check ("perl_version" not neatly backwards-compatible)
 
 ###########################################################################
@@ -5116,15 +5521,22 @@ sub perl_min_version_5010000 { return $] >= 5.010000 }  # perl version check ("p
 1;
 __END__
 
-=head1 LOCALI[SZ]ATION
+=head1 LOCALISATION
 
-A line starting with the text C<lang xx> will only be interpreted
-if the user is in that locale, allowing test descriptions and
+A line starting with the text C<lang xx> will only be interpreted if
+SpamAssassin is running in that locale, allowing test descriptions and
 templates to be set for that language.
 
+Current locale is determined from LANGUAGE, LC_ALL, LC_MESSAGES or LANG
+environment variables, first found is used.
+
 The locales string should specify either both the language and country, e.g.
 C<lang pt_BR>, or just the language, e.g. C<lang de>.
 
+Example:
+
+ lang de describe EXAMPLE_RULE Beispielregel
+
 =head1 SEE ALSO
 
 Mail::SpamAssassin(3)
index 9080a9d558c8c445e7ee62d829d30cc69cda702f..b485930de031a53301e86590a66fc02b19c7132d 100644 (file)
@@ -27,7 +27,7 @@ Mail::SpamAssassin::Conf::LDAP - load SpamAssassin scores from LDAP database
 =head1 DESCRIPTION
 
 Mail::SpamAssassin is a module to identify spam using text analysis and
-several internet-based realtime blacklists.
+several internet-based realtime blocklists.
 
 This class is used internally by SpamAssassin to load scores from an LDAP
 database.  Please refer to the C<Mail::SpamAssassin> documentation for public
@@ -191,6 +191,9 @@ sub load_with_ldap {
   }
   if ($config_text ne '') {
     $conf->{main} = $main;
+    $config_text = "file start (ldap config)\n".
+                   $config_text.
+                   "file end (ldap config)\n";
     $conf->parse_scores_only($config_text);
     delete $conf->{main};
   }
index c512dbcbc50698b2bc005bdb163048377dafba26..37034c8b859452b631c14953007e896940cb0cb5 100644 (file)
@@ -26,7 +26,7 @@ Mail::SpamAssassin::Conf::Parser - parse SpamAssassin configuration
 =head1 DESCRIPTION
 
 Mail::SpamAssassin is a module to identify spam using text analysis and
-several internet-based realtime blacklists.
+several internet-based realtime blocklists.
 
 This class is used internally by SpamAssassin to parse its configuration files.
 Please refer to the C<Mail::SpamAssassin> documentation for public interfaces.
@@ -61,7 +61,7 @@ The type of this setting:
  - $CONF_TYPE_NUMERIC: numeric value (float or int)
  - $CONF_TYPE_BOOL: boolean (0/no or 1/yes)
  - $CONF_TYPE_TEMPLATE: template, like "report"
- - $CONF_TYPE_ADDRLIST: list of mail addresses, like "whitelist_from"
+ - $CONF_TYPE_ADDRLIST: list of mail addresses, like "welcomelist_from"
  - $CONF_TYPE_HASH_KEY_VALUE: hash key/value pair, like "describe" or tflags
  - $CONF_TYPE_STRINGLIST list of strings, stored as an array
  - $CONF_TYPE_IPADDRLIST list of IP addresses, stored as an array of SA::NetSet
@@ -123,11 +123,6 @@ Set to 1 if this setting can only be set in the system-wide config when run
 from spamd.  (All settings can be used by local programs run directly by the
 user.)
 
-=item is_frequent
-
-Set to 1 if this value occurs frequently in the config. this means it's looked
-up first for speed.
-
 =back
 
 =cut
@@ -162,8 +157,6 @@ sub new {
   };
 
   $self->{command_luts} = { };
-  $self->{command_luts}->{frequent} = { };
-  $self->{command_luts}->{remaining} = { };
 
   bless ($self, $class);
   $self;
@@ -197,20 +190,13 @@ sub build_command_luts {
 
   my $conf = $self->{conf};
 
-  my $set;
   foreach my $cmd (@{$arrref}) {
-    # first off, decide what set this is in.
-    if ($cmd->{is_frequent}) { $set = 'frequent'; }
-    else { $set = 'remaining'; }
-
-    # next, its priority (used to ensure frequently-used params
-    # are parsed first)
     my $cmdname = $cmd->{command} || $cmd->{setting};
-    $self->{command_luts}->{$set}->{$cmdname} = $cmd;
+    $self->{command_luts}->{$cmdname} = $cmd;
 
     if ($cmd->{aliases} && scalar @{$cmd->{aliases}} > 0) {
       foreach my $name (@{$cmd->{aliases}}) {
-        $self->{command_luts}->{$set}->{$name} = $cmd;
+        $self->{command_luts}->{$name} = $cmd;
       }
     }
   }
@@ -243,24 +229,29 @@ sub parse {
   }                            # (eg. .utf8 or @euro)
 
   # get fast-access handles on the command lookup tables
-  my $lut_frequent = $self->{command_luts}->{frequent};
-  my $lut_remaining = $self->{command_luts}->{remaining};
+  my $lut = $self->{command_luts};
   my %migrated_keys = map { $_ => 1 }
             @Mail::SpamAssassin::Conf::MIGRATED_SETTINGS;
 
   $self->{currentfile} = '(no file)';
+  $self->{linenum} = ();
   my $skip_parsing = 0;
   my @curfile_stack;
   my @if_stack;
   my @conf_lines = split (/\n/, $_[1]);
   my $line;
   $self->{if_stack} = \@if_stack;
+  $self->{cond_cache} = { };
   $self->{file_scoped_attrs} = { };
 
   my $keepmetadata = $conf->{main}->{keep_config_parsing_metadata};
 
   while (defined ($line = shift @conf_lines)) {
     local ($1);         # bug 3838: prevent random taint flagging of $1
+    my $parse_error;    # undef by default, may be overridden
+
+    # don't count internal file start/end lines
+    $self->{linenum}{$self->{currentfile}}++ if index($line, 'file ') != 0;
 
     if (index($line,'#') > -1) {
       # bug 5545: used to support testing rules in the ruleqa system
@@ -270,7 +261,7 @@ sub parse {
       }
 
       # bug 6800: let X-Spam-Checker-Version also show what sa-update we are at
-      if ($line =~ /^\# UPDATE version (\d+)$/) {
+      if (index($line, '# UPD') == 0 && $line =~ /^\# UPDATE version (\d+)$/) {
         for ($self->{currentfile}) {  # just aliasing, not a loop
           $conf->{update_version}{$_} = $1  if defined $_ && $_ ne '(no file)';
         }
@@ -285,7 +276,9 @@ sub parse {
     next unless($line); # skip empty lines
 
     # handle i18n
-    if ($line =~ s/^lang\s+(\S+)\s+//) { next if ($lang !~ /^$1/i); }
+    if (index($line, 'lang') == 0 && $line =~ s/^lang\s+(\S+)\s+//) {
+      next if $lang !~ /^$1/i;
+    }
 
     my($key, $value) = split(/\s+/, $line, 2);
     $key = lc $key;
@@ -293,92 +286,94 @@ sub parse {
     $key =~ tr/-/_/;
     $value = '' unless defined($value);
 
-#   # Do a better job untainting this info ...
-#   # $value = untaint_var($value);
-#   Do NOT blindly untaint now, do it carefully later when semantics is known!
-
-    my $parse_error;       # undef by default, may be overridden
-
-    # File/line number assertions
-    if ($key eq 'file') {
+    # $key if/elsif blocks sorted by most commonly used
+    if ($key eq 'endif') {
+      if ($value ne '') {
+        $parse_error = "config: '$key' must be standalone";
+        goto failed_line;
+      }
+      my $lastcond = pop @if_stack;
+      if (!defined $lastcond) {
+        $parse_error = "config: missing starting 'if' for '$key'";
+        goto failed_line;
+      }
+      $skip_parsing = $lastcond->{skip_parsing};
+      next;
+    }
+    elsif ($key eq 'ifplugin') {
+      if ($value eq '') {
+        $parse_error = "config: missing '$key' condition";
+        goto failed_line;
+      }
+      $self->handle_conditional ($key, "plugin ($value)",
+                        \@if_stack, \$skip_parsing);
+      next;
+    }
+    elsif ($key eq 'if') {
+      if ($value eq '') {
+        $parse_error = "config: missing '$key' condition";
+        goto failed_line;
+      }
+      $self->handle_conditional ($key, $value,
+                        \@if_stack, \$skip_parsing);
+      next;
+    }
+    elsif ($key eq 'file') {
       if ($value =~ /^start\s+(.+)$/) {
+        dbg("config: parsing file $1");
         push (@curfile_stack, $self->{currentfile});
         $self->{currentfile} = $1;
         next;
       }
-
-      if ($value =~ /^end\s/) {
-        $self->{file_scoped_attrs} = { };
-
-        if (scalar @if_stack > 0) {
-          my $cond = pop @if_stack;
-
-          if ($cond->{type} eq 'if') {
-            my $msg = "config: unclosed 'if' in ".
-                  $self->{currentfile}.": if ".$cond->{conditional}."\n";
-            warn $msg;
-            $self->lint_warn($msg, undef);
-          }
-          else {
-            # die seems a bit excessive here, but this shouldn't be possible
-            # so I suppose it's okay.
-            die "config: unknown 'if' type: ".$cond->{type}."\n";
-          }
-
-          @if_stack = ();
+      elsif ($value =~ /^end\s/) {
+        foreach (@if_stack) {
+          my $msg = "config: unclosed '$_->{type}' found ".
+                    "in $self->{currentfile} (line $_->{linenum})";
+          $self->lint_warn($msg, undef);
         }
+        $self->{file_scoped_attrs} = { };
+        @if_stack = ();
         $skip_parsing = 0;
-
-        my $curfile = pop @curfile_stack;
-        if (defined $curfile) {
-          $self->{currentfile} = $curfile;
-        } else {
-          $self->{currentfile} = '(no file)';
-        }
+        $self->{currentfile} = pop @curfile_stack;
         next;
       }
+      else {
+        $parse_error = "config: missing '$key' value";
+        goto failed_line;
+      }
     }
-
-    # now handle the commands.
     elsif ($key eq 'include') {
+      if ($value eq '') {
+        $parse_error = "config: missing '$key' value";
+        goto failed_line;
+      }
       $value = $self->fix_path_relative_to_current_file($value);
       my $text = $conf->{main}->read_cf($value, 'included file');
-      unshift (@conf_lines, split (/\n/, $text));
+      unshift (@conf_lines,
+          "file end $self->{currentfile}",
+          split (/\n/, $text),
+          "file start $self->{currentfile}");
       next;
     }
-
-    elsif ($key eq 'ifplugin') {
-      $self->handle_conditional ($key, "plugin ($value)",
-                        \@if_stack, \$skip_parsing);
-      next;
-    }
-
-    elsif ($key eq 'if') {
-      $self->handle_conditional ($key, $value,
-                        \@if_stack, \$skip_parsing);
-      next;
-    }
-
     elsif ($key eq 'else') {
+      if ($value ne '') {
+        $parse_error = "config: '$key' must be standalone";
+        goto failed_line;
+      }
+
       # TODO: if/else/else won't get flagged here :(
       if (!@if_stack) {
-        $parse_error = "config: found else without matching conditional";
+        $parse_error = "config: '$key' missing starting if";
         goto failed_line;
       }
 
-      $skip_parsing = !$skip_parsing;
-      next;
-    }
-
-    # and the endif statement:
-    elsif ($key eq 'endif') {
-      my $lastcond = pop @if_stack;
-      if (!defined $lastcond) {
-        $parse_error = "config: found endif without matching conditional";
-        goto failed_line;
+      # Check if we are blocked anywhere in previous if-stack (Bug 7848)
+      if (grep { $_->{skip_parsing} } @if_stack) {
+        $skip_parsing = 1;
+      } else {
+        $skip_parsing = !$skip_parsing;
       }
 
-      $skip_parsing = $lastcond->{skip_parsing};
       next;
     }
 
@@ -386,6 +381,11 @@ sub parse {
     next if $skip_parsing;
 
     if ($key eq 'require_version') {
+      if ($value eq '') {
+        $parse_error = "config: missing '$key' value";
+        goto failed_line;
+      }
+
       # if it wasn't replaced during install, assume current version ...
       next if ($value eq "\@\@VERSION\@\@");
 
@@ -400,11 +400,11 @@ sub parse {
       #$value =~ s/^(\d+)\.(\d{1,3}).*$/sprintf "%d.%d", $1, $2/e;
 
       if ($ver ne $value) {
-        my $msg = "config: configuration file \"$self->{currentfile}\" requires ".
+        my $msg = "config: configuration file '$self->{currentfile}' requires ".
                 "version $value of SpamAssassin, but this is code version ".
                 "$ver. Maybe you need to use ".
                 "the -C switch, or remove the old config files? ".
-                "Skipping this file";
+                "Skipping this file.";
         warn $msg;
         $self->lint_warn($msg, undef);
         $skip_parsing = 1;
@@ -412,10 +412,7 @@ sub parse {
       next;
     }
 
-    my $cmd = $lut_frequent->{$key}; # check the frequent command set
-    if (!$cmd) {
-      $cmd = $lut_remaining->{$key}; # no? try the rest
-    }
+    my $cmd = $lut->{$key};
 
     # we've either fallen through with no match, in which case this
     # if() will fail, or we have a match.
@@ -438,26 +435,18 @@ sub parse {
       }
 
       my $ret = &{$cmd->{code}} ($conf, $cmd->{setting}, $value, $line);
+      next if !$ret;
 
-      if ($ret && $ret eq $Mail::SpamAssassin::Conf::INVALID_VALUE)
-      {
-        $parse_error = "config: SpamAssassin failed to parse line, ".
-                        "\"$value\" is not valid for \"$key\", ".
-                        "skipping: $line";
+      if ($ret eq $Mail::SpamAssassin::Conf::INVALID_VALUE) {
+        $parse_error = "config: invalid '$key' value";
         goto failed_line;
       }
-      elsif ($ret && $ret eq $Mail::SpamAssassin::Conf::INVALID_HEADER_FIELD_NAME)
-      {
-        $parse_error = "config: SpamAssassin failed to parse line, ".
-                       "it does not specify a valid header field name, ".
-                       "skipping: $line";
+      elsif ($ret eq $Mail::SpamAssassin::Conf::INVALID_HEADER_FIELD_NAME) {
+        $parse_error = "config: invalid header field name";
         goto failed_line;
       }
-      elsif ($ret && $ret eq $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE)
-      {
-        $parse_error = "config: SpamAssassin failed to parse line, ".
-                        "no value provided for \"$key\", ".
-                        "skipping: $line";
+      elsif ($ret eq $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE) {
+        $parse_error = "config: missing '$key' value";
         goto failed_line;
       }
       else {
@@ -486,21 +475,28 @@ failed_line:
       if ($migrated_keys{$key}) {
         # this key was moved into a plugin; non-fatal for lint
         $is_error = 0;
-        $msg = "config: failed to parse, now a plugin, skipping, in \"$self->{currentfile}\": $line";
+        $msg = "config: failed to parse line, now a plugin";
       } else {
         # a real syntax error; this is fatal for --lint
-        $msg = "config: failed to parse line, skipping, in \"$self->{currentfile}\": $line";
+        $msg = "config: failed to parse line";
       }
     }
 
+    if ($self->{currentfile} eq '(no file)') {
+      $msg .= " in $self->{currentfile}: $line"; 
+    } else {
+      $msg .= " in $self->{currentfile} ".
+              "(line $self->{linenum}{$self->{currentfile}}): $line"; 
+    }
     $self->lint_warn($msg, undef, $is_error);
   }
 
   delete $self->{if_stack};
+  delete $self->{cond_cache};
+  delete $self->{linenum};
 
   $self->lint_check();
-  $self->set_default_scores();
-  $self->check_for_missing_descriptions();
+  $self->fix_tests();
 
   delete $self->{scoresonly};
 }
@@ -509,12 +505,26 @@ sub handle_conditional {
   my ($self, $key, $value, $if_stack_ref, $skip_parsing_ref) = @_;
   my $conf = $self->{conf};
 
-  my @tokens = ($value =~ /($ARITH_EXPRESSION_LEXER)/og);
+  # If we have already successfully evaled the $value,
+  # just do what we would do then
+  if (exists $self->{cond_cache}{"$key $value"}) {
+    push (@{$if_stack_ref}, {
+        'type' => $key,
+        'conditional' => $value,
+        'skip_parsing' => $$skip_parsing_ref,
+        'linenum' => $self->{linenum}{$self->{currentfile}}
+      });
+    if ($self->{cond_cache}{"$key $value"} == 0) {
+      $$skip_parsing_ref = 1;
+    }
+    return;
+  }
 
+  my @tokens = ($value =~ /($ARITH_EXPRESSION_LEXER)/og);
   my $eval = '';
-  my $bad = 0;
+
   foreach my $token (@tokens) {
-    if ($token =~ /^(?:\W{1,5}|[+-]?\d+(?:\.\d+)?)$/) {
+    if ($token eq '(' || $token eq ')' || $token eq '!') {
       # using tainted subr. argument may taint the whole expression, avoid
       my $u = untaint_var($token);
       $eval .= $u . " ";
@@ -530,53 +540,77 @@ sub handle_conditional {
     elsif ($token eq 'has') {
       # replace with a method call
       $eval .= '$self->cond_clause_has';
-    }
+    }  
     elsif ($token eq 'version') {
       $eval .= $Mail::SpamAssassin::VERSION." ";
     }
     elsif ($token eq 'perl_version') {
       $eval .= $]." ";
     }
+    elsif ($token eq 'local_tests_only') {
+      $eval .= '($self->{conf}->{main}->{local_tests_only}?1:0) '
+    }
+    elsif ($token =~ /^(?:\W{1,5}|[+-]?\d+(?:\.\d+)?)$/) {
+      # using tainted subr. argument may taint the whole expression, avoid
+      my $u = untaint_var($token);
+      $eval .= $u . " ";
+    }
     elsif ($token =~ /^\w[\w\:]+$/) { # class name
       # Strictly controlled form:
       if ($token =~ /^(?:\w+::){0,10}\w+$/) {
+        # trunk Dmarc.pm was renamed to DMARC.pm
+        # (same check also in Conf.pm loadplugin)
+        if ($token eq 'Mail::SpamAssassin::Plugin::Dmarc') {
+          $token = 'Mail::SpamAssassin::Plugin::DMARC';
+        }
+        # backwards compatible - removed in 4.1
+        # (same check also in Conf.pm loadplugin)
+        elsif ($token eq 'Mail::SpamAssassin::Plugin::WhiteListSubject') {
+          $token = 'Mail::SpamAssassin::Plugin::WelcomeListSubject';
+        }
         my $u = untaint_var($token);
         $eval .= "'$u'";
       } else {
-        warn "config: illegal name '$token' in 'if $value'\n";
-        $bad++;
-        last;
+        my $msg = "config: not allowed value '$token' ".
+            "in $self->{currentfile} (line $self->{linenum}{$self->{currentfile}})";
+        $self->lint_warn($msg, undef);
+        return;
       }
     }
     else {
-      $bad++;
-      warn "config: unparseable chars in 'if $value': '$token'\n";
-      last;
+      my $msg = "config: unparseable value '$token' ".
+          "in $self->{currentfile} (line $self->{linenum}{$self->{currentfile}})";
+      $self->lint_warn($msg, undef);
+      return;
     }
   }
 
-  if ($bad) {
-    $self->lint_warn("config: bad 'if' line, in \"$self->{currentfile}\"", undef);
-    return -1;
-  }
-
   push (@{$if_stack_ref}, {
-      type => 'if',
-      conditional => $value,
-      skip_parsing => $$skip_parsing_ref
+      'type' => $key,
+      'conditional' => $value,
+      'skip_parsing' => $$skip_parsing_ref,
+      'linenum' => $self->{linenum}{$self->{currentfile}}
     });
 
   if (eval $eval) {
+    $self->{cond_cache}{"$key $value"} = 1;
     # leave $skip_parsing as-is; we may not be parsing anyway in this block.
     # in other words, support nested 'if's and 'require_version's
   } else {
-    warn "config: error in $key - $eval: $@" if $@ ne '';
+    if ($@) {
+      my $msg = "config: error parsing conditional ".
+          "in $self->{currentfile} (line $self->{linenum}{$self->{currentfile}}): $eval ($@)";
+      warn $msg;
+      $self->lint_warn($msg, undef, 0); # not fatal?
+    }
+    $self->{cond_cache}{"$key $value"} = 0;
     $$skip_parsing_ref = 1;
   }
 }
 
 # functions supported in the "if" eval:
 sub cond_clause_plugin_loaded {
+  return 1 if $_[1] eq 'Mail::SpamAssassin::Plugin::RaciallyCharged'; # removed in 4.1
   return $_[0]->{conf}->{plugins_loaded}->{$_[1]};
 }
 
@@ -599,16 +633,18 @@ sub cond_clause_can_or_has {
 
   local($1,$2);
   if (!defined $method) {
-    $self->lint_warn("config: bad 'if' line, no argument to $fn_name(), ".
-                     "in \"$self->{currentfile}\"", undef);
+    my $msg = "config: bad 'if' line, no argument to $fn_name() ".
+              "in $self->{currentfile} (line $self->{linenum}{$self->{currentfile}})";
+    $self->lint_warn($msg, undef);
   } elsif ($method =~ /^(.*)::([^:]+)$/) {
     no strict "refs";
     my($module, $meth) = ($1, $2);
     return 1  if $module->can($meth) &&
                  ( $fn_name eq 'has' || &{$method}() );
   } else {
-    $self->lint_warn("config: bad 'if' line, cannot find '::' in $fn_name($method), ".
-                     "in \"$self->{currentfile}\"", undef);
+    my $msg = "config: bad 'if' line, cannot find '::' in $fn_name($method) ".
+              "in $self->{currentfile} (line $self->{linenum}{$self->{currentfile}})";
+    $self->lint_warn($msg, undef);
   }
   return;
 }
@@ -625,51 +661,47 @@ sub lint_check {
     # Check for description and score issues in lint fashion
     while ( my $k = each %{$conf->{descriptions}} ) {
       if (!exists $conf->{tests}->{$k}) {
-        dbg("config: warning: description exists for non-existent rule $k");
+        dbg("config: description exists for non-existent rule $k");
       }
     }
 
     while ( my($sk) = each %{$conf->{scores}} ) {
       if (!exists $conf->{tests}->{$sk}) {
         # bug 5514: not a lint warning any more
-        dbg("config: warning: score set for non-existent rule $sk");
+        dbg("config: score set for non-existent rule $sk");
       }
     }
   }
 }
 
-# we should set a default score for all valid rules...  Do this here
-# instead of add_test because mostly 'score' occurs after the rule is
-# specified, so why set the scores to default, then set them again at
-# 'score'?
-# 
-sub set_default_scores {
+# Iterate through tests and check/fix things
+sub fix_tests {
   my ($self) = @_;
+
   my $conf = $self->{conf};
+  my $would_log_dbg = would_log('dbg');
 
   while ( my $k = each %{$conf->{tests}} ) {
+    # we should set a default score for all valid rules...  Do this here
+    # instead of add_test because mostly 'score' occurs after the rule is
+    # specified, so why set the scores to default, then set them again at
+    # 'score'?
+    # 
     if ( ! exists $conf->{scores}->{$k} ) {
       # T_ rules (in a testing probationary period) get low, low scores
-      my $set_score = ($k =~/^T_/) ? 0.01 : 1.0;
+      my $set_score = index($k, 'T_') == 0 ? 0.01 : 1.0;
 
       $set_score = -$set_score if ( ($conf->{tflags}->{$k}||'') =~ /\bnice\b/ );
       for my $index (0..3) {
         $conf->{scoreset}->[$index]->{$k} = $set_score;
       }
     }
-  }
-}
 
-# loop through all the tests and if we are missing a description with debug
-# set, throw a warning except for testing T_ or meta __ rules.
-sub check_for_missing_descriptions {
-  my ($self) = @_;
-  my $conf = $self->{conf};
-
-  while ( my $k = each %{$conf->{tests}} ) {
-    if ($k !~ m/^(?:T_|__)/i) {
+    # loop through all the tests and if we are missing a description with debug
+    # set, throw a note except for testing T_ or meta __ rules.
+    if ($would_log_dbg && $k !~ m/^(?:T_|__)/i) {
       if ( ! exists $conf->{descriptions}->{$k} ) {
-        dbg("config: warning: no description set for $k");
+        dbg("config: no description set for rule $k");
       }
     }
   }
@@ -795,7 +827,7 @@ sub set_string_list {
     return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
   }
 
-  push(@{$conf->{$key}}, split(' ', $value));
+  push(@{$conf->{$key}}, split(/\s+/, $value));
 }
 
 sub set_ipaddr_list {
@@ -805,7 +837,7 @@ sub set_ipaddr_list {
     return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
   }
 
-  foreach my $net (split(' ', $value)) {
+  foreach my $net (split(/\s+/, $value)) {
     $conf->{$key}->add_cidr($net);
   }
   $conf->{$key.'_configured'} = 1;
@@ -828,7 +860,7 @@ sub set_addrlist_value {
   unless (defined $value && $value !~ /^$/) {
     return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
   }
-  $conf->{parser}->add_to_addrlist ($key, split (' ', $value));  # keep tainted
+  $conf->{parser}->add_to_addrlist ($key, split(/\s+/, $value));  # keep tainted
 }
 
 sub remove_addrlist_value {
@@ -837,7 +869,7 @@ sub remove_addrlist_value {
   unless (defined $value && $value !~ /^$/) {
     return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
   }
-  $conf->{parser}->remove_from_addrlist ($key, split (' ', $value));
+  $conf->{parser}->remove_from_addrlist ($key, split(/\s+/, $value));
 }
 
 sub set_template_append {
@@ -868,20 +900,19 @@ sub finish_parsing {
     $conf->{main}->call_plugins("user_conf_parsing_start", { conf => $conf });
   }
 
-  $self->trace_meta_dependencies();
+  # compile meta rules
+  $self->compile_meta_rules();
   $self->fix_priorities();
-
-  # don't do this if allow_user_rules is active, since it deletes entries
-  # from {tests}
-  if (!$conf->{allow_user_rules}) {
-    $self->find_dup_rules();          # must be after fix_priorities()
-  }
+  $self->fix_tflags();
 
   dbg("config: finish parsing");
 
   while (my ($name, $text) = each %{$conf->{tests}}) {
     my $type = $conf->{test_types}->{$name};
-    my $priority = $conf->{priority}->{$name} || 0;
+
+    # Adjust priority -100 for net rules instead of default 0
+    my $priority = $conf->{priority}->{$name} ? $conf->{priority}->{$name} :
+        ($conf->{tflags}->{$name}||'') =~ /\bnet\b/ ? -100 : 0;
     $conf->{priorities}->{$priority}++;
 
     # eval type handling
@@ -892,7 +923,21 @@ sub finish_parsing {
           $self->lint_warn("syntax error for eval function $name: $text");
           next;
         }
-        elsif ($type == $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS) {
+
+        # Validate type
+        my $expected_type = $conf->{eval_plugins_types}->{$function};
+        if (defined $expected_type && $expected_type != $type) {
+          # Allow both body and rawbody if expecting body
+          if (!($expected_type == $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS &&
+              $type == $Mail::SpamAssassin::Conf::TYPE_RAWBODY_EVALS))
+          {
+            my $estr = $Mail::SpamAssassin::Conf::TYPE_AS_STRING{$expected_type};
+            $self->lint_warn("wrong rule type defined for $name, expected '$estr'");
+            next;
+          }
+        }
+
+        if ($type == $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS) {
           $conf->{body_evals}->{$priority}->{$name} = [ $function, [@$argsref] ];
         }
         elsif ($type == $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS) {
@@ -931,7 +976,7 @@ sub finish_parsing {
         $conf->{head_tests}->{$priority}->{$name} = $text;
       }
       elsif ($type == $Mail::SpamAssassin::Conf::TYPE_META_TESTS) {
-        $conf->{meta_tests}->{$priority}->{$name} = $text;
+        # Handled by compile_meta_rules()
       }
       elsif ($type == $Mail::SpamAssassin::Conf::TYPE_URI_TESTS) {
         $conf->{uri_tests}->{$priority}->{$name} = $text;
@@ -970,138 +1015,203 @@ sub finish_parsing {
   }
 }
 
-sub trace_meta_dependencies {
+# Returns all rulenames matching glob (FOO_*)
+sub expand_ruleglob {
+  my ($self, $ruleglob, $rulename) = @_;
+  my $expanded;
+  if (exists $self->{ruleglob_cache}{$ruleglob}) {
+    $expanded = $self->{ruleglob_cache}{$ruleglob};
+  } else {
+    my $reglob = $ruleglob;
+    $reglob =~ s/\?/./g;
+    $reglob =~ s/\*/.*?/g;
+    # Glob rules, but do not match ourselves..
+    my @rules = grep {/^${reglob}$/ && $_ ne $rulename} keys %{$self->{conf}->{scores}};
+    if (@rules) {
+      $expanded = join('+', sort @rules);
+    } else {
+      $expanded = '0';
+    }
+  }
+  my $logstr = $expanded eq '0' ? 'no matches' : $expanded;
+  dbg("rules: meta $rulename rules_matching($ruleglob) expanded: $logstr");
+  $self->{ruleglob_cache}{$ruleglob} = $expanded;
+  return " ($expanded) ";
+}
+
+sub compile_meta_rules {
   my ($self) = @_;
+  my (%meta, %meta_deps, %rule_deps);
   my $conf = $self->{conf};
-  $conf->{meta_dependencies} = { };
 
   foreach my $name (keys %{$conf->{tests}}) {
-    next unless ($conf->{test_types}->{$name}
-                    == $Mail::SpamAssassin::Conf::TYPE_META_TESTS);
-    my $alreadydone = {};
-    $self->_meta_deps_recurse($conf, $name, $name, $alreadydone);
-  }
-}
+    next unless $conf->{test_types}->{$name} == $Mail::SpamAssassin::Conf::TYPE_META_TESTS;
+    my $rule = $conf->{tests}->{$name};
 
-sub _meta_deps_recurse {
-  my ($self, $conf, $toprule, $name, $alreadydone) = @_;
+    # Expand meta rules_matching() before lexing
+    $rule =~ s/${META_RULES_MATCHING_RE}/$self->expand_ruleglob($1,$name)/ge;
 
-  # Avoid recomputing the dependencies of a rule
-  return split(' ', $conf->{meta_dependencies}->{$name}) if defined $conf->{meta_dependencies}->{$name};
+    # Lex the rule into tokens using a rather simple RE method ...
+    my @tokens = ($rule =~ /$ARITH_EXPRESSION_LEXER/og);
 
-  # Obviously, don't trace empty or nonexistent rules
-  my $rule = $conf->{tests}->{$name};
-  unless ($rule) {
-      $conf->{meta_dependencies}->{$name} = '';
-      return ( );
-  }
+    # Set the rule blank to start
+    $meta{$name} = '';
 
-  # Avoid infinite recursion
-  return ( ) if exists $alreadydone->{$name};
-  $alreadydone->{$name} = ( );
+    # List dependencies that are meta tests in the same priority band
+    $meta_deps{$name} = [ ];
 
-  my %deps;
+    # List all rule dependencies
+    $rule_deps{$name} = [ ];
 
-  # Lex the rule into tokens using a rather simple RE method ...
-  my @tokens = ($rule =~ /($ARITH_EXPRESSION_LEXER)/og);
+    # Go through each token in the meta rule
+    foreach my $token (@tokens) {
+      # operator (triage, already validated by is_meta_valid)
+      if ($token !~ tr/+&|()!<>=//c) {
+        $meta{$name} .= "$token ";
+      }
+      # rule-like check for local_tests_only
+      elsif ($token eq 'local_tests_only') {
+        $meta{$name} .= '($_[0]->{main}->{local_tests_only}||0) ';
+      }
+      # ... rulename?
+      elsif ($token =~ IS_RULENAME) {
+        # Will end up later in a compiled sub called from do_meta_tests:
+        #  $_[0] = $pms
+        #  $_[1] = $h ($pms->{tests_already_hit}),
+        $meta{$name} .= "(\$_[1]->{'$token'}||0) ";
+
+        if (!exists $conf->{test_types}->{$token}) {
+          dbg("rules: meta test $name has undefined dependency '$token'");
+          push @{$rule_deps{$name}}, $token;
+          next;
+        }
 
-  # Go through each token in the meta rule
-  my $conf_tests = $conf->{tests};
-  foreach my $token (@tokens) {
-    # has to be an alpha+numeric token
-    next if $token =~ tr{A-Za-z0-9_}{}c || substr($token,0,1) =~ tr{A-Za-z_}{}c; # even faster
+        if ($conf->{scores}->{$token} == 0) {
+          # bug 5040: net rules in a non-net scoreset
+          # there are some cases where this is expected; don't warn
+          # in those cases.
+          unless ((($conf->get_score_set()) & 1) == 0 &&
+              ($conf->{tflags}->{$token}||'') =~ /\bnet\b/)
+          {
+            dbg("rules: meta test $name has dependency '$token' with a zero score");
+          }
+        }
 
-    # and has to be a rule name
-    next unless exists $conf_tests->{$token};
+        # If the token is another meta rule, add it as a dependency
+        if ($conf->{test_types}->{$token} == $Mail::SpamAssassin::Conf::TYPE_META_TESTS) {
+          push @{$meta_deps{$name}}, $token;
+        }
+
+        # Record all dependencies
+        push @{$rule_deps{$name}}, $token;
+      }
+      # ... number or operator (already validated by is_meta_valid)
+      else {
+        $meta{$name} .= "$token ";
+      }
+    }
+  }
+
+  # Sort by length of dependencies list.  It's more likely we'll get
+  # the dependencies worked out this way.
+  my @metas = sort { @{$meta_deps{$a}} <=> @{$meta_deps{$b}} } keys %meta;
+  my $count;
+  do {
+    $count = $#metas;
+    my %metas = map { $_ => 1 } @metas; # keep a small cache for fast lookups
+    # Go through each meta rule we haven't done yet
+    for (my $i = 0 ; $i <= $#metas ; $i++) {
+      next if (grep( $metas{$_}, @{ $meta_deps{ $metas[$i] } }));
+      splice @metas, $i--, 1;    # remove this rule from our list
+    }
+  } while ($#metas != $count && $#metas > -1); # run until we can't go anymore
 
-    # add and recurse
-    $deps{untaint_var($token)} = ( );
-    my @subdeps = $self->_meta_deps_recurse($conf, $toprule, $token, $alreadydone);
-    @deps{@subdeps} = ( );
+  # If there are any rules left, we can't solve the dependencies so complain
+  my %unsolved_metas = map { $_ => 1 } @metas; # keep a small cache for fast lookups
+  foreach my $rulename_t (@metas) {
+    my $msg = "rules: excluding meta test $rulename_t, unsolved meta dependencies: ".
+              join(", ", grep($unsolved_metas{$_}, @{ $meta_deps{$rulename_t} }));
+    $self->lint_warn($msg);
+  }
+
+  foreach my $name (keys %meta) {
+    if ($unsolved_metas{$name}) {
+      $conf->{meta_tests}->{$name} = sub { 0 };
+      $rule_deps{$name} = [ ];
+    }
+    if ($meta{$name} eq '( ) ') {
+      # Bug 8061:
+      # meta FOOBAR () considered a rule declaration to support rule_hits API or
+      #  other dynamic rules, only evaluated at finish_meta_rules unless got_hit.
+      # Other style metas without dependencies will be evaluated immediately.
+      $meta{$name} = '0'; # Evaluating () would result in undef
+    }
+    elsif (@{$rule_deps{$name}}) {
+      $conf->{meta_dependencies}->{$name} = $rule_deps{$name};
+      foreach my $deprule (@{$rule_deps{$name}}) {
+        $conf->{meta_deprules}->{$deprule}->{$name} = 1;
+      }
+    } else {
+      $conf->{meta_nodeps}->{$name} = 1;
+    }
+    # Compile meta sub
+    eval '$conf->{meta_tests}->{$name} = sub { '.$meta{$name}.'};';
+    # Paranoid check
+    die "rules: meta compilation failed for $name: '$meta{$name}': $@" if ($@);
   }
-  $conf->{meta_dependencies}->{$name} = join (' ', keys %deps);
-  return keys %deps;
 }
 
 sub fix_priorities {
   my ($self) = @_;
   my $conf = $self->{conf};
 
-  die unless $conf->{meta_dependencies};    # order requirement
+  return unless $conf->{meta_dependencies};    # order requirement
+
   my $pri = $conf->{priority};
+  my $tflags = $conf->{tflags};
 
   # sort into priority order, lowest first -- this way we ensure that if we
   # rearrange the pri of a rule early on, we cannot accidentally increase its
   # priority later.
-  foreach my $rule (sort {
-            $pri->{$a} <=> $pri->{$b}
-          } keys %{$pri})
-  {
+  foreach my $rule (sort { $pri->{$a} <=> $pri->{$b} } keys %{$pri}) {
     # we only need to worry about meta rules -- they are the
     # only type of rules which depend on other rules
     my $deps = $conf->{meta_dependencies}->{$rule};
     next unless (defined $deps);
 
     my $basepri = $pri->{$rule};
-    foreach my $dep (split ' ', $deps) {
+    foreach my $dep (@$deps) {
       my $deppri = $pri->{$dep};
-      if ($deppri > $basepri) {
-        dbg("rules: $rule (pri $basepri) requires $dep (pri $deppri): fixed");
-        $pri->{$dep} = $basepri;
+      if (defined $deppri && $deppri > $basepri) {
+        if ($basepri < -100 && ($tflags->{$dep}||'') =~ /\bnet\b/) {
+          dbg("rules: $rule (pri $basepri) requires $dep (pri $deppri): fixed to -100 (net rule)");
+          $pri->{$dep} = -100;
+          $conf->{priorities}->{-100}++;
+        } else {
+          dbg("rules: $rule (pri $basepri) requires $dep (pri $deppri): fixed");
+          $pri->{$dep} = $basepri;
+        }
       }
     }
   }
 }
 
-sub find_dup_rules {
+sub fix_tflags {
   my ($self) = @_;
   my $conf = $self->{conf};
-
-  my %names_for_text;
-  my %dups;
-  while (my ($name, $text) = each %{$conf->{tests}}) {
-    my $type = $conf->{test_types}->{$name};
-
-    # skip eval and empty tests
-    next if ($type & 1) ||
-      ($type eq $Mail::SpamAssassin::Conf::TYPE_EMPTY_TESTS);
-
-    my $tf = ($conf->{tflags}->{$name}||''); $tf =~ s/\s+/ /gs;
-    # ensure similar, but differently-typed, rules are not marked as dups;
-    # take tflags into account too due to "tflags multiple"
-    $text = "$type\t$text\t$tf";
-
-    if (defined $names_for_text{$text}) {
-      $names_for_text{$text} .= " ".$name;
-      $dups{$text} = undef;     # found (at least) one
-    } else {
-      $names_for_text{$text} = $name;
-    }
-  }
-
-  foreach my $text (keys %dups) {
-    my $first;
-    my $first_pri;
-    my @names = sort {$a cmp $b} split(' ', $names_for_text{$text});
-    foreach my $name (@names) {
-      my $priority = $conf->{priority}->{$name} || 0;
-
-      if (!defined $first || $priority < $first_pri) {
-        $first_pri = $priority;
-        $first = $name;
+  my $tflags = $conf->{tflags};
+
+  # Inherit net tflags from dependencies
+  while (my($rulename,$deps) = each %{$conf->{meta_dependencies}}) {
+    my $tfl = $tflags->{$rulename}||'';
+    next if $tfl =~ /\bnet\b/;
+    foreach my $deprule (@$deps) {
+      if (($tflags->{$deprule}||'') =~ /\bnet\b/) {
+        dbg("rules: meta $rulename inherits tflag net, depends on $deprule");
+        $tflags->{$rulename} = $tfl eq '' ? 'net' : "$tfl net";
+        last;
       }
     }
-    # $first is now the earliest-occurring rule. mark others as dups
-
-    my @dups;
-    foreach my $name (@names) {
-      next if $name eq $first;
-      push @dups, $name;
-      delete $conf->{tests}->{$name};
-    }
-
-    dbg("rules: $first merged duplicates: ".join(' ', @dups));
-    $conf->{duplicate_rules}->{$first} = \@dups;
   }
 }
 
@@ -1226,10 +1336,11 @@ sub add_test {
 
   # all of these rule types are regexps
   if ($type == $Mail::SpamAssassin::Conf::TYPE_BODY_TESTS ||
-      $type == $Mail::SpamAssassin::Conf::TYPE_FULL_TESTS ||
+      $type == $Mail::SpamAssassin::Conf::TYPE_URI_TESTS ||
       $type == $Mail::SpamAssassin::Conf::TYPE_RAWBODY_TESTS ||
-      $type == $Mail::SpamAssassin::Conf::TYPE_URI_TESTS)
+      $type == $Mail::SpamAssassin::Conf::TYPE_FULL_TESTS)
   {
+    $self->parse_captures($name, \$text);
     my ($rec, $err) = compile_regexp($text, 1, $ignore_amre);
     if (!$rec) {
       $self->lint_warn("config: invalid regexp for $name '$text': $err", $name);
@@ -1239,12 +1350,19 @@ sub add_test {
   }
   elsif ($type == $Mail::SpamAssassin::Conf::TYPE_HEAD_TESTS)
   {
+    # If redefining header test, clear out opt hashes so they don't leak to
+    # the new test.  There are separate hashes for options as it saves lots
+    # of memory (exists, neg, if-unset are rarely used).
+    if (exists $conf->{tests}->{$name}) {
+      delete $conf->{test_opt_exists}->{$name};
+      delete $conf->{test_opt_unset}->{$name};
+      delete $conf->{test_opt_neg}->{$name};
+    }
     local($1,$2,$3);
     # RFC 5322 section 3.6.8, ftext printable US-ASCII chars not including ":"
     # no re "strict";  # since perl 5.21.8: Ranges of ASCII printables...
     if ($text =~ /^exists:(.*)/) {
       my $hdr = $1;
-      # check :addr etc header options
       # $hdr used in eval text, validate carefully
       if ($hdr !~ /^[\w.-]+:?$/) {
         $self->lint_warn("config: invalid head test $name header: $hdr");
@@ -1255,15 +1373,21 @@ sub add_test {
       $conf->{test_opt_exists}->{$name} = 1;
     } else {
       # $hdr used in eval text, validate carefully
+      # check :addr etc header options
       if ($text !~ /^([\w.-]+(?:\:|(?:\:[a-z]+){1,2})?)\s*([=!]~)\s*(.+)$/) {
         $self->lint_warn("config: invalid head test $name: $text");
         return;
       }
       my ($hdr, $op, $pat) = ($1, $2, $3);
       $hdr =~ s/:$//;
+      if ($hdr =~ /:(?!(?:raw|addr|name|host|domain|ip|revip|first|last)\b)/i) {
+        $self->lint_warn("config: invalid header modifier for $name: $hdr", $name);
+        return;
+      }
       if ($pat =~ s/\s+\[if-unset:\s+(.+)\]$//) {
         $conf->{test_opt_unset}->{$name} = $1;
       }
+      $self->parse_captures($name, \$pat);
       my ($rec, $err) = compile_regexp($pat, 1, $ignore_amre);
       if (!$rec) {
         $self->lint_warn("config: invalid regexp for $name '$pat': $err", $name);
@@ -1283,25 +1407,27 @@ sub add_test {
       return;
     }
   }
+  elsif (($type & 1) == 1) { # *_EVALS
+    # create eval_to_rule mappings
+    if (my ($function) = ($text =~ m/(.*?)\s*\(.*?\)\s*$/)) {
+      push @{$conf->{eval_to_rule}->{$function}}, $name;
+    }
+  }
 
   $conf->{tests}->{$name} = $text;
   $conf->{test_types}->{$name} = $type;
 
-  if ($name =~ /AUTOLEARNTEST/i) {
+  if ($name =~ /^AUTOLEARNTEST/) {
      dbg("config: auto-learn: $name has type $type = $conf->{test_types}->{$name} during add_test\n");
   }
 
-  
-  if ($type == $Mail::SpamAssassin::Conf::TYPE_META_TESTS) {
-    $conf->{priority}->{$name} ||= 500;
-  }
-  else {
-    $conf->{priority}->{$name} ||= 0;
-  }
   $conf->{priority}->{$name} ||= 0;
-  $conf->{source_file}->{$name} = $self->{currentfile};
 
   if ($conf->{main}->{keep_config_parsing_metadata}) {
+    # {source_file} eats lots of memory and is unused unless
+    # keep_config_parsing_metadata is set (ruleqa stuff)
+    $conf->{source_file}->{$name} = $self->{currentfile};
+
     $conf->{if_stack}->{$name} = $self->get_if_stack_as_string();
 
     if ($self->{file_scoped_attrs}->{testrules}) {
@@ -1348,8 +1474,8 @@ sub is_meta_valid {
   my $meta = '';
 
   # Paranoid check (Bug #7557)
-  if ($rule =~ /(?:\:\:|->)/)  {
-    warn("config: invalid meta $name rule: $rule") ;
+  if ($rule =~ /(?:\:\:|->|[\$\@\%\;\{\}])/) {
+    warn("config: invalid meta $name rule: $rule\n");
     return 0;
   }
 
@@ -1358,6 +1484,11 @@ sub is_meta_valid {
 
   # Lex the rule into tokens using a rather simple RE method ...
   my @tokens = ($rule =~ /($ARITH_EXPRESSION_LEXER)/og);
+  if (length($name) == 1) {
+    for (@tokens) {
+      print "$name $_\n "  or die "Error writing token: $!";
+    }
+  }
 
   # Go through each token in the meta rule
   foreach my $token (@tokens) {
@@ -1389,6 +1520,26 @@ sub is_meta_valid {
   return 0;
 }
 
+sub parse_captures {
+  my ($self, $name, $re) = @_;
+
+  # Check for named regex capture templates
+  if (index($$re, '%{') >= 0) {
+    local($1);
+    # Replace %{FOO} with %\{FOO\} so compile_regexp doesn't fail with unescaped left brace
+    while ($$re =~ s/(?<!\\)\%\{([A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*(?:\([^\)\}]*\))?)\}/%\\{$1\\}/g) {
+      dbg("config: found named capture for rule $name: $1");
+      $self->{conf}->{capture_template_rules}->{$name}->{$1} = 1;
+    }
+  }
+  # Make rules with captures run before anything else
+  if ($$re =~ /\(\?P?[<'][A-Z]/) {
+    dbg("config: adjusting regex capture rule $name priority to -10000");
+    $self->{conf}->{priority}->{$name} = -10000;
+    $self->{conf}->{capture_rules}->{$name} = 1;
+  }
+}
+
 # Deprecated functions, leave just in case..
 sub is_delimited_regexp_valid {
   my ($self, $rule, $re) = @_;
@@ -1420,7 +1571,12 @@ sub add_to_addrlist {
     $re =~ s/([^\*\?_a-zA-Z0-9])/\\$1/g;       # escape any possible metachars
     $re =~ tr/?/./;                            # "?" -> "."
     $re =~ s/\*+/\.\*/g;                       # "*" -> "any string"
-    $conf->{$singlelist}->{$addr} = "^${re}\$";
+    my ($rec, $err) = compile_regexp("^${re}\$", 0);
+    if (!$rec) {
+      warn "could not compile $singlelist '$addr': $err";
+      return;
+    }
+    $conf->{$singlelist}->{$addr} = $rec;
   }
 }
 
@@ -1439,7 +1595,12 @@ sub add_to_addrlist_rcvd {
     $re =~ s/([^\*\?_a-zA-Z0-9])/\\$1/g;       # escape any possible metachars
     $re =~ tr/?/./;                            # "?" -> "."
     $re =~ s/\*+/\.\*/g;                       # "*" -> "any string"
-    $conf->{$listname}->{$addr}{re} = "^${re}\$";
+    my ($rec, $err) = compile_regexp("^${re}\$", 0);
+    if (!$rec) {
+      warn "could not compile $listname '$addr': $err";
+      return;
+    }
+    $conf->{$listname}->{$addr}{re} = $rec;
     $conf->{$listname}->{$addr}{domain} = [ $domain ];
   }
 }
index bf5fb0225975a46750b3572cad6dfc30aba4e837..754ac307967329bd7817358e38628c8adfb987cc 100644 (file)
@@ -27,7 +27,7 @@ Mail::SpamAssassin::Conf::SQL - load SpamAssassin scores from SQL database
 =head1 DESCRIPTION
 
 Mail::SpamAssassin is a module to identify spam using text analysis and
-several internet-based realtime blacklists.
+several internet-based realtime blocklists.
 
 This class is used internally by SpamAssassin to load scores from an SQL
 database.  Please refer to the C<Mail::SpamAssassin> documentation for public
@@ -166,6 +166,9 @@ sub load_with_dbi {
         }
         if ($config_text ne '') {
           $conf->{main} = $main;
+          $config_text = "file start (sql config)\n".
+                         $config_text.
+                         "file end (sql config)\n";
           $conf->parse_scores_only($config_text);
           delete $conf->{main};
         }
index 9e54cbd0f7a02eac8f9fb9953ebcff4943cb2deb..574d8e6a51557011adc53caab481167ab1bad109 100644 (file)
@@ -34,13 +34,14 @@ our(@BAYES_VARS, @IP_VARS, @SA_VARS, %EXPORT_TAGS, @EXPORT_OK);
 BEGIN { 
   @IP_VARS = qw(
        IP_IN_RESERVED_RANGE IP_PRIVATE LOCALHOST IPV4_ADDRESS IP_ADDRESS
+       IS_IP_PRIVATE IS_LOCALHOST IS_IPV4_ADDRESS IS_IP_ADDRESS
   );
   @BAYES_VARS = qw(
        DUMP_MAGIC DUMP_TOKEN DUMP_BACKUP 
   );
   # These are generic constants that may be used across several modules
   @SA_VARS = qw(
-       HARVEST_DNSBL_PRIORITY MBX_SEPARATOR
+       MBX_SEPARATOR
        MAX_BODY_LINE_LENGTH MAX_HEADER_KEY_LENGTH MAX_HEADER_VALUE_LENGTH
        MAX_HEADER_LENGTH ARITH_EXPRESSION_LEXER AI_TIME_UNKNOWN
        CHARSETS_LIKELY_TO_FP_AS_CAPS MAX_URI_LENGTH RULENAME_RE IS_RULENAME
@@ -168,7 +169,10 @@ use constant IP_PRIVATE => qr{^(?:
     )
     (?![a-f0-9:])
   )
-)}oxi;
+)}xi;
+
+# exact match
+use constant IS_IP_PRIVATE => qr/^${\(IP_PRIVATE)}$/;
 
 # backward compatibility
 use constant IP_IN_RESERVED_RANGE => IP_PRIVATE;
@@ -246,7 +250,10 @@ use constant LOCALHOST => qr/
                      )
                      (?![a-f0-9:])
                    )
-                 /oxi;
+                 /xi;
+
+# exact match
+use constant IS_LOCALHOST => qr/^${\(LOCALHOST)}$/;
 
 # ---------------------------------------------------------------------------
 # an IP address, in IPv4 format only.
@@ -256,11 +263,13 @@ use constant IPV4_ADDRESS => qr/\b
                     (?:1\d\d|2[0-4]\d|25[0-5]|[1-9]\d|\d)\.
                     (?:1\d\d|2[0-4]\d|25[0-5]|[1-9]\d|\d)\.
                     (?:1\d\d|2[0-4]\d|25[0-5]|[1-9]\d|\d)
-                  \b/ox;
+                  \b/x;
+
+# exact match
+use constant IS_IPV4_ADDRESS => qr/^${\(IPV4_ADDRESS)}$/;
 
 # ---------------------------------------------------------------------------
-# an IP address, in IPv4, IPv4-mapped-in-IPv6, or IPv6 format.  NOTE: cannot
-# just refer to $IPV4_ADDRESS, due to perl bug reported in nesting qr//s. :(
+# an IP address, in IPv4, IPv4-mapped-in-IPv6, or IPv6 format.
 #
 use constant IP_ADDRESS => qr/
                    (?:
@@ -344,11 +353,12 @@ use constant IP_ADDRESS => qr/
                      )
                      (?![a-f0-9:])
                    )
-                 /oxi;
+                 /xi;
 
-# ---------------------------------------------------------------------------
+# exact match
+use constant IS_IP_ADDRESS => qr/^${\(IP_ADDRESS)}$/;
 
-use constant HARVEST_DNSBL_PRIORITY =>  500;
+# ---------------------------------------------------------------------------
 
 # regular expression that matches message separators in The University of
 # Washington's MBX mailbox format
@@ -388,7 +398,7 @@ use constant ARITH_EXPRESSION_LEXER => qr/(?:
         !=|                                     # NEQ
         [\+\-\*\/]|                             # Mathematical Operator
         [\?:]                                   # ? : Operator
-      )/ox;
+      )/x;
 
 # ArchiveIterator
 
index 6aaed86cd05fab769e08c4e12f886a0d93fd3c57..d033e37ce159026f02c69c54d46bfb60ff134794 100644 (file)
@@ -53,23 +53,23 @@ sub new_checker {
     'locked_file'      => ''
   };
 
-  my @order = split(/\s+/, $main->{conf}->{auto_whitelist_db_modules});
+  my @order = split(/\s+/, $main->{conf}->{auto_welcomelist_db_modules});
   untaint_var(\@order);
   my $dbm_module = Mail::SpamAssassin::Util::first_available_module (@order);
   if (!$dbm_module) {
-    die "auto-whitelist: cannot find a usable DB package from auto_whitelist_db_modules: " .
-       $main->{conf}->{auto_whitelist_db_modules}."\n";
+    die "auto-welcomelist: cannot find a usable DB package from auto_welcomelist_db_modules: " .
+       $main->{conf}->{auto_welcomelist_db_modules}."\n";
   }
 
-  my $umask = umask ~ (oct($main->{conf}->{auto_whitelist_file_mode}));
+  my $umask = umask ~ (oct($main->{conf}->{auto_welcomelist_file_mode}));
 
   # if undef then don't worry -- empty hash!
-  if (defined($main->{conf}->{auto_whitelist_path})) {
-    my $path = $main->sed_path($main->{conf}->{auto_whitelist_path});
+  if (defined($main->{conf}->{auto_welcomelist_path})) {
+    my $path = $main->sed_path($main->{conf}->{auto_welcomelist_path});
     my ($mod1, $mod2);
 
     if ($main->{locker}->safe_lock
-            ($path, 30, $main->{conf}->{auto_whitelist_file_mode}))
+            ($path, 30, $main->{conf}->{auto_welcomelist_file_mode}))
     {
       $self->{locked_file} = $path;
       $self->{is_locked}   = 1;
@@ -80,20 +80,20 @@ sub new_checker {
       ($mod1, $mod2) = ('R/O', O_RDONLY);
     }
 
-    dbg("auto-whitelist: tie-ing to DB file of type $dbm_module $mod1 in $path");
+    dbg("auto-welcomelist: tie-ing to DB file of type $dbm_module $mod1 in $path");
 
     ($self->{is_locked} && $dbm_module eq 'DB_File') and
             Mail::SpamAssassin::Util::avoid_db_file_locking_bug($path);
 
     if (! tie %{ $self->{accum} }, $dbm_module, $path, $mod2,
-            oct($main->{conf}->{auto_whitelist_file_mode}) & 0666)
+            oct($main->{conf}->{auto_welcomelist_file_mode}) & 0666)
     {
       my $err = $!;   # might get overwritten later
       if ($self->{is_locked}) {
         $self->{main}->{locker}->safe_unlock($self->{locked_file});
         $self->{is_locked} = 0;
       }
-      die "auto-whitelist: cannot open auto_whitelist_path $path: $err\n";
+      die "auto-welcomelist: cannot open auto_welcomelist_path $path: $err\n";
     }
   }
   umask $umask;
@@ -106,10 +106,10 @@ sub new_checker {
 
 sub finish {
   my $self = shift;
-  dbg("auto-whitelist: DB addr list: untie-ing and unlocking");
+  dbg("auto-welcomelist: DB addr list: untie-ing and unlocking");
   untie %{$self->{accum}};
   if ($self->{is_locked}) {
-    dbg("auto-whitelist: DB addr list: file locked, breaking lock");
+    dbg("auto-welcomelist: DB addr list: file locked, breaking lock");
     $self->{main}->{locker}->safe_unlock ($self->{locked_file});
     $self->{is_locked} = 0;
   }
@@ -128,7 +128,7 @@ sub get_addr_entry {
   $entry->{msgcount} = $self->{accum}->{$addr} || 0;
   $entry->{totscore} = $self->{accum}->{$addr.'|totscore'} || 0;
 
-  dbg("auto-whitelist: db-based $addr scores ".$entry->{msgcount}.'/'.$entry->{totscore});
+  dbg("auto-welcomelist: db-based $addr scores ".$entry->{msgcount}.'/'.$entry->{totscore});
   return $entry;
 }
 
@@ -143,7 +143,7 @@ sub add_score {
     $entry->{msgcount}++;
     $entry->{totscore} += $score;
 
-    dbg("auto-whitelist: add_score: new count: ".$entry->{msgcount}.", new totscore: ".$entry->{totscore});
+    dbg("auto-welcomelist: add_score: new count: ".$entry->{msgcount}.", new totscore: ".$entry->{totscore});
 
     $self->{accum}->{$entry->{addr}} = $entry->{msgcount};
     $self->{accum}->{$entry->{addr}.'|totscore'} = $entry->{totscore};
index 3c75efa68a1125cab6e43991ee4040e57949bcc6..f6bb21aee1835f8e092860b4054ed280c98789d7 100644 (file)
@@ -29,7 +29,7 @@ use Mail::SpamAssassin::Conf;
 use Mail::SpamAssassin::PerMsgStatus;
 use Mail::SpamAssassin::AsyncLoop;
 use Mail::SpamAssassin::Constants qw(:ip);
-use Mail::SpamAssassin::Util qw(untaint_var am_running_on_windows);
+use Mail::SpamAssassin::Util qw(untaint_var am_running_on_windows compile_regexp);
 
 use File::Spec;
 use IO::Socket;
@@ -37,30 +37,24 @@ use POSIX ":sys_wait_h";
 
 
 our $KNOWN_BAD_DIALUP_RANGES; # Nothing uses this var???
-our $LAST_DNS_CHECK;
+our $LAST_DNS_CHECK = 0;
 
 # use very well-connected domains (fast DNS response, many DNS servers,
 # geographical distribution is a plus, TTL of at least 3600s)
+# these MUST contain both A/AAAA records so we can test dns_options v6
+# Updated 8/2019 from https://ip6.nl/#!list?db=alexa500
+# 
 our @EXISTING_DOMAINS = qw{
-  adelphia.net
   akamai.com
-  apache.org
-  cingular.com
-  colorado.edu
-  comcast.net
-  doubleclick.com
-  ebay.com
-  gmx.net
+  bing.com
+  cloudflare.com
+  digitalpoint.com
+  facebook.com
   google.com
-  intel.com
-  kernel.org
-  linux.org
-  mit.edu
-  motorola.com
-  msn.com
-  sourceforge.net
-  sun.com
-  w3.org
+  linkedin.com
+  netflix.com
+  php.net
+  wikipedia.org
   yahoo.com
 };
 
@@ -84,10 +78,6 @@ BEGIN {
 # local ($^W) = 0;
 
   no warnings;
-  eval {
-    require Net::DNS;
-    require Net::DNS::Resolver;
-  };
   eval {
     require MIME::Base64;
   };
@@ -101,71 +91,52 @@ BEGIN {
 sub do_rbl_lookup {
   my ($self, $rule, $set, $type, $host, $subtest) = @_;
 
-  $host =~ s/\.\z//s;  # strip a redundant trailing dot
-  my $key = "dns:$type:$host";
-  my $existing_ent = $self->{async}->get_lookup($key);
-
-  # only make a specific query once
-  if (!$existing_ent) {
-    my $ent = {
-      key => $key,
-      zone => $host,  # serves to fetch other per-zone settings
-      type => "DNSBL-".$type,
-      sets => [ ],  # filled in below
-      rules => [ ], # filled in below
-      # id is filled in after we send the query below
-    };
-    $existing_ent = $self->{async}->bgsend_and_start_lookup(
-        $host, $type, undef, $ent,
-        sub { my($ent, $pkt) = @_; $self->process_dnsbl_result($ent, $pkt) },
-      master_deadline => $self->{master_deadline} );
-  }
-
-  if ($existing_ent) {
-    # always add set
-    push @{$existing_ent->{sets}}, $set;
-
-    # sometimes match or always match
-    if (defined $subtest) {
-      $self->{dnspost}->{$set}->{$subtest} = $rule;
-    } else {
-      push @{$existing_ent->{rules}}, $rule;
+  if (defined $subtest) {
+    if ($subtest =~ /^sb:/) {
+      info("dns: ignored $rule, SenderBase rules are deprecated");
+      return 0;
+    }
+    # Compile as regex if not pure ip/bitmask (same check in process_dnsbl_result)
+    if ($subtest !~ /^\d+(?:\.\d+\.\d+\.\d+)?$/) {
+      my ($rec, $err) = compile_regexp($subtest, 0);
+      if (!$rec) {
+        warn("dns: invalid rule $rule subtest regexp '$subtest': $err\n");
+        return 0;
+      }
+      $subtest = $rec;
     }
-
-    $self->{rule_to_rblkey}->{$rule} = $key;
   }
-}
 
-# TODO: these are constant so they should only be added once at startup
-sub register_rbl_subtest {
-  my ($self, $rule, $set, $subtest) = @_;
+  dbg("dns: launching rule %s, set %s, type %s, %s", $rule, $set, $type,
+    defined $subtest ? "subtest $subtest" : 'no subtest');
 
-  if ($subtest =~ /^sb:/) {
-    warn("dns: ignored $rule, SenderBase rules are deprecated\n");
-    return 0;
-  }
+  my $ent = {
+    rulename => $rule,
+    type => "DNSBL",
+    set => $set,
+    subtest => $subtest,
+  };
+  my $ret = $self->{async}->bgsend_and_start_lookup($host, $type, undef, $ent,
+    sub { my($ent, $pkt) = @_; $self->process_dnsbl_result($ent, $pkt) },
+    master_deadline => $self->{master_deadline}
+  );
 
-  $self->{dnspost}->{$set}->{$subtest} = $rule;
+  return 0 if defined $ret; # no query started
+  return; # return undef for async status
 }
 
+# Deprecated, was only used from DNSEval.pm?
 sub do_dns_lookup {
   my ($self, $rule, $type, $host) = @_;
 
-  $host =~ s/\.\z//s;  # strip a redundant trailing dot
-  my $key = "dns:$type:$host";
-
   my $ent = {
-    key => $key,
-    zone => $host,  # serves to fetch other per-zone settings
-    type => "DNSBL-".$type,
-    rules => [ $rule ],
-    # id is filled in after we send the query below
+    rulename => $rule,
+    type => "DNSBL",
   };
-  $ent = $self->{async}->bgsend_and_start_lookup(
-      $host, $type, undef, $ent,
-      sub { my($ent, $pkt) = @_; $self->process_dnsbl_result($ent, $pkt) },
-    master_deadline => $self->{master_deadline} );
-  $ent;
+  $self->{async}->bgsend_and_start_lookup($host, $type, undef, $ent,
+    sub { my($ent, $pkt) = @_; $self->process_dnsbl_result($ent, $pkt) },
+    master_deadline => $self->{master_deadline}
+  );
 }
 
 ###########################################################################
@@ -180,38 +151,31 @@ sub dnsbl_hit {
     # txtdata returns a non- zone-file-format encoded result, unlike rdstring;
     # avoid space-separated RDATA <character-string> fields if possible,
     # txtdata provides a list of strings in a list context since Net::DNS 0.69
-    $log = join('',$answer->txtdata);
+    $log = join('', $answer->txtdata);
+    utf8::encode($log)  if utf8::is_utf8($log);
     local $1;
     $log =~ s{ (?<! [<(\[] ) (https? : // \S+)}{<$1>}xgi;
   } else {  # assuming $answer->type eq 'A'
     local($1,$2,$3,$4,$5);
-    if ($question->string =~ m/^((?:[0-9a-fA-F]\.){32})(\S+\w)/) {
+    if ($question->string =~ /^((?:[0-9a-fA-F]\.){32})(\S+\w)/) {
       $log = ' listed in ' . lc($2);
       my $ipv6addr = join('', reverse split(/\./, lc $1));
       $ipv6addr =~ s/\G(....)/$1:/g;  chop $ipv6addr;
       $ipv6addr =~ s/:0{1,3}/:/g;
       $log = $ipv6addr . $log;
-    } elsif ($question->string =~ m/^(\d+)\.(\d+)\.(\d+)\.(\d+)\.(\S+\w)/) {
+    } elsif ($question->string =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)\.(\S+\w)/) {
       $log = "$4.$3.$2.$1 listed in " . lc($5);
-    } else {
-      $log = 'listed in ' . $question->string;
+    } elsif ($question->string =~ /^(\S+)(?<!\.)/) {
+      $log = "listed in ".lc($1);
     }
   }
 
-  # TODO: this may result in some log messages appearing under the
-  # wrong rules, since we could see this sequence: { test one hits,
-  # test one's message is logged, test two hits, test one fires again
-  # on another IP, test one's message is logged for that other IP --
-  # but under test two's heading }.   Right now though it's better
-  # than just not logging at all.
-
-  $self->{already_logged} ||= { };
-  if ($log && !$self->{already_logged}->{$log}) {
-    $self->test_log($log);
-    $self->{already_logged}->{$log} = 1;
+  if ($log) {
+    $self->test_log($log, $rule);
   }
 
   if (!$self->{tests_already_hit}->{$rule}) {
+    dbg("dns: rbl rule $rule hit");
     $self->got_hit($rule, "RBL: ", ruletype => "dnsbl");
   }
 }
@@ -219,16 +183,26 @@ sub dnsbl_hit {
 sub dnsbl_uri {
   my ($self, $question, $answer) = @_;
 
-  my $qname = $question->qname;
+  my $rdatastr;
+  if ($answer->UNIVERSAL::can('txtdata')) {
+    # txtdata returns a non- zone-file-format encoded result, unlike rdstring;
+    # avoid space-separated RDATA <character-string> fields if possible,
+    # txtdata provides a list of strings in a list context since Net::DNS 0.69
+    $rdatastr = join('', $answer->txtdata);
+  } else {
+    $rdatastr = $answer->rdstring;
+    # encoded in a RFC 1035 zone file format (escaped), decode it
+    $rdatastr =~ s{ \\ ( [0-9]{3} | (?![0-9]{3}) . ) }
+                  { length($1)==3 && $1 <= 255 ? chr($1) : $1 }xgse;
+  }
 
-  # txtdata returns a non- zone-file-format encoded result, unlike rdstring;
-  # avoid space-separated RDATA <character-string> fields if possible,
-  # txtdata provides a list of strings in a list context since Net::DNS 0.69
-  #
-  # rdatastr() is historical/undocumented, use rdstring() since Net::DNS 0.69
-  my $rdatastr = $answer->UNIVERSAL::can('txtdata') ? join('',$answer->txtdata)
-               : $answer->UNIVERSAL::can('rdstring') ? $answer->rdstring
-                                                    : $answer->rdatastr;
+  # Bug 7236: Net::DNS attempts to decode text strings in a TXT record as
+  # UTF-8 since version 0.69, which is undesired: octets failing the UTF-8
+  # decoding are converted to a Unicode "replacement character" U+FFFD, and
+  # ASCII text is unnecessarily flagged as perl native characters.
+  utf8::encode($rdatastr)  if utf8::is_utf8($rdatastr);
+
+  my $qname = $question->qname;
   if (defined $qname && defined $rdatastr) {
     my $qclass = $question->qclass;
     my $qtype = $question->qtype;
@@ -236,8 +210,8 @@ sub dnsbl_uri {
     push(@vals, "class=$qclass") if $qclass ne "IN";
     push(@vals, "type=$qtype") if $qtype ne "A";
     my $uri = "dns:$qname" . (@vals ? "?" . join(";", @vals) : "");
-    push @{ $self->{dnsuri}->{$uri} }, $rdatastr;
 
+    $self->{dnsuri}{$uri}{$rdatastr} = 1;
     dbg("dns: hit <$uri> $rdatastr");
   }
 }
@@ -251,19 +225,17 @@ sub process_dnsbl_result {
   my $question = ($pkt->question)[0];
   return if !$question;
 
-  my $sets = $ent->{sets} || [];
-  my $rules = $ent->{rules};
+  my $rulename = $ent->{rulename};
 
-  # NO_DNS_FOR_FROM
-  if ($self->{sender_host} &&
-        # fishy, qname should have been "RFC 1035 zone format" -decoded first
-      lc($question->qname) eq lc($self->{sender_host}) &&
-      $question->qtype =~ /^(?:A|MX)$/ &&
-      $pkt->header->rcode =~ /^(?:NXDOMAIN|SERVFAIL)$/ &&
-      ++$self->{sender_host_fail} == 2)
-  {
-    for my $rule (@{$rules}) {
-      $self->got_hit($rule, "DNS: ", ruletype => "dns");
+  # Mark rule ready for meta rules, but only if this was the last lookup
+  # pending, rules can have many lookups launched for different IPs
+  if (!$self->get_async_pending_rules($rulename)) {
+    $self->rule_ready($rulename);
+    # Mark depending check_rbl_sub rules too
+    if (exists $self->{rbl_subs}{$ent->{set}}) {
+      foreach (@{$self->{rbl_subs}{$ent->{set}}}) {
+        $self->rule_ready($_->[1]);
+      }
     }
   }
 
@@ -274,86 +246,92 @@ sub process_dnsbl_result {
     $self->dnsbl_uri($question, $answer);
     my $answ_type = $answer->type;
     # TODO: there are some CNAME returns that might be useful
-    next if ($answ_type ne 'A' && $answ_type ne 'TXT');
-    if ($answ_type eq 'A') {
-      # Net::DNS::RR::A::address() is available since Net::DNS 0.69
-      my $ip_address = $answer->UNIVERSAL::can('address') ? $answer->address
-                                                          : $answer->rdatastr;
-      # skip any A record that isn't on 127.0.0.0/8
-      next if $ip_address !~ /^127\./;
-    }
-    for my $rule (@{$rules}) {
-      $self->dnsbl_hit($rule, $question, $answer);
+    next if $answ_type ne 'A' && $answ_type ne 'TXT';
+
+    my $rdatastr;
+    if ($answer->UNIVERSAL::can('txtdata')) {
+      # txtdata returns a non- zone-file-format encoded result, unlike rdstring;
+      # avoid space-separated RDATA <character-string> fields if possible,
+      # txtdata provides a list of strings in a list context since Net::DNS 0.69
+      $rdatastr = join('', $answer->txtdata);
+    } else {
+      $rdatastr = $answer->rdstring;
+      # encoded in a RFC 1035 zone file format (escaped), decode it
+      $rdatastr =~ s{ \\ ( [0-9]{3} | (?![0-9]{3}) . ) }
+                    { length($1)==3 && $1 <= 255 ? chr($1) : $1 }xgse;
     }
-    for my $set (@{$sets}) {
-      if ($self->{dnspost}->{$set}) {
-       $self->process_dnsbl_set($set, $question, $answer);
+
+    # Bug 7236: Net::DNS attempts to decode text strings in a TXT record as
+    # UTF-8 since version 0.69, which is undesired: octets failing the UTF-8
+    # decoding are converted to a Unicode "replacement character" U+FFFD, and
+    # ASCII text is unnecessarily flagged as perl native characters.
+    utf8::encode($rdatastr)  if utf8::is_utf8($rdatastr);
+
+    # skip any A record that isn't on 127.0.0.0/8
+    next if $answ_type eq 'A' && $rdatastr !~ /^127\./;
+
+    # check_rbl tests
+    if (defined $ent->{subtest}) {
+      if ($self->check_subtest($rdatastr, $ent->{subtest})) {
+        $self->dnsbl_hit($rulename, $question, $answer);
       }
+    } else {
+      $self->dnsbl_hit($rulename, $question, $answer);
+    }
+
+    # check_rbl_sub tests
+    if (exists $self->{rbl_subs}{$ent->{set}}) {
+      $self->process_dnsbl_set($ent->{set}, $question, $answer, $rdatastr);
     }
   }
+
   return 1;
 }
 
 sub process_dnsbl_set {
-  my ($self, $set, $question, $answer) = @_;
+  my ($self, $set, $question, $answer, $rdatastr) = @_;
 
-  # txtdata returns a non- zone-file-format encoded result, unlike rdstring;
-  # avoid space-separated RDATA <character-string> fields if possible,
-  # txtdata provides a list of strings in a list context since Net::DNS 0.69
-  #
-  # rdatastr() is historical/undocumented, use rdstring() since Net::DNS 0.69
-  my $rdatastr = $answer->UNIVERSAL::can('txtdata')  ? join('',$answer->txtdata)
-               : $answer->UNIVERSAL::can('rdstring') ? $answer->rdstring
-                                                    : $answer->rdatastr;
-
-  while (my ($subtest, $rule) = each %{ $self->{dnspost}->{$set} }) {
+  foreach my $args (@{$self->{rbl_subs}{$set}}) {
+    my $subtest = $args->[0];
+    my $rule = $args->[1];
     next if $self->{tests_already_hit}->{$rule};
-
-    if ($subtest =~ /^\d+\.\d+\.\d+\.\d+$/) {
-      # test for exact equality, not a regexp (an IPv4 address)
-      $self->dnsbl_hit($rule, $question, $answer)  if $subtest eq $rdatastr;
-    }
-    # bitmask
-    elsif ($subtest =~ /^\d+$/) {
-      # Bug 6803: response should be within 127.0.0.0/8, ignore otherwise
-      if ($rdatastr =~ m/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ &&
-         Mail::SpamAssassin::Util::my_inet_aton($rdatastr) & $subtest)
-      {
-       $self->dnsbl_hit($rule, $question, $answer);
-      }
-    }
-    # regular expression
-    else {
-      my $test = qr/$subtest/;
-      if ($rdatastr =~ /$test/) {
-       $self->dnsbl_hit($rule, $question, $answer);
-      }
+    if ($self->check_subtest($rdatastr, $subtest)) {
+      $self->dnsbl_hit($rule, $question, $answer);
     }
   }
 }
 
-sub harvest_until_rule_completes {
-  my ($self, $rule) = @_;
-
-  dbg("dns: harvest_until_rule_completes");
-  my $result = 0;
-
-  for (my $first=1;  ; $first=0) {
-    # complete_lookups() may call completed_callback(), which may
-    # call start_lookup() again (like in Plugin::URIDNSBL)
-    my ($alldone,$anydone) =
-      $self->{async}->complete_lookups($first ? 0 : 1.0,  1);
-
-    $result = 1  if $self->is_rule_complete($rule);
-    last  if $result || $alldone;
+sub check_subtest {
+  my ($self, $rdatastr, $subtest) = @_;
 
-    dbg("dns: harvest_until_rule_completes - check_tick");
-    $self->{main}->call_plugins ("check_tick", { permsgstatus => $self });
+  # regular expression
+  if (ref($subtest) eq 'Regexp') {
+    if ($rdatastr =~ $subtest) {
+      return 1;
+    }
+  }
+  # bitmask
+  elsif ($subtest =~ /^\d+$/) {
+    # Bug 6803: response should be within 127.0.0.0/8, ignore otherwise
+    if ($rdatastr =~ m/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ &&
+        Mail::SpamAssassin::Util::my_inet_aton($rdatastr) & $subtest)
+    {
+      return 1;
+    }
+  }
+  else {
+    # test for exact equality (an IPv4 address)
+    if ($subtest eq $rdatastr) {
+      return 1;
+    }
   }
 
-  return $result;
+  return 0;
 }
 
+# Deprecated since 4.0, meta rules do not depend on priorities anymore
+sub harvest_until_rule_completes {}
+
 sub harvest_dnsbl_queries {
   my ($self) = @_;
 
@@ -370,7 +348,7 @@ sub harvest_dnsbl_queries {
     my ($alldone,$anydone) =
       $self->{async}->complete_lookups($first ? 0 : 1.0,  1);
 
-    last  if $alldone;
+    last  if $alldone || $self->{deadline_exceeded} || $self->{shortcircuited};
 
     dbg("dns: harvest_dnsbl_queries - check_tick");
     $self->{main}->call_plugins ("check_tick", { permsgstatus => $self });
@@ -379,7 +357,6 @@ sub harvest_dnsbl_queries {
   # explicitly abort anything left
   $self->{async}->abort_remaining_lookups();
   $self->{async}->log_lookups_timing();
-  $self->mark_all_async_rules_complete();
   1;
 }
 
@@ -403,12 +380,14 @@ sub harvest_completed_queries {
 sub set_rbl_tag_data {
   my ($self) = @_;
 
+  return if !$self->{dnsuri};
+
   # DNS URIs
   my $rbl_tag = $self->{tag_data}->{RBL};  # just in case, should be empty
   $rbl_tag = ''  if !defined $rbl_tag;
-  while (my ($dnsuri, $answers) = each %{ $self->{dnsuri} }) {
+  while (my ($dnsuri, $answers) = each %{$self->{dnsuri}}) {
     # when parsing, look for elements of \".*?\" or \S+ with ", " as separator
-    $rbl_tag .= "<$dnsuri>" . " [" . join(", ", @{ $answers }) . "]\n";
+    $rbl_tag .= "<$dnsuri>" . " [" . join(", ", keys %$answers) . "]\n";
   }
   if (defined $rbl_tag && $rbl_tag ne '') {
     chomp $rbl_tag;
@@ -423,7 +402,7 @@ sub rbl_finish {
 
   $self->set_rbl_tag_data();
 
-  delete $self->{dnspost};
+  delete $self->{rbl_subs};
   delete $self->{dnsuri};
 }
 
@@ -442,55 +421,103 @@ sub clear_resolver {
   return 0;
 }
 
+# Deprecated since 4.0.0
 sub lookup_ns {
+  warn "dns: deprecated lookup_ns called, query ignored\n";
+  return;
+}
+
+sub test_dns_a_aaaa {
   my ($self, $dom) = @_;
 
-  return unless $self->load_resolver();
   return if ($self->server_failed_to_respond_for_domain ($dom));
 
-  my $nsrecords;
-  dbg("dns: looking up NS for '$dom'");
+  my ($a, $aaaa) = (0, 0);
 
-  eval {
-    my $query = $self->{resolver}->send($dom, 'NS');
-    my @nses;
-    if ($query) {
-      foreach my $rr ($query->answer) {
-        if ($rr->type eq "NS") { push (@nses, $rr->nsdname); }
+  if ($self->{conf}->{dns_options}->{v4}) {
+    eval {
+      my $query = $self->{resolver}->send($dom, 'A');
+      if ($query) {
+        foreach my $rr ($query->answer) {
+          if ($rr->type eq 'A') { $a = 1; last; }
+        }
       }
+      1;
+    } or do {
+      my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
+      dbg("dns: test A lookup failed horribly, perhaps bad resolv.conf setting? (%s)", $eval_stat);
+      return (undef, undef);
+    };
+    if (!$a) {
+      dbg("dns: test A lookup returned no results, use \"dns_options nov4\" if resolver doesn't support A queries");
     }
-    $nsrecords = [ @nses ];
-    1;
-  } or do {
-    my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
-    dbg("dns: NS lookup failed horribly, perhaps bad resolv.conf setting? (%s)", $eval_stat);
-    return;
-  };
+  } else {
+    $a = 1;
+  }
 
-  $nsrecords;
+  if ($self->{conf}->{dns_options}->{v6}) {
+    eval {
+      my $query = $self->{resolver}->send($dom, 'AAAA');
+      if ($query) {
+        foreach my $rr ($query->answer) {
+          if ($rr->type eq 'AAAA') { $aaaa = 1; last; }
+        }
+      }
+      1;
+    } or do {
+      my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
+      dbg("dns: test AAAA lookup failed horribly, perhaps bad resolv.conf setting? (%s)", $eval_stat);
+      return (undef, undef);
+    };
+    if (!$aaaa) {
+      dbg("dns: test AAAA lookup returned no results, use \"dns_options nov6\" if resolver doesn't support AAAA queries");
+    }
+  } else {
+    $aaaa = 1;
+  }
+
+  return ($a, $aaaa);
 }
 
 sub is_dns_available {
   my ($self) = @_;
   my $dnsopt = $self->{conf}->{dns_available};
-  my $dnsint = $self->{conf}->{dns_test_interval} || 600;
-  my @domains;
 
-  $LAST_DNS_CHECK ||= 0;
-  my $diff = time() - $LAST_DNS_CHECK;
+  # Fast response for the most common cases
+  return 1 if $IS_DNS_AVAILABLE && $dnsopt eq "yes";
+  return 0 if defined $IS_DNS_AVAILABLE && $dnsopt eq "no";
+
+  # croak on misconfigured flags
+  if (!$self->{conf}->{dns_options}->{v4} &&
+      !$self->{conf}->{dns_options}->{v6})
+  {
+    warn 'dns: error: dns_options "nov4" and "nov6" are both set, '.
+         ' only use either, or use "dns_available no" to really disable DNS'.
+         "\n";
+    $IS_DNS_AVAILABLE = 0;
+    $self->{conf}->{dns_available} = "no";
+    return 0;
+  }
 
   # undef $IS_DNS_AVAILABLE if we should be testing for
   # working DNS and our check interval time has passed
-  if ($dnsopt eq "test" && $diff > $dnsint) {
-    $IS_DNS_AVAILABLE = undef;
-    dbg("dns: is_dns_available() last checked %.1f seconds ago; re-checking",
-        $diff);
+  if ($dnsopt eq "test") {
+    my $diff = time - $LAST_DNS_CHECK;
+    if ($diff > ($self->{conf}->{dns_test_interval}||600)) {
+      $IS_DNS_AVAILABLE = undef;
+      if ($LAST_DNS_CHECK) {
+        dbg("dns: is_dns_available() last checked %.1f seconds ago; re-checking", $diff);
+      } else {
+        dbg("dns: is_dns_available() initial check");
+      }
+    }
+    $LAST_DNS_CHECK = time;
   }
 
-  return $IS_DNS_AVAILABLE if (defined $IS_DNS_AVAILABLE);
-  $LAST_DNS_CHECK = time();
+  return $IS_DNS_AVAILABLE if defined $IS_DNS_AVAILABLE;
 
   $IS_DNS_AVAILABLE = 0;
+
   if ($dnsopt eq "no") {
     dbg("dns: dns_available set to no in config file, skipping test");
     return $IS_DNS_AVAILABLE;
@@ -498,26 +525,16 @@ sub is_dns_available {
 
   # Even if "dns_available" is explicitly set to "yes", we want to ignore
   # DNS if we're only supposed to be looking at local tests.
-  goto done if ($self->{main}->{local_tests_only});
-
-  # Check version numbers - runtime check only
-  if (defined $Net::DNS::VERSION) {
-    if (am_running_on_windows()) {
-      if ($Net::DNS::VERSION < 0.46) {
-       warn("dns: Net::DNS version is $Net::DNS::VERSION, but need 0.46 for Win32");
-       return $IS_DNS_AVAILABLE;
-      }
-    }
-    else {
-      if ($Net::DNS::VERSION < 0.34) {
-       warn("dns: Net::DNS version is $Net::DNS::VERSION, but need 0.34");
-       return $IS_DNS_AVAILABLE;
-      }
-    }
+  if ($self->{main}->{local_tests_only}) {
+    dbg("dns: using local tests only, DNS not available");
+    return $IS_DNS_AVAILABLE;
   }
 
-  $self->clear_resolver();
-  goto done unless $self->load_resolver();
+  #$self->clear_resolver();
+  if (!$self->load_resolver()) {
+    dbg("dns: could not load resolver, DNS not available");
+    return $IS_DNS_AVAILABLE;
+  }
 
   if ($dnsopt eq "yes") {
     # optionally shuffle the list of nameservers to distribute the load
@@ -532,13 +549,18 @@ sub is_dns_available {
     return $IS_DNS_AVAILABLE;
   }
 
+  my @domains;
+  my @rtypes;
+  push @rtypes, 'A' if $self->{main}->{conf}->{dns_options}->{v4};
+  push @rtypes, 'AAAA' if $self->{main}->{conf}->{dns_options}->{v6};
   if ($dnsopt =~ /^test:\s*(\S.*)$/) {
     @domains = split (/\s+/, $1);
-    dbg("dns: looking up NS records for user specified domains: %s",
-        join(", ", @domains));
+    dbg("dns: testing %s records for user specified domains: %s",
+        join("/", @rtypes), join(", ", @domains));
   } else {
     @domains = @EXISTING_DOMAINS;
-    dbg("dns: looking up NS records for built-in domains");
+    dbg("dns: testing %s records for built-in domains: %s",
+        join("/", @rtypes), join(", ", @domains));
   }
 
   # do the test with a full set of configured nameservers
@@ -556,19 +578,19 @@ sub is_dns_available {
   my @good_nameservers;
   foreach my $ns (@nameservers) {
     $self->{resolver}->available_nameservers($ns);  # try just this one
-    for (my $retry = 3; $retry > 0 && @domains; $retry--) {
+    for (my $retry = 0; $retry < 3 && @domains; $retry++) {
       my $domain = splice(@domains, rand(@domains), 1);
-      dbg("dns: trying ($retry) $domain, server $ns ...");
-      my $result = $self->lookup_ns($domain);
+      dbg("dns: trying $domain, server $ns ..." .
+          ($retry ? " (retry $retry)" : ""));
+      my ($ok_a, $ok_aaaa) = $self->test_dns_a_aaaa($domain);
       $self->{resolver}->finish_socket();
-      if (!$result) {
-        dbg("dns: NS lookup of $domain using $ns failed horribly, ".
-            "may not be a valid nameserver");
+      if (!defined $ok_a || !defined $ok_aaaa) {
+        # error printed already
         last;
-      } elsif (!@$result) {
-        dbg("dns: NS lookup of $domain using $ns failed, no results found");
+      } elsif (!$ok_a && !$ok_aaaa) {
+        dbg("dns: lookup of $domain using $ns failed, no results found");
       } else {
-        dbg("dns: NS lookup of $domain using $ns succeeded => DNS available".
+        dbg("dns: lookup of $domain using $ns succeeded => DNS available".
             " (set dns_available to override)");
         push(@good_nameservers, $ns);
         last;
@@ -585,8 +607,6 @@ sub is_dns_available {
     $self->{resolver}->available_nameservers(@good_nameservers);
   }
 
-done:
-  # jm: leaving this in!
   dbg("dns: is DNS available? " . $IS_DNS_AVAILABLE);
   return $IS_DNS_AVAILABLE;
 }
@@ -676,55 +696,26 @@ sub cleanup_kids {
 
 ###########################################################################
 
-sub register_async_rule_start {
-  my ($self, $rule) = @_;
-  dbg("dns: $rule lookup start");
-  $self->{rule_to_rblkey}->{$rule} = '*ASYNC_START';
-}
+# Deprecated async functions, everything is handled automatically
+# now by bgsend .. $self->{async}->{pending_rules}
+sub register_async_rule_start {}
+sub register_async_rule_finish {}
+sub mark_all_async_rules_complete {}
+sub is_rule_complete {}
 
-sub register_async_rule_finish {
+# Return number of pending DNS lookups for a rule,
+# or list all of rules still pending
+sub get_async_pending_rules {
   my ($self, $rule) = @_;
-  dbg("dns: $rule lookup finished");
-  delete $self->{rule_to_rblkey}->{$rule};
-}
-
-sub mark_all_async_rules_complete {
-  my ($self) = @_;
-  $self->{rule_to_rblkey} = { };
-}
-
-sub is_rule_complete {
-  my ($self, $rule) = @_;
-
-  my $key = $self->{rule_to_rblkey}->{$rule};
-  if (!defined $key) {
-    # dbg("dns: $rule lookup complete, not in list");
-    return 1;
-  }
-
-  if ($key eq '*ASYNC_START') {
-    dbg("dns: $rule lookup not yet complete");
-    return 0;       # not yet complete
-  }
-
-  my $ent = $self->{async}->get_lookup($key);
-  if (!defined $ent) {
-    dbg("dns: $rule lookup complete, $key no longer pending");
-    return 1;
+  if (defined $rule) {
+    return 0 if !exists $self->{async}->{pending_rules}{$rule};
+    return scalar keys %{$self->{async}->{pending_rules}{$rule}};
+  } else {
+    return grep { %{$self->{async}->{pending_rules}{$_}} }
+             keys %{$self->{async}->{pending_rules}};
   }
-
-  dbg("dns: $rule lookup not yet complete");
-  return 0;         # not yet complete
 }
 
 ###########################################################################
 
-# interface called by SPF plugin
-sub check_for_from_dns {
-  my ($self, $pms) = @_;
-  if (defined $pms->{sender_host_fail}) {
-    return ($pms->{sender_host_fail} == 2); # both MX and A need to fail
-  }
-}
-
 1;
index b5f1804bc6b73e60ff423c31fbb28c0f6a345991..10664173bd832f6a1d780ef88b67fd9cdd4df695 100644 (file)
@@ -40,21 +40,24 @@ use warnings;
 # use bytes;
 use re 'taint';
 
-require 5.008001;  # needs utf8::is_utf8()
-
 use Mail::SpamAssassin;
 use Mail::SpamAssassin::Logger;
 use Mail::SpamAssassin::Constants qw(:ip);
-use Mail::SpamAssassin::Util qw(untaint_var decode_dns_question_entry);
+use Mail::SpamAssassin::Util qw(untaint_var decode_dns_question_entry
+                                idn_to_ascii reverse_ip_address
+                                domain_to_search_list);
 
 use Socket;
 use Errno qw(EADDRINUSE EACCES);
 use Time::HiRes qw(time);
+use version 0.77;
 
 our @ISA = qw();
 
+our $have_net_dns;
 our $io_socket_module_name;
 BEGIN {
+  $have_net_dns = eval { require Net::DNS; };
   if (eval { require IO::Socket::IP }) {
     $io_socket_module_name = 'IO::Socket::IP';
   } elsif (eval { require IO::Socket::INET6 }) {
@@ -78,7 +81,6 @@ sub new {
   };
   bless ($self, $class);
 
-  $self->load_resolver();
   $self;
 }
 
@@ -94,8 +96,8 @@ Load the C<Net::DNS::Resolver> object.  Returns 0 if Net::DNS cannot be used,
 sub load_resolver {
   my ($self) = @_;
 
-  if ($self->{res}) { return 1; }
-  $self->{no_resolver} = 1;
+  return 0 if $self->{no_resolver};
+  return 1 if $self->{res};
 
   # force only ipv4 if no IO::Socket::INET6 or ipv6 doesn't work
   my $force_ipv4 = $self->{main}->{force_ipv4};
@@ -112,7 +114,7 @@ sub load_resolver {
       if ($io_socket_module_name) {
         $sock6 = $io_socket_module_name->new(LocalAddr=>'::', Proto=>'udp');
       }
-      if ($sock6) { $sock6->close() or warn "error closing socket: $!" }
+      if ($sock6) { $sock6->close() or warn "dns: error closing socket: $!\n" }
       $sock6;
     } or do {
       dbg("dns: socket module %s is available, but no host support for IPv6",
@@ -123,13 +125,14 @@ sub load_resolver {
   }
   
   eval {
-    require Net::DNS;
+    die "Net::DNS required\n" if !$have_net_dns;
+    die "Net::DNS 0.69 required\n"
+      if (version->parse(Net::DNS->VERSION) < version->parse(0.69));
     # force_v4 is set in new() to avoid error in older versions of Net::DNS
     # that don't have it; other options are set by function calls so a typo
     # or API change will cause an error here
     my $res = $self->{res} = Net::DNS::Resolver->new(force_v4 => $force_ipv4);
     if ($res) {
-      $self->{no_resolver} = 0;
       $self->{force_ipv4} = $force_ipv4;
       $self->{force_ipv6} = $force_ipv6;
       $self->{retry} = 1;       # retries for non-backgrounded query
@@ -164,7 +167,7 @@ sub load_resolver {
     1;
   } or do {
     my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
-    dbg("dns: eval failed: $eval_stat");
+    warn("dns: resolver create failed: $eval_stat\n");
   };
 
   dbg("dns: using socket module: %s version %s%s",
@@ -173,12 +176,13 @@ sub load_resolver {
       $self->{force_ipv4} ? ', forced IPv4' :
       $self->{force_ipv6} ? ', forced IPv6' : '');
   dbg("dns: is Net::DNS::Resolver available? %s",
-      $self->{no_resolver} ? "no" : "yes" );
-  if (!$self->{no_resolver} && defined $Net::DNS::VERSION) {
+      $self->{res} ? "yes" : "no" );
+  if ($self->{res} && defined $Net::DNS::VERSION) {
     dbg("dns: Net::DNS version: %s", $Net::DNS::VERSION);
   }
 
-  return (!$self->{no_resolver});
+  $self->{no_resolver} = !$self->{res};
+  return defined $self->{res};
 }
 
 =item $resolver = $res->get_resolver()
@@ -237,18 +241,17 @@ sub available_nameservers {
   }
   if ($self->{force_ipv4} || $self->{force_ipv6}) {
     # filter the list according to a chosen protocol family
-    my $ip4_re = IPV4_ADDRESS;
     my(@filtered_addr_port);
     for (@{$self->{available_dns_servers}}) {
       local($1,$2);
       /^ \[ (.*) \] : (\d+) \z/xs  or next;
       my($addr,$port) = ($1,$2);
-      if ($addr =~ /^${ip4_re}\z/o) {
+      if ($addr =~ IS_IPV4_ADDRESS) {
         push(@filtered_addr_port, $_)  unless $self->{force_ipv6};
       } elsif ($addr =~ /:.*:/) {
         push(@filtered_addr_port, $_)  unless $self->{force_ipv4};
       } else {
-        warn "Unrecognized DNS server specification: $_";
+        warn "dns: Unrecognized DNS server specification: $_\n";
       }
     }
     if (@filtered_addr_port < @{$self->{available_dns_servers}}) {
@@ -361,7 +364,7 @@ sub connect_sock {
 
   if ($self->{sock}) {
     $self->{sock}->close()
-      or info("connect_sock: error closing socket %s: %s", $self->{sock}, $!);
+      or info("dns: connect_sock: error closing socket %s: %s", $self->{sock}, $!);
     $self->{sock} = undef;
   }
   my $sock;
@@ -378,13 +381,12 @@ sub connect_sock {
   # is unspecified, causing EINVAL failure when automatically assigned local
   # IP address and a remote address do not belong to the same address family.
   # Let's choose a suitable source address if possible.
-  my $ip4_re = IPV4_ADDRESS;
   my $srcaddr;
   if ($self->{force_ipv4}) {
     $srcaddr = "0.0.0.0";
   } elsif ($self->{force_ipv6}) {
     $srcaddr = "::";
-  } elsif ($ns_addr =~ /^${ip4_re}\z/o) {
+  } elsif ($ns_addr =~ IS_IPV4_ADDRESS) {
     $srcaddr = "0.0.0.0";
   } elsif ($ns_addr =~ /:.*:/) {
     $srcaddr = "::";
@@ -400,10 +402,10 @@ sub connect_sock {
     $lport = $self->pick_random_available_port();
     if (!defined $lport) {
       $lport = 0;
-      dbg("no configured local ports for DNS queries, letting OS choose");
+      dbg("dns: no configured local ports for DNS queries, letting OS choose");
     }
     if ($attempts+1 > 50) {  # sanity check
-      warn "could not create a DNS resolver socket in $attempts attempts\n";
+      warn "dns: could not create a DNS resolver socket in $attempts attempts\n";
       $errno = 0;
       last;
     }
@@ -431,12 +433,12 @@ sub connect_sock {
         $self->disable_available_port($lport);
       }
     } else {
-      warn "error creating a DNS resolver socket: $errno";
+      warn "dns: error creating a DNS resolver socket: $errno";
       goto no_sock;
     }
   }
   if (!$sock) {
-    warn "could not create a DNS resolver socket in $attempts attempts: $errno";
+    warn "dns: could not create a DNS resolver socket in $attempts attempts: $errno\n";
     goto no_sock;
   }
 
@@ -534,9 +536,8 @@ sub new_dns_packet {
 
   # construct a PTR query if it looks like an IPv4 address
   if (!defined($type) || $type eq 'PTR') {
-    local($1,$2,$3,$4);
-    if ($domain =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) {
-      $domain = "$4.$3.$2.$1.in-addr.arpa.";
+    if ($domain =~ IS_IPV4_ADDRESS) {
+      $domain = reverse_ip_address($domain).".in-addr.arpa.";
       $type = 'PTR';
     }
   }
@@ -594,17 +595,11 @@ sub new_dns_packet {
     # RD flag needs to be set explicitly since Net::DNS 1.01, Bug 7223 
     $packet->header->rd(1);
 
-  # my $udp_payload_size = $self->{res}->udppacketsize;
+    # my $udp_payload_size = $self->{res}->udppacketsize;
     my $udp_payload_size = $self->{conf}->{dns_options}->{edns};
     if ($udp_payload_size && $udp_payload_size > 512) {
-    # dbg("dns: adding EDNS ext, UDP payload size %d", $udp_payload_size);
-      if ($packet->UNIVERSAL::can('edns')) {  # available since Net::DNS 0.69
-        $packet->edns->size($udp_payload_size);
-      } else {  # legacy mechanism
-        my $optrr = Net::DNS::RR->new(Type => 'OPT', Name => '', TTL => 0,
-                                      Class => $udp_payload_size);
-        $packet->push('additional', $optrr);
-      }
+      # dbg("dns: adding EDNS ext, UDP payload size %d", $udp_payload_size);
+      $packet->edns->size($udp_payload_size);
     }
   }
 
@@ -658,6 +653,8 @@ sub _packet_id {
 
 =item $id = $res->bgsend($domain, $type, $class, $cb)
 
+DIRECT USE DISCOURAGED, please use bgsend_and_start_lookup in plugins.
+
 Quite similar to C<Net::DNS::Resolver::bgsend>, except that when a reply
 packet eventually arrives, and C<poll_responses> is called, the callback
 sub reference C<$cb> will be called.
@@ -673,7 +670,7 @@ be used, like so:
 
   my $id = $self->{resolver}->bgsend($domain, $type, undef, sub {
         my ($reply, $reply_id, $timestamp) = @_;
-        $self->got_a_reply ($reply, $reply_id);
+        $self->got_a_reply($reply, $reply_id);
       });
 
 The callback can ignore the reply as an invalid packet sent to the listening
@@ -685,6 +682,19 @@ sub bgsend {
   my ($self, $domain, $type, $class, $cb) = @_;
   return if $self->{no_resolver};
 
+  my $dns_query_blockages = $self->{main}->{conf}->{dns_query_blocked};
+  if ($dns_query_blockages) {
+    my $search_list = domain_to_search_list($domain);
+    foreach my $parent_domain ((@$search_list, '*')) {
+      my $blocked = $dns_query_blockages->{$parent_domain};
+      next if !defined $blocked; # not listed
+      last if !$blocked; # allowed
+      # blocked
+      dbg("dns: bgsend, query $type/$domain blocked by dns_query_restriction: $parent_domain");
+      return;
+    }
+  }
+
   $self->{send_timed_out} = 0;
 
   my $pkt = $self->new_dns_packet($domain, $type, $class);
@@ -747,7 +757,7 @@ sub bgread {
   $answerpkt or die "bgread: decoding DNS packet failed: $@";
   $answerpkt->answerfrom($peerhost);
   if (defined $decoded_length && $decoded_length ne "" && $decoded_length != length($data)) {
-    warn sprintf("bgread: received a %d bytes packet from %s, decoded %d bytes\n",
+    warn sprintf("dns: bgread: received a %d bytes packet from %s, decoded %d bytes\n",
                  length($data), $peerhost, $decoded_length);
   }
   return $answerpkt;
@@ -767,13 +777,16 @@ sub poll_responses {
   return if $self->{no_resolver};
   return if !$self->{sock};
   my $cnt = 0;
+  my $cnt_cb = 0;
 
   my $rin = $self->{sock_as_vec};
   my $rout;
 
   for (;;) {
     my ($nfound, $timeleft, $eval_stat);
-    eval {  # use eval to catch alarm signal
+    # if a restartable signal is caught, retry 3 times before aborting
+    my $eintrcount = 3;
+    eval {  # use eval to caught alarm signal
       my $timer;  # collects timestamp when variable goes out of scope
       if (!defined($timeout) || $timeout > 0)
         { $timer = $self->{main}->time_method("poll_dns_idle") }
@@ -787,16 +800,21 @@ sub poll_responses {
       # most likely due to an alarm signal, resignal if so
       die "dns: (2) $eval_stat\n"  if $eval_stat =~ /__alarm__ignore__\(.*\)/s;
       warn "dns: select aborted: $eval_stat\n";
-      return;
+      last;
     } elsif (!defined $nfound || $nfound < 0) {
+      if ($!{EINTR} and $eintrcount > 0) {
+        $eintrcount--;
+        next;
+      }
       if ($!) { warn "dns: select failed: $!\n" }
       else    { info("dns: select interrupted") }  # shouldn't happen
-      return;
+      last;
     } elsif (!$nfound) {
       if (!defined $timeout) { warn("dns: select returned empty-handed\n") }
       elsif ($timeout > 0) { dbg("dns: select timed out %.3f s", $timeout) }
-      return;
+      last;
     }
+    $cnt += $nfound;
 
     my $now = time;
     $timeout = 0;  # next time around collect whatever is available, then exit
@@ -853,12 +871,12 @@ sub poll_responses {
 
         if ($cb) {
           $cb->($packet, $id, $now);
-          $cnt++;
+          $cnt_cb++;
         } else {  # no match, report the problem
           if ($rcode eq 'REFUSED' || $id =~ m{^\d+/NO_QUESTION_IN_PACKET\z}) {
             # the failure was already reported above
           } else {
-            dbg("dns: no callback for id $id, ignored, packet on next debug line");
+            info("dns: no callback for id $id, ignored, packet on next debug line");
             # prevent filling normal logs with huge packet dumps
             dbg("dns: %s", $packet ? $packet->string : "undef");
           }
@@ -867,11 +885,11 @@ sub poll_responses {
           if ($id =~ m{^(\d+)/}) {
             my $dnsid = $1;  # the raw DNS packet id
             my @matches =
-              grep(m{^\Q$dnsid\E/}, keys %{$self->{id_to_callback}});
+              grep(m{^\Q$dnsid\E/}o, keys %{$self->{id_to_callback}});
             if (!@matches) {
-              dbg("dns: no likely matching queries for id %s", $dnsid);
+              info("dns: no likely matching queries for id %s", $dnsid);
             } else {
-              dbg("dns: a likely matching query: %s", join(', ', @matches));
+              info("dns: a likely matching query: %s", join(', ', @matches));
             }
           }
         }
@@ -879,7 +897,35 @@ sub poll_responses {
     }
   }
 
-  return $cnt;
+  return ($cnt, $cnt_cb);
+}
+
+use constant RECV_FLAGS => eval { MSG_DONTWAIT } || 0; # Not in Windows
+
+# Used to flush stale DNS responses, which we don't need to process
+sub flush_responses {
+  my ($self) = @_;
+  return if $self->{no_resolver};
+  return if !$self->{sock};
+
+  my $rin = $self->{sock_as_vec};
+  my $rout;
+  my $nfound;
+
+  my $packetsize = $self->{res}->udppacketsize;
+  $packetsize = 512  if $packetsize < 512;  # just in case
+  $self->{sock}->blocking(0) unless(RECV_FLAGS);
+  for (;;) {
+    eval {  # use eval to catch alarm signal
+      ($nfound, undef) = select($rout=$rin, undef, undef, 0);
+      1;
+    } or do {
+         last;
+    };
+    last if !$nfound;
+    last if !$self->{sock}->recv(my $data, $packetsize+256, RECV_FLAGS);
+  }
+  $self->{sock}->blocking(1) unless(RECV_FLAGS);
 }
 
 ###########################################################################
@@ -920,8 +966,9 @@ sub send {
   # using some arbitrary encoding (they are normally just 7-bit ascii
   # characters anyway, just need to get rid of the utf8 flag).  Bug 6959
   # Most if not all af these come from a SPF plugin.
+  #   (was a call to utf8::encode($name), now we prefer a proper idn_to_ascii)
   #
-  utf8::encode($name);
+  $name = idn_to_ascii($name);
 
   my $retrans = $self->{retrans};
   my $retries = $self->{retry};
@@ -985,7 +1032,7 @@ sub finish_socket {
   my ($self) = @_;
   if ($self->{sock}) {
     $self->{sock}->close()
-      or warn "finish_socket: error closing socket $self->{sock}: $!";
+      or warn "dns: finish_socket: error closing socket $self->{sock}: $!\n";
     undef $self->{sock};
   }
 }
@@ -1014,7 +1061,7 @@ sub fhs_to_vec {
   foreach my $sock (@fhlist) {
     my $fno = fileno($sock);
     if (!defined $fno) {
-      warn "dns: oops! fileno now undef for $sock";
+      warn "dns: oops! fileno now undef for $sock\n";
     } else {
       vec ($rin, $fno, 1) = 1;
     }
diff --git a/upstream/lib/Mail/SpamAssassin/GeoDB.pm b/upstream/lib/Mail/SpamAssassin/GeoDB.pm
new file mode 100644 (file)
index 0000000..1b220de
--- /dev/null
@@ -0,0 +1,899 @@
+# <@LICENSE>
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to you under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# </@LICENSE>
+
+=head1 NAME
+
+Mail::SpamAssassin::GeoDB - unified interface for geoip modules
+
+Plugins need to signal SA main package the modules they want loaded
+
+package Mail::SpamAssassin::Plugin::MyPlugin;
+sub new {
+  ...
+  $self->{main}->{geodb_wanted}->{country} = 1;
+  $self->{main}->{geodb_wanted}->{isp} = 1;
+)
+
+(internal stuff still subject to change)
+
+=cut
+
+package Mail::SpamAssassin::GeoDB;
+
+use strict;
+use warnings;
+# use bytes;
+use re 'taint';
+
+use Socket;
+use version 0.77;
+
+our @ISA = qw();
+
+use Mail::SpamAssassin::Constants qw(:ip);
+use Mail::SpamAssassin::Logger;
+
+my @geoip_default_path = qw(
+  /usr/local/share/GeoIP
+  /usr/share/GeoIP
+  /var/lib/GeoIP
+  /opt/share/GeoIP
+);
+
+# load order (city contains country, isp contains asn)
+my @geoip_types = qw( city country isp asn );
+
+# v6 is not needed, automatically tries *v6.dat also
+my %geoip_default_files = (
+  'city' => ['GeoIPCity.dat','GeoLiteCity.dat'],
+  'country' => ['GeoIP.dat'],
+  'isp' => ['GeoIPISP.dat'],
+  'asn' => ['GeoIPASNum.dat'],
+);
+
+my %geoip2_default_files = (
+  'city' => ['GeoIP2-City.mmdb','GeoLite2-City.mmdb',
+             'dbip-city.mmdb','dbip-city-lite.mmdb'],
+  'country' => ['GeoIP2-Country.mmdb','GeoLite2-Country.mmdb',
+                'dbip-country.mmdb','dbip-country-lite.mmdb'],
+  'isp' => ['GeoIP2-ISP.mmdb','GeoLite2-ISP.mmdb'],
+  'asn' => ['GeoIP2-ASN.mmdb','GeoLite2-ASN.mmdb'],
+);
+
+my %country_to_continent = (
+'AP'=>'AS','EU'=>'EU','AD'=>'EU','AE'=>'AS','AF'=>'AS','AG'=>'NA',
+'AI'=>'NA','AL'=>'EU','AM'=>'AS','CW'=>'NA','AO'=>'AF','AQ'=>'AN',
+'AR'=>'SA','AS'=>'OC','AT'=>'EU','AU'=>'OC','AW'=>'NA','AZ'=>'AS',
+'BA'=>'EU','BB'=>'NA','BD'=>'AS','BE'=>'EU','BF'=>'AF','BG'=>'EU',
+'BH'=>'AS','BI'=>'AF','BJ'=>'AF','BM'=>'NA','BN'=>'AS','BO'=>'SA',
+'BR'=>'SA','BS'=>'NA','BT'=>'AS','BV'=>'AN','BW'=>'AF','BY'=>'EU',
+'BZ'=>'NA','CA'=>'NA','CC'=>'AS','CD'=>'AF','CF'=>'AF','CG'=>'AF',
+'CH'=>'EU','CI'=>'AF','CK'=>'OC','CL'=>'SA','CM'=>'AF','CN'=>'AS',
+'CO'=>'SA','CR'=>'NA','CU'=>'NA','CV'=>'AF','CX'=>'AS','CY'=>'AS',
+'CZ'=>'EU','DE'=>'EU','DJ'=>'AF','DK'=>'EU','DM'=>'NA','DO'=>'NA',
+'DZ'=>'AF','EC'=>'SA','EE'=>'EU','EG'=>'AF','EH'=>'AF','ER'=>'AF',
+'ES'=>'EU','ET'=>'AF','FI'=>'EU','FJ'=>'OC','FK'=>'SA','FM'=>'OC',
+'FO'=>'EU','FR'=>'EU','FX'=>'EU','GA'=>'AF','GB'=>'EU','GD'=>'NA',
+'GE'=>'AS','GF'=>'SA','GH'=>'AF','GI'=>'EU','GL'=>'NA','GM'=>'AF',
+'GN'=>'AF','GP'=>'NA','GQ'=>'AF','GR'=>'EU','GS'=>'AN','GT'=>'NA',
+'GU'=>'OC','GW'=>'AF','GY'=>'SA','HK'=>'AS','HM'=>'AN','HN'=>'NA',
+'HR'=>'EU','HT'=>'NA','HU'=>'EU','ID'=>'AS','IE'=>'EU','IL'=>'AS',
+'IN'=>'AS','IO'=>'AS','IQ'=>'AS','IR'=>'AS','IS'=>'EU','IT'=>'EU',
+'JM'=>'NA','JO'=>'AS','JP'=>'AS','KE'=>'AF','KG'=>'AS','KH'=>'AS',
+'KI'=>'OC','KM'=>'AF','KN'=>'NA','KP'=>'AS','KR'=>'AS','KW'=>'AS',
+'KY'=>'NA','KZ'=>'AS','LA'=>'AS','LB'=>'AS','LC'=>'NA','LI'=>'EU',
+'LK'=>'AS','LR'=>'AF','LS'=>'AF','LT'=>'EU','LU'=>'EU','LV'=>'EU',
+'LY'=>'AF','MA'=>'AF','MC'=>'EU','MD'=>'EU','MG'=>'AF','MH'=>'OC',
+'MK'=>'EU','ML'=>'AF','MM'=>'AS','MN'=>'AS','MO'=>'AS','MP'=>'OC',
+'MQ'=>'NA','MR'=>'AF','MS'=>'NA','MT'=>'EU','MU'=>'AF','MV'=>'AS',
+'MW'=>'AF','MX'=>'NA','MY'=>'AS','MZ'=>'AF','NA'=>'AF','NC'=>'OC',
+'NE'=>'AF','NF'=>'OC','NG'=>'AF','NI'=>'NA','NL'=>'EU','NO'=>'EU',
+'NP'=>'AS','NR'=>'OC','NU'=>'OC','NZ'=>'OC','OM'=>'AS','PA'=>'NA',
+'PE'=>'SA','PF'=>'OC','PG'=>'OC','PH'=>'AS','PK'=>'AS','PL'=>'EU',
+'PM'=>'NA','PN'=>'OC','PR'=>'NA','PS'=>'AS','PT'=>'EU','PW'=>'OC',
+'PY'=>'SA','QA'=>'AS','RE'=>'AF','RO'=>'EU','RU'=>'EU','RW'=>'AF',
+'SA'=>'AS','SB'=>'OC','SC'=>'AF','SD'=>'AF','SE'=>'EU','SG'=>'AS',
+'SH'=>'AF','SI'=>'EU','SJ'=>'EU','SK'=>'EU','SL'=>'AF','SM'=>'EU',
+'SN'=>'AF','SO'=>'AF','SR'=>'SA','ST'=>'AF','SV'=>'NA','SY'=>'AS',
+'SZ'=>'AF','TC'=>'NA','TD'=>'AF','TF'=>'AN','TG'=>'AF','TH'=>'AS',
+'TJ'=>'AS','TK'=>'OC','TM'=>'AS','TN'=>'AF','TO'=>'OC','TL'=>'AS',
+'TR'=>'EU','TT'=>'NA','TV'=>'OC','TW'=>'AS','TZ'=>'AF','UA'=>'EU',
+'UG'=>'AF','UM'=>'OC','US'=>'NA','UY'=>'SA','UZ'=>'AS','VA'=>'EU',
+'VC'=>'NA','VE'=>'SA','VG'=>'NA','VI'=>'NA','VN'=>'AS','VU'=>'OC',
+'WF'=>'OC','WS'=>'OC','YE'=>'AS','YT'=>'AF','RS'=>'EU','ZA'=>'AF',
+'ZM'=>'AF','ME'=>'EU','ZW'=>'AF','AX'=>'EU','GG'=>'EU','IM'=>'EU',
+'JE'=>'EU','BL'=>'NA','MF'=>'NA','BQ'=>'NA','SS'=>'AF','**'=>'**',
+);
+
+sub new {
+  my ($class, $conf) = @_;
+  $class = ref($class) || $class;
+
+  my $self = {};
+  bless ($self, $class);
+
+  $self->{cache} = ();
+  $self->init_database($conf || {});
+  $self;
+}
+
+sub init_database {
+  my ($self, $opts) = @_;
+
+  # Try city too if country wanted
+  $opts->{wanted}->{city} = 1 if $opts->{wanted}->{country};
+  # Try isp too if asn wanted
+  $opts->{wanted}->{isp} = 1 if $opts->{wanted}->{asn};
+
+  my $geodb_opts = {
+    'module' => $opts->{conf}->{module} || undef,
+    'dbs' => $opts->{conf}->{options} || undef,
+    'wanted' => $opts->{wanted} || undef,
+    'search_path' => defined $opts->{conf}->{geodb_search_path} ?
+      $opts->{conf}->{geodb_search_path} : \@geoip_default_path,
+  };
+
+  my ($db, $dbapi, $loaded);
+
+  ## GeoIP2
+  if (!$db && (!$geodb_opts->{module} || $geodb_opts->{module} eq 'geoip2')) {
+    ($db, $dbapi) = $self->load_geoip2($geodb_opts);
+    $loaded = 'geoip2' if $db;
+  }
+
+  ## Geo::IP
+  if (!$db && (!$geodb_opts->{module} || $geodb_opts->{module} eq 'geoip')) {
+    ($db, $dbapi) = $self->load_geoip($geodb_opts);
+    $loaded = 'geoip' if $db;
+  }
+
+  ## IP::Country::DB_File
+  if (!$db && $geodb_opts->{module} && $geodb_opts->{module} eq 'dbfile') {
+    # Only try if geodb_module and path to ipcc.db specified
+    ($db, $dbapi) = $self->load_dbfile($geodb_opts);
+    $loaded = 'dbfile' if $db;
+  }
+
+  ## IP::Country::Fast
+  if (!$db && (!$geodb_opts->{module} || $geodb_opts->{module} eq 'fast')) {
+    ($db, $dbapi) = $self->load_fast($geodb_opts);
+    $loaded = 'fast' if $db;
+  }
+
+  if (!$db) {
+    dbg("geodb: No supported database could be loaded");
+    die("No supported GeoDB database could be loaded\n");
+  }
+
+  # country can be aliased to city
+  if (!$dbapi->{country} && $dbapi->{city}) {
+    $dbapi->{country} = $dbapi->{city};
+  }
+  if (!$dbapi->{country_v6} && $dbapi->{city_v6}) {
+    $dbapi->{country_v6} = $dbapi->{city_v6}
+  }
+  # GeoIP2 asn can be aliased to isp
+  if ($loaded eq 'geoip2') {
+    if (!$dbapi->{asn} && $dbapi->{isp}) {
+      $dbapi->{asn} = $dbapi->{isp};
+    }
+    if (!$dbapi->{asn_v6} && $dbapi->{isp_v6}) {
+      $dbapi->{asn_v6} = $dbapi->{isp_v6}
+    }
+  }
+
+  $self->{db} = $db;
+  $self->{dbapi} = $dbapi;
+
+  foreach (@{$self->get_dbinfo()}) {
+    dbg("geodb: database info: ".$_);
+  }
+  #dbg("geodb: apis available: ".join(', ', sort keys %{$self->{dbapi}}));
+
+  return 1;
+}
+
+sub load_geoip2 {
+  my ($self, $geodb_opts) = @_;
+  my ($db, $dbapi, $ok);
+
+  # Warn about fatal errors if this module was specifically requested
+  my $errwarn = ($geodb_opts->{module}||'') eq 'geoip2';
+
+  eval {
+    require MaxMind::DB::Reader;
+  } or do {
+    my $err = $@;
+    $err =~ s/ at .*//s;
+    $err = "geodb: MaxMind::DB::Reader (GeoIP2) module load failed: $err";
+    $errwarn ? warn("$err\n") : dbg($err);
+    return (undef, undef);
+  };
+
+  my %path;
+  foreach my $dbtype (@geoip_types) {
+    # skip country if city already loaded
+    next if $dbtype eq 'country' && $db->{city};
+    # skip asn if isp already loaded
+    next if $dbtype eq 'asn' && $db->{isp};
+    # skip if not needed
+    next if $geodb_opts->{wanted} && !$geodb_opts->{wanted}->{$dbtype};
+    # only autosearch if no absolute path given
+    if (!defined $geodb_opts->{dbs}->{$dbtype}) {
+      # Try some default locations
+      PATHS_GEOIP2: foreach my $p (@{$geodb_opts->{search_path}}) {
+        foreach my $f (@{$geoip2_default_files{$dbtype}}) {
+          if (-f "$p/$f") {
+            $path{$dbtype} = "$p/$f";
+            dbg("geodb: GeoIP2: search found $dbtype $p/$f");
+            last PATHS_GEOIP2;
+          }
+        }
+      }
+    } else {
+      if (!-f $geodb_opts->{dbs}->{$dbtype}) {
+        dbg("geodb: GeoIP2: $dbtype database requested, but not found: ".
+          $geodb_opts->{dbs}->{$dbtype});
+        next;
+      }
+      $path{$dbtype} = $geodb_opts->{dbs}->{$dbtype};
+    }
+
+    if (defined $path{$dbtype}) {
+      eval {
+        $db->{$dbtype} = MaxMind::DB::Reader->new(
+          file => $path{$dbtype},
+        );
+        die "unknown error" unless $db->{$dbtype};
+        1;
+      };
+      if ($@ || !$db->{$dbtype}) {
+        my $err = $@;
+        $err =~ s/\s+Trace begun.*//s;
+        $err =~ s/ at .*//s;
+        dbg("geodb: GeoIP2: $dbtype load failed: $err");
+      } else {
+        dbg("geodb: GeoIP2: loaded $dbtype from $path{$dbtype}");
+        $ok = 1;
+      }
+    } else {
+      my $from = defined $geodb_opts->{dbs}->{$dbtype} ?
+        $geodb_opts->{dbs}->{$dbtype} : "default locations";
+      dbg("geodb: GeoIP2: $dbtype database not found from $from");
+    } 
+  }
+
+  if (!$ok) {
+    warn("geodb: GeoIP2 requested, but no databases could be loaded\n") if $errwarn;
+    return (undef, undef)
+  }
+
+  # dbinfo_DBTYPE()
+  $db->{city} and $dbapi->{dbinfo_city} = sub {
+    my $m = $_[0]->{db}->{city}->metadata();
+    return "GeoIP2 city: ".$m->description()->{en}." / ".localtime($m->build_epoch());
+  };
+  $db->{country} and $dbapi->{dbinfo_country} = sub {
+    my $m = $_[0]->{db}->{country}->metadata();
+    return "GeoIP2 country: ".$m->description()->{en}." / ".localtime($m->build_epoch());
+  };
+  $db->{isp} and $dbapi->{dbinfo_isp} = sub {
+    my $m = $_[0]->{db}->{isp}->metadata();
+    return "GeoIP2 isp: ".$m->description()->{en}." / ".localtime($m->build_epoch());
+  };
+  $db->{asn} and $dbapi->{dbinfo_asn} = sub {
+    my $m = $_[0]->{db}->{asn}->metadata();
+    return "GeoIP2 asn: ".$m->description()->{en}." / ".localtime($m->build_epoch());
+  };
+
+  # city()
+  $db->{city} and $dbapi->{city} = $dbapi->{city_v6} = sub {
+    my $res = {};
+    my $city;
+    eval {
+      $city = $_[0]->{db}->{city}->record_for_address($_[1]);
+      1;
+    } or do {
+      $@ =~ s/\s+Trace begun.*//s;
+      dbg("geodb: GeoIP2 city query failed for $_[1]: $@");
+      return $res;
+    };
+    eval {
+      $res->{city_name} = $city->{city}->{names}->{en};
+      $res->{country} = $city->{country}->{iso_code};
+      $res->{country_name} = $city->{country}->{names}->{en};
+      $res->{continent} = $city->{continent}->{code};
+      $res->{continent_name} = $city->{continent}->{names}->{en};
+      1;
+    };
+    return $res;
+  };
+
+  # country()
+  $db->{country} and $dbapi->{country} = $dbapi->{country_v6} = sub {
+    my $res = {};
+    my $country;
+    eval {
+      $country = $_[0]->{db}->{country}->record_for_address($_[1]);
+      1;
+    } or do {
+      $@ =~ s/\s+Trace begun.*//s;
+      dbg("geodb: GeoIP2 country query failed for $_[1]: $@");
+      return $res;
+    };
+    eval {
+      $res->{country} = $country->{country}->{iso_code};
+      $res->{country_name} = $country->{country}->{names}->{en};
+      $res->{continent} = $country->{continent}->{code};
+      $res->{continent_name} = $country->{continent}->{names}->{en};
+      1;
+    };
+    return $res;
+  };
+
+  # isp()
+  $db->{isp} and $dbapi->{isp} = $dbapi->{isp_v6} = sub {
+    my $res = {};
+    my $isp;
+    eval {
+      $isp = $_[0]->{db}->{isp}->record_for_address($_[1]);
+      1;
+    } or do {
+      $@ =~ s/\s+Trace begun.*//s;
+      dbg("geodb: GeoIP2 isp query failed for $_[1]: $@");
+      return $res;
+    };
+    eval {
+      $res->{asn} = $isp->{autonomous_system_number};
+      $res->{asn_organization} = $isp->{autonomous_system_organization};
+      $res->{isp} = $isp->{isp};
+      $res->{organization} = $isp->{organization};
+      1;
+    };
+    return $res;
+  };
+
+  # asn()
+  $db->{asn} and $dbapi->{asn} = $dbapi->{asn_v6} = sub {
+    my $res = {};
+    my $asn;
+    eval {
+      $asn = $_[0]->{db}->{asn}->record_for_address($_[1]);
+      1;
+    } or do {
+      $@ =~ s/\s+Trace begun.*//s;
+      dbg("geodb: GeoIP2 asn query failed for $_[1]: $@");
+      return $res;
+    };
+    eval {
+      $res->{asn} = $asn->{autonomous_system_number};
+      $res->{asn_organization} = $asn->{autonomous_system_organization};
+      1;
+    };
+    return $res;
+  };
+
+  return ($db, $dbapi);
+}
+
+sub load_geoip {
+  my ($self, $geodb_opts) = @_;
+  my ($db, $dbapi, $ok);
+  my ($gic_wanted, $gic_have, $gip_wanted, $gip_have);
+  my ($flags, $fix_stderr, $can_ipv6);
+
+  # Warn about fatal errors if this module was specifically requested
+  my $errwarn = ($geodb_opts->{module}||'') eq 'geoip';
+
+  eval {
+    require Geo::IP;
+    # need GeoIP C library 1.6.3 and GeoIP perl API 1.4.4 or later to avoid messages leaking - Bug 7153
+    $gip_wanted = version->parse('v1.4.4');
+    $gip_have = version->parse(Geo::IP->VERSION);
+    $gic_wanted = version->parse('v1.6.3');
+    eval { $gic_have = version->parse(Geo::IP->lib_version()); }; # might not have lib_version()
+    $gic_have = 'none' if !defined $gic_have;
+    dbg("geodb: GeoIP: versions: Geo::IP $gip_have, C library $gic_have");
+    $flags = 0;
+    $fix_stderr = 0;
+    if (ref($gic_have) eq 'version') {
+      # this code burps an ugly message if it fails, but that's redirected elsewhere
+      eval '$flags = Geo::IP::GEOIP_SILENCE' if $gip_wanted >= $gip_have;
+      $fix_stderr = $flags && $gic_wanted >= $gic_have;
+    }
+    $can_ipv6 = Geo::IP->VERSION >= 1.39 && Geo::IP->api eq 'CAPI';
+    1;
+  } or do {
+    my $err = $@;
+    $err =~ s/ at .*//s;
+    $err = "geodb: Geo::IP module load failed: $err";
+    $errwarn ? warn("$err\n") : dbg($err);
+    return (undef, undef);
+  };
+
+  my %path;
+  foreach my $dbtype (@geoip_types) {
+    # skip country if city already loaded
+    next if $dbtype eq 'country' && $db->{city};
+    # skip asn if isp already loaded
+    next if $dbtype eq 'asn' && $db->{isp};
+    # skip if not needed
+    next if $geodb_opts->{wanted} && !$geodb_opts->{wanted}->{$dbtype};
+    # only autosearch if no absolute path given
+    if (!defined $geodb_opts->{dbs}->{$dbtype}) {
+      # Try some default locations
+      PATHS_GEOIP: foreach my $p (@{$geodb_opts->{search_path}}) {
+        foreach my $f (@{$geoip_default_files{$dbtype}}) {
+          if (-f "$p/$f") {
+            $path{$dbtype} = "$p/$f";
+            dbg("geodb: GeoIP: search found $dbtype $p/$f");
+            if ($can_ipv6 && $f =~ s/\.(dat)$/v6.$1/i) {
+              if (-f "$p/$f") {
+                $path{$dbtype."_v6"} = "$p/$f";
+                dbg("geodb: GeoIP: search found $dbtype $p/$f");
+              }
+            }
+            last PATHS_GEOIP;
+          }
+        }
+      }
+    } else {
+      if (!-f $geodb_opts->{dbs}->{$dbtype}) {
+        dbg("geodb: GeoIP: $dbtype database requested, but not found: ".
+          $geodb_opts->{dbs}->{$dbtype});
+        next;
+      }
+      $path{$dbtype} = $geodb_opts->{dbs}->{$dbtype};
+    }
+  }
+
+  if (!$can_ipv6) {
+    dbg("geodb: GeoIP: IPv6 support not enabled, versions Geo::IP 1.39, GeoIP C API 1.4.7 required");
+  }
+
+  if ($fix_stderr) {
+    open(OLDERR, ">&STDERR");
+    open(STDERR, ">/dev/null");
+  }
+  foreach my $dbtype (@geoip_types) {
+    next unless defined $path{$dbtype};
+    eval {
+      $db->{$dbtype} = Geo::IP->open($path{$dbtype}, Geo::IP->GEOIP_STANDARD | $flags);
+      if ($can_ipv6 && defined $path{$dbtype."_v6"}) {
+        $db->{$dbtype."_v6"} = Geo::IP->open($path{$dbtype."_v6"}, Geo::IP->GEOIP_STANDARD | $flags);
+      }
+    };
+    if ($@ || !$db->{$dbtype}) {
+      my $err = $@;
+      $err =~ s/ at .*//s;
+      dbg("geodb: GeoIP: database $path{$dbtype} load failed: $err");
+    } else {
+      dbg("geodb: GeoIP: loaded $dbtype from $path{$dbtype}");
+      $ok = 1;
+    }
+  }
+  if ($fix_stderr) {
+    open(STDERR, ">&OLDERR");
+    close(OLDERR);
+  }
+
+  if (!$ok) {
+    warn("geodb: GeoIP requested, but no databases could be loaded\n") if $errwarn;
+    return (undef, undef)
+  }
+
+  # dbinfo_DBTYPE()
+  $db->{city} and $dbapi->{dbinfo_city} = sub {
+    return "Geo::IP IPv4 city: " . ($_[0]->{db}->{city}->database_info || '?')." / IPv6: ".
+      ($_[0]->{db}->{city_v6} ? $_[0]->{db}->{city_v6}->database_info || '?' : 'no')
+  };
+  $db->{country} and $dbapi->{dbinfo_country} = sub {
+    return "Geo::IP IPv4 country: " . ($_[0]->{db}->{country}->database_info || '?')." / IPv6: ".
+      ($_[0]->{db}->{country_v6} ? $_[0]->{db}->{country_v6}->database_info || '?' : 'no')
+  };
+  $db->{isp} and $dbapi->{dbinfo_isp} = sub {
+    return "Geo::IP IPv4 isp: " . ($_[0]->{db}->{isp}->database_info || '?')." / IPv6: ".
+      ($_[0]->{db}->{isp_v6} ? $_[0]->{db}->{isp_v6}->database_info || '?' : 'no')
+  };
+  $db->{asn} and $dbapi->{dbinfo_asn} = sub {
+    return "Geo::IP IPv4 asn: " . ($_[0]->{db}->{asn}->database_info || '?')." / IPv6: ".
+      ($_[0]->{db}->{asn_v6} ? $_[0]->{db}->{asn_v6}->database_info || '?' : 'no')
+  };
+
+  # city()
+  $db->{city} and $dbapi->{city} = sub {
+    my $res = {};
+    my $city;
+    if ($_[1] =~ IS_IPV4_ADDRESS) {
+      $city = $_[0]->{db}->{city}->record_by_addr($_[1]);
+    } elsif ($_[0]->{db}->{city_v6}) {
+      $city = $_[0]->{db}->{city_v6}->record_by_addr_v6($_[1]);
+    }
+    if (!defined $city) {
+      dbg("geodb: GeoIP city query failed for $_[1]");
+      return $res;
+    }
+    $res->{city_name} = $city->city;
+    $res->{country} = $city->country_code;
+    $res->{country_name} = $city->country_name;
+    $res->{continent} = $city->continent_code;
+    return $res;
+  };
+  $dbapi->{city_v6} = $dbapi->{city} if $db->{city_v6};
+
+  # country()
+  $db->{country} and $dbapi->{country} = sub {
+    my $res = {};
+    my $country;
+    eval {
+      if ($_[1] =~ IS_IPV4_ADDRESS) {
+        $country = $_[0]->{db}->{country}->country_code_by_addr($_[1]);
+      } elsif ($_[0]->{db}->{country_v6}) {
+        $country = $_[0]->{db}->{country_v6}->country_code_by_addr_v6($_[1]);
+      }
+      1;
+    };
+    if (!defined $country) {
+      dbg("geodb: GeoIP country query failed for $_[1]");
+      return $res;
+    };
+    $res->{country} = $country || 'XX';
+    $res->{continent} = $country_to_continent{$country} || 'XX';
+    return $res;
+  };
+  $dbapi->{country_v6} = $dbapi->{country} if $db->{country_v6};
+
+  # isp()
+  $db->{isp} and $dbapi->{isp} = sub {
+    my $res = {};
+    my $isp;
+    eval {
+      if ($_[1] =~ IS_IPV4_ADDRESS) {
+        $isp = $_[0]->{db}->{isp}->isp_by_addr($_[1]);
+      } else {
+        # TODO?
+        return $res;
+      }
+      1;
+    };
+    if (!defined $isp) {
+      dbg("geodb: GeoIP isp query failed for $_[1]");
+      return $res;
+    };
+    $res->{isp} = $isp;
+    return $res;
+  };
+
+  # asn()
+  $db->{asn} and $dbapi->{asn} = sub {
+    my $res = {};
+    my $asn;
+    eval {
+      if ($_[1] =~ IS_IPV4_ADDRESS) {
+        $asn = $_[0]->{db}->{asn}->isp_by_addr($_[1]);
+      } else {
+        # TODO?
+        return $res;
+      }
+      1;
+    };
+    if (!defined $asn || $asn !~ /^((?:AS)?\d+)(?:\s+(.+))?/) {
+      dbg("geodb: GeoIP asn query failed for $_[1]");
+      return $res;
+    };
+    $res->{asn} = $1;
+    $res->{asn_organization} = $2 if defined $2;
+    return $res;
+  };
+
+  return ($db, $dbapi);
+}
+
+sub load_dbfile {
+  my ($self, $geodb_opts) = @_;
+  my ($db, $dbapi);
+
+  # Warn about fatal errors if this module was specifically requested
+  my $errwarn = ($geodb_opts->{module}||'') eq 'dbfile';
+
+  if (!defined $geodb_opts->{dbs}->{country}) {
+    my $err = "geodb: IP::Country::DB_File requires geodb_options country:/path/to/ipcc.db";
+    $errwarn ? warn("$err\n") : dbg($err);
+    return (undef, undef);
+  }
+
+  if (!-f $geodb_opts->{dbs}->{country}) {
+    my $err = "geodb: IP::Country::DB_File database not found: ".$geodb_opts->{dbs}->{country};
+    $errwarn ? warn("$err\n") : dbg($err);
+    return (undef, undef);
+  }
+
+  eval {
+    require IP::Country::DB_File;
+    $db->{country} = IP::Country::DB_File->new($geodb_opts->{dbs}->{country});
+    1;
+  };
+  if ($@ || !$db->{country}) {
+    my $err = $@;
+    $err =~ s/ at .*//s;
+    $err = "geodb: IP::Country::DB_File country load failed: $err";
+    $errwarn ? warn("$err\n") : dbg($err);
+    return (undef, undef);
+  } else {
+    dbg("geodb: IP::Country::DB_File loaded country from ".$geodb_opts->{dbs}->{country});
+  }
+
+  # dbinfo_DBTYPE()
+  $db->{country} and $dbapi->{dbinfo_country} = sub {
+    return "IP::Country::DB_File country: ".localtime($_[0]->{db}->{country}->db_time());
+  };
+
+  # country();
+  $db->{country} and $dbapi->{country} = $dbapi->{country_v6} = sub {
+    my $res = {};
+    my $country;
+    if ($_[1] =~ IS_IPV4_ADDRESS) {
+      $country = $_[0]->{db}->{country}->inet_atocc($_[1]);
+    } else {
+      $country = $_[0]->{db}->{country}->inet6_atocc($_[1]);
+    }
+    if (!defined $country) {
+      dbg("geodb: IP::Country::DB_File country query failed for $_[1]");
+      return $res;
+    };
+    $res->{country} = $country || 'XX';
+    $res->{continent} = $country_to_continent{$country} || 'XX';
+    return $res;
+  };
+
+  return ($db, $dbapi);
+}
+
+sub load_fast {
+  my ($self, $geodb_opts) = @_;
+  my ($db, $dbapi);
+
+  # Warn about fatal errors if this module was specifically requested
+  my $errwarn = ($geodb_opts->{module}||'') eq 'fast';
+
+  eval {
+    require IP::Country::Fast;
+    $db->{country} = IP::Country::Fast->new();
+    1;
+  };
+  if ($@ || !$db->{country}) {
+    my $err = $@;
+    $err =~ s/ at .*//s;
+    $err = "geodb: IP::Country::Fast load failed: $err";
+    $errwarn ? warn("$err\n") : dbg($err);
+    return (undef, undef);
+  }
+
+  # dbinfo_DBTYPE()
+  $db->{country} and $dbapi->{dbinfo_country} = sub {
+    return "IP::Country::Fast country: ".localtime($_[0]->{db}->{country}->db_time());
+  };
+
+  # country();
+  $db->{country} and $dbapi->{country} = sub {
+    my $res = {};
+    my $country;
+    if ($_[1] =~ IS_IPV4_ADDRESS) {
+      $country = $_[0]->{db}->{country}->inet_atocc($_[1]);
+    } else {
+      return $res;
+    }
+    if (!defined $country) {
+      dbg("geodb: IP::Country::Fast country query failed for $_[1]");
+      return $res;
+    };
+    $res->{country} = $country || 'XX';
+    $res->{continent} = $country_to_continent{$country} || 'XX';
+    return $res;
+  };
+
+  return ($db, $dbapi);
+}
+
+# return array, infoline per database type
+sub get_dbinfo {
+  my ($self, $db) = @_;
+
+  my @lines;
+  foreach (@geoip_types) {
+    if (exists $self->{dbapi}->{"dbinfo_".$_}) {
+      push @lines,
+        $self->{dbapi}->{"dbinfo_".$_}->($self) || "$_ failed";
+    }
+  }
+
+  return \@lines;
+}
+
+sub get_country {
+  my ($self, $ip) = @_;
+
+  return undef if !defined $ip || $ip !~ /\S/; ## no critic (ProhibitExplicitReturnUndef)
+
+  if ($ip =~ IS_IP_PRIVATE) {
+    return '**';
+  }
+
+  if ($ip !~ IS_IP_ADDRESS) {
+    $ip = name_to_ip($ip);
+    return 'XX' if !defined $ip;
+  }
+  
+  if ($self->{dbapi}->{city}) {
+    return $self->_get('city',$ip)->{country} || 'XX';
+  } elsif ($self->{dbapi}->{country}) {
+    return $self->_get('country',$ip)->{country} || 'XX';
+  } else {
+    return undef; ## no critic (ProhibitExplicitReturnUndef)
+  }
+}
+
+sub get_continent {
+  my ($self, $ip) = @_;
+
+  return undef if !defined $ip || $ip !~ /\S/; ## no critic (ProhibitExplicitReturnUndef)
+
+  # If it's already CC, use our own lookup table..
+  if (length($ip) == 2) {
+    return $country_to_continent{uc($ip)} || 'XX';
+  }
+
+  if ($self->{dbapi}->{city}) {
+    return $self->_get('city',$ip)->{continent} || 'XX';
+  } elsif ($self->{dbapi}->{country}) {
+    return $self->_get('country',$ip)->{continent} || 'XX';
+  } else {
+    return undef; ## no critic (ProhibitExplicitReturnUndef)
+  }
+}
+
+sub get_isp {
+  my ($self, $ip) = @_;
+
+  return undef if !defined $ip || $ip !~ /\S/; ## no critic (ProhibitExplicitReturnUndef)
+
+  if ($self->{dbapi}->{isp}) {
+    return $self->_get('isp',$ip)->{isp};
+  } else {
+    return undef; ## no critic (ProhibitExplicitReturnUndef)
+  }
+}
+
+sub get_isp_org {
+  my ($self, $ip) = @_;
+
+  return undef if !defined $ip || $ip !~ /\S/; ## no critic (ProhibitExplicitReturnUndef)
+
+  if ($self->{dbapi}->{isp}) {
+    return $self->_get('isp',$ip)->{organization};
+  } else {
+    return undef; ## no critic (ProhibitExplicitReturnUndef)
+  }
+}
+
+sub get_asn {
+  my ($self, $ip) = @_;
+
+  return undef if !defined $ip || $ip !~ /\S/; ## no critic (ProhibitExplicitReturnUndef)
+
+  if ($self->{dbapi}->{asn}) {
+    return $self->_get('asn',$ip)->{asn};
+  } elsif ($self->{dbapi}->{isp}) {
+    return $self->_get('isp',$ip)->{asn};
+  } else {
+    return undef; ## no critic (ProhibitExplicitReturnUndef)
+  }
+}
+
+sub get_asn_org {
+  my ($self, $ip) = @_;
+
+  return undef if !defined $ip || $ip !~ /\S/; ## no critic (ProhibitExplicitReturnUndef)
+
+  if ($self->{dbapi}->{asn}) {
+    return $self->_get('asn',$ip)->{asn_organization};
+  } elsif ($self->{dbapi}->{isp}) {
+    return $self->_get('isp',$ip)->{asn_organization};
+  } else {
+    return undef; ## no critic (ProhibitExplicitReturnUndef)
+  }
+}
+
+sub get_all {
+  my ($self, $ip) = @_;
+
+  return undef if !defined $ip || $ip !~ /\S/; ## no critic (ProhibitExplicitReturnUndef)
+
+  my $all = {};
+
+  if ($ip =~ IS_IP_PRIVATE) {
+    return { 'country' => '**' };
+  }
+
+  if ($ip !~ IS_IP_ADDRESS) {
+    $ip = name_to_ip($ip);
+    if (!defined $ip) {
+      return { 'country' => 'XX' };
+    }
+  }
+  
+  if ($self->{dbapi}->{city}) {
+    my $res = $self->_get('city',$ip);
+    $all->{$_} = $res->{$_} foreach (keys %$res);
+  } elsif ($self->{dbapi}->{country}) {
+    my $res = $self->_get('country',$ip);
+    $all->{$_} = $res->{$_} foreach (keys %$res);
+  }
+
+  if ($self->{dbapi}->{isp}) {
+    my $res = $self->_get('isp',$ip);
+    $all->{$_} = $res->{$_} foreach (keys %$res);
+  }
+
+  if ($self->{dbapi}->{asn}) {
+    my $res = $self->_get('asn',$ip);
+    $all->{$_} = $res->{$_} foreach (keys %$res);
+  }
+
+  return $all;
+}
+
+sub can {
+  my ($self, $check) = @_;
+
+  return defined $self->{dbapi}->{$check};
+}
+
+# TODO: use SA internal dns synchronously?
+# This shouldn't be called much, as plugins
+# should do their own resolving if needed
+sub name_to_ip {
+  my $name = shift;
+  if (my $ip = inet_aton($name)) {
+    $ip = inet_ntoa($ip);
+    dbg("geodb: resolved internally $name: $ip");
+    return $ip;
+  }
+  dbg("geodb: failed to internally resolve $name");
+  return undef; ## no critic (ProhibitExplicitReturnUndef)
+}
+
+sub _get {
+  my ($self, $type, $ip) = @_;
+
+  # reset cache at 100 ips
+  if (scalar keys %{$self->{cache}} >= 100) {
+    $self->{cache} = ();
+  }
+
+  if (!exists $self->{cache}{$ip}{$type}) {
+    if ($self->{dbapi}->{$type}) {
+      $self->{cache}{$ip}{$type} = $self->{dbapi}->{$type}->($self,$ip);
+    } else {
+      return undef; ## no critic (ProhibitExplicitReturnUndef)
+    }
+  }
+
+  return $self->{cache}{$ip}{$type};
+}
+
+1;
index 90a3a4c237768c5881d9a428cfb6d3a05456c422..c3487a07e7968229af744040214860cd00b02f30 100644 (file)
@@ -24,9 +24,6 @@ use strict;
 use warnings;
 use re 'taint';
 
-require 5.008;     # need basic Unicode support for HTML::Parser::utf8_mode
-# require 5.008008;  # Bug 3787; [perl #37950]: Malformed UTF-8 character ...
-
 use HTML::Parser 3.43 ();
 use Mail::SpamAssassin::Logger;
 use Mail::SpamAssassin::Constants qw(:sa);
@@ -66,7 +63,7 @@ my %elements_whitespace = map {; $_ => 1 }
 
 # elements that push URIs
 my %elements_uri = map {; $_ => 1 }
-  qw( body table tr td a area link img frame iframe embed script form base bgsound ),
+  qw( body table tr td a area link img frame iframe embed script form base bgsound meta ),
 ;
 
 # style attribute not accepted
@@ -248,6 +245,18 @@ sub parse {
   # the HTML::Parser API won't do it for us
   $text =~ s/<(\w+)\s*\/>/<$1>/gi;
 
+  # Normalize unicode quotes, messes up attributes parsing
+  # U+201C e2 80 9c LEFT DOUBLE QUOTATION MARK
+  # U+201D e2 80 9d RIGHT DOUBLE QUOTATION MARK
+  # Examples of input:
+  # <a href=\x{E2}\x{80}\x{9D}https://foobar.com\x{E2}\x{80}\x{9D}>
+  # .. results in uri "\x{E2}\x{80}\x{9D}https://foobar.com\x{E2}\x{80}\x{9D}"
+  if (utf8::is_utf8($text)) {
+    $text =~ s/(?:\x{201C}|\x{201D})/"/g;
+  } else {
+    $text =~ s/\x{E2}\x{80}(?:\x{9C}|\x{9D})/"/g;
+  }
+
   if (!$self->UNIVERSAL::can('utf8_mode')) {
     # utf8_mode is cleared by default, only warn if it would need to be set
     warn "message: cannot set utf8_mode, module HTML::Parser is too old\n"
@@ -352,8 +361,8 @@ sub canon_uri {
   my ($self, $uri) = @_;
 
   # URIs don't have leading/trailing whitespace ...
-  $uri =~ s/^\s+//;
-  $uri =~ s/\s+$//;
+  $uri =~ s/^[\s\xA0]+//;
+  $uri =~ s/[\s\xA0]+$//;
 
   # Make sure all the URIs are nice and short
   if (length $uri > MAX_URI_LENGTH) {
@@ -414,6 +423,17 @@ sub html_uri {
       }
     }
   }
+  elsif ($tag eq "meta" &&
+    exists $attr->{'http-equiv'} &&
+    exists $attr->{content} &&
+    $attr->{'http-equiv'} =~ /refresh/i &&
+    $attr->{content} =~ /\burl\s*=/i)
+  {
+      my $uri = $attr->{content};
+      $uri =~ s/^.*\burl\s*=\s*//i;
+      $uri =~ s/\s*;.*//i;
+      $self->push_uri($tag, $uri);
+  }
 }
 
 # this might not be quite right, may need to pay attention to table nesting
@@ -516,7 +536,7 @@ sub text_style {
            my $whcolor = $1 ? 'bgcolor' : 'fgcolor';
            my $value = lc $2;
 
-           if ($value =~ /rgb/) {
+           if (index($value, 'rgb') >= 0) {
              $value =~ tr/0-9,//cd;
              my @rgb = split(/,/, $value);
               $new{$whcolor} = sprintf("#%02x%02x%02x",
@@ -705,6 +725,8 @@ sub html_tests {
   {
     $self->{charsets} .= exists $self->{charsets} ? " $1" : $1;
   }
+
+  # todo: capture URI from meta refresh tag
 }
 
 sub display_text {
@@ -1154,7 +1176,7 @@ sub _merge_uri {
     return "/" . $r_path;
   }
   else {
-    if ($base_path =~ m|/|) {
+    if (index($base_path, '/') >= 0) {
       $base_path =~ s|(?<=/)[^/]*$||;
     }
     else {
index 993cbe8a4c892e5a35f8d503f6f14f8593b569cf..c0d4a9510aa6a3edd9677e3fcb63c387bc9e0642 100644 (file)
@@ -78,9 +78,6 @@ sub is_charset_ok_for_locales {
   $cs =~ s/:.*$//gs;            # trim off multiple charsets, just use 1st
   dbg ("locales: is $cs ok for @locales?");
 
-  study $cs;  # study is a no-op since perl 5.16.0, eliminating related bugs
-  #warn "JMD $cs";
-
   # always OK (the net speaks mostly roman charsets)
   return 1 if ($cs eq 'USASCII');
   return 1 if ($cs eq 'ASCII');
index 1d3ef78732dd3256bae913e57f64a344231aea0a..482d5a70907dd51a3e361e6e01af172931958794 100644 (file)
@@ -69,6 +69,11 @@ sub jittery_one_second_sleep {
   Time::HiRes::sleep(rand(1.0) + 0.5);
 }
 
+sub jittery_half_second_sleep {
+  my ($self) = @_;
+  Time::HiRes::sleep(rand(0.5) + 0.25);
+}
+
 ###########################################################################
 
 1;
index 2bf14100cb69947b000d26de24014283bf3faf03..e2803d4bfec3d236e217e3dc40aef314e1b4f0dd 100644 (file)
@@ -55,7 +55,7 @@ sub safe_lock {
 
   my $lock_file = "$path.mutex";
   my $umask = umask(~$mode);
-  my $fh = new IO::File();
+  my $fh = IO::File->new;
 
   if (!$fh->open ($lock_file, O_RDWR|O_CREAT)) {
       umask $umask; # just in case
index f1fab35a84b52b86174908994dde04ff6636287c..2a0e4b956b362b6fee1962ee916894838658f630 100644 (file)
@@ -29,6 +29,7 @@ use Mail::SpamAssassin::Logger;
 use File::Spec;
 use Time::Local;
 use Fcntl qw(:DEFAULT :flock);
+use Errno qw(EEXIST);
 
 our @ISA = qw(Mail::SpamAssassin::Locker);
 
@@ -60,7 +61,7 @@ sub safe_lock {
   $max_retries ||= 30;
   $mode ||= "0700";
   $mode = (oct $mode) & 0666;
-  dbg ("locker: mode is $mode");
+  dbg ("locker: mode is %03o", $mode);
 
   my $lock_file = "$path.lock";
   my $hname = Mail::SpamAssassin::Util::fq_hostname();
@@ -76,11 +77,11 @@ sub safe_lock {
       die "locker: safe_lock: cannot create tmp lockfile $lock_tmp for $lock_file: $!\n";
   }
   umask $umask;
-  autoflush LTMP 1;
+  LTMP->autoflush(1);
   dbg("locker: safe_lock: created $lock_tmp");
 
-  for (my $retries = 0; $retries < $max_retries; $retries++) {
-    if ($retries > 0) { $self->jittery_one_second_sleep(); }
+  for (my $retries = 0; $retries < $max_retries * 2; $retries++) {
+    if ($retries > 0) { $self->jittery_half_second_sleep(); }
     print LTMP "$hname.$$\n"  or warn "Error writing to $lock_tmp: $!";
     dbg("locker: safe_lock: trying to get lock on $path with $retries retries");
     if (link($lock_tmp, $lock_file)) {
@@ -88,6 +89,10 @@ sub safe_lock {
       $is_locked = 1;
       last;
     }
+    # if lock exists, it's already likely locked, no point complaining here
+    unless ($!{EEXIST}) {
+      warn "locker: creating link $lock_file to $lock_tmp failed: '$!'";
+    }
     # link _may_ return false even if the link _is_ created
     @stat = lstat($lock_tmp);
     @stat  or warn "locker: error accessing $lock_tmp: $!";
@@ -149,7 +154,7 @@ sub safe_unlock {
     warn "locker: safe_unlock: failed to create lock tmpfile $lock_tmp: $!";
     return;
   } else {
-    autoflush LTMP 1;
+    LTMP->autoflush(1);
     print LTMP "\n"  or warn "Error writing to $lock_tmp: $!";
 
     if (!(@stat_ourtmp = stat(LTMP)) || (scalar(@stat_ourtmp) < 11)) {
@@ -157,7 +162,7 @@ sub safe_unlock {
       warn "locker: safe_unlock: failed to create lock tmpfile $lock_tmp";
       close LTMP  or die "error closing $lock_tmp: $!";
       unlink($lock_tmp)
-        or warn "locker: safe_lock: unlink of lock file failed: $!\n";
+        or warn "locker: safe_lock: unlink of lock file $lock_tmp failed: $!\n";
       return;
     }
   }
@@ -169,7 +174,7 @@ sub safe_unlock {
 
   close LTMP  or die "error closing $lock_tmp: $!";
   unlink($lock_tmp)
-    or warn "locker: safe_lock: unlink of lock file failed: $!\n";
+    or warn "locker: safe_lock: unlink of lock file $lock_tmp failed: $!\n";
 
   # 2. If the ctime hasn't been modified, unlink the file and return. If the
   # lock has expired, sleep the usual random interval before returning. If we
@@ -191,7 +196,7 @@ sub safe_unlock {
   {
     # things are good: the ctimes match so it was our lock
     unlink($lock_file)
-      or warn "locker: safe_unlock: unlink failed: $lock_file\n";
+      or warn "locker: safe_unlock: unlinking $lock_file failed: $!\n";
     dbg("locker: safe_unlock: unlink $lock_file");
 
     if ($ourtmp_ctime >= $lock_ctime + LOCK_MAX_AGE) {
index 031a19cd425a0f3c97c3a671e0d9a9c2ffcec9a1..b2ab5e958e51bf486923df231083cb104c6f15e4 100644 (file)
@@ -72,7 +72,7 @@ sub safe_lock {
       return 1;
     }
     my @stat = stat($lock_file);
-    @stat  or warn "locker: error accessing $lock_file: $!";
+    @stat  or dbg("locker: error accessing $lock_file: $!");
 
     # check age of lockfile ctime
     my $age = ($#stat < 11 ? undef : $stat[10]);
index 6e86fca73dc4eff5e09dea195ad37420a6268ecd..34d694368bedd0051d9f816b92ecf9fb5aa5e65f 100644 (file)
@@ -75,7 +75,22 @@ $LOG_SA{facility} = {};              # no dbg facilities turned on
 
 # always log to stderr initially
 use Mail::SpamAssassin::Logger::Stderr;
-$LOG_SA{method}->{stderr} = Mail::SpamAssassin::Logger::Stderr->new();
+$LOG_SA{method}->{stderr} =
+  Mail::SpamAssassin::Logger::Stderr->new(escape =>
+    exists $ENV{'SA_LOGGER_ESCAPE'} ? $ENV{'SA_LOGGER_ESCAPE'} : 1
+  );
+
+# Use of M:SA:Util causes circular dependencies, separate helper here.
+my %escape_map =
+  ("\r" => '\\r', "\n" => '\\n', "\t" => '\\t', "\\" => '\\\\');
+sub escape_str {
+  # Things are already forced as octets by _log, no utf8::encode needed
+  # Control chars, DEL, backslash
+  $_[0] =~ s@
+    ( [\x00-\x1F\x7F\x80-\xFF\\] )
+    @ $escape_map{$1} || sprintf("\\x{%02X}",ord($1))
+    @egsx;
+}
 
 =head1 METHODS
 
@@ -165,7 +180,7 @@ sub log_message {
     # don't log them -- this is caller 0, the use'ing package is 1, the eval is 2
     my @caller = caller 2;
     return if (defined $caller[3] && defined $caller[0] &&
-                      $caller[3] =~ /^\(eval\)$/ &&
+                      $caller[3] eq '(eval)' &&
                       $caller[0] =~ m#^Mail::SpamAssassin(?:$|::)#);
   }
 
@@ -215,13 +230,15 @@ sub _log_message {
   foreach my $line (split(/\n/, $_[1])) {
     # replace control characters with "_", tabs and spaces get
     # replaced with a single space.
-    $line =~ tr/\x09\x20\x00-\x1f/  _/s;
+    # Deprecated here, see new Bug 6583 escaping in Logger/*.pm modules
+    #$line =~ tr/\x09\x20\x00-\x1f/  _/s;
+
     if ($first) {
       $first = 0;
     } else {
-      local $1;
       $line =~ s/^([^:]+?):/$1: [...]/;
     }
+
     while (my ($name, $object) = each %{ $LOG_SA{method} }) {
       $object->log_message($_[0], $line, $_[2]);
     }
@@ -274,6 +291,9 @@ sub _log {
   }
 
   my ($level, $message, @args) = @_;
+
+  utf8::encode($message)  if utf8::is_utf8($message); # handle as octets
+
   $message =~ s/^(?:[a-z0-9_-]*):\s*//i;
 
   $message = sprintf($message,@args)  if @args;
@@ -284,18 +304,28 @@ sub _log {
   log_message(($level == INFO ? "info" : "dbg"), $message);
 }
 
-=item add(method => 'syslog', socket => $socket, facility => $facility)
+=item add(method => 'syslog', socket => $socket, facility => $facility, escape => $escape)
 
 C<socket> is the type the syslog ("unix" or "inet").  C<facility> is the
 syslog facility (typically "mail").
 
-=item add(method => 'file', filename => $file)
+If optional C<escape> is true, all non-ascii characters are escaped for safe
+output: backslashes change to \\ and non-ascii chars to \x{XX} or \x{XXXX}
+(Unicode).  If not defined, pre-4.0 style sanitizing is used
+( tr/\x09\x20\x00-\x1f/_/s ).
 
-C<filename> is the name of the log file.
+Escape value can be overridden with environment variable
+C<SA_LOGGER_ESCAPE>.
 
-=item add(method => 'stderr')
+=item add(method => 'file', filename => $file, escape => $escape)
 
-No options are needed for stderr logging, just don't close stderr first.
+C<filename> is the name of the log file.  C<escape> works as described
+above.
+
+=item add(method => 'stderr', escape => $escape)
+
+No options are needed for stderr logging, just don't close stderr first. 
+C<escape> works as described above.
 
 =cut
 
@@ -307,6 +337,10 @@ sub add {
 
   return 0 if $class !~ /^\w+$/; # be paranoid
 
+  if (exists $ENV{'SA_LOGGER_ESCAPE'}) {
+    $params{escape} = $ENV{'SA_LOGGER_ESCAPE'}
+  }
+
   eval 'use Mail::SpamAssassin::Logger::'.$class.'; 1'
   or do {
     my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
index 32254fcb73a0bda013e8d4dcd265f6c6b3b76a92..016beb162405616cb4a4bae3504f0fd9ee57062c 100644 (file)
@@ -37,13 +37,17 @@ use re 'taint';
 use POSIX ();
 use Time::HiRes ();
 use Mail::SpamAssassin::Logger;
-use Mail::SpamAssassin::Util qw(am_running_on_windows);
 
 our @ISA = ();
 
 # ADDING OS-DEPENDENT LINE TERMINATOR - BUG 6456
+
+# Using Mail::SpamAssassin::Util::am_running_on_windows() leads to circular
+# dependencies. So, we are duplicating the code instead.
+use constant RUNNING_ON_WINDOWS => ($^O =~ /^(?:mswin|dos|os2)/oi);
+
 my $eol = "\n";
-if (am_running_on_windows()) {
+if (RUNNING_ON_WINDOWS) {
   $eol = "\r\n";
 }
 
@@ -58,6 +62,7 @@ sub new {
   my %params = @_;
   $self->{filename} = $params{filename} || 'spamassassin.log';
   $self->{timestamp_fmt} = $params{timestamp_fmt};
+  $self->{escape} = $params{escape} if exists $params{escape};
 
   if (! $self->init()) {
     die "logger: file initialization failed$eol";
@@ -101,6 +106,16 @@ sub log_message {
   }
   $timestamp .= ' '  if $timestamp ne '';
 
+  if ($self->{escape}) {
+    # Bug 6583, escape
+    Mail::SpamAssassin::Logger::escape_str($msg);
+  } elsif (!exists $self->{escape}) {
+    # Backwards compatible pre-4.0 escaping, if $escape not given.
+    # replace control characters with "_", tabs and spaces get
+    # replaced with a single space.
+    $msg =~ tr/\x09\x20\x00-\x1f/  _/s;
+  }
+
   my($nwrite) = syswrite(STDLOG, sprintf("%s[%s] %s: %s%s",
                                          $timestamp, $$, $level, $msg, $eol));
   defined $nwrite  or warn "error writing to log file: $!";
index 68398b8e15c4c43855fac59c0aeed6c5842c44bc..fbd6f83130bde750a1f460a3d209a3da31420e9f 100644 (file)
@@ -36,6 +36,7 @@ use re 'taint';
 
 use POSIX ();
 use Time::HiRes ();
+use Mail::SpamAssassin::Logger;
 
 our @ISA = ();
 
@@ -59,6 +60,7 @@ sub new {
 
   my %params = @_;
   $self->{timestamp_fmt} = $params{timestamp_fmt};
+  $self->{escape} = $params{escape} if exists $params{escape};
 
   return($self);
 }
@@ -83,6 +85,16 @@ sub log_message {
   }
   $timestamp .= ' '  if $timestamp ne '';
 
+  if ($self->{escape}) {
+    # Bug 6583, escape
+    Mail::SpamAssassin::Logger::escape_str($msg);
+  } elsif (!exists $self->{escape}) {
+    # Backwards compatible pre-4.0 escaping, if $escape not given.
+    # replace control characters with "_", tabs and spaces get
+    # replaced with a single space.
+    $msg =~ tr/\x09\x20\x00-\x1f/  _/s;
+  }
+
   my($nwrite) = syswrite(STDERR, sprintf("%s[%d] %s: %s%s",
                                          $timestamp, $$, $level, $msg, $eol));
   defined $nwrite  or warn "error writing to log file: $!";
index 15b68e7d1a15269999ce36246f7232bc1f94d0c3..b96c59ccbaef6c220320e2ee5f1fc6039dc140f8 100644 (file)
@@ -73,6 +73,7 @@ sub new {
   $self->{log_socket} = $params{socket};
   $self->{log_facility} = $params{facility};
   $self->{timestamp_fmt} = $params{timestamp_fmt};
+  $self->{escape} = $params{escape} if exists $params{escape};
 
   if (! $self->init()) {
     die "logger: syslog initialization failed\n";
@@ -148,6 +149,16 @@ sub log_message {
     $msg = '(bad prio: ' . $_[1] . ') ' . $msg;
   }
 
+  if ($self->{escape}) {
+    # Bug 6583, escape
+    Mail::SpamAssassin::Logger::escape_str($msg);
+  } elsif (!exists $self->{escape}) {
+    # Backwards compatible pre-4.0 escaping, if $escape not given
+    # replace control characters with "_", tabs and spaces get
+    # replaced with a single space.
+    $msg =~ tr/\x09\x20\x00-\x1f/  _/s;
+  }
+
   # install a new handler for SIGPIPE -- this signal has been
   # found to occur with syslog-ng after syslog-ng restarts.
   local $SIG{'PIPE'} = sub {
index ba74ba8c05968c1342c6484d1e285f7a415416c8..192fb89deaa89c79e57865bc80e8f257b0a3de4f 100644 (file)
@@ -46,10 +46,8 @@ use strict;
 use warnings;
 use re 'taint';
 
-BEGIN {
-  eval { require Digest::SHA; import Digest::SHA qw(sha1 sha1_hex); 1 }
-  or do { require Digest::SHA1; import Digest::SHA1 qw(sha1 sha1_hex) }
-}
+use Digest::SHA qw(sha1 sha1_hex);
+use Scalar::Util qw(tainted);
 
 use Mail::SpamAssassin;
 use Mail::SpamAssassin::Message::Node;
@@ -159,7 +157,7 @@ sub new {
   if (ref $message eq 'ARRAY') {
      @message = @{$message};
   }
-  elsif (ref($message) eq 'GLOB' || ref($message) =~ /^IO::/) {
+  elsif (ref($message) eq 'GLOB' || index(ref($message), 'IO::') == 0) {
     if (defined fileno $message) {
 
       # sysread+split avoids a Perl I/O bug (Bug 5985)
@@ -207,14 +205,14 @@ sub new {
   # messages? Tainting the message is important because it prevents certain
   # exploits later.
   if (Mail::SpamAssassin::Util::am_running_in_taint_mode() &&
-        grep { !Scalar::Util::tainted($_) } @message) {
+        grep { !tainted($_) } @message) {
     local($_);
     # To preserve newlines, no joining and splitting here, process each line
     # directly as is.
     foreach (@message) {
       $_ = Mail::SpamAssassin::Util::taint_var($_);
     }
-    if (grep { !Scalar::Util::tainted($_) } @message) {
+    if (grep { !tainted($_) } @message) {
       die "Mail::SpamAssassin::Message failed to enforce message taintness";
     }
   }
@@ -256,7 +254,7 @@ sub new {
   # bug 4363
   # Check to see if we should do CRLF instead of just LF
   # For now, just check the first and last line and do whatever it does
-  if (@message && ($message[0] =~ /\015\012/ || $message[-1] =~ /\015\012/)) {
+  if (index($message[0], "\015\012") != -1 || index($message[-1], "\015\012") != -1) {
     $self->{line_ending} = "\015\012";
     dbg("message: line ending changed to CRLF");
   }
@@ -270,7 +268,12 @@ sub new {
   for (;;) {
     # make sure not to lose the last header field when there is no body
     my $eof = !@message;
-    my $current = $eof ? "\n" : shift @message;
+    my $current = $eof ? $self->{line_ending} : shift @message;
+
+    # Bug 7785: spamass-milter breaks wrapped headers, add any missing \r
+    if ($squash_crlf) {
+      $current =~ s/(?<!\015)\012/\015\012/gs;
+    }
 
     if ( $current =~ /^[ \t]/ ) {
       # This wasn't useful in terms of a rule, but we may want to treat it
@@ -306,7 +309,7 @@ sub new {
         }
       }
 
-      if ($current =~ /^\r?$/) {  # a regular end of a header section
+      if ($current eq $self->{line_ending}) {  # a regular end of a header section
        if ($eof) {
          $self->{'missing_head_body_separator'} = 1;
        } else {
@@ -395,7 +398,7 @@ sub new {
   # either a blank line or the boundary (if defined), insert a blank line
   # to ensure proper parsing - do not consider MIME headers at the beginning of the body
   # to be part of the message headers.
-  if ($self->{'type'} =~ /^multipart\//i && $#message > 0 && $message[0] =~ /\S/)
+  if (index($self->{'type'}, 'multipart/') == 0 && $#message > 0 && $message[0] =~ /\S/)
   {
     if (!defined $boundary || $message[0] !~ /^--\Q$boundary\E/)
     {
@@ -537,6 +540,77 @@ sub get_pristine_body {
   return $self->{pristine_body};
 }
 
+=item get_pristine_body_digest()
+
+Returns SHA1 hex digest of the pristine message body.
+CRLF line endings are normalized to LF before hashing.
+
+=cut
+
+sub get_pristine_body_digest {
+  my ($self) = @_;
+
+  return $self->{pristine_body_digest} if exists $self->{pristine_body_digest};
+
+  if ($self->{line_ending} eq "\015\012") {
+    # Don't make a copy, process line by line to save memory
+    # CRLF should be exception, so it's not that critical here
+    my $sha = Digest::SHA->new('sha1');
+    while ($self->{pristine_body} =~ /(.*?)(\015\012)?/gs) {
+      $sha->add($1.(defined $2 ? "\012" : ""));
+    }
+    $self->{pristine_body_digest} = $sha->hexdigest;
+  } else {
+    $self->{pristine_body_digest} = sha1_hex($self->{pristine_body});
+  }
+
+  dbg("message: pristine body digest: ".$self->{pristine_body_digest});
+  return $self->{pristine_body_digest};
+}
+
+# ---------------------------------------------------------------------------
+
+=item get_msgid()
+
+Returns Message-ID header for the message, with <> and surrounding
+whitespace removed. Returns undef, if nothing found between <>.
+
+=cut
+
+sub get_msgid {
+  my ($self) = @_;
+
+  my $msgid = $self->get_header("Message-Id");
+  if (defined $msgid && $msgid =~ /^\s*<(.+)>\s*$/s) {
+    return $1;
+  } else {
+    return;
+  }
+}
+
+=item generate_msgid()
+
+Generate a calculated "Message-ID" in B<sha1hex@sa_generated> format, using
+To, Date headers and pristine body as source for hashing.
+
+=cut
+
+sub generate_msgid {
+  my ($self) = @_;
+
+  return $self->{msgid_generated} if exists $self->{msgid_generated};
+
+  # See Bug 5185, not using Received headers etc anymore
+  my $to = $self->get_header("To") || '';
+  my $date = $self->get_header("Date") || '';
+  my $body_digest = $self->get_pristine_body_digest();
+
+  $self->{msgid_generated} =
+    sha1_hex($to."\000".$date."\000".$body_digest).'@sa_generated';
+
+  return $self->{msgid_generated};
+}
+
 # ---------------------------------------------------------------------------
 
 =item extract_message_metadata($permsgstatus)
@@ -706,6 +780,7 @@ sub finish {
 # temporary files are deleted even if the finish() method is omitted
 sub DESTROY {
   my $self = shift;
+
   # best practices: prevent potential calls to eval and to system routines
   # in code of a DESTROY method from clobbering global variables $@ and $! 
   local($@,$!);  # keep outer error handling unaffected by DESTROY
@@ -774,7 +849,7 @@ sub parse_body {
     #
     my ($msg, $boundary, $body, $subparse) = @$toparse;
 
-    if ($msg->{'type'} =~ m{^multipart/}i && defined $boundary && $subparse > 0) {
+    if (index($msg->{'type'}, 'multipart/') == 0 && defined $boundary && $subparse > 0) {
       $self->_parse_multipart($toparse);
     }
     else {
@@ -782,7 +857,8 @@ sub parse_body {
       $self->_parse_normal($toparse);
 
       # bug 5041: process message/*, but exclude message/partial content types
-      if ($msg->{'type'} =~ m{^message/(?!partial\z)}i && $subparse > 0)
+      if (index($msg->{'type'}, 'message/') == 0 &&
+          $msg->{'type'} ne 'message/partial' && $subparse > 0)
       {
         # Just decode the part, but we don't need the resulting string here.
         $msg->decode(0);
@@ -798,7 +874,7 @@ sub parse_body {
         # bug 5051, bug 3748: check $msg->{decoded}: sometimes message/* parts
         # have no content, and we get stuck waiting for STDIN, which is bad. :(
 
-        if ($msg->{'type'} =~ m{^message/(?:rfc822|global)\z}i &&
+        if (($msg->{'type'} eq 'message/rfc822' || $msg->{'type'} eq 'message/global') &&
             defined $msg->{'decoded'} && $msg->{'decoded'} ne '')
         {
          # Ok, so this part is still semi-recursive, since M::SA::Message
@@ -857,6 +933,7 @@ sub _parse_multipart {
   my($self, $toparse) = @_;
 
   my ($msg, $boundary, $body, $subparse) = @{$toparse};
+  my $nested_boundary = 0;
 
   # we're not supposed to be a leaf, so prep ourselves
   $msg->{'body_parts'} = [];
@@ -907,6 +984,7 @@ sub _parse_multipart {
   my $header;
   my $part_array;
   my $found_end_boundary;
+  my $found_last_end_boundary;
   my $partcnt = 0;
 
   my $line_count = @{$body};
@@ -915,7 +993,12 @@ sub _parse_multipart {
     # deal with the mime part;
     # a triage before an unlikely-to-match regexp avoids a CPU hotspot
     $found_end_boundary = defined $boundary && substr($_,0,2) eq '--'
-                          && /^--\Q$boundary\E(?:--)?\s*$/;
+                          && /^--\Q$boundary\E(--)?\s*$/;
+    $found_last_end_boundary = $found_end_boundary && $1;
+    if ($found_end_boundary && $nested_boundary) {
+      $found_end_boundary = 0;
+      $nested_boundary = 0 if ($found_last_end_boundary); # bug 7358 - handle one level of non-unique boundary string
+    }
     if ( --$line_count == 0 || $found_end_boundary ) {
       my $line = $_; # remember the last line
 
@@ -951,8 +1034,20 @@ sub _parse_multipart {
         $part_array = [];
       }
 
-      my($p_boundary);
-      ($part_msg->{'type'}, $p_boundary) = Mail::SpamAssassin::Util::parse_content_type($part_msg->header('content-type'));
+      ($part_msg->{'type'}, my $p_boundary, undef, undef, my $ct_was_missing) =
+          Mail::SpamAssassin::Util::parse_content_type($part_msg->header('content-type'));
+
+      # bug 5741: if ct was missing and parent == multipart/digest, then
+      # type should be set as message/rfc822
+      if ($ct_was_missing) {
+        if ($msg->{'type'} eq 'multipart/digest') {
+          dbg("message: missing type, setting multipart/digest child as message/rfc822");
+          $part_msg->{'type'} = 'message/rfc822';
+        } else {
+          dbg("message: missing type, setting as default text/plain");
+        }
+      }
+
       $p_boundary ||= $boundary;
       dbg("message: found part of type ".$part_msg->{'type'}.", boundary: ".(defined $p_boundary ? $p_boundary : ''));
 
@@ -962,12 +1057,8 @@ sub _parse_multipart {
       push(@{$self->{'parse_queue'}}, [ $part_msg, $p_boundary, $part_array, $subparse ]);
       $msg->add_body_part($part_msg);
 
-      # rfc 1521 says /^--boundary--$/, some MUAs may just require /^--boundary--/
-      # but this causes problems with horizontal lines when the boundary is
-      # made up of dashes as well, etc.
       if (defined $boundary) {
-        # no re "strict";  # since perl 5.21.8: Ranges of ASCII printables...
-        if ($line =~ /^--\Q${boundary}\E--\s*$/) {
+        if ($found_last_end_boundary) {
          # Make a note that we've seen the end boundary
          $self->{mime_boundary_state}->{$boundary}--;
           last;
@@ -1016,6 +1107,12 @@ sub _parse_multipart {
         if ($header) {
           my ( $key, $value ) = split ( /:\s*/, $header, 2 );
           $part_msg->header( $key, $value );
+          if (defined $boundary && lc $key eq 'content-type') {
+           my (undef, $nested_bound) = Mail::SpamAssassin::Util::parse_content_type($part_msg->header('content-type'));
+            if (defined $nested_bound && $nested_bound eq $boundary) {
+                     $nested_boundary = 1;
+            }
+          }
         }
         $in_body = 1;
 
@@ -1070,12 +1167,18 @@ sub _parse_normal {
 
   dbg("message: parsing normal part");
 
-  # 0: content-type, 1: boundary, 2: charset, 3: filename
+  # 0: content-type, 1: boundary, 2: charset, 3: filename 4: ct_missing
   my @ct = Mail::SpamAssassin::Util::parse_content_type($msg->header('content-type'));
 
   # multipart sections are required to have a boundary set ...  If this
   # one doesn't, assume it's malformed and revert to text/plain
-  $msg->{'type'} = ($ct[0] !~ m@^multipart/@i || defined $boundary ) ? $ct[0] : 'text/plain';
+  # bug 5741: don't overwrite the default type assigned by _parse_multipart()
+  if (!$ct[4]) {
+    $msg->{'type'} = (index($ct[0], 'multipart/') != 0 || defined $boundary) ?
+      $ct[0] : 'text/plain'
+  } else {
+    dbg("message: missing type, setting previous multipart type: %s", $msg->{'type'});
+  }
   $msg->{'charset'} = $ct[2];
 
   # attempt to figure out a name for this attachment if there is one ...
@@ -1086,9 +1189,6 @@ sub _parse_normal {
   elsif ($ct[3]) {
     $msg->{'name'} = $ct[3];
   }
-  if ($msg->{'name'}) {
-    $msg->{'name'} = Encode::decode("MIME-Header", $msg->{'name'});
-  }
 
   $msg->{'boundary'} = $boundary;
 
@@ -1096,7 +1196,8 @@ sub _parse_normal {
   # ahead and write the part data out to a temp file -- why keep sucking
   # up RAM with something we're not going to use?
   #
-  if ($msg->{'type'} !~ m@^(?:text/(?:plain|html)$|message\b)@) {
+  unless ($msg->{'type'} eq 'text/plain' || $msg->{'type'} eq 'text/html' ||
+          index($msg->{'type'}, 'message/') == 0) {
     my($filepath, $fh);
     eval {
       ($filepath, $fh) = Mail::SpamAssassin::Util::secure_tmpfile();  1;
@@ -1131,7 +1232,7 @@ sub get_mimepart_digests {
   if (!exists $self->{mimepart_digests}) {
     # traverse all parts which are leaves, recursively
     $self->{mimepart_digests} =
-      [ map(sha1_hex($_->decode) . ':' . lc($_->{type}||''),
+      [ map(sha1_hex($_->decode) . ':' . ($_->{type}||''),
             $self->find_parts(qr/^/,1,1)) ];
   }
   return $self->{mimepart_digests};
@@ -1202,16 +1303,19 @@ sub get_body_text_array_common {
       # text/plain rendered as html otherwise.
       if ($html_needs_setting && $type eq 'text/html') {
         $self->{metadata}->{html} = $p->{html_results};
+        push @{$self->{metadata}->{html_all}}, $p->{html_results};
       }
     }
   }
 
   # whitespace handling (warning: small changes have large effects!)
-  $text =~ s/\n+\s*\n+/\f/gs;          # double newlines => form feed
+  $text =~ s/\n+\s*\n+/\x00/gs;                # double newlines => null
 # $text =~ tr/ \t\n\r\x0b\xa0/ /s;     # whitespace (incl. VT, NBSP) => space
-  $text =~ tr/ \t\n\r\x0b/ /s;         # whitespace (incl. VT) => space
-  $text =~ tr/\f/\n/;                  # form feeds => newline
+# $text =~ tr/ \t\n\r\x0b/ /s;         # whitespace (incl. VT) => single space
+  $text =~ s/\s+/ /gs;                 # Unicode whitespace => single space
+  $text =~ tr/\x00/\n/;                        # null => newline
 
+  utf8::encode($text) if utf8::is_utf8($text);
   my @textary = split_into_array_of_short_lines($text);
   $self->{$key} = \@textary;
 
@@ -1246,7 +1350,7 @@ sub get_decoded_body_text_array {
   my $scansize = $self->{rawbody_part_scan_size};
 
   # Find all parts which are leaves
-  my @parts = $self->find_parts(qr/^(?:text|message)\b/i,1);
+  my @parts = $self->find_parts(qr/^(?:text|message)\b/,1);
   return $self->{text_decoded} unless @parts;
 
   # Go through each part
index ca164e25009b74aee6cee1cd1f5af11e18c0d38d..f509e92d729dfb50da337fe0f981059a7b26d329 100644 (file)
@@ -50,6 +50,10 @@ use Mail::SpamAssassin::Dns;
 use Mail::SpamAssassin::PerMsgStatus;
 use Mail::SpamAssassin::Constants qw(:ip);
 
+my $IP_ADDRESS = IP_ADDRESS;
+my $IP_PRIVATE = IP_PRIVATE;
+my $LOCALHOST = LOCALHOST;
+
 # ---------------------------------------------------------------------------
 
 sub parse_received_headers {
@@ -117,10 +121,6 @@ sub parse_received_headers {
     }
   }
 
-  my $IP_ADDRESS = IP_ADDRESS;
-  my $IP_PRIVATE = IP_PRIVATE;
-  my $LOCALHOST = LOCALHOST;
-
   my @hdrs = $msg->get_header('Received');
 
   # Now add the single line headers like X-Originating-IP. (bug 5680)
@@ -336,9 +336,6 @@ sub parse_received_line {
   my $ident = '';
   my $envfrom = undef;
   my $mta_looked_up_dns = 0;
-  my $IP_ADDRESS = IP_ADDRESS;
-  my $IP_PRIVATE = IP_PRIVATE;
-  my $LOCALHOST = LOCALHOST;
   my $auth = '';
 
 # ---------------------------------------------------------------------------
@@ -430,7 +427,7 @@ sub parse_received_line {
     $auth = "GMX ($4 / $3)";
   }
   # Critical Path Messaging Server
-  elsif (/ \(authenticated as /&&/\) by .+ \(\d{1,2}\.\d\.\d{3}(?:\.\d{1,3})?\) \(authenticated as .+\) id /) {
+  elsif (/ \(authenticated as / && /\) by .+ \(\d{1,2}\.\d\.\d{3}(?:\.\d{1,3})?\) \(authenticated as .+\) id /) {
     $auth = 'CriticalPath';
   }
   # Postfix 2.3 and later with "smtpd_sasl_authenticated_header yes"
@@ -820,7 +817,7 @@ sub parse_received_line {
     
     # Received: from [193.220.176.134] by web40310.mail.yahoo.com via HTTP;
     # Wed, 12 Feb 2003 14:22:21 PST
-    if (/ via HTTP$/&&/^\[(${IP_ADDRESS})\] by (\S+) via HTTP$/) {
+    if (/ via HTTP$/ && /^\[(${IP_ADDRESS})\] by (\S+) via HTTP$/) {
       $ip = $1; $by = $2; goto enough;
     }
 
@@ -944,13 +941,13 @@ sub parse_received_line {
 
     # Received: from [129.24.215.125] by ws1-7.us4.outblaze.com with http for
     # _bushisevil_@mail.com; Thu, 13 Feb 2003 15:59:28 -0500
-    if (/ with http for /&&/^\[(${IP_ADDRESS})\] by (\S+) with http for /) {
+    if (/ with http for / && /^\[(${IP_ADDRESS})\] by (\S+) with http for /) {
       $ip = $1; $by = $2; goto enough;
     }
 
     # Received: from snake.corp.yahoo.com(216.145.52.229) by x.x.org via smap (V1.3)
     # id xma093673; Wed, 26 Mar 03 20:43:24 -0600
-    if (/ via smap /&&/^(\S+)\((${IP_ADDRESS})\) by (\S+) via smap /) {
+    if (/ via smap / && /^(\S+)\((${IP_ADDRESS})\) by (\S+) via smap /) {
       $mta_looked_up_dns = 1;
       $rdns = $1; $ip = $2; $by = $3; goto enough;
     }
@@ -965,13 +962,13 @@ sub parse_received_line {
     # Received: from [192.168.0.71] by web01-nyc.clicvu.com (Post.Office MTA
     # v3.5.3 release 223 ID# 0-64039U1000L100S0V35) with SMTP id com for
     # <x@x.org>; Tue, 25 Mar 2003 11:42:04 -0500
-    if (/ \(Post/&&/^\[(${IP_ADDRESS})\] by (\S+) \(Post/) {
+    if (/ \(Post/ && /^\[(${IP_ADDRESS})\] by (\S+) \(Post/) {
       $ip = $1; $by = $2; goto enough;
     }
 
     # Received: from [127.0.0.1] by euphoria (ArGoSoft Mail Server 
     # Freeware, Version 1.8 (1.8.2.5)); Sat, 8 Feb 2003 09:45:32 +0200
-    if (/ \(ArGoSoft/&&/^\[(${IP_ADDRESS})\] by (\S+) \(ArGoSoft/) {
+    if (/ \(ArGoSoft/ && /^\[(${IP_ADDRESS})\] by (\S+) \(ArGoSoft/) {
       $ip = $1; $by = $2; goto enough;
     }
 
@@ -984,7 +981,7 @@ sub parse_received_line {
 
     # Received: from faerber.muc.de by slarti.muc.de with BSMTP (rsmtp-qm-ot 0.4)
     # for asrg@ietf.org; 7 Mar 2003 21:10:38 -0000
-    if (/ with BSMTP/&&/^\S+ by \S+ with BSMTP/) {
+    if (/ with BSMTP/ && /^\S+ by \S+ with BSMTP/) {
       return 0;        # BSMTP != a TCP/IP handover, ignore it
     }
 
index 126e3a1f763dd66777aa28641294e0c6aefe5c9b..53bf78dc429bd083e368c948fb85dbb981ca239f 100644 (file)
@@ -36,8 +36,6 @@ use strict;
 use warnings;
 use re 'taint';
 
-require 5.008001;  # needs utf8::is_utf8()
-
 use Mail::SpamAssassin;
 use Mail::SpamAssassin::Constants qw(:sa);
 use Mail::SpamAssassin::HTML;
@@ -167,8 +165,8 @@ sub header {
   my $key    = lc($rawkey);
 
   # Trim whitespace off of the header keys
-  $key       =~ s/^\s+//;
-  $key       =~ s/\s+$//;
+  #$key       =~ s/^\s+//;
+  #$key       =~ s/\s+$//;
 
   if (@_) {
     my $raw_value = shift;
@@ -388,6 +386,12 @@ sub detect_utf16 {
        my $sum_l_o = 0;
        my $decoder = undef;
 
+       # avoid scan if BOM present
+       if( $data =~ /^(?:\xff\xfe|\xfe\xff)/ ) {
+               dbg( "message: detect_utf16: found BOM" );
+               return; # let perl figure it out from the BOM
+       }
+       
        my @msg_h = unpack 'H' x length( $data ), $data;
        my @msg_l = unpack 'h' x length( $data ), $data;
 
@@ -397,11 +401,11 @@ sub detect_utf16 {
                $sum_h_o += hex $msg_h[$i+1];
                $sum_l_e += hex $msg_l[$i];
                $sum_l_o += hex $msg_l[$i+1];
-               if( $check_char =~ /20 00/ ) {
+               if (index($check_char, '20 00') >= 0) {
                        # UTF-16LE space char detected
                        $utf16le_clues++;
                }
-               if( $check_char =~ /00 20/ ) {
+               if (index($check_char, '00 20') >= 0) {
                        # UTF-16BE space char detected
                        $utf16be_clues++;
                }
@@ -416,7 +420,7 @@ sub detect_utf16 {
        if( $utf16le_clues > $utf16be_clues ) {
                dbg( "message: detect_utf16: UTF-16LE" );
                $decoder = Encode::find_encoding("UTF-16LE");
-       } elsif( $utf16le_clues > $utf16be_clues ) {
+       } elsif( $utf16be_clues > $utf16le_clues ) {
                dbg( "message: detect_utf16: UTF-16BE" );
                $decoder = Encode::find_encoding("UTF-16BE");
        } else {
@@ -450,6 +454,7 @@ sub _normalize {
 # my $data = $_[0];  # avoid copying large strings
   my $charset_declared = $_[1];
   my $return_decoded = $_[2];  # true: Unicode characters, false: UTF-8 octets
+  my $insist_on_declared_charset = $_[3];  # no FB_CROAK in Encode::decode
 
   warn "message: _normalize() was given characters, expected bytes: $_[0]\n"
     if utf8::is_utf8($_[0]);
@@ -457,10 +462,6 @@ sub _normalize {
   # workaround for Encode::decode taint laundering bug [rt.cpan.org #84879]
   my $data_taint = substr($_[0], 0, 0);  # empty string, tainted like $data
 
-  if (!defined $charset_declared || $charset_declared eq '') {
-    $charset_declared = 'us-ascii';
-  }
-
   # number of characters with code above 127
   my $cnt_8bits = $_[0] =~ tr/\x00-\x7F//c;
 
@@ -469,7 +470,8 @@ sub _normalize {
         /^(?: (?:US-)?ASCII | ANSI[_ ]? X3\.4- (?:1986|1968) |
               ISO646-US )\z/xsi)
   { # declared as US-ASCII (a.k.a. ANSI X3.4-1986) and it really is
-    dbg("message: kept, charset is US-ASCII as declared");
+    dbg("message: contains only US-ASCII characters, declared %s, not decoding",
+      $charset_declared);
     return $_[0];  # is all-ASCII, no need for decoding
   }
 
@@ -479,17 +481,21 @@ sub _normalize {
               UTF-?8 | (KOI8|EUC)-[A-Z]{1,2} |
               Big5 | GBK | GB[ -]?18030 (?:-20\d\d)? )\z/xsi)
   { # declared as extended ASCII, but it is actually a plain 7-bit US-ASCII
-    dbg("message: kept, charset is US-ASCII, declared %s", $charset_declared);
+    dbg("message: contains only US-ASCII characters, declared %s, not decoding",
+      $charset_declared);
     return $_[0];  # is all-ASCII, no need for decoding
   }
 
   # Try first to strictly decode based on a declared character set.
 
   my $rv;
-  if ($charset_declared =~ /^UTF-?8\z/i) {
-    # attempt decoding as strict UTF-8  (flags: FB_CROAK | LEAVE_SRC)
+
+  # Try first as UTF-8 ignoring declaring?
+  my $tried_utf8;
+  if ($cnt_8bits && !$insist_on_declared_charset) {
     if (eval { $rv = $enc_utf8->decode($_[0], 1|8); defined $rv }) {
-      dbg("message: decoded as declared charset UTF-8");
+      dbg("message: decoded as charset UTF-8, declared %s",
+        $charset_declared);
       return $_[0]  if !$return_decoded;
       $rv .= $data_taint;  # carry taintedness over, avoid Encode bug
       return $rv;  # decoded
@@ -499,8 +505,16 @@ sub _normalize {
         $err = $@; $err =~ s/\s+/ /gs; $err =~ s/(.*) at .*/$1/;
         $err = " ($err)";
       }
-      dbg("message: failed decoding as declared charset UTF-8 ($err)");
+      dbg("message: failed decoding as charset UTF-8, declared %s%s",
+        $charset_declared, $err);
+      $tried_utf8 = 1;
     }
+  }
+
+  if ($charset_declared =~ /^(?:US-)?ASCII\z/i
+           && !$insist_on_declared_charset) {
+    # declared as US-ASCII but contains 8-bit characters, makes no sense
+    # to attempt decoding first as strict US-ASCII as we know it would fail
 
   } elsif ($charset_declared =~ /^UTF[ -]?16/i) {
     # Handle cases where spammers use UTF-16 encoding without including a BOM
@@ -508,31 +522,23 @@ sub _normalize {
     # https://bz.apache.org/SpamAssassin/show_bug.cgi?id=7252
 
     my $decoder = detect_utf16( $_[0] );
-    if (eval { $rv = $decoder->decode($_[0], 1|8); defined $rv }) {
-      dbg("message: declared charset %s decoded as charset %s", $charset_declared, $decoder->name);
-      return $_[0]  if !$return_decoded;
-      $rv .= $data_taint;  # carry taintedness over, avoid Encode bug
-      return $rv;  # decoded
-    } else {
-      my $err = '';
-      if ($@) {
-        $err = $@; $err =~ s/\s+/ /gs; $err =~ s/(.*) at .*/$1/;
-        $err = " ($err)";
+    if (defined $decoder) {
+      if (eval { $rv = $decoder->decode($_[0], 1|8); defined $rv }) {
+        dbg("message: decoded as charset %s, declared %s",
+          $decoder->name, $charset_declared);
+        return $_[0]  if !$return_decoded;
+        $rv .= $data_taint;  # carry taintedness over, avoid Encode bug
+        return $rv;  # decoded
+      } else {
+        my $err = '';
+        if ($@) {
+          $err = $@; $err =~ s/\s+/ /gs; $err =~ s/(.*) at .*/$1/;
+          $err = " ($err)";
+        }
+        dbg("message: failed decoding as charset %s, declared %s%s",
+          $decoder->name, $charset_declared, $err);
       }
-      dbg("message: failed decoding as declared charset %s%s", $charset_declared, $err);
     };
-
-  } elsif ($cnt_8bits &&
-           eval { $rv = $enc_utf8->decode($_[0], 1|8); defined $rv }) {
-    dbg("message: decoded as charset UTF-8, declared %s", $charset_declared);
-    return $_[0]  if !$return_decoded;
-    $rv .= $data_taint;  # carry taintedness over, avoid Encode bug
-    return $rv;  # decoded
-
-  } elsif ($charset_declared =~ /^(?:US-)?ASCII\z/i) {
-    # declared as US-ASCII but contains 8-bit characters, makes no sense
-    # to attempt decoding first as strict US-ASCII as we know it would fail
-
   } else {
     # try decoding as a declared character set
 
@@ -553,8 +559,11 @@ sub _normalize {
     my($chset, $decoder);
     if ($charset_declared =~ /^(?: ISO-?8859-1 | Windows-1252 | CP1252 )\z/xi) {
       $chset = 'Windows-1252'; $decoder = $enc_w1252;
+    } elsif ($charset_declared =~ /^UTF-?8\z/i) {
+      $chset = 'UTF-8'; $decoder = $enc_utf8;
     } else {
-      $chset = $charset_declared; $decoder = Encode::find_encoding($chset);
+      $chset = $charset_declared;
+      $decoder = Encode::find_encoding($chset);
       if (!$decoder && $chset =~ /^GB[ -]?18030(?:-20\d\d)?\z/i) {
         $decoder = Encode::find_encoding('GBK');  # a subset of GB18030
         dbg("message: no decoder for a declared charset %s, using GBK",
@@ -564,20 +573,24 @@ sub _normalize {
     if (!$decoder) {
       dbg("message: failed decoding, no decoder for a declared charset %s",
           $chset);
-    } else {
+    }
+    elsif ($tried_utf8 && $chset eq 'UTF-8') {
+      # was already tried initially, no point doing again
+    }
+    else {
+      my $check_flags = Encode::LEAVE_SRC;  # 0x0008
+      $check_flags |= Encode::FB_CROAK  unless $insist_on_declared_charset;
       my $err = '';
-      eval { $rv = $decoder->decode($_[0], 1|8) };  # FB_CROAK | LEAVE_SRC
-      if ($@) {
-        $err = $@; $err =~ s/\s+/ /gs; $err =~ s/(.*) at .*/$1/;
-        $err = " ($err)";
-      }
-      if (lc $chset eq lc $charset_declared) {
-        dbg("message: %s as declared charset %s%s",
-            defined $rv ? 'decoded' : 'failed decoding', $charset_declared, $err);
+      if (eval { $rv = $decoder->decode($_[0], $check_flags); defined $rv }) {
+        dbg("message: decoded as charset %s, declared %s",
+          $decoder->name, $charset_declared);
       } else {
-        dbg("message: %s as charset %s, declared %s%s",
-            defined $rv ? 'decoded' : 'failed decoding',
-            $chset, $charset_declared, $err);
+        if ($@) {
+          $err = $@; $err =~ s/\s+/ /gs; $err =~ s/(.*) at .*/$1/;
+          $err = " ($err)";
+        }
+        dbg("message: failed decoding as charset %s, declared %s%s",
+          $decoder->name, $charset_declared, $err);
       }
     }
   }
@@ -589,7 +602,7 @@ sub _normalize {
   # Note that Windows-1252 is a proper superset of ISO-8859-1.
   #
   if (!defined $rv && !$cnt_8bits) {
-    dbg("message: kept, guessed charset is US-ASCII, declared %s",
+    dbg("message: contains only US-ASCII characters, declared %s, not decoding",
         $charset_declared);
     return $_[0];  # is all-ASCII, no need for decoding
 
@@ -675,7 +688,7 @@ sub _normalize {
 
 =item rendered()
 
-render_text() takes the given text/* type MIME part, and attempts to
+rendered() takes the given text/* type MIME part, and attempts to
 render it into a text scalar.  It will always render text/html, and will
 use a heuristic to determine if other text/* parts should be considered
 text/html.  Two scalars are returned: the rendered type (either text/html
@@ -686,73 +699,136 @@ or whatever the original type was), and the rendered text.
 sub rendered {
   my ($self) = @_;
 
-  if (!exists $self->{rendered}) {
-    # We only know how to render text/plain and text/html ...
-    # Note: for bug 4843, make sure to skip text/calendar parts
-    # we also want to skip things like text/x-vcard
-    # text/x-aol is ignored here, but looks like text/html ...
-    return(undef,undef) unless ( $self->{'type'} =~ /^text\/(?:plain|html)$/i );
-
-    my $text = $self->decode;  # QP and Base64 decoding, bytes
-    my $text_len = length($text);  # num of bytes in original charset encoding
-
-    # render text/html always
-    if ($text ne '' && $self->{'type'} =~ m{^text/html$}i)
-    {
-      $self->{rendered_type} = 'text/html';
-
-      # will input text to HTML::Parser be provided as Unicode characters?
-      my $character_semantics = 0;  # $text is in bytes
-      if ($self->{normalize} && $enc_utf8) {  # charset decoding requested
-        # Provide input to HTML::Parser as Unicode characters
-        # which avoids a HTML::Parser bug in utf8_mode
-        #   https://rt.cpan.org/Public/Bug/Display.html?id=99755
-        #   Note: the above bug was fixed in HTML-Parser 3.72, January 2016.
-        # Avoid unnecessary step of encoding-then-decoding by telling
-        # subroutine _normalize() to return Unicode text.  See Bug 7133
-        #
-        $character_semantics = 1;  # $text will be in characters
-        $text = _normalize($text, $self->{charset}, 1); # bytes to chars
-      } elsif (!defined $self->{charset} ||
-               $self->{charset} =~ /^(?:US-ASCII|UTF-8)\z/i) {
-        # With some luck input can be interpreted as UTF-8, do not warn.
-        # It is still possible to hit the HTML::Parses utf8_mode bug however.
-      } else {
-        dbg("message: 'normalize_charset' is off, encoding will likely ".
-            "be misinterpreted; declared charset: %s", $self->{charset});
-      }
-      # the 0 requires decoded HTML results to be in bytes (not characters)
-      my $html = Mail::SpamAssassin::HTML->new($character_semantics,0); # object
-
-      $html->parse($text);  # parse+render text
+  # Cached?
+  if (exists $self->{rendered}) {
+    return ($self->{rendered_type}, $self->{rendered});
+  }
 
-      # resulting HTML-decoded text is in bytes, likely encoded as UTF-8
-      $self->{rendered} = $html->get_rendered_text();
-      $self->{visible_rendered} = $html->get_rendered_text(invisible => 0);
-      $self->{invisible_rendered} = $html->get_rendered_text(invisible => 1);
-      $self->{html_results} = $html->get_results();
+  # We only know how to render text/plain and text/html ...
+  # Note: for bug 4843, make sure to skip text/calendar parts
+  # we also want to skip things like text/x-vcard
+  # text/x-aol is ignored here, but looks like text/html ...
+  my $type = lc $self->{'type'};
+  unless ($type eq 'text/plain' || $type eq 'text/html') {
+    return (undef,undef);
+  }
 
-      # end-of-document result values that require looking at the text
-      my $r = $self->{html_results};   # temporary reference for brevity
+  my $text = $self->decode;  # QP and Base64 decoding, bytes
+  my $text_len = length($text);  # num of bytes in original charset encoding
 
-      # count the number of spaces in the rendered text (likely UTF-8 octets)
-      my $space = $self->{rendered} =~ tr/ \t\n\r\x0b//;
-      # we may want to add the count of other Unicode whitespace characters
+  my $charset = $self->{charset};
+  if (!defined $charset) {
+    dbg("message: no charset declared, using us-ascii");
+    $charset = 'us-ascii';
+  }
 
-      $r->{html_length} = length $self->{rendered};  # bytes (likely UTF-8)
-      $r->{non_space_len} = $r->{html_length} - $space;
-      $r->{ratio} = ($text_len - $r->{html_length}) / $text_len  if $text_len;
+  # render text/html always
+  if ($text ne '' && $type eq 'text/html')
+  {
+    $self->{rendered_type} = 'text/html';
+
+    # will input text to HTML::Parser be provided as Unicode characters?
+    my $character_semantics = 0;  # $text is in bytes
+    if ($self->{normalize} && $enc_utf8) {  # charset decoding requested
+      # Provide input to HTML::Parser as Unicode characters
+      # which avoids a HTML::Parser bug in utf8_mode
+      #   https://rt.cpan.org/Public/Bug/Display.html?id=99755
+      #   Note: the above bug was fixed in HTML-Parser 3.72, January 2016.
+      # Avoid unnecessary step of encoding-then-decoding by telling
+      # subroutine _normalize() to return Unicode text.  See Bug 7133
+      #
+      $character_semantics = 1;  # $text will be in characters
+      $text = _normalize($text, $charset, 1); # bytes to chars
+    } elsif ($charset =~ /^(?:US-ASCII|UTF-8)\z/i) {
+      if ($text !~ tr/\x00-\x7F//c) {
+        # all-ASCII, keep as octets (utf8 flag off)
+        dbg("message: contains only US-ASCII characters, declared %s, not decoding",
+          $charset);
+      } else { # non-ASCII, try UTF-8
+        my $rv;
+        # with some luck input can be interpreted as UTF-8
+        if (eval { $rv = $enc_utf8->decode($text, 1|8); defined $rv }) {
+          $text = $rv;  # decoded to perl characters
+          $character_semantics = 1;  # $text will be in characters
+          dbg("message: decoded as charset UTF-8, declared %s", $charset);
+        } else {
+          my $err = '';
+          if ($@) {
+            $err = $@; $err =~ s/\s+/ /gs; $err =~ s/(.*) at .*/$1/;
+            $err = " ($err)";
+          }
+          dbg("message: failed decoding as charset UTF-8, declared %s%s",
+            $charset, $err);
+        }
+      }
+    } else {
+      dbg("message: 'normalize_charset' is off, encoding will likely ".
+          "be misinterpreted; declared charset: %s", $charset);
+    }
+    # the 1 requires decoded HTML results to be in characters (utf8 flag on)
+    my $html = Mail::SpamAssassin::HTML->new($character_semantics,1); # object
+
+    $html->parse($text);  # parse+render text
+
+    # resulting HTML-decoded text is in perl characters (utf8 flag on)
+    $self->{rendered} = $html->get_rendered_text();
+    $self->{visible_rendered} = $html->get_rendered_text(invisible => 0);
+    $self->{invisible_rendered} = $html->get_rendered_text(invisible => 1);
+    $self->{html_results} = $html->get_results();
+
+    # end-of-document result values that require looking at the text
+    my $r = $self->{html_results};     # temporary reference for brevity
+
+    # count the number of spaces in the rendered text
+    my $space;
+    if (utf8::is_utf8($self->{rendered})) {
+      my $str = $self->{rendered};
+      $str =~ s/\S+//g;  # delete non-whitespace Unicode characters
+      $space = length $str;  # count remaining Unicode space characters
+      undef $str;  # deallocate storage
+      dbg("message: spaces (Unicode) in HTML: %d out of %d%s",
+          $space, length $self->{rendered},
+          $character_semantics ? '' : ', octets!?');
+    } else {
+      $space = $self->{rendered} =~ tr/ \t\n\r\x0b//;
+      dbg("message: spaces (octets) in HTML: %d out of %d%s",
+          $space, length $self->{rendered},
+          $character_semantics ? ', chars!?' : '');
     }
+    # we may want to add the count of other Unicode whitespace characters
 
-    else {  # plain text
-      if ($self->{normalize} && $enc_utf8) {
-        # request transcoded result as UTF-8 octets!
-        $text = _normalize($text, $self->{charset}, 0);
+    $r->{html_length} = length $self->{rendered};  # perl characters count
+    $r->{non_space_len} = $r->{html_length} - $space;
+    $r->{ratio} = ($text_len - $r->{html_length}) / $text_len  if $text_len;
+  }
+  else {  # plain text
+    if ($self->{normalize} && $enc_utf8) {
+      # request transcoded result as UTF-8 octets!
+      $text = _normalize($text, $charset, 1); # bytes to chars
+    } elsif ($charset =~ /^(?:US-ASCII|UTF-8)\z/i) {
+      if ($text =~ tr/\x00-\x7F//c) {  # non-ASCII, try UTF-8
+        my $rv;
+        # with some luck input can be interpreted as UTF-8
+        if (eval { $rv = $enc_utf8->decode($text, 1|8); defined $rv }) {
+          $text = $rv;  # decoded to perl characters
+          dbg("message: decoded as charset UTF-8, declared %s", $charset);
+        } else {
+          my $err = '';
+          if ($@) {
+            $err = $@; $err =~ s/\s+/ /gs; $err =~ s/(.*) at .*/$1/;
+            $err = " ($err)";
+          }
+          dbg("message: failed decoding as charset UTF-8, declared %s%s",
+            $charset, $err);
+        }
+      } else {
+        dbg("message: contains only US-ASCII characters, declared %s, not decoding",
+          $charset);
       }
-      $self->{rendered_type} = $self->{type};
-      $self->{rendered} = $self->{'visible_rendered'} = $text;
-      $self->{'invisible_rendered'} = '';
     }
+    $self->{rendered_type} = $type;
+    $self->{rendered} = $self->{visible_rendered} = $text;
+    $self->{invisible_rendered} = '';
   }
 
   return ($self->{rendered_type}, $self->{rendered});
@@ -841,24 +917,24 @@ Delete the specified header (decoded and raw) from the Node information.
 sub delete_header {
   my($self, $hdr) = @_;
 
-  foreach ( grep(/^${hdr}$/i, keys %{$self->{'headers'}}) ) {
+  foreach ( grep(/^${hdr}$/io, keys %{$self->{'headers'}}) ) {
     delete $self->{'headers'}->{$_};
     delete $self->{'raw_headers'}->{$_};
   }
   
-  my @neworder = grep(!/^${hdr}$/i, @{$self->{'header_order'}});
+  my @neworder = grep(!/^${hdr}$/io, @{$self->{'header_order'}});
   $self->{'header_order'} = \@neworder;
 }
 
-# decode a header appropriately.  don't bother adding it to the pod documents.
-sub __decode_header {
+# decode 'encoded-word' (RFC 2047, RFC 2231)
+sub _decode_mime_encoded_word {
   my ( $encoding, $cte, $data ) = @_;
 
-  if ( $cte eq 'B' ) {
+  if ( uc $cte eq 'B' ) {
     # base 64 encoded
     $data = Mail::SpamAssassin::Util::base64_decode($data);
   }
-  elsif ( $cte eq 'Q' ) {
+  elsif ( uc $cte eq 'Q' ) {
     # quoted printable
 
     # the RFC states that in the encoded text, "_" is equal to "=20"
@@ -868,12 +944,24 @@ sub __decode_header {
   }
   else {
     # not possible since the input has already been limited to 'B' and 'Q'
-    die "message: unknown encoding type '$cte' in RFC2047 header";
+    die "message: unknown encoding type '$cte' in RFC 2047 header";
+  }
+
+  if (defined $encoding) {
+    # RFC 2231 section 5: Language specification in Encoded Words
+    #   =?US-ASCII*EN?Q?Keith_Moore?=
+    # strip optional language information following an asterisk
+    $encoding =~ s{ \* .* \z }{}xs;
+
+    $data = _normalize($data, $encoding, 0, 1);  # transcode to UTF-8 octets
   }
-  return _normalize($data, $encoding, 0);  # transcode to UTF-8 octets
+  # dbg("message: _decode_mime_encoded_word (%s, %s): %s",
+  #     $cte, $encoding || '-', $data);
+
+  return $data;  # as UTF-8 octets
 }
 
-# Decode base64 and quoted-printable in headers according to RFC2047.
+# Decode base64 and quoted-printable in headers according to RFC 2047.
 #
 sub _decode_header {
   my($header_field_body, $header_field_name) = @_;
@@ -881,35 +969,86 @@ sub _decode_header {
   return '' unless defined $header_field_body && $header_field_body ne '';
 
   # deal with folding and cream the newlines and such
-  $header_field_body =~ s/\n[ \t]+/\n /g;
+  $header_field_body =~ s/\n[ \t]/\n /g;  # turning tab into space on folds
   $header_field_body =~ s/\015?\012//gs;
 
+  if ($header_field_body =~ tr/\x00-\x7F//c) {
+    # Non-ASCII characters in header are not allowed by RFC 5322, but
+    # RFC 6532 relaxed the rule and allows UTF-8 encoding in header
+    # field bodies; no other encoding is allowed there (apart from
+    # RFC 2047 MIME encoded words, which must be all-ASCII anyway).
+    # The following call keeps UTF-8 octets if valid, otherwise tries
+    # some decoding guesswork so that the result is valid UTF-8 (octets).
+    $header_field_body = _normalize($header_field_body, 'UTF-8', 0);
+  }
+
   if ($header_field_name =~
        /^ (?: Received | (?:Resent-)? (?: Message-ID | Date ) |
               MIME-Version | References | In-Reply-To | List-.* ) \z /xsi ) {
     # Bug 6945: some header fields must not be processed for MIME encoding
-    # Bug 7466: leave out the Content-*
+    # Bug 7249: leave out the Content-*
 
-  } else {
-    local($1,$2,$3);
+  } elsif (index($header_field_body, '=?') != -1) {  # triage for possible encoded-words
+    local($1,$2,$3,$4);
 
     # Multiple encoded sections must ignore the interim whitespace.
     # To avoid possible FPs with (\s+(?==\?))?, look for the whole RE
     # separated by whitespace.
-    1 while $header_field_body =~
-              s{ ( = \? [A-Za-z0-9_-]+ \? [bqBQ] \? [^?]* \? = ) \s+
-                 ( = \? [A-Za-z0-9_-]+ \? [bqBQ] \? [^?]* \? = ) }
-               {$1$2}xsg;
-
-    # transcode properly encoded RFC 2047 substrings into UTF-8 octets,
-    # leave everything else unchanged as it is supposed to be UTF-8 (RFC 6532)
-    # or plain US-ASCII
     $header_field_body =~
-      s{ (?: = \? ([A-Za-z0-9_-]+) \? ([bqBQ]) \? ([^?]*) \? = ) }
-       { __decode_header($1, uc($2), $3) }xsge;
+      s{ (   = \? [A-Za-z0-9*_-]+ \? [bqBQ] \? [^?]* \? = ) \s+
+         (?= = \? [A-Za-z0-9*_-]+ \? [bqBQ] \? [^?]* \? = ) }{$1}xsg;
+
+    # Bug 7249: work around violations of the RFC 2047 section 5 requirement:
+    #   Each 'encoded-word' MUST represent an integral number of characters.
+    #   A multi-octet character may not be split across adjacent 'encoded-word's
+    # Unfortunately such violations are not uncommon.
+    #
+    # Bug 7307: to deal with the above, base64/QP decoding must be decoupled
+    # from decoding a specified multi-byte character set into UTF-8.
+    # A previous simpler code could not handle base64 fill bits correctly
+    # (merging of adjecent encoded sections before base64/QP decoding them).
+
+    my @sections;  # array of pairs: [string, encoding]
+    my $last_encoding = '';
+    while ( $header_field_body =~
+              m{ \G = \? ([A-Za-z0-9*_-]+) \? ([bqBQ]) \? ([^?]*) \? =
+                  | ( [^=]+ | . ) }xsg ) {
+      my($encoding, $str);
+      if (defined $1) {  # we have an encoded section
+        $encoding = lc $1;
+        # decode base64 / QP decoding, remember encoding charset
+        $str = _decode_mime_encoded_word(undef, $2, $3);
+      } else {  # non-encoded text
+        $encoding = '';
+        $str = $4;
+      }
+      if ($encoding eq $last_encoding && @sections) {
+        # merge sections with same encoding - in violation of RFC 2047 sect.5
+        $sections[$#sections]->[0] .= $str;
+      } else {
+        push(@sections, [$str, $encoding]);
+      }
+      $last_encoding = $encoding;
+    }
+
+    # transcode encoded RFC 2047 substrings (already base64/QP-decoded)
+    # into UTF-8 octets, leave everything else unchanged as it is supposed
+    # to be UTF-8 (RFC 6532) or its plain US-ASCII subset (RFC 5322);
+    #
+    my $decoded_result = '';
+    for my $sect (@sections) {
+      my $encoding = $sect->[1];
+      # RFC 2231 section 5: Language specification in Encoded Words
+      #   =?US-ASCII*EN?Q?Keith_Moore?=
+      # strip optional language information following an asterisk
+      $encoding =~ s{ \* .* \z }{}xs;
+      $decoded_result .=
+        $encoding eq '' ? $sect->[0] : _normalize($sect->[0], $encoding, 0, 1);
+    }
+    $header_field_body = $decoded_result;
   }
 
-# dbg("message: _decode_header %s: %s", $header_field_name, $header_field_body);
+  dbg("message: _decode_header %s: %s", $header_field_name, $header_field_body);
   return $header_field_body;
 }
 
index 40cccb734be80f52a63b70257d55fa35410174b5..e7c4b2637d33d478775e9dcaf6536fb193da6646 100644 (file)
@@ -33,7 +33,7 @@ BEGIN {
   eval {
     require Net::Patricia;
     Net::Patricia->VERSION(1.16);  # need AF_INET6 support
-    import Net::Patricia;
+    Net::Patricia->import;
     $have_patricia = 1;
   };
 }
@@ -59,6 +59,7 @@ sub new {
 
 sub DESTROY {
   my($self) = shift;
+
   if (exists $self->{cache}) {
     local($@, $!, $_);  # protect outer layers from a potential surprise
     my($hits, $attempts) = ($self->{cache_hits}, $self->{cache_attempts});
@@ -76,7 +77,45 @@ sub add_cidr {
   my $numadded = 0;
   delete $self->{cache};  # invalidate cache (in case of late additions)
 
+  # Pre-parse x.x.x.x-x.x.x.x range notation into CIDR blocks
+  # requires Net::CIDR::Lite
+  my @nets2;
   foreach my $cidr_orig (@nets) {
+    next if index($cidr_orig, '-') == -1; # Triage
+    my $cidr = $cidr_orig;
+    my $exclude = ($cidr =~ s/^!\s*//) ? 1 : 0;
+    local($1);
+    $cidr =~ s/\b0+(\d+)/$1/; # Strip leading zeroes
+    eval { require Net::CIDR::Lite; }; # Only try to load now when it's necessary
+    if ($@) {
+      warn "netset: IP range notation '$cidr_orig' requires Net::CIDR::Lite module, ignoring\n";
+      $cidr_orig = undef;
+      next;
+    }
+    my $cidrs = Net::CIDR::Lite->new;
+    eval { $cidrs->add_range($cidr); };
+    if ($@) {
+      my $err = $@; $err =~ s/ at .*//s;
+      warn "netset: illegal IP range '$cidr_orig': $err\n";
+      $cidr_orig = undef;
+      next;
+    }
+    my @arr = $cidrs->list;
+    if (!@arr) {
+      my $err = $@; $err =~ s/ at .*//s;
+      warn "netset: failed to parse IP range '$cidr_orig': $err\n";
+      $cidr_orig = undef;
+      next;
+    }
+    # Save exclude flag
+    if ($exclude) { $_ = "!$_" foreach (@arr); }
+    # Rewrite this @nets value directly, add any rest to @nets2
+    $cidr_orig = shift @arr;
+    push @nets2, @arr  if @arr;
+  }
+
+  foreach my $cidr_orig (@nets, @nets2) {
+    next unless defined $cidr_orig;
     my $cidr = $cidr_orig;  # leave original unchanged, useful for logging
 
     # recognizes syntax:
index e7211c97bd276f8e657de94f94f5d39eb6467b05..7c3b7924b078b79bbc806f2e41a507fec34c1754 100644 (file)
@@ -21,7 +21,7 @@ Mail::SpamAssassin::PerMsgLearner - per-message status (spam or not-spam)
 
 =head1 SYNOPSIS
 
-  my $spamtest = new Mail::SpamAssassin ({
+  my $spamtest = Mail::SpamAssassin->new({
     'rules_filename'      => '/etc/spamassassin.rules',
     'userprefs_filename'  => $ENV{HOME}.'/.spamassassin/user_prefs'
   });
@@ -97,8 +97,8 @@ sub learn_spam {
   my ($self, $id) = @_;
 
   # bug 4096
-  # if ($self->{main}->{learn_with_whitelist}) {
-  # $self->{main}->add_all_addresses_to_blacklist ($self->{msg});
+  # if ($self->{main}->{learn_with_welcomelist}) {
+  # $self->{main}->add_all_addresses_to_blocklist ($self->{msg});
   # }
 
   # use the real message-id here instead of mass-check's idea of an "id",
@@ -124,8 +124,8 @@ sub learn_ham {
   my ($self, $id) = @_;
 
   # bug 4096
-  # if ($self->{main}->{learn_with_whitelist}) {
-  # $self->{main}->add_all_addresses_to_whitelist ($self->{msg});
+  # if ($self->{main}->{learn_with_welcomelist}) {
+  # $self->{main}->add_all_addresses_to_welcomelist ($self->{msg});
   # }
 
   $self->{learned} = $self->{bayes_scanner}->learn (0, $self->{msg}, $id);
@@ -148,8 +148,8 @@ sub forget {
   my ($self, $id) = @_;
 
   # bug 4096
-  # if ($self->{main}->{learn_with_whitelist}) {
-  # $self->{main}->remove_all_addresses_from_whitelist ($self->{msg});
+  # if ($self->{main}->{learn_with_welcomelist}) {
+  # $self->{main}->remove_all_addresses_from_welcomelist ($self->{msg});
   # }
 
   $self->{learned} = $self->{bayes_scanner}->forget ($self->{msg}, $id);
index 00003326987498556677715c4872419eab2b0d45..3254690c5d7625addf25da911a1b4fbc0991d10c 100644 (file)
@@ -21,7 +21,7 @@ Mail::SpamAssassin::PerMsgStatus - per-message status (spam or not-spam)
 
 =head1 SYNOPSIS
 
-  my $spamtest = new Mail::SpamAssassin ({
+  my $spamtest = Mail::SpamAssassin->new({
     'rules_filename'      => '/etc/spamassassin.rules',
     'userprefs_filename'  => $ENV{HOME}.'/.spamassassin/user_prefs'
   });
@@ -55,20 +55,23 @@ use re 'taint';
 
 use Errno qw(ENOENT);
 use Time::HiRes qw(time);
+use Encode;
 
-use Mail::SpamAssassin::Constants qw(:sa);
+use Mail::SpamAssassin::Constants qw(:sa :ip);
 use Mail::SpamAssassin::AsyncLoop;
 use Mail::SpamAssassin::Conf;
-use Mail::SpamAssassin::Util qw(untaint_var uri_list_canonicalize is_fqdn_valid);
+use Mail::SpamAssassin::Util qw(untaint_var base64_encode idn_to_ascii
+                                uri_list_canonicalize reverse_ip_address
+                                is_fqdn_valid parse_header_addresses);
 use Mail::SpamAssassin::Timeout;
 use Mail::SpamAssassin::Logger;
 
 our @ISA = qw();
 
-# methods defined by the compiled ruleset; deleted in finish_tests()
+# methods defined by the compiled ruleset; deleted in finish()
 our @TEMPORARY_METHODS;
 
-# methods defined by register_plugin_eval_glue(); deleted in finish_tests()
+# methods defined by register_plugin_eval_glue(); deleted in finish()
 our %TEMPORARY_EVAL_GLUE_METHODS;
 
 ###########################################################################
@@ -133,12 +136,30 @@ BEGIN {
       $pms->{tag_data}->{'REMOTEHOSTADDR'} || "127.0.0.1";
     },
 
+    FIRSTTRUSTEDIP => sub {
+      my $pms = shift;
+      my $lasthop = $pms->{msg}->{metadata}->{relays_trusted}->[-1];
+      $lasthop ? $lasthop->{ip} : '';
+    },
+
+    FIRSTTRUSTEDREVIP => sub {
+      my $pms = shift;
+      my $lasthop = $pms->{msg}->{metadata}->{relays_trusted}->[-1];
+      $lasthop ? reverse_ip_address($lasthop->{ip}) : '';
+    },
+
     LASTEXTERNALIP => sub {
       my $pms = shift;
       my $lasthop = $pms->{msg}->{metadata}->{relays_external}->[0];
       $lasthop ? $lasthop->{ip} : '';
     },
 
+    LASTEXTERNALREVIP => sub {
+      my $pms = shift;
+      my $lasthop = $pms->{msg}->{metadata}->{relays_external}->[0];
+      $lasthop ? reverse_ip_address($lasthop->{ip}) : '';
+    },
+
     LASTEXTERNALRDNS => sub {
       my $pms = shift;
       my $lasthop = $pms->{msg}->{metadata}->{relays_external}->[0];
@@ -228,8 +249,8 @@ BEGIN {
     HEADER => sub {
       my $pms = shift;
       my $hdr = shift;
-      return if !$hdr;
-      $pms->get($hdr,undef);
+      return '' if !$hdr;
+      $pms->get($hdr, '');
     },
 
     TIMING => sub {
@@ -256,6 +277,8 @@ BEGIN {
   );
 }
 
+my $IP_ADDRESS = IP_ADDRESS;
+
 sub new {
   my $class = shift;
   $class = ref($class) || $class;
@@ -265,12 +288,13 @@ sub new {
     'main'              => $main,
     'msg'               => $msg,
     'score'             => 0,
-    'test_log_msgs'     => { },
+    'test_log_msgs'     => { }, # deprecated since 4.0, renamed to test_logs to prevent conflicts
+    'test_logs'         => { },
     'test_names_hit'    => [ ],
     'subtest_names_hit' => [ ],
     'spamd_result_log_items' => [ ],
     'tests_already_hit' => { },
-    'c'                 => { },
+    'get_cache'         => { },
     'tag_data'          => { },
     'rule_errors'       => 0,
     'disable_auto_learning' => 0,
@@ -280,8 +304,9 @@ sub new {
     'async'             => Mail::SpamAssassin::AsyncLoop->new($main),
     'master_deadline'   => $msg->{master_deadline},  # dflt inherited from msg
     'deadline_exceeded' => 0,  # time limit exceeded, skipping further tests
+    'tmpfiles'          => { },
     'uri_detail_list'   => { },
-    'subjprefix'        => "",
+    'subjprefix'        => undef,
   };
 
   dbg("check: pms new, time limit in %.3f s",
@@ -320,8 +345,14 @@ sub new {
 
 sub DESTROY {
   my ($self) = shift;
-  local $@;
-  eval { $self->delete_fulltext_tmpfile() };  # Bug 5808
+
+  # best practices: prevent potential calls to eval and to system routines
+  # in code of a DESTROY method from clobbering global variables $@ and $! 
+  local($@,$!);  # keep outer error handling unaffected by DESTROY
+  # Bug 5808 - cleanup tmpfiles
+  foreach my $fn (keys %{$self->{tmpfiles}}) {
+    unlink($fn) or dbg("check: cannot unlink $fn: $!");
+  }
 }
 
 ###########################################################################
@@ -356,6 +387,9 @@ sub check_timed {
   $self->{head_only_points} = 0;
   $self->{score} = 0;
 
+  # flush any old stale DNS responses
+  $self->{main}->{resolver}->flush_responses();
+
   # clear NetSet cache before every check to prevent it growing too large
   foreach my $nset_name (qw(internal_networks trusted_networks msa_networks)) {
     my $netset = $self->{conf}->{$nset_name};
@@ -369,7 +403,7 @@ sub check_timed {
   # this lets us see ludicrously spammish mails (score: 40) etc., which
   # we can then immediately submit to spamblocking services.
   #
-  # TODO: change this to do whitelist/blacklists first? probably a plan
+  # TODO: change this to do welcomelist/blocklists first? probably a plan
   # NOTE: definitely need AWL stuff last, for regression-to-mean of score
 
   # TVD: we may want to do more than just clearing out the headers, but ...
@@ -402,7 +436,7 @@ sub check_timed {
 
   # now that we've finished checking the mail, clear out this cache
   # to avoid unforeseen side-effects.
-  $self->{c} = { };
+  $self->{get_cache} = { };
 
   # Round the score to 3 decimal places to avoid rounding issues
   # We assume required_score to be properly rounded already.
@@ -421,6 +455,75 @@ sub check_timed {
   1;
 }
 
+# Called from Check.pm after Plugins check_cleanup calls
+# Cleanup and finish things before learning/rewrites etc
+# TODO: document?
+sub check_cleanup {
+  my ($self) = shift;
+
+  # Create subjprefix
+  if (defined $self->{subjprefix}) {
+    $self->{tag_data}->{SUBJPREFIX} = $self->{subjprefix};
+  }
+
+  # Create reports
+  $self->{tag_data}->{REPORT} = '';
+  $self->{tag_data}->{SUMMARY} = '';
+  my $test_logs = $self->{test_logs};
+  my $scores = $self->{conf}->{scores};
+  foreach my $rule (@{$self->{test_names_hit}}) {
+    my $score = $scores->{$rule};
+    my $area = $test_logs->{$rule}->{area} || '';
+    my $desc = $test_logs->{$rule}->{desc} || '';
+
+    if ($score >= 10 || $score <= -10) {
+      $score = sprintf("%4.0f", $score);
+    } else {
+      $score = sprintf("%4.1f", $score);
+    }
+
+    my $terse = '';
+    my $long = '';
+    if (defined $test_logs->{$rule}->{msg}) {
+      my @msgs;
+      if (($self->{conf}->{tflags}->{$rule}||'') =~ /\bnolog\b/) {
+        push(@msgs, '*REDACTED*');
+      } else {
+        @msgs = @{$test_logs->{$rule}->{msg}};
+      }
+      local $1;
+      foreach my $msg (@msgs) {
+        while ($msg =~ s/^(.{30,48})\s//) {
+          $terse .= sprintf ("[%s]\n", $1);
+          if (length($1) > 47) {
+            $long .= sprintf ("%78s\n", "[$1]");
+          } else {
+            $long .= sprintf ("%27s [%s]\n", "", $1);
+          }
+        }
+        $terse .= sprintf ("[%s]\n", $msg);
+        if (length($msg) > 47) {
+          $long .= sprintf ("%78s\n", "[$msg]");
+        } else {
+          $long .= sprintf ("%27s [%s]\n", "", $msg);
+        }
+      }
+    }
+
+    $self->{tag_data}->{REPORT} .= sprintf ("* %s %s %s%s\n%s",
+        $score, $rule, $area,
+        $self->_wrap_desc($desc,
+            4+length($rule)+length($score)+length($area), "*      "),
+        ($terse ? "*      " . $terse : ''));
+
+    $self->{tag_data}->{SUMMARY} .= sprintf ("%s %-22s %s%s\n%s",
+        $score, $rule, $area,
+        $self->_wrap_desc($desc,
+            3+length($rule)+length($score)+length($area), " " x 28),
+        $long);
+  }
+}
+
 ###########################################################################
 
 =item $status->learn()
@@ -528,7 +631,7 @@ ignored:
 
   - rules with tflags set to 'learn' (the Bayesian rules)
 
-  - rules with tflags set to 'userconf' (user white/black-listing rules, etc)
+  - rules with tflags set to 'userconf' (user welcome/block-listing rules, etc)
 
   - rules with tflags set to 'noautolearn'
 
@@ -620,6 +723,21 @@ sub get_autolearn_force_names {
   return $names;
 }
 
+sub _get_autolearn_testtype {
+  my ($self, $test) = @_;
+  return '' unless defined $test;
+  return 'head' if $test == $Mail::SpamAssassin::Conf::TYPE_HEAD_TESTS
+                || $test == $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS;
+  return 'body' if $test == $Mail::SpamAssassin::Conf::TYPE_BODY_TESTS
+                || $test == $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS
+                || $test == $Mail::SpamAssassin::Conf::TYPE_RAWBODY_TESTS
+                || $test == $Mail::SpamAssassin::Conf::TYPE_RAWBODY_EVALS
+                || $test == $Mail::SpamAssassin::Conf::TYPE_URI_TESTS
+                || $test == $Mail::SpamAssassin::Conf::TYPE_URI_EVALS;
+  return 'meta' if $test == $Mail::SpamAssassin::Conf::TYPE_META_TESTS;
+  return '';
+}
+
 sub _get_autolearn_points {
   my ($self) = @_;
 
@@ -627,11 +745,13 @@ sub _get_autolearn_points {
   # ensure it only gets computed once, even if we return early
   $self->{autolearn_points} = 0;
 
+  my $conf = $self->{conf};
+
   # This function needs to use use sum($score[scoreset % 2]) not just {score}.
   # otherwise we shift what we autolearn on and it gets really weird.  - tvd
-  my $orig_scoreset = $self->{conf}->get_score_set();
+  my $orig_scoreset = $conf->get_score_set();
   my $new_scoreset = $orig_scoreset;
-  my $scores = $self->{conf}->{scores};
+  my $scores = $conf->{scores};
 
   if (($orig_scoreset & 2) == 0) { # we don't need to recompute
     dbg("learn: auto-learn: currently using scoreset $orig_scoreset");
@@ -639,10 +759,10 @@ sub _get_autolearn_points {
   else {
     $new_scoreset = $orig_scoreset & ~2;
     dbg("learn: auto-learn: currently using scoreset $orig_scoreset, recomputing score based on scoreset $new_scoreset");
-    $scores = $self->{conf}->{scoreset}->[$new_scoreset];
+    $scores = $conf->{scoreset}->[$new_scoreset];
   }
 
-  my $tflags = $self->{conf}->{tflags};
+  my $tflags = $conf->{tflags};
   my $points = 0;
 
   # Just in case this function is called multiple times, clear out the
@@ -653,6 +773,7 @@ sub _get_autolearn_points {
   $self->{autolearn_force} = 0;
 
   foreach my $test (@{$self->{test_names_hit}}) {
+    my $force_type = '';
     # According to the documentation, noautolearn, userconf, and learn
     # rules are ignored for autolearning.
     if (exists $tflags->{$test}) {
@@ -663,7 +784,7 @@ sub _get_autolearn_points {
       # Use the original scoreset since it'll be 0 in sets 0 and 1.
       if ($tflags->{$test} =~ /\blearn\b/) {
        # we're guaranteed that the score will be defined
-        $self->{learned_points} += $self->{conf}->{scoreset}->[$orig_scoreset]->{$test};
+        $self->{learned_points} += $conf->{scoreset}->[$orig_scoreset]->{$test};
        next;
       }
 
@@ -673,6 +794,12 @@ sub _get_autolearn_points {
         #ADD RULE NAME TO LIST
         $self->{autolearn_force_names}.="$test,";
       }
+
+      # Bug 7907
+      local $1;
+      if ($tflags->{$test} =~ /\bautolearn_(body|header)\b/) {
+        $force_type = $1;
+      }
     }
 
     # ignore tests with 0 score (or undefined) in this scoreset
@@ -680,14 +807,41 @@ sub _get_autolearn_points {
 
     # Go ahead and add points to the proper locations
     # Changed logic because in testing, I was getting both head and body. Bug 5503
-    if ($self->{conf}->maybe_header_only ($test)) {
+    # Cleanup logic, Bug 7905/7906
+    my $type = $self->_get_autolearn_testtype($conf->{test_types}->{$test});
+    if ($force_type eq 'header' || ($force_type eq '' && $type eq 'head')) {
       $self->{head_only_points} += $scores->{$test};
-      dbg("learn: auto-learn: adding head_only points $scores->{$test}");
-    } elsif ($self->{conf}->maybe_body_only ($test)) {
+      dbg("learn: auto-learn: adding header points $scores->{$test} ($test)");
+    }
+    elsif ($force_type eq 'body' || ($force_type eq '' && $type eq 'body')) {
       $self->{body_only_points} += $scores->{$test};
-      dbg("learn: auto-learn: adding body_only points $scores->{$test}");
-    } else {
-      dbg("learn: auto-learn: not considered head or body scores: $scores->{$test}");
+      dbg("learn: auto-learn: adding body points $scores->{$test} ($test)");
+    }
+    elsif ($type eq 'meta') {
+      if ($conf->{meta_dependencies}->{$test}) {
+        my $dep_head = 0;
+        my $dep_body = 0;
+        foreach my $deptest (@{$conf->{meta_dependencies}->{$test}}) {
+          my $deptype = $self->_get_autolearn_testtype($conf->{test_types}->{$deptest});
+          if ($deptype eq 'head') { $dep_head++; }
+          elsif ($deptype eq 'body') { $dep_body++; }
+        }
+        if ($dep_head || $dep_body) {
+          my $dep_total = $dep_head + $dep_body;
+          my $p_head = sprintf "%0.3f", $scores->{$test} * ($dep_head / $dep_total);
+          my $p_body = sprintf "%0.3f", $scores->{$test} * ($dep_body / $dep_total);
+          $self->{head_only_points} += $p_head;
+          $self->{body_only_points} += $p_body;
+          dbg("learn: auto-learn: adding $p_head header and $p_body body points, $dep_head/$dep_body ratio ($test)");
+        } else {
+          dbg("learn: auto-learn: not considered as header or body points, no header/body deps ($test)");
+        }
+      } else {
+          dbg("learn: auto-learn: not considered as header or body points, no meta deps ($test)");
+      }
+    }
+    else {
+      dbg("learn: auto-learn: not considered as header or body points, ignored ruletype ($test)");
     }
 
     $points += $scores->{$test};
@@ -1050,7 +1204,7 @@ sub _get_added_headers {
   foreach my $hf_ref (@{$self->{conf}->{$which}}) {
     my($hfname, $hfbody) = @$hf_ref;
     my $line = $self->_process_header($hfname,$hfbody);
-    $line = $self->qp_encode_header($line);
+    $line = $self->mime_encode_header($line);
     $str .= "X-Spam-$hfname: $line\n";
   }
   return $str;
@@ -1072,21 +1226,23 @@ sub rewrite_report_safe {
   # This is the new message.
   my $newmsg = '';
 
-  # the report charset
-  my $report_charset = "; charset=iso-8859-1";
-  if ($self->{conf}->{report_charset}) {
-    $report_charset = "; charset=" . $self->{conf}->{report_charset};
-  }
+  # the character set of a report
+  my $report_charset = $self->{conf}->{report_charset} || "UTF-8";
 
   # the SpamAssassin report
   my $report = $self->get_report();
 
-  # If there are any wide characters, need to MIME-encode in UTF-8
-  # TODO: If $report_charset is something other than iso-8859-1/us-ascii, then
-  # we could try converting to that charset if possible
-  unless ($] < 5.008 || utf8::downgrade($report, 1)) {
-      $report_charset = "; charset=utf-8";
-      utf8::encode($report);
+  if (!utf8::is_utf8($report)) {
+    # already in octets
+  } else {
+    # encode to octets
+    if (uc $report_charset eq 'UTF-8') {
+      dbg("check: encoding report to $report_charset");
+      utf8::encode($report);  # very fast
+    } else {
+      dbg("check: encoding report to $report_charset. Slow, to be avoided!");
+      $report = Encode::encode($report_charset, $report);  # slow
+    }
   }
 
   # get original headers, "pristine" if we can do it
@@ -1103,7 +1259,7 @@ sub rewrite_report_safe {
   if (defined $self->{conf}->{rewrite_header}->{Subject}) {
     # Add a prefix to the subject if needed
     $subject = "\n" if !defined $subject;
-    if((defined $self->{subjprefix}) and ($self->{subjprefix} ne "")) {
+    if (defined $self->{subjprefix}) {
       $tag = $self->_replace_tags($self->{subjprefix});
       $tag =~ s/\n/ /gs;
       $subject = $tag . $subject;
@@ -1206,7 +1362,7 @@ Content-Type: multipart/mixed; boundary="$boundary"
 This is a multi-part message in MIME format.
 
 --$boundary
-Content-Type: text/plain$report_charset
+Content-Type: text/plain; charset=$report_charset
 Content-Disposition: inline
 Content-Transfer-Encoding: 8bit
 
@@ -1291,19 +1447,14 @@ sub rewrite_no_report_safe {
        # The tag should be a comment for this header ...
        $tag = "($tag)" if ($hdr =~ /^(?:From|To)$/);
 
-        if((defined $self->{subjprefix}) and (defined $self->{conf}->{rewrite_header}->{Subject})) {
-         if($self->{subjprefix} ne "") {
-            $ntag = $self->_replace_tags($self->{subjprefix});
-            $ntag =~ s/\n/ /gs;
-           $ntag =~ s/\s+$//;
-
-            local $1;
-           if(defined $ntag) {
-              s/^([^:]+:)[ \t]*(?:\Q${ntag}\E )?/$1 ${ntag} /i;
-           }
-         }
-        }
-        s/^([^:]+:)[ \t]*(?:\Q${tag}\E )?/$1 ${tag} /i;
+       if (defined $self->{subjprefix}) {
+         $ntag = $self->_replace_tags($self->{subjprefix});
+         $ntag =~ s/\n/ /gs;
+         $ntag =~ s/\s+$//;
+         local $1;
+         s/^([^:]+:)[ \t]*(?:\Q${ntag}\E )?/$1 ${ntag} /i;
+       }
+       s/^([^:]+:)[ \t]*(?:\Q${tag}\E )?/$1 ${tag} /i;
       }
 
       $addition = 'headers_spam';
@@ -1326,20 +1477,14 @@ sub rewrite_no_report_safe {
        my $hdr = ucfirst(lc($1));
        next if (!defined $self->{conf}->{rewrite_header}->{$hdr});
 
-        if((defined $self->{subjprefix}) and (defined $self->{conf}->{rewrite_header}->{Subject})) {
-         if($self->{subjprefix} ne "") {
-            $ntag = $self->_replace_tags($self->{subjprefix});
-            $ntag =~ s/\n/ /gs;
-           $ntag =~ s/\s+$//;
-
-            local $1;
-           if(defined $ntag) {
-              s/^([^:]+:)[ \t]*(?:\Q${ntag}\E )?/$1 ${ntag} /i;
-           }
-         }
+        if (defined $self->{subjprefix}) {
+          $ntag = $self->_replace_tags($self->{subjprefix});
+          $ntag =~ s/\n/ /gs;
+          $ntag =~ s/\s+$//;
+          local $1;
+          s/^([^:]+:)[ \t]*(?:\Q${ntag}\E )?/$1 ${ntag} /i;
         }
       }
-
   }
 
   # Break the pristine header set into two blocks; $new_hdrs_pre is the stuff
@@ -1368,35 +1513,59 @@ sub rewrite_no_report_safe {
   return $newmsg.$self->{msg}->get_pristine_body();
 }
 
-sub qp_encode_header {
+# encode a header field body into ASCII as per RFC 2047
+#
+sub mime_encode_header {
   my ($self, $text) = @_;
 
-  # return unchanged if there are no 8-bit characters
-  return $text  if $text !~ tr/\x00-\x7F//c;
+  utf8::encode($text)  if utf8::is_utf8($text);
 
-  my $cs = 'ISO-8859-1';
-  if ($self->{report_charset}) {
-    $cs = $self->{report_charset};
-  }
+  my $result = '';
+  for my $line (split(/^/, $text)) {
 
-  my @hexchars = split('', '0123456789abcdef');
-  my $ord;
-  local $1;
-  $text =~ s{([\x80-\xff])}{
-               $ord = ord $1;
-               '='.$hexchars[($ord & 0xf0) >> 4].$hexchars[$ord & 0x0f]
-       }ges;
+    if ($line =~ /^[\x09\x20-\x7E]*\r?\n\z/s) {
+      $result .= $line;  # no need for encoding
 
-  $text = '=?'.$cs.'?Q?'.$text.'?=';
+    } else {
+      my $prefix = '';
+      my $suffix = '';
 
-  dbg("markup: encoding header in $cs: $text");
-  return $text;
+      local $1;
+      if ($line =~ s/( (?: ^ | [ \t] ) [\x09\x20-\x7E]* (?: \r?\n )? ) \z//xs) {
+        $suffix = $1;
+      } elsif ($line =~ s/(\r?\n)\z//s) {
+        $suffix = $1;
+      }
+
+      if ($line =~ s/^ ( [\x09\x20-\x7E]* (?: [ \t] | \z ) )//xs) {
+        $prefix = $1;
+      }
+
+      if ($line eq '') {
+        $result .= $prefix . $suffix;
+      } else {
+        my $qp_enc_count = $line =~ tr/=?_\x00-\x1F\x7F-\xFF//;
+        if (length($line) + $qp_enc_count*2 <= 4 * int(length($line)+2)/3) {
+          # RFC 2047: Upper case should be used for hex digits A through F
+          $line =~ s{ ( [=?_\x00-\x20\x7F-\xFF] ) }
+                    { $1 eq ' ' ? '_' : sprintf("=%02X", ord $1) }xges;
+          $result .= $prefix . '=?UTF-8?Q?' . $line;
+        } else {
+          $result .= $prefix . '=?UTF-8?B?' . base64_encode($line);
+        }
+        $result .= '?=' . $suffix;
+      }
+    }
+  }
+
+  dbg("markup: mime_encode_header: %s", $result);
+  return $result;
 }
 
 sub _process_header {
   my ($self, $hdr_name, $hdr_data) = @_;
 
-  $hdr_data = $self->_replace_tags($hdr_data);
+  $hdr_data = $self->_replace_tags($hdr_data);  # as octets
   $hdr_data =~ s/(?:\r?\n)+$//; # make sure there are no trailing newlines ...
 
   if ($self->{conf}->{fold_headers}) {
@@ -1426,19 +1595,24 @@ sub _replace_tags {
 
   # default to leaving the original string in place, if we cannot find
   # a tag for it (bug 4793)
-  local($1,$2,$3);
-  $text =~ s{(_(\w+?)(?:\((.*?)\))?_)}{
-        my $full = $1;
-        my $tag = $2;
+  local($1);
+  $text =~ s{_([A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*(?:\(.*?\))?)_}{
+        my $tag = $1;
         my $result;
         if ($tag =~ /^ADDEDHEADER(?:HAM|SPAM|)\z/) {
           # Bug 6278: break infinite recursion through _get_added_headers and
           # _get_tag on an attempt to use such tag in add_header template
         } else {
-          $result = $self->get_tag_raw($tag,$3);
-          $result = join(' ',@$result)  if ref $result eq 'ARRAY';
+          $result = $self->get_tag_raw($tag);
+          if (!ref $result) {
+            utf8::encode($result) if utf8::is_utf8($result);
+          } elsif (ref $result eq 'ARRAY') {
+            my @values = @$result;  # avoid modifying referenced array
+            for (@values) { utf8::encode($_) if utf8::is_utf8($_) }
+            $result = join(' ', @values);
+          }
         }
-        defined $result ? $result : $full;
+        defined $result ? $result : "_${tag}_";
       }ge;
 
   return $text;
@@ -1465,15 +1639,15 @@ sub action_depends_on_tags {
     or die "action_depends_on_tags: argument must be a subroutine ref";
 
   # tag names on which the given action depends
-  my @dep_tags = !ref $tags ? uc $tags : map(uc($_),@$tags);
-
-  # @{$self->{tagrun_subs}}            list of all submitted subroutines
-  # @{$self->{tagrun_actions}{$tag}}   bitmask of action indices blocked by tag
-  # $self->{tagrun_tagscnt}[$action_ind]  count of tags still pending
+  my @dep_tags = !ref $tags ? $tags : @$tags;
 
-  # store action details, obtain its index
-  push(@{$self->{tagrun_subs}}, [$code,@args]);
-  my $action_ind = $#{$self->{tagrun_subs}};
+  # uppercase tag, but not args, f.e. HEADER(foo)
+  local($1,$2);
+  foreach (@dep_tags) {
+    if (/^ ([^\(]+) (\(.*)? $/x) {
+      $_ = uc($1).(defined $2 ? $2 : '');
+    }
+  }
 
   # list dependency tag names which are not already satisfied
   my @blocking_tags;
@@ -1484,16 +1658,24 @@ sub action_depends_on_tags {
     }
   }
 
-  $self->{tagrun_tagscnt}[$action_ind] = scalar @blocking_tags;
-  $self->{tagrun_actions}{$_}[$action_ind] = 1  for @blocking_tags;
+  if (!@blocking_tags) {
+    dbg("check: tagrun - tag %s was ready, runnable immediately: %s",
+        join(', ',@dep_tags), join(', ',$code,@args));
+    &$code($self, @args);
+  } else {
+    # @{$self->{tagrun_subs}}            list of all submitted subroutines
+    # @{$self->{tagrun_actions}{$tag}}   bitmask of action indices blocked by tag
+    # $self->{tagrun_tagscnt}[$action_ind]  count of tags still pending
+
+    # store action details, obtain its index
+    push(@{$self->{tagrun_subs}}, [$code,@args]);
+    my $action_ind = $#{$self->{tagrun_subs}};
+
+    $self->{tagrun_tagscnt}[$action_ind] = scalar @blocking_tags;
+    $self->{tagrun_actions}{$_}[$action_ind] = 1  for @blocking_tags;
 
-  if (@blocking_tags) {
     dbg("check: tagrun - action %s blocking on tags %s",
         $action_ind, join(', ',@blocking_tags));
-  } else {
-    dbg("check: tagrun - tag %s was ready, action %s runnable immediately: %s",
-        join(', ',@dep_tags), $action_ind, join(', ',$code,@args));
-    &$code($self, @args);
   }
 }
 
@@ -1512,7 +1694,7 @@ sub tag_is_ready {
   $tag = uc $tag;
 
   if (would_log('dbg', 'check')) {
-    my $tag_val = $self->{tag_data}{$tag};
+    my $tag_val = $self->{tag_data}->{$tag};
     dbg("check: tagrun - tag %s is now ready, value: %s",
          $tag, !defined $tag_val ? '<UNDEF>'
                : ref $tag_val ne 'ARRAY' ? $tag_val
@@ -1556,9 +1738,12 @@ sub report_unsatisfied_actions {
 
 =item $status->set_tag($tagname, $value)
 
-Set a template tag, as used in C<add_header>, report templates, etc.
-This API is intended for use by plugins.  Tag names will be converted
-to an all-uppercase representation internally.
+Set a template tag, as used in C<add_header>, report templates, etc.  This
+API is intended for use by plugins.  Tag names will be converted to an
+all-uppercase representation internally.  Tag names must consist only of
+[A-Z0-9_] characters and must not contain consecutive underscores.  Also the
+name must not start or end in an underscore, as that is the template tagging
+format.
 
 C<$value> can be a simple scalar (string or number), or a reference to an
 array, in which case the public method get_tag will join array elements
@@ -1568,7 +1753,7 @@ compatibility.
 C<$value> can also be a subroutine reference, which will be evaluated
 each time the template is expanded. The first argument passed by get_tag
 to a called subroutine will be a PerMsgStatus object (this module's object),
-followed by optional arguments provided a caller to get_tag.
+followed by optional arguments provided by a caller to get_tag.
 
 Note that perl supports closures, which means that variables set in the
 caller's scope can be accessed inside this C<sub>. For example:
@@ -1579,10 +1764,9 @@ caller's scope can be accessed inside this C<sub>. For example:
               return $text;
             });
 
-See C<Mail::SpamAssassin::Conf>'s C<TEMPLATE TAGS> section for more details
-on how template tags are used.
-
-C<undef> will be returned if a tag by that name has not been defined.
+See C<Mail::SpamAssassin::Conf>'s C<TEMPLATE TAGS> and C<CAPTURING TAGS
+USING REGEX NAMED CAPTURE GROUPS> sections for more details on how template
+tags are used.
 
 =cut
 
@@ -1598,9 +1782,11 @@ sub set_tag {
 
 Get the current value of a template tag, as used in C<add_header>, report
 templates, etc. This API is intended for use by plugins.  Tag names will be
-converted to an all-uppercase representation internally.  See
-C<Mail::SpamAssassin::Conf>'s C<TEMPLATE TAGS> section for more details on
-tags.
+converted to an all-uppercase representation internally.
+
+See C<Mail::SpamAssassin::Conf>'s C<TEMPLATE TAGS> and C<CAPTURING TAGS
+USING REGEX NAMED CAPTURE GROUPS> sections for more details on how template
+tags are used.
 
 C<undef> will be returned if a tag by that name has not been defined.
 
@@ -1610,7 +1796,14 @@ sub get_tag {
   my($self, $tag, @args) = @_;
 
   return if !defined $tag;
+
+  # handle TAGNAME(args) format
+  local($1);
+  if ($tag =~ s/\((.*?)\)$//) {
+    @args = ($1);
+  }
   $tag = uc $tag;
+
   my $data;
   if (exists $common_tags{$tag}) {
     # tag data from traditional pre-defined tag subroutines
@@ -1639,6 +1832,13 @@ sub get_tag_raw {
   my($self, $tag, @args) = @_;
 
   return if !defined $tag;
+
+  # handle TAGNAME(args) format
+  local($1);
+  if ($tag =~ s/\((.*?)\)$//) {
+    @args = ($1);
+  }
+
   my $data;
   if (exists $common_tags{$tag}) {
     # tag data from traditional pre-defined tag subroutines
@@ -1748,16 +1948,9 @@ sub finish {
          permsgstatus => $self
        });
 
-  $self->report_unsatisfied_actions;
+  $self->report_unsatisfied_actions();
 
-  # Delete out all of the members of $self.  This will remove any direct
-  # circular references and let the memory get reclaimed while also being more
-  # efficient than a foreach() loop over the keys.
-  %{$self} = ();
-}
-
-sub finish_tests {
-  my ($conf) = @_;
+  # Clean up temporary methods
   foreach my $method (@TEMPORARY_METHODS) {
     if (defined &{$method}) {
       undef &{$method};
@@ -1765,8 +1958,17 @@ sub finish_tests {
   }
   @TEMPORARY_METHODS = ();      # clear for next time
   %TEMPORARY_EVAL_GLUE_METHODS = ();
+
+  # Delete out all of the members of $self.  This will remove any direct
+  # circular references and let the memory get reclaimed while also being more
+  # efficient than a foreach() loop over the keys.
+  %{$self} = ();
 }
 
+# Deprecated for clarity, only Plugins have this function
+sub finish_tests {}
+
+###########################################################################
 
 =item $name = $status->get_current_eval_rule_name()
 
@@ -1802,23 +2004,31 @@ sub extract_message_metadata {
     $self->{$item} = $self->{msg}->{metadata}->{$item};
   }
 
-  # TODO: International domain names (UTF-8) must be converted to
-  # ASCII-compatible encoding (ACE) for the purpose of setting the
-  # SENDERDOMAIN and AUTHORDOMAIN tags (and probably for other uses too).
-  # (explicitly required for DMARC, draft-kucherawy-dmarc-base sect. 5.6.1)
+  # International domain names (UTF-8) must be converted to ASCII-compatible
+  # encoding (ACE) for the purpose of setting the SENDERDOMAIN and AUTHORDOMAIN
+  # tags (explicitly required for DMARC, RFC 7489)
   #
   { local $1;
-    my $addr = $self->get('EnvelopeFrom:addr', undef);
+    my $host = ($self->get('EnvelopeFrom:first:addr:host'))[0];
     # collect a FQDN, ignoring potential trailing WSP
-    if (defined $addr && $addr =~ /\@([^@. \t]+\.[^@ \t]+?)[ \t]*\z/s) {
-      $self->set_tag('SENDERDOMAIN', lc $1);
+    if (defined $host) {
+      my $d = idn_to_ascii($host);
+      $self->set_tag('SENDERDOMAIN', $d);
+      $self->{msg}->put_metadata("X-SenderDomain", $d);
+      dbg("metadata: X-SenderDomain: %s", $d);
     }
-    # TODO: the get ':addr' only returns the first address; this should be
-    # augmented to be able to return all addresses in a header field, multiple
-    # addresses in a From header field are allowed according to RFC 5322
-    $addr = $self->get('From:addr', undef);
-    if (defined $addr && $addr =~ /\@([^@. \t]+\.[^@ \t]+?)[ \t]*\z/s) {
-      $self->set_tag('AUTHORDOMAIN', lc $1);
+    my @from_doms;
+    my %seen;
+    foreach ($self->get('From:addr:host')) {
+      next if $seen{$_}++;
+      my $d = idn_to_ascii($_);
+      push @from_doms, $d;
+    }
+    if (@from_doms) {
+      $self->set_tag('AUTHORDOMAIN', @from_doms > 1 ? \@from_doms : $from_doms[0]);
+      my $d = join(" ", @from_doms);
+      $self->{msg}->put_metadata("X-AuthorDomain", $d);
+      dbg("metadata: X-AuthorDomain: %s", $d);
     }
   }
 
@@ -1833,6 +2043,7 @@ sub extract_message_metadata {
     $self->get_decoded_stripped_body_text_array();
   }
   $self->{html} = $self->{msg}->{metadata}->{html};
+  $self->{html_all} = $self->{msg}->{metadata}->{html_all};
 
   # allow plugins to add more metadata, read the stuff that's there, etc.
   $self->{main}->call_plugins ("parsed_metadata", { permsgstatus => $self });
@@ -1880,25 +2091,32 @@ sub get_decoded_stripped_body_text_array {
 
 =item $status->get (header_name [, default_value])
 
-Returns a message header, pseudo-header, real name or address.
-C<header_name> is the name of a mail header, such as 'Subject', 'To',
-etc.  If C<default_value> is given, it will be used if the requested
-C<header_name> does not exist.
+Returns a message header, pseudo-header or a real name, email-address or
+some other parsed value set by modifiers.  C<header_name> is the name of a
+mail header, such as 'Subject', 'To', etc.
+
+Should be called in list context since 4.0.  Will return list of headers
+content, or other values when modifiers used.
 
-Appending C<:raw> to the header name will inhibit decoding of quoted-printable
-or base-64 encoded strings.
+If C<default_value> is given, it will be used if the requested
+C<header_name> does not exist.  This is mainly useful when called in scalar
+context to set 'undef' instead of legacy '' return value when header does
+not exist.
 
-Appending a modifier C<:addr> to a header field name will cause everything
-except the first email address to be removed from the header field.  It is
-mainly applicable to header fields 'From', 'Sender', 'To', 'Cc' along with
-their 'Resent-*' counterparts, and the 'Return-Path'. For example, all of
-the following will result in "example@foo":
+Appending C<:raw> modifier to the header name will inhibit decoding of
+quoted-printable or base-64 encoded strings.
+
+Appending C<:addr> modifier to the header name will return all
+email-addresses found in the header.  It is mainly applicable to header
+fields 'From', 'Sender', 'To', 'Cc' along with their 'Resent-*'
+counterparts, and the 'Return-Path'.  For example, all of the following will
+result in "example@foo" (and "example@bar"):
 
 =over 4
 
 =item example@foo
 
-=item example@foo (Foo Blah)
+=item example@foo (Foo Blah), <example@bar>
 
 =item example@foo, example@bar
 
@@ -1912,18 +2130,18 @@ the following will result in "example@foo":
 
 =back
 
-Appending a modifier C<:name> to a header field name will cause everything
-except the first display name to be removed from the header field. It is
-mainly applicable to header fields containing a single mail address: 'From',
-'Sender', along with their 'Resent-From' and 'Resent-Sender' counterparts.
-For example, all of the following will result in "Foo Blah". One level of
-single quotes is stripped too, as it is often seen.
+Appending C<:name> modifier to the header name will return all "display
+names" from the header field.  As with C<:addr>, it is mainly applicable to
+header fields 'From', 'Sender', 'To', 'Cc' along with their 'Resent-*'
+counterparts, and the 'Return-Path'.  For example, all of the following will
+result in "Foo Blah" (and "Bar Baz").  One level of single quotes is
+stripped too, as it is often seen.
 
 =over 4
 
 =item example@foo (Foo Blah)
 
-=item example@foo (Foo Blah), example@bar
+=item example@foo (Foo Blah), "Bar Baz" <example@bar>
 
 =item display: example@foo (Foo Blah), example@bar ;
 
@@ -1935,6 +2153,28 @@ single quotes is stripped too, as it is often seen.
 
 =back
 
+Appending C<:host> to the header name will return the first hostname-looking
+string that ends with a valid TLD.  First it tries to find a match after @
+character (possible email), then from any part of the header.  Normal use of
+this would be for example 'From:addr:host' to return the hostname portion of
+a From-address.
+
+Appending C<:domain> to the header name implies C<:host>, but will return
+only domain part of the hostname, as returned by
+RegistryBoundaries::trim_domain().
+
+Appending C<:ip> to the header name, will return the first IPv4 or IPv6
+address string found.  Could be used for example as 'X-Originating-IP:ip'.
+
+Appending C<:revip> to the header name implies C<:ip>, but will return the
+found IP in reverse (usually for DNSBL usage).
+
+Appending C<:first> modifier to the header name will return only the first
+(topmost) header, in case there are multiple ones.  Similarly C<:last> will
+select the last one.  These affect only the physical header line selection. 
+If selected header is parsed further with C<:addr> or similar, it may return
+multiple results, if the selected header contains multiple addresses.
+
 There are several special pseudo-headers that can be specified:
 
 =over 4
@@ -1975,6 +2215,12 @@ the message has passed through
 =item C<X-Spam-Relays-Trusted> is the generated metadata of trusted relays
 the message has passed through
 
+=item C<X-Spam-Relays-External> is the generated metadata of external relays
+the message has passed through
+
+=item C<X-Spam-Relays-Internal> is the generated metadata of internal relays
+the message has passed through
+
 =back
 
 =cut
@@ -1983,90 +2229,106 @@ the message has passed through
 sub _get {
   my ($self, $request) = @_;
 
-  my $result;
+  my @results;
   my $getaddr = 0;
   my $getname = 0;
   my $getraw = 0;
+  my $needraw = 0;
+  my $gethost = 0;
+  my $getdomain = 0;
+  my $getip = 0;
+  my $getrevip = 0;
+  my $getfirst = 0;
+  my $getlast = 0;
 
   # special queries - process and strip modifiers
   if (index($request,':') >= 0) {  # triage
     local $1;
     while ($request =~ s/:([^:]*)//) {
-      if    ($1 eq 'raw')  { $getraw  = 1 }
-      elsif ($1 eq 'addr') { $getaddr = $getraw = 1 }
-      elsif ($1 eq 'name') { $getname = 1 }
+      if    ($1 eq 'raw')    { $getraw  = 1 }
+      elsif ($1 eq 'addr')   { $getaddr = $needraw = 1 }
+      elsif ($1 eq 'name')   { $getname = $needraw = 1 }
+      elsif ($1 eq 'host')   { $gethost = 1 }
+      elsif ($1 eq 'domain') { $gethost = $getdomain = 1 }
+      elsif ($1 eq 'ip')     { $getip = 1 }
+      elsif ($1 eq 'revip')  { $getip = $getrevip = 1 }
+      elsif ($1 eq 'first')  { $getfirst = 1 }
+      elsif ($1 eq 'last')   { $getlast = 1 }
     }
   }
   my $request_lc = lc $request;
 
   # ALL: entire pristine or semi-raw headers
   if ($request eq 'ALL') {
-    return ($getraw ? $self->{msg}->get_pristine_header()
-                    : $self->{msg}->get_all_headers(0));
+    if ($getraw) {
+      @results = $self->{msg}->get_pristine_header() =~ /^([^ \t].*?\n)(?![ \t])/smgi;
+    } else {
+      @results = $self->{msg}->get_all_headers(0);
+    }
+    return \@results;
   }
   # ALL-TRUSTED: entire trusted raw headers
   elsif ($request eq 'ALL-TRUSTED') {
     # '+1' since we added the received header even though it's not considered
     # trusted, so we know that those headers can be trusted too
-    return $self->get_all_hdrs_in_rcvd_index_range(
+    @results = $self->get_all_hdrs_in_rcvd_index_range(
                        undef, $self->{last_trusted_relay_index}+1,
                        undef, undef, $getraw);
+    return \@results;
   }
   # ALL-INTERNAL: entire internal raw headers
   elsif ($request eq 'ALL-INTERNAL') {
     # '+1' for the same reason as in ALL-TRUSTED above
-    return $self->get_all_hdrs_in_rcvd_index_range(
+    @results = $self->get_all_hdrs_in_rcvd_index_range(
                        undef, $self->{last_internal_relay_index}+1,
                        undef, undef, $getraw);
+    return \@results;
   }
   # ALL-UNTRUSTED: entire untrusted raw headers
   elsif ($request eq 'ALL-UNTRUSTED') {
     # '+1' for the same reason as in ALL-TRUSTED above
-    return $self->get_all_hdrs_in_rcvd_index_range(
+    @results = $self->get_all_hdrs_in_rcvd_index_range(
                        $self->{last_trusted_relay_index}+1, undef,
                        undef, undef, $getraw);
+    return \@results;
   }
   # ALL-EXTERNAL: entire external raw headers
   elsif ($request eq 'ALL-EXTERNAL') {
     # '+1' for the same reason as in ALL-TRUSTED above
-    return $self->get_all_hdrs_in_rcvd_index_range(
+    @results = $self->get_all_hdrs_in_rcvd_index_range(
                        $self->{last_internal_relay_index}+1, undef,
                        undef, undef, $getraw);
+    return \@results;
   }
   # EnvelopeFrom: the SMTP MAIL FROM: address
   elsif ($request_lc eq "\LEnvelopeFrom") {
-    $result = $self->get_envelope_from();
+    push @results, $self->get_envelope_from();
   }
   # untrusted relays list, as string
   elsif ($request_lc eq "\LX-Spam-Relays-Untrusted") {
-    $result = $self->{relays_untrusted_str};
+    push @results, $self->{relays_untrusted_str};
   }
   # trusted relays list, as string
   elsif ($request_lc eq "\LX-Spam-Relays-Trusted") {
-    $result = $self->{relays_trusted_str};
+    push @results, $self->{relays_trusted_str};
   }
   # external relays list, as string
   elsif ($request_lc eq "\LX-Spam-Relays-External") {
-    $result = $self->{relays_external_str};
+    push @results, $self->{relays_external_str};
   }
   # internal relays list, as string
   elsif ($request_lc eq "\LX-Spam-Relays-Internal") {
-    $result = $self->{relays_internal_str};
+    push @results, $self->{relays_internal_str};
   }
   # ToCc: the combined recipients list
   elsif ($request_lc eq "\LToCc") {
-    $result = join("\n", $self->{msg}->get_header('To', $getraw));
-    if ($result ne '') {
-      chomp $result;
-      $result .= ", " if $result =~ /\S/;
-    }
-    $result .= join("\n", $self->{msg}->get_header('Cc', $getraw));
-    $result = undef if $result eq '';
+    push @results, $self->{msg}->get_header('To', $getraw);
+    push @results, $self->{msg}->get_header('Cc', $getraw);
   }
   # MESSAGEID: handle lists which move the real message-id to another
   # header for resending.
   elsif ($request eq 'MESSAGEID') {
-    $result = join("\n", grep { defined($_) && $_ ne '' }
+    push @results, grep { defined($_) && $_ ne '' } (
                   $self->{msg}->get_header('X-Message-Id', $getraw),
                   $self->{msg}->get_header('Resent-Message-Id', $getraw),
                   $self->{msg}->get_header('X-Original-Message-ID', $getraw),
@@ -2074,83 +2336,126 @@ sub _get {
   }
   # a conventional header
   else {
-    my @results = $getraw ? $self->{msg}->raw_header($request)
-                          : $self->{msg}->get_header($request);
-  # dbg("message: get(%s)%s = %s",
-  #     $request, $getraw?'raw':'', join(", ",@results));
-    if (@results) {
-      $result = join('', @results);
-    } else {  # metadata
-      $result = $self->{msg}->get_metadata($request);
+    my @res = $getraw||$needraw ? $self->{msg}->raw_header($request)
+                                : $self->{msg}->get_header($request);
+    if (!@res) {
+      if (defined(my $m = $self->{msg}->get_metadata($request))) {
+        push @res, $m;
+      }
     }
+    push @results, @res if @res;
   }
 
-  # special queries
-  if (defined $result && ($getaddr || $getname)) {
-    local $1;
-    $result =~ s/^[^:]+:(.*);\s*$/$1/gs;       # 'undisclosed-recipients: ;'
-    $result =~ s/\s+/ /g;                      # reduce whitespace
-    $result =~ s/^\s+//;                       # leading whitespace
-    $result =~ s/\s+$//;                       # trailing whitespace
-
-    if ($getaddr) {
-      # Get the email address out of the header
-      # All of these should result in "jm@foo":
-      # jm@foo
-      # jm@foo (Foo Blah)
-      # jm@foo, jm@bar
-      # display: jm@foo (Foo Blah), jm@bar ;
-      # Foo Blah <jm@foo>
-      # "Foo Blah" <jm@foo>
-      # "'Foo Blah'" <jm@foo>
-      #
-      # strip out the (comments)
-      $result =~ s/\s*\(.*?\)//g;
-      # strip out the "quoted text", unless it's the only thing in the string
-      if ($result !~ /^".*"$/) {
-        $result =~ s/(?<!<)"[^"]*"(?!\@)//g;   #" emacs
+  # Nothing found to process further, bail out quick
+  if (!@results) {
+    return \@results;
+  }
+
+  # Continue processing only first (topmost) or last header
+  if ($getfirst) {
+    @results = ($results[0]);
+  } elsif ($getlast) {
+    @results = ($results[-1]);
+  }
+
+  # special addr/name
+  if ($getaddr || $getname) {
+    my @res;
+    foreach my $line (@results) {
+      next unless defined $line;
+      # Note: parse_header_addresses always called with raw undecoded value
+      # Skip invalid addresses here
+      my @addrs = parse_header_addresses($line);
+      if (@addrs) {
+        if ($getaddr) {
+          foreach my $addr (@addrs) {
+            push @res, $addr->{address} if defined $addr->{address};
+          }
+        }
+        elsif ($getname) {
+          foreach my $addr (@addrs) {
+            next unless defined $addr->{phrase};
+            if ($getraw) {
+              # phrase=name, could also be username or comment unless name found
+              push @res, $addr->{phrase};
+            } else {
+              # If :raw was not specifically asked, decode mimewords
+              # TODO: silly call to Node module, should probably be in Util
+              my $decoded = Mail::SpamAssassin::Message::Node::_decode_header(
+                              $addr->{phrase}, "PMS:get:$request");
+              # Normalize whitespace, unless it's all white-space
+              if ($decoded =~ /\S/) {
+                $decoded =~ s/\s+/ /gs;
+                $decoded =~ s/^\s+//;
+                $decoded =~ s/\s+$//;
+                $decoded =~ s/^'(.*?)'$/$1/; # remove single quotes
+              }
+              push @res, $decoded if defined $decoded;
+            }
+          }
+        }
       }
-      # Foo Blah <jm@xxx> or <jm@xxx>
-      local $1;
-      $result =~ s/^[^"<]*?<(.*?)>.*$/$1/;
-      # multiple addresses on one line? remove all but first
-      $result =~ s/,.*$//;
     }
-    elsif ($getname) {
-      # Get the display name out of the header
-      # All of these should result in "Foo Blah":
-      #
-      # jm@foo (Foo Blah)
-      # (Foo Blah) jm@foo
-      # jm@foo (Foo Blah), jm@bar
-      # display: jm@foo (Foo Blah), jm@bar ;
-      # Foo Blah <jm@foo>
-      # "Foo Blah" <jm@foo>
-      # "'Foo Blah'" <jm@foo>
-      #
-      local $1;
-      # does not handle mailbox-list or address-list or quotes well, to be improved
-      if ($result =~ /^ \s* " (.*?) (?<!\\)" \s* < [^<>]* >/sx ||
-          $result =~ /^ \s* (.*?) \s* < [^<>]* >/sx) {
-        $result = $1;  # display-name, RFC 5322
-        # name-addr    = [display-name] angle-addr
-        # display-name = phrase
-        # phrase       = 1*word / obs-phrase
-        # word         = atom / quoted-string
-        # obs-phrase   = word *(word / "." / CFWS)
-        $result =~ s{ " ( (?: [^"\\] | \\. )* ) " }
-                { my $s=$1; $s=~s{\\(.)}{$1}gs; $s }gsxe;
-        $result =~ s/\\"/"/gs;
-      } elsif ($result =~ /^ [^(,]*? \( (.*?) \) /sx) {  # legacy form
-        # nested comments are not handled, to be improved
-        $result = $1;
-      } else {  # no display name
-        $result = '';
+    @results = @res;
+  }
+
+  # special host/domain
+  if (@results && ($gethost || $getdomain || $getip)) {
+    my @res;
+    if ($gethost) {
+      # TODO: IDN matching needs honing
+      my $tldsRE = $self->{main}->{registryboundaries}->{valid_tlds_re};
+      #my $hostRE = qr/(?<![._-])\b([a-z\d][a-z\d._-]{0,251}\.${tldsRE})\b(?![._-])/i;
+      my $hostRE = qr/(?<![._-])(\S{1,251}\.${tldsRE})(?![._-])/i;
+      foreach my $line (@results) {
+        next unless defined $line;
+        my $host;
+        if ($getaddr) {
+          # If :addr already preparsed the line, just grab domain liberally
+          if ($line =~ /.*\@(\S+)/) {
+            $host = $1;
+          }
+        }
+        else {
+          # try grabbing email/msgid domain first, because user part might look like
+          # a valid host..
+          if ($line =~ /.*\@${hostRE}/i) {
+            if (is_fqdn_valid(idn_to_ascii($1), 1)) {
+              $host = $1;
+            }
+          }
+          # otherwise try hard to find a valid host
+          if (!$host) {
+            while ($line =~ /${hostRE}/ig) {
+              if (is_fqdn_valid(idn_to_ascii($1), 1)) {
+                $host = $1;
+                last;
+              }
+            }
+          }
+        }
+        if ($host) {
+          if ($getdomain) {
+            $host = $self->{main}->{registryboundaries}->trim_domain($host, 1);
+          }
+          push @res, $host;
+        }
+      }
+    } else {
+      my $ipRE = qr/(?<!\.)\b(${IP_ADDRESS})\b(?!\.)/;
+      foreach my $line (@results) {
+        next unless defined $line;
+        my $host;
+        if ($line =~ $ipRE) {
+          $host = $getrevip ? reverse_ip_address($1) : $1;
+        }
+        push @res, $host  if defined $host;
       }
-      $result =~ s/^ \s* ' \s* (.*?) \s* ' \s* \z/$1/sx;
     }
+    @results = @res;
   }
-  return $result;
+
+  return \@results;
 }
 
 # optimized for speed
@@ -2158,8 +2463,8 @@ sub _get {
 # $_[1] is request
 # $_[2] is defval
 sub get {
-  my $cache = $_[0]->{c};
-  my $found;
+  my $cache = $_[0]->{get_cache};
+  my $found = [];
   if (exists $cache->{$_[1]}) {
     # return cache entry if it is known
     # (measured hit/attempts rate on a production mailer is about 47%)
@@ -2167,13 +2472,34 @@ sub get {
   } else {
     # fill in a cache entry
     $found = _get(@_);
+    # filter out undefined
+    @$found = grep { defined } @$found;
     $cache->{$_[1]} = $found;
   }
   # if the requested header wasn't found, we should return a default value
   # as specified by the caller: if defval argument is present it represents
   # a default value even if undef; if defval argument is absent a default
   # value is an empty string for upwards compatibility
-  return (defined $found ? $found : @_ > 2 ? $_[2] : '');
+  if (@$found) {
+    # new list context usage in 4.0, return all values always
+    if (wantarray) {
+      return @$found;
+    }
+    # legacy scalar context expected only single return value for some
+    # queries, without a newline
+    if ($_[1] =~ /:(?:addr|name|host|domain|ip|revip)\b/ ||
+        $_[1] eq 'EnvelopeFrom') {
+      my $res = $found->[0];
+      $res =~ s/\n\z$//;
+      return $res;
+    } else {
+      return join('', @$found);
+    }
+  } elsif (@_ > 2) {
+    return wantarray ? ($_[2]) : $_[2];
+  } else {
+    return wantarray ? () : '';
+  }
 }
 
 ###########################################################################
@@ -2384,6 +2710,9 @@ sub _process_text_uri_list {
       next if exists $seen{$rawuri};
       $seen{$rawuri} = 1;
 
+      # Ignore bogus mail captures (@ might have been trimmed from the end above..)
+      next if $rawtype eq 'mail' && index($rawuri, '@') == -1;
+
       dbg("uri: found rawuri from text ($rawtype): $rawuri") if $would_log_uri_all;
 
       # Quick ignore if schemeless host not valid
@@ -2411,6 +2740,10 @@ sub _process_text_uri_list {
         elsif ($uri =~ /^www\d{0,2}\./i) {
           $uri = "http://$uri";
         }
+        elsif ($uri =~ /\/.+\@/) {
+          # if a "/" is found before @ it cannot be a valid email address
+          $uri = "http://$uri";
+        }
         elsif (index($uri, '@') != -1) {
           # This is not linkified by MUAs: foo@bar%2Ecom
           # This IS linkified: foo@bar%2Ebar.com
@@ -2456,18 +2789,20 @@ sub _process_html_uri_list {
   my ($self) = @_;
 
   # get URIs from HTML parsing
-  # use the metadata version since $self->{html} may not be setup
-  my $detail = $self->{msg}->{metadata}->{html}->{uri_detail} || { };
-  $self->{'uri_truncated'} = 1 if $self->{msg}->{metadata}->{html}->{uri_truncated};
-
-  # canonicalize the HTML parsed URIs
-  while(my($uri, $info) = each %{ $detail }) {
-    if ($self->add_uri_detail_list($uri, $info->{types}, 'html', 0)) {
-      # Need also to copy and uniq anchor text
-      if (exists $info->{anchor_text}) {
-        my %seen;
-        foreach (grep { !$seen{$_}++ } @{$info->{anchor_text}}) {
-          push @{$self->{uri_detail_list}->{$uri}->{anchor_text}}, $_;
+  # use the metadata version since $self->{html_all} may not be setup
+  foreach my $html (@{$self->{msg}->{metadata}->{html_all}}) {
+    my $detail = $html->{uri_detail} || { };
+    $self->{'uri_truncated'} = 1 if $html->{uri_truncated};
+
+    # canonicalize the HTML parsed URIs
+    while(my($uri, $info) = each %{ $detail }) {
+      if ($self->add_uri_detail_list($uri, $info->{types}, 'html', 0)) {
+        # Need also to copy and uniq anchor text
+        if (exists $info->{anchor_text}) {
+          my %seen;
+          foreach (grep { !$seen{$_}++ } @{$info->{anchor_text}}) {
+            push @{$self->{uri_detail_list}->{$uri}->{anchor_text}}, $_;
+          }
         }
       }
     }
@@ -2486,15 +2821,16 @@ sub _process_dkim_uri_list {
 
   # Look for the domain in DK/DKIM headers
   if ($self->{conf}->{parse_dkim_uris}) {
-    my $dk = join(" ", grep {defined} ( $self->get('DomainKey-Signature',undef ),
-                                        $self->get('DKIM-Signature',undef) ));
-    while ($dk =~ /\bd\s*=\s*([^;]+)/g) {
-      my $d = $1;
-      $d =~ s/\s+//g;
-      # prefix with domainkeys: so it doesn't merge with identical keys
-      $self->add_uri_detail_list("domainkeys:$d",
-        {'domainkeys'=>1, 'nocanon'=>1, 'noclean'=>1},
-        'domainkeys', 1);
+    foreach my $dk ( $self->get('DomainKey-Signature'),
+                     $self->get('DKIM-Signature') ) {
+      while ($dk =~ /\bd\s*=\s*([^;]+)/g) {
+        my $d = $1;
+        $d =~ s/\s+//g;
+        # prefix with domainkeys: so it doesn't merge with identical keys
+        $self->add_uri_detail_list("domainkeys:$d",
+          {'domainkeys'=>1, 'nocanon'=>1, 'noclean'=>1},
+          'domainkeys', 1);
+      }
     }
   }
 }
@@ -2538,7 +2874,7 @@ sub add_uri_detail_list {
   if ($types->{nocanon}) {
     push @uris, $uri;
   } else {
-    @uris = uri_list_canonicalize($self->{conf}->{redirector_patterns}, $uri);
+    @uris = uri_list_canonicalize($self->{conf}->{redirector_patterns}, [$uri], $self->{main}->{registryboundaries});
   }
   foreach my $cleanuri (@uris) {
     # Make sure all the URIs are nice and short
@@ -2583,35 +2919,10 @@ sub add_uri_detail_list {
   return 1;
 }
 
-
 ###########################################################################
 
-sub ensure_rules_are_complete {
-  my $self = shift;
-  my $metarule = shift;
-  # @_ is now the list of rules
-
-  foreach my $r (@_) {
-    # dbg("rules: meta rule depends on net rule $r");
-    next if ($self->is_rule_complete($r));
-
-    dbg("rules: meta rule $metarule depends on pending rule $r, blocking");
-    my $timer = $self->{main}->time_method("wait_for_pending_rules");
-
-    my $start = time;
-    $self->harvest_until_rule_completes($r);
-    my $elapsed = sprintf "%.2f", time - $start;
-
-    if (!$self->is_rule_complete($r)) {
-      dbg("rules: rule $r is still not complete; exited early?");
-    }
-    elsif ($elapsed > 0) {
-      my $txt = "rules: $r took $elapsed seconds to complete, for $metarule";
-      # Info only if something took over 1 sec to wait, prevent log flood
-      if ($elapsed >= 1) { info($txt); } else { dbg($txt); }
-    }
-  }
-}
+# Deprecated since 4.0, meta rules do not depend on priorities anymore
+sub ensure_rules_are_complete {}
 
 ###########################################################################
 
@@ -2658,35 +2969,25 @@ ENDOFEVAL
   eval $evalstr . '; 1'   ## no critic
   or do {
     my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
-    warn "rules: failed to run header tests, skipping some: $eval_stat\n";
+    warn "rules: failed to compile method '$function': $eval_stat\n";
     $self->{rule_errors}++;
   };
 
-  # ensure this method is deleted if finish_tests() is called
+  # ensure this method is deleted if finish() is called
   push (@TEMPORARY_METHODS, $function);
 }
 
 ###########################################################################
 
-# note: only eval tests should store state in $self->{test_log_msgs};
-# pattern tests do not.
-#
-# the clearing of the test state is now inlined as:
-#
-# %{$self->{test_log_msgs}} = ();        # clear test state
-#
-# except for this public API for plugin use:
-
 =item $status->clear_test_state()
 
-Clear test state, including test log messages from C<$status-E<gt>test_log()>.
+DEPRECATED, UNNEEDED SINCE 4.0
 
 =cut
 
-sub clear_test_state {
-    my ($self) = @_;
-    %{$self->{test_log_msgs}} = ();
-}
+sub clear_test_state {}
+
+###########################################################################
 
 # internal API, called only by got_hit()
 # TODO: refactor and merge this into that function
@@ -2701,7 +3002,7 @@ sub _handle_hit {
       });
 
     # ignore meta-match sub-rules.
-    if ($rule =~ /^__/) { push(@{$self->{subtest_names_hit}}, $rule); return; }
+    if (index($rule, '__') == 0) { push(@{$self->{subtest_names_hit}}, $rule); return; }
 
     # this should not happen; warn about it
     if (!defined $score) {
@@ -2719,32 +3020,10 @@ sub _handle_hit {
     $self->{score} += $score;
 
     push(@{$self->{test_names_hit}}, $rule);
-    $area ||= '';
-
-    if ($score >= 10 || $score <= -10) {
-      $score = sprintf("%4.0f", $score);
-    }
-    else {
-      $score = sprintf("%4.1f", $score);
-    }
 
-    # save both summaries
-    # TODO: this is slower than necessary, if we only need one
-    $self->{tag_data}->{REPORT} .= sprintf ("* %s %s %s%s\n%s",
-              $score, $rule, $area,
-              $self->_wrap_desc($desc,
-                  4+length($rule)+length($score)+length($area), "*      "),
-              ($self->{test_log_msgs}->{TERSE} ?
-              "*      " . $self->{test_log_msgs}->{TERSE} : ''));
-
-    $self->{tag_data}->{SUMMARY} .= sprintf ("%s %-22s %s%s\n%s",
-              $score, $rule, $area,
-              $self->_wrap_desc($desc,
-                  3+length($rule)+length($score)+length($area), " " x 28),
-              ($self->{test_log_msgs}->{LONG} || ''));
-    if((defined $self->{subjprefix}) and ($self->{subjprefix} ne "")) {
-      $self->{tag_data}->{SUBJPREFIX} = $self->{subjprefix};
-    }
+    # Save for report processing
+    $self->{test_logs}->{$rule}->{area} = $area;
+    $self->{test_logs}->{$rule}->{desc} = $desc;
 }
 
 sub _wrap_desc {
@@ -2832,7 +3111,6 @@ sub got_hit {
   # adding a hit does nothing if we don't have a score -- we probably
   # shouldn't have run it in the first place
   if (!$score) {
-    %{$self->{test_log_msgs}} = ();
     return;
   }
 
@@ -2851,10 +3129,10 @@ sub got_hit {
   my $already_hit = $self->{tests_already_hit}->{$rule} || 0;
   # don't count hits multiple times, unless 'tflags multiple' is on
   if ($already_hit && ($tflags_ref->{$rule}||'') !~ /\bmultiple\b/) {
-    %{$self->{test_log_msgs}} = ();
     return;
   }
 
+  $self->rule_ready($rule, 1); # mark ready for metas
   $self->{tests_already_hit}->{$rule} = $already_hit + $value;
 
   # default ruletype, if not specified:
@@ -2875,11 +3153,12 @@ sub got_hit {
   #$rule_descr = $rule  if !defined $rule_descr || $rule_descr eq '';
   $rule_descr = "No description available." if !defined $rule_descr || $rule_descr eq '';
 
-  if(defined $self->{conf}->{rewrite_header}->{Subject}) {
+  if (defined $self->{conf}->{rewrite_header}->{Subject}) {
     my $rule_subjprefix = $conf_ref->{subjprefix}->{$rule};
     if (defined $rule_subjprefix) {
       dbg("subjprefix: setting Subject prefix to $rule_subjprefix");
-      if($self->{subjprefix} !~ /\Q$rule_subjprefix\E/) {
+      $self->{subjprefix} ||= '';
+      if (index($self->{subjprefix}, $rule_subjprefix) == -1) {
         $self->{subjprefix} .= $rule_subjprefix . " ";  # save dynamic subject prefix.
       }
     }
@@ -2891,39 +3170,63 @@ sub got_hit {
             $params{ruletype},
             $rule_descr);
 
-  # take care of duplicate rules, too (bug 5206)
-  my $dups = $conf_ref->{duplicate_rules}->{$rule};
-  if ($dups && @{$dups}) {
-    foreach my $dup (@{$dups}) {
-      $self->got_hit($dup, $area, %params);
-    }
-  }
-
-  %{$self->{test_log_msgs}} = ();  # clear test logs
   return 1;
 }
 
-###########################################################################
+=item $status->rule_ready ($rulename [, $no_async])
 
-# TODO: this needs API doc
-sub test_log {
-  my ($self, $msg) = @_;
-  local $1;
-  while ($msg =~ s/^(.{30,48})\s//) {
-    $self->_test_log_line ($1);
+Mark an asynchronous rule ready, so it can be considered for meta rule
+evaluation.  Asynchronous rule is a rule whose eval-function returns undef,
+marking that it's not ready yet, expecting results later. 
+$status->rule_ready() must be called later to mark it ready, alternatively
+$status->got_hit() also does this.  If neither is called, then any meta rule
+that depends on this rule might not evaluate.
+
+Optional boolean $no_async skips checking if there are pending async DNS
+lookups for the rule.
+
+=cut
+
+sub rule_ready {
+  my ($self, $rule, $no_async) = @_;
+
+  # Ready already?
+  return if exists $self->{tests_already_hit}->{$rule};
+
+  if (!$no_async && $self->get_async_pending_rules($rule)) {
+    # Can't be ready if there are pending DNS lookups, ignore for now.
+    return;
+  }
+
+  # record rules that depend on this, so do_meta_tests will be run
+  foreach (keys %{$self->{conf}->{meta_deprules}->{$rule}}) {
+    $self->{meta_check_ready}->{$_} = 1;
   }
-  $self->_test_log_line ($msg);
+
+  # mark ready
+  $self->{tests_already_hit}->{$rule} ||= 0;
 }
 
-sub _test_log_line {
-  my ($self, $msg) = @_;
+###########################################################################
 
-  $self->{test_log_msgs}->{TERSE} .= sprintf ("[%s]\n", $msg);
-  if (length($msg) > 47) {
-    $self->{test_log_msgs}->{LONG} .= sprintf ("%78s\n", "[$msg]");
-  } else {
-    $self->{test_log_msgs}->{LONG} .= sprintf ("%27s [%s]\n", "", $msg);
-  }
+=item $status->test_log ($text [, $rulename])
+
+Add $text log entry for a hit rule in final message REPORT/SUMMARY.
+
+Usually called just before got_hit(), to describe for example what URI the
+rule matched on.  Optional <$rulename> argument is recommended to make sure
+log is written to correct rule.  If rulename is not provided,
+get_current_eval_rule_name() is used as fallback.
+
+Can be called multiple times per rule for additional entries.
+
+=cut
+
+sub test_log {
+  my ($self, $msg, $rulename) = @_;
+  $rulename ||= $self->get_current_eval_rule_name();
+  return if !defined $rulename;
+  push @{$self->{test_logs}->{$rulename}->{msg}}, $msg;
 }
 
 ###########################################################################
@@ -2947,8 +3250,8 @@ sub get_envelope_from {
   # Assume that because they have configured it, their MTA will always add it.
   # This will prevent us falling through and picking up inappropriate headers.
   if (defined $self->{conf}->{envelope_sender_header}) {
-    # make sure we get the most recent copy - there can be only one EnvelopeSender.
-    $envf = $self->get($self->{conf}->{envelope_sender_header}.":addr",undef);
+    # get the most recent (topmost) copy - there can be only one EnvelopeSender.
+    $envf = ($self->get($self->{conf}->{envelope_sender_header}.":first:addr"))[0];
     # ok if it contains an "@" sign, or is "" (ie. "<>" without the < and >)
     if (defined $envf && (index($envf, '@') > 0 || $envf eq '')) {
       dbg("message: using envelope_sender_header '%s' as EnvelopeFrom: '%s'",
@@ -3001,17 +3304,19 @@ sub get_envelope_from {
   # lines, we cannot trust any Envelope-From headers, since they're likely to
   # be incorrect fetchmail guesses.
 
-  if (index($self->get("X-Sender"), '@') != -1) {
-    my $rcvd = join(' ', $self->get("Received"));
-    if (index($rcvd, '(fetchmail') != -1) {
-      dbg("message: X-Sender and fetchmail signatures found, cannot trust envelope-from");
-      $self->{envelopefrom} = undef;
-      return;
+  my $x_sender = ($self->get("X-Sender:first:addr"))[0];
+  if (defined $x_sender && index($x_sender, '@') != -1) {
+    foreach ($self->get("Received")) {
+      if (index($_, '(fetchmail') != -1) {
+        dbg("message: X-Sender and fetchmail signatures found, cannot trust envelope-from");
+        $self->{envelopefrom} = undef;
+        return;
+      }
     }
   }
 
   # procmailrc notes this (we now recommend adding it to Received instead)
-  if (defined($envf = $self->get("X-Envelope-From:addr",undef))) {
+  if (defined($envf = ($self->get("X-Envelope-From:first:addr"))[0])) {
     # heuristic: this could have been relayed via a list which then used
     # a *new* Envelope-from.  check
     if ($self->get("ALL") =~ /^Received:.*?^X-Envelope-From:/smi) {
@@ -3026,7 +3331,7 @@ sub get_envelope_from {
   }
 
   # qmail, new-inject(1)
-  if (defined($envf = $self->get("Envelope-Sender:addr",undef))) {
+  if (defined($envf = ($self->get("Envelope-Sender:first:addr"))[0])) {
     # heuristic: this could have been relayed via a list which then used
     # a *new* Envelope-from.  check
     if ($self->get("ALL") =~ /^Received:.*?^Envelope-Sender:/smi) {
@@ -3045,7 +3350,7 @@ sub get_envelope_from {
   #   data.  This use of return-path is required; mail systems MUST support
   #   it.  The return-path line preserves the information in the <reverse-
   #   path> from the MAIL command.
-  if (defined($envf = $self->get("Return-Path:addr",undef))) {
+  if (defined($envf = ($self->get("Return-Path:first:addr"))[0])) {
     # heuristic: this could have been relayed via a list which then used
     # a *new* Envelope-from.  check
     if ($self->get("ALL") =~ /^Received:.*?^Return-Path:/smi) {
@@ -3085,7 +3390,7 @@ sub get_all_hdrs_in_rcvd_index_range {
   $include_end_rcvd = 1 unless defined $include_end_rcvd;
 
   my $cur_rcvd_index = -1;  # none found yet
-  my $result = '';
+  my @results;
 
   my @hdrs;
   if ($getraw) {
@@ -3104,14 +3409,20 @@ sub get_all_hdrs_in_rcvd_index_range {
     }
     if ((!defined $start_rcvd || $start_rcvd <= $cur_rcvd_index) &&
        (!defined $end_rcvd || $cur_rcvd_index < $end_rcvd)) {
-      $result .= $hdr;
+      push @results, $hdr;
     }
     elsif (defined $end_rcvd && $cur_rcvd_index == $end_rcvd) {
-      $result .= $hdr;
+      push @results, $hdr;
       last;
     }
   }
-  return ($result eq '' ? undef : $result);
+
+  if (wantarray) {
+    return @results;
+  } else {
+    my $result = join('', @results);
+    return ($result eq '' ? undef : $result);
+  }
 }
 
 ###########################################################################
@@ -3123,27 +3434,32 @@ sub sa_die { Mail::SpamAssassin::sa_die(@_); }
 =item $status->create_fulltext_tmpfile (fulltext_ref)
 
 This function creates a temporary file containing the passed scalar
-reference data (typically the full/pristine text of the message).
-This is typically used by external programs like pyzor and dccproc, to
-avoid hangs due to buffering issues.   Methods that need this, should
-call $self->create_fulltext_tmpfile($fulltext) to retrieve the temporary
-filename; it will be created if it has not already been.
+reference data.  If no scalar is passed, full/pristine message text is
+assumed.  This is typically used by external programs like pyzor and
+dccproc, to avoid hangs due to buffering issues.
 
-Note: This can only be called once until $status->delete_fulltext_tmpfile() is
-called.
+All tempfiles are automatically cleaned up by PerMsgStatus destructor.
 
 =cut
 
 sub create_fulltext_tmpfile {
   my ($self, $fulltext) = @_;
 
-  if (defined $self->{fulltext_tmpfile}) {
-    return $self->{fulltext_tmpfile};
+  my $pristine;
+  if (!defined $fulltext) {
+    if (defined $self->{fulltext_tmpfile}) {
+      return $self->{fulltext_tmpfile};
+    }
+    $fulltext = \$self->{msg}->get_pristine();
+    $pristine = 1;
   }
 
   my ($tmpf, $tmpfh) = Mail::SpamAssassin::Util::secure_tmpfile();
   $tmpfh  or die "failed to create a temporary file";
 
+  # record all created files so we can remove on DESTROY
+  $self->{tmpfiles}->{$tmpf} = 1;
+
   # PerlIO's buffered print writes in 8 kB chunks - which can be slow.
   #   print $tmpfh $$fulltext  or die "error writing to $tmpf: $!";
   #
@@ -3156,30 +3472,33 @@ sub create_fulltext_tmpfile {
   }
   close $tmpfh  or die "error closing $tmpf: $!";
 
-  $self->{fulltext_tmpfile} = $tmpf;
+  $self->{fulltext_tmpfile} = $tmpf  if $pristine;
 
   dbg("check: create_fulltext_tmpfile, written %d bytes to file %s",
       length($$fulltext), $tmpf);
 
-  return $self->{fulltext_tmpfile};
+  return $tmpf;
 }
 
-=item $status->delete_fulltext_tmpfile ()
+=item $status->delete_fulltext_tmpfile (tmpfile)
 
 Will cleanup after a $status->create_fulltext_tmpfile() call.  Deletes the
-temporary file and uncaches the filename.
+temporary file and uncaches the filename.  Generally there no need to call
+this, PerMsgStatus destructor cleans up all tmpfiles.
 
 =cut
 
 sub delete_fulltext_tmpfile {
-  my ($self) = @_;
-  if (defined $self->{fulltext_tmpfile}) {
-    if (!unlink $self->{fulltext_tmpfile}) {
-      my $msg = sprintf("cannot unlink %s: %s", $self->{fulltext_tmpfile}, $!);
-      # don't fuss too much if file is missing, perhaps it wasn't even created
-      if ($! == ENOENT) { warn $msg } else { die $msg }
+  my ($self, $tmpfile) = @_;
+
+  $tmpfile = $self->{fulltext_tmpfile} if !defined $tmpfile;
+  if (defined $tmpfile && $self->{tmpfiles}->{$tmpfile}) {
+    unlink($tmpfile) or dbg("cannot unlink $tmpfile: $!");
+    if ($self->{fulltext_tmpfile} &&
+          $tmpfile eq $self->{fulltext_tmpfile}) {
+      delete $self->{fulltext_tmpfile};
     }
-    $self->{fulltext_tmpfile} = undef;
+    delete $self->{tmpfiles}->{$tmpfile};
   }
 }
 
@@ -3193,26 +3512,27 @@ sub all_from_addrs {
   my @addrs;
 
   # Resent- headers take priority, if present. see bug 672
-  my $resent = $self->get('Resent-From',undef);
-  if (defined $resent && $resent =~ /\S/) {
-    @addrs = $self->{main}->find_all_addrs_in_line ($resent);
+  my @resent = $self->get('Resent-From:first:addr');
+  if (@resent) {
+    @addrs = @resent;
   }
   else {
-    # bug 2292: Used to use find_all_addrs_in_line() with the same
-    # headers, but the would catch addresses in comments which caused
-    # FNs for things like whitelist_from.  Since all of these are From
-    # headers, there should only be 1 address in each anyway (not exactly
-    # true, RFC 2822 allows multiple addresses in a From header field),
-    # so use the :addr code...
+    # bug 2292: Used to use find_all_addrs_in_line() with the same headers,
+    # but the would catch addresses in comments which caused FNs for things
+    # like welcomelist_from.  Since all of these are From headers, there
+    # should only be 1 address in each anyway (not exactly true, RFC 2822
+    # allows multiple addresses in a From header field)
+    # *** since 4.0 all addresses are returned from Header correctly ***
     # bug 3366: some addresses come in as 'foo@bar...', which is invalid.
     # so deal with the multiple periods.
+    # TODO: 4.0 need :first:addr here ? Why check so many headers ?
     ## no critic
     @addrs = map { tr/././s; $_ } grep { $_ ne '' }
-        ($self->get('From:addr'),              # std
-         $self->get('Envelope-Sender:addr'),   # qmail: new-inject(1)
-         $self->get('Resent-Sender:addr'),     # procmailrc manpage
-         $self->get('X-Envelope-From:addr'),   # procmailrc manpage
-         $self->get('EnvelopeFrom:addr'));     # SMTP envelope
+      ($self->get('From:addr'),            # std
+       $self->get('Envelope-Sender:addr'), # qmail: new-inject(1)
+       $self->get('Resent-Sender:addr'),   # procmailrc manpage
+       $self->get('X-Envelope-From:addr'), # procmailrc manpage
+       $self->get('EnvelopeFrom:addr'));   # SMTP envelope
     # http://www.cs.tut.fi/~jkorpela/headers.html is useful here
   }
 
@@ -3270,47 +3590,52 @@ sub all_to_addrs {
   my @addrs;
 
   # Resent- headers take priority, if present. see bug 672
-  my $resent = join('', $self->get('Resent-To'), $self->get('Resent-Cc'));
-  if ($resent =~ /\S/) {
-    @addrs = $self->{main}->find_all_addrs_in_line($resent);
+  my @resent = ( $self->get('Resent-To:first:addr'),
+                 $self->get('Resent-Cc:first:addr') );
+  if (@resent) {
+    @addrs = @resent;
   } else {
     # OK, a fetchmail trick: try to find the recipient address from
     # the most recent 3 Received lines.  This is required for sendmail,
     # since it does not add a helpful header like exim, qmail
     # or Postfix do.
     #
-    my $rcvd = $self->get('Received');
-    $rcvd =~ s/\n[ \t]+/ /gs;
-    $rcvd =~ s/\n+/\n/gs;
-
-    my @rcvdlines = split(/\n/, $rcvd, 4); pop @rcvdlines; # forget last one
+    my @rcvd = ($self->get('Received'))[0 .. 2];
     my @rcvdaddrs;
-    foreach my $line (@rcvdlines) {
-      if ($line =~ / for (\S+\@\S+);/) { push (@rcvdaddrs, $1); }
+    foreach my $line (@rcvd) {
+      next unless defined $line;
+      if ($line =~ / for <?(\S+\@(\S+?))>?;/) {
+        if (is_fqdn_valid(idn_to_ascii($2), 1)) {
+          push @rcvdaddrs, $1;
+        }
+      }
     }
 
-    @addrs = $self->{main}->find_all_addrs_in_line (
-       join('',
-        join(" ", @rcvdaddrs)."\n",
-         $self->get('To'),                     # std
-        $self->get('Apparently-To'),           # sendmail, from envelope
-        $self->get('Delivered-To'),            # Postfix, poss qmail
-        $self->get('Envelope-Recipients'),     # qmail: new-inject(1)
-        $self->get('Apparently-Resent-To'),    # procmailrc manpage
-        $self->get('X-Envelope-To'),           # procmailrc manpage
-        $self->get('Envelope-To'),             # exim
-        $self->get('X-Delivered-To'),          # procmail quick start
-        $self->get('X-Original-To'),           # procmail quick start
-        $self->get('X-Rcpt-To'),               # procmail quick start
-        $self->get('X-Real-To'),               # procmail quick start
-        $self->get('Cc')));                    # std
+    # TODO: 4.0 use :first:addr ? Why so many headers ?
+    @addrs = (
+      @rcvdaddrs,
+      $self->get('To:addr'),                   # std
+      $self->get('Apparently-To:addr'),        # sendmail, from envelope
+      $self->get('Delivered-To:addr'),         # Postfix, poss qmail
+      $self->get('Envelope-Recipients:addr'),  # qmail: new-inject(1)
+      $self->get('Apparently-Resent-To:addr'), # procmailrc manpage
+      $self->get('X-Envelope-To:addr'),        # procmailrc manpage
+      $self->get('Envelope-To:addr'),          # exim
+      $self->get('X-Delivered-To:addr'),       # procmail quick start
+      $self->get('X-Original-To:addr'),        # procmail quick start
+      $self->get('X-Rcpt-To:addr'),            # procmail quick start
+      $self->get('X-Real-To:addr'),            # procmail quick start
+      $self->get('Cc:addr'));                  # std
     # those are taken from various sources; thanks to Nancy McGough, who
     # noted some in <http://www.ii.com/internet/robots/procmail/qs/#envelope>
   }
 
-  dbg("eval: all '*To' addrs: " . join(" ", @addrs));
-  $self->{all_to_addrs} = \@addrs;
-  return @addrs;
+  my %seen;
+  my @result = grep { !$seen{$_}++ } @addrs;
+
+  dbg("eval: all '*To' addrs: " . join(" ", @result));
+  $self->{all_to_addrs} = \@result;
+  return @result;
 
 # http://www.cs.tut.fi/~jkorpela/headers.html is useful here, also
 # http://www.exim.org/pipermail/exim-users/Week-of-Mon-20001009/021672.html
@@ -3318,6 +3643,19 @@ sub all_to_addrs {
 
 ###########################################################################
 
+# Save and tag regex named captures, $captures is ref to %- results
+sub set_captures {
+  my ($self, $captures) = @_;
+
+  foreach my $cname (keys %$captures) {
+    next unless $cname =~ /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*$/; # safety check
+    my @cvals = do { my %seen; grep { !$seen{$_}++ } @{$captures->{$cname}} };
+    $self->set_tag($cname, @cvals == 1 ? $cvals[0] : \@cvals);
+  }
+}
+
+###########################################################################
+
 1;
 __END__
 
index 88a7005ee7917f38384ce52cd8d1d0c1827e33c8..021229fa202faa36049d6e18ce13d5b445141357 100644 (file)
@@ -33,7 +33,7 @@ SpamAssassin will call:
 
 =head1 DESCRIPTION
 
-All persistent address list implementations, used by the auto-whitelist
+All persistent address list implementations, used by the auto-welcomelist
 code to track known-good email addresses, use this as a base class.
 
 See C<Mail::SpamAssassin::DBBasedAddrList> for an example.
@@ -81,7 +81,7 @@ SpamAssassin classes.
 
 sub new_checker {
   my ($factory, $main) = @_;
-  die "auto-whitelist: unimplemented base method";     # override this
+  die "auto-welcomelist: unimplemented base method";   # override this
 }
 
 ###########################################################################
@@ -109,7 +109,7 @@ a C<count> key and a C<totscore> key.
 sub get_addr_entry {
   my ($self, $addr, $signedby) = @_;
   my $entry = { };
-  die "auto-whitelist: unimplemented base method";     # override this
+  die "auto-welcomelist: unimplemented base method";   # override this
   return $entry;
 }
 
@@ -117,27 +117,27 @@ sub get_addr_entry {
 
 =item $entry = $addrlist->add_score($entry, $score);
 
-This method should add the given score to the whitelist database for the
+This method should add the given score to the welcomelist database for the
 given entry, and then return the new entry.
 
 =cut
 
 sub add_score {
     my ($self, $entry, $score) = @_;
-    die "auto-whitelist: unimplemented base method"; # override this
+    die "auto-welcomelist: unimplemented base method"; # override this
 }
 
 ###########################################################################
 
 =item $entry = $addrlist->remove_entry ($entry);
 
-This method should remove the given entry from the whitelist database.
+This method should remove the given entry from the welcomelist database.
 
 =cut
 
 sub remove_entry {
   my ($self, $entry) = @_;
-  die "auto-whitelist: unimplemented base method";     # override this
+  die "auto-welcomelist: unimplemented base method";   # override this
 }
 
 ###########################################################################
@@ -145,7 +145,7 @@ sub remove_entry {
 =item $entry = $addrlist->finish ();
 
 Clean up, if necessary.  Called by SpamAssassin when it has finished
-checking, or adding to, the auto-whitelist database.
+checking, or adding to, the auto-welcomelist database.
 
 =cut
 
index 1e0daf01b041883469b7a2c0459bc3db3ff520dc..5493cee5abeba6678dbbab447976afb4cf7c5728 100644 (file)
@@ -427,6 +427,20 @@ The C<Mail::SpamAssassin::PerMsgStatus> context object for this scan.
 
 =back
 
+=item $plugin->check_dnsbl ( { options ... } )
+
+Called when DNSBL or other network lookups are being launched, implying
+current running priority of -100.  This is the place to start your own
+asynchronously-started network lookups.
+
+=over 4
+
+=item permsgstatus
+
+The C<Mail::SpamAssassin::PerMsgStatus> context object for this scan.
+
+=back
+
 =item $plugin->check_post_dnsbl ( { options ... } )
 
 Called after the DNSBL results have been harvested.  This is a good
@@ -440,6 +454,21 @@ The C<Mail::SpamAssassin::PerMsgStatus> context object for this scan.
 
 =back
 
+=item $plugin->check_cleanup ( { options ... } )
+
+Called just before message check is finishing and before possible
+auto-learning.  This is guaranteed to be always called, unlike check_tick
+and check_post_dnsbl.  Used for cleaning up left callbacks or forked
+children etc, last chance to make rules hit.
+
+=over 4
+
+=item permsgstatus
+
+The C<Mail::SpamAssassin::PerMsgStatus> context object for this scan.
+
+=back
+
 =item $plugin->check_post_learn ( { options ... } )
 
 Called after auto-learning may (or may not) have taken place.  If you
@@ -817,7 +846,9 @@ Reference to the original message object.
 
 =back
 
-=item $plugin->whitelist_address( { options ... } )
+=item $plugin->welcomelist_address( { options ... } )
+
+Previously whitelist_address which will work interchangeably until 4.1.
 
 Called when a request is made to add an address to a
 persistent address list.
@@ -834,7 +865,9 @@ Indicate if the call is being made from a command line interface.
 
 =back
 
-=item $plugin->blacklist_address( { options ... } )
+=item $plugin->blocklist_address( { options ... } )
+
+Previously blacklist_address which will work interchangeably until 4.1.
 
 Called when a request is made to add an address to a
 persistent address list.
@@ -882,7 +915,7 @@ only spamd calls this API.
 =item result
 
 The C<'result: ...'> line for this scan.  Format is as described
-at B<http://wiki.apache.org/spamassassin/SpamdSyslogFormat>.
+at B<https://wiki.apache.org/spamassassin/SpamdSyslogFormat>.
 
 =back
 
@@ -1013,17 +1046,27 @@ to receive specific events, or control the callback chain behaviour.
 
 =over 4
 
-=item $plugin->register_eval_rule ($nameofevalsub)
+=item $plugin->register_eval_rule ($nameofevalsub, $ruletype)
 
 Plugins that implement an eval test will need to call this, so that
 SpamAssassin calls into the object when that eval test is encountered.
 See the B<REGISTERING EVAL RULES> section for full details.
 
+Since 4.0, optional $ruletype can be specified to enforce that eval function
+cannot be called with wrong ruletype from configuration, for example user
+using "header FOO eval:foobar()" instead of "body FOO eval:foobar()". 
+Mismatch will result in lint failure. $ruletype can be one of:
+
+  $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS
+  $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS  (allows both body and rawbody)
+  $Mail::SpamAssassin::Conf::TYPE_RAWBODY_EVALS
+  $Mail::SpamAssassin::Conf::TYPE_FULL_EVALS
+
 =cut
 
 sub register_eval_rule {
-  my ($self, $nameofsub) = @_;
-  $self->{main}->{conf}->register_eval_rule ($self, $nameofsub);
+  my ($self, $nameofsub, $ruletype) = @_;
+  $self->{main}->{conf}->register_eval_rule ($self, $nameofsub, $ruletype);
 }
 
 =item $plugin->register_generated_rule_method ($nameofsub)
@@ -1132,7 +1175,7 @@ called from rules in the configuration files, in the plugin class' constructor.
 
 For example,
 
-  $plugin->register_eval_rule ('check_for_foo')
+  $plugin->register_eval_rule ('check_for_foo', $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS)
 
 will cause C<$plugin-E<gt>check_for_foo()> to be called for this
 SpamAssassin rule:
@@ -1157,18 +1200,21 @@ In other words, the eval test method should look something like this:
 
   sub check_for_foo {
     my ($self, $permsgstatus, ...arguments...) = @_;
-    ...code returning 0 or 1
+    ...code returning 0 (miss), 1 (hit), or undef (async function)
   }
 
+The eval rule should return C<1> for a hit, or C<0> if the rule is not hit. 
+Special case of "return undef" must be used when result is not yet ready and
+it will be later declared with PerMsgStatus functions got_hit() or
+rule_ready() - see their documentation for more info.  Make sure not to
+return undef by mistake.
+
 Note that the headers can be accessed using the C<get()> method on the
 C<Mail::SpamAssassin::PerMsgStatus> object, and the body by
 C<get_decoded_stripped_body_text_array()> and other similar methods.
 Similarly, the C<Mail::SpamAssassin::Conf> object holding the current
 configuration may be accessed through C<$permsgstatus-E<gt>{main}-E<gt>{conf}>.
 
-The eval rule should return C<1> for a hit, or C<0> if the rule
-is not hit.
-
 State for a single message being scanned should be stored on the C<$permsgstatus>
 object, not on the C<$self> object, since C<$self> persists between scan
 operations.  See the 'lifecycle note' on the C<check_start()> method above.
@@ -1216,8 +1262,8 @@ Mail::SpamAssassin(3)
 
 Mail::SpamAssassin::PerMsgStatus(3)
 
-http://wiki.apache.org/spamassassin/PluginWritingTips
+https://wiki.apache.org/spamassassin/PluginWritingTips
 
-http://issues.apache.org/SpamAssassin/show_bug.cgi?id=2163
+https://issues.apache.org/SpamAssassin/show_bug.cgi?id=2163
 
 =cut
index bcab0c2b3f0061bfcc3f23544c067650f58449f5..b8a18619508dfa10bac3f129dc06454b7a59e5d0 100644 (file)
@@ -50,13 +50,30 @@ Autonomous System Number (ASN) of the connecting IP address.
 
  loadplugin Mail::SpamAssassin::Plugin::ASN
 
+ # Default / recommended settings
+ asn_use_geodb 1
+ asn_use_dns 1
+ asn_prefer_geodb 1
+
+ # Do lookups and add tags / X-Spam-ASN header
  asn_lookup asn.routeviews.org _ASN_ _ASNCIDR_
  asn_lookup_ipv6 origin6.asn.cymru.com _ASN_ _ASNCIDR_
-
  add_header all ASN _ASN_ _ASNCIDR_
 
- header TEST_AS1234 X-ASN =~ /^1234$/
+ # Rules to test ASN or Organization
+ # NOTE: Do not use rules that check metadata X-ASN header,
+ # only check_asn() eval function works correctly.
+ # Rule argument is full regexp to match.
+
+ # ASN Number: GeoIP ASN or DNS
+ # Matched string includes asn_prefix if defined, and normally
+ # looks like "AS1234" (DNS) or "AS1234 Google LLC" (GeoIP)
+ header AS_1234 eval:check_asn('/^AS1234\b/')
+
+ # ASN Organisation: GeoIP ASN has, DNS lists might not have
+ # Note the second parameter which checks MYASN tag (default is ASN)
+ asn_lookup myview.example.com _MYASN_ _MYASNCIDR_
+ header AS_GOOGLE eval:check_asn('/\bGoogle\b/i', 'MYASN')
 
 =head1 DESCRIPTION
 
@@ -68,12 +85,17 @@ high-volume environment> or that you should use a local mirror of the
 zone (see C<ftp://ftp.routeviews.org/dnszones/>).  Other similar zones
 may also be used.
 
+GeoDB (GeoIP ASN) database lookups are supported since SpamAssassin 4.0 and
+it's recommended to use them instead of DNS queries, unless C<_ASNCIDR_>
+is needed.
+
 =head1 TEMPLATE TAGS
 
 This plugin allows you to create template tags containing the connecting
 IP's AS number and route info for that AS number.
 
-The default config will add a header field that looks like this:
+If you use add_header as documented in the example before, a header field is
+added that looks like this:
 
  X-Spam-ASN: AS24940 213.239.192.0/18
 
@@ -87,29 +109,19 @@ all be added to the C<_ASNCIDR_> tag, separated by spaces, eg:
 Note that the literal "AS" before the ASN in the _ASN_ tag is configurable
 through the I<asn_prefix> directive and may be set to an empty string.
 
-=head1 CONFIGURATION
+C<_ASNCIDR_> is not available with local GeoDB ASN lookups.
 
-The standard ruleset contains a configuration that will add a header field
-containing ASN data to scanned messages.  The bayes tokenizer will use the
-added header field for bayes calculations, and thus affect which BAYES_* rule
-will trigger for a particular message.
+=head1 BAYES
 
-B<Note> that in most cases you should not score on the ASN data directly.
-Bayes learning will probably trigger on the _ASNCIDR_ tag, but probably not
-very well on the _ASN_ tag alone.
+The bayes tokenizer will use ASN data for bayes calculations, and thus
+affect which BAYES_* rule will trigger for a particular message.  No
+in-depth analysis of the usefulness of bayes tokenization of ASN data has
+been performed.
 
 =head1 SEE ALSO
 
 http://www.routeviews.org/ - all data regarding routing, ASNs, etc....
 
-http://issues.apache.org/SpamAssassin/show_bug.cgi?id=4770 -
-SpamAssassin Issue #4770 concerning this plugin
-
-=head1 STATUS
-
-No in-depth analysis of the usefulness of bayes tokenization of ASN data has
-been performed.
-
 =cut
 
 package Mail::SpamAssassin::Plugin::ASN;
@@ -120,27 +132,23 @@ use re 'taint';
 
 use Mail::SpamAssassin::Plugin;
 use Mail::SpamAssassin::Logger;
-use Mail::SpamAssassin::Util qw(reverse_ip_address);
-use Mail::SpamAssassin::Dns;
+use Mail::SpamAssassin::Util qw(reverse_ip_address compile_regexp);
 use Mail::SpamAssassin::Constants qw(:ip);
 
 our @ISA = qw(Mail::SpamAssassin::Plugin);
 
-our $txtdata_can_provide_a_list;
-
-my $IPV4_ADDRESS = IPV4_ADDRESS;
-
 sub new {
   my ($class, $mailsa) = @_;
   $class = ref($class) || $class;
   my $self = $class->SUPER::new($mailsa);
   bless ($self, $class);
 
+  $self->register_eval_rule("check_asn", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+
   $self->set_config($mailsa->{conf});
 
-  #$txtdata_can_provide_a_list = Net::DNS->VERSION >= 0.69;
-  #more robust version check from Damyan Ivanov - Bug 7095
-  $txtdata_can_provide_a_list = version->parse(Net::DNS->VERSION) >= version->parse('0.69');
+  # we need GeoDB ASN
+  $self->{main}->{geodb_wanted}->{asn} = 1;
 
   return $self;
 }
@@ -201,6 +209,25 @@ is kept for backward compatibility with versions of SpamAssassin earlier
 than 3.4.0. A sensible setting is an empty string. The argument may be (but
 need not be) enclosed in single or double quotes for clarity.
 
+=item asn_use_geodb ( 0 / 1 )          (default: 1)
+
+Use Mail::SpamAssassin::GeoDB module to lookup ASN numbers.  You need
+suitable supported module like GeoIP2 or GeoIP with ISP or ASN database
+installed (for example, add EditionIDs GeoLite2-ASN in GeoIP.conf for
+geoipupdate program).
+
+GeoDB can only set _ASN_ tag, it has no data for _ASNCIDR_.  If you need
+both, then set asn_prefer_geodb 0 so DNS rules are tried.
+
+=item asn_prefer_geodb ( 0 / 1 )       (default: 1)
+
+If set, DNS lookups (asn_lookup rules) will not be run if GeoDB successfully
+finds ASN. Set this to 0 to get _ASNCIDR_ even if GeoDB finds _ASN_.
+
+=item asn_use_dns ( 0 / 1 )            (default: 1)
+
+Set to 0 to never allow DNS queries.
+
 =back
 
 =cut
@@ -272,25 +299,52 @@ need not be) enclosed in single or double quotes for clarity.
     }
   });
 
+  push (@cmds, {
+    setting => 'asn_use_geodb',
+    default => 1,
+    is_admin => 1,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
+  });
+
+  push (@cmds, {
+    setting => 'asn_prefer_geodb',
+    default => 1,
+    is_admin => 1,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
+  });
+
+  push (@cmds, {
+    setting => 'asn_use_dns',
+    default => 1,
+    is_admin => 1,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
+  });
+
   $conf->{parser}->register_commands(\@cmds);
 }
 
 # ---------------------------------------------------------------------------
 
-sub parsed_metadata {
+sub extract_metadata {
   my ($self, $opts) = @_;
 
   my $pms = $opts->{permsgstatus};
-  my $conf = $self->{main}->{conf};
+  my $conf = $pms->{conf};
 
-  if (!$pms->is_dns_available()) {
-    dbg("asn: DNS is not available, skipping ASN checks");
-    return;
-  }
-
-  if (!$conf->{asnlookups} && !$conf->{asnlookups_ipv6}) {
-    dbg("asn: no asn_lookups configured, skipping ASN lookups");
-    return;
+  my $geodb = $self->{main}->{geodb};
+  my $has_geodb = $conf->{asn_use_geodb} && $geodb && $geodb->can('asn');
+  if ($has_geodb) {
+    dbg("asn: using GeoDB ASN for lookups");
+  } else {
+    dbg("asn: GeoDB ASN not available");
+    if (!$conf->{asn_use_dns} || !$pms->is_dns_available()) {
+      dbg("asn: DNS is not available, skipping ASN check");
+      return;
+    }
+    if ($self->{main}->{learning}) {
+      dbg("asn: learning message, skipping DNS-based ASN check");
+      return;
+    }
   }
 
   # initialize the tag data so that if no result is returned from the DNS
@@ -309,9 +363,11 @@ sub parsed_metadata {
     }
   }
 
-  # get reversed IP address of last external relay to lookup
-  # don't return until we've initialized the template tags
-  my $relay = $pms->{relays_external}->[0];
+  # Initialize status
+  $pms->{asn_results} = ();
+
+  # get IP address of last external relay to lookup
+  my $relay = $opts->{msg}->{metadata}->{relays_external}->[0];
   if (!defined $relay) {
     dbg("asn: no first external relay IP available, skipping ASN check");
     return;
@@ -319,18 +375,45 @@ sub parsed_metadata {
     dbg("asn: first external relay is a private IP, skipping ASN check");
     return;
   }
-
   my $ip = $relay->{ip};
-  my $reversed_ip = reverse_ip_address($ip);
-  if (defined $reversed_ip) {
-    dbg("asn: using first external relay IP for lookups: %s", $ip);
-  } else {
-    dbg("asn: could not parse first external relay IP: %s, skipping", $ip);
+  dbg("asn: using first external relay IP for lookups: %s", $ip);
+
+  # GeoDB lookup
+  my $asn_found;
+  if ($has_geodb) {
+    my $asn = $geodb->get_asn($ip);
+    my $org = $geodb->get_asn_org($ip);
+    if (!defined $asn) {
+      dbg("asn: GeoDB ASN lookup failed");
+    } else {
+      $asn_found = 1;
+      dbg("asn: GeoDB found ASN $asn");
+      # Prevent double prefix
+      my $asn_value =
+        length($conf->{asn_prefix}) && index($asn, $conf->{asn_prefix}) != 0 ?
+          $conf->{asn_prefix}.$asn : $asn;
+      $asn_value .= ' '.$org if defined $org && length($org);
+      $pms->set_tag('ASN', $asn_value);
+      # For Bayes
+      $pms->{msg}->put_metadata('X-ASN', $asn);
+    }
+  }
+
+  # Skip DNS if GeoDB was successful and preferred
+  if ($asn_found && $conf->{asn_prefer_geodb}) {
+    dbg("asn: GeoDB lookup successful, skipping DNS lookups");
+    return;
+  }
+
+  # No point continuing without DNS from now on
+  if (!$conf->{asn_use_dns} || !$pms->is_dns_available()) {
+    dbg("asn: skipping disabled DNS lookups");
     return;
   }
 
+  dbg("asn: using DNS for lookups");
   my $lookup_zone;
-  if ($ip =~ /^$IPV4_ADDRESS$/o) {
+  if ($ip =~ IS_IPV4_ADDRESS) {
     if (!defined $conf->{asnlookups}) {
       dbg("asn: asn_lookup for IPv4 not defined, skipping");
       return;
@@ -344,6 +427,12 @@ sub parsed_metadata {
     $lookup_zone = "asnlookups_ipv6";
   }
   
+  my $reversed_ip = reverse_ip_address($ip);
+  if (!defined $reversed_ip) {
+    dbg("asn: could not parse IP: %s, skipping", $ip);
+    return;
+  }
+
   # we use arrays and array indices rather than hashes and hash keys
   # in case someone wants the same zone added to multiple sets of tags
   my $index = 0;
@@ -351,14 +440,12 @@ sub parsed_metadata {
     # do the DNS query, have the callback process the result
     my $zone_index = $index;
     my $zone = $reversed_ip . '.' . $entry->{zone};
-    my $key = "asnlookup-${lookup_zone}-${zone_index}-".$entry->{zone};
-    my $ent = $pms->{async}->bgsend_and_start_lookup($zone, 'TXT', undef,
-      { type => 'ASN', key => $key, zone => $lookup_zone },
+    $pms->{async}->bgsend_and_start_lookup($zone, 'TXT', undef,
+      { rulename => 'asn_lookup', type => 'ASN' },
       sub { my($ent, $pkt) = @_;
             $self->process_dns_result($pms, $pkt, $zone_index, $lookup_zone) },
       master_deadline => $pms->{master_deadline}
     );
-    $pms->register_async_rule_start($key) if $ent;
     $index++;
   }
 }
@@ -376,6 +463,9 @@ sub parsed_metadata {
 sub process_dns_result {
   my ($self, $pms, $pkt, $zone_index, $lookup_zone) = @_;
 
+  # NOTE: $pkt will be undef if the DNS query was aborted (e.g. timed out)
+  return if !$pkt;
+
   my $conf = $self->{main}->{conf};
 
   my $zone = $conf->{$lookup_zone}[$zone_index]->{zone};
@@ -403,14 +493,10 @@ sub process_dns_result {
     %route_tag_data_seen = map(($_,1), @route_tag_data);
   }
 
-  # NOTE: $pkt will be undef if the DNS query was aborted (e.g. timed out)
-  my @answer = !defined $pkt ? () : $pkt->answer;
-
-  foreach my $rr (@answer) {
+  foreach my $rr ($pkt->answer) {
     #dbg("asn: %s: lookup result packet: %s", $zone, $rr->string);
     next if $rr->type ne 'TXT';
-    my @strings = $txtdata_can_provide_a_list ? $rr->txtdata :
-      $rr->char_str_list; # historical
+    my @strings = $rr->txtdata;
     next if !@strings;
     for (@strings) { utf8::encode($_) if utf8::is_utf8($_) }
 
@@ -485,7 +571,49 @@ sub process_dns_result {
   }
 }
 
+sub check_asn {
+  my ($self, $pms, $re, $asn_tag) = @_;
+
+  my $rulename = $pms->get_current_eval_rule_name();
+  if (!defined $re) {
+    warn "asn: rule $rulename eval argument missing\n";
+    return 0;
+  }
+
+  my ($rec, $err) = compile_regexp($re, 2);
+  if (!$rec) {
+    warn "asn: invalid regexp for $rulename '$re': $err\n";
+    return 0;
+  }
+
+  $asn_tag = 'ASN' unless defined $asn_tag;
+  $pms->action_depends_on_tags($asn_tag,
+    sub { my($pms,@args) = @_;
+      $self->_check_asn($pms, $rulename, $rec, $asn_tag);
+    }
+  );
+
+  return; # return undef for async status
+}
+
+sub _check_asn {
+  my ($self, $pms, $rulename, $rec, $asn_tag) = @_;
+
+  $pms->rule_ready($rulename); # mark rule ready for metas
+
+  my $asn = $pms->get_tag($asn_tag);
+  return if !defined $asn;
+
+  if ($asn =~ $rec) {
+    $pms->test_log("$asn_tag: $asn", $rulename);
+    $pms->got_hit($rulename, "");
+  }
+}
+
 # Version features
 sub has_asn_lookup_ipv6 { 1 }
+sub has_asn_geodb { 1 }
+sub has_check_asn { 1 }
+sub has_check_asn_tag { 1 } # $asn_tag parameter for check_asn()
 
 1;
index 1fe201efc1af1439d559007466adc56e9bbe8efd..874e44360539dc1a326ee21d1aa3a02c203145f6 100644 (file)
@@ -17,7 +17,7 @@
 
 =head1 NAME
 
-Mail::SpamAssassin::Plugin::AWL - Normalize scores via auto-whitelist
+Mail::SpamAssassin::Plugin::AWL - Normalize scores via auto-welcomelist
 
 =head1 SYNOPSIS
 
@@ -28,14 +28,14 @@ To try this out, add this or uncomment this line in init.pre:
 Use the supplied 60_awl.cf file (ie you don't have to do anything) or
 add these lines to a .cf file:
 
-  header AWL             eval:check_from_in_auto_whitelist()
-  describe AWL           From: address is in the auto white-list
+  header AWL             eval:check_from_in_auto_welcomelist()
+  describe AWL           From: address is in the auto welcome-list
   tflags AWL             userconf noautolearn
   priority AWL           1000
 
 =head1 DESCRIPTION
 
-This plugin module provides support for the auto-whitelist.  It keeps
+This plugin module provides support for the auto-welcomelist.  It keeps
 track of the average SpamAssassin score for senders.  Senders are
 tracked using a combination of their From: address and their IP address.
 It then uses that average score to reduce the variability in scoring
@@ -63,7 +63,7 @@ use warnings;
 # use bytes;
 use re 'taint';
 use Mail::SpamAssassin::Plugin;
-use Mail::SpamAssassin::AutoWhitelist;
+use Mail::SpamAssassin::AutoWelcomelist;
 use Mail::SpamAssassin::Util qw(untaint_var);
 use Mail::SpamAssassin::Logger;
 
@@ -80,7 +80,8 @@ sub new {
   bless ($self, $class);
 
   # the important bit!
-  $self->register_eval_rule("check_from_in_auto_whitelist");
+  $self->register_eval_rule("check_from_in_auto_welcomelist", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_from_in_auto_whitelist", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS); # removed in 4.1
 
   $self->set_config($mailsaobject->{conf});
 
@@ -99,18 +100,20 @@ SpamAssassin handles incoming email messages.
 
 =over 4
 
-=item use_auto_whitelist ( 0 | 1 )             (default: 1)
+=item use_auto_welcomelist ( 0 | 1 )           (default: 1)
 
-Whether to use auto-whitelists.  Auto-whitelists track the long-term
+Previously use_auto_whitelist which will work interchangeably until 4.1.
+
+Whether to use auto-welcomelists.  Auto-welcomelists track the long-term
 average score for each sender and then shift the score of new messages
 toward that long-term average.  This can increase or decrease the score
 for messages, depending on the long-term behavior of the particular
 correspondent.
 
-For more information about the auto-whitelist system, please look
-at the C<Automatic Whitelist System> section of the README file.
-The auto-whitelist is not intended as a general-purpose replacement
-for static whitelist entries added to your config files.
+For more information about the auto-welcomelist system, please look
+at the C<Automatic Welcomelist System> section of the README file.
+The auto-welcomelist is not intended as a general-purpose replacement
+for static welcomelist entries added to your config files.
 
 Note that certain tests are ignored when determining the final
 message score:
@@ -120,12 +123,15 @@ message score:
 =cut
 
   push (@cmds, {
-               setting => 'use_auto_whitelist',
+               setting => 'use_auto_welcomelist',
+               aliases => ['use_auto_whitelist'], # backward compatible - to be removed for 4.1
                default => 1,
                type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL
               });
 
-=item auto_whitelist_factor n  (default: 0.5, range [0..1])
+=item auto_welcomelist_factor n        (default: 0.5, range [0..1])
+
+Previously auto_whitelist_factor which will work interchangeably until 4.1.
 
 How much towards the long-term mean for the sender to regress a message.
 Basically, the algorithm is to track the long-term mean score of messages for
@@ -143,12 +149,15 @@ mean; C<factor> = 0 mean just use the calculated score.
 =cut
 
   push (@cmds, {
-               setting => 'auto_whitelist_factor',
+               setting => 'auto_welcomelist_factor',
+               aliases => ['auto_whitelist_factor'], # backward compatible - to be removed for 4.1
                default => 0.5,
                type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
               });
 
-=item auto_whitelist_ipv4_mask_len n   (default: 16, range [0..32])
+=item auto_welcomelist_ipv4_mask_len n (default: 16, range [0..32])
+
+Previously auto_whitelist_ipv4_mask_len which will work interchangeably until 4.1.
 
 The AWL database keeps only the specified number of most-significant bits
 of an IPv4 address in its fields, so that different individual IP addresses
@@ -164,7 +173,8 @@ of 8, any split is allowed.
 =cut
 
   push (@cmds, {
-               setting => 'auto_whitelist_ipv4_mask_len',
+               setting => 'auto_welcomelist_ipv4_mask_len',
+               aliases => ['auto_whitelist_ipv4_mask_len'], # removed in 4.1
                default => 16,
                type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
                code => sub {
@@ -174,11 +184,13 @@ of 8, any split is allowed.
                  } elsif ($value !~ /^\d+$/ || $value < 0 || $value > 32) {
                    return $Mail::SpamAssassin::Conf::INVALID_VALUE;
                  }
-                 $self->{auto_whitelist_ipv4_mask_len} = $value;
+                 $self->{auto_welcomelist_ipv4_mask_len} = $value;
                }
               });
 
-=item auto_whitelist_ipv6_mask_len n   (default: 48, range [0..128])
+=item auto_welcomelist_ipv6_mask_len n (default: 48, range [0..128])
+
+Previously auto_whitelist_ipv6_mask_len which will work interchangeably until 4.1.
 
 The AWL database keeps only the specified number of most-significant bits
 of an IPv6 address in its fields, so that different individual IP addresses
@@ -195,7 +207,8 @@ is allowed.
 =cut
 
   push (@cmds, {
-               setting => 'auto_whitelist_ipv6_mask_len',
+               setting => 'auto_welcomelist_ipv6_mask_len',
+               aliases => ['auto_whitelist_ipv6_mask_len'], # removed in 4.1
                default => 48,
                type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
                code => sub {
@@ -205,7 +218,7 @@ is allowed.
                  } elsif ($value !~ /^\d+$/ || $value < 0 || $value > 128) {
                    return $Mail::SpamAssassin::Conf::INVALID_VALUE;
                  }
-                 $self->{auto_whitelist_ipv6_mask_len} = $value;
+                 $self->{auto_welcomelist_ipv6_mask_len} = $value;
                }
               });
 
@@ -215,7 +228,7 @@ Used by the SQLBasedAddrList storage implementation.
 
 If this option is set the SQLBasedAddrList module will override the set
 username with the value given.  This can be useful for implementing global
-or group based auto-whitelist databases.
+or group based auto-welcomelist databases.
 
 =cut
 
@@ -225,7 +238,9 @@ or group based auto-whitelist databases.
                type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING
               });
 
-=item auto_whitelist_distinguish_signed
+=item auto_welcomelist_distinguish_signed
+
+Previously auto_whitelist_distinguish_signed which will work interchangeably until 4.1.
 
 Used by the SQLBasedAddrList storage implementation.
 
@@ -240,7 +255,8 @@ turning on this option.
 =cut
 
   push (@cmds, {
-               setting => 'auto_whitelist_distinguish_signed',
+               setting => 'auto_welcomelist_distinguish_signed',
+               aliases => ['auto_whitelist_distinguish_signed'], # removed in 4.1
                default => 0,
                type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL
               });
@@ -256,32 +272,38 @@ user's C<user_prefs> file.
 
 =over 4
 
-=item auto_whitelist_factory module (default: Mail::SpamAssassin::DBBasedAddrList)
+=item auto_welcomelist_factory module (default: Mail::SpamAssassin::DBBasedAddrList)
 
-Select alternative whitelist factory module.
+Previously auto_whitelist_factory which will work interchangeably until 4.1.
+
+Select alternative welcomelist factory module.
 
 =cut
 
   push (@cmds, {
-               setting => 'auto_whitelist_factory',
+               setting => 'auto_welcomelist_factory',
+               aliases => ['auto_whitelist_factory'], # removed in 4.1
                is_admin => 1,
                default => 'Mail::SpamAssassin::DBBasedAddrList',
                type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING
               });
 
-=item auto_whitelist_path /path/filename (default: ~/.spamassassin/auto-whitelist)
+=item auto_welcomelist_path /path/filename (default: ~/.spamassassin/auto-welcomelist)
+
+Previously auto_whitelist_path which will work interchangeably until 4.1.
 
-This is the automatic-whitelist directory and filename.  By default, each user
-has their own whitelist database in their C<~/.spamassassin> directory with
+This is the automatic-welcomelist directory and filename.  By default, each user
+has their own welcomelist database in their C<~/.spamassassin> directory with
 mode 0700.  For system-wide SpamAssassin use, you may want to share this
 across all users, although that is not recommended.
 
 =cut
 
   push (@cmds, {
-               setting => 'auto_whitelist_path',
+               setting => 'auto_welcomelist_path',
+               aliases => ['auto_whitelist_path'], # removed in 4.1
                is_admin => 1,
-               default => '__userstate__/auto-whitelist',
+               default => '__userstate__/auto-welcomelist',
                type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
                code => sub {
                  my ($self, $key, $value, $line) = @_;
@@ -291,13 +313,15 @@ across all users, although that is not recommended.
                  if (-d $value) {
                    return $Mail::SpamAssassin::Conf::INVALID_VALUE;
                  }
-                 $self->{auto_whitelist_path} = $value;
+                 $self->{auto_welcomelist_path} = $value;
                }
               });
 
-=item auto_whitelist_db_modules Module ...     (default: see below)
+=item auto_welcomelist_db_modules Module ...   (default: see below)
 
-What database modules should be used for the auto-whitelist storage database
+Previously auto_whitelist_db_modules which will work interchangeably until 4.1.
+
+What database modules should be used for the auto-welcomelist storage database
 file.   The first named module that can be loaded from the perl include path
 will be used.  The format is:
 
@@ -313,15 +337,18 @@ preclude its use for the AWL (see SpamAssassin bug 4353).
 =cut
 
   push (@cmds, {
-               setting => 'auto_whitelist_db_modules',
+               setting => 'auto_welcomelist_db_modules',
+               aliases => ['auto_whitelist_db_modules'], # removed in 4.1
                is_admin => 1,
                default => 'DB_File GDBM_File SDBM_File',
                type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING
               });
 
-=item auto_whitelist_file_mode         (default: 0700)
+=item auto_welcomelist_file_mode               (default: 0700)
+
+Previously auto_whitelist_file_mode which will work interchangeably until 4.1.
 
-The file mode bits used for the automatic-whitelist directory or file.
+The file mode bits used for the automatic-welcomelist directory or file.
 
 Make sure you specify this using the 'x' mode bits set, as it may also be used
 to create directories.  However, if a file is created, the resulting file will
@@ -330,7 +357,8 @@ not have any execute bits set (the umask is set to 0111).
 =cut
 
   push (@cmds, {
-               setting => 'auto_whitelist_file_mode',
+               setting => 'auto_welcomelist_file_mode',
+               aliases => ['auto_whitelist_file_mode'], # removed in 4.1
                is_admin => 1,
                default => '0700',
                type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
@@ -339,7 +367,7 @@ not have any execute bits set (the umask is set to 0111).
                  if ($value !~ /^0?[0-7]{3}$/) {
                     return $Mail::SpamAssassin::Conf::INVALID_VALUE;
                   }
-                 $self->{auto_whitelist_file_mode} = untaint_var($value);
+                 $self->{auto_welcomelist_file_mode} = untaint_var($value);
                }
               });
 
@@ -390,7 +418,7 @@ The password for the database username, for the above DSN.
 
 Used by the SQLBasedAddrList storage implementation.
 
-The table user auto-whitelists are stored in, for the above DSN.
+The table user auto-welcomelists are stored in, for the above DSN.
 
 =cut
 
@@ -404,15 +432,15 @@ The table user auto-whitelists are stored in, for the above DSN.
   $conf->{parser}->register_commands(\@cmds);
 }
 
-sub check_from_in_auto_whitelist {
+sub check_from_in_auto_welcomelist {
     my ($self, $pms) = @_;
 
-    return 0 unless ($pms->{conf}->{use_auto_whitelist});
+    return 0 unless ($pms->{conf}->{use_auto_welcomelist});
 
     my $timer = $self->{main}->time_method("total_awl");
 
     my $from = lc $pms->get('From:addr');
-  # dbg("auto-whitelist: From: $from");
+  # dbg("auto-welcomelist: From: $from");
     return 0 unless $from =~ /\S/;
 
     # find the earliest usable "originating IP".  ignore private nets
@@ -443,18 +471,18 @@ sub check_from_in_auto_whitelist {
     my $awlpoints = (sprintf "%0.3f", $points) + 0;
 
    # Create the AWL object
-    my $whitelist;
+    my $welcomelist;
     eval {
-      $whitelist = Mail::SpamAssassin::AutoWhitelist->new($pms->{main});
+      $welcomelist = Mail::SpamAssassin::AutoWelcomelist->new($pms->{main});
 
       my $meanscore;
       { # check
         my $timer = $self->{main}->time_method("check_awl");
-        $meanscore = $whitelist->check_address($from, $origip, $signedby);
+        $meanscore = $welcomelist->check_address($from, $origip, $signedby);
       }
       my $delta = 0;
 
-      dbg("auto-whitelist: AWL active, pre-score: %s, autolearn score: %s, ".
+      dbg("auto-welcomelist: AWL active, pre-score: %s, autolearn score: %s, ".
          "mean: %s, IP: %s, address: %s %s",
           $pms->{score}, $awlpoints,
           !defined $meanscore ? 'undef' : sprintf("%.3f",$meanscore),
@@ -463,13 +491,13 @@ sub check_from_in_auto_whitelist {
 
       if (defined $meanscore) {
        $delta = $meanscore - $awlpoints;
-       $delta *= $pms->{main}->{conf}->{auto_whitelist_factor};
+       $delta *= $pms->{main}->{conf}->{auto_welcomelist_factor};
       
        $pms->set_tag('AWL', sprintf("%2.1f",$delta));
         if (defined $meanscore) {
          $pms->set_tag('AWLMEAN', sprintf("%2.1f", $meanscore));
        }
-       $pms->set_tag('AWLCOUNT', sprintf("%2.1f", $whitelist->count()));
+       $pms->set_tag('AWLCOUNT', sprintf("%2.1f", $welcomelist->count()));
        $pms->set_tag('AWLPRESCORE', sprintf("%2.1f", $pms->{score}));
       }
 
@@ -478,7 +506,7 @@ sub check_from_in_auto_whitelist {
       # later ones.  http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=159704
       if (!$pms->{disable_auto_learning}) {
         my $timer = $self->{main}->time_method("update_awl");
-       $whitelist->add_score($awlpoints);
+       $welcomelist->add_score($awlpoints);
       }
 
       # now redundant, got_hit() takes care of it
@@ -491,135 +519,138 @@ sub check_from_in_auto_whitelist {
                       score => sprintf("%0.3f", $delta));
       }
 
-      $whitelist->finish();
+      $welcomelist->finish();
       1;
     } or do {
       my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
-      warn("auto-whitelist: open of auto-whitelist file failed: $eval_stat\n");
+      warn("auto-welcomelist: open of auto-welcomelist file failed: $eval_stat\n");
       # try an unlock, in case we got that far
-      eval { $whitelist->finish(); } if $whitelist;
+      eval { $welcomelist->finish(); } if $welcomelist;
       return 0;
     };
 
-    dbg("auto-whitelist: post auto-whitelist score: %.3f", $pms->{score});
+    dbg("auto-welcomelist: post auto-welcomelist score: %.3f", $pms->{score});
 
     # test hit is above
     return 0;
 }
+*check_from_in_auto_whitelist = \&check_from_in_auto_welcomelist; # removed in 4.1
 
-sub blacklist_address {
+sub blocklist_address {
   my ($self, $args) = @_;
 
-  return 0 unless ($self->{main}->{conf}->{use_auto_whitelist});
+  return 0 unless ($self->{main}->{conf}->{use_auto_welcomelist});
 
   unless ($args->{address}) {
-    print "SpamAssassin auto-whitelist: failed to add address to blacklist\n" if ($args->{cli_p});
-    dbg("auto-whitelist: failed to add address to blacklist");
+    print "SpamAssassin auto-welcomelist: failed to add address to blocklist\n" if ($args->{cli_p});
+    dbg("auto-welcomelist: failed to add address to blocklist");
     return;
   }
   
-  my $whitelist;
+  my $welcomelist;
   my $status;
 
   eval {
-    $whitelist = Mail::SpamAssassin::AutoWhitelist->new($self->{main});
+    $welcomelist = Mail::SpamAssassin::AutoWelcomelist->new($self->{main});
 
-    if ($whitelist->add_known_bad_address($args->{address}, $args->{signedby})) {
-      print "SpamAssassin auto-whitelist: adding address to blacklist: " . $args->{address} . "\n" if ($args->{cli_p});
-      dbg("auto-whitelist: adding address to blacklist: " . $args->{address});
+    if ($welcomelist->add_known_bad_address($args->{address}, $args->{signedby})) {
+      print "SpamAssassin auto-welcomelist: adding address to blocklist: " . $args->{address} . "\n" if ($args->{cli_p});
+      dbg("auto-welcomelist: adding address to blocklist: " . $args->{address});
       $status = 0;
     }
     else {
-      print "SpamAssassin auto-whitelist: error adding address to blacklist\n" if ($args->{cli_p});
-      dbg("auto-whitelist: error adding address to blacklist");
+      print "SpamAssassin auto-welcomelist: error adding address to blocklist\n" if ($args->{cli_p});
+      dbg("auto-welcomelist: error adding address to blocklist");
       $status = 1;
     }
-    $whitelist->finish();
+    $welcomelist->finish();
     1;
   } or do {
     my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
-    warn("auto-whitelist: open of auto-whitelist file failed: $eval_stat\n");
-    eval { $whitelist->finish(); };
+    warn("auto-welcomelist: open of auto-welcomelist file failed: $eval_stat\n");
+    eval { $welcomelist->finish(); };
     return 0;
   };
 
   return $status;
 }
+*blacklist_address = \&blocklist_address; # removed in 4.1
 
-sub whitelist_address {
+sub welcomelist_address {
   my ($self, $args) = @_;
 
-  return 0 unless ($self->{main}->{conf}->{use_auto_whitelist});
+  return 0 unless ($self->{main}->{conf}->{use_auto_welcomelist});
 
   unless ($args->{address}) {
-    print "SpamAssassin auto-whitelist: failed to add address to whitelist\n" if ($args->{cli_p});
-    dbg("auto-whitelist: failed to add address to whitelist");
+    print "SpamAssassin auto-welcomelist: failed to add address to welcomelist\n" if ($args->{cli_p});
+    dbg("auto-welcomelist: failed to add address to welcomelist");
     return 0;
   }
 
-  my $whitelist;
+  my $welcomelist;
   my $status;
 
   eval {
-    $whitelist = Mail::SpamAssassin::AutoWhitelist->new($self->{main});
+    $welcomelist = Mail::SpamAssassin::AutoWelcomelist->new($self->{main});
 
-    if ($whitelist->add_known_good_address($args->{address}, $args->{signedby})) {
-      print "SpamAssassin auto-whitelist: adding address to whitelist: " . $args->{address} . "\n" if ($args->{cli_p});
-      dbg("auto-whitelist: adding address to whitelist: " . $args->{address});
+    if ($welcomelist->add_known_good_address($args->{address}, $args->{signedby})) {
+      print "SpamAssassin auto-welcomelist: adding address to welcomelist: " . $args->{address} . "\n" if ($args->{cli_p});
+      dbg("auto-welcomelist: adding address to welcomelist: " . $args->{address});
       $status = 1;
     }
     else {
-      print "SpamAssassin auto-whitelist: error adding address to whitelist\n" if ($args->{cli_p});
-      dbg("auto-whitelist: error adding address to whitelist");
+      print "SpamAssassin auto-welcomelist: error adding address to welcomelist\n" if ($args->{cli_p});
+      dbg("auto-welcomelist: error adding address to welcomelist");
       $status = 0;
     }
 
-    $whitelist->finish();
+    $welcomelist->finish();
     1;
   } or do {
     my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
-    warn("auto-whitelist: open of auto-whitelist file failed: $eval_stat\n");
-    eval { $whitelist->finish(); };
+    warn("auto-welcomelist: open of auto-welcomelist file failed: $eval_stat\n");
+    eval { $welcomelist->finish(); };
     return 0;
   };
 
   return $status;
 }
+*whitelist_address = \&welcomelist_address; # removed in 4.1
 
 sub remove_address {
   my ($self, $args) = @_;
 
-  return 0 unless ($self->{main}->{conf}->{use_auto_whitelist});
+  return 0 unless ($self->{main}->{conf}->{use_auto_welcomelist});
 
   unless ($args->{address}) {
-    print "SpamAssassin auto-whitelist: failed to remove address\n" if ($args->{cli_p});
-    dbg("auto-whitelist: failed to remove address");
+    print "SpamAssassin auto-welcomelist: failed to remove address\n" if ($args->{cli_p});
+    dbg("auto-welcomelist: failed to remove address");
     return 0;
   }
 
-  my $whitelist;
+  my $welcomelist;
   my $status;
 
   eval {
-    $whitelist = Mail::SpamAssassin::AutoWhitelist->new($self->{main});
+    $welcomelist = Mail::SpamAssassin::AutoWelcomelist->new($self->{main});
 
-    if ($whitelist->remove_address($args->{address}, $args->{signedby})) {
-      print "SpamAssassin auto-whitelist: removing address: " . $args->{address} . "\n" if ($args->{cli_p});
-      dbg("auto-whitelist: removing address: " . $args->{address});
+    if ($welcomelist->remove_address($args->{address}, $args->{signedby})) {
+      print "SpamAssassin auto-welcomelist: removing address: " . $args->{address} . "\n" if ($args->{cli_p});
+      dbg("auto-welcomelist: removing address: " . $args->{address});
       $status = 1;
     }
     else {
-      print "SpamAssassin auto-whitelist: error removing address\n" if ($args->{cli_p});
-      dbg("auto-whitelist: error removing address");
+      print "SpamAssassin auto-welcomelist: error removing address\n" if ($args->{cli_p});
+      dbg("auto-welcomelist: error removing address");
       $status = 0;
     }
   
-    $whitelist->finish();
+    $welcomelist->finish();
     1;
   } or do {
     my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
-    warn("auto-whitelist: open of auto-whitelist file failed: $eval_stat\n");
-    eval { $whitelist->finish(); };
+    warn("auto-welcomelist: open of auto-welcomelist file failed: $eval_stat\n");
+    eval { $welcomelist->finish(); };
     return 0;
   };
 
index b1b0e1862d4109f759d0778121593acd0f625a37..2071d12380089a43ec984e4ca9f6123585e9297a 100644 (file)
@@ -72,7 +72,7 @@ sub new {
   my $self = $class->SUPER::new($mailsaobject);
   bless ($self, $class);
 
-  $self->register_eval_rule("check_access_database");
+  $self->register_eval_rule("check_access_database", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
 
   return $self;
 }
index 590adb4f629e08d1a08f43fc20fc61626b3cd244..20f30e552cab93593cd83616de6f0953a0142674 100644 (file)
@@ -64,8 +64,8 @@ sub new {
   my $self = $class->SUPER::new($mailsaobject);
   bless ($self, $class);
 
-  $self->register_eval_rule("check_microsoft_executable");
-  $self->register_eval_rule("check_suspect_name");
+  $self->register_eval_rule("check_microsoft_executable", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("check_suspect_name", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
 
   return $self;
 }
@@ -107,7 +107,7 @@ sub _check_attachments {
       # file extension indicates an executable
       $pms->{antivirus_microsoft_exe} = 1;
     }
-    elsif ($cte =~ /base64/ && defined $p->raw()->[0] &&
+    elsif (index($cte, 'base64') >= 0 && defined $p->raw()->[0] &&
           $p->raw()->[0] =~ /^TV[opqr].A..[AB].[AQgw][A-H].A/)
     {
       # base64-encoded executable
index 93ca181ad9e76d75776a105c8528ab2bdd109b75..b0d44ae97ee9d1ee65269b12cc69a8b1d8d54a94 100644 (file)
@@ -56,18 +56,30 @@ See the C<Mail::SpamAssassin::Conf> POD for details on C<rbl_timeout>.
 A query template is a string which will be expanded to produce a domain name
 to be used in a DNS query. The template may include SpamAssassin tag names,
 which will be replaced by their values to form a final query domain.
+
 The final query domain must adhere to rules governing DNS domains, i.e.
-must consist of fields each up to 63 characters long, delimited by dots.
-There may be a trailing dot at the end, but it is redundant / carries
-no semantics, because SpamAssassin uses a Net::DSN::Resolver::send method
-for querying DNS, which ignores any 'search' or 'domain' DNS resolver options.
+must consist of fields each up to 63 characters long, delimited by dots,
+not exceeding 255 characters. International domain names (in UTF-8) are
+allowed and will be encoded to ASCII-compatible encoding (ACE) according
+to IDN rules. Syntactically invalid resulting queries will be discarded
+by the DNS resolver code (with some info warnings).
+
+There may be a trailing dot at the end, but it is redundant / carries no
+semantics, because SpamAssassin uses a Net::DSN::Resolver::send method for
+querying DNS, which ignores any 'search' or 'domain' DNS resolver options.
 Domain names in DNS queries are case-insensitive.
 
 A tag name is a string of capital letters, preceded and followed by an
-underscore character. This syntax mirrors the add_header setting, except that
-tags cannot have parameters in parenthesis when used in askdns templates.
-Tag names may appear anywhere in the template - each queried DNS zone
-prescribes how a query should be formed.
+underscore character.  This syntax mirrors the add_header setting, except
+that tags cannot have parameters in parenthesis when used in askdns
+templates (exceptions found below).  Tag names may appear anywhere in the
+template - each queried DNS zone prescribes how a query should be formed.
+
+Special supported tag HEADER() can be used to query any header content,
+using same header names/modifiers that as header rules support.  For example
+_HEADER(Reply-To:addr:domain)_ can be used to query the trimmed domain part
+of Reply-To address.  See Mail::SpamAssassin::Conf documentation about
+header rules.
 
 A query template may contain any number of tag names including none,
 although in the most common anticipated scenario exactly one tag name would
@@ -114,7 +126,8 @@ parameter will only act as a filter on a result.
 
 Currently recognized RR types in the rr_type parameter are: ANY, A, AAAA,
 MX, TXT, PTR, NAPTR, NS, SOA, CERT, CNAME, DNAME, DHCID, HINFO, MINFO,
-RP, HIP, IPSECKEY, KX, LOC, SRV, SSHFP, SPF.
+RP, HIP, IPSECKEY, KX, LOC, GPOS, SRV, OPENPGPKEY, SSHFP, SPF, TLSA, URI,
+CAA, CSYNC.
 
 https://www.iana.org/assignments/dns-parameters/dns-parameters.xml
 
@@ -189,8 +202,10 @@ use warnings;
 use re 'taint';
 
 use Mail::SpamAssassin::Plugin;
-use Mail::SpamAssassin::Util qw(decode_dns_question_entry);
+use Mail::SpamAssassin::Util qw(decode_dns_question_entry idn_to_ascii
+                                compile_regexp is_fqdn_valid);
 use Mail::SpamAssassin::Logger;
+use Mail::SpamAssassin::Constants qw(:ip);
 use version 0.77;
 
 our @ISA = qw(Mail::SpamAssassin::Plugin);
@@ -202,8 +217,6 @@ our %rcode_value = (  # https://www.iana.org/assignments/dns-parameters, RFC 619
   BADMODE => 19, BADNAME => 20, BADALG => 21, BADTRUNC => 22,
 );
 
-our $txtdata_can_provide_a_list;
-
 sub new {
   my($class,$sa_main) = @_;
 
@@ -213,10 +226,6 @@ sub new {
 
   $self->set_config($sa_main->{conf});
 
-  #$txtdata_can_provide_a_list = Net::DNS->VERSION >= 0.69;
-  #more robust version check from Damyan Ivanov - Bug 7095
-  $txtdata_can_provide_a_list = version->parse(Net::DNS->VERSION) >= version->parse('0.69');
-
   return $self;
 }
 
@@ -253,20 +262,14 @@ sub parse_and_canonicalize_subtest {
   my $result;
 
   local($1,$2,$3);
-  # modifiers /a, /d, /l, /u in suffix form were added with perl 5.13.10 (5.14)
-  # currently known modifiers are [msixoadlu], but let's not be too picky here
-  if (     $subtest =~ m{^       /  (.+) /  ([a-z]*) \z}xs) {
-    $result = $2 ne '' ? qr{(?$2)$1} : qr{$1};
-  } elsif ($subtest =~ m{^ m \s* \( (.+) \) ([a-z]*) \z}xs) {
-    $result = $2 ne '' ? qr{(?$2)$1} : qr{$1};
-  } elsif ($subtest =~ m{^ m \s* \[ (.+) \] ([a-z]*) \z}xs) {
-    $result = $2 ne '' ? qr{(?$2)$1} : qr{$1};
-  } elsif ($subtest =~ m{^ m \s* \{ (.+) \} ([a-z]*) \z}xs) {
-    $result = $2 ne '' ? qr{(?$2)$1} : qr{$1};
-  } elsif ($subtest =~ m{^ m \s*  < (.+)  > ([a-z]*) \z}xs) {
-    $result = $2 ne '' ? qr{(?$2)$1} : qr{$1};
-  } elsif ($subtest =~ m{^ m \s* (\S) (.+) \1 ([a-z]*) \z}xs) {
-    $result = $2 ne '' ? qr{(?$2)$1} : qr{$1};
+  if ($subtest =~ m{^/ .+ / [a-z]* \z}xs ||
+      $subtest =~ m{^m (\W) .+ (\W) [a-z]* \z}xs) {
+    my ($rec, $err) = compile_regexp($subtest, 1);
+    if (!$rec) {
+      warn "askdns: subtest compile failed: '$subtest': $err\n";
+    } else {
+      $result = $rec;
+    }
   } elsif ($subtest =~ m{^ (["']) (.*) \1 \z}xs) {  # quoted string
     $result = $2;
   } elsif ($subtest =~ m{^ \[ ( (?:[A-Z]+|\d+)
@@ -324,8 +327,9 @@ sub set_config {
         my @answer_types = split(/,/, $query_type);
         # https://www.iana.org/assignments/dns-parameters/dns-parameters.xml
         if (grep(!/^(?:ANY|A|AAAA|MX|TXT|PTR|NAPTR|NS|SOA|CERT|CNAME|DNAME|
-                       DHCID|HINFO|MINFO|RP|HIP|IPSECKEY|KX|LOC|SRV|
-                       SSHFP|SPF)\z/x, @answer_types)) {
+                       DHCID|HINFO|MINFO|RP|HIP|IPSECKEY|KX|LOC|GPOS|SRV|
+                       OPENPGPKEY|SSHFP|SPF|TLSA|URI|CAA|CSYNC)\z/x,
+                 @answer_types)) {
           return $Mail::SpamAssassin::Conf::INVALID_VALUE;
         }
         $query_type = 'ANY' if @answer_types > 1 || $answer_types[0] eq 'ANY';
@@ -333,25 +337,19 @@ sub set_config {
           $subtest = parse_and_canonicalize_subtest($subtest);
           defined $subtest or return $Mail::SpamAssassin::Conf::INVALID_VALUE;
         }
-        # collect tag names as used in each query template
-        my @tags = $query_template =~ /_([A-Z][A-Z0-9]*)_/g;
-        my %seen; @tags = grep(!$seen{$_}++, @tags);  # filter out duplicates
 
-        # group rules by tag names used in them (to be used as a hash key)
-        my $depends_on_tags = !@tags ? '' : join(',',@tags);
+        # initialize rule structure
+        $self->{askdns}{$rulename}{query} = $query_template;
+        $self->{askdns}{$rulename}{q_type} = $query_type;
+        $self->{askdns}{$rulename}{a_types} = \@answer_types;
+        $self->{askdns}{$rulename}{subtest} = $subtest;
+        $self->{askdns}{$rulename}{tags} = ();
 
-        # subgroup rules by a DNS RR type and a nonexpanded query template
-        my $query_template_key = $query_type . ':' . $query_template;
-
-        $self->{askdns}{$depends_on_tags}{$query_template_key} ||=
-          { query => $query_template, rules => {}, q_type => $query_type,
-            a_types =>  # optimization: undef means "same as q_type"
-              @answer_types == 1 && $answer_types[0] eq $query_type ? undef
-                                                           : \@answer_types };
-        $self->{askdns}{$depends_on_tags}{$query_template_key}{rules}{$rulename}
-          = $subtest;
-      # dbg("askdns: rule: %s, config dep: %s, domkey: %s, subtest: %s",
-      #     $rulename, $depends_on_tags, $query_template_key, $subtest);
+        # collect tag names as used in each query template
+        # also support common HEADER(arg) tag which does $pms->get(arg)
+        my @tags = $query_template =~ /_([A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*(?:\(.*?\))?)_/g;
+        # save rule to tag dependencies
+        $self->{askdns}{$rulename}{tags}{$_} = 1 foreach (@tags);
 
         # just define the test so that scores and lint works
         $self->{parser}->add_test($rulename, undef,
@@ -366,182 +364,127 @@ sub set_config {
 # run as early as possible, launching DNS queries as soon as their
 # dependencies are fulfilled
 #
-sub parsed_metadata {
+sub check_dnsbl {
   my($self, $opts) = @_;
+
   my $pms = $opts->{permsgstatus};
   my $conf = $pms->{conf};
 
-  return if !$pms->is_dns_available;
-  $pms->{askdns_map_dnskey_to_rules} = {};
+  return if !$pms->is_dns_available();
 
   # walk through all collected askdns rules, obtain tag values whenever
   # they may become available, and launch DNS queries right after
-  #
-  for my $depends_on_tags (keys %{$conf->{askdns}}) {
-    my @tags;
-    @tags = split(/,/, $depends_on_tags)  if $depends_on_tags ne '';
-
-    if (would_log("dbg","askdns")) {
-      while ( my($query_template_key, $struct) =
-                each %{$conf->{askdns}{$depends_on_tags}} ) {
-        my($query_template, $query_type, $answer_types_ref, $rules) =
-          @$struct{qw(query q_type a_types rules)};
-        dbg("askdns: depend on tags %s, rules: %s ",
-            $depends_on_tags, join(', ', keys %$rules));
-      }
+  foreach my $rulename (keys %{$conf->{askdns}}) {
+    if (!$conf->{scores}->{$rulename}) {
+      dbg("askdns: skipping disabled rule $rulename");
+      next;
     }
-
-    if (!@tags) {
-      # no dependencies on tags, just call directly
-      $self->launch_queries($pms,$depends_on_tags);
-    } else {
-      # enqueue callback for tags needed
+    my @tags = sort keys %{$conf->{askdns}{$rulename}{tags}};
+    if (@tags) {
+      dbg("askdns: rule %s depends on tags: %s", $rulename,
+          join(', ', @tags));
       $pms->action_depends_on_tags(@tags == 1 ? $tags[0] : \@tags,
-              sub { my($pms,@args) = @_;
-                    $self->launch_queries($pms,$depends_on_tags) }
+            sub { my($pms,@args) = @_;
+                  $self->launch_queries($pms,$rulename,\@tags) }
       );
+    } else {
+      # no dependencies on tags, just call directly
+      $self->launch_queries($pms,$rulename,[]);
     }
   }
 }
 
-# generate DNS queries - called for each set of rules
-# when their tag dependencies are met
+# generate DNS queries - called for each rule when its tag dependencies
+# are met
 #
 sub launch_queries {
-  my($self, $pms, $depends_on_tags) = @_;
-  my $conf = $pms->{conf};
-
-  my %tags;
-  # obtain tag/value pairs of tags we depend upon in this set of rules
-  if ($depends_on_tags ne '') {
-    %tags = map( ($_,$pms->get_tag($_)), split(/,/,$depends_on_tags) );
-  }
-  dbg("askdns: preparing queries which depend on tags: %s",
-      join(', ', map($_.' => '.$tags{$_}, keys %tags)));
-
-  # replace tag names in a query template with actual tag values
-  # and launch DNS queries
-  while ( my($query_template_key, $struct) =
-            each %{$conf->{askdns}{$depends_on_tags}} ) {
-    my($query_template, $query_type, $answer_types_ref, $rules) =
-      @$struct{qw(query q_type a_types rules)};
-
-    my @rulenames = keys %$rules;
-    if (grep($conf->{scores}->{$_}, @rulenames)) {
-      dbg("askdns: query template %s, type %s, rules: %s",
-          $query_template,
-          !$answer_types_ref ? $query_type
-            : $query_type.'/'.join(',',@$answer_types_ref),
-          join(', ', @rulenames));
-    } else {
-      dbg("askdns: query template %s, type %s, all rules disabled: %s",
-          $query_template, $query_type, join(', ', @rulenames));
-      next;
-    }
-
-    # collect all tag names from a template, each may occur more than once
-    my @templ_tags = $query_template =~ /_([A-Z][A-Z0-9]*)_/gs;
-
-    # filter out duplicate tag names, and tags with undefined or empty value
-    my %seen;
-    @templ_tags = grep(!$seen{$_}++ && defined $tags{$_} && $tags{$_} ne '',
-                       @templ_tags);
-
-    my %templ_vals;  # values that each tag takes
-    for my $t (@templ_tags) {
-      my %seen;
-      # a tag value may be a space-separated list,
-      # store it as an arrayref, removing duplicate values
-      $templ_vals{$t} = [ grep(!$seen{$_}++, split(' ',$tags{$t})) ];
-    }
-
-    # count through all tag value tuples
-    my @digit = (0) x @templ_tags;  # counting accumulator
-OUTER:
-    for (;;) {
-      my %current_tag_val;  # maps a tag name to its current iteration value
-      for my $j (0 .. $#templ_tags) {
-        my $t = $templ_tags[$j];
-        $current_tag_val{$t} = $templ_vals{$t}[$digit[$j]];
-      }
-      local $1;
-      my $query_domain = $query_template;
-      $query_domain =~ s{_([A-Z][A-Z0-9]*)_}
-                        { defined $current_tag_val{$1} ? $current_tag_val{$1}
-                                                       : '' }ge;
-
-      # the $dnskey identifies this query in AsyncLoop's pending_lookups
-      my $dnskey = join(':', 'askdns', $query_type, $query_domain);
-      dbg("askdns: expanded query %s, dns key %s", $query_domain, $dnskey);
-
-      if ($query_domain eq '') {
-        # ignore, just in case
-      } else {
-        if (!exists $pms->{askdns_map_dnskey_to_rules}{$dnskey}) {
-          $pms->{askdns_map_dnskey_to_rules}{$dnskey} =
-             [ [$query_type, $answer_types_ref, $rules] ];
-        } else {
-          push(@{$pms->{askdns_map_dnskey_to_rules}{$dnskey}},
-               [$query_type, $answer_types_ref, $rules] );
+  my($self, $pms, $rulename, $tags) = @_;
+
+  my $arule = $pms->{conf}->{askdns}{$rulename};
+  my $query_tmpl = $arule->{query};
+  my $queries;
+  if (@$tags) {
+    if (!exists $pms->{askdns_qtmpl_cache}{$query_tmpl}) {
+      # replace tags in query template
+      # iterate through each tag, replacing list of strings as we go
+      my %q_iter = ( "$query_tmpl" => 1 );
+      foreach my $tag (@$tags) {
+        # cache tag values locally
+        if (!exists $pms->{askdns_tag_cache}{$tag}) {
+          my $valref = $pms->get_tag_raw($tag);
+          my @vals = grep { defined $_ && $_ ne '' } (ref $valref ? @$valref : $valref);
+          # Paranoid check for undefined tag
+          if (!@vals) {
+            dbg("askdns: skipping rule $rulename, no value found for tag: $tag");
+            return;
+          }
+          $pms->{askdns_tag_cache}{$tag} = \@vals;
+        }
+        my %q_iter_new;
+        foreach my $q (keys %q_iter) {
+          # handle space separated multi-valued tags
+          foreach my $val (@{$pms->{askdns_tag_cache}{$tag}}) {
+            my $qtmp = $q;
+            $qtmp =~ s/\Q_${tag}_\E/${val}/g;
+            $q_iter_new{$qtmp} = 1;
+          }
         }
-        # launch a new DNS query for $query_type and $query_domain
-        my $ent = $pms->{async}->bgsend_and_start_lookup(
-          $query_domain, $query_type, undef,
-          { key => $dnskey, zone => $query_domain },
-          sub { my ($ent2,$pkt) = @_;
-                $self->process_response_packet($pms, $ent2, $pkt, $dnskey) },
-          master_deadline => $pms->{master_deadline} );
-        # these rules are now underway;  unless the rule hits, these will
-        # not be considered "finished" until harvest_dnsbl_queries() completes
-        $pms->register_async_rule_start($dnskey) if $ent;
+        %q_iter = %q_iter_new;
       }
+      # cache idn'd queries
+      my @q_arr;
+      push @q_arr, idn_to_ascii($_) foreach (keys %q_iter);
+      $pms->{askdns_qtmpl_cache}{$query_tmpl} = \@q_arr;
+    }
+    $queries = $pms->{askdns_qtmpl_cache}{$query_tmpl};
+  } else {
+    push @$queries, idn_to_ascii($query_tmpl);
+  }
 
-      last  if !@templ_tags;
-      # increment accumulator, little-endian
-      for (my $j = 0;  ; $j++) {
-        last  if ++$digit[$j] <= $#{$templ_vals{$templ_tags[$j]}};
-        $digit[$j] = 0;  # and carry
-        last OUTER  if $j >= $#templ_tags;
-      }
+  foreach my $query (@$queries) {
+    if (!is_fqdn_valid($query, 1)) {
+      dbg("askdns: skipping invalid query ($rulename): $query");
+      next;
     }
+    dbg("askdns: launching query ($rulename): $query");
+    my $ret = $pms->{async}->bgsend_and_start_lookup(
+      $query, $arule->{q_type}, undef,
+        { rulename => $rulename, type => 'AskDNS' },
+        sub { my ($ent,$pkt) = @_;
+              $self->process_response_packet($pms, $ent, $pkt, $rulename) },
+        master_deadline => $pms->{master_deadline}
+    );
+    $pms->rule_ready($rulename) if !$ret; # mark ready if nothing launched
   }
 }
 
 sub process_response_packet {
-  my($self, $pms, $ent, $pkt, $dnskey) = @_;
+  my($self, $pms, $ent, $pkt, $rulename) = @_;
 
-  my $conf = $pms->{conf};
-  my %rulenames_hit;
+  # NOTE: $pkt will be undef if the DNS query was aborted (e.g. timed out)
+  return if !$pkt;
 
-  # map a dnskey back to info on queries which caused this DNS lookup
-  my $queries_ref = $pms->{askdns_map_dnskey_to_rules}{$dnskey};
+  my @question = $pkt->question;
+  return if !@question;
 
-  my($header, @question, @answer, $qtype, $rcode);
-  # NOTE: $pkt will be undef if the DNS query was aborted (e.g. timed out)
-  if ($pkt) {
-    @answer = $pkt->answer;
-    $header = $pkt->header;
-    @question = $pkt->question;
-    $qtype = uc $question[0]->qtype  if @question;
-    $rcode = uc $header->rcode  if $header;  # 'NOERROR', 'NXDOMAIN', ...
-
-    # NOTE: qname is encoded in RFC 1035 zone format, decode it
-    dbg("askdns: answer received, rcode %s, query %s, answer has %d records",
-        $rcode,
-        join(', ', map(join('/', decode_dns_question_entry($_)), @question)),
-        scalar @answer);
-
-    if (defined $rcode && exists $rcode_value{$rcode}) {
-      # Net::DNS return a rcode name for codes it knows about,
-      # and returns a number for the rest; we deal with numbers from here on
-      $rcode = $rcode_value{$rcode}  if exists $rcode_value{$rcode};
-    }
-  }
-  if (!@answer) {
-    # a trick to make the following loop run at least once, so that we can
-    # evaluate also rules which only care for rcode status
-    @answer = ( undef );
-  }
+  $pms->rule_ready($rulename); # mark rule ready for metas
+
+  my @answer = $pkt->answer;
+  my $rcode = uc $pkt->header->rcode;  # 'NOERROR', 'NXDOMAIN', ...
+
+  # NOTE: qname is encoded in RFC 1035 zone format, decode it
+  dbg("askdns: answer received (%s), rcode %s, query %s, answer has %d records",
+      $rulename, $rcode,
+      join(', ', map(join('/', decode_dns_question_entry($_)), @question)),
+      scalar @answer);
+
+  # Net::DNS return a rcode name for codes it knows about,
+  # and returns a number for the rest; we deal with numbers from here on
+  $rcode = $rcode_value{$rcode}  if exists $rcode_value{$rcode};
+
+  # a trick to make the following loop run at least once, so that we can
+  # evaluate also rules which only care for rcode status
+  @answer = (undef)  if !@answer;
 
   # NOTE:  $rr->rdstring returns the result encoded in a DNS zone file
   # format, i.e. enclosed in double quotes if a result contains whitespace
@@ -561,9 +504,12 @@ sub process_response_packet {
   # the code handling such reply from DNS MUST assemble all of these
   # marshaled text blocks into a single one before any syntactical
   # verification takes place.
-  # The same goes for RFC 4408 (SPF), RFC 4871 (DKIM), RFC 5617 (ADSP),
+  # The same goes for RFC 7208 (SPF), RFC 4871 (DKIM), RFC 5617 (ADSP),
   # draft-kucherawy-dmarc-base (DMARC), ...
 
+  my $arule = $pms->{conf}->{askdns}{$rulename};
+  my $subtest = $arule->{subtest};
+
   for my $rr (@answer) {
     my($rr_rdatastr, $rdatanum, $rr_type);
     if (!$rr) {
@@ -571,72 +517,62 @@ sub process_response_packet {
     } else {
       $rr_type = uc $rr->type;
       if ($rr_type eq 'A') {
-        # Net::DNS::RR::A::address() is available since Net::DNS 0.69
-        $rr_rdatastr = $rr->UNIVERSAL::can('address') ? $rr->address
-                                                      : $rr->rdatastr;
+        $rr_rdatastr = $rr->address;
         if ($rr_rdatastr =~ m/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/) {
           $rdatanum = Mail::SpamAssassin::Util::my_inet_aton($rr_rdatastr);
         }
 
       } elsif ($rr->UNIVERSAL::can('txtdata')) {
         # TXT, SPF: join with no intervening spaces, as per RFC 5518
-        if ($txtdata_can_provide_a_list || $rr_type ne 'TXT') {
-          $rr_rdatastr = join('', $rr->txtdata);  # txtdata() in list context!
-        } else {  # char_str_list() is only available for TXT records
-          $rr_rdatastr = join('', $rr->char_str_list);  # historical
-        }
+        $rr_rdatastr = join('', $rr->txtdata);  # txtdata() in list context!
+        # Net::DNS attempts to decode text strings in a TXT record as UTF-8,
+        # which is undesired: octets failing the UTF-8 decoding are converted
+        # to a Unicode "replacement character" U+FFFD (encoded as octets
+        # \x{EF}\x{BF}\x{BD} in UTF-8), and ASCII text is unnecessarily
+        # flagged as perl native characters (utf8 flag on), which can be
+        # disruptive on later processing, e.g. implicitly upgrading strings
+        # on concatenation. Unfortunately there is no way of legally bypassing
+        # the UTF-8 decoding by Net::DNS::RR::TXT in Net::DNS::RR::Text.
+        # Try to minimize damage by encoding back to UTF-8 octets:
+        utf8::encode($rr_rdatastr)  if utf8::is_utf8($rr_rdatastr);
+
       } else {
-        # rdatastr() is historical, use rdstring() since Net::DNS 0.69
-        $rr_rdatastr = $rr->UNIVERSAL::can('rdstring') ? $rr->rdstring
-                                                       : $rr->rdatastr;
+        $rr_rdatastr = $rr->rdstring;
         utf8::encode($rr_rdatastr)  if utf8::is_utf8($rr_rdatastr);
       }
-    # dbg("askdns: received rr type %s, data: %s", $rr_type, $rr_rdatastr);
+      # dbg("askdns: received rr type %s, data: %s", $rr_type, $rr_rdatastr);
     }
 
-    for my $q_tuple (!ref $queries_ref ? () : @$queries_ref) {
-      next  if !$q_tuple;
-      my($query_type, $answer_types_ref, $rules) = @$q_tuple;
-
-      next  if !defined $qtype;
-      $answer_types_ref = [$query_type]  if !defined $answer_types_ref;
-
-      while (my($rulename,$subtest) = each %$rules) {
-        my $match;
-        local($1,$2,$3);
-        if (ref $subtest eq 'HASH') {  # a list of DNS rcodes (as hash keys)
-          $match = 1  if $subtest->{$rcode};
-        } elsif ($rcode != 0) {
-          # skip remaining tests on DNS error
-        } elsif (!defined($rr_type) ||
-                 !grep($_ eq 'ANY' || $_ eq $rr_type, @$answer_types_ref) ) {
-          # skip remaining tests on wrong RR type
-        } elsif (!defined $subtest) {
-          $match = 1;  # any valid response of the requested RR type matches
-        } elsif (ref $subtest eq 'Regexp') {  # a regular expression
-          $match = 1  if $rr_rdatastr =~ $subtest;
-        } elsif ($rr_rdatastr eq $subtest) {  # exact equality
-          $match = 1;
-        } elsif (defined $rdatanum &&
-                 $subtest =~ m{^ (\d+) (?: ([/-]) (\d+) )? \z}x) {
-          my($n1,$delim,$n2) = ($1,$2,$3);
-          $match =
-            !defined $n2  ? ($rdatanum & $n1) &&                  # mask only
-                              (($rdatanum & 0xff000000) == 0x7f000000)  # 127/8
-          : $delim eq '-' ? $rdatanum >= $n1 && $rdatanum <= $n2  # range
-          : $delim eq '/' ? ($rdatanum & $n2) == (int($n1) & $n2) # value/mask
-          : 0; # notice int($n1) to fix perl ~5.14 taint bug (Bug 7725)
-        }
-        if ($match) {
-          $self->askdns_hit($pms, $ent->{query_domain}, $qtype,
-                            $rr_rdatastr, $rulename);
-          $rulenames_hit{$rulename} = 1;
-        }
-      }
+    my $match;
+    local($1,$2,$3);
+    if (ref $subtest eq 'HASH') {  # a list of DNS rcodes (as hash keys)
+      $match = 1  if $subtest->{$rcode};
+    } elsif ($rcode != 0) {
+      # skip remaining tests on DNS error
+    } elsif (!defined($rr_type) ||
+             !grep($_ eq 'ANY' || $_ eq $rr_type, @{$arule->{a_types}}) ) {
+      # skip remaining tests on wrong RR type
+    } elsif (!defined $subtest) {
+      $match = 1;  # any valid response of the requested RR type matches
+    } elsif (ref $subtest eq 'Regexp') {  # a regular expression
+      $match = 1  if $rr_rdatastr =~ $subtest;
+    } elsif ($rr_rdatastr eq $subtest) {  # exact equality
+      $match = 1;
+    } elsif (defined $rdatanum &&
+             $subtest =~ m{^ (\d+) (?: ([/-]) (\d+) )? \z}x) {
+      my($n1,$delim,$n2) = ($1,$2,$3);
+      $match =
+        !defined $n2 ? ($rdatanum & $n1) &&                     # mask only
+                       (($rdatanum & 0xff000000) == 0x7f000000) # 127/8
+        : $delim eq '-' ? $rdatanum >= $n1 && $rdatanum <= $n2  # range
+        : $delim eq '/' ? ($rdatanum & $n2) == (int($n1) & $n2) # value/mask
+        : 0; # notice int($n1) to fix perl ~5.14 taint bug (Bug 7725)
+    }
+    if ($match) {
+      $self->askdns_hit($pms, $ent->{query_domain}, $question[0]->qtype,
+                        $rr_rdatastr, $rulename);
     }
   }
-  # these rules have completed (since they got at least 1 hit)
-  $pms->register_async_rule_finish($_)  for keys %rulenames_hit;
 }
 
 sub askdns_hit {
@@ -648,9 +584,11 @@ sub askdns_hit {
 
   # only the first hit will show in the test log report, even if
   # an answer section matches more than once - got_hit() handles this
-  $pms->clear_test_state;
-  $pms->test_log(sprintf("%s %s:%s", $query_domain,$qtype,$rr_rdatastr));
+  $pms->test_log(sprintf("%s %s:%s", $query_domain,$qtype,$rr_rdatastr), $rulename);
   $pms->got_hit($rulename, 'ASKDNS: ', ruletype => 'askdns');  # score=>$score
 }
 
+# Version features
+sub has_tag_header { 1 } # HEADER() was implemented together with Conf::feature_get_host # Bug 7734
+
 1;
diff --git a/upstream/lib/Mail/SpamAssassin/Plugin/AuthRes.pm b/upstream/lib/Mail/SpamAssassin/Plugin/AuthRes.pm
new file mode 100644 (file)
index 0000000..4a7543f
--- /dev/null
@@ -0,0 +1,553 @@
+# <@LICENSE>
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to you under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# </@LICENSE>
+
+=head1 NAME
+
+Mail::SpamAssassin::Plugin::AuthRes - use Authentication-Results header fields
+
+=head1 SYNOPSIS
+
+=head2 SpamAssassin configuration:
+
+loadplugin     Mail::SpamAssassin::Plugin::AuthRes
+
+authres_trusted_authserv  myserv.example.com
+authres_networks  all
+
+=head1 DESCRIPTION
+
+This plugin parses Authentication-Results header fields and can supply the
+results obtained to other plugins, so as to avoid repeating checks that have
+been performed already.
+
+=cut
+
+package Mail::SpamAssassin::Plugin::AuthRes;
+
+use Mail::SpamAssassin::Plugin;
+use Mail::SpamAssassin::Logger;
+use strict;
+use warnings;
+# use bytes;
+use re 'taint';
+
+our @ISA = qw(Mail::SpamAssassin::Plugin);
+
+# list of valid methods and values
+# https://www.iana.org/assignments/email-auth/email-auth.xhtml
+# some others not in that list:
+#   dkim-atps=neutral
+my %method_result = (
+  'auth' => {'fail'=>1,'none'=>1,'pass'=>1,'permerror'=>1,'temperror'=>1},
+  'dkim' => {'fail'=>1,'neutral'=>1,'none'=>1,'pass'=>1,'permerror'=>1,'policy'=>1,'temperror'=>1},
+  'dkim-adsp' => {'discard'=>1,'fail'=>1,'none'=>1,'nxdomain'=>1,'pass'=>1,'permerror'=>1,'temperror'=>1,'unknown'=>1},
+  'dkim-atps' => {'fail'=>1,'none'=>1,'pass'=>1,'permerror'=>1,'temperror'=>1,'neutral'=>1},
+  'dmarc' => {'fail'=>1,'none'=>1,'pass'=>1,'permerror'=>1,'temperror'=>1},
+  'domainkeys' => {'fail'=>1,'neutral'=>1,'none'=>1,'permerror'=>1,'policy'=>1,'pass'=>1,'temperror'=>1},
+  'iprev' => {'fail'=>1,'pass'=>1,'permerror'=>1,'temperror'=>1},
+  'rrvs' => {'fail'=>1,'none'=>1,'pass'=>1,'permerror'=>1,'temperror'=>1,'unknown'=>1},
+  'sender-id' => {'fail'=>1,'hardfail'=>1,'neutral'=>1,'none'=>1,'pass'=>1,'permerror'=>1,'policy'=>1,'softfail'=>1,'temperror'=>1},
+  'smime' => {'fail'=>1,'neutral'=>1,'none'=>1,'pass'=>1,'permerror'=>1,'policy'=>1,'temperror'=>1},
+  'spf' => {'fail'=>1,'hardfail'=>1,'neutral'=>1,'none'=>1,'pass'=>1,'permerror'=>1,'policy'=>1,'softfail'=>1,'temperror'=>1},
+  'vbr' => {'fail'=>1,'none'=>1,'pass'=>1,'permerror'=>1,'temperror'=>1},
+);
+my %method_ptype_prop = (
+  'auth' => {'smtp' => {'auth'=>1,'mailfrom'=>1}},
+  'dkim' => {'header' => {'d'=>1,'i'=>1,'b'=>1}},
+  'dkim-adsp' => {'header' => {'from'=>1}},
+  'dkim-atps' => {'header' => {'from'=>1}},
+  'dmarc' => {'header' => {'from'=>1}},
+  'domainkeys' => {'header' => {'d'=>1,'from'=>1,'sender'=>1}},
+  'iprev' => {'policy' => {'iprev'=>1}},
+  'rrvs' => {'smtp' => {'rcptto'=>1}},
+  'sender-id' => {'header' => {'*'=>1}},
+  'smime' => {'body' => {'smime-part'=>1,'smime-identifer'=>1,'smime-serial'=>1,'smime-issuer'=>1}},
+  'spf' => {'smtp' => {'mailfrom'=>1,'helo'=>1}},
+  'vbr' => {'header' => {'md'=>1,'mv'=>1}},
+);
+      
+# Some MIME helpers
+my $QUOTED_STRING = qr/"((?:[^"\\]++|\\.)*+)"?/;
+my $TOKEN = qr/[^\s\x00-\x1f\x80-\xff\(\)\<\>\@\,\;\:\/\[\]\?\=\"]+/;
+my $ATOM = qr/[a-zA-Z0-9\@\!\#\$\%\&\\\'\*\+\-\/\=\?\^\_\`\{\|\}\~]+/;
+
+sub new {
+  my ($class, $mailsa) = @_;
+
+  # the usual perlobj boilerplate to create a subclass object
+  $class = ref($class) || $class;
+  my $self = $class->SUPER::new($mailsa);
+  bless ($self, $class);
+
+  $self->set_config($mailsa->{conf});
+
+  # process first as other plugins might depend on us
+  $self->register_method_priority("parsed_metadata", -10);
+
+  $self->register_eval_rule("check_authres_result", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+
+  return $self;
+}
+
+sub set_config {
+  my ($self, $conf) = @_;
+  my @cmds;
+
+=head1 ADMINISTRATOR OPTIONS
+
+=over
+
+=item authres_networks internal/trusted/all    (default: internal)
+
+Process Authenticated-Results headers set by servers from these networks
+(refers to SpamAssassin *_networks zones).  Any header outside this is
+completely ignored (affects all module settings).
+
+ internal   = internal_networks
+ trusted    = internal_networks + trusted_networks
+ all        = all above + all external
+
+Setting "all" is safe only if your MX servers filter properly all incoming
+A-R headers, and you use authres_trusted_authserv to match your authserv-id. 
+This is suitable for default OpenDKIM for example.  These settings might
+also be required if your filters do not insert A-R header to correct
+position above the internal Received header (some known offenders: OpenDKIM,
+OpenDMARC, amavisd-milter).
+
+=back
+
+=cut
+
+  push (@cmds, {
+    setting => 'authres_networks',
+    is_admin => 1,
+    default => 'internal',
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      if (!defined $value || $value =~ /^$/) {
+        return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
+      }
+      $value = lc($value);
+      if ($value =~ /^(?:internal|trusted|all)$/) {
+        $self->{authres_networks} = $value;
+      } else {
+        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+      }
+    }
+  });
+
+=over
+
+=item authres_trusted_authserv authservid1 id2 ...   (default: none)
+
+Trusted authentication server IDs (the domain-name-like first word of
+Authentication-Results field, also known as C<authserv-id>).
+
+Note that if set, ALL A-R headers are ignored unless a match is found.
+
+Use strongly recommended, possibly along with authres_networks all.
+
+=back
+
+=cut
+
+  push (@cmds, {
+    setting => 'authres_trusted_authserv',
+    is_admin => 1,
+    default => {},
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE,
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      if (!defined $value || $value =~ /^$/) {
+        return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
+      }
+      foreach my $id (split(/\s+/, lc $value)) {
+        $self->{authres_trusted_authserv}->{$id} = 1;
+      }
+    }
+  });
+
+=over
+
+=item authres_ignored_authserv authservid1 id2 ...   (default: none)
+
+Ignored authentication server IDs (the domain-name-like first word of
+Authentication-Results field, also known as C<authserv-id>).
+
+Any A-R header is ignored if match is found.
+
+=back
+
+=cut
+
+  push (@cmds, {
+    setting => 'authres_ignored_authserv',
+    is_admin => 1,
+    default => {},
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE,
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      if (!defined $value || $value =~ /^$/) {
+        return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
+      }
+      foreach my $id (split(/\s+/, lc $value)) {
+        $self->{authres_ignored_authserv}->{$id} = 1;
+      }
+    }
+  });
+
+  $conf->{parser}->register_commands(\@cmds);
+}
+
+=head1 METADATA
+
+Parsed headers are stored in $pms-E<gt>{authres_parsed}, as a hash of array
+of hashes where results are collected by method.  For example, the header
+field:
+
+  Authentication-Results: server.example.com;
+    spf=pass smtp.mailfrom=bounce.example.org;
+    dkim=pass header.i=@example.org;
+    dkim=fail header.i=@another.signing.domain.example
+
+Produces the following structure:
+
+ $pms->{authres_parsed} = {
+   'dkim' => [
+     {
+       'properties' => {
+         'header' => {
+           'i' => '@example.org'
+         }
+       },
+       'authserv' => 'server.example.com',
+       'result' => 'pass',
+       'version' => 1,
+       'reason' => ''
+     },
+     {
+       'properties' => {
+         'header' => {
+           'i' => '@another.signing.domain.example'
+         }
+       },
+       'result' => 'fail',
+       'authserv' => 'server.example.com',
+       'version' => 1,
+       'reason' => ''
+     },
+   ],
+ }
+
+Within each array, the order of results is the original, which should be most
+recent results first.
+
+For checking result of methods, $pms-E<gt>{authres_result} is available:
+
+ $pms->{authres_result} = {
+   'dkim' => 'pass',
+   'spf' => 'fail',
+ }
+
+=head1 EVAL FUNCTIONS
+
+=over 4
+
+=item header RULENAME eval:check_authres_result(method, result)
+
+Can be used to check results.
+
+  ifplugin Mail::SpamAssassin::Plugin::AuthRes
+  ifplugin !(Mail::SpamAssassin::Plugin::SPF)
+    header  SPF_PASS      eval:check_authres_result('spf', 'pass')
+    header  SPF_FAIL      eval:check_authres_result('spf', 'fail')
+    header  SPF_SOFTFAIL  eval:check_authres_result('spf', 'softfail')
+    header  SPF_TEMPFAIL  eval:check_authres_result('spf', 'tempfail')
+  endif
+  ifplugin !(Mail::SpamAssassin::Plugin::DKIM)
+    header  DKIM_VERIFIED  eval:check_authres_result('dkim', 'pass')
+    header  DKIM_INVALID   eval:check_authres_result('dkim', 'fail')
+  endif
+  endif
+
+=back
+
+=cut
+
+sub check_authres_result {
+  my ($self, $pms, $method, $wanted_result) = @_;
+
+  my $result = $pms->{authres_result}->{$method};
+  $wanted_result = lc($wanted_result);
+
+  if ($wanted_result eq 'missing') {
+    return !defined($result) ? 1 : 0;
+  }
+
+  return ($wanted_result eq $result);
+}
+
+sub parsed_metadata {
+  my ($self, $opts) = @_;
+
+  my $pms = $opts->{permsgstatus};
+
+  my @authres;
+  my $nethdr;
+
+  if ($pms->{conf}->{authres_networks} eq 'internal') {
+    $nethdr = 'ALL-INTERNAL';
+  } elsif ($pms->{conf}->{authres_networks} eq 'trusted') {
+    $nethdr = 'ALL-TRUSTED';
+  } else {
+    $nethdr = 'ALL';
+  }
+
+  foreach my $hdr (split(/^/m, $pms->get($nethdr))) {
+    if ($hdr =~ /^(?:Arc\-)?Authentication-Results:\s*(.+)/i) {
+      push @authres, $1;
+    }
+  }
+
+  if (!@authres) {
+    dbg("authres: no Authentication-Results headers found from %s",
+      $pms->{conf}->{authres_networks});
+    return 0;
+  }
+
+  foreach (@authres) {
+    eval {
+      $self->parse_authres($pms, $_);
+    } or do {
+      dbg("authres: skipping header, $@");
+    }
+  }
+
+  $pms->{authres_result} = {};
+  # Set $pms->{authres_result} info for all found methods
+  # 'pass' will always win if multiple results
+  foreach my $method (keys %method_result) {
+    my $parsed = $pms->{authres_parsed}->{$method};
+    next if !$parsed;
+    foreach my $pref (@$parsed) {
+      if (!$pms->{authres_result}->{$method} ||
+            $pref->{result} eq 'pass')
+      {
+        $pms->{authres_result}->{$method} = $pref->{result};
+      }
+    }
+  }
+
+  if (%{$pms->{authres_result}}) {
+    dbg("authres: results: %s",
+      join(' ', map { $_.'='.$pms->{authres_result}->{$_} }
+        sort keys %{$pms->{authres_result}}));
+  } else {
+    dbg("authres: no results");
+  }
+}
+
+sub parse_authres {
+  my ($self, $pms, $hdr) = @_;
+
+  dbg("authres: parsing Authentication-Results: $hdr");
+
+  my $authserv;
+  my $version = 1;
+  my @methods;
+
+  local $_ = $hdr;
+
+  # authserv-id
+  if (!/\G($TOKEN)/gcs) {
+    die("invalid authserv\n");
+  }
+  $authserv = lc($1);
+
+  if (%{$pms->{conf}->{authres_trusted_authserv}}) {
+    if (!$pms->{conf}->{authres_trusted_authserv}->{$authserv}) {
+      die("authserv not trusted: $authserv\n");
+    }
+  }
+  if ($pms->{conf}->{authres_ignored_authserv}->{$authserv}) {
+    die("ignored authserv: $authserv\n");
+  }
+
+  skip_cfws();
+  if (/\G\d+/gcs) { # skip authserv version
+    skip_cfws();
+  }
+  if (!/\G;/gcs) {
+    die("missing delimiter\n");
+  }
+  skip_cfws();
+
+  while (pos() < length()) {
+    my ($method, $result);
+    my $reason = '';
+    my $props = {};
+
+    # skip none method
+    if (/\Gnone\b/igcs) {
+      die("method none\n");
+    }
+
+    # method / version = result
+    if (!/\G([\w-]+)/gcs) {
+      die("invalid method\n");
+    }
+    $method = lc($1);
+    if (!exists $method_result{$method}) {
+      die("unknown method: $method\n");
+    }
+    skip_cfws();
+    if (/\G\//gcs) {
+      skip_cfws();
+      if (!/\G\d+/gcs) {
+        die("invalid $method version\n");
+      }
+      $version = $1;
+      skip_cfws();
+    }
+    if (!/\G=/gcs) {
+      die("missing result for $method: ".substr($_, pos())."\n");
+    }
+    skip_cfws();
+    if (!/\G(\w+)/gcs) {
+      die("invalid result for $method\n");
+    }
+    $result = $1;
+    if (!exists $method_result{$method}{$result}) {
+      die("unknown result for $method: $result\n");
+    }
+    skip_cfws();
+
+    # reason = value
+    if (/\Greason\b/igcs) {
+      skip_cfws();
+      if (!/\G=/gcs) {
+        die("invalid reason\n");
+      }
+      skip_cfws();
+      if (!/\G$QUOTED_STRING|($TOKEN)/gcs) {
+        die("invalid reason\n");
+      }
+      $reason = defined $1 ? $1 : $2;
+      skip_cfws();
+    }
+
+    # ptype.property = value
+    while (pos() < length()) {
+      my ($ptype, $property, $value);
+
+      # ptype
+      if (!/\G(\w+)/gcs) {
+        die("invalid ptype: ".substr($_,pos())."\n");
+      }
+      $ptype = lc($1);
+      if (!exists $method_ptype_prop{$method}{$ptype}) {
+        die("unknown ptype: $ptype\n");
+      }
+      skip_cfws();
+
+      # dot
+      if (!/\G\./gcs) {
+        die("missing property\n");
+      }
+      skip_cfws();
+
+      # property
+      if (!/\G(\w+)/gcs) {
+        die("invalid property\n");
+      }
+      $property = lc($1);
+      if (!exists $method_ptype_prop{$method}{$ptype}{$property} &&
+          !exists $method_ptype_prop{$method}{$ptype}{'*'}) {
+        die("unknown property for $ptype: $property\n");
+      }
+      skip_cfws();
+
+      # =
+      if (!/\G=/gcs) {
+        die("missing property value\n");
+      }
+      skip_cfws();
+
+      # value:
+      # The grammar is ( value / [ [ local-part ] "@" ] domain-name )
+      # where value := token / quoted-string
+      # and local-part := dot-atom / quoted-string / obs-local-part
+      if (!/\G$QUOTED_STRING|($ATOM(?:\.$ATOM)*|$TOKEN)(?=(?:[\s;]|$))/gcs) {
+        die("invalid $ptype.$property value\n");
+      }
+      $value = defined $1 ? $1 : $2;
+      skip_cfws();
+
+      $props->{$ptype}->{$property} = $value;
+
+      if (/\G(?:;|$)/gcs) {
+        skip_cfws();
+        last;
+      }
+    }
+
+    push @methods, [$method, {
+        'authserv' => $authserv,
+        'version' => $version,
+        'result' => $result,
+        'reason' => $reason,
+        'properties' => $props,
+        }];
+  }
+
+  # paranoid check..
+  if (pos() < length()) {
+    die("parse ended prematurely?\n");
+  }
+
+  # Pushed to pms only if header parsed completely
+  foreach my $marr (@methods) {
+    push @{$pms->{authres_parsed}->{$marr->[0]}}, $marr->[1];
+  }
+
+  return 1;
+}
+
+# skip whitespace and comments
+sub skip_cfws {
+  /\G\s*/gcs;
+  if (/\G\(/gcs) {
+    my $i = 1;
+    while (/\G.*?([()]|\z)/gcs) {
+      $1 eq ')' ? $i-- : $i++;
+      last if !$i;
+    }
+    die("comment not ended\n") if $i;
+    /\G\s*/gcs;
+  }
+}
+
+#sub check_cleanup {
+#  my ($self, $opts) = @_;
+#  my $pms = $opts->{permsgstatus};
+#  use Data::Dumper;
+#  print STDERR Dumper($pms->{authres_parsed});
+#  print STDERR Dumper($pms->{authres_result});
+#}
+
+1;
index 9a5a570d8b001b49543fd12106f28736c44e6126..02246d0136b54d5554c6cf58901af2277fb49462 100644 (file)
@@ -105,7 +105,11 @@ Note: SpamAssassin requires at least 3 points from the header, and 3
 points from the body to auto-learn as spam.  Therefore, the minimum
 working value for this option is 6.
 
-If the test option autolearn_force is set, the minimum value will 
+If test option C<autolearn_header> or C<autolearn_body> is set, points from
+that rule are forced to count as coming from header or body accordingly. 
+This can be useful for adjusting some meta rules.
+
+If the test option C<autolearn_force> is set, the minimum value will 
 remain at 6 points but there is no requirement that the points come
 from body and header rules.  This option is useful for autolearning
 with rules that are considered to be extremely safe indicators of 
index 1962c1b7a50173e0fe44b2f367f395571d740da5..4e3146385009865af85c80e04a5ba4632262caf3 100644 (file)
@@ -37,7 +37,43 @@ And the chi-square probability combiner as described here:
 
 The results are incorporated into SpamAssassin as the BAYES_* rules.
 
-=head1 METHODS
+=head1 ADMINISTRATOR SETTINGS
+
+=over 4
+
+=item bayes_stopword_languages lang             (default: en)
+
+Languages enabled in bayes stopwords processing, every language have a
+default stopwords regexp, tokens matching this regular expression will not
+be considered in bayes processing.
+
+Custom regular expressions for additional languages can be defined in C<local.cf>.
+
+Custom regular expressions can be specified by using the C<bayes_stopword_lang>
+keyword like in the following example:
+
+ bayes_stopword_languages en se
+ bayes_stopword_en (?:you|me)
+ bayes_stopword_se (?:du|mig)
+
+Regexps are case-insensitive will be anchored automatically at beginning and
+end.
+
+To disable stopwords usage, specify C<bayes_stopword_languages disable>.
+
+Only one bayes_stopword_languages or bayes_stopword_xx configuration line
+can be used.  New configuration line will override the old one, for example
+the ones from SpamAssassin default ruleset (60_bayes_stopwords.cf).
+
+=back
+
+=over 4
+
+=item bayes_max_token_length (default: 15)
+
+Configure the maximum number of character a token could contain
+
+=back
 
 =cut
 
@@ -48,16 +84,12 @@ use warnings;
 # use bytes;
 use re 'taint';
 
-BEGIN {
-  eval { require Digest::SHA; import Digest::SHA qw(sha1 sha1_hex); 1 }
-  or do { require Digest::SHA1; import Digest::SHA1 qw(sha1 sha1_hex) }
-}
+use Digest::SHA qw(sha1 sha1_hex);
 
-use Mail::SpamAssassin;
 use Mail::SpamAssassin::Plugin;
 use Mail::SpamAssassin::PerMsgStatus;
 use Mail::SpamAssassin::Logger;
-use Mail::SpamAssassin::Util qw(untaint_var);
+use Mail::SpamAssassin::Util qw(compile_regexp untaint_var);
 
 # pick ONLY ONE of these combining implementations.
 use Mail::SpamAssassin::Bayes::CombineChi;
@@ -135,13 +167,16 @@ our $IGNORED_HDRS = qr{(?: (?:X-)?Sender    # misc noise
   | X-Gnus-Mail-Source
   | Xref
 
-)}x;
+)}ix;
 
 # Note only the presence of these headers, in order to reduce the
 # hapaxen they generate.
 our $MARK_PRESENCE_ONLY_HDRS = qr{(?: X-Face
   |X-(?:Gnu-?PG|PGP|GPG)(?:-Key)?-Fingerprint
   |D(?:KIM|omainKey)-Signature
+  |X-Google-DKIM-Signature
+  |ARC-(?:Message-Signature|Seal)
+  |Autocrypt
 )}ix;
 
 # tweaks tested as of Nov 18 2002 by jm posted to -devel at
@@ -220,6 +255,8 @@ use constant REQUIRE_SIGNIFICANT_TOKENS_TO_SCORE => -1;
 
 # How long a token should we hold onto?  (note: German speakers typically
 # will require a longer token than English ones.)
+# This is just a default value, option can be changed using
+# bayes_max_token_length option
 use constant MAX_TOKEN_LENGTH => 15;
 
 ###########################################################################
@@ -236,10 +273,100 @@ sub new {
   $self->{conf} = $main->{conf};
   $self->{use_ignores} = 1;
 
-  $self->register_eval_rule("check_bayes");
+  # Old default stopword list, need to have hardcoded one incase sa-update is not available
+  $self->{bayes_stopword}{en} = qr/(?:a(?:ble|l(?:ready|l)|n[dy]|re)|b(?:ecause|oth)|c(?:an|ome)|e(?:ach|mail|ven)|f(?:ew|irst|or|rom)|give|h(?:a(?:ve|s)|ttp)|i(?:n(?:formation|to)|t\'s)|just|know|l(?:ike|o(?:ng|ok))|m(?:a(?:de|il(?:(?:ing|to))?|ke|ny)|o(?:re|st)|uch)|n(?:eed|o[tw]|umber)|o(?:ff|n(?:ly|e)|ut|wn)|p(?:eople|lace)|right|s(?:ame|ee|uch)|t(?:h(?:at|is|rough|e)|ime)|using|w(?:eb|h(?:ere|y)|ith(?:out)?|or(?:ld|k))|y(?:ears?|ou(?:(?:\'re|r))?))/;
+
+  $self->set_config($self->{conf});
+  $self->register_eval_rule("check_bayes", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
   $self;
 }
 
+sub set_config {
+  my ($self, $conf) = @_;
+  my @cmds;
+
+  push(@cmds, {
+    setting => 'bayes_max_token_length',
+    default => MAX_TOKEN_LENGTH,
+    is_admin => 1,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
+  });
+
+  push(@cmds, {
+    setting => 'bayes_stopword_languages',
+    default => ['en'],
+    is_admin => 1,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRINGLIST,
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      my @langs;
+      if ($value eq 'disable') {
+        @{$self->{bayes_stopword_languages}} = ();
+      }
+      else {
+        foreach my $lang (split(/(?:\s*,\s*|\s+)/, lc($value))) {
+          if ($lang !~ /^([a-z]{2})$/) {
+            return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+          }
+          push @langs, $lang;
+        }
+        if (!@langs) {
+          return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
+        }
+        @{$self->{bayes_stopword_languages}} = @langs;
+      }
+    }
+  });
+
+  $conf->{parser}->register_commands(\@cmds);
+}
+
+sub parse_config {
+  my ($self, $opts) = @_;
+
+  # Ignore users's configuration lines
+  return 0 if $opts->{user_config};
+
+  if ($opts->{key} =~ /^bayes_stopword_([a-z]{2})$/i) {
+      $self->inhibit_further_callbacks();
+      my $lang = lc($1);
+      foreach my $re (split(/\s+/, $opts->{value})) {
+        my ($rec, $err) = compile_regexp('^(?i)'.$re.'$', 0);
+        if (!$rec) {
+          warn "bayes: invalid regexp for $opts->{key}: $err\n";
+          return 0;
+        }
+        $self->{bayes_stopword}{$lang} = $rec;
+      }
+      return 1;
+  }
+
+  return 0;
+}
+
+sub finish_parsing_end {
+  my ($self, $opts) = @_;
+  my $conf = $opts->{conf};
+
+  my @langs;
+  foreach my $lang (@{$conf->{bayes_stopword_languages}}) {
+    if (defined $self->{bayes_stopword}{$lang}) {
+      push @langs, $lang;
+    } else {
+      warn "bayes: missing stopwords regexp for language '$lang'\n";
+    }
+  }
+  if (@langs) {
+    dbg("bayes: stopwords for languages enabled: ".join(' ', @langs));
+    @{$conf->{bayes_stopword_languages}} = @langs;
+  } else {
+    dbg("bayes: no stopword languages enabled");
+    $conf->{bayes_stopword_languages} = [];
+  }
+
+  return 0;
+}
+
 sub finish {
   my $self = shift;
   if ($self->{store}) {
@@ -410,10 +537,11 @@ sub _learn_trapped {
   my @msgid = ( $msgid );
 
   if (!defined $msgid) {
-    @msgid = $self->get_msgid($msg);
+    @msgid = ( $msg->generate_msgid(), $msg->get_msgid() );
   }
 
   foreach my $msgid_t ( @msgid ) {
+    next if !defined $msgid_t;
     my $seen = $self->{store}->seen_get ($msgid_t);
 
     if (defined ($seen)) {
@@ -545,7 +673,7 @@ sub _forget_trapped {
   my $isspam;
 
   if (!defined $msgid) {
-    @msgid = $self->get_msgid($msg);
+    @msgid = ( $msg->generate_msgid(), $msg->get_msgid() );
   }
 
   while( $msgid = shift @msgid ) {
@@ -671,7 +799,6 @@ sub learner_is_scan_available {
 
 sub scan {
   my ($self, $permsgstatus, $msg) = @_;
-  my $score;
 
   return unless $self->{conf}->{use_learner};
 
@@ -756,6 +883,7 @@ sub scan {
   if (@pw_keys > N_SIGNIFICANT_TOKENS) { $#pw_keys = N_SIGNIFICANT_TOKENS - 1 }
 
   my @sorted;
+  my $score;
   foreach my $tok (@pw_keys) {
     next if $tok_strength{$tok} <
                 $Mail::SpamAssassin::Bayes::Combine::MIN_PROB_STRENGTH;
@@ -964,49 +1092,6 @@ sub learner_dump_database {
 ###########################################################################
 # TODO: these are NOT public, but the test suite needs to call them.
 
-sub get_msgid {
-  my ($self, $msg) = @_;
-
-  my @msgid;
-
-  my $msgid = $msg->get_header("Message-Id");
-  if (defined $msgid && $msgid ne '' && $msgid !~ /^\s*<\s*(?:\@sa_generated)?>.*$/) {
-    # remove \r and < and > prefix/suffixes
-    chomp $msgid;
-    $msgid =~ s/^<//; $msgid =~ s/>.*$//g;
-    push(@msgid, $msgid);
-  }
-
-  # Modified 2012-01-17  per bug 5185 to remove last received from msg_id calculation
-
-  # Use sha1_hex(Date: and top N bytes of body)
-  # where N is MIN(1024 bytes, 1/2 of body length)
-  #
-  my $date = $msg->get_header("Date");
-  $date = "None" if (!defined $date || $date eq ''); # No Date?
-
-  #Removed per bug 5185
-  #my @rcvd = $msg->get_header("Received");
-  #my $rcvd = $rcvd[$#rcvd];
-  #$rcvd = "None" if (!defined $rcvd || $rcvd eq ''); # No Received?
-
-  # Make a copy since pristine_body is a reference ...
-  my $body = join('', $msg->get_pristine_body());
-
-  if (length($body) > 64) { # Small Body?
-    my $keep = ( length $body > 2048 ? 1024 : int(length($body) / 2) );
-    substr($body, $keep) = '';
-  }
-
-  #Stripping all CR and LF so that testing midstream from MTA and post delivery don't 
-  #generate different id's simply because of LF<->CR<->CRLF changes.
-  $body =~ s/[\r\n]//g;
-
-  unshift(@msgid, sha1_hex($date."\000".$body).'@sa_generated');
-
-  return wantarray ? @msgid : $msgid[0];
-}
-
 sub get_body_from_msg {
   my ($self, $msg) = @_;
 
@@ -1024,7 +1109,7 @@ sub get_body_from_msg {
 
   if (!defined $msgdata) {
     # why?!
-    warn "bayes: failed to get body for ".scalar($self->get_msgid($self->{msg}))."\n";
+    warn "bayes: failed to get body for ".scalar($self->{msg}->generate_msgid())."\n";
     return { };
   }
 
@@ -1052,8 +1137,10 @@ sub _get_msgdata_from_permsgstatus {
 # The calling functions expect a uniq'ed array of tokens ...
 sub tokenize {
   my ($self, $msg, $msgdata) = @_;
+  my $conf = $self->{conf};
+  my $t_src = $conf->{bayes_token_sources};
 
-  my $t_src = $self->{conf}->{bayes_token_sources};
+  $self->{stopword_cache} = ();
 
   # visible tokens from the body
   my @tokens_body;
@@ -1117,6 +1204,8 @@ sub tokenize {
     dbg("bayes: tokenized header: %d tokens", scalar @tokens_header);
   }
 
+  delete $self->{stopword_cache};
+
   # Go ahead and uniq the array, skip null tokens (can happen sometimes)
   # generate an SHA1 hash and take the lower 40 bits as our token
   my %tokens;
@@ -1137,6 +1226,7 @@ sub _tokenize_line {
   my $region = $_[3];
   local ($_) = $_[1];
 
+  my $conf = $self->{conf};
   my @rettokens;
 
   # include quotes, .'s and -'s for URIs, and [$,]'s for Nigerian-scam strings,
@@ -1174,7 +1264,7 @@ sub _tokenize_line {
   # cleared, even if the source string has perl characters semantics !!!
   # Is this really still desirable?
 
-  foreach my $token (split) {
+TOKEN: foreach my $token (split) {
     $token =~ s/^[-'"\.,]+//;        # trim non-alphanum chars at start or end
     $token =~ s/[-'"\.,]+$//;        # so we don't get loads of '"foo' tokens
 
@@ -1183,7 +1273,7 @@ sub _tokenize_line {
     # tokens, so the SQL BayesStore returns undef.  I really want a way
     # of optimizing that out, but I haven't come up with anything yet.
     #
-    next if ( defined $magic_re && $token =~ /$magic_re/ );
+    next if ( defined $magic_re && $token =~ /$magic_re/o );
 
     # *do* keep 3-byte tokens; there's some solid signs in there
     my $len = length($token);
@@ -1192,8 +1282,24 @@ sub _tokenize_line {
     # area, and it just slows us down to record them.
     # See http://wiki.apache.org/spamassassin/BayesStopList for more info.
     #
-    next if $len < 3 ||
-       ($token =~ /^(?:a(?:ble|l(?:ready|l)|n[dy]|re)|b(?:ecause|oth)|c(?:an|ome)|e(?:ach|mail|ven)|f(?:ew|irst|or|rom)|give|h(?:a(?:ve|s)|ttp)|i(?:n(?:formation|to)|t\'s)|just|know|l(?:ike|o(?:ng|ok))|m(?:a(?:de|il(?:(?:ing|to))?|ke|ny)|o(?:re|st)|uch)|n(?:eed|o[tw]|umber)|o(?:ff|n(?:ly|e)|ut|wn)|p(?:eople|lace)|right|s(?:ame|ee|uch)|t(?:h(?:at|is|rough|e)|ime)|using|w(?:eb|h(?:ere|y)|ith(?:out)?|or(?:ld|k))|y(?:ears?|ou(?:(?:\'re|r))?))$/i);
+    next if $len < 3;
+
+    # check stopwords regexp if not cached
+    if (@{$conf->{bayes_stopword_languages}}) {
+      if (!exists $self->{stopword_cache}{$token}) {
+        foreach my $lang (@{$conf->{bayes_stopword_languages}}) {
+          if ($token =~ $self->{bayes_stopword}{$lang}) {
+            dbg("bayes: skipped token '$token' because it's in stopword list for language '$lang'");
+            $self->{stopword_cache}{$token} = 1;
+            next TOKEN;
+          }
+        }
+        $self->{stopword_cache}{$token} = 0;
+      } else {
+        # bail out if cached known
+        next if $self->{stopword_cache}{$token};
+      }
+    }
 
     # are we in the body?  If so, apply some body-specific breakouts
     if ($region == 1 || $region == 2) {
@@ -1212,7 +1318,7 @@ sub _tokenize_line {
     # used as part of split tokens such as "HTo:D*net" indicating that 
     # the domain ".net" appeared in the To header.
     #
-    if ($len > MAX_TOKEN_LENGTH && $token !~ /\*/) {
+    if ($len > $conf->{bayes_max_token_length} && index($token, '*') == -1) {
 
       if (TOKENIZE_LONG_8BIT_SEQS_AS_UTF8_CHARS && $token =~ /[\x80-\xBF]{2}/) {
        # Bug 7135
@@ -1287,9 +1393,6 @@ sub _tokenize_headers {
 
   my %parsed;
 
-  my %user_ignore;
-  $user_ignore{lc $_} = 1 for @{$self->{main}->{conf}->{bayes_ignore_headers}};
-
   # get headers in array context
   my @hdrs;
   my @rcvdlines;
@@ -1317,7 +1420,7 @@ sub _tokenize_headers {
 
     # remove user-specified headers here, after Received, in case they
     # want to ignore that too
-    next if exists $user_ignore{lc $hdr};
+    next if exists $self->{conf}->{bayes_ignore_header}->{lc $hdr};
 
     # Prep the header value
     $val ||= '';
@@ -1344,20 +1447,36 @@ sub _tokenize_headers {
     elsif ($hdr =~ /^${MARK_PRESENCE_ONLY_HDRS}$/i) {
       $val = "1"; # just mark the presence, they create lots of hapaxen
     }
+    elsif ($hdr =~ /^x-spam-relays-(?:external|internal|trusted|untrusted)$/) {
+      # remove redundant rdns helo ident envfrom intl auth msa words
+      $val =~ s/ [a-z]+=/ /g;
+    }
 
     if (MAP_HEADERS_MID) {
       if ($hdr =~ /^(?:In-Reply-To|References|Message-ID)$/i) {
-        $parsed{"*MI"} = $val;
+        if (exists $parsed{"*MI"}) {
+          $parsed{"*MI"} .= " ".$val;
+        } else {
+          $parsed{"*MI"} = $val;
+        }
       }
     }
     if (MAP_HEADERS_FROMTOCC) {
       if ($hdr =~ /^(?:From|To|Cc)$/i) {
-        $parsed{"*Ad"} = $val;
+        if (exists $parsed{"*Ad"}) {
+          $parsed{"*Ad"} .= " ".$val;
+        } else {
+          $parsed{"*Ad"} = $val;
+        }
       }
     }
     if (MAP_HEADERS_USERAGENT) {
       if ($hdr =~ /^(?:X-Mailer|User-Agent)$/i) {
-        $parsed{"*UA"} = $val;
+        if (exists $parsed{"*UA"}) {
+          $parsed{"*UA"} .= " ".$val;
+        } else {
+          $parsed{"*UA"} = $val;
+        }
       }
     }
 
@@ -1371,11 +1490,13 @@ sub _tokenize_headers {
     } else {
       $parsed{$hdr} = $val;
     }
-    if (would_log('dbg', 'bayes') > 1) {
+  }
+
+  if (would_log('dbg', 'bayes') > 1) {
+    foreach my $hdr (sort keys %parsed) {
       dbg("bayes: header tokens for $hdr = \"$parsed{$hdr}\"");
     }
   }
-
   return %parsed;
 }
 
@@ -1393,7 +1514,7 @@ sub _pre_chew_content_type {
   }
 
   # stop-list words for Content-Type header: these wind up totally gray
-  $val =~ s/\b(?:text|charset)\b//;
+  $val =~ s/\b(?:text|charset)\b/ /g;
 
   $val;
 }
@@ -1468,10 +1589,17 @@ sub _pre_chew_addr_header {
   my ($self, $val) = @_;
   local ($_);
 
-  my @addrs = $self->{main}->find_all_addrs_in_line ($val);
+  my @addrs = Mail::SpamAssassin::Util::parse_header_addresses($val);
   my @toks;
-  foreach (@addrs) {
-    push (@toks, $self->_tokenize_mail_addrs ($_));
+  foreach my $addr (@addrs) {
+    if (defined $addr->{phrase}) {
+      foreach (split(/\s+/, $addr->{phrase})) {
+        push @toks, "N*".$_; # Bug 6319
+      }
+    }
+    if (defined $addr->{address}) {
+      push @toks, $self->_tokenize_mail_addrs($addr->{address});
+    }
   }
   return join (' ', @toks);
 }
index 5e0f318e3cda7857ab2d6dcb8753ed646a7f7fd2..f85c1258571f834f9514d6625d65ab9698719257 100644 (file)
@@ -39,16 +39,16 @@ sub new {
   bless ($self, $class);
 
   # the important bit!
-  $self->register_eval_rule("multipart_alternative_difference");
-  $self->register_eval_rule("multipart_alternative_difference_count");
-  $self->register_eval_rule("check_blank_line_ratio");
-  $self->register_eval_rule("tvd_vertical_words");
-  $self->register_eval_rule("check_stock_info");
-  $self->register_eval_rule("check_body_length");
+  $self->register_eval_rule("multipart_alternative_difference", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("multipart_alternative_difference_count", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("check_blank_line_ratio", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("tvd_vertical_words", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("check_stock_info", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("check_body_length", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
 
-  $self->register_eval_rule("plaintext_body_length");
-  $self->register_eval_rule("plaintext_sig_length");
-  $self->register_eval_rule("plaintext_body_sig_ratio");
+  $self->register_eval_rule("plaintext_body_length", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("plaintext_sig_length", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("plaintext_body_sig_ratio", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
 
   return $self;
 }
@@ -69,7 +69,7 @@ sub multipart_alternative_difference_count {
   my ($self, $pms, $fulltext, $ratio, $minhtml) = @_;
   $self->_multipart_alternative_difference($pms) unless (exists $pms->{madiff});
   return 0 unless $pms->{madiff_html} > $minhtml;
-  return(($pms->{madiff_text} / $pms->{madiff_html}) > $ratio);
+  return (($pms->{madiff_text} / $pms->{madiff_html}) > $ratio);
 }
 
 sub _multipart_alternative_difference {
@@ -81,7 +81,7 @@ sub _multipart_alternative_difference {
   my $msg = $pms->{msg};
 
   # Find all multipart/alternative parts in the message
-  my @ma = $msg->find_parts(qr@^multipart/alternative\b@i);
+  my @ma = $msg->find_parts(qr@^multipart/alternative\b@);
 
   # If there are no multipart/alternative sections, skip this test.
   return if (!@ma);
@@ -104,7 +104,7 @@ sub _multipart_alternative_difference {
     my %text;
 
     # limit our search to text-based parts
-    my @txt = $part->find_parts(qr@^text\b@i);
+    my @txt = $part->find_parts(qr@^text\b@);
     foreach my $text (@txt) {
       # we only care about the rendered version of the part
       my ($type, $rnd) = $text->rendered();
@@ -123,7 +123,7 @@ sub _multipart_alternative_difference {
         }
 
        # If there are no words, mark if there's at least 1 image ...
-       if (!%html && exists $pms->{html}{inside}{img}) {
+       if (!%html && exists $text->{html_results}{inside}{img}) {
          # Use "\n" as the mark since it can't ever occur normally
          $html{"\n"}=1;
        }
@@ -222,7 +222,7 @@ sub tvd_vertical_words {
   }
 
   dbg("eval: tvd_vertical_words value: $pms->{tvd_vertical_words} / min: $min / max: $max - value must be >= min and < max");
-  return 1 if ($pms->{tvd_vertical_words} >= $min && $pms->{tvd_vertical_words} < $max);
+  return ($pms->{tvd_vertical_words} >= $min && $pms->{tvd_vertical_words} < $max);
 }
 
 sub check_stock_info {
@@ -241,7 +241,7 @@ sub _check_stock_info {
   $pms->{stock_info} = 0;
 
   # Find all multipart/alternative parts in the message
-  my @parts = $pms->{msg}->find_parts(qr@^text/plain$@i);
+  my @parts = $pms->{msg}->find_parts(qr@^text/plain$@);
   return if (!@parts);
 
   # Go through each of the multipart parts
@@ -360,11 +360,17 @@ sub _plaintext_body_sig_ratio {
 
   # Find the last occurence of a signature delimiter and get the body and
   # signature lengths.
-  my ($len_b, $len_s) = map { length } $text =~ /(^|.*\n)-- \n(.*?)$/s;
 
-  if (! defined $len_b) {     # no sig marker, all body
-      $len_b = length $text;
-      $len_s = 0;
+  my $len_b = length($text);
+  my $len_s = 0;
+
+  while ($text =~ /^-- ?\r?$/mg) {
+
+    # ignore decoy marker at the end
+    next if ( length($text) - $+[0] <= 4 );
+
+    $len_b = $-[0];
+    $len_s = length($text) - $+[0];
   }
 
   $pms->{plaintext_body_sig_ratio}->{body_length} = $len_b;
index 7c034ccac5739611b0141d7ab782dfd6545f892f..75aa2b73877f8541a2eaf0581449df50478a6a95 100644 (file)
@@ -119,6 +119,12 @@ sub extract_set {
     my $nicepri = $pri; $nicepri =~ s/-/neg/g;
     $self->extract_set_pri($conf, $test_set->{$pri}, $ruletype.'_'.$nicepri);
   }
+
+  # Clear extract_hints tmpfile
+  if ($self->{tmpf}) {
+    unlink $self->{tmpf};
+    delete $self->{tmpf};
+  }
 }
 
 ###########################################################################
@@ -190,11 +196,13 @@ NEXT_RULE:
       next NEXT_RULE;
     }
 
-    # ignore ReplaceTags rules
-    my $is_a_replacetags_rule = $conf->{replace_rules}->{$name};
+    # ignore ReplaceTags rules, and regex capture template rules
+    my $is_a_replace_rule = $conf->{replace_rules}->{$name} ||
+                            $conf->{capture_rules}->{$name} ||
+                            $conf->{capture_template_rules}->{$name};
     my ($minlen, $lossy, @bases);
 
-    if (!$is_a_replacetags_rule) {
+    if (!$is_a_replace_rule) {
       eval {  # catch die()s
         my ($qr, $mods) = $self->simplify_and_qr_regexp($rule);
         ($lossy, @bases) = $self->extract_hints($rule, $qr, $mods);
@@ -202,6 +210,7 @@ NEXT_RULE:
         1;
       } or do {
         my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
+        $eval_stat =~ s/ at .*//s;
         dbg("zoom: giving up on regexp: $eval_stat");
       };
 
@@ -220,9 +229,9 @@ NEXT_RULE:
       }
     }
 
-    if ($is_a_replacetags_rule || !$minlen || !@bases) {
+    if ($is_a_replace_rule || !$minlen || !@bases) {
       dbg("zoom: ignoring rule %s, %s", $name,
-          $is_a_replacetags_rule ? 'is a replace rule'
+          $is_a_replace_rule ? 'is a replace rule'
           : !@bases ? 'no bases' : 'no minlen');
       push @failed, { orig => $rule };
       $cached->{rule_bases}->{$cachekey} = { };
@@ -469,7 +478,7 @@ sub simplify_and_qr_regexp {
   }
   else {
     die "case-i" if $rule =~ /\(\?i\)/;
-    die "case-i" if $mods =~ /i/;
+    die "case-i" if index($mods, 'i') >= 0;
 
     # always case-i: /A(?i:ct) N(?i:ow)/ => /Act Now/
     $rule =~ s/(?<!\\)\(\?i\:(.*?)\)/$1/gs and die "case-i";
@@ -504,10 +513,7 @@ sub simplify_and_qr_regexp {
 }
 
 sub extract_hints {
-  my $self = shift;
-  my $rawrule = shift;
-  my $rule = shift;
-  my $mods = shift;
+  my ($self, $rawrule, $rule, $mods) = @_;
 
   my $main = $self->{main};
   my $orig = $rule;
@@ -534,24 +540,30 @@ sub extract_hints {
   # r? => (r|)
   $rule =~ s/(?<!\\)(\w)\?/\($1\|\)/gs;
 
-  my ($tmpf, $tmpfh) = Mail::SpamAssassin::Util::secure_tmpfile();
-  $tmpfh  or die "failed to create a temporary file";
-  untaint_var(\$tmpf);
+  # Create single tmpfile for extract_hints to use, instead of thousands
+  if (!$self->{tmpf}) {
+    ($self->{tmpf}, my $tmpfh) = Mail::SpamAssassin::Util::secure_tmpfile();
+    $tmpfh  or die "failed to create a temporary file";
+    close $tmpfh;
+    $self->{tmpf} = untaint_var($self->{tmpf});
+  }
 
+  open(my $tmpfh, '>'.$self->{tmpf})
+    or die "error opening $self->{tmpf}: $!";
+  binmode $tmpfh;
   print $tmpfh "use bytes; m{" . $rule . "}" . $mods
-    or die "error writing to $tmpf: $!";
-  close $tmpfh  or die "error closing $tmpf: $!";
+    or die "error writing to $self->{tmpf}: $!";
+  close $tmpfh  or die "error closing $self->{tmpf}: $!";
 
-  my $perl = $self->get_perl();
+  $self->{perl} = $self->get_perl()  if !exists $self->{perl};
   local *IN;
-  open (IN, "$perl -c -Mre=debug $tmpf 2>&1 |")
-    or die "cannot run $perl: ".exit_status_str($?,$!);
+  open (IN, "$self->{perl} -c -Mre=debug $self->{tmpf} 2>&1 |")
+    or die "cannot run $self->{perl}: ".exit_status_str($?,$!);
 
   my($inbuf,$nread,$fullstr); $fullstr = '';
   while ( $nread=read(IN,$inbuf,16384) ) { $fullstr .= $inbuf }
   defined $nread  or die "error reading from pipe: $!";
 
-  unlink $tmpf  or die "cannot unlink $tmpf: $!";
   close IN      or die "error closing pipe: $!";
   defined $fullstr  or warn "empty result from a pipe";
 
@@ -1110,7 +1122,7 @@ sub fixup_re {
         $output .= "\"$esc\"";
       }
     }
-    else {
+    elsif ($fixup_re_test) {
       print "PRE: $pre\nTOK: $tok\n"  or die "error writing: $!";
     }
   }
@@ -1132,6 +1144,8 @@ sub fixup_re {
   $output =~ s/\*\*BACKSLASH\*\*/\\\\/gs;
 
   if ($fixup_re_test) { print "OUTPUT: $output\n"  or die "error writing: $!" }
+
+  utf8::encode($output)  if utf8::is_utf8($output); # force octets
   return $output;
 }
 
index e8d933da0cb4f687ec5af649ba70149d355179c7..3b5578148cec05a758e8c50c392a7b7d98a3245c 100644 (file)
@@ -28,12 +28,12 @@ use Mail::SpamAssassin::Constants qw(:sa);
 
 our @ISA = qw(Mail::SpamAssassin::Plugin);
 
-my $ARITH_EXPRESSION_LEXER = ARITH_EXPRESSION_LEXER;
-my $META_RULES_MATCHING_RE = META_RULES_MATCHING_RE;
-
 # methods defined by the compiled ruleset; deleted in finish_tests()
 our @TEMPORARY_METHODS;
 
+# will cache would_log('dbg', 'rules-all') later
+my $would_log_rules_all = 0;
+
 # constructor
 sub new {
   my $class = shift;
@@ -52,7 +52,23 @@ sub check_main {
   my ($self, $args) = @_;
 
   my $pms = $args->{permsgstatus};
+  my $conf = $pms->{conf};
+  $would_log_rules_all = would_log('dbg', 'rules-all') == 2;
+
+  # Make AsyncLoop wait launch_queue() for launching queries
+  $pms->{async}->start_queue();
+
+  # initialize meta stuff
+  $pms->{meta_pending} = {};
+  foreach my $rulename (keys %{$conf->{meta_tests}}) {
+    $pms->{meta_pending}->{$rulename} = 1  if $conf->{scores}->{$rulename};
+  }
+  # metas without dependencies are ready to be run
+  foreach my $rulename (keys %{$conf->{meta_nodeps}}) {
+    $pms->{meta_check_ready}->{$rulename} = 1;
+  }
 
+  # rule_hits API implemented in 3.3.0
   my $suppl_attrib = $pms->{msg}->{suppl_attrib};
   if (ref $suppl_attrib && ref $suppl_attrib->{rule_hits}) {
     my @caller_rule_hits = @{$suppl_attrib->{rule_hits}};
@@ -63,6 +79,8 @@ sub check_main {
          $ruletype, $tflags, $description) =
         @$caller_rule_hit{qw(rule area score defscore value
                              ruletype tflags descr)};
+      dbg("rules: ran rule_hits rule $rulename ======> got hit (%s)",
+          defined $value ? $value : '1');
       $pms->got_hit($rulename, $area,
                     !defined $score ? () : (score => $score),
                     !defined $defscore ? () : (defscore => $defscore),
@@ -70,6 +88,8 @@ sub check_main {
                     !defined $tflags ? () : (tflags => $tflags),
                     !defined $description ? () : (description => $description),
                     ruletype => $ruletype);
+      delete $pms->{meta_pending}->{$rulename};
+      delete $pms->{meta_check_ready}->{$rulename};
     }
   }
 
@@ -79,10 +99,8 @@ sub check_main {
   # rbl calls.
   $pms->extract_message_metadata();
 
-  # Here, we launch all the DNS RBL queries and let them run while we
-  # inspect the message
-  $self->run_rbl_eval_tests($pms);
-  my $needs_dnsbl_harvest_p = 1; # harvest needs to be run
+  my $do_dns = $pms->is_dns_available();
+  my $rbls_running = 0;
 
   my $decoded = $pms->get_decoded_stripped_body_text_array();
   my $bodytext = $pms->get_decoded_body_text_array();
@@ -91,12 +109,14 @@ sub check_main {
   dbg("check: check_main, time limit in %.3f s",
       $master_deadline - time)  if $master_deadline;
 
-  my @uris = $pms->get_uri_list();
+  # Make sure priority -100 exists for launching DNS
+  $conf->{priorities}->{-100} ||= 1 if $do_dns;
 
-  foreach my $priority (sort { $a <=> $b } keys %{$pms->{conf}->{priorities}}) {
+  my @priorities = sort { $a <=> $b } keys %{$conf->{priorities}};
+  foreach my $priority (@priorities) {
     # no need to run if there are no priorities at this level.  This can
     # happen in Conf.pm when we switch a rule from one priority to another
-    next unless ($pms->{conf}->{priorities}->{$priority} > 0);
+    next unless ($conf->{priorities}->{$priority} > 0);
 
     if ($pms->{deadline_exceeded}) {
       last;
@@ -107,96 +127,78 @@ sub check_main {
     } elsif ($self->{main}->call_plugins("have_shortcircuited",
                                          { permsgstatus => $pms })) {
       # if shortcircuiting is hit, we skip all other priorities...
+      $pms->{shortcircuited} = 1;
       last;
     }
 
     my $timer = $self->{main}->time_method("tests_pri_".$priority);
     dbg("check: running tests for priority: $priority");
 
-    # only harvest the dnsbl queries once priority HARVEST_DNSBL_PRIORITY
-    # has been reached and then only run once
-    #
-    # TODO: is this block still needed here? is HARVEST_DNSBL_PRIORITY used?
-    #
-    if ($priority >= HARVEST_DNSBL_PRIORITY
-        && $needs_dnsbl_harvest_p
-        && !$self->{main}->call_plugins("have_shortcircuited",
-                                        { permsgstatus => $pms }))
-    {
-      # harvest the DNS results
-      $pms->harvest_dnsbl_queries();
-      $needs_dnsbl_harvest_p = 0;
-
-      # finish the DNS results
-      $pms->rbl_finish();
-      $self->{main}->call_plugins("check_post_dnsbl", { permsgstatus => $pms });
-      $pms->{resolver}->finish_socket() if $pms->{resolver};
+    # Here, we launch all the DNS RBL queries and let them run while we
+    # inspect the message.  We try to launch all DNS queries at priority
+    # -100, so one can shortcircuit tests at lower priority and not launch
+    # unneeded DNS queries.
+    if ($do_dns && !$rbls_running && $priority >= -100) {
+      $rbls_running = 1;
+      $pms->{async}->launch_queue(); # check if something was queued
+      $self->run_rbl_eval_tests($pms);
+      $self->{main}->call_plugins ("check_dnsbl", { permsgstatus => $pms });
     }
 
-    $pms->harvest_completed_queries();
+    $pms->harvest_completed_queries() if $rbls_running;
     # allow other, plugin-defined rule types to be called here
     $self->{main}->call_plugins ("check_rules_at_priority",
         { permsgstatus => $pms, priority => $priority, checkobj => $self });
 
     # do head tests
     $self->do_head_tests($pms, $priority);
-    $pms->harvest_completed_queries();
-    last if $pms->{deadline_exceeded};
+    $pms->harvest_completed_queries() if $rbls_running;
+    last if $pms->{deadline_exceeded} || $pms->{shortcircuited};
 
     $self->do_head_eval_tests($pms, $priority);
-    $pms->harvest_completed_queries();
-    last if $pms->{deadline_exceeded};
+    $pms->harvest_completed_queries() if $rbls_running;
+    last if $pms->{deadline_exceeded} || $pms->{shortcircuited};
 
     $self->do_body_tests($pms, $priority, $decoded);
-    $pms->harvest_completed_queries();
-    last if $pms->{deadline_exceeded};
+    $pms->harvest_completed_queries() if $rbls_running;
+    last if $pms->{deadline_exceeded} || $pms->{shortcircuited};
 
-    $self->do_uri_tests($pms, $priority, @uris);
-    $pms->harvest_completed_queries();
-    last if $pms->{deadline_exceeded};
+    $self->do_uri_tests($pms, $priority, $pms->get_uri_list());
+    $pms->harvest_completed_queries() if $rbls_running;
+    last if $pms->{deadline_exceeded} || $pms->{shortcircuited};
 
     $self->do_body_eval_tests($pms, $priority, $decoded);
-    $pms->harvest_completed_queries();
-    last if $pms->{deadline_exceeded};
+    $pms->harvest_completed_queries() if $rbls_running;
+    last if $pms->{deadline_exceeded} || $pms->{shortcircuited};
   
     $self->do_rawbody_tests($pms, $priority, $bodytext);
-    $pms->harvest_completed_queries();
-    last if $pms->{deadline_exceeded};
+    $pms->harvest_completed_queries() if $rbls_running;
+    last if $pms->{deadline_exceeded} || $pms->{shortcircuited};
 
     $self->do_rawbody_eval_tests($pms, $priority, $bodytext);
-    $pms->harvest_completed_queries();
-    last if $pms->{deadline_exceeded};
+    $pms->harvest_completed_queries() if $rbls_running;
+    last if $pms->{deadline_exceeded} || $pms->{shortcircuited};
   
     $self->do_full_tests($pms, $priority, \$fulltext);
-    $pms->harvest_completed_queries();
-    last if $pms->{deadline_exceeded};
+    $pms->harvest_completed_queries() if $rbls_running;
+    last if $pms->{deadline_exceeded} || $pms->{shortcircuited};
 
     $self->do_full_eval_tests($pms, $priority, \$fulltext);
-    $pms->harvest_completed_queries();
-    last if $pms->{deadline_exceeded};
-
-    $self->do_meta_tests($pms, $priority);
-    $pms->harvest_completed_queries();
-    last if $pms->{deadline_exceeded};
+    $pms->harvest_completed_queries() if $rbls_running;
+    last if $pms->{deadline_exceeded} || $pms->{shortcircuited};
 
     # we may need to call this more often than once through the loop, but
     # it needs to be done at least once, either at the beginning or the end.
     $self->{main}->call_plugins ("check_tick", { permsgstatus => $pms });
-    $pms->harvest_completed_queries();
-    last if $pms->{deadline_exceeded};
-  }
+    $pms->harvest_completed_queries() if $rbls_running;
 
-  # sanity check, it is possible that no rules >= HARVEST_DNSBL_PRIORITY ran so the harvest
-  # may not have run yet.  Check, and if so, go ahead and harvest here.
-  if ($needs_dnsbl_harvest_p) {
-    if (!$self->{main}->call_plugins("have_shortcircuited",
-                                        { permsgstatus => $pms }))
-    {
-      # harvest the DNS results
-      $pms->harvest_dnsbl_queries();
-    }
+    # check for ready metas
+    $self->do_meta_tests($pms, $priority);
+  }
 
-    # finish the DNS results
+  # Finish DNS results
+  if ($do_dns) {
+    $pms->harvest_dnsbl_queries();
     $pms->rbl_finish();
     $self->{main}->call_plugins ("check_post_dnsbl", { permsgstatus => $pms });
     $pms->{resolver}->finish_socket() if $pms->{resolver};
@@ -213,6 +215,32 @@ sub check_main {
   undef $bodytext;
   undef $fulltext;
 
+  # last chance to handle left callbacks, make rule hits etc
+  $self->{main}->call_plugins ("check_cleanup", { permsgstatus => $pms });
+
+  # final check for ready metas
+  $self->do_meta_tests($pms, undef, 1);
+
+  # check dns_block_rule (bug 6728)
+  # TODO No idea yet what would be the most logical place to do all these..
+  if ($conf->{dns_block_rule}) {
+    foreach my $rule (keys %{$conf->{dns_block_rule}}) {
+      next if !$pms->{tests_already_hit}->{$rule}; # hit?
+      foreach my $domain (keys %{$conf->{dns_block_rule}{$rule}}) {
+        my $blockfile = $self->{main}->sed_path("__global_state_dir__/dnsblock_$domain");
+        next if -f $blockfile; # no need to warn and create again..
+        warn "check: dns_block_rule $rule hit, creating $blockfile ".
+             "(This means DNSBL blocked you due to too many queries. ".
+             "Set all affected rules score to 0, or use ".
+             "\"dns_query_restriction deny $domain\" to disable queries)\n";
+        Mail::SpamAssassin::Util::touch_file($blockfile, { create_exclusive => 1 });
+      }
+    }
+  }
+
+  # PMS cleanup will write reports etc, all rule hits must be registered by now
+  $pms->check_cleanup();
+
   if ($pms->{deadline_exceeded}) {
   # dbg("check: exceeded time limit, skipping auto-learning");
   } elsif ($master_deadline && time > $master_deadline) {
@@ -226,7 +254,7 @@ sub check_main {
 
   # track user_rules recompilations; each scanned message is 1 tick on this counter
   if ($self->{done_user_rules}) {
-    my $counters = $pms->{conf}->{want_rebuild_for_type};
+    my $counters = $conf->{want_rebuild_for_type};
     foreach my $type (keys %{$self->{done_user_rules}}) {
       if ($counters->{$type} > 0) {
         $counters->{$type}--;
@@ -250,26 +278,84 @@ sub finish_tests {
 
 ###########################################################################
 
-sub run_rbl_eval_tests {
-  my ($self, $pms) = @_;
-  my ($rulename, $pat, @args);
+sub do_meta_tests {
+  my ($self, $pms, $priority, $finish) = @_;
+
+  return if $pms->{deadline_exceeded} || $pms->{shortcircuited};
 
-  # XXX - possible speed up, moving this check out of the subroutine into Check->new()
-  if ($self->{main}->{local_tests_only}) {
-    dbg("rules: local tests only, ignoring RBL eval");
-    return 0;
+  # Needed for Reuse to work, otherwise we don't care about priorities
+  if (defined $priority && $self->{main}->have_plugin('start_rules')) {
+    $self->{main}->call_plugins('start_rules', {
+      permsgstatus => $pms,
+      ruletype => 'meta',
+      priority => $priority
+    });
+  }
+
+  return if $self->{am_compiling}; # nothing to compile here
+  return if !$finish && !$pms->{meta_check_ready}; # nothing to check
+
+  my $mr = $pms->{meta_check_ready};
+  my $mp = $pms->{meta_pending};
+  my $md = $pms->{conf}->{meta_dependencies};
+  my $mt = $pms->{conf}->{meta_tests};
+  my $h = $pms->{tests_already_hit};
+  my $retry;
+
+  # When finishing, first mark all unrun non-meta rules as finished,
+  # it will enable the next loop to finish everything properly
+  if ($finish) {
+    foreach my $rulename (keys %$mp) {
+      foreach my $deprule (@{$md->{$rulename}||[]}) {
+        if (!exists $mt->{$deprule}) {
+          $h->{$deprule} ||= 0;
+        }
+      }
+    }
+  }
+
+RULE:
+  foreach my $rulename ($finish ? keys %$mp : keys %$mr) {
+    # Meta is not ready if some dependency has not run yet
+    foreach my $deprule (@{$md->{$rulename}||[]}) {
+      if (!exists $h->{$deprule}) {
+        next RULE;
+      }
+    }
+    # Metasubs look like ($_[1]->{$rulename}||0) ...
+    my $result = $mt->{$rulename}->($pms, $h);
+    if ($result) {
+      dbg("rules: ran meta rule $rulename ======> got hit ($result)");
+      $pms->got_hit($rulename, '', ruletype => 'meta', value => $result);
+    } else {
+      dbg("rules-all: ran meta rule $rulename, no hit") if $would_log_rules_all;
+      $pms->rule_ready($rulename, 1); # mark meta done
+    }
+    delete $mr->{$rulename};
+    delete $mp->{$rulename};
+    # Reiterate all metas again, in case some meta depended on us
+    $retry = 1;
   }
 
+  goto RULE if $retry--;
+
+  delete $pms->{meta_check_ready};
+}
+
+###########################################################################
+
+sub run_rbl_eval_tests {
+  my ($self, $pms) = @_;
+
   while (my ($rulename, $test) = each %{$pms->{conf}->{rbl_evals}}) {
     my $score = $pms->{conf}->{scores}->{$rulename};
     next unless $score;
 
-    %{$pms->{test_log_msgs}} = ();        # clear test state
-
     my $function = $test->[0];
     if (!exists $pms->{conf}->{eval_plugins}->{$function}) {
-      warn("rules: unknown eval '$function' for $rulename, ignoring RBL eval\n");
-      return 0;
+      warn "rules: unknown eval '$function' for $rulename, ignoring RBL eval\n";
+      $pms->{rule_errors}++;
+      next;
     }
 
     my $result;
@@ -277,7 +363,7 @@ sub run_rbl_eval_tests {
       $result = $pms->$function($rulename, @{$test->[1]});  1;
     } or do {
       my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
-      die "rules: $eval_stat\n"  if $eval_stat =~ /__alarm__ignore__/;
+      die "rules: $eval_stat\n"  if index($eval_stat, '__alarm__ignore__') >= 0;
       warn "rules: failed to run $rulename RBL test, skipping:\n".
            "\t($eval_stat)\n";
       $pms->{rule_errors}++;
@@ -300,12 +386,12 @@ sub run_generic_tests {
     return;
   } elsif ($self->{main}->call_plugins("have_shortcircuited",
                                         { permsgstatus => $pms })) {
+    $pms->{shortcircuited} = 1;
     return;
   }
 
   my $ruletype = $opts{type};
   dbg("rules: running $ruletype tests; score so far=".$pms->{score});
-  %{$pms->{test_log_msgs}} = ();        # clear test state
 
   my $conf = $pms->{conf};
   my $doing_user_rules = $conf->{want_rebuild_for_type}->{$opts{consttype}};
@@ -342,6 +428,7 @@ sub run_generic_tests {
         # start_rules_plugin_code '.$ruletype.' '.$priority.'
         my $scoresptr = $self->{conf}->{scores};
         my $qrptr = $self->{conf}->{test_qrs};
+        my $test_qr;
     ');
     if (defined $opts{pre_loop_body}) {
       $opts{pre_loop_body}->($self, $pms, $conf, %nopts);
@@ -394,6 +481,7 @@ EOT
     dbg("rules: run_generic_tests - compiling eval code: %s, priority %s",
         $ruletype, $priority);
   # dbg("rules: eval code to compile: %s", $evalstr);
+
     my $eval_result;
     { my $timer = $self->{main}->time_method('compile_gen');
       $eval_result = eval($evalstr);
@@ -436,6 +524,7 @@ package $package_name;
 sub $chunk_methodname {
   my \$self = shift;
   my \$hits = 0;
+  my \%captures;
 EOT
   $evalstr .= '  '.$_  for @{$self->{evalstr_chunk_prefix}};
   $self->{evalstr} = $evalstr;
@@ -519,170 +608,12 @@ sub add_evalstr2 {
 
 sub add_temporary_method {
   my ($self, $methodname, $methodbody) = @_;
-  $self->add_evalstr2 (' sub '.$methodname.' { '.$methodbody.' } ');
+  $self->add_evalstr2(' sub '.$methodname.' { '.$methodbody.' } '."\n");
   push (@TEMPORARY_METHODS, $methodname);
 }
 
 ###########################################################################
 
-# Returns all rulenames matching glob (FOO_*)
-sub expand_ruleglob {
-  my ($self, $ruleglob, $pms, $conf, $rulename) = @_;
-  my $expanded;
-  if (exists $pms->{ruleglob_cache}{$ruleglob}) {
-    $expanded = $pms->{ruleglob_cache}{$ruleglob};
-  } else {
-    my $reglob = $ruleglob;
-    $reglob =~ s/\?/./g;
-    $reglob =~ s/\*/.*?/g;
-    # Glob rules, but do not match ourselves..
-    my @rules = grep {/^${reglob}$/ && $_ ne $rulename} keys %{$conf->{scores}};
-    if (@rules) {
-      $expanded = join('+', sort @rules);
-    } else {
-      $expanded = '0';
-    }
-  }
-  my $logstr = $expanded eq '0' ? 'no matches' : $expanded;
-  dbg("rules: meta $rulename rules_matching($ruleglob) expanded: $logstr");
-  $pms->{ruleglob_cache}{$ruleglob} = $expanded;
-  return " ($expanded) ";
-};
-
-sub do_meta_tests {
-  my ($self, $pms, $priority) = @_;
-  my (%rule_deps, %meta, $rulename);
-
-  $self->run_generic_tests ($pms, $priority,
-    consttype => $Mail::SpamAssassin::Conf::TYPE_META_TESTS,
-    type => 'meta',
-    testhash => $pms->{conf}->{meta_tests},
-    args => [ ],
-    loop_body => sub
-  {
-    my ($self, $pms, $conf, $rulename, $rule, %opts) = @_;
-
-    # Expand meta rules_matching() before lexing
-    $rule =~ s/${META_RULES_MATCHING_RE}/$self->expand_ruleglob($1,$pms,$conf,$rulename)/ge;
-
-    # Lex the rule into tokens using a rather simple RE method ...
-    my @tokens = ($rule =~ /$ARITH_EXPRESSION_LEXER/og);
-
-    # Set the rule blank to start
-    $meta{$rulename} = "";
-
-    # List dependencies that are meta tests in the same priority band
-    $rule_deps{$rulename} = [ ];
-
-    # Go through each token in the meta rule
-    foreach my $token (@tokens) {
-
-      # ... rulename?
-      if ($token =~ IS_RULENAME) {
-        # the " || 0" formulation is to avoid "use of uninitialized value"
-        # warnings; this is better than adding a 0 to a hash for every
-        # rule referred to in a meta...
-        $meta{$rulename} .= "(\$h->{'$token'}||0) ";
-      
-        if (!exists $conf->{scores}->{$token}) {
-          dbg("rules: meta test $rulename has undefined dependency '$token'");
-        }
-        elsif ($conf->{scores}->{$token} == 0) {
-          # bug 5040: net rules in a non-net scoreset
-          # there are some cases where this is expected; don't warn
-          # in those cases.
-          unless ((($conf->get_score_set()) & 1) == 0 &&
-              ($conf->{tflags}->{$token}||'') =~ /\bnet\b/)
-          {
-            info("rules: meta test $rulename has dependency '$token' with a zero score");
-          }
-        }
-
-        # If the token is another meta rule, add it as a dependency
-        push (@{ $rule_deps{$rulename} }, $token)
-          if (exists $conf->{meta_tests}->{$opts{priority}}->{$token});
-      } else {
-        # ... number or operator
-        $meta{$rulename} .= "$token ";
-      }
-    }
-  },
-    pre_loop_body => sub
-  {
-    my ($self, $pms, $conf, %opts) = @_;
-    $self->push_evalstr_prefix($pms, '
-      my $r;
-      my $h = $self->{tests_already_hit};
-    ');
-  },
-    post_loop_body => sub
-  {
-    my ($self, $pms, $conf, %opts) = @_;
-
-    # Sort by length of dependencies list.  It's more likely we'll get
-    # the dependencies worked out this way.
-    my @metas = sort { @{ $rule_deps{$a} } <=> @{ $rule_deps{$b} } }
-                keys %{$conf->{meta_tests}->{$opts{priority}}};
-
-    my $count;
-    my $tflags = $conf->{tflags};
-
-    # Now go ahead and setup the eval string
-    do {
-      $count = $#metas;
-      my %metas = map { $_ => 1 } @metas; # keep a small cache for fast lookups
-
-      # Go through each meta rule we haven't done yet
-      for (my $i = 0 ; $i <= $#metas ; $i++) {
-
-        # If we depend on meta rules that haven't run yet, skip it
-        next if (grep( $metas{$_}, @{ $rule_deps{ $metas[$i] } }));
-
-        # If we depend on network tests, call ensure_rules_are_complete()
-        # to block until they are
-        if (!defined $conf->{meta_dependencies}->{ $metas[$i] }) {
-          warn "no meta_dependencies defined for $metas[$i]";
-        }
-        my $alldeps = join ' ', grep {
-                ($tflags->{$_}||'') =~ /\bnet\b/
-              } split (' ', $conf->{meta_dependencies}->{ $metas[$i] } );
-
-        if ($alldeps ne '') {
-          $self->add_evalstr($pms, '
-            $self->ensure_rules_are_complete(q{'.$metas[$i].'}, qw{'.$alldeps.'});
-          ');
-        }
-
-        # Add this meta rule to the eval line
-        $self->add_evalstr($pms, '
-          $r = '.$meta{$metas[$i]}.';
-          if ($r) { $self->got_hit(q#'.$metas[$i].'#, "", ruletype => "meta", value => $r); }
-        ');
-
-        splice @metas, $i--, 1;    # remove this rule from our list
-      }
-    } while ($#metas != $count && $#metas > -1); # run until we can't go anymore
-
-    # If there are any rules left, we can't solve the dependencies so complain
-    my %metas = map { $_ => 1 } @metas; # keep a small cache for fast lookups
-    foreach my $rulename_t (@metas) {
-      $pms->{rule_errors}++; # flag to --lint that there was an error ...
-      my $msg =
-          "rules: excluding meta test $rulename_t, unsolved meta dependencies: " .
-              join(", ", grep($metas{$_}, @{ $rule_deps{$rulename_t} }));
-      if ($self->{main}->{lint_rules}) {
-        warn $msg."\n";
-      }
-      else {
-        info($msg);
-      }
-    }
-  }
-  );
-}
-
-###########################################################################
-
 sub do_head_tests {
   my ($self, $pms, $priority) = @_;
   # hash to hold the rules, "header\tdefault value" => rulename
@@ -700,28 +631,25 @@ sub do_head_tests {
     loop_body => sub
   {
     my ($self, $pms, $conf, $rulename, $pat, %opts) = @_;
+
+    push @{$ordered{
+            $conf->{test_opt_header}->{$rulename} .
+            (!exists $conf->{test_opt_unset}->{$rulename} ? '' : "\t$rulename")
+         }}, $rulename;
+
+    return if ($opts{doing_user_rules} &&
+            !$self->is_user_rule_sub($rulename.'_head_test'));
+
     my ($op, $op_infix);
-    my $hdrname = $conf->{test_opt_header}->{$rulename};
     if (exists $conf->{test_opt_exists}->{$rulename}) {
       $op_infix = 0;
-      if (exists $conf->{test_opt_neg}->{$rulename}) {
-        $op = '!defined';
-      } else {
-        $op = 'defined';
-      }
+      $op = exists $conf->{test_opt_neg}->{$rulename} ? '!defined' : 'defined';
     }
     else {
       $op_infix = 1;
-      $op = $conf->{test_opt_neg}->{$rulename} ? '!~' : '=~';
+      $op = exists $conf->{test_opt_neg}->{$rulename} ? '!~' : '=~';
     }
 
-    my $def = $conf->{test_opt_unset}->{$rulename};
-    push(@{ $ordered{$hdrname . (!defined $def ? '' : "\t$rulename")} },
-         $rulename);
-
-    return if ($opts{doing_user_rules} &&
-            !$self->is_user_rule_sub($rulename.'_head_test'));
-
     $testcode{$rulename} = [$op_infix, $op, $pat];
   },
     pre_loop_body => sub
@@ -729,7 +657,7 @@ sub do_head_tests {
     my ($self, $pms, $conf, %opts) = @_;
     $self->push_evalstr_prefix($pms, '
       no warnings q(uninitialized);
-      my $hval;
+      my $hval; my @harr;
     ');
   },
     post_loop_body => sub
@@ -739,9 +667,10 @@ sub do_head_tests {
     while(my($k,$v) = each %ordered) {
       my($hdrname, $def) = split(/\t/, $k, 2);
       $self->push_evalstr_prefix($pms, '
-        $hval = $self->get(q{'.$hdrname.'}, ' .
+        @harr = $self->get(q{'.$hdrname.'});
+        $hval = scalar(@harr) ? join("\n", @harr) : ' .
                            (!defined($def) ? 'undef' :
-                              '$self->{conf}->{test_opt_unset}->{q{'.$def.'}}') . ');
+                              '$self->{conf}->{test_opt_unset}->{q{'.$def.'}}') . ';
       ');
       foreach my $rulename (@{$v}) {
           my $tc_ref = $testcode{$rulename};
@@ -768,24 +697,28 @@ sub do_head_tests {
                 $whlast = 'last if ++$hits >= '.untaint_var($1).';';
               }
             }
-            if ($matchg) {
-              $expr = '$hval '.$op.' /$qrptr->{q{'.$rulename.'}}/go';
-            } else {
-              $expr = '$hval '.$op.' /$qrptr->{q{'.$rulename.'}}/o';
-            }
+            $expr = '$hval '.$op.' /$test_qr/'.$matchg.'op';
           }
 
+          # Make sure rule is marked ready for meta rules
           $self->add_evalstr($pms, '
           if ($scoresptr->{q{'.$rulename.'}}) {
-            '.$posline.'
-            '.$self->hash_line_for_rule($pms, $rulename).'
-            '.$ifwhile.' ('.$expr.') {
-              $self->got_hit(q{'.$rulename.'}, "", ruletype => "header");
-              '.$self->hit_rule_plugin_code($pms, $rulename, "header", "",
-                                            $matching_string_unavailable).'
-              '.$whlast.'
-            }
-            '.$self->ran_rule_plugin_code($rulename, "header").'
+            '.($op_infix ? '$test_qr = $qrptr->{q{'.$rulename.'}};' : '').'
+            '.($op_infix ? $self->capture_rules_replace($conf, $rulename) : '').'
+              '.($would_log_rules_all ?
+                'dbg("rules-all: running header rule %s", q{'.$rulename.'});' : '').'
+              $self->rule_ready(q{'.$rulename.'}, 1);
+              '.$posline.'
+              '.$self->hash_line_for_rule($pms, $rulename).'
+              '.$ifwhile.' ('.$expr.') {
+                '.($op_infix ? $self->capture_plugin_code() : '').'
+                $self->got_hit(q{'.$rulename.'}, "", ruletype => "header");
+                '.$self->hit_rule_plugin_code($pms, $rulename, "header", "",
+                                  $matching_string_unavailable).'
+                '.$whlast.'
+              }
+              '.$self->ran_rule_plugin_code($rulename, "header").'
+            '.($op_infix ? "}\n" : '').'
           }
           ');
       }
@@ -810,7 +743,7 @@ sub do_body_tests {
   {
     my ($self, $pms, $conf, $rulename, $pat, %opts) = @_;
     my $sub = '';
-    if (would_log('dbg', 'rules-all') == 2) {
+    if ($would_log_rules_all) {
       $sub .= '
       dbg("rules-all: running body rule %s", q{'.$rulename.'});
       ';
@@ -825,7 +758,7 @@ sub do_body_tests {
     {
       # support multiple matches
       $loopid++;
-      my ($max) = ($pms->{conf}->{tflags}->{$rulename}||'') =~ /\bmaxhits=(\d+)\b/;
+      my ($max) = $conf->{tflags}->{$rulename} =~ /\bmaxhits=(\d+)\b/;
       $max = untaint_var($max);
       $sub .= '
       $hits = 0;
@@ -839,7 +772,8 @@ sub do_body_tests {
       $sub .= '
         pos $l = 0;
         '.$self->hash_line_for_rule($pms, $rulename).'
-        while ($l =~ /$qrptr->{q{'.$rulename.'}}/go) {
+        while ($l =~ /$test_qr/gop) {
+          '.$self->capture_plugin_code().'
           $self->got_hit(q{'.$rulename.'}, "BODY: ", ruletype => "body");
           '. $self->hit_rule_plugin_code($pms, $rulename, "body", "") . '
           '. ($max? 'last body_'.$loopid.' if ++$hits >= '.$max.';' : '') .'
@@ -860,7 +794,8 @@ sub do_body_tests {
       }
       $sub .= '
         '.$self->hash_line_for_rule($pms, $rulename).'
-        if ($l =~ /$qrptr->{q{'.$rulename.'}}/o) {
+        if ($l =~ /$test_qr/op) {
+          '.$self->capture_plugin_code().'
           $self->got_hit(q{'.$rulename.'}, "BODY: ", ruletype => "body");
           '. $self->hit_rule_plugin_code($pms, $rulename, "body", "last") .'
         }
@@ -868,10 +803,15 @@ sub do_body_tests {
       ';
     }
 
+    # Make sure rule is marked ready for meta rules
     $self->add_evalstr($pms, '
       if ($scoresptr->{q{'.$rulename.'}}) {
-        '.$sub.'
-        '.$self->ran_rule_plugin_code($rulename, "body").'
+        $test_qr = $qrptr->{q{'.$rulename.'}};
+        '.$self->capture_rules_replace($conf, $rulename).'
+          $self->rule_ready(q{'.$rulename.'}, 1);
+          '.$sub.'
+          '.$self->ran_rule_plugin_code($rulename, "body").'
+        }
       }
     ');
 
@@ -886,6 +826,7 @@ sub do_body_tests {
 sub do_uri_tests {
   my ($self, $pms, $priority, @uris) = @_;
   my $loopid = 0;
+
   $self->run_generic_tests ($pms, $priority,
     consttype => $Mail::SpamAssassin::Conf::TYPE_URI_TESTS,
     type => 'uri',
@@ -895,21 +836,22 @@ sub do_uri_tests {
   {
     my ($self, $pms, $conf, $rulename, $pat, %opts) = @_;
     my $sub = '';
-    if (would_log('dbg', 'rules-all') == 2) {
+    if ($would_log_rules_all) {
       $sub .= '
       dbg("rules-all: running uri rule %s", q{'.$rulename.'});
       ';
     }
     if (($conf->{tflags}->{$rulename}||'') =~ /\bmultiple\b/) {
       $loopid++;
-      my ($max) = ($pms->{conf}->{tflags}->{$rulename}||'') =~ /\bmaxhits=(\d+)\b/;
+      my ($max) = $conf->{tflags}->{$rulename} =~ /\bmaxhits=(\d+)\b/;
       $max = untaint_var($max);
       $sub .= '
       $hits = 0;
       uri_'.$loopid.': foreach my $l (@_) {
         pos $l = 0;
         '.$self->hash_line_for_rule($pms, $rulename).'
-        while ($l =~ /$qrptr->{q{'.$rulename.'}}/go) {
+        while ($l =~ /$test_qr/gop) {
+           '.$self->capture_plugin_code().'
            $self->got_hit(q{'.$rulename.'}, "URI: ", ruletype => "uri");
            '. $self->hit_rule_plugin_code($pms, $rulename, "uri", "") . '
            '. ($max? 'last uri_'.$loopid.' if ++$hits >= '.$max.';' : '') .'
@@ -920,7 +862,8 @@ sub do_uri_tests {
       $sub .= '
       foreach my $l (@_) {
         '.$self->hash_line_for_rule($pms, $rulename).'
-        if ($l =~ /$qrptr->{q{'.$rulename.'}}/o) {
+          if ($l =~ /$test_qr/op) {
+           '.$self->capture_plugin_code().'
            $self->got_hit(q{'.$rulename.'}, "URI: ", ruletype => "uri");
            '. $self->hit_rule_plugin_code($pms, $rulename, "uri", "last") .'
         }
@@ -928,15 +871,17 @@ sub do_uri_tests {
       ';
     }
 
+    # Make sure rule is marked ready for meta rules
     $self->add_evalstr($pms, '
       if ($scoresptr->{q{'.$rulename.'}}) {
-        '.$sub.'
-        '.$self->ran_rule_plugin_code($rulename, "uri").'
+        $test_qr = $qrptr->{q{'.$rulename.'}};
+        '.$self->capture_rules_replace($conf, $rulename).'
+          $self->rule_ready(q{'.$rulename.'}, 1);
+          '.$sub.'
+          '.$self->ran_rule_plugin_code($rulename, "uri").'
+        }
       }
     ');
-
-    return if ($opts{doing_user_rules} &&
-            !$self->is_user_rule_sub($rulename.'_uri_test'));
   }
   );
 }
@@ -955,23 +900,24 @@ sub do_rawbody_tests {
   {
     my ($self, $pms, $conf, $rulename, $pat, %opts) = @_;
     my $sub = '';
-    if (would_log('dbg', 'rules-all') == 2) {
+    if ($would_log_rules_all) {
       $sub .= '
       dbg("rules-all: running rawbody rule %s", q{'.$rulename.'});
       ';
     }
-    if (($pms->{conf}->{tflags}->{$rulename}||'') =~ /\bmultiple\b/)
+    if (($conf->{tflags}->{$rulename}||'') =~ /\bmultiple\b/)
     {
       # support multiple matches
       $loopid++;
-      my ($max) = ($pms->{conf}->{tflags}->{$rulename}||'') =~ /\bmaxhits=(\d+)\b/;
+      my ($max) = $conf->{tflags}->{$rulename} =~ /\bmaxhits=(\d+)\b/;
       $max = untaint_var($max);
       $sub .= '
       $hits = 0;
       rawbody_'.$loopid.': foreach my $l (@_) {
         pos $l = 0;
         '.$self->hash_line_for_rule($pms, $rulename).'
-        while ($l =~ /$qrptr->{q{'.$rulename.'}}/go) {
+        while ($l =~ /$test_qr/gop) {
+           '.$self->capture_plugin_code().'
            $self->got_hit(q{'.$rulename.'}, "RAW: ", ruletype => "rawbody");
            '. $self->hit_rule_plugin_code($pms, $rulename, "rawbody", "") . '
            '. ($max? 'last rawbody_'.$loopid.' if ++$hits >= '.$max.';' : '') .'
@@ -983,7 +929,8 @@ sub do_rawbody_tests {
       $sub .= '
       foreach my $l (@_) {
         '.$self->hash_line_for_rule($pms, $rulename).'
-        if ($l =~ /$qrptr->{q{'.$rulename.'}}/o) {
+        if ($l =~ /$test_qr/op) {
+           '.$self->capture_plugin_code().'
            $self->got_hit(q{'.$rulename.'}, "RAW: ", ruletype => "rawbody");
            '. $self->hit_rule_plugin_code($pms, $rulename, "rawbody", "last") . '
         }
@@ -991,10 +938,15 @@ sub do_rawbody_tests {
       ';
     }
 
+    # Make sure rule is marked ready for meta rules
     $self->add_evalstr($pms, '
       if ($scoresptr->{q{'.$rulename.'}}) {
-        '.$sub.'
-        '.$self->ran_rule_plugin_code($rulename, "rawbody").'
+        $test_qr = $qrptr->{q{'.$rulename.'}};
+        '.$self->capture_rules_replace($conf, $rulename).'
+          $self->rule_ready(q{'.$rulename.'}, 1);
+          '.$sub.'
+          '.$self->ran_rule_plugin_code($rulename, "rawbody").'
+        }
       }
     ');
 
@@ -1024,22 +976,33 @@ sub do_full_tests {
                 loop_body => sub
   {
     my ($self, $pms, $conf, $rulename, $pat, %opts) = @_;
-    my ($max) = ($pms->{conf}->{tflags}->{$rulename}||'') =~ /\bmaxhits=(\d+)\b/;
-    $max = untaint_var($max);
-    $max ||= 0;
+    my $whlast = 'last;';
+    if (($conf->{tflags}->{$rulename}||'') =~ /\bmultiple\b/) {
+      if (($conf->{tflags}->{$rulename}||'') =~ /\bmaxhits=(\d+)\b/) {
+        $whlast = 'last if ++$hits >= '.untaint_var($1).';';
+      } else {
+        $whlast = '';
+      }
+    }
+    # Make sure rule is marked ready for meta rules
     $self->add_evalstr($pms, '
       if ($scoresptr->{q{'.$rulename.'}}) {
-        pos $$fullmsgref = 0;
-        '.$self->hash_line_for_rule($pms, $rulename).'
-        dbg("rules-all: running full rule %s", q{'.$rulename.'});
-        $hits = 0;
-        while ($$fullmsgref =~ /$qrptr->{q{'.$rulename.'}}/g) {
-          $self->got_hit(q{'.$rulename.'}, "FULL: ", ruletype => "full");
-          '. $self->hit_rule_plugin_code($pms, $rulename, "full", "last") . '
-          last if ++$hits >= '.$max.';
+        $test_qr = $qrptr->{q{'.$rulename.'}};
+        '.$self->capture_rules_replace($conf, $rulename).'
+          $self->rule_ready(q{'.$rulename.'}, 1);
+          pos $$fullmsgref = 0;
+          '.$self->hash_line_for_rule($pms, $rulename).'
+          dbg("rules-all: running full rule %s", q{'.$rulename.'});
+          $hits = 0;
+          while ($$fullmsgref =~ /$test_qr/gp) {
+            '.$self->capture_plugin_code().'
+            $self->got_hit(q{'.$rulename.'}, "FULL: ", ruletype => "full");
+            '. $self->hit_rule_plugin_code($pms, $rulename, "full", "last") . '
+            '.$whlast.'
+          }
+          pos $$fullmsgref = 0;
+          '.$self->ran_rule_plugin_code($rulename, "full").'
         }
-        pos $$fullmsgref = 0;
-        '.$self->ran_rule_plugin_code($rulename, "full").'
       }
     ');
   }
@@ -1092,6 +1055,7 @@ sub run_eval_tests {
     return;
   } elsif ($self->{main}->call_plugins("have_shortcircuited",
                                         { permsgstatus => $pms })) {
+    $pms->{shortcircuited} = 1;
     return;
   }
 
@@ -1134,7 +1098,6 @@ sub run_eval_tests {
   my $tflagsref = $conf->{tflags};
   my $scoresref = $conf->{scores};
   my $eval_pluginsref = $conf->{eval_plugins};
-  my $have_start_rules = $self->{main}->have_plugin("start_rules");
   my $have_ran_rule = $self->{main}->have_plugin("ran_rule");
 
   # the buffer for the evaluated code 
@@ -1146,6 +1109,17 @@ sub run_eval_tests {
     $dbgstr = 'dbg("rules: ran eval rule $rulename ======> got hit ($result)");';
   }
 
+  if ($self->{main}->have_plugin("start_rules")) {
+    # XXX - should we use helper function here?
+    $evalstr .= '
+      $self->{main}->call_plugins("start_rules", {
+              permsgstatus => $self,
+              ruletype => "eval",
+              priority => '.$priority.'
+            });
+';
+  }
+
   while (my ($rulename, $test) = each %{$evalhash}) {
     if ($tflagsref->{$rulename}) {
       # If the rule is a net rule, and we are in a non-net scoreset, skip it.
@@ -1157,25 +1131,26 @@ sub run_eval_tests {
         next if (($scoreset & 2) == 0);
       }
     }
-
     # skip if score zeroed
     next if !$scoresref->{$rulename};
+
     my $function = untaint_var($test->[0]); # was validated with \w+
     if (!$function) {
-      warn "rules: error: no eval function defined for $rulename";
+      warn "rules: no eval function defined for $rulename\n";
+      $pms->{rule_errors}++;
       next;
     }
-
     if (!exists $conf->{eval_plugins}->{$function}) {
-      warn("rules: error: unknown eval '$function' for $rulename\n");
+      warn "rules: unknown eval '$function' for $rulename\n";
+      $pms->{rule_errors}++;
       next;
     }
 
     $evalstr .= '
-    {
+    if ($scoresptr->{q{'.$rulename.'}}) {
       $rulename = q#'.$rulename.'#;
-      %{$self->{test_log_msgs}} = ();
 ';
  
     # only need to set current_rule_name for plugin evals
@@ -1188,26 +1163,18 @@ sub run_eval_tests {
 ';
     }
 
-    # this stuff is quite slow, and totally superfluous if
-    # no plugin is loaded for those hooks
-    if ($have_start_rules) {
-      # XXX - should we use helper function here?
+    if ($would_log_rules_all) {
       $evalstr .= '
-        $self->{main}->call_plugins("start_rules", {
-                permsgstatus => $self,
-                ruletype => "eval",
-                priority => '.$priority.'
-              });
-
-';
+      dbg("rules-all: running eval rule %s (%s)", $rulename, q{'.$function.'});
+      ';
     }
 
     $evalstr .= '
       eval {
-        $result = $self->'.$function.'(@extraevalargs, @{$testptr->{q#'.$rulename.'#}->[1]}); 1;
+        $result = $self->'.$function.'(@extraevalargs, @{$testptr->{$rulename}->[1]}); 1;
       } or do {
         $result = 0;
-        die "rules: $@\n"  if $@ =~ /__alarm__ignore__/;
+        die "rules: $@\n"  if index($@, "__alarm__ignore__") >= 0;
         $self->handle_eval_rule_errors($rulename);
       };
 ';
@@ -1221,10 +1188,16 @@ sub run_eval_tests {
 ';
     }
 
+    # If eval returns undef, it means rule is running async and
+    # will be marked ready later by rule_ready() or got_hit()
     $evalstr .= '
-      if ($result) {
-        $self->got_hit($rulename, $prepend2desc, ruletype => "eval", value => $result);
-        '.$dbgstr.'
+      if (defined $result) {
+        if ($result) {
+          $self->got_hit($rulename, $prepend2desc, ruletype => "eval", value => $result);
+          '.$dbgstr.'
+        } else {
+          $self->rule_ready($rulename);
+        }
       }
     }
 ';
@@ -1243,6 +1216,7 @@ sub run_eval_tests {
     my (\$self, \@extraevalargs) = \@_;
 
     my \$testptr = \$self->{conf}->{$evalname}->{$priority};
+    my \$scoresptr = \$self->{conf}->{scores};
     my \$prepend2desc = q#$prepend2desc#;
     my \$rulename;
     my \$result;
@@ -1265,7 +1239,7 @@ EOT
   if (!$eval_result) {
     my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
     warn "rules: failed to compile eval tests, skipping some: $eval_stat\n";
-    $self->{rule_errors}++;
+    $pms->{rule_errors}++;
   }
   else {
     my $method = "${package_name}::${methodname}";
@@ -1288,6 +1262,11 @@ EOT
 
 sub hash_line_for_rule {
   my ($self, $pms, $rulename) = @_;
+  # I have no idea why evals are being cluttered by "hashlines" ??
+  # Nobody cares about source_file unless keep_config_parsing_metadata is set!
+  # If you are debugging hanging rule, then simply uncomment this..
+  #return "\ndbg(\"rules: will run %s\", q(".$rulename."));\n";
+  return '' if !%{$pms->{conf}->{source_file}};
   # using tainted subr. argument may taint the whole expression, avoid
   my $u = untaint_var($pms->{conf}->{source_file}->{$rulename});
   return sprintf("\n#line 1 \"%s, rule %s,\"", $u, $rulename);
@@ -1319,58 +1298,116 @@ sub start_rules_plugin_code {
   return $evalstr;
 }
 
+sub capture_plugin_code {
+  my ($self) = @_;
+
+  # Save named captures for regex template rules, tags will be set in
+  # ran_rule_plugin_code to allow tflags multiple to save all
+  return '
+        if (%-) {
+          foreach my $cname (keys %-) {
+            push @{$captures{$cname}}, grep { $_ ne "" } @{$-{$cname}};
+          }
+        }
+  ';
+}
+
 sub hit_rule_plugin_code {
   my ($self, $pms, $rulename, $ruletype, $loop_break_directive,
       $matching_string_unavailable) = @_;
 
-  # note: keep this in 'single quotes' to avoid the $ & performance hit,
-  # unless specifically requested by the caller.   Also split the
-  # two chars, just to be paranoid and ensure that a buggy perl interp
-  # doesn't impose that hit anyway (just in case)
   my $match;
   if ($matching_string_unavailable) {
-    $match = '"<YES>"'; # nothing better to report, $& is not set by this rule
+    $match = '"<YES>"'; # nothing better to report, match is not set by this rule
   } else {
     # simple, but suffers from 'user data interpreted as a boolean', Bug 6360
-    $match = '(defined $'.'& ? $'.'& : "negative match")';
+    # ... which is fixed now with defined stanza
+    $match = '(defined ${^MATCH} ? ${^MATCH} : "<negative match>")';
   }
 
-  my $debug_code = '';
+  my $code = '';
   if (exists($pms->{should_log_rule_hits})) {
-    $debug_code = '
+    $code .= '
         dbg("rules: ran '.$ruletype.' rule '.$rulename.' ======> got hit: \"" . '.
             $match.' . "\"");
     ';
   }
 
-  my $save_hits_code = '';
   if ($pms->{save_pattern_hits}) {
-    $save_hits_code = '
+    $code .= '
         $self->{pattern_hits}->{q{'.$rulename.'}} = '.$match.';
     ';
   }
 
   # if we're not running "tflags multiple", break out of the matching
   # loop this way
-  my $multiple_code = '';
   if ($loop_break_directive &&
       ($pms->{conf}->{tflags}->{$rulename}||'') !~ /\bmultiple\b/) {
-    $multiple_code = $loop_break_directive.';';
+    $code .= $loop_break_directive.';';
   }
 
-  return $debug_code.$save_hits_code.$multiple_code;
+  return $code;
 }
 
 sub ran_rule_plugin_code {
   my ($self, $rulename, $ruletype) = @_;
 
-  return '' unless $self->{main}->have_plugin("ran_rule");
+  # Set tags from captured values
+  my $code = '
+    if (%captures) {
+      $self->set_captures(\%captures);
+      %captures = ();
+    }
+  ';
 
-  # The $self here looks odd, but since we are inserting this into eval'd code it
-  # needs to be $self which in that case is actually the PerMsgStatus object
-  return '
+  if ($self->{main}->have_plugin("ran_rule")) {
+    $code .= '
     $self->{main}->call_plugins ("ran_rule", { permsgstatus => $self, rulename => \''.$rulename.'\', ruletype => \''.$ruletype.'\' });
+    ';
+  }
+
+  return $code;
+}
+
+sub capture_rules_replace {
+  my ($self, $conf, $rulename) = @_;
+
+  return '{' unless exists $conf->{capture_template_rules}->{$rulename};
+
+  # Replace all named capture templates in regex, format %{CAPTURE_NAME}
+  # Note that backquotes must be double escaped in $test_qr
+  my $code = '
+      foreach my $cname (keys %{$self->{conf}->{capture_template_rules}->{q{'.$rulename.'}}}) {
+        my $valref = $self->get_tag_raw($cname);
+        my @vals = grep { defined $_ && $_ ne "" } (ref $valref ? @$valref : $valref);
+        if (@vals) {
+          my $cval = "(?:".join("|", map { quotemeta($_) } @vals).")";
+          $test_qr =~ s/(?<!\\\\)\\%\\\\\\{\Q${cname}\E\\\\\\}/$cval/gs;
   ';
+  if ($would_log_rules_all) {
+    $code .= '
+          dbg("rules-all: replaced regex capture template: %s, %s, %s",
+            q{'.$rulename.'}, $cname, $test_qr);
+    ';
+  }
+  $code .= '
+        } else {
+  ';
+  if ($would_log_rules_all) {
+    $code .= '
+          dbg("rules-all: not running rule %s, dependent tag not defined: %s",
+            q{'.$rulename.'}, $cname);
+    ';
+  }
+  $code .= '
+          $test_qr = undef;
+          last;
+        }
+      }
+      if ($test_qr) {
+  ';
+
+  return $code;
 }
 
 sub free_ruleset_source {
@@ -1387,4 +1424,16 @@ sub free_ruleset_source {
 
 ###########################################################################
 
+sub compile_now_start {
+  my ($self, $params) = @_;
+  $self->{am_compiling} = 1;
+}
+
+sub compile_now_finish {
+  my ($self, $params) = @_;
+  delete $self->{am_compiling};
+}
+
+###########################################################################
+
 1;
index 2d447e8204696924109a544f813ff3d6a9f08460..910736b8ff6024c24785a3857a34e923e9516232 100644 (file)
@@ -87,6 +87,7 @@ use Mail::SpamAssassin::Util qw(untaint_var untaint_file_path
                                 proc_status_ok exit_status_str);
 use Errno qw(ENOENT EACCES);
 use IO::Socket;
+use IO::Select;
 
 our @ISA = qw(Mail::SpamAssassin::Plugin);
 
@@ -111,15 +112,12 @@ sub new {
 
   # are network tests enabled?
   if ($mailsaobject->{local_tests_only}) {
-    $self->{use_dcc} = 0;
+    $self->{dcc_disabled} = 1;
     dbg("dcc: local tests only, disabling DCC");
   }
-  else {
-    dbg("dcc: network tests on, registering DCC");
-  }
 
-  $self->register_eval_rule("check_dcc");
-  $self->register_eval_rule("check_dcc_reputation_range");
+  $self->register_eval_rule("check_dcc", $Mail::SpamAssassin::Conf::TYPE_FULL_EVALS);
+  $self->register_eval_rule("check_dcc_reputation_range", $Mail::SpamAssassin::Conf::TYPE_FULL_EVALS);
 
   $self->set_config($mailsaobject->{conf});
 
@@ -143,6 +141,22 @@ Whether to use DCC, if it is available.
   push(@cmds, {
     setting => 'use_dcc',
     default => 1,
+    is_admin => 1,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
+  });
+
+=item use_dcc_rep (0|1)                (default: 1)
+
+Whether to use the commercial DCC Reputation feature, if it is available. 
+Note that reputation data is free for all starting from DCC 2.x version,
+where it's automatically used.
+
+=cut
+
+  push(@cmds, {
+    setting => 'use_dcc_rep',
+    default => 1,
+    is_admin => 1,
     type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
   });
 
@@ -160,35 +174,40 @@ The default is C<999999> for all these options.
 
 =item dcc_rep_percent NUMBER
 
-Only the commercial DCC software provides DCC Reputations.  A DCC Reputation
-is the percentage of bulk mail received from the last untrusted relay in the
-path taken by a mail message as measured by all commercial DCC installations.
-See http://www.rhyolite.com/dcc/reputations.html
-You C<must> whitelist your trusted relays or MX servers with MX or
-MXDCC lines in /var/dcc/whiteclnt as described in the main DCC man page
-to avoid seeing your own MX servers as sources of bulk mail.
-See https://www.dcc-servers.net/dcc/dcc-tree/dcc.html#White-and-Blacklists
-The default is C<90>.
+Only the commercial DCC software provides DCC Reputations (but starting from
+DCC 2.x version it is available for all).  A DCC Reputation is the
+percentage of bulk mail received from the last untrusted relay in the path
+taken by a mail message as measured by all commercial DCC installations. 
+See http://www.rhyolite.com/dcc/reputations.html You C<must> whitelist your
+trusted relays or MX servers with MX or MXDCC lines in /var/dcc/whiteclnt as
+described in the main DCC man page to avoid seeing your own MX servers as
+sources of bulk mail.  See
+https://www.dcc-servers.net/dcc/dcc-tree/dcc.html#White-and-Blacklists The
+default is C<90>.
 
 =cut
 
   push (@cmds, {
     setting => 'dcc_body_max',
+    is_admin => 1,
     default => 999999,
     type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
   },
   {
     setting => 'dcc_fuz1_max',
+    is_admin => 1,
     default => 999999,
     type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
   },
   {
     setting => 'dcc_fuz2_max',
+    is_admin => 1,
     default => 999999,
     type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
   },
   {
     setting => 'dcc_rep_percent',
+    is_admin => 1,
     default => 90,
     type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
   });
@@ -199,7 +218,7 @@ The default is C<90>.
 
 =over 4
 
-=item dcc_timeout n            (default: 8)
+=item dcc_timeout n            (default: 5)
 
 How many seconds you wait for DCC to complete, before scanning continues
 without the DCC results. A numeric value is optionally suffixed by a
@@ -211,7 +230,7 @@ days, weeks).
   push (@cmds, {
     setting => 'dcc_timeout',
     is_admin => 1,
-    default => 8,
+    default => 5,
     type => $Mail::SpamAssassin::Conf::CONF_TYPE_DURATION,
   });
 
@@ -408,9 +427,6 @@ SpamAssassin spam threshold to DCC as spam.
   $conf->{parser}->register_commands(\@cmds);
 }
 
-
-
-
 sub ck_dir {
   my ($self, $dir, $tgt, $src) = @_;
 
@@ -442,7 +458,6 @@ sub find_dcc_home {
 
   my $conf = $self->{main}->{conf};
 
-
   # Get the DCC software version for talking to dccifd and formatting the
   # dccifd options and the built-in DCC homedir.  Use -q to prevent delays.
   my $cdcc_home;
@@ -452,7 +467,8 @@ sub find_dcc_home {
     my $cdcc_output = do { local $/ = undef; <CDCC> };
     close CDCC;
 
-    $cdcc_output =~ s/\n/ /g;          # everything in 1 line for debugging
+    $cdcc_output =~ s/\s+/ /gs;                # everything in 1 line for debugging
+    $cdcc_output =~ s/\s+$//;
     dbg("dcc: `%s %s` reports '%s'", $cdcc, $cmd, $cdcc_output);
     $self->{dcc_version} = ($cdcc_output =~ /^(\d+\.\d+\.\d+)/) ? $1 : '';
     $cdcc_home = ($cdcc_output =~ /\s+homedir=(\S+)/) ? $1 : '';
@@ -538,11 +554,13 @@ sub dcc_pgm_path {
 
 sub is_dccifd_available {
   my ($self) = @_;
-  my $conf = $self->{main}->{conf};
 
   # dccifd remains available until it breaks
   return $self->{dccifd_available} if $self->{dccifd_available};
 
+  $self->find_dcc_home();
+  my $conf = $self->{main}->{conf};
+
   # deal with configured INET or INET6 socket
   if (defined $conf->{dcc_dccifd_host}) {
     dbg("dcc: dccifd is available via socket [%s]:%s",
@@ -574,8 +592,9 @@ sub is_dccproc_available {
   my $conf = $self->{main}->{conf};
 
   # dccproc remains (un)available so check only once
-  return $self->{dccproc_available} if  defined $self->{dccproc_available};
+  return $self->{dccproc_available} if defined $self->{dccproc_available};
 
+  $self->find_dcc_home();
   my $dccproc = $conf->{dcc_path};
   if (!defined $dccproc || $dccproc eq '') {
     $dccproc = $self->dcc_pgm_path('dccproc');
@@ -619,74 +638,278 @@ sub dccifd_connect {
 # check for dccifd every time in case enough uses of dccproc starts dccifd
 sub get_dcc_interface {
   my ($self) = @_;
-  my $conf = $self->{main}->{conf};
 
-  if (!$conf->{use_dcc}) {
-    $self->{dcc_disabled} = 1;
-    return;
+  if (!$self->is_dccifd_available() && !$self->is_dccproc_available()) {
+    dbg("dcc: dccifd or dccproc is not available");
+    return 0;
   }
 
-  $self->find_dcc_home();
-  if (!$self->is_dccifd_available() && !$self->is_dccproc_available()) {
-    dbg("dcc: dccifd and dccproc are not available");
-    $self->{dcc_disabled} = 1;
+  return 1;
+}
+
+sub check_tick {
+  my ($self, $opts) = @_;
+
+  $self->_check_async($opts, 0);
+
+  my $pms = $opts->{permsgstatus};
+
+  # Finish callbacks
+  if ($pms->{dcc_range_callbacks}) {
+    while (@{$pms->{dcc_range_callbacks}}) {
+      my $cb_args = shift @{$pms->{dcc_range_callbacks}};
+      $self->check_dcc_reputation_range($pms, @$cb_args);
+    }
   }
+}
+
+sub check_cleanup {
+  my ($self, $opts) = @_;
+
+  $self->_check_async($opts, 1);
 
-  $self->{dcc_disabled} = 0;
+  my $pms = $opts->{permsgstatus};
+
+  # Finish callbacks
+  if ($pms->{dcc_range_callbacks}) {
+    while (@{$pms->{dcc_range_callbacks}}) {
+      my $cb_args = shift @{$pms->{dcc_range_callbacks}};
+      $self->check_dcc_reputation_range($pms, @$cb_args);
+    }
+  }
 }
 
-sub dcc_query {
-  my ($self, $permsgstatus, $fulltext) = @_;
+sub _check_async {
+  my ($self, $opts, $timeout) = @_;
+  my $pms = $opts->{permsgstatus};
 
-  $permsgstatus->{dcc_checked} = 1;
+  return if !$pms->{dcc_sock};
 
-  if (!$self->{main}->{conf}->{use_dcc}) {
-    dbg("dcc: DCC is not available: use_dcc is 0");
-    return;
+  my $timer = $self->{main}->time_method("check_dcc");
+
+  $pms->{dcc_abort} =
+    $pms->{dcc_abort} || $pms->{deadline_exceeded} || $pms->{shortcircuited};
+
+  if ($pms->{dcc_abort}) {
+    $timeout = 0;
+  } elsif ($timeout) {
+    # Calculate how much time left from original timeout
+    $timeout = $self->{main}->{conf}->{dcc_timeout} -
+      (time - $pms->{dcc_async_start});
+    $timeout = 1 if $timeout < 1;
+    $timeout = 20 if $timeout > 20; # hard sanity check
+    dbg("dcc: final wait for dccifd, timeout in $timeout sec");
+  }
+
+  if (IO::Select->new($pms->{dcc_sock})->can_read($timeout)) {
+    dbg("dcc: reading dccifd response");
+    my @resp;
+    # if DCC is ready, should never block? timeout 1s just in case
+    my $timer = Mail::SpamAssassin::Timeout->new({ secs => 1 });
+    my $err = $timer->run_and_catch(sub {
+      local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };
+      @resp = $pms->{dcc_sock}->getlines();
+    });
+    delete $pms->{dcc_sock};
+    if ($timer->timed_out()) {
+      info("dcc: dccifd read failed");
+    } elsif ($err) {
+      chomp $err;
+      info("dcc: dccifd read failed: $err");
+    } else {
+      shift @resp; shift @resp; # ignore status/multistatus line
+      if (@resp) {
+        dbg("dcc: dccifd raw response: ".join("", @resp));
+        ($pms->{dcc_x_result}, $pms->{dcc_cksums}) =
+          $self->parse_dcc_response(\@resp, 'dccifd');
+        if ($pms->{dcc_x_result}) {
+          dbg("dcc: dccifd parsed response: $pms->{dcc_x_result}");
+          ($pms->{dcc_result}, $pms->{dcc_rep}) =
+            $self->check_dcc_result($pms, $pms->{dcc_x_result});
+          if ($pms->{dcc_result}) {
+            foreach (@{$pms->{conf}->{eval_to_rule}->{check_dcc}}) {
+              $pms->got_hit($_, "", ruletype => 'eval');
+            }
+          } else {
+            foreach (@{$pms->{conf}->{eval_to_rule}->{check_dcc}}) {
+              $pms->rule_ready($_);
+            }
+          }
+        }
+      } else {
+        info("dcc: empty response from dccifd?");
+      }
+    }
+  } elsif ($pms->{dcc_abort}) {
+    dbg("dcc: bailing out due to deadline/shortcircuit");
+    delete $pms->{dcc_sock};
+    delete $pms->{dcc_range_callbacks};
+  } elsif ($timeout) {
+    dbg("dcc: no response from dccifd, timed out");
+    delete $pms->{dcc_sock};
+    delete $pms->{dcc_range_callbacks};
+  } else {
+    dbg("dcc: still waiting for dccifd response");
   }
+}
+
+sub check_dnsbl {
+  my($self, $opts) = @_;
+
+  return 0 if $self->{dcc_disabled};
+  return 0 if !$self->{main}->{conf}->{use_dcc};
+
+  my $pms = $opts->{permsgstatus};
+
+  # Check that rules are active
+  return 0 if !grep {$pms->{conf}->{scores}->{$_}}
+    ( @{$pms->{conf}->{eval_to_rule}->{check_dcc}},
+      @{$pms->{conf}->{eval_to_rule}->{check_dcc_reputation_range}} );
+
+  # Launch async only if dccifd found
+  if ($self->is_dccifd_available()) {
+    $self->_launch_dcc($pms);
+  }
+}
+
+sub _launch_dcc {
+  my ($self, $pms) = @_;
+
+  return if $pms->{dcc_running};
+  $pms->{dcc_running} = 1;
+
+  my $timer = $self->{main}->time_method("check_dcc");
 
   # initialize valid tags
-  $permsgstatus->{tag_data}->{DCCB} = "";
-  $permsgstatus->{tag_data}->{DCCR} = "";
-  $permsgstatus->{tag_data}->{DCCREP} = "";
+  $pms->{tag_data}->{DCCB} = '';
+  $pms->{tag_data}->{DCCR} = '';
+  $pms->{tag_data}->{DCCREP} = '';
 
-  if ($$fulltext eq '') {
+  my $fulltext = $pms->{msg}->get_pristine();
+  if ($fulltext eq '') {
     dbg("dcc: empty message; skipping dcc check");
+    $pms->{dcc_result} = 0;
+    $pms->{dcc_abort} = 1;
+    return;
+  }
+
+  if (!$self->get_dcc_interface()) {
+    $pms->{dcc_result} = 0;
+    $pms->{dcc_abort} = 1;
     return;
   }
 
-  if ($permsgstatus->get('ALL') =~ /^(X-DCC-.*-Metrics:.*)$/m) {
-    $permsgstatus->{dcc_raw_x_dcc} = $1;
+  if ($pms->get('ALL-TRUSTED') =~ /^(X-DCC-[^:]*?-Metrics: .*)$/m) {
     # short-circuit if there is already a X-DCC header with value of
     # "bulk" from an upstream DCC check
     # require "bulk" because then at least one body checksum will be "many"
     # and so we know the X-DCC header is not forged by spammers
-    return if $permsgstatus->{dcc_raw_x_dcc} =~ / bulk /;
+    #if ($1 =~ / bulk /) {
+    #  return $self->check_dcc_result($pms, $1);
+    #}
   }
 
-  my $timer = $self->{main}->time_method("check_dcc");
+  my $envelope = $pms->{relays_external}->[0];
 
-  $self->get_dcc_interface();
-  return if $self->{dcc_disabled};
+  ($pms->{dcc_x_result}, $pms->{dcc_cksums}) =
+    $self->ask_dcc('dcc:', $pms, \$fulltext, $envelope);
 
-  my $envelope = $permsgstatus->{relays_external}->[0];
-  ($permsgstatus->{dcc_raw_x_dcc},
-   $permsgstatus->{dcc_cksums}) = $self->ask_dcc("dcc:", $permsgstatus,
-                                                $fulltext, $envelope);
+  return;
 }
 
 sub check_dcc {
-  my ($self, $permsgstatus, $full) = @_;
-  my $conf = $self->{main}->{conf};
+  my ($self, $pms) = @_;
+
+  return 0 if $self->{dcc_disabled};
+  return 0 if !$pms->{conf}->{use_dcc};
+  return 0 if $pms->{dcc_abort};
+
+  # async already handling?
+  if ($pms->{dcc_async_start}) {
+    return; # return undef for async status
+  }
 
-  $self->dcc_query($permsgstatus, $full)  if !$permsgstatus->{dcc_checked};
+  return $pms->{dcc_result} if defined $pms->{dcc_result};
 
-  my $x_dcc = $permsgstatus->{dcc_raw_x_dcc};
-  return 0  if !defined $x_dcc || $x_dcc eq '';
+  $self->_launch_dcc($pms);
+  return if $pms->{dcc_async_start}; # return undef for async status
 
-  if ($x_dcc =~ /^X-DCC-(.*)-Metrics: (.*)$/) {
-    $permsgstatus->set_tag('DCCB', $1);
-    $permsgstatus->set_tag('DCCR', $2);
+  if (!defined $pms->{dcc_x_result}) {
+    $pms->{dcc_abort} = 1;
+    return 0;
+  }
+
+  ($pms->{dcc_result}, $pms->{dcc_rep}) =
+    $self->check_dcc_result($pms, $pms->{dcc_x_result});
+
+  return $pms->{dcc_result};
+}
+
+sub check_dcc_reputation_range {
+  my ($self, $pms, undef, $min, $max, $cb_rulename) = @_;
+
+  return 0 if $self->{dcc_disabled};
+  return 0 if !$pms->{conf}->{use_dcc};
+  return 0 if !$pms->{conf}->{use_dcc_rep};
+  return 0 if $pms->{dcc_abort};
+
+  my $timer = $self->{main}->time_method("check_dcc");
+
+  if (exists $pms->{dcc_rep}) {
+    my $result;
+
+    # Process result
+    if ($pms->{dcc_rep} < 0) {
+      # Not used or missing reputation
+      $result = 0;
+    } else {
+      # cover the entire range of reputations if not told otherwise
+      $min = 0   if !defined $min;
+      $max = 100 if !defined $max;
+      $result = $pms->{dcc_rep} >= $min && $pms->{dcc_rep} <= $max ? 1 : 0;
+      dbg("dcc: dcc_rep %s, min %s, max %s => result=%s",
+        $pms->{dcc_rep}, $min, $max, $result ? 'YES' : 'no');
+    }
+
+    if (defined $cb_rulename) {
+      # If callback, use got_hit()
+      if ($result) {
+        $pms->got_hit($cb_rulename, "", ruletype => 'eval');
+      } else {
+        $pms->rule_ready($cb_rulename);
+      }
+      return 0;
+    } else {
+      return $result;
+    }
+  } else {
+    # Install callback if waiting for async result
+    if (!defined $cb_rulename) {
+      my $rulename = $pms->get_current_eval_rule_name();
+      # array matches check_dcc_reputation_range() argument order
+      push @{$pms->{dcc_range_callbacks}}, [undef, $min, $max, $rulename];
+      return; # return undef for async status
+    }
+  }
+
+  return 0;
+}
+
+sub check_dcc_result {
+  my ($self, $pms, $x_dcc) = @_;
+
+  my $dcc_result = 0;
+  my $dcc_rep = -1;
+
+  if (!defined $x_dcc || $x_dcc eq '') {
+    return ($dcc_result, $dcc_rep);
+  }
+
+  my $conf = $pms->{conf};
+
+  if ($x_dcc =~ /^X-DCC-([^:]*?)-Metrics: (.*)$/) {
+    $pms->set_tag('DCCB', $1);
+    $pms->set_tag('DCCR', $2);
   }
   $x_dcc =~ s/many/999999/ig;
   $x_dcc =~ s/ok\d?/0/ig;
@@ -701,8 +924,10 @@ sub check_dcc {
   if ($x_dcc =~ /\bFuz2=(\d+)/) {
     $count{fuz2} = $1+0;
   }
-  if ($x_dcc =~ /\brep=(\d+)/) {
+  if ($pms->{conf}->{use_dcc_rep} && $x_dcc =~ /\brep=(\d+)/) {
     $count{rep}  = $1+0;
+    $dcc_rep = $count{rep};
+    $pms->set_tag('DCCREP', $dcc_rep);
   }
   if ($count{body} >= $conf->{dcc_body_max} ||
       $count{fuz1} >= $conf->{dcc_fuz1_max} ||
@@ -716,45 +941,15 @@ sub check_dcc {
                  $count{fuz2}, $conf->{dcc_fuz2_max},
                  $count{rep},  $conf->{dcc_rep_percent})
                ));
-    return 1;
-  }
-  return 0;
-}
-
-sub check_dcc_reputation_range {
-  my ($self, $permsgstatus, $fulltext, $min, $max) = @_;
-
-  # this is called several times per message, so parse the X-DCC header once
-  my $dcc_rep = $permsgstatus->{dcc_rep};
-  if (!defined $dcc_rep) {
-    $self->dcc_query($permsgstatus, $fulltext)  if !$permsgstatus->{dcc_checked};
-    my $x_dcc = $permsgstatus->{dcc_raw_x_dcc};
-    if (defined $x_dcc && $x_dcc =~ /\brep=(\d+)/) {
-      $dcc_rep = $1+0;
-      $permsgstatus->set_tag('DCCREP', $dcc_rep);
-    } else {
-      $dcc_rep = -1;
-    }
-    $permsgstatus->{dcc_rep} = $dcc_rep;
+    $dcc_result = 1;
   }
 
-  # no X-DCC header or no reputation in the X-DCC header, perhaps for lack
-  # of data in the DCC Reputation server
-  return 0 if $dcc_rep < 0;
-
-  # cover the entire range of reputations if not told otherwise
-  $min = 0   if !defined $min;
-  $max = 100 if !defined $max;
-
-  my $result = $dcc_rep >= $min && $dcc_rep <= $max ? 1 : 0;
-  dbg("dcc: dcc_rep %s, min %s, max %s => result=%s",
-      $dcc_rep, $min, $max, $result?'YES':'no');
-  return $result;
+  return ($dcc_result, $dcc_rep);
 }
 
 # get the X-DCC header line and save the checksums from dccifd or dccproc
 sub parse_dcc_response {
-  my ($self, $resp) = @_;
+  my ($self, $resp, $pgm) = @_;
   my ($raw_x_dcc, $cksums);
 
   # The first line is the header we want.  It uses SMTP folded whitespace
@@ -773,175 +968,212 @@ sub parse_dcc_response {
     $cksums .= $v;
   }
 
+  if (!defined $raw_x_dcc || $raw_x_dcc !~ /^X-DCC/) {
+    info("dcc: instead of X-DCC header, $pgm returned '%s'", $raw_x_dcc||'');
+  }
+
   return ($raw_x_dcc, $cksums);
 }
 
 sub ask_dcc {
-  my ($self, $tag, $permsgstatus, $fulltext, $envelope) = @_;
-  my $conf = $self->{main}->{conf};
-  my ($pgm, $err, $sock, $pid, @resp);
-  my ($client, $clientname, $helo, $opts);
-
-  $permsgstatus->enter_helper_run_mode();
+  my ($self, $tag, $pms, $fulltext, $envelope) = @_;
 
+  my $conf = $pms->{conf};
   my $timeout = $conf->{dcc_timeout};
-  my $timer = Mail::SpamAssassin::Timeout->new(
-         { secs => $timeout, deadline => $permsgstatus->{master_deadline} });
 
-  $err = $timer->run_and_catch(sub {
-    local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };
-
-    # prefer dccifd to dccproc
-    if ($self->{dccifd_available}) {
-      $pgm = 'dccifd';
+  if ($self->is_dccifd_available()) {
+    my @resp;
+    my $timer = Mail::SpamAssassin::Timeout->new(
+      { secs => $timeout, deadline => $pms->{master_deadline} });
+    my $err = $timer->run_and_catch(sub {
+      local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };
 
-      $sock = $self->dccifd_connect($tag);
-      if (!$sock) {
+      $pms->{dcc_sock} = $self->dccifd_connect($tag);
+      if (!$pms->{dcc_sock}) {
        $self->{dccifd_available} = 0;
-       die("dccproc not available") if (!$self->is_dccproc_available());
-
        # fall back on dccproc if the socket is an orphan from
        # a killed dccifd daemon or some other obvious (no timeout) problem
-       dbg("$tag fall back on dccproc");
+       dbg("$tag dccifd failed: trying dccproc as fallback");
+       return;
       }
-    }
-
-    if ($self->{dccifd_available}) {
 
       # send the options and other parameters to the daemon
-      $client = $envelope->{ip};
-      $clientname = $envelope->{rdns};
+      my $client = $envelope->{ip};
+      my $clientname = $envelope->{rdns};
       if (!defined $client) {
        $client = '';
       } else {
        $client .= ("\r" . $clientname) if defined $clientname;
       }
-      $helo = $envelope->{helo} || '';
-      if ($tag ne "dcc:") {
-       $opts = $self->{dccifd_report_options}
-      } else {
+      my $helo = $envelope->{helo} || '';
+      my $opts;
+      if ($tag eq 'dcc:') {
        $opts = $self->{dccifd_lookup_options};
-       if (defined $permsgstatus->{dcc_raw_x_dcc}) {
+       if (defined $pms->{dcc_x_result}) {
          # only query if there is an X-DCC header
          $opts =~ s/grey-off/grey-off query/;
        }
+      } else {
+       $opts = $self->{dccifd_report_options};
+      }
+
+      $pms->{dcc_sock}->print($opts)  or die "failed write options\n";
+      $pms->{dcc_sock}->print("$client\n")  or die "failed write SMTP client\n";
+      $pms->{dcc_sock}->print("$helo\n")  or die "failed write HELO value\n";
+      $pms->{dcc_sock}->print("\n")  or die "failed write sender\n";
+      $pms->{dcc_sock}->print("unknown\n\n")  or die "failed write 1 recipient\n";
+      $pms->{dcc_sock}->print($$fulltext)  or die "failed write mail message\n";
+      $pms->{dcc_sock}->shutdown(1)  or die "failed socket shutdown: $!";
+
+      # don't async report and learn
+      if ($tag ne 'dcc:') {
+        @resp = $pms->{dcc_sock}->getlines();
+        delete $pms->{dcc_sock};
+        shift @resp; shift @resp; # ignore status/multistatus line
+        if (!@resp) {
+          die("no response");
+        }
+      } else {
+        $pms->{dcc_async_start} = time;
       }
+    });
+
+    if ($timer->timed_out()) {
+      delete $pms->{dcc_sock};
+      dbg("$tag dccifd timed out after $timeout seconds");
+      return (undef, undef);
+    } elsif ($err) {
+      delete $pms->{dcc_sock};
+      chomp $err;
+      info("$tag dccifd failed: $err");
+      return (undef, undef);
+    }
 
-      $sock->print($opts)         or die "failed write options\n";
-      $sock->print($client . "\n") or die "failed write SMTP client\n";
-      $sock->print($helo . "\n")   or die "failed write HELO value\n";
-      $sock->print("\n")          or die "failed write sender\n";
-      $sock->print("unknown\n\n")  or die "failed write 1 recipient\n";
-      $sock->print($$fulltext)     or die "failed write mail message\n";
-      $sock->shutdown(1) or die "failed socket shutdown: $!";
+    # report, learn
+    if ($tag ne 'dcc:') {
+      my ($raw_x_dcc, $cksums) = $self->parse_dcc_response(\@resp, 'dccifd');
+      if ($raw_x_dcc) {
+        dbg("$tag dccifd responded with '$raw_x_dcc'");
+        return ($raw_x_dcc, $cksums);
+      } else {
+        return (undef, undef);
+      }
+    }
 
-      $sock->getline()   or die "failed read status\n";
-      $sock->getline()   or die "failed read multistatus\n";
+    # async lookup
+    return ('async', undef) if $pms->{dcc_async_start};
 
-      @resp = $sock->getlines();
-      die "failed to read dccifd response\n" if !@resp;
+    # or falling back to dccproc..
+  }
+
+  if ($self->is_dccproc_available()) {
+    $pms->enter_helper_run_mode();
+
+    my $pid;
+    my @resp;
+    my $timer = Mail::SpamAssassin::Timeout->new(
+      { secs => $timeout, deadline => $pms->{master_deadline} });
+    my $err = $timer->run_and_catch(sub {
+      local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };
 
-    } else {
-      $pgm = 'dccproc';
       # use a temp file -- open2() is unreliable, buffering-wise, under spamd
-      # first ensure that we do not hit a stray file from some other filter.
-      $permsgstatus->delete_fulltext_tmpfile();
-      my $tmpf = $permsgstatus->create_fulltext_tmpfile($fulltext);
+      my $tmpf = $pms->create_fulltext_tmpfile();
 
-      my $path = $conf->{dcc_path};
-      $opts = $conf->{dcc_options};
-      my @opts = !defined $opts ? () : split(' ',$opts);
+      my @opts = split(/\s+/, $conf->{dcc_options} || '');
       untaint_var(\@opts);
       unshift(@opts, '-w', 'whiteclnt');
-      $client = $envelope->{ip};
+      my $client = $envelope->{ip};
       if ($client) {
-       unshift(@opts, '-a', untaint_var($client));
+        unshift(@opts, '-a', untaint_var($client));
       } else {
-       # get external relay IP address from Received: header if not available
-       unshift(@opts, '-R');
+        # get external relay IP address from Received: header if not available
+        unshift(@opts, '-R');
       }
-      if ($tag eq "dcc:") {
-       # query instead of report if there is an X-DCC header from upstream
-       unshift(@opts, '-Q') if defined $permsgstatus->{dcc_raw_x_dcc};
+      if ($tag eq 'dcc:') {
+        # query instead of report if there is an X-DCC header from upstream
+        unshift(@opts, '-Q') if defined $pms->{dcc_x_result};
       } else {
-       # learn or report spam
-       unshift(@opts, '-t', 'many');
+        # learn or report spam
+        unshift(@opts, '-t', 'many');
       }
       if ($conf->{dcc_home}) {
         # set home directory explicitly
         unshift(@opts, '-h', $conf->{dcc_home});
-      };
+      }
 
-      defined $path  or die "no dcc_path found\n";
       dbg("$tag opening pipe to " .
-         join(' ', $path, "-C", "-x", "0", @opts, "<$tmpf"));
+        join(' ', $conf->{dcc_path}, "-C", "-x", "0", @opts, "<$tmpf"));
 
       $pid = Mail::SpamAssassin::Util::helper_app_pipe_open(*DCC,
-               $tmpf, 1, $path, "-C", "-x", "0", @opts);
+        $tmpf, 1, $conf->{dcc_path}, "-C", "-x", "0", @opts);
       $pid or die "DCC: $!\n";
 
       # read+split avoids a Perl I/O bug (Bug 5985)
-      my($inbuf,$nread,$resp); $resp = '';
-      while ( $nread=read(DCC,$inbuf,8192) ) { $resp .= $inbuf }
+      my($inbuf, $nread);
+      my $resp = '';
+      while ($nread = read(DCC, $inbuf, 8192)) { $resp .= $inbuf }
       defined $nread  or die "error reading from pipe: $!";
-      @resp = split(/^/m, $resp, -1);  undef $resp;
+      @resp = split(/^/m, $resp, -1);
 
-      my $errno = 0;  close DCC or $errno = $!;
+      my $errno = 0;
+      close DCC or $errno = $!;
       proc_status_ok($?,$errno)
-         or info("$tag [%s] finished: %s", $pid, exit_status_str($?,$errno));
+        or info("$tag [%s] finished: %s", $pid, exit_status_str($?,$errno));
 
       die "failed to read X-DCC header from dccproc\n" if !@resp;
-    }
-  });
 
-  if (defined $pgm && $pgm eq 'dccproc') {
-    if (defined(fileno(*DCC))) {       # still open
+    });
+
+    if (defined(fileno(*DCC))) { # still open
       if ($pid) {
-       if (kill('TERM',$pid)) {
+        if (kill('TERM', $pid)) {
          dbg("$tag killed stale dccproc process [$pid]")
        } else {
          dbg("$tag killing dccproc process [$pid] failed: $!")
        }
       }
-      my $errno = 0;  close(DCC) or $errno = $!;
+      my $errno = 0;
+      close(DCC) or $errno = $!;
       proc_status_ok($?,$errno) or info("$tag [%s] dccproc terminated: %s",
                                        $pid, exit_status_str($?,$errno));
     }
-  }
 
-  $permsgstatus->leave_helper_run_mode();
+    $pms->leave_helper_run_mode();
 
-  if ($timer->timed_out()) {
-    dbg("$tag %s timed out after %d seconds", $pgm||'', $timeout);
-    return (undef, undef);
-  }
+    if ($timer->timed_out()) {
+      dbg("$tag dccproc timed out after $timeout seconds");
+      return (undef, undef);
+    } elsif ($err) {
+      chomp $err;
+      info("$tag dccproc failed: $err");
+      return (undef, undef);
+    }
 
-  if ($err) {
-    chomp $err;
-    info("$tag %s failed: %s", $pgm||'', $err);
-    return (undef, undef);
+    my ($raw_x_dcc, $cksums) = $self->parse_dcc_response(\@resp, 'dccproc');
+    if ($raw_x_dcc) {
+      dbg("$tag dccproc responded with '$raw_x_dcc'");
+      return ($raw_x_dcc, $cksums);
+    } else {
+      info("$tag instead of X-DCC header, dccproc returned '$raw_x_dcc'");
+      return (undef, undef);
+    }
   }
 
-  my ($raw_x_dcc, $cksums) = $self->parse_dcc_response(\@resp);
-  if (!defined $raw_x_dcc || $raw_x_dcc !~ /^X-DCC/) {
-    info("$tag instead of X-DCC header, $pgm returned '$raw_x_dcc'");
-    return (undef, undef);
-  }
-  dbg("$tag $pgm responded with '$raw_x_dcc'");
-  return ($raw_x_dcc, $cksums);
+  return (undef, undef);
 }
 
 # tell DCC server that the message is spam according to SpamAssassin
 sub check_post_learn {
-  my ($self, $options) = @_;
+  my ($self, $opts) = @_;
+
+  return if $self->{dcc_disabled};
+  return if !$self->{main}->{conf}->{use_dcc};
+
+  my $pms = $opts->{permsgstatus};
+  return if $pms->{dcc_abort};
 
   # learn only if allowed
-  return if $self->{learn_disabled};
   my $conf = $self->{main}->{conf};
-  if (!$conf->{use_dcc}) {
-    $self->{learn_disabled} = 1;
-    return;
-  }
   my $learn_score = $conf->{dcc_learn_score};
   if (!defined $learn_score || $learn_score eq '') {
     dbg("dcc: DCC learning not enabled by dcc_learn_score");
@@ -951,10 +1183,9 @@ sub check_post_learn {
 
   # and if SpamAssassin concluded that the message is spam
   # worse than our threshold
-  my $permsgstatus = $options->{permsgstatus};
-  if ($permsgstatus->is_spam()) {
-    my $score = $permsgstatus->get_score();
-    my $required_score = $permsgstatus->get_required_score();
+  if ($pms->is_spam()) {
+    my $score = $pms->get_score();
+    my $required_score = $pms->get_required_score();
     if ($score < $required_score + $learn_score) {
       dbg("dcc: score=%d required_score=%d dcc_learn_score=%d",
          $score, $required_score, $learn_score);
@@ -963,32 +1194,32 @@ sub check_post_learn {
   }
 
   # and if we checked the message
-  return if (!defined $permsgstatus->{dcc_raw_x_dcc});
+  return if (!defined $pms->{dcc_x_result});
 
   # and if the DCC server thinks it was not spam
-  if ($permsgstatus->{dcc_raw_x_dcc} !~ /\b(Body|Fuz1|Fuz2)=\d/) {
-    dbg("dcc: already known as spam; no need to learn");
+  if ($pms->{dcc_x_result} !~ /\b(Body|Fuz1|Fuz2)=\d/) {
+    dbg("dcc: already known as spam; no need to learn: $pms->{dcc_x_result}");
     return;
   }
 
+  my $timer = $self->{main}->time_method("dcc_learn");
+
   # dccsight is faster than dccifd or dccproc if we have checksums,
   #   which we do not have with dccifd before 1.3.123
-  my $old_cksums = $permsgstatus->{dcc_cksums};
-  return if ($old_cksums && $self->dccsight_learn($permsgstatus, $old_cksums));
+  my $old_cksums = $pms->{dcc_cksums};
+  return if ($old_cksums && $self->dccsight_learn($pms, $old_cksums));
 
   # Fall back on dccifd or dccproc without saved checksums or dccsight.
   # get_dcc_interface() was called when the message was checked
-
-  my $fulltext = $permsgstatus->{msg}->get_pristine();
-  my $envelope = $permsgstatus->{relays_external}->[0];
-  my ($raw_x_dcc, $cksums) = $self->ask_dcc("dcc: learn:", $permsgstatus,
+  my $fulltext = $pms->{msg}->get_pristine();
+  my $envelope = $pms->{relays_external}->[0];
+  my ($raw_x_dcc, undef) = $self->ask_dcc('dcc: learn:', $pms,
                                            \$fulltext, $envelope);
   dbg("dcc: learned as spam") if defined $raw_x_dcc;
 }
 
 sub dccsight_learn {
-  my ($self, $permsgstatus, $old_cksums) = @_;
-  my ($raw_x_dcc, $new_cksums);
+  my ($self, $pms, $old_cksums) = @_;
 
   return 0 if !$old_cksums;
 
@@ -998,17 +1229,17 @@ sub dccsight_learn {
     return 0;
   }
 
-  $permsgstatus->enter_helper_run_mode();
+  $pms->enter_helper_run_mode();
 
   # use a temp file here -- open2() is unreliable, buffering-wise, under spamd
-  # ensure that we do not hit a stray file from some other filter.
-  $permsgstatus->delete_fulltext_tmpfile();
-  my $tmpf = $permsgstatus->create_fulltext_tmpfile(\$old_cksums);
+  my $tmpf = $pms->create_fulltext_tmpfile(\$old_cksums);
+
+  my ($raw_x_dcc, $new_cksums);
   my $pid;
 
   my $timeout = $self->{main}->{conf}->{dcc_timeout};
   my $timer = Mail::SpamAssassin::Timeout->new(
-          { secs => $timeout, deadline => $permsgstatus->{master_deadline} });
+          { secs => $timeout, deadline => $pms->{master_deadline} });
   my $err = $timer->run_and_catch(sub {
     local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };
 
@@ -1020,47 +1251,51 @@ sub dccsight_learn {
     $pid or die "$!\n";
 
     # read+split avoids a Perl I/O bug (Bug 5985)
-    my($inbuf,$nread,$resp); $resp = '';
-    while ( $nread=read(DCC,$inbuf,8192) ) { $resp .= $inbuf }
+    my($inbuf, $nread);
+    my $resp = '';
+    while ($nread = read(DCC, $inbuf, 8192)) { $resp .= $inbuf }
     defined $nread  or die "error reading from pipe: $!";
-    my @resp = split(/^/m, $resp, -1);  undef $resp;
+    my @resp = split(/^/m, $resp, -1);
 
-    my $errno = 0;  close DCC or $errno = $!;
+    my $errno = 0;
+    close DCC or $errno = $!;
     proc_status_ok($?,$errno)
          or info("dcc: [%s] finished: %s", $pid, exit_status_str($?,$errno));
 
     die "dcc: failed to read learning response\n" if !@resp;
 
-    ($raw_x_dcc, $new_cksums) = $self->parse_dcc_response(\@resp);
+    ($raw_x_dcc, $new_cksums) = $self->parse_dcc_response(\@resp, 'dccsight');
   });
 
   if (defined(fileno(*DCC))) {   # still open
     if ($pid) {
-      if (kill('TERM',$pid)) {
-       dbg("dcc: killed stale dccsight process [$pid]")
+      if (kill('TERM', $pid)) {
+       dbg("dcc: killed stale dccsight process [$pid]");
       } else {
-       dbg("dcc: killing stale dccsight process [$pid] failed: $!") }
+       dbg("dcc: killing stale dccsight process [$pid] failed: $!");
+      }
     }
-    my $errno = 0;  close(DCC) or $errno = $!;
+    my $errno = 0;
+    close(DCC) or $errno = $!;
     proc_status_ok($?,$errno) or info("dcc: dccsight [%s] terminated: %s",
                                      $pid, exit_status_str($?,$errno));
   }
-  $permsgstatus->delete_fulltext_tmpfile();
-  $permsgstatus->leave_helper_run_mode();
+
+  $pms->delete_fulltext_tmpfile($tmpf);
+
+  $pms->leave_helper_run_mode();
 
   if ($timer->timed_out()) {
     dbg("dcc: dccsight timed out after $timeout seconds");
     return 0;
-  }
-
-  if ($err) {
+  } elsif ($err) {
     chomp $err;
     info("dcc: dccsight failed: $err\n");
     return 0;
   }
 
-  if ($raw_x_dcc) {
-    dbg("dcc: learned response: %s", $raw_x_dcc);
+  if ($raw_x_dcc ne '') { #TODO check if working
+    dbg("dcc: learned response: $raw_x_dcc");
     return 1;
   }
 
@@ -1068,22 +1303,26 @@ sub dccsight_learn {
 }
 
 sub plugin_report {
-  my ($self, $options) = @_;
+  my ($self, $opts) = @_;
 
-  return if $options->{report}->{options}->{dont_report_to_dcc};
-  $self->get_dcc_interface();
   return if $self->{dcc_disabled};
+  return if !$self->{main}->{conf}->{use_dcc};
+  return if $opts->{report}->{options}->{dont_report_to_dcc};
 
-  # get the metadata from the message so we can report the external relay
-  $options->{msg}->extract_message_metadata($options->{report}->{main});
-  my $envelope = $options->{msg}->{metadata}->{relays_external}->[0];
-  my ($raw_x_dcc, $cksums) = $self->ask_dcc("reporter:", $options->{report},
-                                           $options->{text}, $envelope);
+  return if !$self->get_dcc_interface();
+
+  my $report = $opts->{report};
 
+  my $timer = $self->{main}->time_method("dcc_report");
+
+  # get the metadata from the message so we can report the external relay
+  $opts->{msg}->extract_message_metadata($report->{main});
+  my $envelope = $opts->{msg}->{metadata}->{relays_external}->[0];
+  my ($raw_x_dcc, undef) = $self->ask_dcc('reporter:', $report,
+                                           $opts->{text}, $envelope);
   if (defined $raw_x_dcc) {
-    $options->{report}->{report_available} = 1;
+    $report->{report_available} = $report->{report_return} = 1;
     info("reporter: spam reported to DCC");
-    $options->{report}->{report_return} = 1;
   } else {
     info("reporter: could not report spam to DCC");
   }
index 02c9c33668e2093439bf538427dd196bcfaffde7..d98d9407d074e61066f23159c504253d849ade93 100644 (file)
@@ -30,6 +30,12 @@ Taking into account signatures from any signing domains:
  full   DKIM_VALID_AU         eval:check_dkim_valid_author_sig()
  full   DKIM_VALID_EF         eval:check_dkim_valid_envelopefrom()
 
+Taking into account ARC signatures (Authenticated Received Chain, RFC 8617)
+from any signing domains:
+
+ full   ARC_SIGNED            eval:check_arc_signed()
+ full   ARC_VALID             eval:check_arc_valid()
+
 Taking into account signatures from specified signing domains only:
 (quotes may be omitted on domain names consisting only of letters, digits,
 dots, and minus characters)
@@ -127,6 +133,7 @@ package Mail::SpamAssassin::Plugin::DKIM;
 use Mail::SpamAssassin::Plugin;
 use Mail::SpamAssassin::Logger;
 use Mail::SpamAssassin::Timeout;
+use Mail::SpamAssassin::Util qw(idn_to_ascii);
 
 use strict;
 use warnings;
@@ -145,19 +152,23 @@ sub new {
   bless ($self, $class);
 
   # signatures
-  $self->register_eval_rule("check_dkim_signed");
-  $self->register_eval_rule("check_dkim_valid");
-  $self->register_eval_rule("check_dkim_valid_author_sig");
-  $self->register_eval_rule("check_dkim_testing");
-  $self->register_eval_rule("check_dkim_valid_envelopefrom");
+  $self->register_eval_rule("check_dkim_signed", $Mail::SpamAssassin::Conf::TYPE_FULL_EVALS);
+  $self->register_eval_rule("check_arc_signed", $Mail::SpamAssassin::Conf::TYPE_FULL_EVALS);
+  $self->register_eval_rule("check_dkim_valid", $Mail::SpamAssassin::Conf::TYPE_FULL_EVALS);
+  $self->register_eval_rule("check_arc_valid", $Mail::SpamAssassin::Conf::TYPE_FULL_EVALS);
+  $self->register_eval_rule("check_dkim_valid_author_sig", $Mail::SpamAssassin::Conf::TYPE_FULL_EVALS);
+  $self->register_eval_rule("check_dkim_testing", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_dkim_valid_envelopefrom", $Mail::SpamAssassin::Conf::TYPE_FULL_EVALS);
 
   # author domain signing practices
-  $self->register_eval_rule("check_dkim_adsp");
-  $self->register_eval_rule("check_dkim_dependable");
+  $self->register_eval_rule("check_dkim_adsp", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_dkim_dependable", $Mail::SpamAssassin::Conf::TYPE_FULL_EVALS);
 
-  # whitelisting
-  $self->register_eval_rule("check_for_dkim_whitelist_from");
-  $self->register_eval_rule("check_for_def_dkim_whitelist_from");
+  # welcomelisting
+  $self->register_eval_rule("check_for_dkim_welcomelist_from", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_for_dkim_whitelist_from", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);  #Stub - Remove in SA 4.1
+  $self->register_eval_rule("check_for_def_dkim_welcomelist_from", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_for_def_dkim_whitelist_from", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);  #Stub - Remove in SA 4.1
 
   # old names (aliases) for compatibility
   $self->register_eval_rule("check_dkim_verified");  # = check_dkim_valid
@@ -179,17 +190,19 @@ sub set_config {
 
 =over 4
 
-=item whitelist_from_dkim author@example.com [signing-domain]
+=item welcomelist_from_dkim author@example.com [signing-domain]
+
+Previously whitelist_from_dkim which will work interchangeably until 4.1.
 
-Works similarly to whitelist_from, except that in addition to matching
+Works similarly to welcomelist_from, except that in addition to matching
 an author address (From) to the pattern in the first parameter, the message
 must also carry a valid Domain Keys Identified Mail (DKIM) signature made by
 a signing domain (SDID, i.e. the d= tag) that is acceptable to us.
 
-Only one whitelist entry is allowed per line, as in C<whitelist_from_rcvd>.
-Multiple C<whitelist_from_dkim> lines are allowed. File-glob style characters
+Only one welcomelist entry is allowed per line, as in C<welcomelist_from_rcvd>.
+Multiple C<welcomelist_from_dkim> lines are allowed. File-glob style characters
 are allowed for the From address (the first parameter), just like with
-C<whitelist_from_rcvd>.
+C<welcomelist_from_rcvd>.
 
 The second parameter (the signing-domain) does not accept full file-glob style
 wildcards, although a simple '*.' (or just a '.') prefix to a domain name
@@ -201,39 +214,43 @@ will be an Author Domain Signature (sometimes called first-party signature)
 which is a signature where the signing domain (SDID) of a signature matches
 the domain of the author's address (i.e. the address in a From header field).
 
-Since this whitelist requires a DKIM check to be made, network tests must
+Since this welcomelist requires a DKIM check to be made, network tests must
 be enabled.
 
-Examples of whitelisting based on an author domain signature (first-party):
+Examples of welcomelisting based on an author domain signature (first-party):
 
-  whitelist_from_dkim joe@example.com
-  whitelist_from_dkim *@corp.example.com
-  whitelist_from_dkim *@*.example.com
+  welcomelist_from_dkim joe@example.com
+  welcomelist_from_dkim *@corp.example.com
+  welcomelist_from_dkim *@*.example.com
 
-Examples of whitelisting based on third-party signatures:
+Examples of welcomelisting based on third-party signatures:
 
-  whitelist_from_dkim jane@example.net      example.org
-  whitelist_from_dkim rick@info.example.net example.net
-  whitelist_from_dkim *@info.example.net    example.net
-  whitelist_from_dkim *@*                   mail7.remailer.example.com
-  whitelist_from_dkim *@*                   *.remailer.example.com
+  welcomelist_from_dkim jane@example.net      example.org
+  welcomelist_from_dkim rick@info.example.net example.net
+  welcomelist_from_dkim *@info.example.net    example.net
+  welcomelist_from_dkim *@*                   mail7.remailer.example.com
+  welcomelist_from_dkim *@*                   *.remailer.example.com
 
-=item def_whitelist_from_dkim author@example.com [signing-domain]
+=item def_welcomelist_from_dkim author@example.com [signing-domain]
 
-Same as C<whitelist_from_dkim>, but used for the default whitelist entries
-in the SpamAssassin distribution.  The whitelist score is lower, because
+Previously def_whitelist_from_dkim which will work interchangeably until 4.1.
+
+Same as C<welcomelist_from_dkim>, but used for the default welcomelist entries
+in the SpamAssassin distribution.  The welcomelist score is lower, because
 these are often targets for abuse of public mailers which sign their mail.
 
-=item unwhitelist_from_dkim author@example.com [signing-domain]
+=item unwelcomelist_from_dkim author@example.com [signing-domain]
+
+Previously unwhitelist_from_dkim which will work interchangeably until 4.1.
 
 Removes an email address with its corresponding signing-domain field
-from def_whitelist_from_dkim and whitelist_from_dkim tables, if it exists.
-Parameters to unwhitelist_from_dkim must exactly match the parameters of
-a corresponding whitelist_from_dkim or def_whitelist_from_dkim config
+from def_welcomelist_from_dkim and welcomelist_from_dkim tables, if it exists.
+Parameters to unwelcomelist_from_dkim must exactly match the parameters of
+a corresponding welcomelist_from_dkim or def_welcomelist_from_dkim config
 option which created the entry, for it to be removed (a domain name is
 matched case-insensitively);  i.e. if a signing-domain parameter was
-specified in a whitelisting command, it must also be specified in the
-unwhitelisting command.
+specified in a welcomelisting command, it must also be specified in the
+unwelcomelisting command.
 
 Useful for removing undesired default entries from a distributed configuration
 by a local or site-specific configuration or by C<user_prefs>.
@@ -375,7 +392,7 @@ Example:
 =item dkim_minimum_key_bits n             (default: 1024)
 
 The smallest size of a signing key (in bits) for a valid signature to be
-considered for whitelisting. Additionally, the eval function check_dkim_valid()
+considered for welcomelisting. Additionally, the eval function check_dkim_valid()
 will return false on short keys when called with explicitly listed domains,
 and the eval function check_dkim_valid_author_sig() will return false on short
 keys (regardless of its arguments). Setting the option to 0 disables a key
@@ -389,11 +406,13 @@ prepend its own signature on a copy of some third party mail and re-send it,
 which makes it no more trustworthy than without such signature. This is also
 a reason for a rule DKIM_VALID to have a near-zero score, i.e. a rule hit
 is only informational.
+This option is evaluated on ARC signatures checks as well.
 
 =cut
 
   push (@cmds, {
-    setting => 'whitelist_from_dkim',
+    setting => 'welcomelist_from_dkim',
+    aliases => ['whitelist_from_dkim'], # removed in 4.1
     type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST,
     code => sub {
       my ($self, $key, $value, $line) = @_;
@@ -407,13 +426,14 @@ is only informational.
       my $address = $1;
       my $sdid = defined $2 ? $2 : '';  # empty implies author domain signature
       $address =~ s/(\@[^@]*)\z/lc($1)/e;  # lowercase the email address domain
-      $self->{parser}->add_to_addrlist_dkim('whitelist_from_dkim',
+      $self->{parser}->add_to_addrlist_dkim('welcomelist_from_dkim',
                                             $address, lc $sdid);
     }
   });
 
   push (@cmds, {
-    setting => 'def_whitelist_from_dkim',
+    setting => 'def_welcomelist_from_dkim',
+    aliases => ['def_whitelist_from_dkim'], # removed in 4.1
     type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST,
     code => sub {
       my ($self, $key, $value, $line) = @_;
@@ -427,13 +447,14 @@ is only informational.
       my $address = $1;
       my $sdid = defined $2 ? $2 : '';  # empty implies author domain signature
       $address =~ s/(\@[^@]*)\z/lc($1)/e;  # lowercase the email address domain
-      $self->{parser}->add_to_addrlist_dkim('def_whitelist_from_dkim',
+      $self->{parser}->add_to_addrlist_dkim('def_welcomelist_from_dkim',
                                             $address, lc $sdid);
     }
   });
 
   push (@cmds, {
-    setting => 'unwhitelist_from_dkim',
+    setting => 'unwelcomelist_from_dkim',
+    aliases => ['unwhitelist_from_dkim'], # removed in 4.1
     type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST,
     code => sub {
       my ($self, $key, $value, $line) = @_;
@@ -447,9 +468,9 @@ is only informational.
       my $address = $1;
       my $sdid = defined $2 ? $2 : '';  # empty implies author domain signature
       $address =~ s/(\@[^@]*)\z/lc($1)/e;  # lowercase the email address domain
-      $self->{parser}->remove_from_addrlist_dkim('whitelist_from_dkim',
+      $self->{parser}->remove_from_addrlist_dkim('welcomelist_from_dkim',
                                                  $address, lc $sdid);
-      $self->{parser}->remove_from_addrlist_dkim('def_whitelist_from_dkim',
+      $self->{parser}->remove_from_addrlist_dkim('def_welcomelist_from_dkim',
                                                  $address, lc $sdid);
     }
   });
@@ -480,7 +501,7 @@ is only informational.
     }
   });
 
-  # minimal signing key size in bits that is acceptable for whitelisting
+  # minimal signing key size in bits that is acceptable for welcomelisting
   push (@cmds, {
     setting => 'dkim_minimum_key_bits',
     default => 1024,
@@ -530,6 +551,18 @@ sub check_dkim_signed {
   return $result;
 }
 
+sub check_arc_signed {
+  my ($self, $pms, $full_ref, @acceptable_domains) = @_;
+  $self->_check_dkim_signature($pms)  if !$pms->{arc_checked_signature};
+  my $result = 0;
+  if (!$pms->{arc_signed}) {
+    # don't bother
+  } elsif (!@acceptable_domains) {
+    $result = 1;  # no additional constraints, any signing domain will do
+  }
+  return $result;
+}
+
 sub check_dkim_valid {
   my ($self, $pms, $full_ref, @acceptable_domains) = @_;
   $self->_check_dkim_signature($pms)  if !$pms->{dkim_checked_signature};
@@ -545,6 +578,19 @@ sub check_dkim_valid {
   return $result;
 }
 
+sub check_arc_valid {
+  my ($self, $pms, $full_ref, @acceptable_domains) = @_;
+  $self->_check_dkim_signature($pms)  if !$pms->{arc_checked_signature};
+  my $result = 0;
+  if (!$pms->{arc_valid}) {
+    # don't bother
+  } elsif (!@acceptable_domains) {
+    $result = 1;  # no additional constraints, any signing domain will do,
+                  # also any signing key size will do
+  }
+  return $result;
+}
+
 sub check_dkim_valid_author_sig {
   my ($self, $pms, $full_ref, @acceptable_domains) = @_;
   $self->_check_dkim_signature($pms)  if !$pms->{dkim_checked_signature};
@@ -560,9 +606,10 @@ sub check_dkim_valid_author_sig {
 sub check_dkim_valid_envelopefrom {
   my ($self, $pms, $full_ref) = @_;
   my $result = 0;
-  my $envfrom=$self->{'main'}->{'registryboundaries'}->uri_to_domain($pms->get("EnvelopeFrom"));
+  my ($envfrom) = ($pms->get('EnvelopeFrom:addr')||'') =~ /\@(\S+)/;
   # if no envelopeFrom, it cannot be valid
-  return $result if !$envfrom;
+  return $result if !defined $envfrom;
+  $envfrom = lc $envfrom;
   $self->_check_dkim_signature($pms)  if !$pms->{dkim_checked_signature};
   if (!$pms->{dkim_valid}) {
     # don't bother
@@ -646,19 +693,21 @@ sub check_dkim_testing {
   return $result;
 }
 
-sub check_for_dkim_whitelist_from {
+sub check_for_dkim_welcomelist_from {
   my ($self, $pms) = @_;
-  $self->_check_dkim_whitelist($pms)  if !$pms->{whitelist_checked};
-  return $pms->{dkim_match_in_whitelist_from_dkim} || 
-         $pms->{dkim_match_in_whitelist_auth};
+  $self->_check_dkim_welcomelist($pms)  if !$pms->{welcomelist_checked};
+  return ($pms->{dkim_match_in_welcomelist_from_dkim} || 
+          $pms->{dkim_match_in_welcomelist_auth}) ? 1 : 0;
 }
+*check_for_dkim_whitelist_from = \&check_for_dkim_welcomelist_from; # removed in 4.1
 
-sub check_for_def_dkim_whitelist_from {
+sub check_for_def_dkim_welcomelist_from {
   my ($self, $pms) = @_;
-  $self->_check_dkim_whitelist($pms)  if !$pms->{whitelist_checked};
-  return $pms->{dkim_match_in_def_whitelist_from_dkim} || 
-         $pms->{dkim_match_in_def_whitelist_auth};
+  $self->_check_dkim_welcomelist($pms)  if !$pms->{welcomelist_checked};
+  return ($pms->{dkim_match_in_def_welcomelist_from_dkim} || 
+         $pms->{dkim_match_in_def_welcomelist_auth}) ? 1 : 0;
 }
+*check_for_def_dkim_whitelist_from = \&check_for_def_dkim_welcomelist_from; # removed in 4.1
 
 # ---------------------------------------------------------------------------
 
@@ -667,8 +716,7 @@ sub _dkim_load_modules {
 
   if (!$self->{tried_loading}) {
     $self->{service_available} = 0;
-    my $timemethod = $self->{main}->UNIVERSAL::can("time_method") &&
-                     $self->{main}->time_method("dkim_load_modules");
+    my $timemethod = $self->{main}->time_method("dkim_load_modules");
     my $eval_stat;
     eval {
       # Have to do this so that RPM doesn't find these as required perl modules.
@@ -686,6 +734,9 @@ sub _dkim_load_modules {
       my $version = Mail::DKIM::Verifier->VERSION;
       if (version->parse($version) >= version->parse(0.31)) {
         dbg("dkim: using Mail::DKIM version $version");
+      } elsif (version->parse($version) < version->parse(0.50)) {
+        dbg("dkim: Mail::DKIM $version is older than 0.50 ".
+             "ARC support will not be available, suggested upgrade to 0.50 or later!");
       } else {
         info("dkim: Mail::DKIM $version is older than the required ".
              "minimal version 0.31, suggested upgrade to 0.37 or later!");
@@ -698,6 +749,18 @@ sub _dkim_load_modules {
         eval { require Mail::DKIM::DkimPolicy }  # ignoring status
       }
     }
+    eval {
+      # Have to do this so that RPM doesn't find these as required perl modules.
+      { require Mail::DKIM::ARC::Verifier }
+      $self->{arc_available} = 1;
+    } or do {
+      $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
+      if (defined $eval_stat) {
+        dbg("dkim: cannot load Mail::DKIM::ARC module, DKIM::ARC checks disabled: %s",
+          $eval_stat);
+      }
+      $self->{arc_available} = 0;
+    };
   }
   return $self->{service_available};
 }
@@ -720,8 +783,8 @@ sub _check_dkim_signed_by {
       next if $minimum_key_bits && $sig->{_spamassassin_key_size} &&
               $sig->{_spamassassin_key_size} < $minimum_key_bits;
     }
-    my $sdid = $sig->domain;
-    next if !defined $sdid;  # a signature with a missing required tag 'd' ?
+    my ($sdid) = (defined $sig->identity)? $sig->identity =~ /\@(\S+)/ : ($sig->domain);
+    next if !defined $sdid;  # a signature with a missing required tag 'd' or 'i' ?
     $sdid = lc $sdid;
     if ($must_be_author_domain_signature) {
       next if !$pms->{dkim_author_domains}->{$sdid};
@@ -765,9 +828,10 @@ sub _check_dkim_signature {
   my ($self, $pms) = @_;
 
   my $conf = $pms->{conf};
-  my($verifier, @signatures, @valid_signatures);
+  my($verifier, $arc_verifier, @signatures, @arc_signatures, @valid_signatures, @arc_valid_signatures);
 
   $pms->{dkim_checked_signature} = 1; # has this sub already been invoked?
+  $pms->{arc_checked_signature} = 1;  # has this sub already been invoked?
   $pms->{dkim_signatures_ready} = 0;  # have we obtained & verified signatures?
   $pms->{dkim_signatures_dependable} = 0;
   # dkim_signatures_dependable =
@@ -776,16 +840,18 @@ sub _check_dkim_signature {
   #     (no signatures, or message was not truncated) )
   $pms->{dkim_signatures} = \@signatures;
   $pms->{dkim_valid_signatures} = \@valid_signatures;
+  $pms->{arc_signatures} = \@arc_signatures;
+  $pms->{arc_valid_signatures} = \@arc_valid_signatures;
   $pms->{dkim_signed} = 0;
+  $pms->{arc_signed} = 0;
   $pms->{dkim_valid} = 0;
+  $pms->{arc_valid} = 0;
   $pms->{dkim_key_testing} = 0;
   # the following hashes are keyed by a signing domain (SDID):
   $pms->{dkim_author_sig_tempfailed} = {}; # DNS timeout verifying author sign.
   $pms->{dkim_has_valid_author_sig} = {};  # a valid author domain signature
   $pms->{dkim_has_any_author_sig} = {};  # valid or invalid author domain sign.
 
-  $self->_get_authors($pms)  if !$pms->{dkim_author_addresses};
-
   my $suppl_attrib = $pms->{msg}->{suppl_attrib};
   if (defined $suppl_attrib && exists $suppl_attrib->{dkim_signatures}) {
     # caller of SpamAssassin already supplied DKIM signature objects
@@ -793,97 +859,166 @@ sub _check_dkim_signature {
     @signatures = @$provided_signatures  if ref $provided_signatures;
     $pms->{dkim_signatures_ready} = 1;
     $pms->{dkim_signatures_dependable} = 1;
-    dbg("dkim: signatures provided by the caller, %d signatures",
+    dbg("dkim: DKIM signatures provided by the caller, %d signatures",
         scalar(@signatures));
   }
+  if (defined $suppl_attrib && exists $suppl_attrib->{arc_signatures}) {
+    # caller of SpamAssassin already supplied ARC signature objects
+    my $provided_arc_signatures = $suppl_attrib->{arc_signatures};
+    @arc_signatures = @$provided_arc_signatures  if ref $provided_arc_signatures;
+    $pms->{arc_signatures_ready} = 1;
+    $pms->{arc_signatures_dependable} = 1;
+    dbg("dkim: ARC signatures provided by the caller, %d signatures",
+        scalar(@arc_signatures));
+  }
 
-  if ($pms->{dkim_signatures_ready}) {
+  if ($pms->{dkim_signatures_ready} or $pms->{arc_signatures_ready}) {
     # signatures already available and verified
+    _check_valid_signature($self, $pms, $verifier, 'DKIM', \@signatures) if $self->{service_available};
+    _check_valid_signature($self, $pms, $arc_verifier, 'ARC', \@arc_signatures) if $self->{arc_available};
   } elsif (!$pms->is_dns_available()) {
     dbg("dkim: signature verification disabled, DNS resolving not available");
   } elsif (!$self->_dkim_load_modules()) {
     # Mail::DKIM module not available
   } else {
     # signature objects not provided by the caller, must verify for ourselves
-    my $timemethod = $self->{main}->UNIVERSAL::can("time_method") &&
-                     $self->{main}->time_method("check_dkim_signature");
-    use version 0.77;
-    if (version->parse(Mail::DKIM::Verifier->VERSION) >= version->parse(0.40)) {
+    my $timemethod = $self->{main}->time_method("check_dkim_signature");
+    if (Mail::DKIM::Verifier->VERSION >= 0.40) {
       my $edns = $conf->{dns_options}->{edns};
       if ($edns && $edns >= 1024) {
         # Let Mail::DKIM use our interface to Net::DNS::Resolver.
         # Only do so if EDNS0 provides a reasonably-sized UDP payload size,
         # as our interface does not provide a DNS fallback to TCP, unlike
         # the Net::DNS::Resolver::send which does provide it.
+        # See also Bug 7265 regarding a choice of a resolver.
+      # my $res = $self->{main}->{resolver}->get_resolver;
         my $res = $self->{main}->{resolver};
         dbg("dkim: providing our own resolver: %s", ref $res);
         Mail::DKIM::DNS::resolver($res);
       }
     }
-    $verifier = Mail::DKIM::Verifier->new;
-    if (!$verifier) {
+    $verifier = Mail::DKIM::Verifier->new if $self->{service_available};
+    _check_signature($self, $pms, $verifier, 'DKIM', \@signatures) if $self->{service_available};
+    $arc_verifier = Mail::DKIM::ARC::Verifier->new if $self->{arc_available};
+    _check_signature($self, $pms, $arc_verifier, 'ARC', \@arc_signatures) if $self->{arc_available};
+  }
+}
+
+sub _check_signature {
+  my($self, $pms, $verifier, $type, $signatures) = @_;
+
+  my $sig_type = lc $type;
+  $self->_get_authors($pms)  if !$pms->{"${sig_type}_author_addresses"};
+
+  my(@valid_signatures);
+  my $conf = $pms->{conf};
+  if (!$verifier) {
+    if ($type eq 'DKIM') {
       dbg("dkim: cannot create Mail::DKIM::Verifier object");
-      return;
+    } elsif ($type eq 'ARC') {
+      dbg("dkim: cannot create Mail::DKIM::ARC::Verifier object");
     }
-    $pms->{dkim_verifier} = $verifier;
-    #
-    # feed content of a message into verifier, using \r\n endings,
-    # required by Mail::DKIM API (see bug 5300)
-    # note: bug 5179 comment 28: perl does silly things on non-Unix platforms
-    # unless we use \015\012 instead of \r\n
-    eval {
-      my $str = $pms->{msg}->get_pristine();
-      $str =~ s/\r?\n/\015\012/sg;  # ensure \015\012 ending
+    return;
+  } else {
+    if ($type eq 'DKIM') {
+      $pms->{dkim_verifier} = $verifier;
+    } elsif ($type eq 'ARC') {
+      $pms->{arc_verifier} = $verifier;
+    }
+  }
+  # feed content of a message into verifier, using \r\n endings,
+  # required by Mail::DKIM API (see bug 5300)
+  # note: bug 5179 comment 28: perl does silly things on non-Unix platforms
+  # unless we use \015\012 instead of \r\n
+  eval {
+    my $str = $pms->{msg}->get_pristine();
+    if ($pms->{msg}->{line_ending} eq "\015\012") {
+      # message already CRLF, just feed it
       $verifier->PRINT($str);
-      1;
-    } or do {  # intercept die() exceptions and render safe
-      my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
-      dbg("dkim: verification failed, intercepted error: $eval_stat");
-      return 0;           # cannot verify message
-    };
-
-    my $timeout = $conf->{dkim_timeout};
-    my $timer = Mail::SpamAssassin::Timeout->new(
-                  { secs => $timeout, deadline => $pms->{master_deadline} });
-
-    my $err = $timer->run_and_catch(sub {
-      dbg("dkim: performing public key lookup and signature verification");
-      $verifier->CLOSE();  # the action happens here
-
-      # currently SpamAssassin's parsing is better than Mail::Address parsing,
-      # don't bother fetching $verifier->message_originator->address
-      # to replace what we already have in $pms->{dkim_author_addresses}
-
-      # versions before 0.29 only provided a public interface to fetch one
-      # signature, newer versions allow access to all signatures of a message
-      @signatures = $verifier->UNIVERSAL::can("signatures") ?
-                                 $verifier->signatures : $verifier->signature;
-    });
-    if ($timer->timed_out()) {
-      dbg("dkim: public key lookup or verification timed out after %s s",
-          $timeout );
+    } else {
+      # feeding large chunk to Mail::DKIM is _much_ faster than line-by-line
+      $str =~ s/\012/\015\012/gs; # LF -> CRLF
+      $verifier->PRINT($str);
+      undef $str;
+    }
+    1;
+  } or do {  # intercept die() exceptions and render safe
+    my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
+    dbg("dkim: verification failed, intercepted error: $eval_stat");
+    return 0;           # cannot verify message
+  };
+
+  my $timeout = $conf->{dkim_timeout};
+  my $timer = Mail::SpamAssassin::Timeout->new(
+                { secs => $timeout, deadline => $pms->{master_deadline} });
+
+  my $err = $timer->run_and_catch(sub {
+    dbg("dkim: performing public $type key lookup and signature verification");
+    $verifier->CLOSE();  # the action happens here
+
+    # currently SpamAssassin's parsing is better than Mail::Address parsing,
+    # don't bother fetching $verifier->message_originator->address
+    # to replace what we already have in $pms->{dkim_author_addresses}
+
+    # versions before 0.29 only provided a public interface to fetch one
+    # signature, newer versions allow access to all signatures of a message
+    @$signatures = $verifier->UNIVERSAL::can("signatures") ?
+                               $verifier->signatures : $verifier->signature;
+    if (would_log("dbg","dkim")) {
+      foreach my $signature (@$signatures) {
+        dbg("dkim: $type signature i=%s d=%s",
+          map(!defined $_ ? '(undef)' : $_,
+            $signature->identity, $signature->domain
+          )
+        );
+      }
+    }
+  });
+  if ($timer->timed_out()) {
+    dbg("dkim: public key lookup or verification timed out after %s s",
+        $timeout );
 #***
-    # $pms->{dkim_author_sig_tempfailed}->{$_} = 1  for ...
+  # $pms->{dkim_author_sig_tempfailed}->{$_} = 1  for ...
 
-    } elsif ($err) {
-      chomp $err;
-      dbg("dkim: public key lookup or verification failed: $err");
-    }
+  } elsif ($err) {
+    chomp $err;
+    dbg("dkim: $type public key lookup or verification failed: $err");
+  }
+  if ($type eq 'DKIM') {
     $pms->{dkim_signatures_ready} = 1;
-    if (!@signatures || !$pms->{tests_already_hit}->{'__TRUNCATED'}) {
+    if (!@$signatures || !$pms->{tests_already_hit}->{'__TRUNCATED'}) {
       $pms->{dkim_signatures_dependable} = 1;
     }
+    _check_valid_signature($self, $pms, $verifier, 'DKIM', \@$signatures) if $self->{service_available};
+  } elsif ($type eq 'ARC') {
+    $pms->{arc_signatures_ready} = 1;
+    if (!@$signatures || !$pms->{tests_already_hit}->{'__TRUNCATED'}) {
+      $pms->{arc_signatures_dependable} = 1;
+    }
+    _check_valid_signature($self, $pms, $verifier, 'ARC', \@$signatures) if $self->{arc_available};
   }
+}
 
-  if ($pms->{dkim_signatures_ready}) {
+sub _check_valid_signature {
+  my($self, $pms, $verifier, $type, $signatures) = @_;
+
+  my $sig_type = lc $type;
+  $self->_get_authors($pms)  if !$pms->{"${sig_type}_author_addresses"};
+
+  my(@valid_signatures);
+  my $conf = $pms->{conf};
+  # DKIM signatures check
+  if ($pms->{"${sig_type}_signatures_ready"}) {
     my $sig_result_supported;
+    # dkim_minimum_key_bits is evaluated for ARC signatures as well
     my $minimum_key_bits = $conf->{dkim_minimum_key_bits};
-    foreach my $signature (@signatures) {
+    foreach my $signature (@$signatures) {
       # old versions of Mail::DKIM would give undef for an invalid signature
       next if !defined $signature;
-      next if !$signature->selector; # empty selector
-
       $sig_result_supported = $signature->UNIVERSAL::can("result_detail");
+      # test for empty selector (must not treat a selector "0" as missing!)
+      next if !defined $signature->selector || $signature->selector eq "";
+
       my($info, $valid, $expired);
       $valid =
         ($sig_result_supported ? $signature : $verifier)->result eq 'pass';
@@ -904,68 +1039,96 @@ sub _check_dkim_signature {
       push(@valid_signatures, $signature)  if $valid && !$expired;
 
       # check if we have a potential Author Domain Signature, valid or not
-      my $d = $signature->domain;
+      my ($d) = (defined $signature->identity)? $signature->identity =~ /\@(\S+)/ : ($signature->domain);
       if (!defined $d) {
         # can be undefined on a broken signature with missing required tags
       } else {
         $d = lc $d;
-        if ($pms->{dkim_author_domains}->{$d}) {  # SDID matches author domain
-          $pms->{dkim_has_any_author_sig}->{$d} = 1;
+        if ($pms->{"${sig_type}_author_domains"}->{$d}) {  # SDID matches author domain
+          $pms->{"${sig_type}_has_any_author_sig"}->{$d} = 1;
           if ($valid && !$expired &&
               $key_size && $key_size >= $minimum_key_bits) {
-            $pms->{dkim_has_valid_author_sig}->{$d} = 1;
+            $pms->{"${sig_type}_has_valid_author_sig"}->{$d} = 1;
           } elsif ( ($sig_result_supported ? $signature
                                            : $verifier)->result_detail
                    =~ /\b(?:timed out|SERVFAIL)\b/i) {
-            $pms->{dkim_author_sig_tempfailed}->{$d} = 1;
+            $pms->{"${sig_type}_author_sig_tempfailed"}->{$d} = 1;
           }
         }
       }
-      if (would_log("dbg","dkim")) {
-        dbg("dkim: %s %s, i=%s, d=%s, s=%s, a=%s, c=%s, %s, %s, %s",
-          $info,
-          $signature->isa('Mail::DKIM::DkSignature') ? 'DK' : 'DKIM',
-          map(!defined $_ ? '(undef)' : $_,
-            $signature->identity, $d, $signature->selector,
-            $signature->algorithm, scalar($signature->canonicalization),
-            $key_size ? "key_bits=$key_size" : "unknown key size",
-            ($sig_result_supported ? $signature : $verifier)->result ),
-          defined $d && $pms->{dkim_author_domains}->{$d}
-            ? 'matches author domain'
-            : 'does not match author domain',
-        );
+      if ($type eq 'DKIM') {
+        if (would_log("dbg","dkim")) {
+          dbg("dkim: %s %s, i=%s, d=%s, s=%s, a=%s, c=%s, %s, %s, %s",
+            $info,
+            $signature->isa('Mail::DKIM::DkSignature') ? 'DK' : 'DKIM',
+            map(!defined $_ ? '(undef)' : $_,
+              $signature->identity, $d, $signature->selector,
+              $signature->algorithm, scalar($signature->canonicalization),
+              $key_size ? "key_bits=$key_size" : "unknown key size",
+              ($sig_result_supported ? $signature : $verifier)->result ),
+            defined $d && $pms->{dkim_author_domains}->{$d}
+              ? 'matches author domain'
+              : 'does not match author domain',
+          );
+        }
+      } elsif ($type eq 'ARC') {
+        if (would_log("dbg","dkim")) {
+          dbg("dkim: %s %s, i=%s, d=%s, s=%s, a=%s, c=%s, %s, %s, %s",
+            $info,
+            $type,
+            map(!defined $_ ? '(undef)' : $_,
+              $signature->identity, $d, $signature->selector,
+              $signature->algorithm, scalar($signature->canonicalization),
+              $key_size ? "key_bits=$key_size" : "unknown key size",
+              ($sig_result_supported ? $signature : $verifier)->result ),
+            defined $d && $pms->{arc_author_domains}->{$d}
+              ? 'matches author domain'
+              : 'does not match author domain',
+          );
+        }
       }
     }
+
     if (@valid_signatures) {
-      $pms->{dkim_signed} = 1;
-      $pms->{dkim_valid} = 1;
-      # let the result stand out more clearly in the log, use uppercase
-      my $sig = $valid_signatures[0];
-      my $sig_res = ($sig_result_supported ? $sig : $verifier)->result_detail;
-      dbg("dkim: signature verification result: %s", uc($sig_res));
+      if ($type eq 'DKIM') {
+        $pms->{dkim_signed} = 1;
+        $pms->{dkim_valid} = 1;
 
-      # supply values for both tags
-      my(%seen1, %seen2, %seen3, @identity_list, @domain_list, @selector_list);
-      @identity_list = grep(defined $_ && $_ ne '' && !$seen1{$_}++,
+        # supply values for both tags
+        my(%seen1, %seen2, %seen3, @identity_list, @domain_list, @selector_list);
+        @identity_list = grep(defined $_ && $_ ne '' && !$seen1{$_}++,
                             map($_->identity, @valid_signatures));
-      @domain_list =   grep(defined $_ && $_ ne '' && !$seen2{$_}++,
+        @domain_list =   grep(defined $_ && $_ ne '' && !$seen2{$_}++,
                             map($_->domain, @valid_signatures));
-      @selector_list = grep(defined $_ && $_ ne '' && !$seen3{$_}++,
+        @selector_list = grep(defined $_ && $_ ne '' && !$seen3{$_}++,
                             map($_->selector, @valid_signatures));
-      $pms->set_tag('DKIMIDENTITY',
+        $pms->set_tag('DKIMIDENTITY',
                     @identity_list == 1 ? $identity_list[0] : \@identity_list);
-      $pms->set_tag('DKIMDOMAIN',
+        $pms->set_tag('DKIMDOMAIN',
                     @domain_list == 1   ? $domain_list[0]   : \@domain_list);
-      $pms->set_tag('DKIMSELECTOR',
-                    @selector_list == 1   ? $selector_list[0]   : \@selector_list);
-    } elsif (@signatures) {
-      $pms->{dkim_signed} = 1;
-      my $sig = $signatures[0];
-      my $sig_res =
-        ($sig_result_supported && $sig ? $sig : $verifier)->result_detail;
-      dbg("dkim: signature verification result: %s", uc($sig_res));
+        $pms->set_tag('DKIMSELECTOR',
+                    @selector_list == 1 ? $selector_list[0] : \@selector_list);
+      } elsif ($type eq 'ARC') {
+        $pms->{arc_signed} = 1;
+        $pms->{arc_valid} = 1;
+      }
+      # let the result stand out more clearly in the log, use uppercase
+      my $sig = $valid_signatures[0];
+      my $sig_res = ($sig_result_supported ? $sig : $verifier)->result_detail;
+      dbg("dkim: $type signature verification result: %s", uc($sig_res));
+
+    } elsif (@$signatures) {
+      if ($type eq 'DKIM') {
+        $pms->{dkim_signed} = 1;
+      } elsif ($type eq 'ARC') {
+        $pms->{arc_signed} = 1;
+      }
+      my $sig = @$signatures[0];
+      my $sig_res = ($sig_result_supported ? $sig : $verifier)->result_detail;
+      dbg("dkim: $type signature verification result: %s", uc($sig_res));
+
     } else {
-      dbg("dkim: signature verification result: none");
+      dbg("dkim: $type signature verification result: none");
     }
   }
 }
@@ -1064,8 +1227,7 @@ sub _check_dkim_adsp {
           dbg("dkim: adsp not retrieved, module Mail::DKIM not available");
 
         } else {  # do the ADSP DNS lookup
-          my $timemethod = $self->{main}->UNIVERSAL::can("time_method") &&
-                           $self->{main}->time_method("check_dkim_adsp");
+          my $timemethod = $self->{main}->time_method("check_dkim_adsp");
 
           my $practices;  # author domain signing practices object
           my $timeout = $pms->{conf}->{dkim_timeout};
@@ -1074,12 +1236,13 @@ sub _check_dkim_adsp {
           my $err = $timer->run_and_catch(sub {
             eval {
               if (Mail::DKIM::AuthorDomainPolicy->UNIVERSAL::can("fetch")) {
+                my $author_domain_ace = idn_to_ascii($author_domain);
                 dbg("dkim: adsp: performing lookup on _adsp._domainkey.%s",
-                    $author_domain);
+                    $author_domain_ace);
                 # get our Net::DNS::Resolver object
                 my $res = $self->{main}->{resolver}->get_resolver;
                 $practices = Mail::DKIM::AuthorDomainPolicy->fetch(
-                               Protocol => "dns", Domain => $author_domain,
+                               Protocol => "dns", Domain => $author_domain_ace,
                                DnsResolver => $res);
               }
               1;
@@ -1128,36 +1291,36 @@ sub _check_dkim_adsp {
   }
 }
 
-sub _check_dkim_whitelist {
+sub _check_dkim_welcomelist {
   my ($self, $pms) = @_;
 
-  $pms->{whitelist_checked} = 1;
+  $pms->{welcomelist_checked} = 1;
 
   $self->_get_authors($pms)  if !$pms->{dkim_author_addresses};
 
   my $authors_str = join(", ", @{$pms->{dkim_author_addresses}});
   if ($authors_str eq '') {
-    dbg("dkim: check_dkim_whitelist: could not find author address");
+    dbg("dkim: check_dkim_weclomelist: could not find author address");
     return;
   }
 
-  # collect whitelist entries matching the author from all lists
+  # collect welcomelist entries matching the author from all lists
   my @acceptable_sdid_tuples;
   $self->_wlcheck_acceptable_signature($pms, \@acceptable_sdid_tuples,
-                                       'def_whitelist_from_dkim');
+                                       'def_welcomelist_from_dkim');
   $self->_wlcheck_author_signature($pms, \@acceptable_sdid_tuples,
-                                       'def_whitelist_auth');
+                                       'def_welcomelist_auth');
   $self->_wlcheck_acceptable_signature($pms, \@acceptable_sdid_tuples,
-                                       'whitelist_from_dkim');
+                                       'welcomelist_from_dkim');
   $self->_wlcheck_author_signature($pms, \@acceptable_sdid_tuples,
-                                       'whitelist_auth');
+                                       'welcomelist_auth');
   if (!@acceptable_sdid_tuples) {
     dbg("dkim: no wl entries match author %s, no need to verify sigs",
         $authors_str);
     return;
   }
 
-  # if the message doesn't pass DKIM validation, it can't pass DKIM whitelist
+  # if the message doesn't pass DKIM validation, it can't pass DKIM welcomelist
 
   # trigger a DKIM check;
   # continue if one or more signatures are valid or we want the debug info
@@ -1177,38 +1340,38 @@ sub _check_dkim_whitelist {
     }
   }
   if (@valid) {
-    dbg("dkim: author %s, WHITELISTED by %s",
+    dbg("dkim: author %s, WELCOMELISTED by %s",
         $authors_str, join(", ",@valid));
   } elsif (@fail) {
     dbg("dkim: author %s, found in %s BUT IGNORED",
         $authors_str, join(", ",@fail));
   } else {
-    dbg("dkim: author %s, not in any dkim whitelist", $authors_str);
+    dbg("dkim: author %s, not in any dkim welcomelist", $authors_str);
   }
 }
 
 # check for verifier-acceptable signatures; an empty (or undefined) signing
-# domain in a whitelist implies checking for an Author Domain Signature
+# domain in a welcomelist implies checking for an Author Domain Signature
 #
 sub _wlcheck_acceptable_signature {
   my ($self, $pms, $acceptable_sdid_tuples_ref, $wl) = @_;
   my $wl_ref = $pms->{conf}->{$wl};
   foreach my $author (@{$pms->{dkim_author_addresses}}) {
-    foreach my $white_addr (keys %$wl_ref) {
-      my $wl_addr_ref = $wl_ref->{$white_addr};
-      my $re = qr/$wl_addr_ref->{re}/i;
-    # dbg("dkim: WL %s %s, d: %s", $wl, $white_addr,
+    my $author_lc = lc($author);
+    foreach my $welcome_addr (keys %$wl_ref) {
+      my $wl_addr_ref = $wl_ref->{$welcome_addr};
+    # dbg("dkim: WL %s %s, d: %s", $wl, $welcome_addr,
     #     join(", ", map { $_ eq '' ? "''" : $_ } @{$wl_addr_ref->{domain}}));
-      if ($author =~ $re) {
+      if ($author_lc =~ /$wl_addr_ref->{re}/) {
         foreach my $sdid (@{$wl_addr_ref->{domain}}) {
-          push(@$acceptable_sdid_tuples_ref, [$author,$sdid,$wl,$re]);
+          push(@$acceptable_sdid_tuples_ref, [$author,$sdid,$wl,$welcome_addr]);
         }
       }
     }
   }
 }
 
-# use a traditional whitelist_from -style addrlist, the only acceptable DKIM
+# use a traditional welcomelist_from -style addrlist, the only acceptable DKIM
 # signature is an Author Domain Signature.  Note: don't pre-parse and store
 # domains; that's inefficient memory-wise and only saves one m//
 #
@@ -1216,11 +1379,11 @@ sub _wlcheck_author_signature {
   my ($self, $pms, $acceptable_sdid_tuples_ref, $wl) = @_;
   my $wl_ref = $pms->{conf}->{$wl};
   foreach my $author (@{$pms->{dkim_author_addresses}}) {
-    foreach my $white_addr (keys %$wl_ref) {
-      my $re = qr/$wl_ref->{$white_addr}/i;
-    # dbg("dkim: WL %s %s", $wl, $white_addr);
-      if ($author =~ $re) {
-        push(@$acceptable_sdid_tuples_ref, [$author,undef,$wl,$re]);
+    my $author_lc = lc($author);
+    foreach my $welcome_addr (keys %$wl_ref) {
+    # dbg("dkim: WL %s %s", $wl, $welcome_addr);
+      if ($author_lc =~ /$wl_ref->{$welcome_addr}/) {
+        push(@$acceptable_sdid_tuples_ref, [$author,undef,$wl,$welcome_addr]);
       }
     }
   }
@@ -1238,9 +1401,10 @@ sub _wlcheck_list {
   foreach my $signature (@{$pms->{dkim_signatures}}) {
     # old versions of Mail::DKIM would give undef for an invalid signature
     next if !defined $signature;
-    next if !$signature->selector; # empty selector
-
     my $sig_result_supported = $signature->UNIVERSAL::can("result_detail");
+    # test for empty selector (must not treat a selector "0" as missing!)
+    next if !defined $signature->selector || $signature->selector eq "";
+
     my($info, $valid, $expired, $key_size_weak);
     $valid =
       ($sig_result_supported ? $signature : $verifier)->result eq 'pass';
@@ -1256,13 +1420,13 @@ sub _wlcheck_list {
       }
     }
 
-    my $sdid = $signature->domain;
+    my ($sdid) = (defined $signature->identity)? $signature->identity =~ /\@(\S+)/ : ($signature->domain);
     $sdid = lc $sdid  if defined $sdid;
 
     my %tried_authors;
     foreach my $entry (@$acceptable_sdid_tuples_ref) {
-      my($author, $acceptable_sdid, $wl, $re) = @$entry;
-      # $re and $wl are here for logging purposes only, $re already checked.
+      my($author, $acceptable_sdid, $wl, $welcome_addr) = @$entry;
+      # $welcome_addr and $wl are here for logging purposes only, already checked.
       # The $acceptable_sdid is a verifier-acceptable signing domain
       # identifier (to be matched against a 'd' tag in signatures).
       # When $acceptable_sdid is undef or an empty string it implies
@@ -1274,7 +1438,7 @@ sub _wlcheck_list {
 
       my $matches = 0;
       if (!defined $sdid) {
-        # don't bother, invalid signature with a missing 'd' tag
+        # don't bother, invalid signature with a missing 'd' or 'i' tag
 
       } elsif (!defined $acceptable_sdid || $acceptable_sdid eq '') {
         # An "Author Domain Signature" (sometimes called a first-party
@@ -1287,7 +1451,7 @@ sub _wlcheck_list {
         $matches = 1  if $sdid eq $author_domain;
 
       } else {  # checking for verifier-acceptable signature
-        # The second argument to a 'whitelist_from_dkim' option is now (since
+        # The second argument to a 'welcomelist_from_dkim' option is now (since
         # version 3.3.0) supposed to be a signing domain (SDID), no longer an
         # identity (AUID). Nevertheless, be prepared to accept the full e-mail
         # address there for compatibility, and just ignore its local-part.
@@ -1303,17 +1467,17 @@ sub _wlcheck_list {
         if (would_log("dbg","dkim")) {
           if ($sdid eq $author_domain) {
             dbg("dkim: %s author domain signature by %s, MATCHES %s %s",
-                $info, $sdid, $wl, $re);
+                $info, $sdid, $wl, $welcome_addr);
           } else {
             dbg("dkim: %s third-party signature by %s, author domain %s, ".
-                "MATCHES %s %s", $info, $sdid, $author_domain, $wl, $re);
+                "MATCHES %s %s", $info, $sdid, $author_domain, $wl, $welcome_addr);
           }
         }
         # a defined value indicates at least a match, not necessarily valid
         # (this complication servers to preserve logging compatibility)
         $any_match_by_wl{$wl} = ''  if !exists $any_match_by_wl{$wl};
       }
-      # only valid signature can cause whitelisting
+      # only valid signature can cause welcomelisting
       $matches = 0  if !$valid || $expired || $key_size_weak;
 
       if ($matches) {
@@ -1328,4 +1492,7 @@ sub _wlcheck_list {
   return ($any_match_at_all, \%any_match_by_wl);
 }
 
+# Version features
+sub has_arc { 1 }
+
 1;
diff --git a/upstream/lib/Mail/SpamAssassin/Plugin/DMARC.pm b/upstream/lib/Mail/SpamAssassin/Plugin/DMARC.pm
new file mode 100644 (file)
index 0000000..4810c9f
--- /dev/null
@@ -0,0 +1,360 @@
+# <@LICENSE>
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to you under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# </@LICENSE>
+#
+# Author: Giovanni Bechis <gbechis@apache.org>
+
+=head1 NAME
+
+Mail::SpamAssassin::Plugin::DMARC - check DMARC policy
+
+=head1 SYNOPSIS
+
+  loadplugin Mail::SpamAssassin::Plugin::DMARC
+
+  ifplugin Mail::SpamAssassin::Plugin::DMARC
+    header DMARC_PASS eval:check_dmarc_pass()
+    describe DMARC_PASS DMARC pass policy
+    tflags DMARC_PASS net nice
+    score DMARC_PASS -0.001
+
+    header DMARC_REJECT eval:check_dmarc_reject()
+    describe DMARC_REJECT DMARC reject policy
+    tflags DMARC_REJECT net
+    score DMARC_REJECT 0.001
+
+    header DMARC_QUAR eval:check_dmarc_quarantine()
+    describe DMARC_QUAR DMARC quarantine policy
+    tflags DMARC_QUAR net
+    score DMARC_QUAR 0.001
+
+    header DMARC_NONE eval:check_dmarc_none()
+    describe DMARC_NONE DMARC none policy
+    tflags DMARC_NONE net
+    score DMARC_NONE 0.001
+
+    header DMARC_MISSING eval:check_dmarc_missing()
+    describe DMARC_MISSING Missing DMARC policy
+    tflags DMARC_MISSING net
+    score DMARC_MISSING 0.001
+  endif
+
+=head1 DESCRIPTION
+
+This plugin checks if emails match DMARC policy, the plugin needs both DKIM
+and SPF plugins enabled.
+
+=cut
+
+package Mail::SpamAssassin::Plugin::DMARC;
+
+use strict;
+use warnings;
+use re 'taint';
+
+my $VERSION = 0.2;
+
+use Mail::SpamAssassin;
+use Mail::SpamAssassin::Plugin;
+
+our @ISA = qw(Mail::SpamAssassin::Plugin);
+
+sub dbg { my $msg = shift; Mail::SpamAssassin::Logger::dbg("DMARC: $msg", @_); }
+sub info { my $msg = shift; Mail::SpamAssassin::Logger::info("DMARC: $msg", @_); }
+
+sub new {
+  my ($class, $mailsa) = @_;
+
+  $class = ref($class) || $class;
+  my $self = $class->SUPER::new($mailsa);
+  bless ($self, $class);
+
+  $self->set_config($mailsa->{conf});
+  $self->register_eval_rule("check_dmarc_pass", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_dmarc_reject", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_dmarc_quarantine", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_dmarc_none", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_dmarc_missing", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+
+  return $self;
+}
+
+sub set_config {
+  my ($self, $conf) = @_;
+  my @cmds;
+
+=over 4
+
+=item dmarc_save_reports ( 0 | 1 ) (default: 0)
+
+Store DMARC reports using Mail::DMARC::Store, mail-dmarc.ini must be configured to save and send DMARC reports.
+
+=back
+
+=cut
+
+  push(@cmds, {
+    setting => 'dmarc_save_reports',
+    default => 0,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
+  });
+
+  $conf->{parser}->register_commands(\@cmds);
+}
+
+sub parsed_metadata {
+  my ($self, $opts) = @_;
+  my $pms = $opts->{permsgstatus};
+
+  # Force waiting of SPF and DKIM results
+  $pms->{dmarc_async_queue} = [];
+}
+
+sub _check_eval {
+  my ($self, $pms, $result) = @_;
+
+  if (exists $pms->{dmarc_async_queue}) {
+    my $rulename = $pms->get_current_eval_rule_name();
+    push @{$pms->{dmarc_async_queue}}, sub {
+      if ($result->()) {
+        $pms->got_hit($rulename, '', ruletype => 'header');
+      } else {
+        $pms->rule_ready($rulename);
+      }
+    };
+    return; # return undef for async status
+  }
+
+  $self->_check_dmarc($pms);
+  # make sure not to return undef, as this is not async anymore
+  return $result->() || 0;
+}
+
+sub check_dmarc_pass {
+  my ($self, $pms, $name) = @_;
+
+  my $result = sub {
+    defined $pms->{dmarc_result} &&
+      $pms->{dmarc_result} eq 'pass' &&
+      $pms->{dmarc_policy} ne 'no policy available';
+  };
+
+  return $self->_check_eval($pms, $result);
+}
+
+sub check_dmarc_reject {
+  my ($self, $pms, $name) = @_;
+
+  my $result = sub {
+    defined $pms->{dmarc_result} &&
+      $pms->{dmarc_result} eq 'fail' &&
+      $pms->{dmarc_policy} eq 'reject';
+  };
+
+  return $self->_check_eval($pms, $result);
+}
+
+sub check_dmarc_quarantine {
+  my ($self, $pms, $name) = @_;
+
+  my $result = sub {
+    defined $pms->{dmarc_result} &&
+      $pms->{dmarc_result} eq 'fail' &&
+      $pms->{dmarc_policy} eq 'quarantine';
+  };
+
+  return $self->_check_eval($pms, $result);
+}
+
+sub check_dmarc_none {
+  my ($self, $pms, $name) = @_;
+
+  my $result = sub {
+    defined $pms->{dmarc_result} &&
+      $pms->{dmarc_result} eq 'fail' &&
+      $pms->{dmarc_policy} eq 'none';
+  };
+
+  return $self->_check_eval($pms, $result);
+}
+
+sub check_dmarc_missing {
+  my ($self, $pms, $name) = @_;
+
+  my $result = sub {
+    defined $pms->{dmarc_result} &&
+      $pms->{dmarc_policy} eq 'no policy available';
+  };
+
+  return $self->_check_eval($pms, $result);
+}
+
+sub check_tick {
+  my ($self, $opts) = @_;
+
+  $self->_check_async_queue($opts->{permsgstatus});
+}
+
+sub check_cleanup {
+  my ($self, $opts) = @_;
+
+  # Finish it whether SPF and DKIM is ready or not
+  $self->_check_async_queue($opts->{permsgstatus}, 1);
+}
+
+sub _check_async_queue {
+  my ($self, $pms, $finish) = @_;
+
+  return unless exists $pms->{dmarc_async_queue};
+
+  # Check if SPF or DKIM is ready
+  if ($finish || ($pms->{spf_checked} && $pms->{dkim_checked_signature})) {
+    $self->_check_dmarc($pms);
+    $_->() foreach (@{$pms->{dmarc_async_queue}});
+    # No more async queueing needed.  If any evals are called later, they
+    # will act on the results directly.
+    delete $pms->{dmarc_async_queue};
+  }
+}
+
+sub _check_dmarc {
+  my ($self, $pms, $name) = @_;
+
+  return unless $pms->is_dns_available();
+
+  # Load DMARC module
+  if (!exists $self->{has_mail_dmarc}) {
+    my $eval_stat;
+    eval {
+      require Mail::DMARC::PurePerl;
+    } or do {
+      $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
+    };
+    if (!defined($eval_stat)) {
+      dbg("using Mail::DMARC::PurePerl for DMARC checks");
+      $self->{has_mail_dmarc} = 1;
+    } else {
+      dbg("cannot load Mail::DMARC::PurePerl: module: $eval_stat");
+      dbg("Mail::DMARC::PurePerl is required for DMARC checks, DMARC checks disabled");
+      $self->{has_mail_dmarc} = undef;
+    }
+  }
+
+  return if !$self->{has_mail_dmarc};
+  return if $pms->{dmarc_checked};
+  $pms->{dmarc_checked} = 1;
+
+  my $lasthop = $pms->{relays_external}->[0];
+  if (!defined $lasthop) {
+    dbg("no external relay found, skipping DMARC check");
+    return;
+  }
+
+  my $from_addr = ($pms->get('From:first:addr'))[0];
+  return if not defined $from_addr;
+  return if index($from_addr, '@') == -1;
+
+  my $mfrom_domain = ($pms->get('EnvelopeFrom:first:addr:host'))[0];
+  if (!defined $mfrom_domain) {
+    $mfrom_domain = ($pms->get('From:first:addr:domain'))[0];
+    return if !defined $mfrom_domain;
+    dbg("EnvelopeFrom header not found, using From");
+  }
+
+  my $spf_status = 'none';
+  if ($pms->{spf_pass})         { $spf_status = 'pass'; }
+  elsif ($pms->{spf_fail})      { $spf_status = 'fail'; }
+  elsif ($pms->{spf_permerror}) { $spf_status = 'fail'; }
+  elsif ($pms->{spf_none})      { $spf_status = 'fail'; }
+  elsif ($pms->{spf_neutral})   { $spf_status = 'neutral'; }
+  elsif ($pms->{spf_softfail})  { $spf_status = 'softfail'; }
+
+  my $spf_helo_status = 'none';
+  if ($pms->{spf_helo_pass})         { $spf_helo_status = 'pass'; }
+  elsif ($pms->{spf_helo_fail})      { $spf_helo_status = 'fail'; }
+  elsif ($pms->{spf_helo_permerror}) { $spf_helo_status = 'fail'; }
+  elsif ($pms->{spf_helo_none})      { $spf_helo_status = 'fail'; }
+  elsif ($pms->{spf_helo_neutral})   { $spf_helo_status = 'neutral'; }
+  elsif ($pms->{spf_helo_softfail})  { $spf_helo_status = 'softfail'; }
+
+  my $dmarc = Mail::DMARC::PurePerl->new();
+  $dmarc->source_ip($lasthop->{ip});
+  $dmarc->header_from_raw($from_addr);
+
+  my $suppl_attrib = $pms->{msg}->{suppl_attrib};
+  if (defined $suppl_attrib && exists $suppl_attrib->{dkim_signatures}) {
+    my $dkim_signatures = $suppl_attrib->{dkim_signatures};
+    foreach my $signature ( @$dkim_signatures ) {
+      $dmarc->dkim( domain => $signature->domain, result => $signature->result );
+      dbg("DKIM result for domain " . $signature->domain . ": " . $signature->result);
+    }
+  } else {
+    $dmarc->dkim($pms->{dkim_verifier}) if (ref($pms->{dkim_verifier}));
+  }
+
+  my $result;
+  eval {
+    $dmarc->spf([
+      {
+        scope  => 'mfrom',
+        domain => $mfrom_domain,
+        result => $spf_status,
+      },
+      {
+        scope  => 'helo',
+        domain => $lasthop->{lc_helo},
+        result => $spf_helo_status,
+      },
+    ]);
+    $result = $dmarc->validate();
+  };
+  if ($@) {
+    dbg("error while evaluating domain $mfrom_domain: $@");
+    return;
+  }
+
+  if (defined($pms->{dmarc_result} = $result->result)) {
+    if ($pms->{conf}->{dmarc_save_reports}) {
+      my $rua = eval { $result->published()->rua(); };
+      if (defined $rua && index($rua, 'mailto:') >= 0) {
+        eval { $dmarc->save_aggregate(); };
+        if ($@) {
+          info("report could not be saved: $@");
+        } else {
+          dbg("report will be sent to $rua");
+        }
+      }
+    }
+
+    if (defined $result->reason->[0]{comment} &&
+          $result->reason->[0]{comment} eq 'too many policies') {
+      dbg("result: no policy available (too many policies)");
+      $pms->{dmarc_policy} = 'no policy available';
+    } elsif ($result->result eq 'pass') {
+      dbg("result: pass");
+      $pms->{dmarc_policy} = $result->published->p;
+    } elsif ($result->result ne 'none') {
+      dbg("result: $result->{result}, disposition: $result->{disposition}, dkim: $result->{dkim}, spf: $result->{spf} (spf: $spf_status, spf_helo: $spf_helo_status)");
+      $pms->{dmarc_policy} = $result->disposition;
+    } else {
+      dbg("result: no policy available");
+      $pms->{dmarc_policy} = 'no policy available';
+    }
+  }
+}
+
+1;
+
index 600d7b9b1fd1cfd0659b5821533e165121efa6aa..56f31307ecbe9b4426ea419220842899fa42d083 100644 (file)
@@ -45,7 +45,7 @@ package Mail::SpamAssassin::Plugin::DNSEval;
 use Mail::SpamAssassin::Plugin;
 use Mail::SpamAssassin::Logger;
 use Mail::SpamAssassin::Constants qw(:ip);
-use Mail::SpamAssassin::Util qw(reverse_ip_address is_fqdn_valid);
+use Mail::SpamAssassin::Util qw(reverse_ip_address idn_to_ascii compile_regexp is_fqdn_valid);
 
 use strict;
 use warnings;
@@ -55,7 +55,6 @@ use re 'taint';
 our @ISA = qw(Mail::SpamAssassin::Plugin);
 
 my $IP_ADDRESS = IP_ADDRESS;
-my $IP_PRIVATE = IP_PRIVATE;
 
 # constructor: register the eval rule
 sub new {
@@ -75,7 +74,6 @@ sub new {
     'check_rbl_ns_from',
     'check_rbl_txt',
     'check_rbl_sub',
-    'check_rbl_results_for',
     'check_rbl_from_host',
     'check_rbl_from_domain',
     'check_rbl_envfrom',
@@ -86,7 +84,7 @@ sub new {
 
   $self->set_config($mailsaobject->{conf});
   foreach(@{$self->{'evalrules'}}) {
-    $self->register_eval_rule($_);
+    $self->register_eval_rule($_, $Mail::SpamAssassin::Conf::TYPE_RBL_EVALS);
   }
 
   return $self;
@@ -154,21 +152,104 @@ sub set_config {
 # directly as part of PMS
 sub check_start {
   my ($self, $opts) = @_;
+  my $pms = $opts->{permsgstatus};
 
   foreach(@{$self->{'evalrules'}}) {
-    $opts->{'permsgstatus'}->register_plugin_eval_glue($_);
+    $pms->register_plugin_eval_glue($_);
   }
+
+  # Initialize check_rbl_sub tests
+  $self->_init_rbl_subs($pms);
+}
+
+sub _init_rbl_subs {
+  my ($self, $pms) = @_;
+  my $conf = $pms->{conf};
+
+  # Very hacky stuff and direct rbl_evals usage for now, TODO rewrite everything
+  foreach my $rule (@{$conf->{eval_to_rule}->{check_rbl_sub}||[]}) {
+    next if !exists $conf->{rbl_evals}->{$rule};
+    next if !$conf->{scores}->{$rule};
+    # rbl_evals is [$function,[@args]]
+    my $args = $conf->{rbl_evals}->{$rule}->[1];
+    my ($set, $subtest) = @$args;
+    if (!defined $subtest) {
+      warn("dnseval: missing subtest for rule $rule\n");
+      next;
+    }
+    if ($subtest =~ /^sb:/) {
+      warn("dnseval: ignored $rule, SenderBase rules are deprecated\n");
+      next;
+    }
+    # Compile as regex if not pure ip/bitmask (same check in process_dnsbl_result)
+    if ($subtest !~ /^\d+(?:\.\d+\.\d+\.\d+)?$/) {
+      my ($rec, $err) = compile_regexp($subtest, 0);
+      if (!$rec) {
+        warn("dnseval: invalid rule $rule subtest regexp '$subtest': $err\n");
+        next;
+      }
+      $subtest = $rec;
+    }
+    dbg("dnseval: initialize check_rbl_sub for rule $rule, set $set, subtest $subtest");
+    push @{$pms->{rbl_subs}{$set}}, [$subtest, $rule];
+  }
+}
+
+sub parsed_metadata {
+  my ($self, $opts) = @_;
+
+  my $pms = $opts->{permsgstatus};
+
+  return 1 if $self->{main}->{conf}->{skip_rbl_checks};
+  return 1 if !$pms->is_dns_available();
+
+  # Process relaylists only once, not everytime in check_rbl_backend
+  #
+  # ok, make a list of all the IPs in the untrusted set
+  my @fullips = map { $_->{ip} } @{$pms->{relays_untrusted}};
+  # now, make a list of all the IPs in the external set, for use in
+  # notfirsthop testing.  This will often be more IPs than found
+  # in @fullips.  It includes the IPs that are trusted, but
+  # not in internal_networks.
+  my @fullexternal = map {
+       (!$_->{internal}) ? ($_->{ip}) : ()
+      } @{$pms->{relays_trusted}};
+  push @fullexternal, @fullips; # add untrusted set too
+  # Make sure a header significantly improves results before adding here
+  # X-Sender-Ip: could be worth using (very low occurence for me)
+  # X-Sender: has a very low bang-for-buck for me
+  my @originating;
+  foreach my $header (@{$pms->{conf}->{originating_ip_headers}}) {
+    my $str = $pms->get($header, undef);
+    next unless defined $str && $str ne '';
+    push @originating, ($str =~ m/($IP_ADDRESS)/g);
+  }
+  # Let's go ahead and trim away all private ips (KLC)
+  # also uniq the list and strip dups. (jm)
+  my @ips = $self->ip_list_uniq_and_strip_private(@fullips);
+  # if there's no untrusted IPs, it means we trust all the open-internet
+  # relays, so we skip checks
+  if (scalar @ips + scalar @originating > 0) {
+    dbg("dnseval: IPs found: full-external: ".join(", ", @fullexternal).
+      " untrusted: ".join(", ", @ips).
+      " originating: ".join(", ", @originating));
+    @{$pms->{dnseval_fullexternal}} = @fullexternal;
+    @{$pms->{dnseval_ips}} = @ips;
+    @{$pms->{dnseval_originating}} = @originating;
+  }
+
+  return 1;
 }
 
 sub ip_list_uniq_and_strip_private {
   my ($self, @origips) = @_;
   my @ips;
   my %seen;
-  my $IP_PRIVATE = IP_PRIVATE;
   foreach my $ip (@origips) {
     next unless $ip;
-    next if (exists ($seen{$ip})); $seen{$ip} = 1;
-    next if ($ip =~ /$IP_PRIVATE/o);
+    next if exists $seen{$ip};
+    $seen{$ip} = 1;
+    next if $ip =~ IS_IP_PRIVATE;
     push(@ips, $ip);
   }
   return @ips;
@@ -181,12 +262,17 @@ sub ip_list_uniq_and_strip_private {
 sub check_rbl_accreditor {
   my ($self, $pms, $rule, $set, $rbl_server, $subtest, $accreditor) = @_;
 
+  return 0 if $self->{main}->{conf}->{skip_rbl_checks};
+  return 0 if !$pms->is_dns_available();
+
   if (!defined $pms->{accreditor_tag}) {
     $self->message_accreditor_tag($pms);
   }
   if ($pms->{accreditor_tag}->{$accreditor}) {
-    $self->check_rbl_backend($pms, $rule, $set, $rbl_server, 'A', $subtest);
+    # return undef for async status
+    return $self->_check_rbl_backend($pms, $rule, $set, $rbl_server, 'A', $subtest);
   }
+
   return 0;
 }
 
@@ -226,58 +312,16 @@ sub message_accreditor_tag {
   $pms->{accreditor_tag} = \%acctags;
 }
 
-sub check_rbl_backend {
+sub _check_rbl_backend {
   my ($self, $pms, $rule, $set, $rbl_server, $type, $subtest) = @_;
-  local ($_);
-
-  # First check that DNS is available, if not do not perform this check
-  return 0 if $self->{main}->{conf}->{skip_rbl_checks};
-  return 0 unless $pms->is_dns_available();
-
-  if (($rbl_server !~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) &&
-      (index($rbl_server, '.') >= 0) &&
-      ($rbl_server !~ /\.$/)) {
-    $rbl_server .= ".";
-  }
-
-  dbg("dns: checking RBL $rbl_server, set $set");
-
-  # ok, make a list of all the IPs in the untrusted set
-  my @fullips = map { $_->{ip} } @{$pms->{relays_untrusted}};
-
-  # now, make a list of all the IPs in the external set, for use in
-  # notfirsthop testing.  This will often be more IPs than found
-  # in @fullips.  It includes the IPs that are trusted, but
-  # not in internal_networks.
-  my @fullexternal = map {
-       (!$_->{internal}) ? ($_->{ip}) : ()
-      } @{$pms->{relays_trusted}};
-  push (@fullexternal, @fullips);      # add untrusted set too
-
-  # Make sure a header significantly improves results before adding here
-  # X-Sender-Ip: could be worth using (very low occurence for me)
-  # X-Sender: has a very low bang-for-buck for me
-  my $IP_ADDRESS = IP_ADDRESS;
-  my @originating;
-  for my $header (@{$pms->{conf}->{originating_ip_headers}}) {
-    my $str = $pms->get($header,undef);
-    next unless defined $str && $str ne '';
-    push (@originating, ($str =~ m/($IP_ADDRESS)/g));
-  }
 
-  # Let's go ahead and trim away all private ips (KLC)
-  # also uniq the list and strip dups. (jm)
-  my @ips = $self->ip_list_uniq_and_strip_private(@fullips);
+  return if !exists $pms->{dnseval_ips}; # no untrusted ips
 
-  # if there's no untrusted IPs, it means we trust all the open-internet
-  # relays, so we can return right now.
-  return 0 unless (scalar @ips + scalar @originating > 0);
-
-  dbg("dns: IPs found: full-external: ".join(", ", @fullexternal).
-       " untrusted: ".join(", ", @ips).
-       " originating: ".join(", ", @originating));
+  $rbl_server =~ s/\.+\z//; # strip unneeded trailing dot
+  dbg("dnseval: checking RBL $rbl_server, set $set, rule $rule");
 
   my $trusted = $self->{main}->{conf}->{trusted_networks};
+  my @ips = @{$pms->{dnseval_ips}};
 
   # If name is foo-notfirsthop, check all addresses except for
   # the originating one.  Suitable for use with dialup lists, like the PDL.
@@ -293,9 +337,9 @@ sub check_rbl_backend {
     # specified some third-party relays as trusted.  Also, don't use
     # @originating; those headers are added by a phase of relaying through
     # a server like Hotmail, which is not going to be in dialup lists anyway.
-    @ips = $self->ip_list_uniq_and_strip_private(@fullexternal);
+    @ips = $self->ip_list_uniq_and_strip_private(@{$pms->{dnseval_fullexternal}});
     if ($1 eq "lastexternal") {
-      @ips = (defined $ips[0]) ? ($ips[0]) : ();
+      @ips = defined $ips[0] ? ($ips[0]) : ();
     } else {
        pop @ips if (scalar @ips > 1);
     }
@@ -307,14 +351,14 @@ sub check_rbl_backend {
   elsif ($set =~ /-(first|un)trusted$/)
   {
     my @tips;
-    foreach my $ip (@originating) {
+    foreach my $ip (@{$pms->{dnseval_originating}}) {
       if ($ip && !$trusted->contains_ip($ip)) {
         push(@tips, $ip);
       }
     }
-    @ips = $self->ip_list_uniq_and_strip_private (@ips, @tips);
+    @ips = $self->ip_list_uniq_and_strip_private(@ips, @tips);
     if ($1 eq "first") {
-      @ips = (defined $ips[0]) ? ($ips[0]) : ();
+      @ips = defined $ips[0] ? ($ips[0]) : ();
     } else {
       shift @ips;
     }
@@ -322,7 +366,7 @@ sub check_rbl_backend {
   else
   {
     my @tips;
-    foreach my $ip (@originating) {
+    foreach my $ip (@{$pms->{dnseval_originating}}) {
       if ($ip && !$trusted->contains_ip($ip)) {
         push(@tips, $ip);
       }
@@ -333,71 +377,77 @@ sub check_rbl_backend {
   }
 
   # How many IPs max you check in the received lines
-  my $checklast=$self->{main}->{conf}->{num_check_received};
+  my $checklast = $self->{main}->{conf}->{num_check_received};
 
   if (scalar @ips > $checklast) {
     splice (@ips, $checklast); # remove all others
   }
 
-  my $tflags = $pms->{conf}->{tflags}->{$rule};
-
   # Trusted relays should only be checked against nice rules (dnswls)
-  if (defined $tflags && $tflags !~ /\bnice\b/) {
+  if (($pms->{conf}->{tflags}->{$rule}||'') !~ /\bnice\b/) {
     # remove trusted hosts from beginning
     while (@ips && $trusted->contains_ip($ips[0])) { shift @ips }
   }
 
   unless (scalar @ips > 0) {
-    dbg("dns: no untrusted IPs to check");
+    dbg("dnseval: no untrusted IPs to check");
     return 0;
   }
 
-  dbg("dns: only inspecting the following IPs: ".join(", ", @ips));
+  dbg("dnseval: only inspecting the following IPs: ".join(", ", @ips));
 
-  eval {
-    foreach my $ip (@ips) {
-      my $revip = reverse_ip_address($ip);
-      $pms->do_rbl_lookup($rule, $set, $type,
-                          $revip.'.'.$rbl_server, $subtest) if defined $revip;
+  my $queries;
+  foreach my $ip (@ips) {
+    if (defined(my $revip = reverse_ip_address($ip))) {
+      my $ret = $pms->do_rbl_lookup($rule, $set, $type, $revip.'.'.$rbl_server, $subtest);
+      $queries++ if defined $ret;
     }
-  };
+  }
 
   # note that results are not handled here, hits are handled directly
   # as DNS responses are harvested
-  return 0;
+  return 0 if !$queries; # no query started
+  return; # return undef for async status
 }
 
 sub check_rbl {
   my ($self, $pms, $rule, $set, $rbl_server, $subtest) = @_;
-  $self->check_rbl_backend($pms, $rule, $set, $rbl_server, 'A', $subtest);
+
+  return 0 if $self->{main}->{conf}->{skip_rbl_checks};
+  return 0 if !$pms->is_dns_available();
+
+  # return undef for async status
+  return $self->_check_rbl_backend($pms, $rule, $set, $rbl_server, 'A', $subtest);
 }
 
 sub check_rbl_txt {
   my ($self, $pms, $rule, $set, $rbl_server, $subtest) = @_;
-  $self->check_rbl_backend($pms, $rule, $set, $rbl_server, 'TXT', $subtest);
-}
-
-# run for first message 
-sub check_rbl_sub {
-  my ($self, $pms, $rule, $set, $subtest) = @_;
 
   return 0 if $self->{main}->{conf}->{skip_rbl_checks};
-  return 0 unless $pms->is_dns_available();
+  return 0 if !$pms->is_dns_available();
 
-  $pms->register_rbl_subtest($rule, $set, $subtest);
+  # return undef for async status
+  return $self->_check_rbl_backend($pms, $rule, $set, $rbl_server, 'TXT', $subtest);
 }
 
-# backward compatibility
-sub check_rbl_results_for {
-  #warn "dns: check_rbl_results_for() is deprecated, use check_rbl_sub()\n";
-  check_rbl_sub(@_);
+sub check_rbl_sub {
+  my ($self, $pms, $rule, $set, $subtest) = @_;
+  # just a dummy, _init_rbl_subs/do_rbl_lookup handles the subs
+
+  return; # return undef for async status
 }
 
 # this only checks the address host name and not the domain name because
 # using the domain name had much worse results for dsn.rfc-ignorant.org
 sub check_rbl_from_host {
   my ($self, $pms, $rule, $set, $rbl_server, $subtest) = @_; 
-  _check_rbl_addresses($self, $pms, $rule, $set, $rbl_server, $subtest, $pms->all_from_addrs());
+
+  return 0 if $self->{main}->{conf}->{skip_rbl_checks};
+  return 0 if !$pms->is_dns_available();
+
+  # return undef for async status
+  return $self->_check_rbl_addresses($pms, $rule, $set, $rbl_server,
+                                     $subtest, $pms->all_from_addrs());
 }
 
 sub check_rbl_headers {
@@ -415,16 +465,18 @@ sub check_rbl_headers {
     @env_hdr = split(/,/, $conf->{rbl_headers});
   }
 
+  my $queries;
   foreach my $rbl_headers (@env_hdr) {
     my $addr = $pms->get($rbl_headers.':addr', undef);
     if ( defined $addr && $addr =~ /\@([^\@\s]+)/ ) {
-      $self->_check_rbl_addresses($pms, $rule, $set, $rbl_server,
-        $subtest, $addr);
+      my $ret = $self->_check_rbl_addresses($pms, $rule, $set, $rbl_server,
+                                            $subtest, $addr);
+      $queries++ if defined $ret;
     } else {
       my $unsplitted_host = $pms->get($rbl_headers);
       chomp($unsplitted_host);
       foreach my $host (split(/\n/, $unsplitted_host)) {
-        if($host =~ /^$IP_ADDRESS$/ ) {
+        if ($host =~ IS_IP_ADDRESS) {
           next if ($conf->{tflags}->{$rule}||'') =~ /\bdomains_only\b/;
           $host = reverse_ip_address($host);
         } else {
@@ -432,17 +484,23 @@ sub check_rbl_headers {
           next unless is_fqdn_valid($host);
           next unless $pms->{main}->{registryboundaries}->is_domain_valid($host);
         }
-        $pms->do_rbl_lookup($rule, $set, 'A', "$host.$rbl_server", $subtest);
+        my $ret = $pms->do_rbl_lookup($rule, $set, 'A', "$host.$rbl_server", $subtest);
+        $queries++ if defined $ret;
       }
     }
   }
+
+  return 0 if !$queries; # no query started
+  return; # return undef for async status
 }
 
 =over 4
 
 =item check_rbl_from_domain
 
-This checks all the from addrs domain names as an alternate to check_rbl_from_host.  As of v3.4.1, it has been improved to include a subtest for a specific octet.
+This checks all the from addrs domain names as an alternate to
+check_rbl_from_host.  As of v3.4.1, it has been improved to include a
+subtest for a specific octet.
 
 =back
 
@@ -450,9 +508,14 @@ This checks all the from addrs domain names as an alternate to check_rbl_from_ho
 
 sub check_rbl_from_domain {
   my ($self, $pms, $rule, $set, $rbl_server, $subtest) = @_;
-  _check_rbl_addresses($self, $pms, $rule, $set, $rbl_server, $subtest, $pms->all_from_addrs_domains());
-}
 
+  return 0 if $self->{main}->{conf}->{skip_rbl_checks};
+  return 0 if !$pms->is_dns_available();
+
+  # return undef for async status
+  return $self->_check_rbl_addresses($pms, $rule, $set, $rbl_server,
+                                     $subtest, $pms->all_from_addrs_domains());
+}
 =over 4
 
 =item check_rbl_ns_from
@@ -472,6 +535,7 @@ sub check_rbl_ns_from {
   return 0 if $self->{main}->{conf}->{skip_rbl_checks};
   return 0 unless $pms->is_dns_available();
 
+  dbg("dnseval: EnvelopeFrom header not found") unless defined (($pms->get("EnvelopeFrom:addr"))[0]);
   for my $from ($pms->get('EnvelopeFrom:addr')) {
     next unless defined $from;
     $from =~ tr/././s;          # bug 3366
@@ -482,20 +546,20 @@ sub check_rbl_ns_from {
   }
   return 0 unless defined $domain;
 
-  dbg("dns: checking NS for host $domain");
+  dbg("dnseval: checking NS for host $domain");
 
-  my $key = "NS:" . $domain;
   my $obj = { dom => $domain, rule => $rule, set => $set, rbl_server => $rbl_server, subtest => $subtest };
   my $ent = {
-    rulename => $rule, key => $key, zone => $domain, obj => $obj, type => "URI-NS",
+    rulename => $rule, zone => $domain, obj => $obj, type => "URI-NS",
   };
   # dig $dom ns
-  $ent = $pms->{async}->bgsend_and_start_lookup(
+  my $ret = $pms->{async}->bgsend_and_start_lookup(
     $domain, 'NS', undef, $ent,
     sub { my ($ent2,$pkt) = @_;
           $self->complete_ns_lookup($pms, $ent2, $pkt, $domain) },
     master_deadline => $pms->{master_deadline} );
-  return $ent;
+  return 0 if !defined $ret; # no query started
+  return; # return undef for async status
 }
 
 sub complete_ns_lookup {
@@ -508,11 +572,11 @@ sub complete_ns_lookup {
 
   if (!$pkt) {
     # $pkt will be undef if the DNS query was aborted (e.g. timed out)
-    dbg("DNSEval: complete_ns_lookup aborted %s", $ent->{key});
+    dbg("dnseval: complete_ns_lookup aborted %s", $ent->{key});
     return;
   }
 
-  dbg("DNSEval: complete_ns_lookup %s", $ent->{key});
+  dbg("dnseval: complete_ns_lookup %s", $ent->{key});
   my @ns = $pkt->authority;
 
   foreach my $rr (@ns) {
@@ -521,9 +585,9 @@ sub complete_ns_lookup {
     chomp($nshost);
     if (is_fqdn_valid($nshost)) {
       if ( defined $subtest ) {
-        dbg("dns: checking [$nshost] / $rule / $set / $rbl_server / $subtest");
+        dbg("dnseval: checking [$nshost] / $rule / $set / $rbl_server / $subtest");
       } else {
-        dbg("dns: checking [$nshost] / $rule / $set / $rbl_server");
+        dbg("dnseval: checking [$nshost] / $rule / $set / $rbl_server");
       }
       $pms->do_rbl_lookup($rule, $set, 'A',
         "$nshost.$rbl_server", $subtest);
@@ -548,7 +612,7 @@ sub check_rbl_rcvd {
   my @udnsrcvd = ();
 
   return 0 if $self->{main}->{conf}->{skip_rbl_checks};
-  return 0 if !$pms->is_dns_available();  
+  return 0 if !$pms->is_dns_available();
 
   my $rcvd = $pms->{relays_untrusted}->[$pms->{num_relays_untrusted} - 1];
   my @dnsrcvd = ( $rcvd->{ip}, $rcvd->{by}, $rcvd->{helo}, $rcvd->{rdns} );
@@ -559,10 +623,11 @@ sub check_rbl_rcvd {
     }
   }
 
+  my $queries;
   foreach my $host ( @udnsrcvd ) {
     if((defined $host) and ($host ne "")) {
       chomp($host);
-      if($host =~ /^$IP_ADDRESS$/ ) {
+      if ($host =~ IS_IP_ADDRESS) {
         next if ($pms->{conf}->{tflags}->{$rule}||'') =~ /\bdomains_only\b/;
         $host = reverse_ip_address($host);
       } else {
@@ -572,49 +637,55 @@ sub check_rbl_rcvd {
         next unless $pms->{main}->{registryboundaries}->is_domain_valid($host);
       }
       if ( defined $subtest ) {
-        dbg("dns: checking [$host] / $rule / $set / $rbl_server / $subtest");
+        dbg("dnseval: checking [$host] / $rule / $set / $rbl_server / $subtest");
       } else {
-        dbg("dns: checking [$host] / $rule / $set / $rbl_server");
+        dbg("dnseval: checking [$host] / $rule / $set / $rbl_server");
       }
-      $pms->do_rbl_lookup($rule, $set, 'A', "$host.$rbl_server", $subtest);
+      my $ret = $pms->do_rbl_lookup($rule, $set, 'A', "$host.$rbl_server", $subtest);
+      $queries++ if defined $ret;
     }
   }
-  return 0;
+
+  return 0 if !$queries; # no query started
+  return; # return undef for async status
 }
 
 # this only checks the address host name and not the domain name because
 # using the domain name had much worse results for dsn.rfc-ignorant.org
 sub check_rbl_envfrom {
   my ($self, $pms, $rule, $set, $rbl_server, $subtest) = @_; 
-  _check_rbl_addresses($self, $pms, $rule, $set, $rbl_server, $subtest, $pms->get('EnvelopeFrom:addr',undef));
+
+  return 0 if $self->{main}->{conf}->{skip_rbl_checks};
+  return 0 if !$pms->is_dns_available();
+
+  # return undef for async status
+  return $self->_check_rbl_addresses($pms, $rule, $set, $rbl_server,
+                                 $subtest, $pms->get('EnvelopeFrom:addr',undef));
 }
 
 sub _check_rbl_addresses {
   my ($self, $pms, $rule, $set, $rbl_server, $subtest, @addresses) = @_;
   
-  return 0 if $self->{main}->{conf}->{skip_rbl_checks};
-  return 0 unless $pms->is_dns_available();
+  $rbl_server =~ s/\.+\z//; # strip unneeded trailing dot
 
   my %hosts;
   for (@addresses) {
-    next if !defined($_) || !/ \@ ( [^\@\s]+ )/x;
+    next if !defined($_) || !/\@([^\@\s]+)/;
     my $address = $1;
     # strip leading & trailing dots (as seen in some e-mail addresses)
-    $address =~ s/^\.+//; $address =~ s/\.+\z//;
+    $address =~ s/^\.+//;
+    $address =~ s/\.+\z//;
     # squash duplicate dots to avoid an invalid DNS query with a null label
-    $address =~ tr/.//s;
-    $hosts{lc($address)} = 1  if $address =~ /\./;  # must by a FQDN
+    # Also checks it's FQDN
+    if ($address =~ tr/.//s) {
+      $hosts{lc($address)} = 1;
+    }
   }
   return unless scalar keys %hosts;
 
-  if (($rbl_server !~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) &&
-      (index($rbl_server, '.') >= 0) &&
-      ($rbl_server !~ /\.$/)) {
-    $rbl_server .= ".";
-  }
-
+  my $queries;
   for my $host (keys %hosts) {
-    if ($host =~ /^$IP_ADDRESS$/) {
+    if ($host =~ IS_IP_ADDRESS) {
       next if ($pms->{conf}->{tflags}->{$rule}||'') =~ /\bdomains_only\b/;
       $host = reverse_ip_address($host);
     } else {
@@ -622,23 +693,26 @@ sub _check_rbl_addresses {
       next unless is_fqdn_valid($host);
       next unless $pms->{main}->{registryboundaries}->is_domain_valid($host);
     }
-    dbg("dns: checking [$host] / $rule / $set / $rbl_server");
-    $pms->do_rbl_lookup($rule, $set, 'A', "$host.$rbl_server", $subtest);
+    dbg("dnseval: checking [$host] / $rule / $set / $rbl_server");
+    my $ret = $pms->do_rbl_lookup($rule, $set, 'A', "$host.$rbl_server", $subtest);
+    $queries++ if defined $ret;
   }
+
+  return 0 if !$queries; # no async
+  return; # return undef for async status
 }
 
 sub check_dns_sender {
   my ($self, $pms, $rule) = @_;
 
   return 0 if $self->{main}->{conf}->{skip_rbl_checks};
-  return 0 unless $pms->is_dns_available();
+  return 0 if !$pms->is_dns_available();
 
   my $host;
-  for my $from ($pms->get('EnvelopeFrom:addr',undef)) {
+  foreach my $from ($pms->get('EnvelopeFrom:addr', undef)) {
     next unless defined $from;
-
-    $from =~ tr/././s;         # bug 3366
-    if ($from =~ m/ \@ ( [^\@\s]+ \. [^\@\s]+ )/x ) {
+    $from =~ tr/.//s; # bug 3366
+    if ($from =~ m/\@([^\@\s]+\.[^\@\s]+)/) {
       $host = lc($1);
       last;
     }
@@ -650,15 +724,45 @@ sub check_dns_sender {
     return 0;
   }
 
-  dbg("dns: checking A and MX for host $host");
+  $host = idn_to_ascii($host);
+  dbg("dnseval: checking A and MX for host $host");
 
-  $pms->do_dns_lookup($rule, 'A', $host);
-  $pms->do_dns_lookup($rule, 'MX', $host);
+  my $queries;
+  my $ret = $self->do_sender_lookup($pms, $rule, 'A', $host);
+  $queries++ if defined $ret;
+  $ret = $self->do_sender_lookup($pms, $rule, 'MX', $host);
+  $queries++ if defined $ret;
 
-  # cache name of host for later checking
-  $pms->{sender_host} = $host;
+  return 0 if !$queries; # no query started
+  return; # return undef for async status
+}
 
-  return 0;
+sub do_sender_lookup {
+  my ($self, $pms, $rule, $type, $host) = @_;
+
+  my $ent = {
+    rulename => $rule,
+    type => "DNSBL-Sender",
+  };
+  return $pms->{async}->bgsend_and_start_lookup(
+    $host, $type, undef, $ent, sub {
+      my ($ent, $pkt) = @_;
+      return if !$pkt; # aborted / timed out
+      $pms->rule_ready($ent->{rulename}); # mark as run, could still hit
+      foreach my $answer ($pkt->answer) {
+        next if !$answer;
+        next if $answer->type ne 'A' && $answer->type ne 'MX';
+        if ($pkt->header->rcode eq 'NXDOMAIN' ||
+            $pkt->header->rcode eq 'SERVFAIL')
+        {
+          if (++$pms->{sender_host_fail} == 2) {
+            $pms->got_hit($ent->{rulename}, "DNS: ", ruletype => "dns");
+          }
+        }
+      }
+    },
+    master_deadline => $self->{master_deadline},
+  );
 }
 
 # capability checks for "if can(Mail::SpamAssassin::Plugin::DNSEval::XXX)":
diff --git a/upstream/lib/Mail/SpamAssassin/Plugin/DecodeShortURLs.pm b/upstream/lib/Mail/SpamAssassin/Plugin/DecodeShortURLs.pm
new file mode 100644 (file)
index 0000000..8f75c19
--- /dev/null
@@ -0,0 +1,944 @@
+# <@LICENSE>
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to you under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at:
+# 
+#     http://www.apache.org/licenses/LICENSE-2.0
+# 
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# </@LICENSE>
+
+=head1 NAME
+
+DecodeShortURLs - Check for shortened URLs
+
+=head1 SYNOPSIS
+
+  loadplugin    Mail::SpamAssassin::Plugin::DecodeShortURLs
+
+  url_shortener tinyurl.com
+  url_shortener_get bit.ly
+
+  body HAS_SHORT_URL          eval:short_url()
+  describe HAS_SHORT_URL      Message has one or more shortened URLs
+
+  body SHORT_URL_REDIR        eval:short_url_redir()
+  describe SHORT_URL_REDIR    Message has shortened URL that resulted in a valid redirection
+
+  body SHORT_URL_CHAINED      eval:short_url_chained()
+  describe SHORT_URL_CHAINED  Message has shortened URL chained to other shorteners
+
+  body SHORT_URL_MAXCHAIN     eval:short_url_maxchain()
+  describe SHORT_URL_MAXCHAIN Message has shortened URL that causes too many redirections
+
+  body SHORT_URL_LOOP         eval:short_url_loop()
+  describe SHORT_URL_LOOP     Message has short URL that loops back to itself
+
+  body SHORT_URL_200          eval:short_url_code('200') # Can check any non-redirect HTTP code
+  describe SHORT_URL_200      Message has shortened URL returning HTTP 200
+
+  body SHORT_URL_404          eval:short_url_code('404') # Can check any non-redirect HTTP code
+  describe SHORT_URL_404      Message has shortened URL returning HTTP 404
+
+  uri URI_TINYURL_BLOCKED      m,https://tinyurl\.com/app/nospam,
+  describe URI_TINYURL_BLOCKED Message contains a tinyurl that has been disabled due to abuse
+
+  uri URI_BITLY_BLOCKED       m,^https://bitly\.com/a/blocked,
+  describe URI_BITLY_BLOCKED  Message contains a bit.ly URL that has been disabled due to abuse
+
+=head1 DESCRIPTION
+
+This plugin looks for URLs shortened by a list of URL shortening services. 
+Upon finding a matching URL, plugin will send a HTTP request to the
+shortening service and retrieve the Location-header which points to the
+actual shortened URL.  It then adds this URL to the list of URIs extracted
+by SpamAssassin which can then be accessed by uri rules and plugins such as
+URIDNSBL.
+
+This plugin will follow chained redirections, where a short URL redirects to
+another short URL.  Redirection depth limit can be set with
+C<max_short_url_redirections>.
+
+Maximum of C<max_short_urls> short URLs are checked in a message (10 by
+default).  Setting it to 0 disables HTTP requests, allowing only short_url()
+test to work and report found shorteners.
+
+All supported rule types for checking short URLs and redirection status are
+documented in L<SYNOPSIS> section.
+
+=head1 NOTES
+
+This plugin runs at the check_dnsbl hook (priority -100) so that it may
+modify the parsed URI list prior to normal uri rules or the URIDNSBL plugin.
+
+=cut
+
+package Mail::SpamAssassin::Plugin::DecodeShortURLs;
+
+use Mail::SpamAssassin::Plugin;
+use strict;
+use warnings;
+
+use vars qw(@ISA);
+@ISA = qw(Mail::SpamAssassin::Plugin);
+
+my $VERSION = 4.00;
+
+use constant HAS_LWP_USERAGENT => eval { require LWP::UserAgent; };
+
+sub dbg { my $msg = shift; return Mail::SpamAssassin::Logger::dbg("DecodeShortURLs: $msg", @_); }
+sub info { my $msg = shift; return Mail::SpamAssassin::Logger::info("DecodeShortURLs: $msg", @_); }
+
+sub new {
+  my $class = shift;
+  my $mailsaobject = shift;
+
+  $class = ref($class) || $class;
+  my $self = $class->SUPER::new($mailsaobject);
+  bless ($self, $class);
+
+  if ($mailsaobject->{local_tests_only}) {
+    dbg("local tests only, disabling HTTP requests");
+    $self->{net_disabled} = 1;
+  }
+  elsif (!HAS_LWP_USERAGENT) {
+    dbg("module LWP::UserAgent not installed, disabling HTTP requests");
+    $self->{net_disabled} = 1;
+  }
+
+  $self->set_config($mailsaobject->{conf});
+  $self->register_method_priority ('check_dnsbl', -10);
+  $self->register_eval_rule('short_url', $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule('short_url_redir', $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule('short_url_200', $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule('short_url_404', $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule('short_url_code', $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule('short_url_chained', $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule('short_url_maxchain', $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule('short_url_loop', $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule('short_url_tests'); # for legacy plugin compatibility warning
+
+  return $self;
+}
+
+=head1 PRIVILEGED SETTINGS
+
+=over 4
+
+=item url_shortener  domain [domain...]     (default: none)
+
+Domains that should be considered as an URL shortener.  If the domain begins
+with a '.', 3rd level tld of the main domain will be checked.
+
+Example:
+
+ url_shortener tinyurl.com
+ url_shortener .page.link
+
+=back
+
+=over 4
+
+=item url_shortener_get  domain [domain...]     (default: none)
+
+Alias to C<url_shortener>.  HTTP request will be done with GET method,
+instead of default HEAD.  Required for some services like bit.ly to return
+blocked URL correctly.
+
+Example:
+
+ url_shortener_get bit.ly
+
+=back
+
+=cut
+
+sub set_config {
+  my($self, $conf) = @_;
+  my @cmds = ();
+
+  push (@cmds, {
+    setting => 'url_shortener',
+    default => {},
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE,
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      if ($value eq '') {
+        return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
+      }
+      foreach my $domain (split(/\s+/, $value)) {
+        $self->{url_shortener}->{lc $domain} = 1; # 1 == head
+      }
+    }
+  });
+
+  push (@cmds, {
+    setting => 'url_shortener_get',
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      if ($value eq '') {
+        return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
+      }
+      foreach my $domain (split(/\s+/, $value)) {
+        $self->{url_shortener}->{lc $domain} = 2; # 2 == get
+      }
+    }
+  });
+
+=over 4
+
+=item clear_url_shortener  [domain] [domain...]
+
+Clear configured url_shortener and url_shortener_get domains, for example to
+override default settings from an update channel.  If domains are specified,
+then only those are removed from list.
+
+=back
+
+=cut
+
+  push (@cmds, {
+    setting => 'clear_url_shortener',
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      if ($value eq '') {
+        $self->{url_shortener} = {};
+      } else {
+        foreach my $domain (split(/\s+/, $value)) {
+          delete $self->{url_shortener}->{lc $domain};
+        }
+      }
+    }
+  });
+
+=over 4
+
+=item url_shortener_cache_type     (default: none)
+
+The cache type that is being utilized.  Currently only supported value is
+C<dbi> that implies C<url_shortener_cache_dsn> is a DBI connect string.
+DBI module is required.
+
+Example:
+url_shortener_cache_type dbi
+
+=back
+
+=cut
+
+  push (@cmds, {
+    setting => 'url_shortener_cache_type',
+    default => '',
+    is_priv => 1,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING
+  });
+
+=over 4
+
+=item url_shortener_cache_dsn          (default: none)
+
+The DBI dsn of the database to use.
+
+For SQLite, the database will be created automatically if it does not
+already exist, the supplied path and file must be read/writable by the
+user running spamassassin or spamd.
+
+For MySQL/MariaDB or PostgreSQL, see sql-directory for database table
+creation clauses.
+
+You will need to have the proper DBI module for your database.  For example
+DBD::SQLite, DBD::mysql, DBD::MariaDB or DBD::Pg.
+
+Minimum required SQLite version is 3.24.0 (available from DBD::SQLite 1.59_01).
+
+Examples:
+
+ url_shortener_cache_dsn dbi:SQLite:dbname=/var/lib/spamassassin/DecodeShortURLs.db
+
+=back
+
+=cut
+
+  push (@cmds, {
+    setting => 'url_shortener_cache_dsn',
+    default => '',
+    is_priv => 1,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING
+  });
+
+=over 4
+
+=item url_shortener_cache_username  (default: none)
+
+The username that should be used to connect to the database.  Not used for
+SQLite.
+
+=back
+
+=cut
+
+  push (@cmds, {
+    setting => 'url_shortener_cache_username',
+    default => '',
+    is_priv => 1,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING
+  });
+
+=over 4
+
+=item url_shortener_cache_password  (default: none)
+
+The password that should be used to connect to the database.  Not used for
+SQLite.
+
+=back
+
+=cut
+
+  push (@cmds, {
+    setting => 'url_shortener_cache_password',
+    default => '',
+    is_priv => 1,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING
+  });
+
+=over 4
+
+=item url_shortener_cache_ttl          (default: 86400)
+
+The length of time a cache entry will be valid for in seconds.
+Default is 86400 (1 day).
+
+See C<url_shortener_cache_autoclean> for database cleaning.
+
+=back
+
+=cut
+
+  push (@cmds, {
+    setting => 'url_shortener_cache_ttl',
+    is_admin => 1,
+    default => 86400,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
+  });
+
+=over 4
+
+=item url_shortener_cache_autoclean    (default: 1000)
+
+Automatically purge old entries from database.  Value describes a random run
+chance of 1/x.  The default value of 1000 means that cleaning is run
+approximately once for every 1000 messages processed.  Value of 1 would mean
+database is cleaned every time a message is processed.
+
+Set 0 to disable automatic cleaning and to do it manually.
+
+=back
+
+=cut
+
+  push (@cmds, {
+    setting => 'url_shortener_cache_autoclean',
+    is_admin => 1,
+    default => 1000,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
+  });
+
+=over 4
+
+=item url_shortener_loginfo           (default: 0 (off))
+
+If this option is enabled (set to 1), then short URLs and the decoded URLs will be logged with info priority.
+
+=back
+
+=cut
+
+  push (@cmds, {
+    setting => 'url_shortener_loginfo',
+    is_admin => 1,
+    default => 0,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL
+  });
+
+=over 4
+
+=item url_shortener_timeout     (default: 5)
+
+Maximum time a short URL HTTP request can take, in seconds.
+
+=back
+
+=cut
+
+  push (@cmds, {
+    setting => 'url_shortener_timeout',
+    is_admin => 1,
+    default => 5,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
+  });
+
+=over 4
+
+=item max_short_urls                 (default: 10)
+
+Maximum amount of short URLs that will be looked up per message.  Chained
+redirections are not counted, only initial short URLs found.
+
+Setting it to 0 disables HTTP requests, allowing only short_url() test to
+work and report any found shortener URLs.
+
+=back
+
+=cut
+
+  push (@cmds, {
+    setting => 'max_short_urls',
+    is_admin => 1,
+    default => 10,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
+  });
+
+=over 4
+
+=item max_short_url_redirections     (default: 10)
+
+Maximum depth of chained redirections that a short URL can generate.
+
+=back
+
+=cut
+
+  push (@cmds, {
+    setting => 'max_short_url_redirections',
+    is_admin => 1,
+    default => 10,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
+  });
+
+=over 4
+
+=item url_shortener_user_agent       (default: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36)
+
+Set User-Agent header for HTTP requests.  Some services require it to look
+like a common browser.
+
+=back
+
+=cut
+
+  push (@cmds, {
+    setting => 'url_shortener_user_agent',
+    is_admin => 1,
+    default => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36',
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING
+  });
+
+  $conf->{parser}->register_commands(\@cmds);
+}
+
+=head1 ACKNOWLEDGEMENTS
+
+Original DecodeShortURLs plugin was developed by Steve Freegard.
+
+=cut
+
+sub short_url_tests {
+  # Legacy compatibility warning done in finish_parsing_start
+  return 0;
+}
+
+sub finish_parsing_start {
+  my ($self, $opts) = @_;
+
+  if ($opts->{conf}->{eval_to_rule}->{short_url_tests}) {
+    warn "DecodeShortURLs: Legacy configuration format detected. ".
+         "Eval function short_url_tests() is no longer supported, ".
+         "please see documentation for the new rule format.\n";
+  }
+}
+
+sub initialise_url_shortener_cache {
+  my ($self, $conf) = @_;
+
+  return if $self->{dbh};
+  return if !$conf->{url_shortener_cache_type};
+
+  if (!$conf->{url_shortener_cache_dsn}) {
+    warn "DecodeShortURLs: invalid cache configuration\n";
+    return;
+  }
+
+  ##
+  ## SQLite
+  ## 
+  if ($conf->{url_shortener_cache_type} =~ /^(?:dbi|sqlite)$/i
+      && $conf->{url_shortener_cache_dsn} =~ /^dbi:SQLite/)
+  {
+    eval {
+      local $SIG{'__DIE__'};
+      require DBI;
+      require DBD::SQLite;
+      DBD::SQLite->VERSION(1.59_01); # Required for ON CONFLICT
+      $self->{dbh} = DBI->connect_cached(
+        $conf->{url_shortener_cache_dsn}, '', '',
+        {RaiseError => 1, PrintError => 0, InactiveDestroy => 1, AutoCommit => 1}
+      );
+      $self->{dbh}->do("
+        CREATE TABLE IF NOT EXISTS short_url_cache (
+          short_url   TEXT PRIMARY KEY NOT NULL,
+          decoded_url TEXT NOT NULL,
+          hits        INTEGER NOT NULL DEFAULT 1,
+          created     INTEGER NOT NULL,
+          modified    INTEGER NOT NULL
+        )
+      ");
+      # Maintaining index for cleaning is likely more expensive than occasional full table scan
+      #$self->{dbh}->do("
+      #  CREATE INDEX IF NOT EXISTS short_url_modified
+      #    ON short_url_cache(created)
+      #");
+      $self->{sth_insert} = $self->{dbh}->prepare("
+        INSERT INTO short_url_cache (short_url, decoded_url, created, modified)
+        VALUES (?,?,strftime('%s','now'),strftime('%s','now'))
+        ON CONFLICT(short_url) DO UPDATE
+          SET decoded_url = excluded.decoded_url,
+              modified = excluded.modified,
+              hits = hits + 1
+      ");
+      $self->{sth_select} = $self->{dbh}->prepare("
+        SELECT decoded_url FROM short_url_cache
+        WHERE short_url = ?
+      ");
+      $self->{sth_delete} = $self->{dbh}->prepare("
+        DELETE FROM short_url_cache
+        WHERE short_url = ? AND created < strftime('%s','now') - $conf->{url_shortener_cache_ttl}
+      ");
+      $self->{sth_clean} = $self->{dbh}->prepare("
+        DELETE FROM short_url_cache
+        WHERE created < strftime('%s','now') - $conf->{url_shortener_cache_ttl}
+      ");
+    };
+  }
+  ##
+  ## MySQL/MariaDB
+  ## 
+  elsif (lc $conf->{url_shortener_cache_type} eq 'dbi'
+      && $conf->{url_shortener_cache_dsn} =~ /^dbi:(?:mysql|MariaDB)/i)
+  {
+    eval {
+      local $SIG{'__DIE__'};
+      require DBI;
+      $self->{dbh} = DBI->connect_cached(
+        $conf->{url_shortener_cache_dsn},
+        $conf->{url_shortener_cache_username},
+        $conf->{url_shortener_cache_password},
+        {RaiseError => 1, PrintError => 0, InactiveDestroy => 1, AutoCommit => 1}
+      );
+      $self->{sth_insert} = $self->{dbh}->prepare("
+        INSERT INTO short_url_cache (short_url, decoded_url, created, modified)
+        VALUES (?,?,UNIX_TIMESTAMP(),UNIX_TIMESTAMP())
+        ON DUPLICATE KEY UPDATE
+          decoded_url = VALUES(decoded_url),
+          modified = VALUES(modified),
+          hits = hits + 1
+      ");
+      $self->{sth_select} = $self->{dbh}->prepare("
+        SELECT decoded_url FROM short_url_cache
+        WHERE short_url = ?
+      ");
+      $self->{sth_delete} = $self->{dbh}->prepare("
+        DELETE FROM short_url_cache
+        WHERE short_url = ? AND created < UNIX_TIMESTAMP() - $conf->{url_shortener_cache_ttl}
+      ");
+      $self->{sth_clean} = $self->{dbh}->prepare("
+        DELETE FROM short_url_cache
+        WHERE created < UNIX_TIMESTAMP() - $conf->{url_shortener_cache_ttl}
+      ");
+    };
+  }
+  ##
+  ## PostgreSQL
+  ## 
+  elsif (lc $conf->{url_shortener_cache_type} eq 'dbi'
+      && $conf->{url_shortener_cache_dsn} =~ /^dbi:Pg/i)
+  {
+    eval {
+      local $SIG{'__DIE__'};
+      require DBI;
+      $self->{dbh} = DBI->connect_cached(
+        $conf->{url_shortener_cache_dsn},
+        $conf->{url_shortener_cache_username},
+        $conf->{url_shortener_cache_password},
+        {RaiseError => 1, PrintError => 0, InactiveDestroy => 1, AutoCommit => 1}
+      );
+      $self->{sth_insert} = $self->{dbh}->prepare("
+        INSERT INTO short_url_cache (short_url, decoded_url, created, modified)
+        VALUES (?,?,CAST(EXTRACT(epoch FROM NOW()) AS INT),CAST(EXTRACT(epoch FROM NOW()) AS INT))
+        ON CONFLICT (short_url) DO UPDATE SET
+          decoded_url = EXCLUDED.decoded_url,
+          modified = EXCLUDED.modified,
+          hits = short_url_cache.hits + 1
+      ");
+      $self->{sth_select} = $self->{dbh}->prepare("
+        SELECT decoded_url FROM short_url_cache
+        WHERE short_url = ?
+      ");
+      $self->{sth_delete} = $self->{dbh}->prepare("
+        DELETE FROM short_url_cache
+        WHERE short_url ? = AND created < CAST(EXTRACT(epoch FROM NOW()) AS INT) - $conf->{url_shortener_cache_ttl}
+      ");
+      $self->{sth_clean} = $self->{dbh}->prepare("
+        DELETE FROM short_url_cache
+        WHERE created < CAST(EXTRACT(epoch FROM NOW()) AS INT) - $conf->{url_shortener_cache_ttl}
+      ");
+    };
+  ##
+  ## ...
+  ##
+  } else {
+    warn "DecodeShortURLs: invalid cache configuration\n";
+    return;
+  }
+
+  if ($@ || !$self->{sth_clean}) {
+    warn "DecodeShortURLs: cache connect failed: $@\n";
+    undef $self->{dbh};
+    undef $self->{sth_insert};
+    undef $self->{sth_select};
+    undef $self->{sth_delete};
+    undef $self->{sth_clean};
+  }
+}
+
+sub short_url {
+  my ($self, $pms) = @_;
+
+  # Make sure checks are run
+  $self->_check_short($pms);
+
+  return $pms->{short_url} ? 1 : 0;
+}
+
+sub short_url_redir {
+  my ($self, $pms) = @_;
+
+  # Make sure checks are run
+  $self->_check_short($pms);
+
+  return $pms->{short_url_redir} ? 1 : 0;
+}
+
+sub short_url_200 {
+  my ($self, $pms) = @_;
+
+  # Make sure checks are run
+  $self->_check_short($pms);
+
+  return $pms->{short_url_200} ? 1 : 0;
+}
+
+sub short_url_404 {
+  my ($self, $pms) = @_;
+
+  # Make sure checks are run
+  $self->_check_short($pms);
+
+  return $pms->{short_url_404} ? 1 : 0;
+}
+
+sub short_url_code {
+  my ($self, $pms, undef, $code) = @_;
+
+  # Make sure checks are run
+  $self->_check_short($pms);
+
+  return 0 unless defined $code && $code =~ /^\d{3}$/;
+  return $pms->{"short_url_$code"} ? 1 : 0;
+}
+
+sub short_url_chained {
+  my ($self, $pms) = @_;
+
+  # Make sure checks are run
+  $self->_check_short($pms);
+
+  return $pms->{short_url_chained} ? 1 : 0;
+}
+
+sub short_url_maxchain {
+  my ($self, $pms) = @_;
+
+  # Make sure checks are run
+  $self->_check_short($pms);
+
+  return $pms->{short_url_maxchain} ? 1 : 0;
+}
+
+sub short_url_loop {
+  my ($self, $pms) = @_;
+
+  # Make sure checks are run
+  $self->_check_short($pms);
+
+  return $pms->{short_url_loop} ? 1 : 0;
+}
+
+sub _check_shortener_uri {
+  my ($uri, $conf) = @_;
+
+  local($1,$2);
+  return 0 unless $uri =~ m{^
+    https?://          # Only http
+    (?:[^\@/?#]*\@)?   # Ignore user:pass@
+    ([^/?#:]+)         # (Capture hostname)
+    (?::\d+)?          # Possible port
+    (.*?\w)?           # Some path wanted
+    }ix;
+  my $host = lc $1;
+  my $has_path = defined $2;
+  my $levels = $host =~ tr/.//;
+  # No point looking at single level "xxx.yy" without a path
+  return if $levels == 1 && !$has_path;
+  if (exists $conf->{url_shortener}->{$host}) {
+    return {
+      'uri' => $uri,
+      'method' => $conf->{url_shortener}->{$host} == 1 ? 'head' : 'get',
+    };
+  }
+  # if domain is a 3rd level domain check if there is a url shortener
+  # on the 2nd level tld
+  elsif ($levels == 2 && $host =~ /^(?!www)[^.]+(\.[^.]+\.[^.]+)$/i &&
+           exists $conf->{url_shortener}->{$1}) {
+    return {
+      'uri' => $uri,
+      'method' => $conf->{url_shortener}->{$1} == 1 ? 'head' : 'get',
+    };
+  }
+  return;
+}
+
+sub check_dnsbl {
+  my ($self, $opts) = @_;
+
+  $self->_check_short($opts->{permsgstatus});
+}
+
+sub _check_short {
+  my ($self, $pms) = @_;
+
+  return if $pms->{short_url_checked}++;
+  my $conf = $pms->{conf};
+
+  # Sort short URLs into hash to de-dup them
+  my %short_urls;
+  my $uris = $pms->get_uri_detail_list();
+  while (my($uri, $info) = each %{$uris}) {
+    next unless $info->{domains} && $info->{cleaned};
+    if (my $short_url_info = _check_shortener_uri($uri, $conf)) {
+      $short_urls{$uri} = $short_url_info;
+      last if scalar keys %short_urls >= $conf->{max_short_urls};
+    }
+  }
+
+  # Bail out if no shortener was found
+  return unless %short_urls;
+
+  # Mark that a URL shortener was found
+  $pms->{short_url} = 1;
+
+  # Bail out if network lookups not enabled or max_short_urls 0
+  return if $self->{net_disabled};
+  return if !$conf->{max_short_urls};
+
+  # Initialize cache
+  $self->initialise_url_shortener_cache($conf);
+
+  # Initialize LWP
+  my $ua = LWP::UserAgent->new(
+    'agent' => $conf->{url_shortener_user_agent},
+    'max_redirect' => 0,
+    'timeout' => $conf->{url_shortener_timeout},
+  );
+  $ua->env_proxy;
+
+  # Launch HTTP requests
+  foreach my $uri (keys %short_urls) {
+    $self->recursive_lookup($short_urls{$uri}, $pms, $ua);
+  }
+
+  # Automatically purge old entries
+  if ($self->{dbh} && $conf->{url_shortener_cache_autoclean}
+      && rand() < 1/$conf->{url_shortener_cache_autoclean})
+  {
+    dbg("cleaning stale cache entries");
+    eval { $self->{sth_clean}->execute(); };
+    if ($@) { dbg("cache cleaning failed: $@"); }
+  }
+}
+
+sub recursive_lookup {
+  my ($self, $short_url_info, $pms, $ua, %been_here) = @_;
+  my $conf = $pms->{conf};
+
+  my $count = scalar keys %been_here;
+  dbg("redirection count $count") if $count;
+  if ($count >= $conf->{max_short_url_redirections}) {
+    dbg("found more than $conf->{max_short_url_redirections} shortener redirections");
+    # Fire test
+    $pms->{short_url_maxchain} = 1;
+    return;
+  }
+
+  my $short_url = $short_url_info->{uri};
+  my $location;
+  if (defined($location = $self->cache_get($short_url))) {
+    if ($conf->{url_shortener_loginfo}) {
+      info("found cached $short_url => $location");
+    } else {
+      dbg("found cached $short_url => $location");
+    }
+    # Cached http code?
+    if ($location =~ /^\d{3}$/) {
+      $pms->{"short_url_$location"} = 1;
+      # Update cache
+      $self->cache_add($short_url, $location);
+      return;
+    }
+  } else {
+    # Not cached; do lookup
+    my $method = $short_url_info->{method};
+    my $response = $ua->$method($short_url);
+    if (!$response->is_redirect) {
+      dbg("URL is not redirect: $short_url = ".$response->status_line);
+      my $rcode = $response->code;
+      if ($rcode =~ /^\d{3}$/) {
+        $pms->{"short_url_$rcode"} = 1;
+        # Update cache
+        $self->cache_add($short_url, $rcode);
+      }
+      return;
+    }
+    $location = $response->headers->{location};
+    if ($self->{url_shortener_loginfo}) {
+      info("found $short_url => $location");
+    } else {
+      dbg("found $short_url => $location");
+    }
+  }
+
+  # Update cache
+  $self->cache_add($short_url, $location);
+
+  # Bail out if $short_url redirects to itself
+  if ($short_url eq $location) {
+    dbg("URL is redirect to itself");
+    return;
+  }
+
+  # At this point we have a valid redirection and new URL in $response
+  $pms->{short_url_redir} = 1;
+
+  # Set chained here otherwise we might mark a disabled page or
+  # redirect back to the same host as chaining incorrectly.
+  $pms->{short_url_chained} = 1 if $count;
+
+  # Check if we are being redirected to a local page
+  # Don't recurse in this case...
+  if ($location !~ m{^[a-z]+://}i) {
+    my $orig_location = $location;
+    my $orig_short_url = $short_url;
+    # Strip to..
+    if (index($location, '/') == 0) {
+      $short_url =~ s{^([a-z]+://.*?)[/?#].*}{$1}; # ..absolute path
+    } else {
+      $short_url =~ s{^([a-z]+://.*)/}{$1}; # ..relative path
+    }
+    $location = "$short_url/$location";
+    dbg("looks like a local redirection: $orig_short_url => $location ($orig_location)");
+    $pms->add_uri_detail_list($location) if !$pms->{uri_detail_list}->{$location};
+    return;
+  }
+
+  if (exists $been_here{$location}) {
+    # Loop detected
+    dbg("error: loop detected: $location");
+    $pms->{short_url_loop} = 1;
+    return;
+  }
+  $been_here{$location} = 1;
+  $pms->add_uri_detail_list($location) if !$pms->{uri_detail_list}->{$location};
+
+  # Check for recursion
+  if (my $short_url_info = _check_shortener_uri($location, $conf)) {
+    # Recurse...
+    $self->recursive_lookup($short_url_info, $pms, $ua, %been_here);
+  }
+}
+
+sub cache_add {
+  my ($self, $short_url, $decoded_url) = @_;
+
+  return if !$self->{dbh};
+  return if length($short_url) > 256 || length($decoded_url) > 512;
+
+  # Upsert
+  eval { $self->{sth_insert}->execute($short_url, $decoded_url); };
+  if ($@) {
+    dbg("could not add to cache: $@");
+  }
+
+  return;
+}
+
+sub cache_get {
+  my ($self, $key) = @_;
+
+  return if !$self->{dbh};
+
+  # Make sure expired entries are gone.  Just a quick check for primary key,
+  # not that expensive.
+  eval { $self->{sth_delete}->execute($key); };
+  if ($@) {
+    dbg("cache delete failed: $@");
+    return;
+  }
+
+  # Now try to get it (don't bother parsing if something was deleted above,
+  # it would be rare event anyway)
+  eval { $self->{sth_select}->execute($key); };
+  if ($@) {
+    dbg("cache get failed: $@");
+    return;
+  }
+
+  my @row = $self->{sth_select}->fetchrow_array();
+  if (@row) {
+    return $row[0];
+  }
+
+  return;
+}
+
+# Version features
+sub has_short_url { 1 }
+sub has_autoclean { 1 }
+sub has_short_url_code { 1 }
+sub has_user_agent { 1 } # url_shortener_user_agent
+sub has_get { 1 } # url_shortener_get
+sub has_clear { 1 } # clear_url_shortener
+sub has_timeout { 1 } # url_shortener_timeout
+sub has_max_redirections { 1 } # max_short_url_redirections
+# short_url() will always hit if matching url_shortener was found, even
+# without HTTP requests.  To check if a valid HTTP redirection response was
+# seen, use short_url_redir().
+sub has_short_url_redir { 1 }
+
+1;
diff --git a/upstream/lib/Mail/SpamAssassin/Plugin/ExtractText.pm b/upstream/lib/Mail/SpamAssassin/Plugin/ExtractText.pm
new file mode 100644 (file)
index 0000000..a95925e
--- /dev/null
@@ -0,0 +1,713 @@
+# <@LICENSE>
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to you under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# </@LICENSE>
+
+# Authors: Jonas Eckerman, Dave Wreski, Giovanni Bechis
+
+=head1 NAME
+
+ExtractText - extracts text from documenmts.
+
+=head1 SYNOPSIS
+
+loadplugin Mail::SpamAssassin::Plugin::ExtractText
+
+ifplugin Mail::SpamAssassin::Plugin::ExtractText
+
+  extracttext_external  pdftotext  /usr/bin/pdftotext -nopgbrk -layout -enc UTF-8 {} -
+  extracttext_use       pdftotext  .pdf application/pdf
+
+  # http://docx2txt.sourceforge.net
+  extracttext_external  docx2txt   /usr/bin/docx2txt {} -
+  extracttext_use       docx2txt   .docx application/docx
+
+  extracttext_external  antiword   /usr/bin/antiword -t -w 0 -m UTF-8.txt {}
+  extracttext_use       antiword   .doc application/(?:vnd\.?)?ms-?word.*
+
+  extracttext_external  unrtf      /usr/bin/unrtf --nopict {}
+  extracttext_use       unrtf      .doc .rtf application/rtf text/rtf
+
+  extracttext_external  odt2txt    /usr/bin/odt2txt --encoding=UTF-8 {}
+  extracttext_use       odt2txt    .odt .ott application/.*?opendocument.*text
+  extracttext_use       odt2txt    .sdw .stw application/(?:x-)?soffice application/(?:x-)?starwriter
+
+  extracttext_external  tesseract  {OMP_THREAD_LIMIT=1} /usr/bin/tesseract -c page_separator= {} -
+  extracttext_use       tesseract  .jpg .png .bmp .tif .tiff image/(?:jpeg|png|x-ms-bmp|tiff)
+
+  add_header   all          ExtractText-Flags _EXTRACTTEXTFLAGS_
+  header       PDF_NO_TEXT  X-ExtractText-Flags =~ /\bpdftotext_NoText\b/
+  describe     PDF_NO_TEXT  PDF without text
+  score        PDF_NO_TEXT  0.001
+
+  header       DOC_NO_TEXT  X-ExtractText-Flags =~ /\b(?:antiword|openxml|unrtf|odt2txt)_NoText\b/
+  describe     DOC_NO_TEXT  Document without text
+  score        DOC_NO_TEXT  0.001
+
+  header       EXTRACTTEXT  exists:X-ExtractText-Flags
+  describe     EXTRACTTEXT  Email processed by extracttext plugin
+  score        EXTRACTTEXT  0.001
+
+endif
+
+=head1 DESCRIPTION
+
+This module uses external tools to extract text from message parts,
+and then sets the text as the rendered part. External tool must output
+plain text, not HTML or other non-textual result.
+
+How to extract text is completely configurable, and based on
+MIME part type and file name.
+
+=head1 CONFIGURATION
+
+All configuration lines in user_prefs files will be ignored.
+
+=over 4
+
+=item extracttext_maxparts (default: 10)
+
+Configure the maximum mime parts number to analyze, a value of 0 means all mime parts
+will be analyzed
+
+=item extracttext_timeout (default: 5 10)
+
+Configure the timeout in seconds of external tool checks, per attachment.
+
+Second argument speficies maximum total time for all checks.
+
+=back
+
+=head2 Tools
+
+=over
+
+=item extracttext_use
+
+Specifies what tool to use for what message parts.
+
+The general syntax is
+
+extracttext_use  C<name>  C<specifiers>
+
+=back
+
+=over
+
+=item name
+
+the internal name of a tool.
+
+=item specifiers
+
+File extension and regular expressions for file names and MIME
+types. The regular expressions are anchored to beginning and end.
+
+=back
+
+=head3 Examples
+
+       extracttext_use  antiword  .doc application/(?:vnd\.?)?ms-?word.*
+       extracttext_use  openxml   .docx .dotx .dotm application/(?:vnd\.?)openxml.*?word.*
+       extracttext_use  openxml   .doc .dot application/(?:vnd\.?)?ms-?word.*
+       extracttext_use  unrtf     .doc .rtf application/rtf text/rtf
+
+=over
+
+=item extracttext_external
+
+Defines an external tool.  The tool must read a document on standard input
+or from a file and write text to standard output.
+
+The special keyword "{}" will be substituted at runtime with the temporary
+filename to be scanned by the external tool.
+
+Environment variables can be defined with "{KEY=VALUE}", these strings will
+be removed from commandline.
+
+It is required that commandline used outputs result directly to STDOUT.
+
+The general syntax is
+
+extracttext_external C<name> C<command> C<parameters>
+
+=back
+
+=over
+
+=item name
+
+The internal name of this tool.
+
+=item command
+
+The full path to the external command to run.
+
+=item parameters
+
+Parameters for the external command. The temporary file name containing
+the document will be automatically added as last parameter.
+
+=back
+
+=head3 Examples
+
+       extracttext_external  antiword  /usr/bin/antiword -t -w 0 -m UTF-8.txt {} -
+       extracttext_external  unrtf     /usr/bin/unrtf --nopict {}
+       extracttext_external  odt2txt   /usr/bin/odt2txt --encoding=UTF-8 {}
+
+=head2 Metadata
+
+The plugin adds some pseudo headers to the message. These headers are seen by
+the bayes system, and can be used in normal SpamAssassin rules.
+
+The headers are also available as template tags as noted below.
+
+=head3 Example
+
+The fictional example headers below are based on a message containing this:
+
+=over
+
+=item 1
+A perfectly normal PDF.
+
+=item 2
+An OpenXML document with a word document inside.
+Neither Office document contains text.
+
+=back
+
+=head3 Headers
+
+=over
+
+=item X-ExtractText-Chars
+
+Tag: _EXTRACTTEXTCHARS_
+
+Contains a count of characters that were extracted.
+
+X-ExtractText-Chars: 10970
+
+=item X-ExtractText-Words
+
+Tag: _EXTRACTTEXTWORDS_
+
+Contains a count of "words" that were extracted.
+
+X-ExtractText-Chars: 1599
+
+=item X-ExtractText-Tools
+
+Tag: _EXTRACTTEXTTOOLS_
+
+Contains chains of tools used for extraction.
+
+X-ExtractText-Tools: pdftotext openxml_antiword
+
+=item X-ExtractText-Types
+
+Tag: _EXTRACTTEXTTYPES_
+
+Contains chains of MIME types for parts found during extraction.
+
+X-ExtractText-Types: application/pdf; application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/ms-word
+
+=item X-ExtractText-Extensions
+
+Tag: _EXTRACTTEXTEXTENSIONS_
+
+Contains chains of canonicalized file extensions for parts
+found during extraction.
+
+X-ExtractText-Extensions: pdf docx
+
+=item X-ExtractText-Flags
+
+Tag: _EXTRACTTEXTFLAGS_
+
+Contains notes from the plugin.
+
+X-ExtractText-Flags: openxml_NoText
+
+=back
+
+=head3 Rules
+
+Example:
+
+       header    PDF_NO_TEXT  X-ExtractText-Flags =~ /\bpdftotext_Notext\b/
+       describe  PDF_NO_TEXT  PDF without text
+
+=cut
+
+package Mail::SpamAssassin::Plugin::ExtractText;
+
+use strict;
+use warnings;
+use re 'taint';
+
+my $VERSION = 0.001;
+
+use File::Basename;
+
+use Mail::SpamAssassin::Logger;
+use Mail::SpamAssassin::Util qw (compile_regexp untaint_var untaint_file_path
+  proc_status_ok exit_status_str);
+
+our @ISA = qw(Mail::SpamAssassin::Plugin);
+
+sub new {
+  my ($class, $mailsa) = @_;
+
+  $class = ref($class) || $class;
+  my $self = $class->SUPER::new($mailsa);
+  bless ($self, $class);
+
+  $self->{match} = [];
+  $self->{tools} = {};
+  $self->{magic} = 0;
+
+  $self->register_method_priority('post_message_parse', -1);
+  $self->set_config($mailsa->{conf});
+  return $self;
+}
+
+sub set_config {
+  my ($self, $conf) = @_;
+  my @cmds;
+
+  push(@cmds, {
+    setting => 'extracttext_maxparts',
+    default => 10,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
+  });
+
+  push(@cmds, {
+    setting => 'extracttext_timeout',
+    default => 5,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      unless (defined $value && $value !~ /^$/) {
+        return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
+      }
+      local ($1,$2);
+      unless ($value =~ /^(\d+)(?:\s+(\d+))?$/) {
+        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+      }
+      $self->{extracttext_timeout} = $1;
+      $self->{extracttext_timeout_total} = $2;
+    }
+  });
+
+  $conf->{parser}->register_commands(\@cmds);
+}
+
+sub parse_config {
+  my ($self, $opts) = @_;
+
+  # Ignore users's configuration lines
+  return 0 if $opts->{user_config};
+
+  if ($opts->{key} eq 'extracttext_use') {
+    $self->inhibit_further_callbacks();
+    # Temporary kludge to notify users. Double backslashes have zero benefit for this plugin config.
+    if ($opts->{value} =~ s/\\\\/\\/g) {
+      warn "extracttext: DOUBLE BACKSLASHES DEPRECATED, change config to single backslashes, autoconverted for backward compatibility: $opts->{key} $opts->{value}\n";
+    }
+    if ($opts->{value} =~ /(?:to|2)html\b/) {
+      warn "extracttext: HTML tools are not supported, plain text output is required. Please remove: $opts->{key} $opts->{value}\n";
+      return 1;
+    }
+    my @vals = split(/\s+/, $opts->{value});
+    my $tool = lc(shift @vals);
+    return 0 unless @vals;
+    foreach my $what (@vals) {
+      my $where;
+      if (index($what, '/') >= 0) {
+        $where = 'type';
+      } else {
+        $where = 'name';
+        if ($what =~ /^\.[a-zA-Z0-9]+$/) {
+          $what = ".*\\$what";
+        }
+      }
+      my ($rec, $err) = compile_regexp('^(?i)'.$what.'$', 0);
+      if (!$rec) {
+        warn("invalid regexp '$what': $err\n");
+        return 0;
+      }
+      push @{$self->{match}}, {where=>$where, what=>$rec, tool=>$tool};
+      dbg('extracttext: use: %s %s %s', $tool, $where, $what);
+    }
+    return 1;
+  }
+  
+  if ($opts->{key} eq 'extracttext_external') {
+    $self->inhibit_further_callbacks();
+    # Temporary kludge to notify users. Double backslashes have zero benefit for this plugin config.
+    if ($opts->{value} =~ s/\\\\/\\/g) {
+      warn "extracttext: DOUBLE BACKSLASHES DEPRECATED, change config to single backslashes, autoconverted for backward compatibility: $opts->{key} $opts->{value}\n";
+    }
+    if ($opts->{value} =~ /(?:to|2)html\b/) {
+      warn "extracttext: HTML tools are not supported, plain text output is required. Please remove: $opts->{key} $opts->{value}\n";
+      return 1;
+    }
+    my %env;
+    while ($opts->{value} =~ s/\{(.+?)\}/ /g) {
+      my ($k,$v) = split(/=/, $1, 2);
+      $env{$k} = defined $v ? $v : '';
+    }
+    my @vals = split(/\s+/, $opts->{value});
+    my $name = lc(shift @vals);
+    return 0 unless @vals > 1;
+    if ($self->{tools}->{$name}) {
+      warn "extracttext: duplicate tool defined: $name\n";
+      return 0;
+    }
+    #unless (-x $vals[0]) {
+    #  warn "extracttext: missing tool: $name ($vals[0])\n";
+    #  return 0;
+    #}
+    $self->{tools}->{$name} = {
+      'name' => $name,
+      'type' => 'external',
+      'env' => \%env,
+      'cmd' => \@vals,
+    };
+    dbg('extracttext: external: %s "%s"', $name, join('","', @vals));
+    return 1;
+  }
+
+  return 0;
+}
+
+# Extract 'text' via running an external command.
+sub _extract_external {
+  my ($self, $object, $tool) = @_;
+
+  my ($errno, $pipe_errno, $tmp_file, $err_file, $pid);
+  my $resp = '';
+  my @cmd = @{$tool->{cmd}};
+
+  Mail::SpamAssassin::PerMsgStatus::enter_helper_run_mode($self);
+
+  # Set environment variables
+  foreach (keys %{$tool->{env}}) {
+    $ENV{$_} = $tool->{env}{$_};
+  }
+
+  my $timer = Mail::SpamAssassin::Timeout->new(
+    { secs => $self->{main}->{conf}->{extracttext_timeout},
+      deadline => $self->{'master_deadline'} });
+
+  my $err = $timer->run_and_catch(sub {
+    local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };
+
+    ($tmp_file, my $tmp_fh) = Mail::SpamAssassin::Util::secure_tmpfile();
+    $tmp_file  or die "failed to create a temporary file";
+    print $tmp_fh ${$object->{data}};
+    close($tmp_fh);
+
+    ($err_file, my $err_fh) = Mail::SpamAssassin::Util::secure_tmpfile();
+    $err_file  or die "failed to create a temporary file";
+    close($err_fh);
+    $err_file = untaint_file_path($err_file);
+
+    foreach (@cmd) {
+      # substitute "{}" with the temporary file name to pass to the external software
+      s/\{\}/$tmp_file/;
+      $_ = untaint_var($_);
+    }
+
+    $pid = Mail::SpamAssassin::Util::helper_app_pipe_open(*EXTRACT, undef, ">$err_file", @cmd);
+    $pid or die "$!\n";
+
+    # read+split avoids a Perl I/O bug (Bug 5985)
+    my($inbuf, $nread);
+
+    while ($nread = read(EXTRACT, $inbuf, 8192)) { $resp .= $inbuf }
+    defined $nread  or die "error reading from pipe: $!";
+
+    $errno = 0;
+    close EXTRACT or $errno = $!;
+
+    if (proc_status_ok($?, $errno)) {
+      dbg("extracttext: [%s] (%s) finished successfully", $pid, $cmd[0]);
+    } elsif (proc_status_ok($?, $errno, 0, 1)) {  # sometimes it exits with 1
+      dbg("extracttext: [%s] (%s) finished: %s", $pid, $cmd[0], exit_status_str($?, $errno));
+    } else {
+      info("extracttext: [%s] (%s) error: %s", $pid, $cmd[0], exit_status_str($?, $errno));
+    }
+    # Save return status for later
+    $pipe_errno = $?;
+  });
+
+  if (defined(fileno(*EXTRACT))) {  # still open
+    if ($pid) {
+      if (kill('TERM', $pid)) {
+        dbg("extracttext: killed stale helper [$pid] ($cmd[0])");
+      } else {
+        dbg("extracttext: killing helper application [$pid] ($cmd[0]) failed: $!");
+      }
+    }
+    $errno = 0;
+    close EXTRACT or $errno = $!;
+    proc_status_ok($?, $errno)
+      or info("extracttext: [%s] (%s) error: %s", $pid, $cmd[0], exit_status_str($?, $errno));
+  }
+
+  Mail::SpamAssassin::PerMsgStatus::leave_helper_run_mode($self);
+  unlink($tmp_file);
+  # Read first line from STDERR
+  my $err_resp = -s $err_file ?
+    do { open(ERRF, $err_file); $_ = <ERRF>; close(ERRF); chomp; $_; } : '';
+  unlink($err_file);
+
+  if ($err_resp ne '') {
+    dbg("extracttext: [$pid] ($cmd[0]) stderr output: $err_resp");
+  }
+
+  # If the output starts with the command that has been run it's
+  # probably an error message
+  if ($pipe_errno) {
+    if ($err_resp =~ /\b(?:Usage:|No such file or directory)/) {
+      warn "extracttext: error from $cmd[0], please verify configuration: $err_resp\n";
+    }
+    elsif ($err_resp =~ /^Syntax (?:Warning|Error): (?:May not be a PDF file|Couldn't find trailer dictionary)/) {
+      # Ignore pdftotext
+    }
+    elsif ($err_resp =~ /^Error in (?:findFileFormatStream|fopenReadStream): (?:truncated file|file not found)/) {
+      # Ignore tesseract
+    }
+    elsif ($err_resp =~ /^libpng error:/) {
+      # Ignore tesseract
+    }
+    elsif ($err_resp =~ /^Corrupt JPEG data:/) {
+      # Ignore tesseract
+    }
+    elsif ($err_resp =~ /^\S+ is not a Word Document/) {
+      # Ignore antiword
+    }
+    elsif (!$resp) {
+      warn "extracttext: error (".($pipe_errno/256).") from $cmd[0]: $err_resp\n";
+    }
+    return (0, $resp);
+  }
+  return (1, $resp);
+}
+
+sub _extract_object {
+  my ($self, $object, $tool) = @_;
+  my ($ok, $text);
+
+  if ($tool->{type} eq 'external') {
+    ($ok, $text) = $self->_extract_external($object, $tool);
+  } else {
+    warn "extracttext: bad tool type: $tool->{type}\n";
+    return 0;
+  }
+
+  return 0 unless $ok;
+
+  if ($text =~ /^[\s\r\n]*$/s) {
+    $text = '';
+  } else {
+    # Remove not important html elements
+    #$text =~ s/(?=<!DOCTYPE)([\s\S]*?)>//g;
+    #$text =~ s/(?=<!--)([\s\S]*?)-->//g;
+  }
+
+  if ($text eq '') {
+    dbg('extracttext: No text extracted');
+  }
+
+  $text = untaint_var($text);
+  utf8::encode($text) if utf8::is_utf8($text);
+
+  return (1, $text);
+}
+
+sub _get_extension {
+  my ($self, $object) = @_;
+  my $fext;
+  if ($object->{name} && $object->{name} =~ /\.([^.\\\/]+)$/) {
+    $fext = $1;
+  }
+  elsif ($object->{file} && $object->{file} =~ /\.([^.\\\/]+)$/) {
+    $fext = $1;
+  }
+  return $fext ? ($fext) : ();
+}
+
+sub _extract {
+  my ($self, $coll, $part, $type, $name, $data, $tool) = @_;
+  my $object = {
+    'data' => $data,
+    'type' => $type,
+    'name' => $name
+  };
+  my @fexts;
+  my @types;
+
+  my @tools = ($tool->{name});
+  my ($ok, $text) = $self->_extract_object($object,$tool);
+
+  # when url+text, script never returns to this point from _extract_object above
+  #
+  return 0 unless $ok;
+  if ($text ne '' && would_log('dbg','extracttext') > 1) {
+    dbg("extracttext: text extracted:\n$text");
+  }
+
+  push @{$coll->{text}}, $text;
+  push @types, $type;
+  push @fexts, $self->_get_extension($object);
+  if ($text eq '') {
+    push @{$coll->{flags}}, 'NoText';
+    push @{$coll->{text}}, 'NoText';
+  } else {
+    if ($text =~ /<a(?:\s+[^>]+)?\s+href="([^">]*)"/) {
+      push @{$coll->{flags}}, 'ActionURI';
+      dbg("extracttext: ActionURI: $1");
+      push @{$coll->{text}}, $text;
+    }
+    if ($text =~ /NoText/) {
+      push @{$coll->{flags}},'NoText';
+      dbg("extracttext: NoText");
+      push @{$coll->{text}}, $text;
+    }
+    $coll->{chars} += length($text);
+
+    # the following is safe (regarding clobbering the @_) since perl v5.11.0
+    $coll->{words} += split(/\W+/s,$text) - 1;
+    # $coll->{words} += scalar @{[split(/\W+/s,$text)]} - 1;  # old perl hack
+
+    dbg("extracttext: rendering text for type $type with $tool->{name}");
+    $part->set_rendered($text);
+  }
+
+  if (@types) {
+    push @{$coll->{types}}, join(', ', @types);
+  }
+  if (@fexts) {
+    push @{$coll->{extensions}}, join('_', @fexts);
+  }
+  push @{$coll->{tools}}, join('_', @tools);
+  return 1;
+}
+
+#
+# check attachment type and match with the right tool
+#
+sub _check_extract {
+  my ($self, $coll, $checked, $part, $decoded, $data, $type, $name) = @_;
+  return 0 unless (defined $type || defined $name);
+  foreach my $match (@{$self->{match}}) {
+    next unless $self->{tools}->{$match->{tool}};
+    next if $checked->{$match->{tool}};
+
+    if ($match->{where} eq 'name') {
+      next unless (defined $name && $name =~ $match->{what});
+    } elsif ($match->{where} eq 'type') {
+      next unless (defined $type && $type =~ $match->{what});
+    } else {
+      next;
+    }
+    $checked->{$match->{tool}} = 1;
+    # dbg("extracttext: coll: $coll, part: $part, type: $type, name: $name, data: $data, tool: $self->{tools}->{$match->{tool}}");
+    return 1 if $self->_extract($coll,$part,$type,$name,$data,$self->{tools}->{$match->{tool}});
+  }
+  return 0;
+}
+
+sub post_message_parse {
+  my ($self, $opts) = @_;
+
+  my $timer = $self->{main}->time_method("extracttext");
+
+  my $msg = $opts->{'message'};
+  $self->{'master_deadline'} = $msg->{'master_deadline'};
+  my $starttime = time;
+
+  my %collect = (
+    'tools'            => [],
+    'types'            => [],
+    'extensions'       => [],
+    'flags'            => [],
+    'chars'            => 0,
+    'words'            => 0,
+    'text'             => [],
+  );
+
+  my $conf = $self->{main}->{conf};
+  my $maxparts = $conf->{extracttext_maxparts};
+  my $ttimeout = $conf->{extracttext_timeout_total} ||
+    $conf->{extracttext_timeout} > 10 ? $conf->{extracttext_timeout} : 10;
+  my $nparts = 0;
+  foreach my $part ($msg->find_parts(qr/./, 1)) {
+    next unless $part->is_leaf;
+    if ($maxparts > 0 && ++$nparts > $maxparts) {
+      dbg("extracttext: Skipping MIME parts exceeding the ${maxparts}th");
+      last;
+    }
+    if (time - $starttime >= $ttimeout) {
+      dbg("extracttext: Skipping MIME parts, total execution timeout exceeded");
+      last;
+    }
+    my (undef,$rtd) = $part->rendered;
+    next if defined $rtd;
+    my %checked = ();
+    my $dat = $part->decode();
+    my $typ = $part->{type};
+    my $nam = $part->{name};
+    my $dec = 1;
+    next if $self->_check_extract(\%collect,\%checked,$part,\$dec,\$dat,$typ,$nam);
+  }
+
+  return 1 unless @{$collect{tools}};
+
+  my @uniq_tools = do { my %seen; grep { !$seen{$_}++ } @{$collect{tools}} };
+  my @uniq_types = do { my %seen; grep { !$seen{$_}++ } @{$collect{types}} };
+  my @uniq_ext   = do { my %seen; grep { !$seen{$_}++ } @{$collect{extensions}} };
+  my @uniq_flags = do { my %seen; grep { !$seen{$_}++ } @{$collect{flags}} };
+
+  $msg->put_metadata('X-ExtractText-Words', $collect{words});
+  $msg->put_metadata('X-ExtractText-Chars', $collect{chars});
+  $msg->put_metadata('X-ExtractText-Tools', join(' ', @uniq_tools));
+  $msg->put_metadata('X-ExtractText-Types', join(' ', @uniq_types));
+  $msg->put_metadata('X-ExtractText-Extensions', join(' ', @uniq_ext));
+  $msg->put_metadata('X-ExtractText-Flags', join(' ', @uniq_flags));
+
+  return 1;
+}
+
+sub parsed_metadata {
+  my ($self, $opts) = @_;
+  my $pms = $opts->{permsgstatus};
+  my $msg = $pms->get_message();
+  foreach my $tag (('Words','Chars','Tools','Types','Extensions','Flags')) {
+    my $v = $msg->get_metadata("X-ExtractText-$tag");
+    if (defined $v) {
+      $pms->set_tag("ExtractText$tag", $v);
+      dbg("extracttext: tag: $tag $v");
+    }
+  }
+  return 1;
+}
+
+1;
index b0ec042751012d5fb82d21ec81c910a5c45ebc08..c2c85430f3a37aea8a630f3102fc4a2ffe46d82f 100644 (file)
@@ -20,7 +20,7 @@ use strict;
 use warnings;
 use re 'taint';
 
-my $VERSION = 2.003;
+my $VERSION = 4.000;
 
 =head1 NAME
 
@@ -48,19 +48,21 @@ freemail_domains domain ...
    For example:
    freemail_domains hotmail.com hotmail.co.?? yahoo.* yahoo.*.*
 
-freemail_whitelist email/domain ...
+freemail_welcomelist email/domain ...
+
+   Previously freemail_whitelist which will work interchangeably until 4.1.
 
    Emails or domains listed here are ignored (pretend they aren't
    freemail). No wildcards!
 
-freemail_import_whitelist_auth 1/0
+freemail_import_welcomelist_auth 1/0
 
-   Entries in whitelist_auth will also be used to whitelist emails
+   Entries in welcomelist_auth will also be used to welcomelist emails
    or domains from being freemail.  Default is 0.
 
-freemail_import_def_whitelist_auth 1/0
+freemail_import_def_welcomelist_auth 1/0
 
-   Entries in def_whitelist_auth will also be used to whitelist emails
+   Entries in def_welcomelist_auth will also be used to welcomelist emails
    or domains from being freemail.  Default is 0.
 
 header FREEMAIL_REPLYTO eval:check_freemail_replyto(['option'])
@@ -96,17 +98,6 @@ header FREEMAIL_BODY eval:check_freemail_body(['regex'])
 
    Searches body for freemail address. With optional regex to match.
 
-=head1 CHANGELOG
-
- 1.996 - fix freemail_skip_bulk_envfrom
- 1.997 - set freemail_skip_when_over_max to 1 by default
- 1.998 - don't warn about missing freemail_domains when linting
- 1.999 - default whitelist undisclosed-recipient@yahoo.com etc
- 2.000 - some cleaning up
- 2.001 - fix freemail_whitelist
- 2.002 - _add_desc -> _got_hit, fix description email append bug
- 2.003 - freemail_import_(def_)whitelist_auth
-
 =cut
 
 use Mail::SpamAssassin::Plugin;
@@ -115,8 +106,8 @@ use Mail::SpamAssassin::Util qw(compile_regexp);
 
 our @ISA = qw(Mail::SpamAssassin::Plugin);
 
-# default email whitelist
-our $email_whitelist = qr/
+# default email welcomelist
+our $email_welcomelist = qr/
   ^(?:
       abuse|support|sales|info|helpdesk|contact|kontakt
     | (?:post|host|domain)master
@@ -142,7 +133,7 @@ our $skip_replyto_envfrom = qr/
   )\@
 /xi;
 
-sub dbg { Mail::SpamAssassin::Plugin::dbg ("FreeMail: @_"); }
+sub dbg { my $msg = shift; Mail::SpamAssassin::Plugin::dbg("FreeMail: $msg", @_); }
 
 sub new {
     my ($class, $mailsa) = @_;
@@ -153,10 +144,10 @@ sub new {
 
     $self->{freemail_available} = 1;
     $self->set_config($mailsa->{conf});
-    $self->register_eval_rule("check_freemail_replyto");
-    $self->register_eval_rule("check_freemail_from");
-    $self->register_eval_rule("check_freemail_header");
-    $self->register_eval_rule("check_freemail_body");
+    $self->register_eval_rule("check_freemail_replyto", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+    $self->register_eval_rule("check_freemail_from", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+    $self->register_eval_rule("check_freemail_header", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+    $self->register_eval_rule("check_freemail_body", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
 
     return $self;
 }
@@ -215,13 +206,15 @@ sub set_config {
         }
     );
     push(@cmds, {
-        setting => 'freemail_import_whitelist_auth',
+        setting => 'freemail_import_welcomelist_auth',
+        aliases => ['freemail_import_whitelist_auth'], # removed in 4.1
         default => 0,
         type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
         }
     );
     push(@cmds, {
-        setting => 'freemail_import_def_whitelist_auth',
+        setting => 'freemail_import_def_welcomelist_auth',
+        aliases => ['freemail_import_def_whitelist_auth'], # removed in 4.1
         default => 0,
         type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
         }
@@ -234,9 +227,9 @@ sub parse_config {
 
     if ($opts->{key} eq "freemail_domains") {
         foreach my $temp (split(/\s+/, $opts->{value})) {
-            if ($temp =~ /^[a-z0-9.*?-]+$/i) {
+            if ($temp !~ tr/a-zA-Z0-9.*?-//c) {
                 my $value = lc($temp);
-                if ($value =~ /[*?]/) { # separate wildcard list
+                if ($value =~ tr/*?//) { # separate wildcard list
                     $self->{freemail_temp_wc}{$value} = 1;
                 }
                 else {
@@ -244,21 +237,21 @@ sub parse_config {
                 }
             }
             else {
-                warn("invalid freemail_domains: $temp");
+                warn("freemail: invalid freemail_domains: $temp\n");
             }
         }
         $self->inhibit_further_callbacks();
         return 1;
     }
 
-    if ($opts->{key} eq "freemail_whitelist") {
+    if ($opts->{key} eq "freemail_welcomelist" || $opts->{key} eq "freemail_whitelist") {
         foreach my $temp (split(/\s+/, $opts->{value})) {
             my $value = lc($temp);
             if ($value =~ /\w[.@]\w/) {
-                $self->{freemail_whitelist}{$value} = 1;
+                $self->{freemail_welcomelist}{$value} = 1;
             }
             else {
-                warn("invalid freemail_whitelist: $temp");
+                warn("freemail: invalid freemail_welcomelist: $temp\n");
             }
         }
         $self->inhibit_further_callbacks();
@@ -292,12 +285,7 @@ sub finish_parsing_end {
         dbg("loaded freemail_domains entries: $count normal, $wcount wildcard");
     }
     else {
-        if ($self->{main}->{lint_rules} ||1) {
-            dbg("no freemail_domains entries defined, disabling plugin");
-        }
-        else {
-            warn("no freemail_domains entries defined, disabling plugin");
-        }
+        dbg("no freemail_domains entries defined, disabling plugin");
         $self->{freemail_available} = 0;
     }
 
@@ -315,29 +303,29 @@ sub _is_freemail {
 
     return 0 if $email eq '';
 
-    if (defined $self->{freemail_whitelist}{$email}) {
-        dbg("whitelisted email: $email");
+    if (defined $self->{freemail_welcomelist}{$email}) {
+        dbg("welcomelisted email: $email");
         return 0;
     }
 
     my $domain = $email;
     $domain =~ s/.*\@//;
 
-    if (defined $self->{freemail_whitelist}{$domain}) {
-        dbg("whitelisted domain: $domain");
+    if (defined $self->{freemail_welcomelist}{$domain}) {
+        dbg("welcomelisted domain: $domain");
         return 0;
     }
 
-    if ($email =~ $email_whitelist) {
-        dbg("whitelisted email, default: $email");
+    if ($email =~ $email_welcomelist) {
+        dbg("welcomelisted email, default: $email");
         return 0;
     }
 
-    foreach my $list ('whitelist_auth','def_whitelist_auth') {
+    foreach my $list ('welcomelist_auth','def_welcomelist_auth') {
         if ($pms->{conf}->{"freemail_import_$list"}) {
             foreach my $regexp (values %{$pms->{conf}->{$list}}) {
                 if ($email =~ /$regexp/o) {
-                    dbg("whitelisted email, $list: $email");
+                    dbg("welcomelisted email, $list: $email");
                     return 0;
                 }
             }
@@ -422,21 +410,13 @@ sub _parse_body {
     return 1;
 }
 
-sub _got_hit {
-    my ($self, $pms, $email, $desc) = @_;
+sub _test_log {
+    my ($self, $pms, $email, $rulename) = @_;
 
-    my $rulename = $pms->get_current_eval_rule_name();
-
-    if (defined $pms->{conf}->{descriptions}->{$rulename}) {
-        $desc = $pms->{conf}->{descriptions}->{$rulename};
-    }
-
-    if ($pms->{main}->{conf}->{freemail_add_describe_email}) {
-        $email =~ s/\@/[at]/g;
-        $pms->test_log($email);
+    if ($pms->{conf}->{freemail_add_describe_email}) {
+        $email =~ s/\@/(at)/g;
+        $pms->test_log($email, $rulename);
     }
-
-    $pms->got_hit($rulename, "", description => $desc, ruletype => 'eval');
 }
 
 sub check_freemail_header {
@@ -448,7 +428,7 @@ sub check_freemail_header {
     dbg("RULE ($rulename) check_freemail_header".(defined $regex ? " regex:$regex" : ""));
 
     unless (defined $header) {
-        warn("check_freemail_header needs argument");
+        warn("freemail: check_freemail_header needs argument\n");
         return 0;
     }
 
@@ -462,13 +442,13 @@ sub check_freemail_header {
         $re = $rec;
     }
 
-    my @emails = map (lc, $pms->{main}->find_all_addrs_in_line ($pms->get($header)));
+    my @emails = map (lc, $pms->get("$header:addr"));
 
     if (!scalar (@emails)) {
          dbg("header $header not found from mail");
          return 0;
     }
-    dbg("addresses from header $header: ".join(';',@emails));
+    dbg("addresses from header $header: ".join(', ', @emails));
 
     foreach my $email (@emails) {    
         if ($self->_is_freemail($email, $pms)) {
@@ -479,7 +459,7 @@ sub check_freemail_header {
             else {
                 dbg("HIT! $email is freemail");
             }
-            $self->_got_hit($pms, $email, "Header $header is freemail");
+            $self->_test_log($pms, $email, $rulename);
             return 1;
          }
     }
@@ -511,16 +491,16 @@ sub check_freemail_body {
         foreach my $email (keys %{$pms->{freemail_cache}{body}}) {
             if ($email =~ /$re/o) {
                 dbg("HIT! email from body is freemail and matches regex: $email");
-                $self->_got_hit($pms, $email, "Email from body is freemail");
-                return 0;
+                $self->_test_log($pms, $email, $rulename);
+                return 1;
             }
         }
     }
     elsif (scalar keys %{$pms->{freemail_cache}{body}}) {
         my $emails = join(', ', keys %{$pms->{freemail_cache}{body}});
         dbg("HIT! body has freemails: $emails");
-        $self->_got_hit($pms, $emails, "Body contains freemails");
-        return 0;
+        $self->_test_log($pms, $emails, $rulename);
+        return 1;
     }
 
     return 0;
@@ -563,8 +543,8 @@ sub check_freemail_from {
         else {
             dbg("HIT! $email is freemail");
         }
-        $self->_got_hit($pms, $email, "Sender address is freemail");
-        return 0;
+        $self->_test_log($pms, $email, $rulename);
+        return 1;
     }
 
     return 0;
@@ -580,7 +560,7 @@ sub check_freemail_replyto {
 
     if (defined $what) {
         if ($what ne 'replyto' and $what ne 'reply') {
-            warn("invalid check_freemail_replyto option: $what");
+            warn("freemail: invalid check_freemail_replyto option: $what\n");
             return 0;
         }
     }
@@ -590,25 +570,34 @@ sub check_freemail_replyto {
 
     # Skip mailing-list etc looking requests, mostly FPs from them
     if ($pms->{main}->{conf}->{freemail_skip_bulk_envfrom}) {
-        my $envfrom = lc($pms->get("EnvelopeFrom"));
-        if ($envfrom =~ $skip_replyto_envfrom) {
+        my $envfrom = ($pms->get("EnvelopeFrom"))[0];
+        if (defined $envfrom && $envfrom =~ $skip_replyto_envfrom) {
             dbg("envelope sender looks bulk, skipping check: $envfrom");
             return 0;
         }
     }
 
-    my $from = lc($pms->get("From:addr"));
-    my $replyto = lc($pms->get("Reply-To:addr"));
-    my $from_is_fm = $self->_is_freemail($from, $pms);
-    my $replyto_is_fm = $self->_is_freemail($replyto, $pms);
+    my @from_addrs = map (lc, $pms->get("From:addr"));
+    dbg("From address: ".join(", ", @from_addrs)) if @from_addrs;
 
-    dbg("From address: $from") if $from ne '';
-    dbg("Reply-To address: $replyto") if $replyto ne '';
+    my @replyto_addrs = map (lc, $pms->get("Reply-To:addr"));
+    dbg("Reply-To address: ".join(", ", @replyto_addrs)) if @replyto_addrs;
+
+    my $from_is_fm = grep { $self->_is_freemail($_, $pms) } @from_addrs;
+    my $replyto_is_fm = grep { $self->_is_freemail($_, $pms) } @replyto_addrs;
+
+    my $from_not_in_replyto = 1;
+    foreach my $from (@from_addrs) {
+        next unless grep { $_ eq $from } @replyto_addrs;
+        $from_not_in_replyto = 0;
+    }
 
-    if ($from_is_fm and $replyto_is_fm and ($from ne $replyto)) {
+    if ($from_is_fm and $replyto_is_fm and $from_not_in_replyto) {
         dbg("HIT! From and Reply-To are different freemails");
-        $self->_got_hit($pms, "$from, $replyto", "From and Reply-To are different freemails");
-        return 0;
+        my $from = join(",", @from_addrs);
+        my $replyto = join(",", @replyto_addrs);
+        $self->_test_log($pms, "$from -> $replyto", $rulename);
+        return 1;
     }
 
     if ($what eq 'replyto') {
@@ -618,7 +607,7 @@ sub check_freemail_replyto {
         }
     }
     elsif ($what eq 'reply') {
-        if ($replyto ne '' and !$replyto_is_fm) {
+        if (@replyto_addrs and !$replyto_is_fm) {
             dbg("Reply-To defined and is not freemail, skipping check");
             return 0;
         }
@@ -627,19 +616,21 @@ sub check_freemail_replyto {
             return 0;
         }
     }
-    my $reply = $replyto_is_fm ? $replyto : $from;
 
     return 0 unless $self->_parse_body($pms);
-    
+
     # Compare body to headers
     if (scalar keys %{$pms->{freemail_cache}{body}}) {
-        my $check = $what eq 'replyto' ? $replyto : $reply;
-        dbg("comparing $check to body freemails");
-        foreach my $email (keys %{$pms->{freemail_cache}{body}}) {
-            if ($email ne $check) {
-                dbg("HIT! $check and $email are different freemails");
-                $self->_got_hit($pms, "$check, $email", "Different freemails in reply header and body");
-                return 0;
+        my $reply_addrs = $what eq 'replyto' ? \@replyto_addrs :
+                              $replyto_is_fm ? \@replyto_addrs : \@from_addrs;
+        dbg("comparing to body freemails: ".join(", ", @$reply_addrs));
+        foreach my $body_email (keys %{$pms->{freemail_cache}{body}}) {
+            foreach my $reply_email (@$reply_addrs) {
+                if ($body_email ne $reply_email) {
+                    dbg("HIT! $reply_email (Reply) and $body_email (Body) are different freemails");
+                    $self->_test_log($pms, "$reply_email, $body_email", $rulename);
+                    return 1;
+                }
             }
         }
     }
index 93914b6b6e45e7bd2a8f144e19813be5f606ab6e..85f6e1ccb30b6eafdc4c1b6dd2eb7080010b9d12 100644 (file)
 
 =head1 NAME
 
-FromNameSpoof - perform various tests to detect spoof attempts using the From header name section
+FromNameSpoof - perform various tests to detect spoof attempts using the
+From header name section
 
 =head1 SYNOPSIS
 
 loadplugin    Mail::SpamAssassin::Plugin::FromNameSpoof
 
- # Does the From:name look like it contains an email address
- header   __PLUGIN_FROMNAME_EMAIL  eval:check_fromname_contains_email()
+ # From:name and From:addr do not match, matching depends on C<fns_check> setting
+ header  __PLUGIN_FROMNAME_SPOOF  eval:check_fromname_spoof()
+  
+ # From:name and From:addr do not match (same as above rule and C<fns_check 0>)
+ header  __PLUGIN_FROMNAME_DIFFERENT  eval:check_fromname_different()
 
- # Is the From:name different to the From:addr header
- header   __PLUGIN_FROMNAME_DIFFERENT  eval:check_fromname_different()
+ # From:name and From:addr domains differ
+ header  __PLUGIN_FROMNAME_DOMAIN_DIFFER  eval:check_fromname_domain_differ()
 
- # From:name and From:addr owners differ
- header   __PLUGIN_FROMNAME_OWNERS_DIFFER  eval:check_fromname_owners_differ()
+ # From:name looks like it contains an email address (not same as From:addr)
+ header  __PLUGIN_FROMNAME_EMAIL  eval:check_fromname_contains_email()
 
- # From:name domain differs to from header
- header   __PLUGIN_FROMNAME_DOMAIN_DIFFER  eval:check_fromname_domain_differ()
+ # From:name matches any To:addr
+ header  __PLUGIN_FROMNAME_EQUALS_TO  eval:check_fromname_equals_to()
 
- # From:name and From:address don't match and owners differ
- header   __PLUGIN_FROMNAME_SPOOF  eval:check_fromname_spoof()
-  
- # From:name address matches To:address
- header __PLUGIN_FROMNAME_EQUALS_TO  eval:check_fromname_equals_to()
+ # From:name and From:addr owners differ
+ header  __PLUGIN_FROMNAME_OWNERS_DIFFER  eval:check_fromname_owners_differ()
+
+ # From:name matches Reply-To:addr
+ header  __PLUGIN_FROMNAME_EQUALS_REPLYTO  eval:check_fromname_equals_replyto()
 
 =head1 DESCRIPTION
 
@@ -50,27 +54,31 @@ ensure minimal FPs.
 
 The plugin allows you to skip emails that have been DKIM signed by specific senders:
 
- fns_ignore_dkim googlegroups.com
 fns_ignore_dkim googlegroups.com
 
 FromNameSpoof allows for a configurable closeness when matching the From:addr and From:name,
 the closeness can be adjusted with:
 
- fns_extrachars 50
 fns_extrachars 50
 
 B<Note> that FromNameSpoof detects the "owner" of a domain by the following search:
 
- <owner>.<tld>
 <owner>.<tld>
 
-By default FromNameSpoof will ignore the TLD when testing if From:addr is spoofed.
-Default 1
+By default FromNameSpoof will ignore the TLD when comparing addresses:
 
   fns_check 1
 
 Check levels:
 
- 0 - Strict checking of From:name != From:addr
- 1 - Allow for different tlds
- 2 - Allow for different aliases but same domain
+  0 - Strict checking of From:name != From:addr
+  1 - Allow for different TLDs
+  2 - Allow for different aliases but same domain
+
+"Owner" info can also be mapped as aliases with C<fns_add_addrlist>.  For
+example, to consider "googlemail.com" as "gmail":
+
+  fns_add_addrlist (gmail) *@googlemail.com
 
 =head1 TAGS
 
@@ -93,48 +101,35 @@ use in reports, header fields, other plugins, etc.:
     Actual From:addr domain
 
   _FNSFADDROWNER_
-    Actual From:addr detected owner
+    Actual From:addr owner
 
 =head1 EXAMPLE 
 
-header   __PLUGIN_FROMNAME_SPOOF eval:check_fromname_spoof()
-header   __PLUGIN_FROMNAME_EQUALS_TO eval:check_fromname_equals_to()
-
-meta     FROMNAME_SPOOF_EQUALS_TO  (__PLUGIN_FROMNAME_SPOOF && __PLUGIN_FROMNAME_EQUALS_TO)
-describe FROMNAME_SPOOF_EQUALS_TO From:name is spoof to look like To: address
-score    FROMNAME_SPOOF_EQUALS_TO 1.2
+  header  __PLUGIN_FROMNAME_SPOOF  eval:check_fromname_spoof()
+  header  __PLUGIN_FROMNAME_EQUALS_TO  eval:check_fromname_equals_to()
+  meta     FROMNAME_SPOOF_EQUALS_TO (__PLUGIN_FROMNAME_SPOOF && __PLUGIN_FROMNAME_EQUALS_TO)
+  describe FROMNAME_SPOOF_EQUALS_TO From:name is spoof to look like To: address
+  score    FROMNAME_SPOOF_EQUALS_TO 1.2
 
 =cut
 
-use strict;
-
 package Mail::SpamAssassin::Plugin::FromNameSpoof;
-my $VERSION = 0.9;
+
+use strict;
+use warnings;
+use re 'taint';
 
 use Mail::SpamAssassin::Plugin;
-use List::Util ();
-use Mail::SpamAssassin::Util;
 
 use vars qw(@ISA);
 @ISA = qw(Mail::SpamAssassin::Plugin);
 
-sub dbg { Mail::SpamAssassin::Plugin::dbg ("FromNameSpoof: @_"); }
-
-sub uri_to_domain {
-  my ($self, $domain) = @_;
+my $VERSION = 1.0;
 
-  return unless defined $domain;
-
-  if ($Mail::SpamAssassin::VERSION <= 3.004000) {
-    Mail::SpamAssassin::Util::uri_to_domain($domain);
-  } else {
-    $self->{main}->{registryboundaries}->uri_to_domain($domain);
-  }
-}
+sub dbg { my $msg = shift; Mail::SpamAssassin::Plugin::dbg("FromNameSpoof: $msg", @_); }
 
 # constructor: register the eval rule
-sub new
-{
+sub new {
   my $class = shift;
   my $mailsaobject = shift;
 
@@ -146,13 +141,13 @@ sub new
   $self->set_config($mailsaobject->{conf});
 
   # the important bit!
-  $self->register_eval_rule("check_fromname_spoof");
-  $self->register_eval_rule("check_fromname_different");
-  $self->register_eval_rule("check_fromname_domain_differ");
-  $self->register_eval_rule("check_fromname_contains_email");
-  $self->register_eval_rule("check_fromname_equals_to");
-  $self->register_eval_rule("check_fromname_owners_differ");
-  $self->register_eval_rule("check_fromname_equals_replyto");
+  $self->register_eval_rule("check_fromname_spoof", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_fromname_different", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_fromname_domain_differ", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_fromname_contains_email", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_fromname_equals_to", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_fromname_owners_differ", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_fromname_equals_replyto", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
   return $self;
 }
 
@@ -164,14 +159,13 @@ sub set_config {
     setting => 'fns_add_addrlist',
     type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST,
     code => sub {
-      my($self, $key, $value, $line) = @_;
+      my ($self, $key, $value, $line) = @_;
       local($1,$2);
-      if ($value !~ /^ \( (.*?) \) \s+ (.*) \z/sx) {
+      if ($value !~ /^ \( (.+?) \) \s+ (.+) \z/sx) {
         return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
       }
-      my $listname = "FNS_$1";
-      $value = $2;
-      $self->{parser}->add_to_addrlist ($listname, split(/\s+/, lc($value)));
+      my $listname = "FNS_".lc($1);
+      $self->{parser}->add_to_addrlist($listname, split(/\s+/, lc $2));
       $self->{fns_addrlists}{$listname} = 1;
     }
   });
@@ -180,14 +174,13 @@ sub set_config {
     setting => 'fns_remove_addrlist',
     type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST,
     code => sub {
-      my($self, $key, $value, $line) = @_;
+      my ($self, $key, $value, $line) = @_;
       local($1,$2);
-      if ($value !~ /^ \( (.*?) \) \s+ (.*) \z/sx) {
+      if ($value !~ /^ \( (.+?) \) \s+ (.+) \z/sx) {
         return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
       }
-      my $listname = "FNS_$1";
-      $value = $2;
-      $self->{parser}->remove_from_addrlist ($listname, split (/\s+/, $value));
+      my $listname = "FNS_".lc($1);
+      $self->{parser}->remove_from_addrlist($listname, split (/\s+/, lc $2));
     }
   });
 
@@ -206,7 +199,7 @@ sub set_config {
       if ($value eq '') {
         return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
       }
-      $self->{fns_ignore_dkim}->{$_} = 1 foreach (split(/\s+/, lc($value)));
+      $self->{fns_ignore_dkim}->{$_} = 1 foreach (split(/\s+/, lc $value));
     }
   });
 
@@ -227,6 +220,16 @@ sub set_config {
     setting => 'fns_check',
     default => 1,
     type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      if ($value eq '') {
+        return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
+      }
+      if ($value !~ /^[012]$/) {
+        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+      }
+      $self->{fns_check} = $value;
+    }
   });
 
   $conf->{parser}->register_commands(\@cmds);
@@ -235,203 +238,287 @@ sub set_config {
 sub parsed_metadata {
   my ($self, $opts) = @_;
   my $pms = $opts->{permsgstatus};
-  $pms->action_depends_on_tags('DKIMDOMAIN',
-      sub { my($pms,@args) = @_;
-        $self->_check_fromnamespoof($pms);
-      }
-  );
-  1;
-}
 
-sub check_fromname_different
-{
-  my ($self, $pms) = @_;
-  $self->_check_fromnamespoof($pms);
-  return $pms->{fromname_address_different};
+  # If fns_ignore_dkim used, force wait for DKIM results
+  if (%{$pms->{conf}->{fns_ignore_dkim}}) {
+    if ($self->{main}->{local_tests_only}) {
+      dbg("local tests only, ignoring fns_ignore_dkim setting");
+    }
+    # Check that DKIM module is loaded (a bit kludgy check)
+    elsif (exists $pms->{conf}->{dkim_timeout}) {
+      # Initialize async queue, any eval calls will queue their checks
+      $pms->{fromname_async_queue} = [];
+      # Process and finish queue as soon as DKIM is ready
+      $pms->action_depends_on_tags('DKIMDOMAIN', sub {
+        $self->_check_async_queue($pms);
+      });
+    } else {
+      dbg("DKIM plugin not loaded, ignoring fns_ignore_dkim setting");
+    }
+  }
 }
 
-sub check_fromname_domain_differ
-{
-  my ($self, $pms) = @_;
+sub _check_eval {
+  my ($self, $pms, $result) = @_;
+
+  if (exists $pms->{fromname_async_queue}) {
+    my $rulename = $pms->get_current_eval_rule_name();
+    push @{$pms->{fromname_async_queue}}, sub {
+      if ($result->()) {
+        $pms->got_hit($rulename, '', ruletype => 'header');
+      } else {
+        $pms->rule_ready($rulename);
+      }
+    };
+    return; # return undef for async status
+  }
+
   $self->_check_fromnamespoof($pms);
-  return $pms->{fromname_domain_different};
+  # make sure not to return undef, as this is not async anymore
+  return $result->() || 0;
 }
 
-sub check_fromname_spoof
-{
+sub check_fromname_spoof {
   my ($self, $pms, $check_lvl) = @_;
-  $self->_check_fromnamespoof($pms);
 
-  if ( not defined $check_lvl ) {
+  # Some deprecated eval parameter, was not documented?
+  if (!defined $check_lvl || $check_lvl !~ /^[012]$/) {
     $check_lvl = $pms->{conf}->{fns_check};
   }
 
-  my @array = (
-    ($pms->{fromname_address_different}) ,
-    ($pms->{fromname_address_different} && $pms->{fromname_owner_different}) ,
-    ($pms->{fromname_address_different} && $pms->{fromname_domain_different})
-  );
+  my $result = sub {
+    my @array = (
+      ($pms->{fromname_address_different}),
+      ($pms->{fromname_address_different} && $pms->{fromname_owner_different}),
+      ($pms->{fromname_address_different} && $pms->{fromname_domain_different})
+    );
+    $array[$check_lvl];
+  };
 
-  return $array[$check_lvl];
+  return $self->_check_eval($pms, $result);
+}
+
+sub check_fromname_different {
+  my ($self, $pms) = @_;
+
+  my $result = sub {
+    $pms->{fromname_address_different};
+  };
 
+  return $self->_check_eval($pms, $result);
 }
 
-sub check_fromname_contains_email
-{
+sub check_fromname_domain_differ {
   my ($self, $pms) = @_;
-  $self->_check_fromnamespoof($pms);
-  return $pms->{fromname_contains_email};
+
+  my $result = sub {
+    $pms->{fromname_domain_different};
+  };
+
+  return $self->_check_eval($pms, $result);
 }
 
-sub check_fromname_equals_replyto
-{
+sub check_fromname_contains_email {
   my ($self, $pms) = @_;
-  $self->_check_fromnamespoof($pms);
-  return $pms->{fromname_equals_replyto};
+
+  my $result = sub {
+    $pms->{fromname_contains_email};
+  };
+
+  return $self->_check_eval($pms, $result);
 }
 
-sub check_fromname_equals_to
-{
+sub check_fromname_equals_to {
   my ($self, $pms) = @_;
-  $self->_check_fromnamespoof($pms);
-  return $pms->{fromname_equals_to_addr};
+
+  my $result = sub {
+    $pms->{fromname_equals_to_addr};
+  };
+
+  return $self->_check_eval($pms, $result);
 }
 
-sub check_fromname_owners_differ
-{
+sub check_fromname_owners_differ {
   my ($self, $pms) = @_;
-  $self->_check_fromnamespoof($pms);
-  return $pms->{fromname_owner_different};
+
+  my $result = sub {
+    $pms->{fromname_owner_different};
+  };
+
+  return $self->_check_eval($pms, $result);
 }
 
-sub _check_fromnamespoof
-{
+sub check_fromname_equals_replyto {
   my ($self, $pms) = @_;
 
-  return if (defined $pms->{fromname_contains_email});
+  my $result = sub {
+    $pms->{fromname_equals_replyto};
+  };
 
-  my $conf = $pms->{conf};
+  return $self->_check_eval($pms, $result);
+}
+
+sub check_cleanup {
+  my ($self, $opts) = @_;
+
+  $self->_check_async_queue($opts->{permsgstatus});
+}
+
+# Shall only be called when DKIMDOMAIN is ready, or from check_cleanup() to
+# make sure _check_fromnamespoof is called if DKIMDOMAIN was never set
+sub _check_async_queue {
+  my ($self, $pms) = @_;
+
+  if (exists $pms->{fromname_async_queue}) {
+    $self->_check_fromnamespoof($pms);
+    $_->() foreach (@{$pms->{fromname_async_queue}});
+    # No more async queueing needed.  If any evals are called later, they
+    # will act on the results directly.
+    delete $pms->{fromname_async_queue};
+  }
+}
+
+sub _check_fromnamespoof {
+  my ($self, $pms) = @_;
 
-  $pms->{fromname_contains_email} = 0;
-  $pms->{fromname_address_different} = 0;
-  $pms->{fromname_equals_to_addr} = 0;
-  $pms->{fromname_domain_different} = 0;
-  $pms->{fromname_owner_different} = 0;
-  $pms->{fromname_equals_replyto} = 0;
+  return if $pms->{fromname_checked};
+  $pms->{fromname_checked} = 1;
 
-  foreach my $addr (split / /, $pms->get_tag('DKIMDOMAIN') || '') {
-    if ($conf->{fns_ignore_dkim}->{lc($addr)}) {
+  my $conf = $pms->{conf};
+
+  foreach my $addr (split(/\s+/, $pms->get_tag('DKIMDOMAIN')||'')) {
+    if ($conf->{fns_ignore_dkim}->{lc $addr}) {
       dbg("ignoring, DKIM signed: $addr");
-      return 0;
+      return;
     }
   }
 
   foreach my $iheader (keys %{$conf->{fns_ignore_header}}) {
     if ($pms->get($iheader)) {
       dbg("ignoring, header $iheader found");
-      return 0 if ($pms->get($iheader));
+      return;
     }
   }
 
-  my $list_refs = {};
+  # Parse From addr
+  my $from_addr = lc $pms->get('From:addr');
+  my $from_domain = $self->{main}->{registryboundaries}->uri_to_domain("mailto:$from_addr");
+  return unless defined $from_domain;
+
+  # Parse From name
+  my $fromname = lc $pms->get('From:name');
+  # Very common to have From address cloned into name, ignore?
+  #if ($fromname eq $from_addr) {
+  #  dbg("ignoring, From-name is exactly same as From addr: $fromname");
+  #  return;
+  #}
+  my ($fromname_addr, $fromname_domain);
+  if ($fromname =~ /\b([\w\.\!\#\$\%\&\'\*\+\/\=\?\^\_\`\{\|\}\~-]+\@\w[\w-]*\.\w[\w.-]++)\b/i) {
+    $fromname_addr = $1;
+    $fromname_domain = $self->{main}->{registryboundaries}->uri_to_domain("mailto:$fromname_addr");
+    # No valid domain/TLD found? Any reason to keep testing a possibly obfuscated one?
+    if (!defined $fromname_domain) {
+      dbg("no From-name addr found");
+      return;
+    }
+    $pms->{fromname_contains_email} = 1; # check_fromname_contains_email hit
+    # Calculate "closeness" (this really needs documentation, as it's hard to understand)
+    my $nochar = ($fromname =~ y/a-z0-9//c);
+    $nochar -= ($fromname_addr =~ y/a-z0-9//c);
+    my $len = length($fromname) + $nochar - length($fromname_addr);
+    unless ($len <= $conf->{fns_extrachars}) {
+      dbg("not enough closeness for From-name/addr: $fromname <=> $fromname_addr ($len <= $conf->{fns_extrachars})");
+      return;
+    }
+  } else {
+    # No point continuing if email was not found inside name
+    dbg("no From-name addr found");
+    return;
+  }
 
+  # Parse owners
+  my $list_refs = {};
   if ($conf->{fns_addrlists}) {
     my @lists = keys %{$conf->{fns_addrlists}};
     foreach my $list (@lists) {
       $list_refs->{$list} = $conf->{$list};
     }
-    s/^FNS_// foreach (@lists);
-    dbg("using addrlists: ".join(', ', @lists));
+    dbg("using addrlists for owner aliases: ".join(', ', map { s/^FNS_//r; } @lists));
   }
+  my $fromname_owner = $self->_find_address_owner($fromname_addr, $fromname_domain, $list_refs);
+  my $from_owner = $self->_find_address_owner($from_addr, $from_domain, $list_refs);
 
-  my %fnd = ();
-  my %fad = ();
-  my %tod = ();
-
-  $fnd{'addr'} = $pms->get("From:name");
-
-  if ($fnd{'addr'} =~ /\b((?>[\w\.\!\#\$\%\&\'\*\+\/\=\?\^\_\`\{\|\}\~\-]+@[\w\-\.]+\.[\w\-\.]+))\b/i) {
-    my $nochar = ($fnd{'addr'} =~ y/A-Za-z0-9//c);
-    $nochar -= ($1 =~ y/A-Za-z0-9//c);
+  dbg("Parsed From-name addr/domain/owner: $fromname_addr/$fromname_domain/$fromname_owner");
+  dbg("Parsed From-addr addr/domain/owner: $from_addr/$from_domain/$from_owner");
 
-    return 0 unless ((length($fnd{'addr'})+$nochar) - length($1) <= $conf->{'fns_extrachars'});
-
-    $fnd{'addr'} = lc $1;
-  } else {
-    return 0;
+  if ($fromname_addr ne $from_addr) {
+    dbg("From-name addr differs from From addr: $fromname_addr != $from_addr");
+    $pms->{fromname_address_different} = 1;
+  }
+  if ($fromname_domain ne $from_domain) {
+    dbg("From-name domain differs from From domain: $fromname_domain != $from_domain");
+    $pms->{fromname_domain_different} = 1;
+  }
+  if ($fromname_owner ne $from_owner) {
+    dbg("From-name owner differs from From owner: $fromname_owner != $from_owner");
+    $pms->{fromname_owner_different} = 1;
   }
 
-  my $replyto = lc $pms->get("Reply-To:addr");
-
-  $fad{'addr'} = lc $pms->get("From:addr");
-  my @toaddrs = $pms->all_to_addrs();
-  return 0 unless @toaddrs;
-
-  $tod{'addr'} = lc $toaddrs[0];
-
-  $fnd{'domain'} = $self->uri_to_domain($fnd{'addr'});
-  $fad{'domain'} = $self->uri_to_domain($fad{'addr'});
-  $tod{'domain'} = $self->uri_to_domain($tod{'addr'});
-
-  return 0 unless (defined $fnd{'domain'} && defined $fad{'domain'});
-
-  $pms->{fromname_contains_email} = 1;
-
-  $fnd{'owner'} = $self->_find_address_owner($fnd{'addr'}, $list_refs);
-
-  $fad{'owner'} = $self->_find_address_owner($fad{'addr'}, $list_refs);
-
-  $tod{'owner'} = $self->_find_address_owner($tod{'addr'}, $list_refs);
-
-  $pms->{fromname_address_different} = 1 if ($fnd{'addr'} ne $fad{'addr'});
-
-  $pms->{fromname_domain_different} = 1 if ($fnd{'domain'} ne $fad{'domain'});
-
-  $pms->{fromname_equals_to_addr} = 1 if ($fnd{'addr'} eq $tod{addr});
-
-  $pms->{fromname_equals_replyto} = 1 if ($fnd{'addr'} eq $replyto);
+  # Check Reply-To related
+  my $replyto_addr = lc $pms->get('Reply-To:addr');
+  if ($fromname_addr eq $replyto_addr) {
+    dbg("From-name addr is same as Reply-To addr: $fromname_addr");
+    $pms->{fromname_equals_replyto} = 1;
+  }
 
-  if ($fnd{'owner'} ne $fad{'owner'}) {
-    $pms->{fromname_owner_different} = 1;
+  # Check To related
+  foreach my $to_addr ($pms->all_to_addrs()) {
+    if ($fromname_addr eq $to_addr) {
+      dbg("From-name addr is same as To addr: $fromname_addr");
+      $pms->{fromname_equals_to_addr} = 1;
+      last;
+    }
   }
 
-  if ($pms->{fromname_address_different}) {
-    $pms->set_tag("FNSFNAMEADDR", $fnd{'addr'});
-    $pms->set_tag("FNSFADDRADDR", $fad{'addr'});
-    $pms->set_tag("FNSFNAMEOWNER", $fnd{'owner'});
-    $pms->set_tag("FNSFADDROWNER", $fad{'owner'});
-    $pms->set_tag("FNSFNAMEDOMAIN", $fnd{'domain'});
-    $pms->set_tag("FNSFADDRDOMAIN", $fad{'domain'});
-
-    dbg("From name spoof: $fnd{addr} $fnd{domain} $fnd{owner}");
-    dbg("Actual From: $fad{addr} $fad{domain} $fad{owner}");
-    dbg("To Address: $tod{addr} $tod{domain} $tod{owner}");
+  # Set tags
+  if ($pms->{fromname_address_different} || $pms->{fromname_owner_different}) {
+    $pms->set_tag("FNSFNAMEADDR", $fromname_addr);
+    $pms->set_tag("FNSFNAMEDOMAIN", $fromname_domain);
+    $pms->set_tag("FNSFNAMEOWNER", $fromname_owner);
+    $pms->set_tag("FNSFADDRADDR", $from_addr);
+    $pms->set_tag("FNSFADDRDOMAIN", $from_domain);
+    $pms->set_tag("FNSFADDROWNER", $from_owner);
   }
 }
 
-sub _find_address_owner
-{
-  my ($self, $check, $list_refs) = @_;
+sub _find_address_owner {
+  my ($self, $addr, $addr_domain, $list_refs) = @_;
+
+  # Check fns addrlist first for user defined mapping
   foreach my $owner (keys %{$list_refs}) {
-    foreach my $white_addr (keys %{$list_refs->{$owner}}) {
-      my $regexp = qr/$list_refs->{$owner}{$white_addr}/i;
-      if ($check =~ /$regexp/)  {
-        $owner =~ s/^FNS_//i;
+    foreach my $listaddr (keys %{$list_refs->{$owner}}) {
+      if ($addr =~ $list_refs->{$owner}{$listaddr}) {
+        $owner =~ s/^FNS_//;
         return lc $owner;
       }
     }
   }
 
-  my $owner = $self->uri_to_domain($check);
-
-  $check =~ /^([^\@]+)\@(.*)$/;
-
-  if ($owner ne $2) {
-    return $self->_find_address_owner("$1\@$owner", $list_refs);
+  # If we have subdomain addr foo.bar@sub.domain.com,
+  # this will try to recheck foo.bar@domain.com from addrlist
+  local($1,$2);
+  if ($addr =~ /^([^\@]+)\@(.+)$/) {
+    if ($2 ne $addr_domain) {
+      return $self->_find_address_owner("$1\@$addr_domain", $addr_domain, $list_refs);
+    }
   }
 
-  $owner =~ /^([^\.]+)\./;
-  return lc $1;
+  # Grab the first component of TLD
+  if ($addr_domain =~ /^([^.]+)\./) {
+    return $1;
+  } else {
+    return $addr_domain;
+  }
 }
 
 1;
index 01175c2b520d2d9654f9ccfdb5054d6411290bc3..4974b06905132d2f3256ee297dc1e56bf1e4da9f 100644 (file)
@@ -39,18 +39,18 @@ sub new {
   bless ($self, $class);
 
   # the important bit!
-  $self->register_eval_rule("html_tag_balance");
-  $self->register_eval_rule("html_image_only");
-  $self->register_eval_rule("html_image_ratio");
-  $self->register_eval_rule("html_charset_faraway");
-  $self->register_eval_rule("html_tag_exists");
-  $self->register_eval_rule("html_test");
-  $self->register_eval_rule("html_eval");
-  $self->register_eval_rule("html_text_match");
-  $self->register_eval_rule("html_title_subject_ratio");
-  $self->register_eval_rule("html_text_not_match");
-  $self->register_eval_rule("html_range");
-  $self->register_eval_rule("check_iframe_src");
+  $self->register_eval_rule("html_tag_balance", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("html_image_only", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("html_image_ratio", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("html_charset_faraway", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("html_tag_exists", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("html_test", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("html_eval", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("html_text_match", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("html_title_subject_ratio", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("html_text_not_match", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("html_range", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("check_iframe_src", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
 
   return $self;
 }
@@ -61,65 +61,88 @@ sub html_tag_balance {
   return 0 if $rawtag !~ /^([a-zA-Z0-9]+)$/;
   my $tag = $1;
 
-  return 0 unless exists $pms->{html}{inside}{$tag};
-
   return 0 if $rawexpr !~ /^([\<\>\=\!\-\+ 0-9]+)$/;
   my $expr = untaint_var($1);
 
-  $pms->{html}{inside}{$tag} =~ /^([\<\>\=\!\-\+ 0-9]+)$/;
-  my $val = untaint_var($1);
+  foreach my $html (@{$pms->{html_all}}) {
+    next unless exists $html->{inside}{$tag};
+    $html->{inside}{$tag} =~ /^([\<\>\=\!\-\+ 0-9]+)$/;
+    my $val = untaint_var($1);
+    return 1 if eval "\$val $expr";
+  }
 
-  return eval "\$val $expr";
+  return 0;
 }
 
 sub html_image_only {
   my ($self, $pms, undef, $min, $max) = @_;
 
-  return (exists $pms->{html}{inside}{img} &&
-         exists $pms->{html}{length} &&
-         $pms->{html}{length} > $min &&
-         $pms->{html}{length} <= $max);
+  foreach my $html (@{$pms->{html_all}}) {
+    if (exists $html->{inside}{img} && exists $html->{length} &&
+        $html->{length} > $min && $html->{length} <= $max)
+    {
+      return 1;
+    }
+  }
+
+  return 0;
 }
 
 sub html_image_ratio {
   my ($self, $pms, undef, $min, $max) = @_;
 
-  return 0 unless (exists $pms->{html}{non_space_len} &&
-                  exists $pms->{html}{image_area} &&
-                  $pms->{html}{image_area} > 0);
-  my $ratio = $pms->{html}{non_space_len} / $pms->{html}{image_area};
-  return ($ratio > $min && $ratio <= $max);
+  foreach my $html (@{$pms->{html_all}}) {
+    next unless (exists $html->{non_space_len} &&
+                 exists $html->{image_area} &&
+                 $html->{image_area} > 0);
+    my $ratio = $html->{non_space_len} / $html->{image_area};
+    return 1 if $ratio > $min && $ratio <= $max;
+  }
+
+  return 0;
 }
 
 sub html_charset_faraway {
   my ($self, $pms) = @_;
 
-  return 0 unless exists $pms->{html}{charsets};
-
   my @locales = Mail::SpamAssassin::Util::get_my_locales($pms->{conf}->{ok_locales});
   return 0 if grep { $_ eq "all" } @locales;
 
-  my $okay = 0;
-  my $bad = 0;
-  for my $c (split(' ', $pms->{html}{charsets})) {
-    if (Mail::SpamAssassin::Locales::is_charset_ok_for_locales($c, @locales)) {
-      $okay++;
-    }
-    else {
-      $bad++;
+  foreach my $html (@{$pms->{html_all}}) {
+    next unless exists $html->{charsets};
+    my $okay = 0;
+    my $bad = 0;
+    foreach my $c (split(/\s+/, $html->{charsets})) {
+      if (Mail::SpamAssassin::Locales::is_charset_ok_for_locales($c, @locales)) {
+        $okay++;
+      } else {
+        $bad++;
+      }
     }
+    return 1 if $bad && $bad >= $okay;
   }
-  return ($bad && ($bad >= $okay));
+
+  return 0;
 }
 
 sub html_tag_exists {
   my ($self, $pms, undef, $tag) = @_;
-  return exists $pms->{html}{inside}{$tag};
+
+  foreach my $html (@{$pms->{html_all}}) {
+    return 1 if exists $html->{inside}{$tag};
+  }
+
+  return 0;
 }
 
 sub html_test {
   my ($self, $pms, undef, $test) = @_;
-  return $pms->{html}{$test};
+
+  foreach my $html (@{$pms->{html_all}}) {
+    return 1 if $html->{$test};
+  }
+
+  return 0;
 }
 
 sub html_eval {
@@ -128,29 +151,38 @@ sub html_eval {
   return 0 if $rawexpr !~ /^([\<\>\=\!\-\+ 0-9]+)$/;
   my $expr = untaint_var($1);
 
-  # workaround bug 3320: weird perl bug where additional, very explicit
-  # untainting into a new var is required.
-  my $tainted = $pms->{html}{$test};
-  return 0 unless defined($tainted);
-  my $val = $tainted;
+  foreach my $html (@{$pms->{html_all}}) {
+    # workaround bug 3320: weird perl bug where additional, very explicit
+    # untainting into a new var is required.
+    my $tainted = $html->{$test};
+    next unless defined($tainted);
+    my $val = $tainted;
+    # just use the value in $val, don't copy it needlessly
+    return 1 if eval "\$val $expr";
+  }
 
-  # just use the value in $val, don't copy it needlessly
-  return eval "\$val $expr";
+  return 0;
 }
 
 sub html_text_match {
   my ($self, $pms, undef, $text, $regexp) = @_;
+
   my ($rec, $err) = compile_regexp($regexp, 0);
   if (!$rec) {
     warn "htmleval: html_text_match invalid regexp '$regexp': $err";
     return 0;
   }
-  foreach my $string (@{$pms->{html}{$text}}) {
-    next unless defined $string;
-    if ($string =~ $rec) {
-      return 1;
+
+  foreach my $html (@{$pms->{html_all}}) {
+    next unless ref($html->{$text}) eq 'ARRAY';
+    foreach my $string (@{$html->{$text}}) {
+      next unless defined $string;
+      if ($string =~ $rec) {
+        return 1;
+      }
     }
   }
+
   return 0;
 }
 
@@ -161,53 +193,73 @@ sub html_title_subject_ratio {
   if ($subject eq '') {
     return 0;
   }
-  my $max = 0;
-  for my $string (@{ $pms->{html}{title} }) {
-    if ($string) {
-      my $ratio = length($string) / length($subject);
-      $max = $ratio if $ratio > $max;
+
+  foreach my $html (@{$pms->{html_all}}) {
+    my $max = 0;
+    foreach my $string (@{$html->{title}}) {
+      if ($string) {
+        my $ratio_s = length($string) / length($subject);
+        $max = $ratio_s if $ratio_s > $max;
+      }
     }
+    return 1 if $max > $ratio;
   }
-  return $max > $ratio;
+
+  return 0;
 }
 
 sub html_text_not_match {
   my ($self, $pms, undef, $text, $regexp) = @_;
-  for my $string (@{ $pms->{html}{$text} }) {
-    if (defined $string && $string !~ /${regexp}/) {
-      return 1;
+
+  my ($rec, $err) = compile_regexp($regexp, 0);
+  if (!$rec) {
+    warn "htmleval: html_text_not_match invalid regexp '$regexp': $err";
+    return 0;
+  }
+
+  foreach my $html (@{$pms->{html_all}}) {
+    next unless ref($html->{$text}) eq 'ARRAY';
+    foreach my $string (@{$html->{$text}}) {
+      if (defined $string && $string !~ $rec) {
+        return 1;
+      }
     }
   }
+
   return 0;
 }
 
 sub html_range {
   my ($self, $pms, undef, $test, $min, $max) = @_;
 
-  return 0 unless exists $pms->{html}{$test};
-
-  $test = $pms->{html}{$test};
-
-  # not all perls understand what "inf" means, so we need to do
-  # non-numeric tests!  urg!
-  if (!defined $max || $max eq "inf") {
-    return ($test eq "inf") ? 1 : ($test > $min);
-  }
-  elsif ($test eq "inf") {
-    # $max < inf, so $test == inf means $test > $max
-    return 0;
-  }
-  else {
-    # if we get here everything should be a number
-    return ($test > $min && $test <= $max);
+  foreach my $html (@{$pms->{html_all}}) {
+    next unless defined $html->{$test};
+    my $value = $html->{$test};
+    # not all perls understand what "inf" means, so we need to do
+    # non-numeric tests!  urg!
+    if (!defined $max || $max eq "inf") {
+      return 1 if $value > $min;
+    }
+    elsif ($value eq "inf") {
+      # $max < inf, so $value == inf means $value > $max
+      next;
+    }
+    else {
+      # if we get here everything should be a number
+      return 1 if $value > $min && $value <= $max;
+    }
   }
+
+  return 0;
 }
 
 sub check_iframe_src {
   my ($self, $pms) = @_;
 
-  foreach my $v ( values %{$pms->{html}->{uri_detail}} ) {
-    return 1 if $v->{types}->{iframe};
+  foreach my $html (@{$pms->{html_all}}) {
+    foreach my $v (values %{$html->{uri_detail}}) {
+      return 1 if $v->{types}->{iframe};
+    }
   }
 
   return 0;
index 8251159f4fbefaee3a564541bade6144283aa15f..36cc7a6b6077ed04bab5a00b92c17447521e3371 100644 (file)
@@ -38,7 +38,7 @@ sub new {
   bless ($self, $class);
 
   # the important bit!
-  $self->register_eval_rule ("check_https_http_mismatch");
+  $self->register_eval_rule ("check_https_http_mismatch", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
 
   return $self;
 }
@@ -47,64 +47,62 @@ sub new {
 # ("<" and ">" replaced with "[lt]" and "[gt]" to avoid Kaspersky Desktop AV
 # false positive ;)
 sub check_https_http_mismatch {
-  my ($self, $permsgstatus, undef, $minanchors, $maxanchors) = @_;
-
-  my $IP_ADDRESS = IP_ADDRESS;
+  my ($self, $pms, undef, $minanchors, $maxanchors) = @_;
 
   $minanchors ||= 1;
 
-  if (!exists $permsgstatus->{chhm_hit}) {
-    $permsgstatus->{chhm_hit} = 0;
-    $permsgstatus->{chhm_anchors} = 0;
+  foreach my $html (@{$pms->{html_all}}) {
+    my $hit = 0;
+    my $anchors = 0;
+    foreach my $k (keys %{$html->{uri_detail}}) {
+      my $v = $html->{uri_detail}->{$k};
 
-    foreach my $k ( keys %{$permsgstatus->{html}->{uri_detail}} ) {
-      my %uri_detail = %{$permsgstatus->{html}->{uri_detail}};
-      my $v = ${uri_detail}{$k};
       # if the URI wasn't used for an anchor tag, or the anchor text didn't
       # exist, skip this.
-      next unless (exists $v->{anchor_text} && @{$v->{anchor_text}});
+      next unless exists $v->{anchor_text} && @{$v->{anchor_text}};
 
       my $uri;
-      if ($k =~ m@^https?://([^/:]+)@i) {
+      if ($k =~ m@^https?://([^/:?#]+)@i) {
         $uri = $1;
         # Skip IPs since there's another rule to catch that already
-        if ($uri =~ /^$IP_ADDRESS+$/) {
-          undef $uri;
+        if ($uri =~ IS_IP_ADDRESS) {
+          $uri = undef;
           next;
         } 
         # want to compare whole hostnames instead of domains?
         # comment this next section to the blank line.
         $uri = $self->{main}->{registryboundaries}->trim_domain($uri);
-        undef $uri unless ($self->{main}->{registryboundaries}->is_domain_valid($uri));
+        my $domain = $self->{main}->{registryboundaries}->uri_to_domain($uri);
+        $uri = undef  unless $self->{main}->{registryboundaries}->is_domain_valid($domain);
       }
-
       next unless $uri;
-      $permsgstatus->{chhm_anchors}++ if exists $v->{anchor_text};
 
+      $anchors++ if exists $v->{anchor_text};
       foreach (@{$v->{anchor_text}}) {
-        if (m@https://([^/:]+)@i) {
+        if (m@https://([^\s/:?#]+)@i) {
           my $https = $1;
 
          # want to compare whole hostnames instead of domains?
          # comment this next section to the blank line.
-          if ($https !~ /^$IP_ADDRESS+$/) {
+          if ($https !~ IS_IP_ADDRESS) {
            $https = $self->{main}->{registryboundaries}->trim_domain($https);
-            undef $https unless ($self->{main}->{registryboundaries}->is_domain_valid($https));
+            $https = undef  unless $self->{main}->{registryboundaries}->is_domain_valid($https);
           }
          next unless $https;
-
          dbg("https_http_mismatch: domains $uri -> $https");
-
          next if $uri eq $https;
-         $permsgstatus->{chhm_hit} = 1;
+         $hit = 1;
          last;
         }
       }
     }
-    dbg("https_http_mismatch: anchors ".$permsgstatus->{chhm_anchors});
+
+    dbg("https_http_mismatch: anchors $anchors");
+    return 1 if $hit && $anchors >= $minanchors &&
+                (!defined $maxanchors || $anchors < $maxanchors);
   }
 
-  return ( $permsgstatus->{chhm_hit} && $permsgstatus->{chhm_anchors} >= $minanchors && (defined $maxanchors && $permsgstatus->{chhm_anchors} < $maxanchors) );
+  return 0;
 }
 
 1;
index c7fdcda7a32d14493f854ece5ba8aceee66a73a3..68d1cd69497ca81a9d62ebc983da1ecfff944716 100644 (file)
@@ -28,59 +28,68 @@ HashBL - query hashed (and unhashed) DNS blocklists
 
   header   HASHBL_EMAIL eval:check_hashbl_emails('ebl.example.invalid')
   describe HASHBL_EMAIL Message contains email address found on EBL
-  priority HASHBL_EMAIL -100 # required priority to launch async lookups early
   tflags   HASHBL_EMAIL net
 
+  # rewrite googlemail.com -> gmail.com, applied before acl/welcomelist
+  hashbl_email_domain_alias gmail.com googlemail.com
+  # only query gmail.com addresses
   hashbl_acl_freemail gmail.com
   header   HASHBL_OSENDR eval:check_hashbl_emails('rbl.example.invalid/A', 'md5/max=10/shuffle', 'X-Original-Sender', '^127\.', 'freemail')
   describe HASHBL_OSENDR Message contains email address found on HASHBL
-  priority HASHBL_OSENDR -100 # required priority to launch async lookups early
   tflags   HASHBL_OSENDR net
 
   body     HASHBL_BTC eval:check_hashbl_bodyre('btcbl.example.invalid', 'sha1/max=10/shuffle', '\b([13][a-km-zA-HJ-NP-Z1-9]{25,34})\b')
   describe HASHBL_BTC Message contains BTC address found on BTCBL
-  priority HASHBL_BTC -100 # required priority to launch async lookups early
   tflags   HASHBL_BTC net
 
-  header   HASHBL_URI eval:check_hashbl_uris('rbl.example.invalid', 'sha1', '127.0.0.32')
+  header   HASHBL_URI eval:check_hashbl_uris('rbl.example.invalid', 'sha1', '^127\.0\.0\.32$')
   describe HASHBL_URI Message contains uri found on rbl
-  priority HASHBL_URI -100 # required priority to launch async lookups early
   tflags   HASHBL_URI net
 
+  body     HASHBL_ATTACHMENT eval:check_hashbl_attachments('attbl.example.invalid', 'sha256')
+  describe HASHBL_ATTACHMENT Message contains attachment found on attbl
+  tflags   HASHBL_ATTACHMENT net
+
+  # Capture tag using SA 4.0 regex named capture feature
+  header   __X_SOME_ID X-Some-ID =~ /^(?<XSOMEID>\d{10,20})$/
+  # Query the tag value as is from a DNSBL
+  header   HASHBL_TAG eval:check_hashbl_tag('idbl.example.invalid/A', 'raw', 'XSOMEID', '^127\.')
+
 =head1 DESCRIPTION
 
-This plugin support multiple types of hashed or unhashed DNS blocklists.
+This plugin supports multiple types of hashed or unhashed DNS blocklist queries.
 
-OPTS refers to multiple generic options:
+=over 4
 
-  raw      do not hash data, query as is
+=item Common OPTS that apply to all functions:
+
+  raw      no hashing, query as is (can break if value is not valid DNS label)
   md5      hash query with MD5
   sha1     hash query with SHA1
+  sha256   hash query with Base32 encoded SHA256
   case     keep case before hashing, default is to lowercase
-  max=x           maximum number of queries
+  max=x           maximum number of queries (defaults to 10 if not specified)
   shuffle  if max exceeded, random shuffle queries before truncating to limit
 
-Multiple options can be separated with slash or other non-word character.
-If OPTS is empty ('') or missing, default is used.
+Multiple options can be separated with slash.
 
-HEADERS refers to slash separated list of Headers to process:
-
-  ALL           all headers
-  ALLFROM       all From headers as returned by $pms->all_from_addrs()
-  EnvelopeFrom  message envelope from (Return-Path etc)
-  HeaderName    any header as used with $pms->get()
+When rule OPTS is empty ('') or missing, default is used as documented by
+each query type.  If any options are defined, then all needed options must
+be explicitly defined.
 
-if HEADERS is empty ('') or missing, default is used.
+=back 
 
 =over 4
 
-=item header RULE check_hashbl_emails('bl.example.invalid/A', 'OPTS', 'HEADERS/body', '^127\.')
+=item header RULE check_hashbl_emails('bl.example.invalid/A', 'OPTS', 'HEADERS', '^127\.')
 
-Check email addresses from DNS list, "body" can be specified along with
-headers to search body for emails.  Optional subtest regexp to match DNS
-answer.  Note that eval rule type must always be "header".
+Check email addresses from DNS list.  Note that "body" can be specified
+along with headers to search message body for emails.  Rule type must always
+be "header".
 
-DNS query type can be appended to list with /A (default) or /TXT.
+Optional DNS query type can be appended to list with /A (default) or /TXT.
+
+Default OPTS: sha1/notag/noquote/max=10/shuffle
 
 Additional supported OPTS:
 
@@ -88,37 +97,135 @@ Additional supported OPTS:
   notag    strip username tags from email
   nouri    ignore emails inside uris
   noquote  ignore emails inside < > or possible quotings
-
-Default OPTS: sha1/notag/noquote/max=10/shuffle
+  user     query userpart of email only
+  host     query hostpart of email only
+  domain   query domain of email only (hostpart+trim_domain)
 
 Default HEADERS: ALLFROM/Reply-To/body
 
-For existing public email blacklist, see: http://msbl.org/ebl.html
+HEADERS refers to slash separated list of Headers to process:
+
+  ALL           all headers
+  ALLFROM       all From headers as returned by $pms->all_from_addrs()
+  EnvelopeFrom  message envelope from (Return-Path etc)
+  <HeaderName>  any header as used with header rules or $pms->get()
+  body          all emails found in message body
+
+If HEADERS is empty ('') or missing, default is used.
+
+Optional subtest regexp to match DNS answer (default: '^127\.').
 
-  # Working example, see http://msbl.org/ebl.html before usage
+For existing public email blocklist, see: http://msbl.org/ebl.html
+
+  # Working example, see https://msbl.org/ebl.html before usage
   header   HASHBL_EMAIL eval:check_hashbl_emails('ebl.msbl.org')
   describe HASHBL_EMAIL Message contains email address found on EBL
-  priority HASHBL_EMAIL -100 # required priority to launch async lookups early
   tflags   HASHBL_EMAIL net
 
+Default regex for matching and capturing emails can be overridden with
+C<hashbl_email_regex>.  Likewise, the default welcomelist can be changed with
+C<hashbl_email_welcomelist>.  Only change if you know what you are doing, see
+plugin source code for the defaults.  Example: hashbl_email_regex \S+@\S+.com
+
+=back
+
 =over 4
 
 =item header RULE check_hashbl_uris('bl.example.invalid/A', 'OPTS', '^127\.')
 
-Check uris from DNS list, optional subtest regexp to match DNS
-answer.
+Check all URIs parsed from message from DNS list.
 
-DNS query type can be appended to list with /A (default) or /TXT.
+Optional DNS query type can be appended to list with /A (default) or /TXT.
 
 Default OPTS: sha1/max=10/shuffle
 
+Optional subtest regexp to match DNS answer (default: '^127\.').
+
 =back
 
-=item body RULE check_hashbl_bodyre('bl.example.invalid/A', 'OPTS', '\b(match)\b', '^127\.')
+=over 4
+
+=item [raw]body RULE check_hashbl_bodyre('bl.example.invalid/A', 'OPTS', '\b(match)\b', '^127\.')
 
 Search body for matching regexp and query the string captured.  Regexp must
-have a single capture ( ) for the string ($1).  Optional subtest regexp to
-match DNS answer.  Note that eval rule type must be "body" or "rawbody".
+have a single capture ( ) for the string ($1).  Rule type must be "body" or
+"rawbody".
+
+Optional DNS query type can be appended to list with /A (default) or /TXT.
+
+Default OPTS: sha1/max=10/shuffle
+
+Additional supported OPTS:
+
+  num      remove the chars from the match that are not numbers
+
+Optional subtest regexp to match DNS answer (default: '^127\.').
+
+=back
+
+=over 4
+
+=item header RULE check_hashbl_tag('bl.example.invalid/A', 'OPTS', 'TAGNAME', '^127\.')
+
+Query value of SpamAssassin tag _TAGNAME_ from DNS list.
+
+Optional DNS query type can be appended to list with /A (default) or /TXT.
+
+Default OPTS: sha1/max=10/shuffle
+
+Additional supported OPTS:
+
+  ip        only query if value is valid IPv4/IPv6 address
+  ipv4      only query if value is valid IPv4 address
+  ipv6      only query if value is valid IPv6 address
+  revip     reverse IP before query
+  fqdn      only query if value is valid FQDN (is_fqdn_valid)
+  tld       only query if value has valid TLD (is_domain_valid)
+  trim      trim name from hostname to domain (trim_domain)
+
+  If both ip/ipv4/ipv6 and fqdn/tld are enabled, only either of them is
+  required to match.  Both fqdn and tld are needed for complete FQDN+TLD
+  check.
+
+Optional subtest regexp to match DNS answer (default: '^127\.').
+
+=back
+
+=over 4
+
+=item header RULE check_hashbl_attachments('bl.example.invalid/A', 'OPTS', '^127\.')
+
+Check all all message attachments (mimeparts) from DNS list.
+
+Optional DNS query type can be appended to list with /A (default) or /TXT.
+
+Default OPTS: sha1/max=10/shuffle
+
+Additional supported OPTS:
+
+  minsize=x  skip any parts smaller than x bytes
+  maxsize=x  skip any parts larger than x bytes
+
+Optional subtest regexp to match DNS answer (default: '^127\.').
+
+Specific attachment filenames can be skipped with C<hashbl_ignore>.  For
+example "hashbl_ignore safe.pdf".
+
+Specific mime types can be skipped with C<hashbl_ignore>.  For example
+"hashbl_ignore text/plain".
+
+=back
+
+=over 4
+
+=item hashbl_ignore value [value...]
+
+Skip any type of query, if either the hash or original value (email for
+example) matches.  Multiple values can be defined, separated by whitespace. 
+Matching is case-insensitive.
+
+Any host or its domain part matching uridnsbl_skip_domains is also ignored
+by default.
 
 =back
 
@@ -127,21 +234,21 @@ match DNS answer.  Note that eval rule type must be "body" or "rawbody".
 package Mail::SpamAssassin::Plugin::HashBL;
 use strict;
 use warnings;
+use re 'taint';
 
 my $VERSION = 0.101;
 
 use Digest::MD5 qw(md5_hex);
-use Digest::SHA qw(sha1_hex);
+use Digest::SHA qw(sha1_hex sha256);
 
 use Mail::SpamAssassin::Plugin;
-use Mail::SpamAssassin::Util qw(compile_regexp);
+use Mail::SpamAssassin::Constants qw(:ip);
+use Mail::SpamAssassin::Util qw(compile_regexp is_fqdn_valid reverse_ip_address
+                                base32_encode);
 
 our @ISA = qw(Mail::SpamAssassin::Plugin);
 
-sub dbg {
-  my $msg = shift;
-  Mail::SpamAssassin::Plugin::dbg("HashBL: $msg", @_);
-}
+sub dbg { my $msg = shift; Mail::SpamAssassin::Plugin::dbg("HashBL: $msg", @_); }
 
 sub new {
   my ($class, $mailsa) = @_;
@@ -158,9 +265,16 @@ sub new {
     $self->{hashbl_available} = 1;
   }
 
-  $self->register_eval_rule("check_hashbl_emails");
-  $self->register_eval_rule("check_hashbl_uris");
-  $self->register_eval_rule("check_hashbl_bodyre");
+  $self->{evalfuncs} = {
+    'check_hashbl_emails' => $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS,
+    'check_hashbl_uris' => $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS,
+    'check_hashbl_bodyre' => $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS,
+    'check_hashbl_tag' => $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS,
+    'check_hashbl_attachments' => $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS,
+  };
+  while (my ($func, $type) = each %{$self->{evalfuncs}}) {
+    $self->register_eval_rule($func, $type);
+  }
   $self->set_config($mailsa->{conf});
 
   return $self;
@@ -186,60 +300,120 @@ sub set_config {
     }
   });
 
-  $conf->{parser}->register_commands(\@cmds);
-}
-
-sub _parse_args {
-    my ($self, $acl) = @_;
-
-    if (not defined $acl) {
-      return ();
-    }
-    $acl =~ s/\s+//g;
-    if ($acl !~ /^[a-z0-9]{1,32}$/) {
-        warn("invalid acl name: $acl");
-        return ();
+  push (@cmds, {
+    setting => 'hashbl_email_domain_alias',
+    is_admin => 1,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE,
+    default => {},
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      if (!defined $value || $value eq '') {
+        return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
+      }
+      my @vals = split(/\s+/, lc $value);
+      if (@vals < 2 || index($value, '@') >= 0) {
+        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+      }
+      my $domain = shift @vals;
+      foreach my $alias (@vals) {
+        $self->{hashbl_email_domain_alias}->{$alias} = $domain;
+      }
     }
-    if ($acl eq 'all') {
-        return ();
+  });
+
+  push (@cmds, {
+    setting => 'hashbl_email_regex',
+    is_admin => 1,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
+    # Some regexp tips courtesy of http://www.regular-expressions.info/email.html
+    # full email regex v0.02
+    default => qr/(?i)
+      (?=.{0,64}\@)                            # limit userpart to 64 chars (and speed up searching?)
+      (?<![a-z0-9!#\$%&'*+\/=?^_`{|}~-])       # start boundary
+      (                                                # capture email
+      [a-z0-9!#\$%&'*+\/=?^_`{|}~-]+           # no dot in beginning
+      (?:\.[a-z0-9!#\$%&'*+\/=?^_`{|}~-]+)*    # no consecutive dots, no ending dot
+      \@
+      (?:[a-z0-9](?:[a-z0-9-]{0,59}[a-z0-9])?\.){1,4} # max 4x61 char parts (should be enough?)
+      _TLDS_ # ends with valid tld, _TLDS_ is template which will be replaced in finish_parsing_end()
+      )
+    /x,
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      if (!defined $value || $value eq '') {
+        return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
+      }
+      my ($rec, $err) = compile_regexp($value, 0);
+      if (!$rec) {
+        dbg("config: invalid hashbl_email_regex '$value': $err");
+        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+      }
+      $self->{hashbl_email_regex} = $rec;
     }
-    if (defined $self->{hashbl_acl}{$acl}) {
-        warn("no such acl defined: $acl");
-        return ();
+  });
+
+  push (@cmds, {
+    setting => 'hashbl_email_welcomelist',
+    aliases => ['hashbl_email_whitelist'], # removed in 4.1
+    is_admin => 1,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
+    default => qr/(?i)
+      ^(?:
+          abuse|support|sales|info|helpdesk|contact|kontakt
+        | (?:post|host|domain)master
+        | undisclosed.*                     # yahoo.com etc(?)
+        | request-[a-f0-9]{16}              # live.com
+        | bounced?-                         # yahoo.com etc
+        | [a-f0-9]{8}(?:\.[a-f0-9]{8}|-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}) # gmail msgids?
+        | .+=.+=.+                          # gmail forward
+      )\@
+    /x,
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      if (!defined $value || $value eq '') {
+      }
+      my ($rec, $err) = compile_regexp($value, 0);
+      if (!$rec) {
+        dbg("config: invalid hashbl_email_welcomelist '$value': $err");
+        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+      }
+      $self->{hashbl_email_welcomelist} = $rec;
     }
+  });
+
+  $conf->{parser}->register_commands(\@cmds);
 }
 
 sub parse_config {
-    my ($self, $opt) = @_;
-
-    if ($opt->{key} =~ /^hashbl_acl_([a-z0-9]{1,32})$/i) {
-        $self->inhibit_further_callbacks();
-        return 1 unless $self->{hashbl_available};
-
-        my $acl = lc($1);
-        my @opts = split(/\s+/, $opt->{value});
-        foreach my $tmp (@opts)
-        {
-            if ($tmp =~ /^(\!)?(\S+)$/i) {
-                my $neg = $1;
-                my $value = lc($2);
-
-                if (defined $neg) {
-                    $self->{hashbl_acl}{$acl}{$value} = 0;
-                } else {
-                    next if $acl eq 'all';
-                    # exclusions overrides
-                    if ( not defined $self->{hashbl_acl}{$acl}{$value} ) {
-                      $self->{hashbl_acl}{$acl}{$value} = 1
-                    }
-                }
-            } else {
-                warn("invalid acl: $tmp");
-            }
+  my ($self, $opt) = @_;
+
+  if ($opt->{key} =~ /^hashbl_acl_([a-z0-9]{1,32})$/i) {
+    $self->inhibit_further_callbacks();
+    return 1 unless $self->{hashbl_available};
+
+    my $acl = lc($1);
+    my @opts = split(/\s+/, $opt->{value});
+    foreach my $tmp (@opts) {
+      if ($tmp =~ /^(\!)?(\S+)$/i) {
+        my $neg = $1;
+        my $value = lc($2);
+        if (defined $neg) {
+          $self->{hashbl_acl}{$acl}{$value} = 0;
+        } else {
+          next if $acl eq 'all';
+          # exclusions overrides
+          if (!defined $self->{hashbl_acl}{$acl}{$value}) {
+            $self->{hashbl_acl}{$acl}{$value} = 1
+          }
         }
-        return 1;
+      } else {
+        warn("invalid acl: $tmp");
+      }
     }
-    return 0;
+    return 1;
+  }
+
+  return 0;
 }
 
 sub finish_parsing_end {
@@ -249,67 +423,43 @@ sub finish_parsing_end {
 
   # valid_tlds_re will be available at finish_parsing_end, compile it now,
   # we only need to do it once and before possible forking
-  if (!exists $self->{email_re}) {
-    $self->_init_email_re();
-  }
+  # replace _TLDS_ with valid list of TLDs
+  $opts->{conf}->{hashbl_email_regex} =~ s/_TLDS_/$self->{main}->{registryboundaries}->{valid_tlds_re}/g;
+  #dbg("hashbl_email_regex: $opts->{conf}->{hashbl_email_regex}");
+  $opts->{conf}->{hashbl_email_welcomelist} =~ s/_TLDS_/$self->{main}->{registryboundaries}->{valid_tlds_re}/g;
+  #dbg("hashbl_email_welcomelist: $opts->{conf}->{hashbl_email_regex}");
 
   return 0;
 }
 
-sub _init_email_re {
-  my ($self) = @_;
-
-  # Some regexp tips courtesy of http://www.regular-expressions.info/email.html
-  # full email regex v0.02
-  $self->{email_re} = qr/
-    (?=.{0,64}\@)                      # limit userpart to 64 chars (and speed up searching?)
-    (?<![a-z0-9!#\$%&'*+\/=?^_`{|}~-]) # start boundary
-    (                                  # capture email
-    [a-z0-9!#\$%&'*+\/=?^_`{|}~-]+     # no dot in beginning
-    (?:\.[a-z0-9!#\$%&'*+\/=?^_`{|}~-]+)* # no consecutive dots, no ending dot
-    \@
-    (?:[a-z0-9](?:[a-z0-9-]{0,59}[a-z0-9])?\.){1,4} # max 4x61 char parts (should be enough?)
-    $self->{main}->{registryboundaries}->{valid_tlds_re} # ends with valid tld
-    )
-  /xi;
-
-  # default email whitelist
-  $self->{email_whitelist} = qr/
-    ^(?:
-        abuse|support|sales|info|helpdesk|contact|kontakt
-      | (?:post|host|domain)master
-      | undisclosed.*                     # yahoo.com etc(?)
-      | request-[a-f0-9]{16}              # live.com
-      | bounced?-                         # yahoo.com etc
-      | [a-f0-9]{8}(?:\.[a-f0-9]{8}|-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}) # gmail msgids?
-      | .+=.+=.+                          # gmail forward
-    )\@
-  /xi;
+sub _parse_opts {
+  my %opts;
+  foreach my $o (split(/\s*\/\s*/, lc $_[0])) {
+    my ($k, $v) = split(/=/, $o);
+    $opts{$k} = defined $v ? $v : 1;
+  }
+  return \%opts;
 }
 
 sub _get_emails {
   my ($self, $pms, $opts, $from, $acl) = @_;
+  my $conf = $pms->{conf};
 
   my @emails; # keep find order
   my %seen;
-  my @tmp_email;
-  my $domain;
 
-  foreach my $hdr (split(/\//, $from)) {
+  foreach my $hdr (split(/\s*\/\s*/, $from)) {
     my $parsed_emails = $self->_parse_emails($pms, $opts, $hdr);
-    foreach (@$parsed_emails) {
-      next if exists $seen{$_};
-      my @tmp_email = split('@', $_);
-      my $domain = $tmp_email[1];
-      if (defined($acl) and ($acl ne "all") and defined($domain)) {
-        if (defined($self->{hashbl_acl}{$acl}{$domain}) and ($self->{hashbl_acl}{$acl}{$domain} eq 1)) {
-          push @emails, $_;
-          $seen{$_} = 1;
-        }
-      } else {
-        push @emails, $_;
-        $seen{$_} = 1;
+    foreach my $email (@$parsed_emails) {
+      my ($username, $domain) = ($email =~ /(.*)\@(.+)/);
+      next unless defined $domain;
+      if (exists $conf->{hashbl_email_domain_alias}->{lc $domain}) {
+        $domain = $conf->{hashbl_email_domain_alias}->{lc $domain};
+        $email = $username.'@'.$domain;
       }
+      next if $seen{$email}++;
+      next if defined $acl && $acl ne 'all' && !$self->{hashbl_acl}{$acl}{$domain};
+      push @emails, $email;
     }
   }
 
@@ -328,16 +478,14 @@ sub _parse_emails {
     return $pms->{hashbl_email_cache}{$hdr} = \@emails;
   }
 
-  if (not defined $pms->{hashbl_whitelist}) {
-    %{$pms->{hashbl_whitelist}} = map { lc($_) => 1 }
+  if (!exists $pms->{hashbl_welcomelist}) {
+    %{$pms->{hashbl_welcomelist}} = map { lc($_) => 1 }
         ( $pms->get("X-Original-To:addr"),
           $pms->get("Apparently-To:addr"),
           $pms->get("Delivered-To:addr"),
           $pms->get("Envelope-To:addr"),
         );
-    if ( defined $pms->{hashbl_whitelist}{''} ) {
-      delete $pms->{hashbl_whitelist}{''};
-    }
+    delete $pms->{hashbl_welcomelist}{''};
   }
 
   my $str = '';
@@ -354,14 +502,14 @@ sub _parse_emails {
       }
     }
     my $body = join('', @{$pms->get_decoded_stripped_body_text_array()});
-    if ($opts =~ /\bnouri\b/) {
+    if ($opts->{nouri}) {
       # strip urls with possible emails inside
       $body =~ s#<?https?://\S{0,255}(?:\@|%40)\S{0,255}# #gi;
     }
-    if ($opts =~ /\bnoquote\b/) {
+    if ($opts->{noquote}) {
       # strip emails contained in <>, not mailto:
       # also strip ones followed by quote-like "wrote:" (but not fax: and tel: etc)
-      $body =~ s#<?(?<!mailto:)$self->{email_re}(?:>|\s{1,10}(?!(?:fa(?:x|csi)|tel|phone|e?-?mail))[a-z]{2,11}:)# #gi;
+      $body =~ s#<?(?<!mailto:)$pms->{conf}->{hashbl_email_regex}(?:>|\s{1,10}(?!(?:fa(?:x|csi)|tel|phone|e?-?mail))[a-z]{2,11}:)# #gi;
     }
     $str .= $body;
   } else {
@@ -371,8 +519,8 @@ sub _parse_emails {
   my @emails; # keep find order
   my %seen;
 
-  while ($str =~ /($self->{email_re})/g) {
-    next if exists $seen{$1};
+  while ($str =~ /($pms->{conf}->{hashbl_email_regex})/g) {
+    next if $seen{$1}++;
     push @emails, $1;
   }
 
@@ -384,8 +532,8 @@ sub check_hashbl_emails {
 
   return 0 if !$self->{hashbl_available};
   return 0 if !$pms->is_dns_available();
-  return 0 if !$self->{email_re};
 
+  my $conf = $pms->{conf};
   my $rulename = $pms->get_current_eval_rule_name();
 
   if (!defined $list) {
@@ -393,6 +541,11 @@ sub check_hashbl_emails {
     return 0;
   }
 
+  if (defined $acl && $acl ne 'all' && !exists $self->{hashbl_acl}{$acl}) {
+    warn "HashBL: $rulename acl '$acl' not defined\n";
+    return 0;
+  }
+
   if ($subtest) {
     my ($rec, $err) = compile_regexp($subtest, 0);
     if (!$rec) {
@@ -402,15 +555,14 @@ sub check_hashbl_emails {
     $subtest = $rec;
   }
 
-  # Defaults
-  $opts = 'sha1/notag/noquote/max=10/shuffle' if !$opts;
-
+  # Parse opts, defaults
+  $opts = _parse_opts($opts || 'sha1/notag/noquote/max=10/shuffle');
   $from = 'ALLFROM/Reply-To/body' if !$from;
 
   # Find all emails
   my $emails = $self->_get_emails($pms, $opts, $from, $acl);
   if (!@$emails) {
-    if(defined $acl) {
+    if (defined $acl) {
       dbg("$rulename: no emails found ($from) on acl $acl");
     } else {
       dbg("$rulename: no emails found ($from)");
@@ -421,42 +573,69 @@ sub check_hashbl_emails {
   }
 
   # Filter list
-  my $keep_case = $opts =~ /\bcase\b/i;
-  my $nodot = $opts =~ /\bnodot\b/i;
-  my $notag = $opts =~ /\bnotag\b/i;
   my @filtered_emails; # keep order
   my %seen;
   foreach my $email (@$emails) {
-    next if exists $seen{$email};
-    next if $email !~ /.*\@.*/;
-    if (($email =~ $self->{email_whitelist}) or defined ($pms->{hashbl_whitelist}{$email})) {
-      dbg("Address whitelisted: $email");
+    next if $seen{$email}++;
+    if (exists $pms->{hashbl_welcomelist}{$email} ||
+        $email =~ $conf->{hashbl_email_welcomelist})
+    {
+      dbg("query skipped, address welcomelisted: $email");
       next;
     }
-    if ($nodot || $notag) {
-      my ($username, $domain) = ($email =~ /(.*)(\@.*)/);
-      $username =~ tr/.//d if $nodot;
-      $username =~ s/\+.*// if $notag;
-      $email = $username.$domain;
+    my ($username, $domain) = ($email =~ /(.*)\@(.*)/);
+    # Don't check uridnsbl_skip_domains when explicit acl is used
+    if (!defined $acl) {
+      if (exists $conf->{uridnsbl_skip_domains}->{lc $domain}) {
+        dbg("query skipped, uridnsbl_skip_domains: $email");
+        next;
+      }
+      my $dom = $pms->{main}->{registryboundaries}->trim_domain($domain);
+      if (exists $conf->{uridnsbl_skip_domains}->{lc $dom}) {
+        dbg("query skipped, uridnsbl_skip_domains: $email");
+        next;
+      }
+    }
+    $username =~ tr/.//d if $opts->{nodot};
+    $username =~ s/\+.*// if $opts->{notag};
+    # Final query assembly
+    my $qmail;
+    if ($opts->{host} || $opts->{domain}) {
+      if ($opts->{domain}) {
+        $domain = $pms->{main}->{registryboundaries}->trim_domain($domain);
+      }
+      $qmail = $domain;
+    } elsif ($opts->{user}) {
+      $qmail = $username;
+    } else {
+      $qmail = $username.'@'.$domain;
     }
-    push @filtered_emails, $keep_case ? $email : lc($email);
-    $seen{$email} = 1;
+    $qmail = lc $qmail  if !$opts->{case};
+    push @filtered_emails, $qmail;
   }
 
+  return 0 unless @filtered_emails;
+
+  # Unique
+  @filtered_emails = do { my %seen; grep { !$seen{$_}++ } @filtered_emails; };
+
   # Randomize order
-  if ($opts =~ /\bshuffle\b/) {
+  if ($opts->{shuffle}) {
     Mail::SpamAssassin::Util::fisher_yates_shuffle(\@filtered_emails);
   }
 
   # Truncate list
-  my $max = $opts =~ /\bmax=(\d+)\b/ ? $1 : 10;
+  my $max = $opts->{max} || 10;
   $#filtered_emails = $max-1 if scalar @filtered_emails > $max;
 
+  my $queries;
   foreach my $email (@filtered_emails) {
-    $self->_submit_query($pms, $rulename, $email, $list, $opts, $subtest);
+    my $ret = $self->_submit_query($pms, $rulename, $email, $list, $opts, $subtest);
+    $queries++ if defined $ret;
   }
 
-  return 0;
+  return 0 if !$queries; # no query started
+  return; # return undef for async status
 }
 
 sub check_hashbl_uris {
@@ -465,6 +644,7 @@ sub check_hashbl_uris {
   return 0 if !$self->{hashbl_available};
   return 0 if !$pms->is_dns_available();
 
+  my $conf = $pms->{conf};
   my $rulename = $pms->get_current_eval_rule_name();
 
   if (!defined $list) {
@@ -481,13 +661,10 @@ sub check_hashbl_uris {
     $subtest = $rec;
   }
 
-  # Defaults
-  $opts = 'sha1/max=10/shuffle' if !$opts;
-
-  # Filter list
-  my $keep_case = $opts =~ /\bcase\b/i;
+  # Parse opts, defaults
+  $opts = _parse_opts($opts || 'sha1/max=10/shuffle');
 
-  if ($opts =~ /raw/) {
+  if ($opts->{raw}) {
     warn "HashBL: $rulename raw option invalid\n";
     return 0;
   }
@@ -496,36 +673,52 @@ sub check_hashbl_uris {
   my %seen;
   my @filtered_uris;
 
+URI:
   while (my($uri, $info) = each %{$uris}) {
     # we want to skip mailto: uris
     next if ($uri =~ /^mailto:/i);
-    next if exists $seen{$uri};
+    next if $seen{$uri}++;
 
     # no hosts/domains were found via this uri, so skip
     next unless $info->{hosts};
     next unless $info->{cleaned};
     next unless $info->{types}->{a} || $info->{types}->{parsed};
+    foreach my $host (keys %{$info->{hosts}}) {
+      if (exists $conf->{uridnsbl_skip_domains}->{$host} ||
+          exists $conf->{uridnsbl_skip_domains}->{$info->{hosts}->{$host}})
+      {
+        dbg("query skipped, uridnsbl_skip_domains: $uri");
+        next URI;
+      }
+    }
     foreach my $uri (@{$info->{cleaned}}) {
       # check url
-      push @filtered_uris, $keep_case ? $uri : lc($uri);
+      push @filtered_uris, $opts->{case} ? $uri : lc($uri);
     }
-    $seen{$uri} = 1;
   }
 
+  return 0 unless @filtered_uris;
+
+  # Unique
+  @filtered_uris = do { my %seen; grep { !$seen{$_}++ } @filtered_uris; };
+
   # Randomize order
-  if ($opts =~ /\bshuffle\b/) {
+  if ($opts->{shuffle}) {
     Mail::SpamAssassin::Util::fisher_yates_shuffle(\@filtered_uris);
   }
 
   # Truncate list
-  my $max = $opts =~ /\bmax=(\d+)\b/ ? $1 : 10;
+  my $max = $opts->{max} || 10;
   $#filtered_uris = $max-1 if scalar @filtered_uris > $max;
 
+  my $queries;
   foreach my $furi (@filtered_uris) {
-    $self->_submit_query($pms, $rulename, $furi, $list, $opts, $subtest);
+    my $ret = $self->_submit_query($pms, $rulename, $furi, $list, $opts, $subtest);
+    $queries++ if defined $ret;
   }
 
-  return 0;
+  return 0 if !$queries; # no query started
+  return; # return undef for async status
 }
 
 sub check_hashbl_bodyre {
@@ -561,33 +754,36 @@ sub check_hashbl_bodyre {
     $subtest = $rec;
   }
 
-  # Defaults
-  $opts = 'sha1/max=10/shuffle' if !$opts;
-
-  my $keep_case = $opts =~ /\bcase\b/i;
+  # Parse opts, defaults
+  $opts = _parse_opts($opts || 'sha1/max=10/shuffle');
 
   # Search body
   my @matches;
   my %seen;
+
   if (ref($bodyref) eq 'ARRAY') {
     # body, rawbody
-    foreach (@$bodyref) {
-      while ($_ =~ /$re/gs) {
+    foreach my $body (@$bodyref) {
+      while ($body =~ /$re/gs) {
         next if !defined $1;
-        my $match = $keep_case ? $1 : lc($1);
-        next if exists $seen{$match};
-        $seen{$match} = 1;
-        push @matches, $match;
+        my $match = $opts->{case} ? $1 : lc($1);
+        if($opts->{num}) {
+          $match =~ tr/0-9//cd;
+        }
+        next if $seen{$match}++;
+        push @matches, $match if $match ne '';
       }
     }
   } else {
     # full
     while ($$bodyref =~ /$re/gs) {
       next if !defined $1;
-      my $match = $keep_case ? $1 : lc($1);
-      next if exists $seen{$match};
-      $seen{$match} = 1;
-      push @matches, $match;
+      my $match = $opts->{case} ? $1 : lc($1);
+      if($opts->{num}) {
+        $match =~ tr/0-9//cd;
+      }
+      next if $seen{$match}++;
+      push @matches, $match if $match ne '';
     }
   }
 
@@ -598,29 +794,252 @@ sub check_hashbl_bodyre {
     dbg("$rulename: matches found: '".join("', '", @matches)."'");
   }
 
+  # Unique
+  @matches = do { my %seen; grep { !$seen{$_}++ } @matches; };
+
   # Randomize order
-  if ($opts =~ /\bshuffle\b/) {
+  if ($opts->{shuffle}) {
     Mail::SpamAssassin::Util::fisher_yates_shuffle(\@matches);
   }
 
   # Truncate list
-  my $max = $opts =~ /\bmax=(\d+)\b/ ? $1 : 10;
+  my $max = $opts->{max} || 10;
   $#matches = $max-1 if scalar @matches > $max;
 
+  my $queries;
   foreach my $match (@matches) {
-    $self->_submit_query($pms, $rulename, $match, $list, $opts, $subtest);
+    my $ret = $self->_submit_query($pms, $rulename, $match, $list, $opts, $subtest);
+    $queries++ if defined $ret;
   }
 
-  return 0;
+  return 0 if !$queries; # no query started
+  return; # return undef for async status
+}
+
+sub check_hashbl_tag {
+  my ($self, $pms, $list, $opts, $tag, $subtest) = @_;
+
+  return 0 if !$self->{hashbl_available};
+  return 0 if !$pms->is_dns_available();
+
+  my $rulename = $pms->get_current_eval_rule_name();
+
+  if (!defined $list) {
+    warn "HashBL: $rulename blocklist argument missing\n";
+    return 0;
+  }
+
+  if (!defined $tag || $tag eq '') {
+    warn "HashBL: $rulename tag argument missing\n";
+    return 0;
+  }
+
+  if ($subtest) {
+    my ($rec, $err) = compile_regexp($subtest, 0);
+    if (!$rec) {
+      warn "HashBL: $rulename invalid subtest regex: $@\n";
+      return 0;
+    }
+    $subtest = $rec;
+  }
+
+  # Parse opts, defaults
+  $opts = _parse_opts($opts || 'sha1/max=10/shuffle');
+  $opts->{fqdn} = $opts->{tld} = 1  if $opts->{trim};
+
+  # Strip possible _ delimiters
+  $tag =~ s/^_(.+)_$/$1/;
+
+  # Force uppercase
+  $tag = uc($tag);
+
+  $pms->action_depends_on_tags($tag, sub {
+    $self->_check_hashbl_tag($pms, $list, $opts, $tag, $subtest, $rulename);
+  });
+
+  return; # return undef for async status
+}
+
+sub _check_hashbl_tag {
+  my ($self, $pms, $list, $opts, $tag, $subtest, $rulename) = @_;
+  my $conf = $pms->{conf};
+
+  # Get raw array of tag values, get_tag() returns joined string
+  my $valref = $pms->get_tag_raw($tag);
+  my @vals = ref $valref ? @$valref : $valref;
+
+  # Lowercase
+  @vals = map { lc } @vals  if !$opts->{case};
+
+  # Options
+  foreach my $value (@vals) {
+    my $is_ip = $value =~ IS_IP_ADDRESS;
+    if ($opts->{ip}) {
+      if (!$is_ip) {
+        $value = undef;
+        next;
+      }
+    }
+    if ($opts->{ipv4}) {
+      if ($value =~ IS_IPV4_ADDRESS) {
+        $is_ip = 1;
+      } else {
+        $value = undef;
+        next;
+      }
+    }
+    if ($opts->{ipv6}) {
+      if (!$is_ip || $value =~ IS_IPV4_ADDRESS) {
+        $value = undef;
+        next;
+      }
+    }
+    if ($is_ip && $opts->{revip}) {
+      $value = reverse_ip_address($value);
+    }
+    if (!$is_ip) {
+      my $fqdn_valid = is_fqdn_valid($value);
+      if ($opts->{fqdn} && !$fqdn_valid) {
+        $value = undef;
+        next;
+      }
+      my $domain;
+      if ($fqdn_valid) {
+        $domain = $pms->{main}->{registryboundaries}->trim_domain($value);
+        if (exists $conf->{uridnsbl_skip_domains}->{lc $value} ||
+            exists $conf->{uridnsbl_skip_domains}->{lc $domain})
+        {
+          dbg("query skipped, uridnsbl_skip_domains: $value");
+          $value = undef;
+          next;
+        }
+      }
+      if ($opts->{tld} && !$pms->{main}->{registryboundaries}->is_domain_valid($value)) {
+        $value = undef;
+        next;
+      }
+      if ($opts->{trim} && $domain) {
+        $value = $domain;
+      }
+    }
+  }
+
+  # Unique (and remove empty)
+  @vals = do { my %seen; grep { defined $_ && !$seen{$_}++ } @vals; };
+
+  if (!@vals) {
+    $pms->rule_ready($rulename); # mark rule ready for metas
+    return;
+  }
+
+  # Randomize order
+  if ($opts->{shuffle}) {
+    Mail::SpamAssassin::Util::fisher_yates_shuffle(\@vals);
+  }
+
+  # Truncate list
+  my $max = $opts->{max} || 10;
+  $#vals = $max-1 if scalar @vals > $max;
+
+  foreach my $value (@vals) {
+    $self->_submit_query($pms, $rulename, $value, $list, $opts, $subtest);
+  }
+
+  return;
+}
+
+sub check_hashbl_attachments {
+  my ($self, $pms, undef, $list, $opts, $subtest) = @_;
+
+  return 0 if !$self->{hashbl_available};
+  return 0 if !$pms->is_dns_available();
+
+  my $rulename = $pms->get_current_eval_rule_name();
+
+  if (!defined $list) {
+    warn "HashBL: $rulename blocklist argument missing\n";
+    return 0;
+  }
+
+  if ($subtest) {
+    my ($rec, $err) = compile_regexp($subtest, 0);
+    if (!$rec) {
+      warn "HashBL: $rulename invalid subtest regex: $@\n";
+      return 0;
+    }
+    $subtest = $rec;
+  }
+
+  # Parse opts, defaults
+  $opts = _parse_opts($opts || 'sha1/max=10/shuffle');
+
+  if ($opts->{raw}) {
+    warn "HashBL: $rulename raw option invalid\n";
+    return 0;
+  }
+
+  my %seen;
+  my @hashes;
+  foreach my $part ($pms->{msg}->find_parts(qr/./, 1, 1)) {
+    my $body = $part->decode();
+    next if !defined $body || $body eq '';
+    my $type = lc $part->{'type'} || '';
+    my $name = $part->{'name'} || '';
+    my $len = length($body);
+    dbg("found attachment, type: $type, length: $len, name: $name");
+    if (exists $pms->{conf}->{hashbl_ignore}->{$type}) {
+      dbg("query skipped, ignored type: $type");
+      next;
+    }
+    if (exists $pms->{conf}->{hashbl_ignore}->{lc $name}) {
+      dbg("query skipped, ignored filename: $name");
+      next;
+    }
+    if ($opts->{minsize} && $len < $opts->{minsize}) {
+      dbg("query skipped, size smaller than $opts->{minsize}");
+      next;
+    }
+    if ($opts->{maxsize} && $len > $opts->{minsize}) {
+      dbg("query skipped, size larger than $opts->{maxsize}");
+      next;
+    }
+    my $hash = $self->_hash($opts, $body);
+    next if $seen{$hash}++;
+    push @hashes, $hash;
+  }
+
+  return 0 unless @hashes;
+
+  # Randomize order
+  if ($opts->{shuffle}) {
+    Mail::SpamAssassin::Util::fisher_yates_shuffle(\@hashes);
+  }
+
+  # Truncate list
+  my $max = $opts->{max} || 10;
+  $#hashes = $max-1 if scalar @hashes > $max;
+
+  my $queries;
+  foreach my $hash (@hashes) {
+    my $ret = $self->_submit_query($pms, $rulename, $hash, $list, $opts, $subtest, 1);
+    $queries++ if defined $ret;
+  }
+
+  return 0 if !$queries; # no query started
+  return; # return undef for async status
 }
 
 sub _hash {
   my ($self, $opts, $value) = @_;
 
-  my $hashtype = $opts =~ /\b(raw|sha1|md5)\b/i ? lc($1) : 'sha1';
-  if ($hashtype eq 'sha1') {
+  if ($opts->{sha256}) {
+    utf8::encode($value) if utf8::is_utf8($value); # sha256 expects bytes
+    return lc base32_encode(sha256($value));
+  } elsif ($opts->{sha1}) {
+    utf8::encode($value) if utf8::is_utf8($value); # sha1_hex expects bytes
     return sha1_hex($value);
-  } elsif ($hashtype eq 'md5') {
+  } elsif ($opts->{md5}) {
+    utf8::encode($value) if utf8::is_utf8($value); # md5_hex expects bytes
     return md5_hex($value);
   } else {
     return $value;
@@ -628,59 +1047,59 @@ sub _hash {
 }
 
 sub _submit_query {
-  my ($self, $pms, $rulename, $value, $list, $opts, $subtest) = @_;
+  my ($self, $pms, $rulename, $value, $list, $opts, $subtest, $already_hashed) = @_;
+  my $conf = $pms->{conf};
 
-  if (exists $pms->{conf}->{hashbl_ignore}->{lc $value}) {
+  if (!$already_hashed && exists $conf->{hashbl_ignore}->{lc $value}) {
     dbg("query skipped, ignored string: $value");
-    return 1;
+    return 0;
   }
 
-  my $hash = $self->_hash($opts, $value);
-  dbg("querying $value ($hash) from $list");
-
-  if (exists $pms->{conf}->{hashbl_ignore}->{$hash}) {
+  my $hash = $already_hashed ? $value : $self->_hash($opts, $value);
+  if (exists $conf->{hashbl_ignore}->{lc $hash}) {
     dbg("query skipped, ignored hash: $value");
-    return 1;
+    return 0;
   }
 
+  dbg("querying $value ($hash) from $list");
+
   my $type = $list =~ s,/(A|TXT)$,,i ? uc($1) : 'A';
   my $lookup = "$hash.$list";
 
-  my $key = "HASHBL_EMAIL:$lookup";
   my $ent = {
-    key => $key,
-    zone => $list,
     rulename => $rulename,
     type => "HASHBL",
     hash => $hash,
     value => $value,
     subtest => $subtest,
   };
-  $ent = $pms->{async}->bgsend_and_start_lookup($lookup, $type, undef, $ent,
+  return $pms->{async}->bgsend_and_start_lookup($lookup, $type, undef, $ent,
     sub { my ($ent, $pkt) = @_; $self->_finish_query($pms, $ent, $pkt); },
     master_deadline => $pms->{master_deadline}
   );
-  $pms->register_async_rule_start($rulename) if $ent;
 }
 
 sub _finish_query {
   my ($self, $pms, $ent, $pkt) = @_;
 
+  my $rulename = $ent->{rulename};
+
   if (!$pkt) {
     # $pkt will be undef if the DNS query was aborted (e.g. timed out)
-    dbg("lookup was aborted: $ent->{rulename} $ent->{key}");
+    dbg("lookup was aborted: $rulename $ent->{key}");
     return;
   }
 
+  $pms->rule_ready($rulename); # mark rule ready for metas
+
   my $dnsmatch = $ent->{subtest} ? $ent->{subtest} : qr/^127\./;
   my @answer = $pkt->answer;
   foreach my $rr (@answer) {
     if ($rr->address =~ $dnsmatch) {
-      dbg("$ent->{rulename}: $ent->{zone} hit '$ent->{value}'");
+      dbg("$rulename: $ent->{zone} hit '$ent->{value}'");
       $ent->{value} =~ s/\@/[at]/g;
-      $pms->test_log($ent->{value});
-      $pms->got_hit($ent->{rulename}, '', ruletype => 'eval');
-      $pms->register_async_rule_finish($ent->{rulename});
+      $pms->test_log($ent->{value}, $rulename);
+      $pms->got_hit($rulename, '', ruletype => 'eval');
       return;
     }
   }
@@ -688,8 +1107,17 @@ sub _finish_query {
 
 # Version features
 sub has_hashbl_bodyre { 1 }
+sub has_hashbl_bodyre_num { 1 }
 sub has_hashbl_emails { 1 }
 sub has_hashbl_uris { 1 }
 sub has_hashbl_ignore { 1 }
+sub has_hashbl_email_regex { 1 }
+sub has_hashbl_email_welcomelist { 1 }
+sub has_hashbl_email_whitelist { 1 }
+sub has_hashbl_tag { 1 }
+sub has_hashbl_sha256 { 1 }
+sub has_hashbl_attachments { 1 }
+sub has_hashbl_email_domain { 1 } # user/host/domain option for emails
+sub has_hashbl_email_domain_alias { 1 } # hashbl_email_domain_alias
 
 1;
diff --git a/upstream/lib/Mail/SpamAssassin/Plugin/Hashcash.pm b/upstream/lib/Mail/SpamAssassin/Plugin/Hashcash.pm
deleted file mode 100644 (file)
index 615cd83..0000000
+++ /dev/null
@@ -1,352 +0,0 @@
-# <@LICENSE>
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements.  See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to you under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License.  You may obtain a copy of the License at:
-# 
-#     http://www.apache.org/licenses/LICENSE-2.0
-# 
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-# </@LICENSE>
-
-=head1 NAME
-
-Mail::SpamAssassin::Plugin::Hashcash - perform hashcash verification tests
-
-=head1 SYNOPSIS
-
-  loadplugin     Mail::SpamAssassin::Plugin::Hashcash
-
-=head1 DESCRIPTION
-
-Hashcash is a payment system for email where CPU cycles used as the
-basis for an e-cash system.  This plugin makes it possible to use valid
-hashcash tokens added by mail programs as a bonus for messages.
-
-=cut
-
-=head1 USER SETTINGS
-
-=over 4
-
-=item use_hashcash { 1 | 0 }   (default: 1)
-
-Whether to use hashcash, if it is available.
-
-=cut
-
-=item hashcash_accept user@example.com ...
-
-Used to specify addresses that we accept HashCash tokens for.  You should set
-it to match all the addresses that you may receive mail at.
-
-Like whitelist and blacklist entries, the addresses are file-glob-style
-patterns, so C<friend@somewhere.com>, C<*@isp.com>, or C<*.domain.net> will all
-work.  Specifically, C<*> and C<?> are allowed, but all other metacharacters
-are not.  Regular expressions are not used for security reasons.
-
-The sequence C<%u> is replaced with the current user's username, which
-is useful for ISPs or multi-user domains.
-
-Multiple addresses per line, separated by spaces, is OK.  Multiple
-C<hashcash_accept> lines is also OK.
-
-=cut
-
-=item hashcash_doublespend_path /path/to/file   (default: ~/.spamassassin/hashcash_seen)
-
-Path for HashCash double-spend database.  HashCash tokens are only usable once,
-so their use is tracked in this database to avoid providing a loophole.
-
-By default, each user has their own, in their C<~/.spamassassin> directory with
-mode 0700/0600.  Note that once a token is 'spent' it is written to this file,
-and double-spending of a hashcash token makes it invalid, so this is not
-suitable for sharing between multiple users.
-
-=cut
-
-=item hashcash_doublespend_file_mode            (default: 0700)
-
-The file mode bits used for the HashCash double-spend database file.
-
-Make sure you specify this using the 'x' mode bits set, as it may also be used
-to create directories.  However, if a file is created, the resulting file will
-not have any execute bits set (the umask is set to 111).
-
-=cut
-
-package Mail::SpamAssassin::Plugin::Hashcash;
-
-use strict;
-use warnings;
-# use bytes;
-use re 'taint';
-
-use Mail::SpamAssassin::Plugin;
-use Mail::SpamAssassin::Logger;
-use Mail::SpamAssassin::Util qw(untaint_var);
-
-use Errno qw(ENOENT EACCES);
-use Fcntl;
-use File::Path;
-use File::Basename;
-
-BEGIN {
-  eval { require Digest::SHA; import Digest::SHA qw(sha1); 1 }
-  or do { require Digest::SHA1; import Digest::SHA1 qw(sha1) }
-}
-
-our @ISA = qw(Mail::SpamAssassin::Plugin);
-
-use constant HAS_DB_FILE => eval { require DB_File; };
-
-# constructor: register the eval rule
-sub new {
-  my $class = shift;
-  my $mailsaobject = shift;
-
-  # some boilerplate...
-  $class = ref($class) || $class;
-  my $self = $class->SUPER::new($mailsaobject);
-  bless ($self, $class);
-
-  $self->register_eval_rule ("check_hashcash_value");
-  $self->register_eval_rule ("check_hashcash_double_spend");
-
-  $self->set_config($mailsaobject->{conf});
-
-  return $self;
-}
-
-###########################################################################
-
-sub set_config {
-  my($self, $conf) = @_;
-  my @cmds;
-
-  push(@cmds, {
-    setting => 'use_hashcash',
-    default => 1,
-    type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
-  });
-
-  push(@cmds, {
-    setting => 'hashcash_doublespend_path',
-    default => '__userstate__/hashcash_seen',
-    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
-  });
-
-  push(@cmds, {
-    setting => 'hashcash_doublespend_file_mode',
-    default => "0700",
-    type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
-  });
-
-  push(@cmds, {
-    setting => 'hashcash_accept',
-    default => {},
-    type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST,
-  });
-
-  $conf->{parser}->register_commands(\@cmds);
-}
-
-###########################################################################
-
-sub check_hashcash_value {
-  my ($self, $scanner, $valmin, $valmax) = @_;
-  my $val = $self->_run_hashcash($scanner);
-  return ($val >= $valmin && $val < $valmax);
-}
-
-sub check_hashcash_double_spend {
-  my ($self, $scanner) = @_;
-  $self->_run_hashcash($scanner);
-  return ($scanner->{hashcash_double_spent});
-}
-
-############################################################################
-
-sub _run_hashcash {
-  my ($self, $scanner) = @_;
-
-  if (defined $scanner->{hashcash_value}) { return $scanner->{hashcash_value}; }
-
-  $scanner->{hashcash_value} = 0;
-
-  # X-Hashcash: 0:031118:camram-spam@camram.org:c068b58ade6dcbaf
-  # or:
-  # X-hashcash: 1:20:040803:hashcash@freelists.org::6dcdb3a3ad4e1b86:1519d
-  # X-hashcash: 1:20:040803:jm@jmason.org::6b484d06469ccb28:8838a
-  # X-hashcash: 1:20:040803:adam@cypherspace.org::a1cbc54bf0182ea8:5d6a0
-
-  # call down to {msg} so that we can get it as an array of
-  # individual headers
-  my @hdrs = $scanner->{msg}->get_header ("X-Hashcash");
-  if (scalar @hdrs == 0) {
-    @hdrs = $scanner->{msg}->get_header ("Hashcash");
-  }
-
-  foreach my $hc (@hdrs) {
-    my $value = $self->_run_hashcash_for_one_string($scanner, $hc);
-    if ($value) {
-      # remove the "double-spend" bool if we did find a usable string;
-      # this happens when one string is already spent, but another
-      # string has not yet been.
-      delete $scanner->{hashcash_double_spent};
-      return $value;
-    }
-  }
-  return 0;
-}
-
-sub _run_hashcash_for_one_string {
-  my ($self, $scanner, $hc) = @_;
-
-  if (!$hc) { return 0; }
-  $hc =~ s/\s+//gs;       # remove whitespace from multiline, folded tokens
-
-  # untaint the string for paranoia, making sure not to allow \n \0 \' \"
-  if ($hc =~ /^[-A-Za-z0-9\xA0-\xFF:_\/\%\@\.\,\= \*\+\;]+$/) {
-    $hc = untaint_var($hc);
-  }
-  if (!$hc) { return 0; }
-
-  my ($ver, $bits, $date, $rsrc, $exts, $rand, $trial);
-  if ($hc =~ /^0:/) {
-    ($ver, $date, $rsrc, $trial) = split (/:/, $hc, 4);
-  }
-  elsif ($hc =~ /^1:/) {
-    ($ver, $bits, $date, $rsrc, $exts, $rand, $trial) =
-                                    split (/:/, $hc, 7);
-    # extensions are, as yet, unused by SpamAssassin
-  }
-  else {
-    dbg("hashcash: version $ver stamps not yet supported");
-    return 0;
-  }
-
-  if (!$trial) {
-    dbg("hashcash: no trial in stamp '$hc'");
-    return 0;
-  }
-
-  my $accept = $scanner->{conf}->{hashcash_accept};
-  if (!$self->_check_hashcash_resource ($scanner, $accept, $rsrc)) {
-    dbg("hashcash: resource $rsrc not accepted here");
-    return 0;
-  }
-
-  # get the hash collision from the token.  Computing the hash collision
-  # is very easy (great!) -- just get SHA1(token) and count the 0 bits at
-  # the start of the SHA1 hash, according to the draft at
-  # http://www.hashcash.org/draft-hashcash.txt .
-  my $value = 0;
-  my $bitstring = unpack ("B*", sha1($hc));
-  $bitstring =~ /^(0+)/ and $value = length $1;
-
-  # hashcash v1 tokens: if the "claimed value" of the token is less than
-  # what the token actually contains (ie. token was accidentally generated
-  # with 24 bits instead of the claimed 20), then cut it down to just the
-  # claimed value.  that way it's a bit tidier and more deterministic.
-  if ($bits && $value > $bits) {
-    $value = $bits;
-  }
-
-  dbg("hashcash: token value: $value");
-
-  if ($self->was_hashcash_token_double_spent ($scanner, $hc)) {
-    $scanner->{hashcash_double_spent} = 1;
-    return 0;
-  }
-
-  $scanner->{hashcash_value} = $value;
-  return $value;
-}
-
-sub was_hashcash_token_double_spent {
-  my ($self, $scanner, $token) = @_;
-
-  my $main = $self->{main};
-  if (!$main->{conf}->{hashcash_doublespend_path}) {
-    dbg("hashcash: hashcash_doublespend_path not defined or empty");
-    return 0;
-  }
-  if (!HAS_DB_FILE) {
-    dbg("hashcash: DB_File module not installed, cannot use double-spend db");
-    return 0;
-  }
-
-  my $path = $main->sed_path ($main->{conf}->{hashcash_doublespend_path});
-  my $parentdir = dirname ($path);
-  my $stat_errn = stat($parentdir) ? 0 : 0+$!;
-  if ($stat_errn == 0 && !-d _) {
-    dbg("hashcash: parent dir $parentdir exists but is not a directory");
-  } elsif ($stat_errn == ENOENT) {
-    # run in an eval(); if mkpath has no perms, it calls die()
-    eval {
-      mkpath ($parentdir, 0, (oct ($main->{conf}->{hashcash_doublespend_file_mode}) & 0777));
-    };
-  }
-
-  my %spenddb;
-  if (!tie %spenddb, "DB_File", $path, O_RDWR|O_CREAT,
-                (oct ($main->{conf}->{hashcash_doublespend_file_mode}) & 0666))
-  {
-    dbg("hashcash: failed to tie to $path: $@ $!");
-    # not a serious error. TODO?
-    return 0;
-  }
-
-  if (exists $spenddb{$token}) {
-    untie %spenddb;
-    dbg("hashcash: token '$token' spent already");
-    return 1;
-  }
-
-  $spenddb{$token} = time;
-  dbg("hashcash: marking token '$token' as spent");
-
-  # TODO: expiry?
-
-  untie %spenddb;
-
-  return 0;
-}
-
-sub _check_hashcash_resource {
-  my ($self, $scanner, $list, $addr) = @_;
-  $addr = lc $addr;
-  if (defined ($list->{$addr})) { return 1; }
-  study $addr;  # study is a no-op since perl 5.16.0, eliminating related bugs
-
-  foreach my $regexp (values %{$list})
-  {
-    # allow %u == current username
-    # \\ is added by $conf->add_to_addrlist()
-    $regexp =~ s/\\\%u/$scanner->{main}->{username}/gs;
-
-    if ($addr =~ /$regexp/i) {
-      return 1;
-    }
-  }
-
-  # TODO: use "To" and "Cc" addresses gleaned from the mails in the Bayes
-  # database trained as ham, as well.
-
-  return 0;
-}
-
-############################################################################
-
-1;
-
-=back
-
-=cut
index f169455b80c612d8684eda227163b5848684f1d2..51df0a7a4b2b83fdbfb3050e27a0eff55fd174b3 100644 (file)
@@ -25,12 +25,15 @@ use Errno qw(EBADF);
 
 use Mail::SpamAssassin::Plugin;
 use Mail::SpamAssassin::Locales;
-use Mail::SpamAssassin::Util qw(get_my_locales parse_rfc822_date);
+use Mail::SpamAssassin::Util qw(get_my_locales parse_rfc822_date
+                                is_valid_utf_8);
 use Mail::SpamAssassin::Logger;
 use Mail::SpamAssassin::Constants qw(:sa :ip);
 
 our @ISA = qw(Mail::SpamAssassin::Plugin);
 
+my $IP_ADDRESS = IP_ADDRESS;
+
 # constructor: register the eval rule
 sub new {
   my $class = shift;
@@ -42,51 +45,44 @@ sub new {
   bless ($self, $class);
 
   # the important bit!
-  $self->register_eval_rule("check_for_fake_aol_relay_in_rcvd");
-  $self->register_eval_rule("check_for_faraway_charset_in_headers");
-  $self->register_eval_rule("check_for_unique_subject_id");
-  $self->register_eval_rule("check_illegal_chars");
-  $self->register_eval_rule("check_for_forged_hotmail_received_headers");
-  $self->register_eval_rule("check_for_no_hotmail_received_headers");
-  $self->register_eval_rule("check_for_msn_groups_headers");
-  $self->register_eval_rule("check_for_forged_eudoramail_received_headers");
-  $self->register_eval_rule("check_for_forged_yahoo_received_headers");
-  $self->register_eval_rule("check_for_forged_juno_received_headers");
-  $self->register_eval_rule("check_for_forged_gmail_received_headers");
-  $self->register_eval_rule("check_for_matching_env_and_hdr_from");
-  $self->register_eval_rule("sorted_recipients");
-  $self->register_eval_rule("similar_recipients");
-  $self->register_eval_rule("check_for_missing_to_header");
-  $self->register_eval_rule("check_for_forged_gw05_received_headers");
-  $self->register_eval_rule("check_for_shifted_date");
-  $self->register_eval_rule("subject_is_all_caps");
-  $self->register_eval_rule("check_for_to_in_subject");
-  $self->register_eval_rule("check_outlook_message_id");
-  $self->register_eval_rule("check_messageid_not_usable");
-  $self->register_eval_rule("check_header_count_range");
-  $self->register_eval_rule("check_unresolved_template");
-  $self->register_eval_rule("check_ratware_name_id");
-  $self->register_eval_rule("check_ratware_envelope_from");
-  $self->register_eval_rule("gated_through_received_hdr_remover");
-  $self->register_eval_rule("received_within_months");
-  $self->register_eval_rule("check_equal_from_domains");
+  $self->register_eval_rule("check_for_fake_aol_relay_in_rcvd", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_for_faraway_charset_in_headers", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_for_unique_subject_id", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_illegal_chars", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_for_forged_hotmail_received_headers", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_for_no_hotmail_received_headers", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_for_msn_groups_headers", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_for_forged_eudoramail_received_headers", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_for_forged_yahoo_received_headers", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_for_forged_juno_received_headers", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_for_forged_gmail_received_headers", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_for_matching_env_and_hdr_from", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("sorted_recipients", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("similar_recipients", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_for_missing_to_header", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_for_forged_gw05_received_headers", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_for_shifted_date", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("subject_is_all_caps", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_for_to_in_subject", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_outlook_message_id", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_messageid_not_usable", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_header_count_range", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_unresolved_template", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_ratware_name_id", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_ratware_envelope_from", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("gated_through_received_hdr_remover", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("received_within_months", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_equal_from_domains", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
 
   return $self;
 }
 
-# load triplets.txt into memory 
-sub compile_now_start {
-  my ($self) = @_;
-
-  $self->word_is_in_dictionary("aba");
-}
-
 sub check_for_fake_aol_relay_in_rcvd {
   my ($self, $pms) = @_;
   local ($_);
 
   $_ = $pms->get('Received');
-  s/\s/ /gs;
+  s/\s+/ /gs;
 
   # this is the hostname format used by AOL for their relays. Spammers love 
   # forging it.  Don't make it more specific to match aol.com only, though --
@@ -122,142 +118,20 @@ sub check_for_faraway_charset_in_headers {
   return 0 if grep { $_ eq "all" } @locales;
 
   for my $h (qw(From Subject)) {
-    my @hdrs = $pms->get("$h:raw");  # ??? get() returns a scalar ???
-    if ($#hdrs >= 0) {
-      $hdr = join(" ", @hdrs);
-    } else {
-      $hdr = '';
-    }
-    while ($hdr =~ /=\?(.+?)\?.\?.*?\?=/g) {
-      Mail::SpamAssassin::Locales::is_charset_ok_for_locales($1, @locales)
-         or return 1;
-    }
+    my @hdrs = $pms->get("$h:raw");
+    foreach my $hdr (@hdrs) {
+      while ($hdr =~ /=\?(.+?)\?.\?.*?\?=/g) {
+        Mail::SpamAssassin::Locales::is_charset_ok_for_locales($1, @locales)
+          or return 1;
+      }
+    }     
   }
   0;
 }
 
+# Deprecated (Bug 8051)
 sub check_for_unique_subject_id {
-  my ($self, $pms) = @_;
-  local ($_);
-  $_ = lc $pms->get('Subject');
-  study;  # study is a no-op since perl 5.16.0, eliminating related bugs
-
-  my $id = 0;
-  if (/[-_\.\s]{7,}([-a-z0-9]{4,})$/
-       || /\s{10,}(?:\S\s)?(\S+)$/
-       || /\s{3,}[-:\#\(\[]+([-a-z0-9]{4,})[\]\)]+$/
-       || /\s{3,}[:\#\(\[]*([a-f0-9]{4,})[\]\)]*$/
-       || /\s{3,}[-:\#]([a-z0-9]{5,})$/
-       || /[\s._]{3,}([^0\s._]\d{3,})$/
-       || /[\s._]{3,}\[(\S+)\]$/
-
-        # (7217vPhZ0-478TLdy5829qicU9-0@26) and similar
-        || /\(([-\w]{7,}\@\d+)\)$/
-
-        # Seven or more digits at the end of a subject is almost certainly a id
-        || /\b(\d{7,})\s*$/
-
-        # stuff at end of line after "!" or "?" is usually an id
-        || /[!\?]\s*(\d{4,}|\w+(-\w+)+)\s*$/
-
-        # 9095IPZK7-095wsvp8715rJgY8-286-28 and similar
-       # excluding 'Re:', etc and the first word
-        || /(?:\w{2,3}:\s)?\w+\s+(\w{7,}-\w{7,}(-\w+)*)\s*$/
-
-        # #30D7 and similar
-        || /\s#\s*([a-f0-9]{4,})\s*$/
-     )
-  {
-    $id = $1;
-    # exempt online purchases
-    if ($id =~ /\d{5,}/
-       && /(?:item|invoice|order|number|confirmation).{1,6}\Q$id\E\s*$/)
-    {
-      $id = 0;
-    }
-
-    # for the "foo-bar-baz" case, otherwise it won't
-    # be found in the dict:
-    $id =~ s/-//;
-  }
-
-  return ($id && !$self->word_is_in_dictionary($id));
-}
-
-# word_is_in_dictionary()
-#
-# See if the word looks like an English word, by checking if each triplet
-# of letters it contains is one that can be found in the English language.
-# Does not include triplets only found in proper names, or in the Latin
-# and Greek terms that might be found in a larger dictionary
-
-my %triplets;
-my $triplets_loaded = 0;
-
-sub word_is_in_dictionary {
-  my ($self, $word) = @_;
-  local ($_);
-  local $/ = "\n";             # Ensure $/ is set appropriately
-
-  # $word =~ tr/A-Z/a-z/;      # already done by this stage
-  $word =~ s/^\s+//;
-  $word =~ s/\s+$//;
-
-  # If it contains a digit, dash, etc, it's not a valid word.
-  # Don't reject words like "can't" and "I'll"
-  return 0 if ($word =~ /[^a-z\']/);
-
-  # handle a few common "blah blah blah (comment)" styles
-  return 1 if ($word eq "ot"); # off-topic
-  return 1 if ($word =~ /(?:linux|nix|bsd)/); # not in most dicts
-  return 1 if ($word =~ /(?:whew|phew|attn|tha?nx)/);  # not in most dicts
-
-  my $word_len = length($word);
-
-  # Unique IDs probably aren't going to be only one or two letters long
-  return 1 if ($word_len < 3);
-
-  if (!$triplets_loaded) {
-    # take a copy to avoid modifying the real one
-    my @default_triplets_path = @Mail::SpamAssassin::default_rules_path;
-    s{$}{/triplets.txt}  for @default_triplets_path;
-    my $filename = $self->{main}->first_existing_path (@default_triplets_path);
-
-    if (!defined $filename) {
-      dbg("eval: failed to locate the triplets.txt file");
-      return 1;
-    }
-
-    local *TRIPLETS;
-    if (!open (TRIPLETS, "<$filename")) {
-      dbg("eval: failed to open '$filename', cannot check dictionary: $!");
-      return 1;
-    }
-    for($!=0; <TRIPLETS>; $!=0) {
-      chomp;
-      $triplets{$_} = 1;
-    }
-    defined $_ || $!==0  or
-      $!==EBADF ? dbg("eval: error reading from $filename: $!")
-                : die "error reading from $filename: $!";
-    close(TRIPLETS)  or die "error closing $filename: $!";
-
-    $triplets_loaded = 1;
-  } # if (!$triplets_loaded)
-
-
-  my $i;
-
-  for ($i = 0; $i < ($word_len - 2); $i++) {
-    my $triplet = substr($word, $i, 3);
-    if (!$triplets{$triplet}) {
-      dbg("eval: unique ID: letter triplet '$triplet' from word '$word' not valid");
-      return 0;
-    }
-  } # for ($i = 0; $i < ($word_len - 2); $i++)
-
-  # All letter triplets in word were found to be valid
-  return 1;
+  return 0;
 }
 
 # look for 8-bit and other illegal characters that should be MIME
@@ -268,7 +142,18 @@ sub check_illegal_chars {
 
   $header .= ":raw" unless $header =~ /:raw$/;
   my $str = $pms->get($header);
-  return 0 if !defined $str || $str eq '';
+  return 0 if !defined $str || $str !~ /\S/;
+
+  if ($str =~ tr/\x00-\x7F//c && is_valid_utf_8($str)) {
+    # is non-ASCII and is valid UTF-8
+    if ($str =~ tr/\x00-\x08\x0B\x0C\x0E-\x1F//) {
+      dbg("eval: %s is valid UTF-8 but contains controls: %s", $header, $str);
+    } else {
+      # todo: only with a SMTPUTF8 mail
+      dbg("eval: %s is valid UTF-8: %s", $header, $str);
+      return 0;
+    }
+  }
 
   # count illegal substrings (RFC 2045)
   # (non-ASCII + C0 controls except TAB, NL, CR)
@@ -291,12 +176,12 @@ sub gated_through_received_hdr_remover {
   my ($self, $pms) = @_;
 
   my $txt = $pms->get("Mailing-List",undef);
-  if (defined $txt && $txt =~ /^contact \S+\@\S+\; run by ezmlm$/) {
+  if (defined $txt && $txt =~ /^contact \S+\@\S+\; run by ezmlm$/m) {
     my $dlto = $pms->get("Delivered-To");
     my $rcvd = $pms->get("Received");
 
     # ensure we have other indicative headers too
-    if ($dlto =~ /^mailing list \S+\@\S+/ &&
+    if ($dlto =~ /^mailing list \S+\@\S+/m &&
         $rcvd =~ /qmail \d+ invoked (?:from network|by .{3,20})\); \d+ ... \d+/)
     {
       return 1;
@@ -336,9 +221,8 @@ sub _check_for_forged_hotmail_received_headers {
   return if $self->check_for_msn_groups_headers($pms);
 
   my $ip = $pms->get('X-Originating-Ip',undef);
-  my $IP_ADDRESS = IP_ADDRESS;
   my $orig = $pms->get('X-OriginatorOrg',undef);
-  my $ORIGINATOR = 'hotmail.com';
+  my $ORIGINATOR = qr/hotmail\.com|msonline\-outlook/;
 
   if (defined $ip && $ip =~ /$IP_ADDRESS/) { $ip = 1; } else { $ip = 0; }
   if (defined $orig && $orig =~ /$ORIGINATOR/) { $orig = 1; } else { $orig = 0; }
@@ -347,6 +231,8 @@ sub _check_for_forged_hotmail_received_headers {
   # Received: from hotmail.com (f135.law8.hotmail.com [216.33.241.135])
   # or like
   # Received: from EUR01-VE1-obe.outbound.protection.outlook.com (mail-oln040092066056.outbound.protection.outlook.com [40.92.66.56])
+  # or
+  # Received: from VI1PR04MB3039.eurprd04.prod.outlook.com (2603:10a6:802:b::13)
   # spammers do not ;)
 
   if ($self->gated_through_received_hdr_remover($pms)) { return; }
@@ -355,6 +241,8 @@ sub _check_for_forged_hotmail_received_headers {
                 { return; }
   if ($rcvd =~ /from \S*\.outbound\.protection\.outlook\.com \(\S+\.outbound\.protection\.outlook\.com[ \)]/ && $orig)
                 { return; }
+  if ($rcvd =~ /from \S*\.eurprd\d+\.prod\.outlook\.com \($IP_ADDRESS\)/ && $orig)
+                { return; }
   if ($rcvd =~ /from \S*\.hotmail.com \(\[$IP_ADDRESS\][ \):]/ && $ip)
                 { return; }
   if ($rcvd =~ /from \S+ by \S+\.hotmail(?:\.msn)?\.com with HTTP\;/ && $ip)
@@ -465,7 +353,6 @@ sub check_for_forged_eudoramail_received_headers {
   $rcvd =~ s/\s+/ /gs;         # just spaces, simplify the regexp
 
   my $ip = $pms->get('X-Sender-Ip',undef);
-  my $IP_ADDRESS = IP_ADDRESS;
   if (defined $ip && $ip =~ /$IP_ADDRESS/) { $ip = 1; } else { $ip = 0; }
 
   # Eudoramail formats its received headers like this:
@@ -518,7 +405,6 @@ sub check_for_forged_yahoo_received_headers {
   if ($rcvd =~ /by web\S+\.mail\S*\.yahoo\.com via HTTP/) { return 0; }
   if ($rcvd =~ /by sonic\S+\.consmr\.mail\S*\.yahoo\.com with HTTP/) { return 0; }
   if ($rcvd =~ /by smtp\S+\.yahoo\.com with SMTP/) { return 0; }
-  my $IP_ADDRESS = IP_ADDRESS;
   if ($rcvd =~
       /from \[$IP_ADDRESS\] by \S+\.(?:groups|scd|dcn)\.yahoo\.com with NNFMP/) {
     return 0;
@@ -554,13 +440,12 @@ sub check_for_forged_juno_received_headers {
   my $xorig = $pms->get('X-Originating-IP');
   my $xmailer = $pms->get('X-Mailer');
   my $rcvd = $pms->get('Received');
-  my $IP_ADDRESS = IP_ADDRESS;
 
   if ($xorig ne '') {
     # New style Juno has no X-Originating-IP header, and other changes
     if($rcvd !~ /from.*\b(?:juno|untd)\.com.*[\[\(]$IP_ADDRESS[\]\)].*by/
         && $rcvd !~ / cookie\.(?:juno|untd)\.com /) { return 1; }
-    if($xmailer !~ /Juno /) { return 1; }
+    if(index($xmailer, 'Juno ') == -1) { return 1; }
   } else {
     if($rcvd =~ /from.*\bmail\.com.*\[$IP_ADDRESS\].*by/) {
       if($xmailer !~ /\bmail\.com/) { return 1; }
@@ -593,6 +478,7 @@ sub check_for_forged_gmail_received_headers {
   if ($received =~ /by smtp\.googlemail\.com with ESMTPSA id \S+/) {
     return 0;
   }
+
   if ( (length($xgms) >= GOOGLE_MESSAGE_STATE_LENGTH_MIN) && 
     (length($xss) >= GOOGLE_SMTP_SOURCE_LENGTH_MIN)) {
       return 0;
@@ -637,10 +523,9 @@ sub _check_recipients {
   my @inputs;
 
   # ToCc: pseudo-header works best, but sometimes Bcc: is better
-  for ('ToCc', 'Bcc') {
-    my $to = $pms->get($_);    # get recipients
-    $to =~ s/\(.*?\)//g;       # strip out the (comments)
-    push(@inputs, ($to =~ m/([\w.=-]+\@\w+(?:[\w.-]+\.)+\w+)/g));
+  for ('ToCc:addr', 'Bcc:addr') {
+    my @to = $pms->get($_);    # get recipients
+    push @inputs, @to;
     last if scalar(@inputs) >= TOCC_SIMILAR_COUNT;
   }
 
index 219eb21c0de1cb2914b36a3235e641427ac98fc6..5dc5a213a00fc0800f7c57e09c0260adbafb6e91 100644 (file)
@@ -99,13 +99,13 @@ sub new {
   my $self = $class->SUPER::new($mailsaobject);
   bless ($self, $class);
 
-  $self->register_eval_rule ("image_count");
-  $self->register_eval_rule ("pixel_coverage");
-  $self->register_eval_rule ("image_size_exact");
-  $self->register_eval_rule ("image_size_range");
-  $self->register_eval_rule ("image_named");
-  $self->register_eval_rule ("image_name_regex");
-  $self->register_eval_rule ("image_to_text_ratio");
+  $self->register_eval_rule ("image_count", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule ("pixel_coverage", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule ("image_size_exact", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule ("image_size_range", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule ("image_named", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule ("image_name_regex", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule ("image_to_text_ratio", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
 
   return $self;
 }
@@ -256,7 +256,7 @@ sub _get_images {
 
 sub image_named {
   my ($self,$pms,$body,$name) = @_;
-  return unless (defined $name);
+  return unless (defined $name);
 
   # make sure we have image data read in.
   if (!exists $pms->{'imageinfo'}) {
@@ -272,7 +272,7 @@ sub image_named {
 
 sub image_name_regex {
   my ($self,$pms,$body,$re) = @_;
-  return unless (defined $re);
+  return unless (defined $re);
 
   # make sure we have image data read in.
   if (!exists $pms->{'imageinfo'}) {
@@ -300,7 +300,7 @@ sub image_name_regex {
 sub image_count {
   my ($self,$pms,$body,$type,$min,$max) = @_;
 
-  return unless defined $min;
+  return unless defined $min;
 
   # make sure we have image data read in.
   if (!exists $pms->{'imageinfo'}) {
@@ -316,7 +316,7 @@ sub image_count {
 sub pixel_coverage {
   my ($self,$pms,$body,$type,$min,$max) = @_;
 
-  return unless (defined $type && defined $min);
+  return unless (defined $type && defined $min);
 
   # make sure we have image data read in.
   if (!exists $pms->{'imageinfo'}) {
@@ -331,7 +331,7 @@ sub pixel_coverage {
 
 sub image_to_text_ratio {
   my ($self,$pms,$body,$type,$min,$max) = @_;
-  return unless (defined $type && defined $min && defined $max);
+  return unless (defined $type && defined $min && defined $max);
 
   # make sure we have image data read in.
   if (!exists $pms->{'imageinfo'}) {
@@ -353,7 +353,7 @@ sub image_to_text_ratio {
 
 sub image_size_exact {
   my ($self,$pms,$body,$type,$height,$width) = @_;
-  return unless (defined $type && defined $height && defined $width);
+  return unless (defined $type && defined $height && defined $width);
 
   # make sure we have image data read in.
   if (!exists $pms->{'imageinfo'}) {
@@ -369,7 +369,7 @@ sub image_size_exact {
 
 sub image_size_range {
   my ($self,$pms,$body,$type,$minh,$minw,$maxh,$maxw) = @_;
-  return unless (defined $type && defined $minh && defined $minw);
+  return unless (defined $type && defined $minh && defined $minw);
 
   # make sure we have image data read in.
   if (!exists $pms->{'imageinfo'}) {
@@ -377,7 +377,7 @@ sub image_size_range {
   }
 
   my $name = 'dems_'.$type;
-  return unless (exists $pms->{'imageinfo'}->{$name});
+  return unless (exists $pms->{'imageinfo'}->{$name});
 
   foreach my $dem ( keys %{$pms->{'imageinfo'}->{"dems_$type"}}) {
     my ($h,$w) = split(/x/,$dem);
index 41122c950e5892355e39c730d92604416b77396d..305c6cf5ae5c469868caf386cc24864712be306f 100644 (file)
@@ -68,18 +68,18 @@ sub new {
   bless ($self, $class);
 
   # the important bit!
-  $self->register_eval_rule("check_for_mime");
-  $self->register_eval_rule("check_for_mime_html");
-  $self->register_eval_rule("check_for_mime_html_only");
-  $self->register_eval_rule("check_mime_multipart_ratio");
-  $self->register_eval_rule("check_msg_parse_flags");
-  $self->register_eval_rule("check_for_ascii_text_illegal");
-  $self->register_eval_rule("check_abundant_unicode_ratio");
-  $self->register_eval_rule("check_for_faraway_charset");
-  $self->register_eval_rule("check_for_uppercase");
-  $self->register_eval_rule("check_ma_non_text");
-  $self->register_eval_rule("check_base64_length");
-  $self->register_eval_rule("check_qp_ratio");
+  $self->register_eval_rule("check_for_mime", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("check_for_mime_html", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("check_for_mime_html_only", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("check_mime_multipart_ratio", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("check_msg_parse_flags", $Mail::SpamAssassin::Conf::TYPE_HEADER_EVALS);
+  $self->register_eval_rule("check_for_ascii_text_illegal", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("check_abundant_unicode_ratio", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("check_for_faraway_charset", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("check_for_uppercase", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("check_ma_non_text", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("check_base64_length", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("check_qp_ratio", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
 
   return $self;
 }
@@ -89,8 +89,7 @@ sub new {
 sub are_more_high_bits_set {
   my ($self, $str) = @_;
 
-  # TODO: I suspect a tr// trick may be faster here
-  my $numhis = () = ($str =~ /[\200-\377]/g);
+  my $numhis = $str =~ tr/\x00-\x7F//c;  # number of non-ASCII chars
   my $numlos = length($str) - $numhis;
 
   ($numlos <= $numhis && $numhis > 3);
@@ -182,7 +181,7 @@ sub check_for_mime {
 
   $self->_check_attachments($pms) unless exists $pms->{mime_checked_attachments};
   return 0 unless exists $pms->{$test};
-  return $pms->{$test};
+  return $pms->{$test} ? 1 : 0;
 }
 
 # any text/html MIME part
@@ -232,19 +231,19 @@ sub _check_mime_header {
     $pms->{mime_body_text_count}++;
   }
 
-  if ($cte =~ /base64/) {
+  if (index($cte, 'base64') >= 0) {
     $pms->{mime_base64_count}++;
   }
-  elsif ($cte =~ /quoted-printable/) {
+  elsif (index($cte, 'quoted-printable') >= 0) {
     $pms->{mime_qp_count}++;
   }
 
-  if ($cd && $cd =~ /attachment/) {
+  if ($cd && index($cd, 'attachment') >= 0) {
     $pms->{mime_attachment}++;
   }
 
   if ($ctype =~ /^text/ &&
-      $cte =~ /base64/ &&
+      index($cte, 'base64') >= 0 &&
       (!$charset || $charset =~ /(?:us-ascii|ansi_x3\.4-1968|iso-ir-6|ansi_x3\.4-1986|iso_646\.irv:1991|ascii|iso646-us|us|ibm367|cp367|csascii)/) &&
       !($cd && $cd =~ /^(?:attachment|inline)/))
   {
@@ -366,9 +365,9 @@ sub _check_attachments {
 
     $part++;
     $part_type[$part] = $ctype;
-    $part_bytes[$part] = 0 if $cd !~ /attachment/;
+    $part_bytes[$part] = 0 if index($cd, 'attachment') == -1;
 
-    my $cte_is_base64 = $cte =~ /base64/i;
+    my $cte_is_base64 = index($cte, 'base64') >= 0;
     my $previous = '';
     foreach (@{$p->raw()}) {
 
@@ -385,12 +384,12 @@ sub _check_attachments {
       # if ($pms->{mime_html_no_charset} && $ctype eq 'text/html' && defined $charset) {
       # $pms->{mime_html_no_charset} = 0;
       # }
-      if ($pms->{mime_multipart_alternative} && $cd !~ /attachment/ &&
+      if ($pms->{mime_multipart_alternative} && index($cd, 'attachment') == -1 &&
           ($ctype eq 'text/plain' || $ctype eq 'text/html')) {
        $part_bytes[$part] += length;
       }
 
-      if ($where != 1 && $cte eq "quoted-printable" && ! /^SPAM: /) {
+      if ($where != 1 && $cte eq "quoted-printable" && index($_, 'SPAM: ') != 0) {
         # RFC 5322: Each line SHOULD be no more than 78 characters,
         #           excluding the CRLF.
         # RFC 2045: The Quoted-Printable encoding REQUIRES that
@@ -415,9 +414,11 @@ sub _check_attachments {
         # }
 
         # count excessive QP bytes
-        if (index($_, '=') != -1) {
+        if (index($_, '=') >= 0) {
+## no critic (Perlsecret)
          # whoever wrote this next line is an evil hacker -- jm
          my $qp = () = m/=(?:09|3[0-9ABCEF]|[2456][0-9A-F]|7[0-9A-E])/g;
+## use critic
          if ($qp) {
            $qp_count += $qp;
            # tabs and spaces at end of encoded line are okay.  Also, multiple
@@ -630,7 +631,7 @@ sub get_charset_from_ct_line {
 sub check_ma_non_text {
   my($self, $pms) = @_;
 
-  foreach my $map ($pms->{msg}->find_parts(qr@^multipart/alternative$@i)) {
+  foreach my $map ($pms->{msg}->find_parts(qr@^multipart/alternative$@)) {
     foreach my $p ($map->find_parts(qr/./, 1, 0)) {
       next if (lc $p->{'type'} eq 'multipart/related');
       next if (lc $p->{'type'} eq 'application/rtf');
index 7acb94d2ae7954ab8bac736e225c5e4452acf776..e90340c46dece47bb1dc7a0317cc197ea6477e7f 100644 (file)
@@ -51,6 +51,23 @@ around the newline character in "folded" headers will be replaced with a single
 space.  Append C<:raw> to the header name to retrieve the raw, undecoded value,
 including pristine whitespace, instead.
 
+=item tflags NAME_OF_RULE range=x-y
+
+Match only from specific MIME parts, indexed in the order they are parsed.
+Part 1 = main message headers. Part 2 = next part etc.
+
+ range=1    (match only main headers, not any subparts)
+ range=2-   (match any subparts, but not the main headers)
+ range=-3   (match only first three parts, including main headers)
+ range=2-3  (match only first two subparts)
+
+=item tflags NAME_OF_RULE concat
+
+Concatenate all headers from all mime parts (possible range applied) into a
+single string for matching.  This allows matching headers across multiple
+parts with single regex.  Normally pattern is tested individually for
+different mime parts.
+
 =back
 
 =cut
@@ -151,6 +168,9 @@ sub set_config {
       $self->{parser}->add_test($rulename, $evalfn."()",
                 $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
 
+      # Support named regex captures
+      $self->{parser}->parse_captures($rulename, $rec);
+
       # evalfn/rulename safe, sanitized by $RULENAME_RE
       my $evalcode = '
         sub Mail::SpamAssassin::Plugin::MIMEHeader::'.$evalfn.' {
@@ -178,37 +198,104 @@ sub set_config {
 # ---------------------------------------------------------------------------
 
 sub eval_hook_called {
-  my ($pobj, $scanner, $rulename) = @_;
+  my ($pobj, $pms, $rulename) = @_;
 
-  my $rule = $scanner->{conf}->{mimeheader_tests}->{$rulename};
+  my $conf = $pms->{conf};
+  my $rule = $conf->{mimeheader_tests}->{$rulename};
   my $hdr = $rule->{hdr};
   my $negated = $rule->{negated};
-  my $if_unset = $rule->{if_unset};
   my $pattern = $rule->{pattern};
-
-
-  my $getraw;
+  my $tflags = $conf->{tflags}->{$rulename}||'';
+  
+  my $getraw = 0;
   if ($hdr =~ s/:raw$//) {
     $getraw = 1;
-  } else {
-    $getraw = 0;
   }
 
-  foreach my $p ($scanner->{msg}->find_parts(qr/./)) {
+  my $range_min = 0;
+  my $range_max = 1000;
+  if ($tflags =~ /(?:^|\s)range=(\d+)?(-)?(\d+)?(?:\s|$)/) {
+    if (defined $1 && defined $2 && defined $3) {
+      $range_min = $1;
+      $range_max = $3;
+    }
+    elsif (defined $1 && defined $2) {
+      $range_min = $1;
+    }
+    elsif (defined $2 && defined $3) {
+      $range_max = $3;
+    }
+    elsif (defined $1) {
+      $range_min = $range_max = $1;
+    }
+  }
+
+  my $multiple = $tflags =~ /\bmultiple\b/;
+  my $concat = $tflags =~ /\bconcat\b/;
+  my $maxhits = $tflags =~ /\bmaxhits=(\d+)\b/ ? $1 :
+                           $multiple ? 1000 : 1;
+  my $cval = '';
+
+  my $idx = 0;
+  foreach my $p ($pms->{msg}->find_parts(qr/./)) {
+    $idx++;
+    last if $idx > $range_max;
+    next if $idx < $range_min;
+
     my $val;
-    if ($getraw) {
+    if ($hdr eq 'ALL') {
+      $val = $p->get_all_headers($getraw, 0);
+    } elsif ($getraw) {
       $val = $p->raw_header($hdr);
     } else {
       $val = $p->get_header($hdr);
     }
-    $val = $if_unset if !defined $val;
+    $val = $rule->{if_unset}  if !defined $val;
+
+    if ($concat) {
+      $val .= "\n" unless $val =~ /\n$/;
+      $cval .= $val;
+      next;
+    }
+
+    if (_check($pms, $rulename, $val, $pattern, $negated, $maxhits, "part $idx")) {
+      return 0;
+    }
+  }
 
-    if ($val =~ $pattern) {
-      return ($negated ? 0 : 1);
+  if ($concat) {
+    if (_check($pms, $rulename, $cval, $pattern, $negated, $maxhits, 'concat')) {
+      return 0;
     }
   }
 
-  return ($negated ? 1 : 0);
+  if ($negated) {
+    dbg("mimeheader: ran rule $rulename ======> got hit: \"<negative match>\"");
+    return 1;
+  }
+
+  return 0;
+}
+
+sub _check {
+  my ($pms, $rulename, $value, $pattern, $negated, $maxhits, $desc) = @_;
+
+  my $hits = 0;
+  my %captures;
+  while ($value =~ /$pattern/gp) {
+    last if $negated;
+    if (%-) {
+      foreach my $cname (keys %-) {
+        push @{$captures{$cname}}, grep { $_ ne "" } @{$-{$cname}};
+      }
+    }
+    my $match = defined ${^MATCH} ? ${^MATCH} : "<negative match>";
+    $pms->got_hit($rulename, '', ruletype => 'eval');
+    dbg("mimeheader: ran rule $rulename ======> got hit: \"$match\" ($desc)");
+    last if ++$hits >= $maxhits;
+  }
+  $pms->set_captures(\%captures) if %captures;
+  return $hits;
 }
 
 # ---------------------------------------------------------------------------
@@ -224,4 +311,10 @@ sub finish_tests {
 
 # ---------------------------------------------------------------------------
 
+sub has_all_header { 1 } # Supports ALL header query (Bug 5582)
+sub has_tflags_range { 1 } # Supports tflags range=x-y
+sub has_tflags_concat { 1 } # Supports tflags concat
+sub has_tflags_multiple { 1 } # Supports tflags multiple
+sub has_capture_rules { 1 } # Supports named regex captures (Bug 7992)
+
 1;
index cdca3c78ec5f6966099640ed9a14b06db97d4839..f4a89059929176575724ecb5d328b2bc514655f5 100644 (file)
@@ -17,7 +17,7 @@
 
 =head1 NAME
 
-Mail::SpamAssassin::Plugin::OLEVBMacro - search attached documents for evidence of containing an OLE Macro
+Mail::SpamAssassin::Plugin::OLEVBMacro - scan Office documents for evidence of OLE Macros or other exploits
 
 =head1 SYNOPSIS
 
@@ -27,6 +27,12 @@ Mail::SpamAssassin::Plugin::OLEVBMacro - search attached documents for evidence
     body     OLEMACRO eval:check_olemacro()
     describe OLEMACRO Attachment has an Office Macro
 
+    body     OLEOBJ eval:check_oleobject()
+    describe OLEOBJ Attachment has an Ole Object
+
+    body     OLERTF eval:check_olertfobject()
+    describe OLERTF Attachment has an Ole Rtf Object
+
     body     OLEMACRO_MALICE eval:check_olemacro_malice()
     describe OLEMACRO_MALICE Potentially malicious Office Macro
 
@@ -44,12 +50,19 @@ Mail::SpamAssassin::Plugin::OLEVBMacro - search attached documents for evidence
 
     body     OLEMACRO_DOWNLOAD_EXE eval:check_olemacro_download_exe()
     describe OLEMACRO_DOWNLOAD_EXE Malicious code inside the Office doc that tries to download a .exe file detected
+
+    body     OLEMACRO_URI_TARGET eval:check_olemacro_redirect_uri()
+    describe OLEMACRO_URI_TARGET Uri inside an Office doc
+
+    body     OLEMACRO_MHTML_TARGET eval:check_olemacro_mhtml_uri()
+    describe OLEMACRO_MHTML_TARGET Exploitable mhtml uri inside an Office doc
   endif
 
 =head1 DESCRIPTION
 
-This plugin detects OLE Macro inside documents attached to emails.
-It can detect documents inside zip files as well as encrypted documents.
+This plugin detects OLE Macros or other exploits inside Office documents
+attached to emails.  It can detect documents inside zip files as well as
+encrypted documents.
 
 =head1 REQUIREMENT
 
@@ -76,10 +89,10 @@ use constant HAS_IO_STRING => eval { require IO::String; };
 BEGIN
 {
     eval{
-      import Archive::Zip qw( :ERROR_CODES :CONSTANTS )
+      Archive::Zip->import(qw( :ERROR_CODES :CONSTANTS ))
     };
     eval{
-      import  IO::String
+      IO::String->import
     };
 }
 
@@ -88,7 +101,7 @@ use re 'taint';
 use vars qw(@ISA);
 @ISA = qw(Mail::SpamAssassin::Plugin);
 
-our $VERSION = '0.52';
+our $VERSION = '4.00';
 
 # https://www.openoffice.org/sc/compdocfileformat.pdf
 # http://blog.rootshell.be/2015/01/08/searching-for-microsoft-office-files-containing-macro/
@@ -102,10 +115,16 @@ my $marker4 = "\x5c\x6f\x62\x6a\x64\x61\x74";
 my $marker5 = "\x5c\x20\x6f\x62\x6a\x64\x61\x74";
 # Excel .xlsx encrypted package, thanks to Dan Bagwell for the sample
 my $encrypted_marker = "\x45\x00\x6e\x00\x63\x00\x72\x00\x79\x00\x70\x00\x74\x00\x65\x00\x64\x00\x50\x00\x61\x00\x63\x00\x6b\x00\x61\x00\x67\x00\x65";
+# Excel .xls marker present only on unencrypted files
+my $workbook_marker = "\x57\x00\x6f\x00\x72\x00\x6b\x00\x62\x00\x6f\x00\x6f\x00\x6b\x00";
 # .exe file downloaded from external website
-my $exe_marker1 = "\x00((https?)://)[-A-Za-z0-9+&@#/%?=~_|!:,.;]{5,1000}[-A-Za-z0-9+&@#/%=~_|]{5,1000}(\.exe|\.cmd|\.bat)([\x06|\x00])";
+my $exe_marker1 = "\x00(https?://[-a-z0-9+&@#/%?=~_|!:,.;]{5,1000}[-a-z0-9+&@#/%=~_|]{5,1000}\.(?:exe|cmd|bat))[\x06|\x00]";
 my $exe_marker2 = "URLDownloadToFileA";
 
+# CVE-2021-40444 marker
+my $mhtml_marker1 = "^MHTML:&#x48;&#x54;&#x50;&#x3a;&#x5c;&#x5c;&#x31;&";
+my $mhtml_marker2 = "^mhtml:https?://";
+
 # this code burps an ugly message if it fails, but that's redirected elsewhere
 # AZ_OK is a constant exported by Archive::Zip
 my $az_ok;
@@ -123,25 +142,48 @@ sub new {
 
   $self->set_config($mailsaobject->{conf});
 
-  $self->register_eval_rule("check_olemacro");
-  $self->register_eval_rule("check_olemacro_csv");
-  $self->register_eval_rule("check_olemacro_malice");
-  $self->register_eval_rule("check_olemacro_renamed");
-  $self->register_eval_rule("check_olemacro_encrypted");
-  $self->register_eval_rule("check_olemacro_zip_password");
-  $self->register_eval_rule("check_olemacro_download_exe");
+  $self->register_eval_rule("check_olemacro", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("check_oleobject", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("check_olertfobject", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("check_olemacro_csv", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("check_olemacro_malice", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("check_olemacro_renamed", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("check_olemacro_encrypted", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("check_olemacro_zip_password", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("check_olemacro_download_exe", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("check_olemacro_redirect_uri", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule("check_olemacro_mhtml_uri", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+
+  # lower priority for add_uri_detail_list to work
+  $self->register_method_priority ("parsed_metadata", -1);
+
+  if (!HAS_ARCHIVE_ZIP) {
+    warn "OLEVBMacro: check_zip not supported, required module Archive::Zip missing\n";
+  }
+  if (!HAS_IO_STRING) {
+    warn "OLEVBMacro: check_macrotype_doc not supported, required module IO::String missing\n";
+  }
 
   return $self;
 }
 
-sub dbg {
-  Mail::SpamAssassin::Plugin::dbg ("OLEVBMacro: @_");
-}
+sub dbg { my $msg = shift; Mail::SpamAssassin::Plugin::dbg("OLEVBMacro: $msg", @_); }
 
 sub set_config {
   my ($self, $conf) = @_;
   my @cmds = ();
 
+=over 4
+
+=item olemacro_num_mime (default: 5)
+
+Configure the maximum number of matching MIME parts (attachments) the plugin
+will scan.
+
+=back
+
+=cut
+
   push(@cmds, {
     setting => 'olemacro_num_mime',
     default => 5,
@@ -150,9 +192,10 @@ sub set_config {
 
 =over 4
 
-=item olemacro_num_mime (default: 5)
+=item olemacro_num_zip (default: 8)
 
-Configure the maximum number of matching MIME parts the plugin will scan
+Configure the maximum number of matching files inside the zip to scan.
+To disable zip scanning, set 0.
 
 =back
 
@@ -166,9 +209,9 @@ Configure the maximum number of matching MIME parts the plugin will scan
 
 =over 4
 
-=item olemacro_num_zip (default: 8)
+=item olemacro_zip_depth (default: 2)
 
-Configure the maximum number of matching zip members the plugin will scan
+Depth to recurse within zip files.
 
 =back
 
@@ -182,9 +225,14 @@ Configure the maximum number of matching zip members the plugin will scan
 
 =over 4
 
-=item olemacro_zip_depth (default: 2)
+=item olemacro_extended_scan ( 0 | 1 ) (default: 0)
 
-Depth to recurse within Zip files
+Scan all files for potential office files and/or macros, the
+C<olemacro_skip_exts> parameter will still be honored.  This parameter is
+off by default, this option is needed only to run
+C<eval:check_olemacro_renamed> rule.  If this is turned on consider
+adjusting values for C<olemacro_num_mime> and C<olemacro_num_zip> and
+prepare for more CPU overhead.
 
 =back
 
@@ -198,13 +246,9 @@ Depth to recurse within Zip files
 
 =over 4
 
-=item olemacro_extended_scan ( 0 | 1 ) (default: 0)
+=item olemacro_prefer_contentdisposition ( 0 | 1 ) (default: 1)
 
-Scan more files for potential macros, the C<olemacro_skip_exts> parameter will still be honored.
-This parameter is off by default, this option is needed only to run
-C<eval:check_olemacro_renamed> rule.
-If this is turned on consider adjusting values for C<olemacro_num_mime> and C<olemacro_num_zip>
-and prepare for more CPU overhead
+Choose if the content-disposition header filename be preferred if ambiguity is encountered whilst trying to get filename.
 
 =back
 
@@ -218,9 +262,10 @@ and prepare for more CPU overhead
 
 =over 4
 
-=item olemacro_prefer_contentdisposition ( 0 | 1 ) (default: 1)
+=item olemacro_max_file (default: 1024000)
 
-Choose if the content-disposition header filename be preferred if ambiguity is encountered whilst trying to get filename
+Limit the amount of bytes that the plugin will decode and scan from the MIME
+objects (attachments).
 
 =back
 
@@ -234,9 +279,10 @@ Choose if the content-disposition header filename be preferred if ambiguity is e
 
 =over 4
 
-=item olemacro_max_file (default: 1024000)
+=item olemacro_exts (default: (?:doc|docx|dot|pot|ppa|pps|ppt|rtf|sldm|xl|xla|xls|xlsx|xlt|xltx|xslb)$)
 
-Configure the largest file that the plugin will decode from the MIME objects
+Set the case-insensitive regexp used to configure the extensions the plugin
+targets for macro scanning.
 
 =back
 
@@ -256,20 +302,19 @@ Configure the largest file that the plugin will decode from the MIME objects
       }
       my ($rec, $err) = compile_regexp($value, 0);
       if (!$rec) {
-       dbg("config: invalid olemacro_exts '$value': $err");
-       return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+        dbg("config: invalid olemacro_exts '$value': $err");
+        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
       }
       $self->{olemacro_exts} = $rec;
-      },
-    }
-  );
+    },
+  });
 
 =over 4
 
-=item olemacro_exts (default: (?:doc|docx|dot|pot|ppa|pps|ppt|rtf|sldm|xl|xla|xls|xlsx|xlt|xltx|xslb)$)
+=item olemacro_macro_exts (default: (?:docm|dotm|ppam|potm|ppst|ppsm|pptm|sldm|xlm|xlam|xlsb|xlsm|xltm|xps)$)
 
 Set the case-insensitive regexp used to configure the extensions the plugin
-targets for macro scanning
+treats as containing a macro.
 
 =back
 
@@ -277,7 +322,7 @@ targets for macro scanning
 
   push(@cmds, {
     setting => 'olemacro_macro_exts',
-    default => qr/(?:docm|dotm|ppam|potm|ppst|ppsm|pptm|sldm|xlm|xlam|xlsb|xlsm|xltm|xltx|xps)$/,
+    default => qr/(?:docm|dotm|ppam|potm|ppst|ppsm|pptm|sldm|xlm|xlam|xlsb|xlsm|xltm|xps)$/,
     type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
     code => sub {
       my ($self, $key, $value, $line) = @_;
@@ -286,8 +331,8 @@ targets for macro scanning
       }
       my ($rec, $err) = compile_regexp($value, 0);
       if (!$rec) {
-       dbg("config: invalid olemacro_macro_exts '$value': $err");
-       return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+        dbg("config: invalid olemacro_macro_exts '$value': $err");
+        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
       }
       $self->{olemacro_macro_exts} = $rec;
     },
@@ -295,10 +340,10 @@ targets for macro scanning
 
 =over 4
 
-=item olemacro_macro_exts (default: (?:docm|dotm|ppam|potm|ppst|ppsm|pptm|sldm|xlm|xlam|xlsb|xlsm|xltm|xltx|xps)$)
+=item olemacro_skip_exts (default: (?:dotx|potx|ppsx|pptx|sldx)$)
 
-Set the case-insensitive regexp used to configure the extensions the plugin
-treats as containing a macro
+Set the case-insensitive regexp used to configure extensions for the plugin
+to skip entirely, these should only be guaranteed macro free files.
 
 =back
 
@@ -315,20 +360,19 @@ treats as containing a macro
       }
       my ($rec, $err) = compile_regexp($value, 0);
       if (!$rec) {
-       dbg("config: invalid olemacro_skip_exts '$value': $err");
-       return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+        dbg("config: invalid olemacro_skip_exts '$value': $err");
+        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
       }
-
       $self->{olemacro_skip_exts} = $rec;
     },
   });
 
 =over 4
 
-=item olemacro_skip_exts (default: (?:dotx|potx|ppsx|pptx|sldx|xltx)$)
+=item olemacro_skip_ctypes (default: ^(?:text\/))
 
-Set the case-insensitive regexp used to configure extensions for the plugin
-to skip entirely, these should only be guaranteed macro free files
+Set the case-insensitive regexp used to configure content types for the
+plugin to skip entirely, these should only be guaranteed macro free.
 
 =back
 
@@ -345,20 +389,19 @@ to skip entirely, these should only be guaranteed macro free files
       }
       my ($rec, $err) = compile_regexp($value, 0);
       if (!$rec) {
-       dbg("config: invalid olemacro_skip_ctypes '$value': $err");
-       return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+        dbg("config: invalid olemacro_skip_ctypes '$value': $err");
+        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
       }
-
       $self->{olemacro_skip_ctypes} = $rec;
     },
   });
 
 =over 4
 
-=item olemacro_skip_ctypes (default: ^(?:text\/))
+=item olemacro_zips (default: (?:zip)$)
 
-Set the case-insensitive regexp used to configure content types for the
-plugin to skip entirely, these should only be guaranteed macro free
+Set the case-insensitive regexp used to configure extensions for the plugin
+to target as zip files, files listed in configs above are also tested for zip.
 
 =back
 
@@ -375,334 +418,357 @@ plugin to skip entirely, these should only be guaranteed macro free
       }
       my ($rec, $err) = compile_regexp($value, 0);
       if (!$rec) {
-       dbg("config: invalid olemacro_zips '$value': $err");
-       return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+        dbg("config: invalid olemacro_zips '$value': $err");
+        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
       }
-
       $self->{olemacro_zips} = $rec;
     },
   });
 
 =over 4
 
-=item olemacro_zips (default: (?:zip)$)
+=item olemacro_download_marker (default: (?:cmd(?:\.exe)? \/c ms\^h\^ta ht\^tps?:\/\^\/))
 
-Set the case-insensitive regexp used to configure extensions for the plugin
-to target as zip files, files listed in configs above are also tested for zip
+Set the case-insensitive regexp used to match the script used to
+download files from the Office document.
 
 =back
 
 =cut
 
+  push(@cmds, {
+    setting => 'olemacro_download_marker',
+    default => qr/(?:cmd(?:\.exe)? \/c ms\^h\^ta ht\^tps?:\/\^\/)/,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      unless (defined $value && $value !~ /^$/) {
+        return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
+      }
+      my ($rec, $err) = compile_regexp($value, 0);
+      if (!$rec) {
+        dbg("config: invalid olemacro_download_marker '$value': $err");
+        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+      }
+      $self->{olemacro_download_marker} = $rec;
+    },
+  });
+
   $conf->{parser}->register_commands(\@cmds);
 }
 
-sub check_olemacro {
-  my ($self,$pms,$body,$name) = @_;
-
-  _check_attachments(@_) unless exists $pms->{olemacro_exists};
+sub parsed_metadata {
+  my ($self, $opts) = @_;
 
-  return $pms->{olemacro_exists};
+  _check_attachments($opts->{permsgstatus});
 }
 
-sub check_olemacro_csv {
-  my ($self,$pms,$body,$name) = @_;
+sub check_olemacro {
+  my ($self, $pms) = @_;
 
-  my $chunk_size = $pms->{conf}->{olemacro_max_file};
+  return $pms->{olemacro_exists} ? 1 : 0;
+}
 
-  foreach my $part ($pms->{msg}->find_parts(qr/./, 1)) {
+sub check_oleobject {
+  my ($self, $pms) = @_;
 
-    next unless ($part->{type} eq "text/plain");
+  return $pms->{oleobject_exists} ? 1 : 0;
+}
 
-    my ($ctt, $ctd, $cte, $name) = _get_part_details($pms, $part);
-    next unless defined $ctt;
+sub check_olertfobject {
+  my ($self, $pms) = @_;
 
-    next if $name eq '';
+  return $pms->{olertfobject_exists} ? 1 : 0;
+}
 
-    # we skipped what we need/want to
-    my $data = undef;
+sub check_olemacro_csv {
+  my ($self, $pms) = @_;
 
-    # if name extension is csv - return true
-    if ($name =~ /\.csv/i) {
-      dbg("Found csv file with name $name");
-      $data = $part->decode($chunk_size) unless defined $data;
-      if($data =~ /MSEXCEL\|.{1,20}Windows\\System32\\cmd\.exe/) {
-        $pms->{olemacro_csv} = 1;
-      }
-    }
-  }
-  return $pms->{olemacro_csv};
+  return $pms->{olemacro_csv} ? 1 : 0;
 }
 
 sub check_olemacro_malice {
-  my ($self,$pms,$body,$name) = @_;
+  my ($self, $pms) = @_;
 
-  _check_attachments(@_) unless exists $pms->{olemacro_malice};
-
-  return $pms->{olemacro_malice};
+  return $pms->{olemacro_malice} ? 1 : 0;
 }
 
 sub check_olemacro_renamed {
-  my ($self,$pms,$body,$name) = @_;
+  my ($self, $pms) = @_;
 
-  _check_attachments(@_) unless exists $pms->{olemacro_renamed};
+  return $pms->{olemacro_renamed} ? 1 : 0;
+}
 
-  if ( $pms->{olemacro_renamed} == 1 ) {
-    dbg("Found Office document with a renamed macro");
-  }
+sub check_olemacro_encrypted {
+  my ($self, $pms) = @_;
 
-  return $pms->{olemacro_renamed};
+  return $pms->{olemacro_encrypted} ? 1 : 0;
 }
 
-sub check_olemacro_encrypted {
-  my ($self,$pms,$body,$name) = @_;
+sub check_olemacro_zip_password {
+  my ($self, $pms) = @_;
 
-  _check_attachments(@_) unless exists $pms->{olemacro_encrypted};
+  return $pms->{olemacro_zip_password} ? 1 : 0;
+}
+
+sub check_olemacro_download_exe {
+  my ($self, $pms) = @_;
 
-  return $pms->{olemacro_encrypted};
+  return $pms->{olemacro_download_exe} ? 1 : 0;
 }
 
-sub check_olemacro_zip_password {
-  my ($self,$pms,$body,$name) = @_;
+sub check_olemacro_redirect_uri {
+  my ($self, $pms) = @_;
 
-  _check_attachments(@_) unless exists $pms->{olemacro_zip_password};
+  if (exists $pms->{olemacro_redirect_uri}) {
+    my $rulename = $pms->get_current_eval_rule_name();
+    $pms->test_log($_, $rulename) foreach (keys %{$pms->{olemacro_redirect_uri}});
+    return 1;
+  }
 
-  return $pms->{olemacro_zip_password};
+  return 0;
 }
 
-sub check_olemacro_download_exe {
-  my ($self,$pms,$body,$name) = @_;
+sub check_olemacro_mhtml_uri {
+  my ($self, $pms) = @_;
 
-  _check_attachments(@_) unless exists $pms->{olemacro_download_exe};
+  if (exists $pms->{olemacro_mhtml_uri}) {
+    my $rulename = $pms->get_current_eval_rule_name();
+    $pms->test_log($_, $rulename) foreach (keys %{$pms->{olemacro_mhtml_uri}});
+    return 1;
+  }
 
-  return $pms->{olemacro_download_exe};
+  return 0;
 }
 
 sub _check_attachments {
+  my ($pms) = @_;
 
-  my ($self,$pms,$body,$name) = @_;
-
+  my $conf = $pms->{conf};
   my $mimec = 0;
-  my $chunk_size = $pms->{conf}->{olemacro_max_file};
-
-  $pms->{olemacro_exists} = 0;
-  $pms->{olemacro_malice} = 0;
-  $pms->{olemacro_renamed} = 0;
-  $pms->{olemacro_encrypted} = 0;
-  $pms->{olemacro_zip_password} = 0;
-  $pms->{olemacro_office_xml} = 0;
 
   foreach my $part ($pms->{msg}->find_parts(qr/./, 1)) {
-
-    next if ($part->{type} =~ /$pms->{conf}->{olemacro_skip_ctypes}/i);
+    next if $part->{type} =~ /$conf->{olemacro_skip_ctypes}/i;
 
     my ($ctt, $ctd, $cte, $name) = _get_part_details($pms, $part);
     next unless defined $ctt;
-
     next if $name eq '';
-    next if ($name =~ /$pms->{conf}->{olemacro_skip_exts}/i);
 
-    # we skipped what we need/want to
-    my $data = undef;
-
-    # if name is macrotype - return true
-    if ($name =~ /$pms->{conf}->{olemacro_macro_exts}/i) {
-      dbg("Found macrotype attachment with name $name");
-      $pms->{olemacro_exists} = 1;
-
-      $data = $part->decode($chunk_size) unless defined $data;
-
-      if (defined $data) {
-        _check_encrypted_doc($pms, $name, $data);
-        _check_macrotype_doc($pms, $name, $data);
-      }
-
-      return 1 if $pms->{olemacro_exists} == 1;
+    if ($name =~ /$conf->{olemacro_skip_exts}/i) {
+      dbg("Skipping file \"$name\" (olemacro_skip_exts)");
+      next;
     }
 
-    # if name is ext type - check and return true if needed
-    if ($name =~ /$pms->{conf}->{olemacro_exts}/i) {
-      dbg("Found attachment with name $name");
-      $data = $part->decode($chunk_size) unless defined $data;
-
-      if (defined $data) {
-        _check_encrypted_doc($pms, $name, $data);
-        _check_oldtype_doc($pms, $name, $data);
-        # zipped doc that matches olemacro_exts - strange
-        if (_check_macrotype_doc($pms, $name, $data)) {
-          $pms->{olemacro_renamed} = $pms->{olemacro_office_xml};
-        }
-      }
-
-      return 1 if $pms->{olemacro_exists} == 1;
+    my $data = $part->decode($conf->{olemacro_max_file});
+    if (!defined $data || $data eq '') {
+      dbg("Skipping empty file \"$name\"");
+      next;
     }
 
-    if ($name =~ /$pms->{conf}->{olemacro_zips}/i) {
-      dbg("Found zip attachment with name $name");
-      $data = $part->decode($chunk_size) unless defined $data;
+    # csv
+    if ($name =~ /\.csv$/i && $conf->{eval_to_rule}->{check_olemacro_csv}) {
+      dbg("Checking csv file \"$name\" for exploits");
+      _check_csv($pms, $name, $data);
+    }
 
-      if (defined $data) {
-        _check_zip($pms, $name, $data);
+    # zip extensions
+    if ($name =~ /$conf->{olemacro_zips}/i) {
+      dbg("Found zip attachment with name \"$name\"");
+      _check_zip($pms, $name, $data);
+    }
+    # macro extensions
+    elsif ($name =~ /$conf->{olemacro_macro_exts}/i) {
+      dbg("Found macrotype attachment with name \"$name\"");
+      $pms->{olemacro_exists} = 1;
+      _check_encrypted_doc($pms, $name, $data);
+      _check_macrotype_doc($pms, $name, $data);
+      _check_download_marker($pms, $name, $data);
+    }
+    # normal extensions
+    elsif ($name =~ /$conf->{olemacro_exts}/i) {
+      dbg("Found attachment with name \"$name\"");
+      _check_encrypted_doc($pms, $name, $data);
+      _check_oldtype_doc($pms, $name, $data);
+      _check_macrotype_doc($pms, $name, $data);
+      _check_download_marker($pms, $name, $data);
+    }
+    # other files, check for rename?
+    elsif ($conf->{olemacro_extended_scan}) {
+      dbg("Extended scan for file \"$name\"");
+      my $renamed = 0;
+      $renamed = 1 if _is_office_doc($data);
+      $renamed = 1 if _check_encrypted_doc($pms, $name, $data);
+      $renamed = 1 if _check_oldtype_doc($pms, $name, $data);
+      $renamed = 1 if _check_macrotype_doc($pms, $name, $data);
+      if ($renamed) {
+        dbg("Found renamed office file \"$name\"");
+        $pms->{olemacro_renamed} = 1;
+        _check_download_marker($pms, $name, $data);
       }
-
-      return 1 if $pms->{olemacro_exists} == 1;
+      _check_zip($pms, $name, $data);
+    }
+    # nothing to check for this file
+    else {
+      next;
     }
 
-    if ((defined $data) and ($data =~ /$exe_marker1/) and (index($data, $exe_marker2))) {
-      dbg('Url that triggers a download to an .exe file found in Office file');
-      $pms->{olemacro_download_exe} = 1;
+    # something was checked, increment counter
+    if (++$mimec >= $conf->{olemacro_num_mime}) {
+      dbg('MIME limit reached');
+      last;
     }
+  }
 
-    if ($pms->{conf}->{olemacro_extended_scan} == 1) {
-      dbg("Extended scan attachment with name $name");
-      $data = $part->decode($chunk_size) unless defined $data;
+  return 0;
+}
 
-      if (defined $data) {
-        if (_is_office_doc($data)) {
-          $pms->{olemacro_renamed} = 1;
-          dbg("Found $name to be an Office Doc!");
-          _check_encrypted_doc($pms, $name, $data);
-          _check_oldtype_doc($pms, $name, $data);
-        }
+sub _check_download_marker {
+  my ($pms, $name, $data) = @_;
 
-        if (_check_macrotype_doc($pms, $name, $data)) {
-          $pms->{olemacro_renamed} = $pms->{olemacro_office_xml};
-        }
+  return 0 unless $pms->{conf}->{eval_to_rule}->{check_olemacro_download_exe};
 
-        _check_zip($pms, $name, $data);
-      }
+  if ((index($data, $exe_marker2) && $data =~ /$exe_marker1/i)
+       || $data =~ /($pms->{conf}->{olemacro_download_marker})/i) {
+    my $uri = defined $1 ? $1 : $2;
+    dbg("Found URI that triggers a download in \"$name\": $uri");
+    $pms->{olemacro_download_exe} = 1;
+    return 1;
+  }
 
-      return 1 if $pms->{olemacro_exists} == 1;
-    }
+  return 0;
+}
 
-    # if we get to here with data a part has been scanned nudge as reqd
-    $mimec+=1 if defined $data;
-    if ($mimec >= $pms->{conf}->{olemacro_num_mime}) {
-      dbg('MIME limit reached');
-      last;
-    }
-  dbg("No Marker of a Macro found in file $name");
+sub _check_csv {
+  my ($pms, $name, $data) = @_;
+
+  if (index($data, 'cmd.exe') >= 0 &&
+        $data =~ /MSEXCEL\|.{1,20}Windows\\System32\\cmd\.exe/) {
+    dbg("Found cmd.exe exploit in \"$name\"");
+    $pms->{olemacro_csv} = 1;
   }
-  return 0;
 }
 
 sub _check_zip {
   my ($pms, $name, $data, $depth) = @_;
 
-  if (!HAS_ARCHIVE_ZIP) {
-    warn "check_zip not supported, required module Archive::Zip missing\n";
+  return 0 if !$pms->{conf}->{olemacro_num_zip};
+
+  if (++$depth > $pms->{conf}->{olemacro_zip_depth}) {
+    dbg("Zip recursion limit exceeded");
     return 0;
   }
-  return 0 if $pms->{conf}->{olemacro_num_zip} == 0;
 
-  $depth = $depth || 1;
-  return 0 if ($depth > $pms->{conf}->{olemacro_zip_depth});
+  return 0 if !defined $data || $data eq '';
 
   return 0 unless _is_zip_file($name, $data);
   my $zip = _open_zip_handle($data);
-  return 0 unless $zip;
+  return 0 unless defined $zip;
 
-  dbg("Zip opened");
+  dbg("Zip \"$name\" opened");
 
+  my $conf = $pms->{conf};
   my $filec = 0;
   my @members = $zip->members();
-  # foreach zip member
-  # - skip if in skip exts
-  # - return 1 if in macro types
-  # - check for marker if doc type
-  # - check if a zip
-  foreach my $member (@members){
-    my $mname = lc $member->fileName();
-    next if ($mname =~ /$pms->{conf}->{olemacro_skip_exts}/i);
+  foreach my $member (@members) {
+    my $name = $member->fileName();
+    my $data; # open zip member lazily
 
-    my $data = undef;
-    my $status = undef;
-
-    # if name is macrotype - return true
-    if ($mname =~ /$pms->{conf}->{olemacro_macro_exts}/i) {
-      dbg("Found macrotype zip member $mname");
-      $pms->{olemacro_exists} = 1;
+    if ($name =~ /$conf->{olemacro_skip_exts}/i) {
+      dbg("Skipping zip member \"$name\" (olemacro_skip_exts)");
+      next;
+    }
 
-      if ($member->isEncrypted()) {
-        dbg("Zip member $mname is encrypted (zip pw)");
-        $pms->{olemacro_zip_password} = 1;
-        return 1;
+    if ($member->isEncrypted()) {
+      if ($name =~ /$conf->{olemacro_macro_exts}/i) {
+        dbg("Found macrotype zip member \"$name\"");
+        $pms->{olemacro_exists} = 1;
       }
-
-      ( $data, $status ) = $member->contents() unless defined $data;
-      return 1 unless $status == $az_ok;
-
-      _check_encrypted_doc($pms, $name, $data);
-      _check_macrotype_doc($pms, $name, $data);
-
-      return 1 if $pms->{olemacro_exists} == 1;
+      dbg("Zip member \"$name\" is encrypted (zip pw)");
+      $pms->{olemacro_zip_password} = 1;
+      next;
     }
 
-    if ($mname =~ /$pms->{conf}->{olemacro_exts}/i) {
-      dbg("Found zip member $mname");
-
-      if ($member->isEncrypted()) {
-        dbg("Zip member $mname is encrypted (zip pw)");
-        $pms->{olemacro_zip_password} = 1;
-        next;
+    # csv
+    if ($name =~ /\.csv$/i && $conf->{eval_to_rule}->{check_olemacro_csv}) {
+      dbg("Checking zipped csv file \"$name\" for exploits");
+      if (!defined $data) {
+        ($data, my $status) = $member->contents();
+        $data = undef  unless $status == $az_ok;
       }
+      _check_csv($pms, $name, $data) if defined $data;
+    }
 
-      ( $data, $status ) = $member->contents() unless defined $data;
-      next unless $status == $az_ok;
-
-
-      _check_encrypted_doc($pms, $name, $data);
-      _check_oldtype_doc($pms, $name, $data);
-      # zipped doc that matches olemacro_exts - strange
-      if (_check_macrotype_doc($pms, $name, $data)) {
-        $pms->{olemacro_renamed} = $pms->{olemacro_office_xml};
+    # zip extensions
+    if ($name =~ /$conf->{olemacro_zips}/i) {
+      dbg("Found zippy zip member \"$name\"");
+      if (!defined $data) {
+        ($data, my $status) = $member->contents();
+        $data = undef  unless $status == $az_ok;
       }
-
-      return 1 if $pms->{olemacro_exists} == 1;
-
+      _check_zip($pms, $name, $data, $depth) if defined $data;
     }
-
-    if ($mname =~ /$pms->{conf}->{olemacro_zips}/i) {
-      dbg("Found zippy zip member $mname");
-      ( $data, $status ) = $member->contents() unless defined $data;
-      next unless $status == $az_ok;
-
-      _check_zip($pms, $name, $data, $depth);
-
-      return 1 if $pms->{olemacro_exists} == 1;
-
+    # macro extensions
+    elsif ($name =~ /$conf->{olemacro_macro_exts}/i) {
+      dbg("Found macrotype zip member \"$name\"");
+      $pms->{olemacro_exists} = 1;
+      if (!defined $data) {
+        ($data, my $status) = $member->contents();
+        $data = undef  unless $status == $az_ok;
+      }
+      if (defined $data) {
+        _check_encrypted_doc($pms, $name, $data);
+        _check_macrotype_doc($pms, $name, $data);
+        _check_download_marker($pms, $name, $data);
+      }
     }
-
-    if ($pms->{conf}->{olemacro_extended_scan} == 1) {
-      dbg("Extended scan attachment with member name $mname");
-      ( $data, $status ) = $member->contents() unless defined $data;
-      next unless $status == $az_ok;
-
-      if (_is_office_doc($data)) {
-        dbg("Found $name to be an Office Doc!");
+    # normal extensions
+    elsif ($name =~ /$conf->{olemacro_exts}/i) {
+      dbg("Found zip member \"$name\"");
+      if (!defined $data) {
+        ($data, my $status) = $member->contents();
+        $data = undef  unless $status == $az_ok;
+      }
+      if (defined $data) {
         _check_encrypted_doc($pms, $name, $data);
-        $pms->{olemacro_renamed} = 1;
         _check_oldtype_doc($pms, $name, $data);
+        _check_macrotype_doc($pms, $name, $data);
+        _check_download_marker($pms, $name, $data);
       }
-
-      if (_check_macrotype_doc($pms, $name, $data)) {
-        $pms->{olemacro_renamed} = $pms->{olemacro_office_xml};
+    }
+    # other files, check for rename?
+    elsif ($conf->{olemacro_extended_scan}) {
+      dbg("Extended scan for zip member \"$name\"");
+      if (!defined $data) {
+        ($data, my $status) = $member->contents();
+        $data = undef  unless $status == $az_ok;
       }
-
-      _check_zip($pms, $name, $data, $depth);
-
-      return 1 if $pms->{olemacro_exists} == 1;
-
+      if (defined $data) {
+        my $renamed = 0;
+        $renamed = 1 if _is_office_doc($data);
+        $renamed = 1 if _check_encrypted_doc($pms, $name, $data);
+        $renamed = 1 if _check_oldtype_doc($pms, $name, $data);
+        $renamed = 1 if _check_macrotype_doc($pms, $name, $data);
+        if ($renamed) {
+          dbg("Found renamed office file \"$name\"");
+          $pms->{olemacro_renamed} = 1;
+          _check_download_marker($pms, $name, $data);
+        }
+        _check_zip($pms, $name, $data, $depth);
+      }
+    }
+    # nothing to check for this file
+    else {
+      next;
     }
 
-    # if we get to here with data a member has been scanned nudge as reqd
-    $filec+=1 if defined $data;
-    if ($filec >= $pms->{conf}->{olemacro_num_zip}) {
+    # something was checked, increment counter
+    if (++$filec >= $conf->{olemacro_num_zip}) {
       dbg('Zip limit reached');
       last;
     }
   }
-  return 0;
+
+  return 1;
 }
 
 sub _get_part_details {
@@ -722,7 +788,7 @@ sub _get_part_details {
     my $cttname = '';
     my $ctdname = '';
 
-    if($ctt =~ m/(?:file)?name\s*=\s*["']?([^"';]*)["']?/is){
+    if ($ctt =~ m/name\s*=\s*["']?([^"';]*)/is) {
       $cttname = $1;
       $cttname =~ s/\s+$//;
     }
@@ -730,7 +796,7 @@ sub _get_part_details {
     my $ctd = $part->get_header('content-disposition');
     $ctd = _decode_part_header($part, $ctd || '');
 
-    if($ctd =~ m/filename\s*=\s*["']?([^"';]*)["']?/is){
+    if ($ctd =~ m/filename\s*=\s*["']?([^"';]*)/is) {
       $ctdname = $1;
       $ctdname =~ s/\s+$//;
     }
@@ -749,36 +815,39 @@ sub _get_part_details {
       }
     }
 
-    return $ctt, $ctd, $cte, lc $name;
+    return $ctt, $ctd, $cte, $name;
 }
 
 sub _open_zip_handle {
   my ($data) = @_;
+
+  return unless HAS_ARCHIVE_ZIP && HAS_IO_STRING;
+
   # open our archive from raw data
   my $SH = IO::String->new($data);
-
-  Archive::Zip::setErrorHandler( \&_zip_error_handler );
+  Archive::Zip::setErrorHandler(\&_zip_error_handler);
   my $zip = Archive::Zip->new();
-  if($zip->readFromFileHandle( $SH ) != $az_ok){
+  if ($zip->readFromFileHandle($SH) != $az_ok) {
     dbg("cannot read zipfile");
     # as we cannot read it its not a zip (or too big/corrupted)
     # so skip processing.
-    return 0;
+    return;
   }
+
   return $zip;
 }
 
 sub _check_macrotype_doc {
   my ($pms, $name, $data) = @_;
 
-  if (!HAS_IO_STRING) {
-    warn "check_macrotype_doc not supported, required module IO::String missing\n";
-    return 0;
-  }
-  return 0 unless _is_zip_file($name, $data);
+  return if !defined $data || $data eq '';
 
+  return unless _is_zip_file($name, $data);
   my $zip = _open_zip_handle($data);
-  return 0 unless $zip;
+  return unless $zip;
+
+  my $is_doc = 0;
+  my $olemacro_exists = 0;
 
   # https://www.decalage.info/vba_tools
   # Consider macrofiles as lowercase, they are checked later with a case-insensitive method
@@ -792,119 +861,163 @@ sub _check_macrotype_doc {
 
   my @members = $zip->members();
   foreach my $member (@members){
-    my $mname = lc $member->fileName();
-    if (exists($macrofiles{lc($mname)})) {
-      dbg("Found $macrofiles{$mname} vba file");
-      $pms->{olemacro_exists} = 1;
-      last;
+    my $name = lc $member->fileName();
+    if (exists $macrofiles{$name}) {
+      dbg("Found vba file \"$name\"");
+      $is_doc = 1;
+      $olemacro_exists = $pms->{olemacro_exists} = 1;
+    }
+    if (index($name, 'xl/embeddings/') == 0) {
+      dbg("Found ole file \"$name\"");
+      $is_doc = 1;
+      $pms->{oleobject_exists} = 1;
+    }
+    if ($name =~ /^word\/.{1,50}\.rtf\b/) {
+      dbg("Found ole rtf file \"$name\"");
+      $is_doc = 1;
+      $pms->{olertfobject_exists} = 1;
     }
   }
 
   # Look for a member named [Content_Types].xml and do checks
   if (my $ctypesxml = $zip->memberNamed('[Content_Types].xml')) {
     dbg('Found [Content_Types].xml file');
-    $pms->{olemacro_office_xml} = 1;
+    $is_doc = 1;
     if (!$pms->{olemacro_exists}) {
-      my ( $data, $status ) = $ctypesxml->contents();
-
-      if (($status == $az_ok) && (_check_ctype_xml($data))) {
+      my ($data, $status) = $ctypesxml->contents();
+      if ($status == $az_ok && _check_ctype_xml($data)) {
         $pms->{olemacro_exists} = 1;
       }
     }
   }
 
-  if (($pms->{olemacro_exists}) && (_find_malice_bins($zip))) {
-    $pms->{olemacro_malice} = 1;
+  my @rels = $zip->membersMatching('.*\.rels');
+  foreach my $rel (@rels) {
+    dbg("Found \"".$rel->fileName."\" configuration file");
+    my ($data, $status) = $rel->contents();
+    next unless $status == $az_ok;
+    my @relations = split(/Relationship\s/, $data);
+    $is_doc = 1 if @relations;
+    foreach my $rl (@relations) {
+      if ($rl =~ /Target=\"([^"]*)\".*?TargetMode=\"External\"/is) {
+        my $uri = $1;
+        if ($uri =~ /(?:$mhtml_marker1|$mhtml_marker2)/i) {
+          dbg("Found target mhtml uri: $uri");
+          if (keys %{$pms->{olemacro_mhtml_uri}} < 5) {
+            $pms->{olemacro_mhtml_uri}{$uri} = 1;
+          }
+        }
+        $uri =~ s/^mhtml://i;
+        if ($uri =~ /^https?:\/\//i) {
+          dbg("Found target uri: $uri");
+          if (!exists $pms->{olemacro_redirect_uri}{$uri}) {
+            if (keys %{$pms->{olemacro_redirect_uri}} < 10) {
+              $pms->add_uri_detail_list($uri);
+              $pms->{olemacro_redirect_uri}{$uri} = 1;
+            }
+          }
+        }
+      }
+    }
   }
 
-  return $pms->{olemacro_exists};
+  if ($olemacro_exists && _find_malice_bins($zip)) {
+    $pms->{olemacro_malice} = 1;
+  }
 
+  return $is_doc;
 }
 
 # Office 2003
-
 sub _check_oldtype_doc {
   my ($pms, $name, $data) = @_;
 
+  return 0 if !defined $data || $data eq '';
+
   if (_check_markers($data)) {
     $pms->{olemacro_exists} = 1;
     if (_check_malice($data)) {
-     $pms->{olemacro_malice} = 1;
+      $pms->{olemacro_malice} = 1;
     }
     return 1;
   }
+
+  return 0;
 }
 
 # Encrypted doc
-
 sub _check_encrypted_doc {
   my ($pms, $name, $data) = @_;
 
+  return 0 if !defined $data || $data eq '';
+
   if (_is_encrypted_doc($data)) {
-    dbg("File $name is encrypted");
+    dbg("File \"$name\" is encrypted");
     $pms->{olemacro_encrypted} = 1;
+    return 1;
   }
 
-  return $pms->{olemacro_encrypted};
+  return 0;
 }
 
 sub _is_encrypted_doc {
   my ($data) = @_;
 
+  return 0 unless _is_office_doc($data);
+
   #http://stackoverflow.com/questions/14347513/how-to-detect-if-a-word-document-is-password-protected-before-uploading-the-file/14347730#14347730
-  if (_is_office_doc($data)) {
-    if ($data =~ /(?:<encryption xmlns)/i) {
-      return 1;
-    }
-    if (index($data, "\x13") == 523) {
-      return 1;
-    }
-    if (index($data, "\x2f") == 532) {
-      return 1;
-    }
-    if (index($data, "\xfe") == 520) {
-      return 1;
-    }
-    my $tdata = substr $data, 2000;
-    $tdata =~ s/\\0/ /g;
-    if (index($tdata, "E n c r y p t e d P a c k a g e") > -1) {
-      return 1;
-    }
-    if (index($tdata, $encrypted_marker) > -1) {
-      return 1;
-    }
-  }
+  return 1 if $data =~ /(?:<encryption xmlns)/i;
+  my $tdata = substr($data, 0, 2000);
+  return 1 if index($tdata, $encrypted_marker) > -1;
+  $tdata =~ s/\\0/ /g;
+  return 1 if index($tdata, "E n c r y p t e d P a c k a g e") > -1;
+  return 0 if index($tdata, $workbook_marker) > -1;
+  return 1 if substr($data, 0x208, 1) eq "\xfe";
+  return 1 if substr($data, 0x214, 1) eq "\x2f";
+  return 1 if substr($data, 0x20B, 1) eq "\x13";
+
+  return 0;
 }
 
 sub _is_office_doc {
   my ($data) = @_;
+
+  return 0 if !defined $data || $data eq '';
+
   if (index($data, $marker1) == 0) {
     return 1;
   }
+
+  return 0;
 }
 
 sub _is_zip_file {
   my ($name, $data) = @_;
-  if (index($data, 'PK') == 0) {
+
+  if (index($data, 'PK') == 0 || $name =~ /\.zip$/i) {
     return 1;
-  } else {
-    return($name =~ /(?:zip)$/i);
   }
+
+  return 0;
 }
 
 sub _check_markers {
   my ($data) = @_;
 
-  if (index($data, $marker1) == 0 && index($data, $marker2) > -1) {
-    dbg('Marker 1 & 2 found');
-    return 1;
-  }
-
-  if (index($data, $marker1) == 0 && index($data, $marker2a) > -1) {
-    dbg('Marker 1 & 2a found');
-    return 1;
+  # Check for Office 2003 markers
+  if (index($data, $marker1) == 0) {
+    if (index($data, $marker2) > -1) {
+      dbg('Marker 1 & 2 found');
+      return 1;
+    }
+    if (index($data, $marker2a) > -1) {
+      dbg('Marker 1 & 2a found');
+      return 1;
+    }
+    return 0;
   }
 
+  # Check for rtf markers
   if (index($data, $marker3) > -1) {
     dbg('Marker 3 found');
     return 1;
@@ -920,6 +1033,7 @@ sub _check_markers {
     return 1;
   }
 
+  # Check for Office 2007 markers
   if (index($data, 'w:macrosPresent="yes"') > -1) {
     dbg('XML macros marker found');
     return 1;
@@ -929,16 +1043,15 @@ sub _check_markers {
     dbg('XML macros marker found');
     return 1;
   }
-
 }
 
 sub _find_malice_bins {
   my ($zip) = @_;
 
-  my @binfiles = $zip->membersMatching( '.*\.bin' );
+  my @binfiles = $zip->membersMatching('.*\.bin');
 
-  foreach my $member (@binfiles){
-    my ( $data, $status ) = $member->contents();
+  foreach my $member (@binfiles) {
+    my ($data, $status) = $member->contents();
     next unless $status == $az_ok;
     if (_check_malice($data)) {
       return 1;
@@ -959,8 +1072,10 @@ sub _check_malice {
 sub _check_ctype_xml {
   my ($data) = @_;
 
+  return if !defined $data || $data eq '';
+
   # http://download.microsoft.com/download/D/3/3/D334A189-E51B-47FF-B0E8-C0479AFB0E3C/[MS-OFFMACRO].pdf
-  if ($data =~ /ContentType=["']application\/vnd\.ms-office\.vbaProject["']/i){
+  if ($data =~ /ContentType=["']application\/vnd\.ms-office\.vbaProject["']/i) {
     dbg('Found VBA ref');
     return 1;
   }
@@ -975,7 +1090,7 @@ sub _check_ctype_xml {
 }
 
 sub _zip_error_handler {
- 1;
 1;
 }
 
 sub _decode_part_header {
@@ -1007,4 +1122,9 @@ sub _decode_part_header {
   return $header_field_body;
 }
 
+# Version features
+sub has_olemacro_redirect_uri { 1 }
+sub has_olemacro_mhtml_uri { 1 }
+sub has_olertfobject { 1 }
+
 1;
index 9306822c810d348b5cd2a29ee1a248d767fd7328..b414e74f23be6c27326de10c2e0b419e8548ee10 100644 (file)
@@ -66,6 +66,20 @@ sub check_start {
   }
 }
 
+sub check_cleanup {
+  my ($self, $params) = @_;
+  my $pms = $params->{permsgstatus};
+  my $scoresptr = $pms->{conf}->{scores};
+
+  # Force all body rules ready for meta rules.  Need to do it here in
+  # cleanup, because the body is scanned per line instead of per rule
+  if ($pms->{conf}->{skip_body_rules}) {
+    foreach (keys %{$pms->{conf}->{skip_body_rules}}) {
+      $pms->rule_ready($_, 1)  if $scoresptr->{$_};
+    }
+  }
+}
+
 ###########################################################################
 
 1;
@@ -96,13 +110,16 @@ sub do_one_line_body_tests {
 
     if (($conf->{tflags}->{$rulename}||'') =~ /\bmultiple\b/)
     {
+      $sub .= '
+        my $hitsptr = $self->{tests_already_hit};
+      ';
       # support multiple matches
       my ($max) = $conf->{tflags}->{$rulename} =~ /\bmaxhits=(\d+)\b/;
       $max = untaint_var($max);
       if ($max) {
         $sub .= '
-          if (exists $self->{tests_already_hit}->{q{'.$rulename.'}}) {
-            return 0 if $self->{tests_already_hit}->{q{'.$rulename.'}} >= '.$max.';
+          if ($hitsptr->{q{'.$rulename.'}}) {
+            return 0 if $hitsptr->{q{'.$rulename.'}} >= '.$max.';
           }
         ';
       }
@@ -111,17 +128,17 @@ sub do_one_line_body_tests {
       my $lref = \$line;
       pos $$lref = 0;
       '.$self->hash_line_for_rule($pms, $rulename).'
-      while ($$lref =~ /$qrptr->{q{'.$rulename.'}}/go) {
+      while ($$lref =~ /$qrptr->{q{'.$rulename.'}}/gop) {
         $self->got_hit(q{'.$rulename.'}, "BODY: ", ruletype => "one_line_body");
         '. $self->hit_rule_plugin_code($pms, $rulename, "one_line_body", "") . '
-        '. ($max? 'last if $self->{tests_already_hit}->{q{'.$rulename.'}} >= '.$max.';' : '') . '
+        '. ($max? 'last if $hitsptr->{q{'.$rulename.'}} >= '.$max.';' : '') . '
       }
       ';
 
     } else {
       $sub .= '
       '.$self->hash_line_for_rule($pms, $rulename).'
-      if ($line =~ /$qrptr->{q{'.$rulename.'}}/o) {
+      if ($line =~ /$qrptr->{q{'.$rulename.'}}/op) {
         $self->got_hit(q{'.$rulename.'}, "BODY: ", ruletype => "one_line_body");
         '. $self->hit_rule_plugin_code($pms, $rulename, "one_line_body", "return 1") . '
       }
@@ -129,6 +146,11 @@ sub do_one_line_body_tests {
 
     }
 
+    # Make sure rule is marked ready for meta rules
+    $sub .= '
+      $self->rule_ready(q{'.$rulename.'}, 1);
+    ';
+
     return if ($opts{doing_user_rules} &&
                   !$self->is_user_rule_sub($rulename.'_one_line_body_test'));
 
index 6420dcc3677609f6575796b4a9bc84c11b572c51..fd3da6d811988848ad5c596d308ec7292c931ec0 100644 (file)
@@ -130,6 +130,27 @@ This plugin helps detected spam using attached PDF files
      body RULENAME eval:pdf_is_empty_body(<bytes>)
         bytes: maximum byte count to allow and still consider it empty
 
+  pdf_image_to_text_ratio()
+
+     body RULENAME eval:pdf_image_to_text_ratio(<min>,<max>)
+        Ratio calculated as body_length / total_image_area
+        min: minimum ratio
+        max: maximum ratio
+
+  pdf_image_size_exact()
+
+     body RULENAME eval:pdf_image_size_exact(<h>,<w>)
+        h: image height is exactly h
+        w: image width is exactly w
+
+  pdf_image_size_range()
+
+     body RULENAME eval:pdf_image_size_range(<minh>,<minw>,[<maxh>],[<maxw>])
+        minh: image height is atleast minh
+        minw: image width is atleast minw
+        maxh: (optional) image height is no more than maxh
+        maxw: (optional) image width is no more than maxw
+
   NOTE: See the ruleset for more examples that are not documented here.
 
 =back
@@ -145,9 +166,8 @@ use Mail::SpamAssassin::Logger;
 use Mail::SpamAssassin::Util qw(compile_regexp);
 use strict;
 use warnings;
-# use bytes;
+use re 'taint';
 use Digest::MD5 qw(md5_hex);
-use MIME::QuotedPrint;
 
 our @ISA = qw(Mail::SpamAssassin::Plugin);
 
@@ -161,502 +181,396 @@ sub new {
   my $self = $class->SUPER::new($mailsaobject);
   bless ($self, $class);
 
-  $self->register_eval_rule ("pdf_count");
-  $self->register_eval_rule ("pdf_image_count");
-  $self->register_eval_rule ("pdf_pixel_coverage");
-  $self->register_eval_rule ("pdf_image_size_exact");
-  $self->register_eval_rule ("pdf_image_size_range");
-  $self->register_eval_rule ("pdf_named");
-  $self->register_eval_rule ("pdf_name_regex");
-  $self->register_eval_rule ("pdf_image_to_text_ratio");
-  $self->register_eval_rule ("pdf_match_md5");
-  $self->register_eval_rule ("pdf_match_fuzzy_md5");
-  $self->register_eval_rule ("pdf_match_details");
-  $self->register_eval_rule ("pdf_is_encrypted");
-  $self->register_eval_rule ("pdf_is_empty_body");
+  $self->register_eval_rule ("pdf_count", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule ("pdf_image_count", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule ("pdf_pixel_coverage", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule ("pdf_image_size_exact", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule ("pdf_image_size_range", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule ("pdf_named", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule ("pdf_name_regex", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule ("pdf_image_to_text_ratio", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule ("pdf_match_md5", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule ("pdf_match_fuzzy_md5", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule ("pdf_match_details", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule ("pdf_is_encrypted", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+  $self->register_eval_rule ("pdf_is_empty_body", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+
+  # lower priority for add_uri_detail_list to work
+  $self->register_method_priority ("parsed_metadata", -1);
 
   return $self;
 }
 
-# -----------------------------------------
+sub parsed_metadata {
+  my ($self, $opts) = @_;
 
-my %get_details = (
-  'pdf' => sub {
-    my ($self, $pms, $part) = @_;
+  my $pms = $opts->{permsgstatus};
 
-    my $type = $part->{'type'} || 'base64';
-    my $data = '';
+  # initialize
+  $pms->{pdfinfo}->{count_pdf} = 0;
+  $pms->{pdfinfo}->{count_pdf_images} = 0;
 
-    if ($type eq 'quoted-printable') {
-      $data = decode_qp($data); # use QuotedPrint->decode_qp
-    }
-    else {
-      $data = $part->decode();  # just use built in base64 decoder
-    }
+  my @parts = $pms->{msg}->find_parts(qr@^(image|application)/(pdf|octet\-stream)$@, 1);
+  my $part_count = scalar @parts;
 
-    my $index = substr($data, 0, 8);
+  dbg("pdfinfo: Identified $part_count possible mime parts that need checked for PDF content");
 
-    return unless ($index =~ /.PDF\-(\d\.\d)/);
-    my $version = $1;
-    $self->_set_tag($pms, 'PDFVERSION', $version);
-    # dbg("pdfinfo: pdf version = $version");
+  foreach my $p (@parts) {
+    my $type = $p->{type} || '';
+    my $name = $p->{name} || '';
 
-    my ($height, $width, $fuzzy_data, $pdf_tags);
-    my ($producer, $created, $modified, $title, $creator, $author) = ('unknown','0','0','untitled','unknown','unknown');
-    my ($md5, $fuzzy_md5) = ('', '');
-    my ($total_height, $total_width, $total_area, $line_count) = (0,0,0,0);
+    dbg("pdfinfo: found part, type=$type file=$name");
 
-    my $name = $part->{'name'} || '';
-    $self->_set_tag($pms, 'PDFNAME', $name);
+    # filename must end with .pdf, or application type can be pdf
+    # sometimes windows muas will wrap a pdf up inside a .dat file
+    # v0.8 - Added .fdf phoney PDF detection
+    next unless ($name =~ /\.[fp]df$/i || $type =~ m@/pdf$@);
 
-    my $no_more_fuzzy = 0;
-    my $got_image = 0;
-    my $encrypted = 0;
+    _get_pdf_details($pms, $p);
+    $pms->{pdfinfo}->{count_pdf}++;
+  }
 
-    while($data =~ /([^\n]+)/g) {
-      # dbg("pdfinfo: line=$1");
-      my $line = $1;
+  _set_tag($pms, 'PDFCOUNT',  $pms->{pdfinfo}->{count_pdf});
+  _set_tag($pms, 'PDFIMGCOUNT', $pms->{pdfinfo}->{count_pdf_images});
+}
 
-      $line_count++;
+sub _get_pdf_details {
+  my ($pms, $part) = @_;
 
-      # lines containing high bytes will have no data we need, so save some cycles
-      next if ($line =~ /[\x80-\xff]/);
+  my $data = $part->decode();
 
-      if (!$no_more_fuzzy && $line_count < 70) {
-        if ($line !~ m/^\%/ && $line !~ m/^\/(?:Height|Width|(?:(?:Media|Crop)Box))/ && $line !~ m/^\d+\s+\d+\s+\d+\s+\d+\s+\d+\s+\d+\s+cm$/) {
-          $line =~ s/\s+$//;  # strip off whitespace at end.
-          $fuzzy_data .= $line;
-       }
-      }
+  # Remove UTF-8 BOM
+  $data =~ s/^\xef\xbb\xbf//;
 
-      if ($line =~ m/^\/([A-Za-z]+)/) {
-         $pdf_tags .= $1;
+  # Search magic in first 1024 bytes
+  if ($data !~ /^.{0,1024}\%PDF\-(\d\.\d)/s) {
+    dbg("pdfinfo: PDF magic header not found, invalid file?");
+    return;
+  }
+  my $version = $1;
+  _set_tag($pms, 'PDFVERSION', $version);
+  # dbg("pdfinfo: pdf version = $version");
+
+  my ($fuzzy_data, $pdf_tags);
+  my ($md5, $fuzzy_md5) = ('','');
+  my ($total_height, $total_width, $total_area, $line_count) = (0,0,0,0);
+
+  my $name = $part->{name} || '';
+  _set_tag($pms, 'PDFNAME', $name);
+  # store the file name so we can check pdf_named() or pdf_name_match() later.
+  $pms->{pdfinfo}->{names_pdf}->{$name} = 1 if $name;
+
+  my $no_more_fuzzy = 0;
+  my $got_image = 0;
+  my $encrypted = 0;
+  my %uris;
+
+  while ($data =~ /([^\n]+)/g) {
+    # dbg("pdfinfo: line=$1");
+    my $line = $1;
+
+    if (!$no_more_fuzzy && ++$line_count < 70) {
+      if ($line !~ m/^\%/ && $line !~ m/^\/(?:Height|Width|(?:(?:Media|Crop)Box))/ && $line !~ m/^\d+\s+\d+\s+\d+\s+\d+\s+\d+\s+\d+\s+cm$/) {
+        $line =~ s/\s+$//;  # strip off whitespace at end.
+        $fuzzy_data .= $line;
       }
+      # once we hit the first stream, we stop collecting data for fuzzy md5
+      $no_more_fuzzy = 1  if index($line, 'stream') >= 0;
+    }
 
-      $got_image=1 if ($line =~ m/\/Image/);
-      $encrypted=1 if ($line =~ m/^\/Encrypt/);
+    $got_image = 1  if index($line, '/Image') >= 0;
+    if (!$encrypted && index($line, '/Encrypt') == 0) {
+      # store encrypted flag.
+      $encrypted = $pms->{pdfinfo}->{encrypted} = 1;
+    }
 
-      # once we hit the first stream, we stop collecting data for fuzzy md5
-      $no_more_fuzzy = 1 if ($line =~ m/stream/);
-
-      # From a v1.3 pdf
-      # [12234] dbg: pdfinfo: line=630 0 0 149 0 0 cm
-      # [12234] dbg: pdfinfo: line=/Width 630
-      # [12234] dbg: pdfinfo: line=/Height 149
-      if ($got_image) {
-        if ($line =~ /^(\d+)\s+\d+\s+\d+\s+(\d+)\s+\d+\s+\d+\s+cm$/) {
-          $width = $1;
-          $height = $2;
-        }
-        elsif ($line =~ /^\/Width\s(\d+)/) {
-          $width = $1;
-        }
-        elsif ($line =~ /^\/Height\s(\d+)/) {
-          $height = $1;
-        }
-        elsif ($line =~ m/\/Width\s(\d+)\/Height\s(\d+)/) {
-          $width = $1;
-          $height = $2;
-        }
+    # From a v1.3 pdf
+    # [12234] dbg: pdfinfo: line=630 0 0 149 0 0 cm
+    # [12234] dbg: pdfinfo: line=/Width 630
+    # [12234] dbg: pdfinfo: line=/Height 149
+    if ($got_image) {
+      my ($width, $height);
+      if ($line =~ /^(\d+)\s+\d+\s+\d+\s+(\d+)\s+\d+\s+\d+\s+cm$/) {
+        $width = $1;
+        $height = $2;
       }
-
-      # did pdf contain image data?
-      if ($got_image && $width && $height) {
+      elsif ($line =~ /^\/Width\s(\d+)/) {
+        $width = $1;
+      }
+      elsif ($line =~ /^\/Height\s(\d+)/) {
+        $height = $1;
+      }
+      elsif ($line =~ m/\/Width\s(\d+)\/Height\s(\d+)/) {
+        $width = $1;
+        $height = $2;
+      }
+      if ($width && $height) {
         $no_more_fuzzy = 1;
         my $area = $width * $height;
         $total_height += $height;
         $total_width += $width;
         $total_area += $area;
         $pms->{pdfinfo}->{dems_pdf}->{"${height}x${width}"} = 1;
-        $pms->{'pdfinfo'}->{"count_pdf_images"} ++;
-        dbg("pdfinfo: Found image in PDF ".($name ? $name : '')." - $height x $width pixels ($area pixels sq.)");
-        $self->_set_tag($pms, 'PDFIMGDIM', "${height}x${width}");
-        $height=0; $width=0;  # reset and check for next image
-        $got_image = 0;
-      }
-
-      # [5310] dbg: pdfinfo: line=<</Producer(GPL Ghostscript 8.15)
-      # [5310] dbg: pdfinfo: line=/CreationDate(D:20070703144220)
-      # [5310] dbg: pdfinfo: line=/ModDate(D:20070703144220)
-      # [5310] dbg: pdfinfo: line=/Title(Microsoft Word - Document1)
-      # [5310] dbg: pdfinfo: line=/Creator(PScript5.dll Version 5.2)
-      # [5310] dbg: pdfinfo: line=/Author(colet)>>endobj
-      # or all on same line inside xml - v1.6+
-      # <</CreationDate(D:20070226165054-06'00')/Creator( Adobe Photoshop CS2 Windows)/Producer(Adobe Photoshop for Windows -- Image Conversion Plug-in)/ModDate(D:20070226165100-06'00')>>
-
-      if ($line =~ /\/Producer\s?\(([^\)\\]+)/) {
-        $producer = $1;
-      }
-      if ($line =~ /\/CreationDate\s?\(D\:(\d+)/) {
-        $created = $1;
-      }
-      if ($line =~ /\/ModDate\s?\(D\:(\d+)/) {
-        $modified = $1;
-      }
-      if ($line =~ /\/Title\s?\(([^\)\\]+)/) {
-        $title = $1;
-        # Title=\376\377\000w\000w\000n\000g
-        # Title=wwng
-        $title =~ s/\\\d{3}//g;
-      }
-      if ($line =~ /\/Creator\s?\(([^\)\\]+)/) {
-        $creator = $1;
-      }
-      if ($line =~ /\/Author\s?\(([^\)]+)/) {
-        $author = $1;
-        # Author=\376\377\000H\000P\000_\000A\000d\000m\000i\000n\000i\000s\000t\000r\000a\000t\000o\000r
-        # Author=HP_Administrator
-        $author =~ s/\\\d{3}//g;
+        $pms->{pdfinfo}->{count_pdf_images}++;
+        dbg("pdfinfo: Found image in PDF $name: $height x $width pixels ($area pixels sq.)");
+        _set_tag($pms, 'PDFIMGDIM', "${height}x${width}");
+        $got_image = $height = $width = 0;  # reset and check for next image
       }
     }
 
-    # store the file name so we can check pdf_named() or pdf_name_match() later.
-    $pms->{pdfinfo}->{names_pdf}->{$name} = 1 if $name;
-
-    # store encrypted flag.
-    $pms->{pdfinfo}->{encrypted} = $encrypted;
-
-    # if we had multiple images in the pdf, we need to store the total HxW as well.
-    # If it was a single Image PDF, then this value will already be in the hash.
-    $pms->{pdfinfo}->{dems_pdf}->{"${total_height}x${total_width}"} = 1 if ($total_height && $total_width);;
+    #
+    # Triage - expecting / to be found for rest of the checks
+    #
+    next unless index($line, '/') >= 0;
 
-    if ($total_area) {
-      $pms->{pdfinfo}->{pc_pdf} = $total_area;
-      $self->_set_tag($pms, 'PDFIMGAREA', $total_area);
-      dbg("pdfinfo: Filename=$name Total HxW: $total_height x $total_width ($total_area area)") if ($total_area);
+    if ($line =~ m/^\/([A-Za-z]+)/) {
+      $pdf_tags .= $1;
     }
 
-    dbg("pdfinfo: Filename=$name Title=$title Author=$author Producer=$producer Created=$created Modified=$modified");
-
-    $md5 = uc(md5_hex($data)) if $data;
-    $fuzzy_md5 = uc(md5_hex($fuzzy_data)) if $fuzzy_data;
-    my $tags_md5;
-    $tags_md5 = uc(md5_hex($pdf_tags)) if $pdf_tags;
-
-    dbg("pdfinfo: MD5 results for ".($name ? $name : '')." - md5=".($md5 ? $md5 : '')." fuzzy1=".($fuzzy_md5 ? $fuzzy_md5 : '')." fuzzy2=".($tags_md5 ? $tags_md5 : ''));
-
-    # we dont need tags for these.
-    $pms->{pdfinfo}->{details}->{created} = $created if $created;
-    $pms->{pdfinfo}->{details}->{modified} = $modified if $modified;
-
-    if ($producer) {
-      $pms->{pdfinfo}->{details}->{producer} = $producer if $producer;
-      $self->_set_tag($pms, 'PDFPRODUCER', $producer);
+    # XXX some pdf have uris but are stored inside binary data
+    if (keys %uris < 20 && $line =~ /(?:\/S\s{0,2}\/URI\s{0,2}|^\s*)\/URI\s{0,2}( \( .*? (?<!\\) \) | < [^>]* > )/x) {
+      my $location = _parse_string($1);
+      next unless index($location, '.') > 0; # ignore some binary mess
+      if (!exists $uris{$location}) {
+        $uris{$location} = 1;
+        dbg("pdfinfo: found URI: $location");
+        $pms->add_uri_detail_list($location);
+      }
     }
-    if ($title) {
-      $pms->{pdfinfo}->{details}->{title} = $title;
-      $self->_set_tag($pms, 'PDFTITLE', $title);
+
+    # [5310] dbg: pdfinfo: line=<</Producer(GPL Ghostscript 8.15)
+    # [5310] dbg: pdfinfo: line=/CreationDate(D:20070703144220)
+    # [5310] dbg: pdfinfo: line=/ModDate(D:20070703144220)
+    # [5310] dbg: pdfinfo: line=/Title(Microsoft Word - Document1)
+    # [5310] dbg: pdfinfo: line=/Creator(PScript5.dll Version 5.2)
+    # [5310] dbg: pdfinfo: line=/Author(colet)>>endobj
+    # or all on same line inside xml - v1.6+
+    # <</CreationDate(D:20070226165054-06'00')/Creator( Adobe Photoshop CS2 Windows)/Producer(Adobe Photoshop for Windows -- Image Conversion Plug-in)/ModDate(D:20070226165100-06'00')>>
+    # Or hex values
+    # /Creator<FEFF005700720069007400650072>
+    if ($line =~ /\/Author\s{0,2}( \( .*? (?<!\\) \) | < [^>]* > )/x) {
+      my $author = _parse_string($1);
+      dbg("pdfinfo: found property Author=$author");
+      $pms->{pdfinfo}->{details}->{author}->{$author} = 1;
+      _set_tag($pms, 'PDFAUTHOR', $author);
     }
-    if ($creator) {
-      $pms->{pdfinfo}->{details}->{creator} = $creator;
-      $self->_set_tag($pms, 'PDFCREATOR', $creator);
+    if ($line =~ /\/Creator\s{0,2}( \( .*? (?<!\\) \) | < [^>]* > )/x) {
+      my $creator = _parse_string($1);
+      dbg("pdfinfo: found property Creator=$creator");
+      $pms->{pdfinfo}->{details}->{creator}->{$creator} = 1;
+      _set_tag($pms, 'PDFCREATOR', $creator);
     }
-    if ($author) {
-      $pms->{pdfinfo}->{details}->{author} = $author;
-      $self->_set_tag($pms, 'PDFAUTHOR', $author);
+    if ($line =~ /\/CreationDate\s{0,2}\(D\:(\d+)/) {
+      my $created = _parse_string($1);
+      dbg("pdfinfo: found property Created=$created");
+      $pms->{pdfinfo}->{details}->{created}->{$created} = 1;
     }
-    if ($md5) {
-      $pms->{pdfinfo}->{md5}->{$md5} = 1;
-      $self->_set_tag($pms, 'PDFMD5', $fuzzy_md5);
+    if ($line =~ /\/ModDate\s{0,2}\(D\:(\d+)/) {
+      my $modified = _parse_string($1);
+      dbg("pdfinfo: found property Modified=$modified");
+      $pms->{pdfinfo}->{details}->{modified}->{$modified} = 1;
     }
-    if ($fuzzy_md5) {
-      $pms->{pdfinfo}->{fuzzy_md5}->{$fuzzy_md5} = 1;
-      $self->_set_tag($pms, 'PDFMD5FUZZY1', $fuzzy_md5);
+    if ($line =~ /\/Producer\s{0,2}( \( .*? (?<!\\) \) | < [^>]* > )/x) {
+      my $producer = _parse_string($1);
+      dbg("pdfinfo: found property Producer=$producer");
+      $pms->{pdfinfo}->{details}->{producer}->{$producer} = 1;
+      _set_tag($pms, 'PDFPRODUCER', $producer);
     }
-    if ($tags_md5) {
-      $pms->{pdfinfo}->{fuzzy_md5}->{$tags_md5} = 1;
-      $self->_set_tag($pms, 'PDFMD5FUZZY2', $tags_md5);
+    if ($line =~ /\/Title\s{0,2}( \( .*? (?<!\\) \) | < [^>]* > )/x) {
+      my $title = _parse_string($1);
+      dbg("pdfinfo: found property Title=$title");
+      $pms->{pdfinfo}->{details}->{title}->{$title} = 1;
+      _set_tag($pms, 'PDFTITLE', $title);
     }
-  },
-
-);
+  }
 
-# ----------------------------------------
+  # if we had multiple images in the pdf, we need to store the total HxW as well.
+  # If it was a single Image PDF, then this value will already be in the hash.
+  $pms->{pdfinfo}->{dems_pdf}->{"${total_height}x${total_width}"} = 1 if ($total_height && $total_width);
 
-sub _set_tag {
+  if ($total_area) {
+    $pms->{pdfinfo}->{pc_pdf} = $total_area;
+    _set_tag($pms, 'PDFIMGAREA', $total_area);
+    dbg("pdfinfo: Total HxW: $total_height x $total_width ($total_area area)");
+  }
 
-  my ($self, $pms, $tag, $value) = @_;
+  $md5 = uc(md5_hex($data)) if $data;
+  $fuzzy_md5 = uc(md5_hex($fuzzy_data)) if $fuzzy_data;
+  my $tags_md5 = '';
+  $tags_md5 = uc(md5_hex($pdf_tags)) if $pdf_tags;
 
-  dbg("pdfinfo: set_tag called for $tag $value");
-  return unless ($tag && $value);
+  dbg("pdfinfo: MD5 results for $name: md5=$md5 fuzzy1=$fuzzy_md5 fuzzy2=$tags_md5");
 
-  if (exists $pms->{tag_data}->{$tag}) {
-    $pms->{tag_data}->{$tag} .= " $value";  # append value
+  if ($md5) {
+    $pms->{pdfinfo}->{md5}->{$md5} = 1;
+    _set_tag($pms, 'PDFMD5', $fuzzy_md5);
   }
-  else {
-    $pms->{tag_data}->{$tag} = $value;
+  if ($fuzzy_md5) {
+    $pms->{pdfinfo}->{fuzzy_md5}->{$fuzzy_md5} = 1;
+    _set_tag($pms, 'PDFMD5FUZZY1', $fuzzy_md5);
+  }
+  if ($tags_md5) {
+    $pms->{pdfinfo}->{fuzzy_md5}->{$tags_md5} = 1;
+    _set_tag($pms, 'PDFMD5FUZZY2', $tags_md5);
   }
 }
 
-# ----------------------------------------
-
-sub _find_pdf_mime_parts {
-  my ($self,$pms) = @_;
-
-  # bail early if message does not have pdf parts
-  return 0 if (exists $pms->{'pdfinfo'}->{'no_parts'});
-
-  # initialize
-  $pms->{'pdfinfo'}->{"pc_pdf"} = 0;
-  $pms->{'pdfinfo'}->{"count_pdf"} = 0;
-  $pms->{'pdfinfo'}->{"count_pdf_images"} = 0;
-
-  my @parts = $pms->{msg}->find_parts(qr@^(image|application)/(pdf|octet\-stream)$@, 1);
-  my $part_count = scalar @parts;
-
-  dbg("pdfinfo: Identified $part_count possible mime parts that need checked for PDF content");
-
-  # cache this so we can easily bail
-  $pms->{'pdfinfo'}->{'no_parts'} = 1 unless $part_count;
-
-  foreach my $p (@parts) {
-    my $type = $p->{'type'} =~ m@/([\w\-]+)$@;
-    my $name = $p->{'name'} || '';
-
-    my $cte = lc( $p->get_header('content-transfer-encoding') || '' );
-
-    dbg("pdfinfo: found part, type=".($type ? $type : '')." file=".($name ? $name : '')." cte=".($cte ? $cte : '')."");
-
-    # make sure its a cte we support
-    next unless ($cte =~ /^(?:base64|quoted\-printable)$/);
+sub _parse_string {
+  local $_ = shift;
+  # Anything inside < > is hex encoded
+  if (/^</) {
+    # Might contain whitespace so search all hex values
+    my $str = '';
+    $str .= pack("H*", $1) while (/([0-9A-Fa-f]{2})/g);
+    $_ = $str;
+    # Handle/strip UTF-16 (in ultra-naive way for now)
+    s/\x00//g if (s/^(?:\xfe\xff|\xff\xfe)//);
+  } else {
+    s/^\(//; s/\)$//;
+    # Decode octals
+    # Author=\376\377\000H\000P\000_\000A\000d\000m\000i\000n\000i\000s\000t\000r\000a\000t\000o\000r
+    s/(?<!\\)\\([0-3][0-7][0-7])/pack("C",oct($1))/ge;
+    # Handle/strip UTF-16 (in ultra-naive way for now)
+    s/\x00//g if (s/^(?:\xfe\xff|\xff\xfe)//);
+    # Unescape some stuff like \\ \( \)
+    # Title(Foo \(bar\))
+    s/\\([()\\])/$1/g;
+  }
+  # Limit to some sane length
+  return substr($_, 0, 256);
+}
 
-    # filename must end with .pdf, or application type can be pdf
-    # sometimes windows muas will wrap a pdf up inside a .dat file
-    # v0.8 - Added .fdf phoney PDF detection
-    next unless ($name =~ /\.[fp]df$/ || $type eq 'pdf');
+sub _set_tag {
+  my ($pms, $tag, $value) = @_;
 
-    # if we get this far, make sure type is pdf for sure (not octet-stream or anything else)
-    $type='pdf';
+  return unless defined $value && $value ne '';
+  dbg("pdfinfo: set_tag called for $tag: $value");
 
-    if ($type && exists $get_details{$type}) {
-       $get_details{$type}->($self, $pms, $p);
-       $pms->{'pdfinfo'}->{"count_$type"} ++;
+  if (exists $pms->{tag_data}->{$tag}) {
+    # Limit to some sane length
+    if (length($pms->{tag_data}->{$tag}) < 2048) {
+      $pms->{tag_data}->{$tag} .= ' '.$value;  # append value
     }
   }
-
-  $self->_set_tag($pms, 'PDFCOUNT',  $pms->{'pdfinfo'}->{"count_pdf"});
-  $self->_set_tag($pms, 'PDFIMGCOUNT', $pms->{'pdfinfo'}->{"count_pdf_images"});
-
+  else {
+    $pms->{tag_data}->{$tag} = $value;
+  }
 }
 
-# ----------------------------------------
-
 sub pdf_named {
-  my ($self,$pms,$body,$name) = @_;
-  return unless (defined $name);
+  my ($self, $pms, $body, $name) = @_;
 
-  # make sure we have image data read in.
-  if (!exists $pms->{'pdfinfo'}) {
-    $self->_find_pdf_mime_parts($pms);
-  }
-
-  return 0 if (exists $pms->{'pdfinfo'}->{'no_parts'});
+  return 0 unless defined $name;
 
-  return 0 unless (exists $pms->{'pdfinfo'}->{"names_pdf"});
-  return 1 if (exists $pms->{'pdfinfo'}->{"names_pdf"}->{$name});
+  return 1 if exists $pms->{pdfinfo}->{names_pdf}->{$name};
   return 0;
 }
 
-# -----------------------------------------
-
 sub pdf_name_regex {
-  my ($self,$pms,$body,$re) = @_;
-  return unless (defined $re);
-
-  # make sure we have image data read in.
-  if (!exists $pms->{'pdfinfo'}) {
-    $self->_find_pdf_mime_parts($pms);
-  }
+  my ($self, $pms, $body, $regex) = @_;
 
-  return 0 if (exists $pms->{'pdfinfo'}->{'no_parts'});
-  return 0 unless (exists $pms->{'pdfinfo'}->{"names_pdf"});
+  return 0 unless defined $regex;
+  return 0 unless exists $pms->{pdfinfo}->{names_pdf};
 
-  my ($rec, $err) = compile_regexp($re, 2);
+  my ($rec, $err) = compile_regexp($regex, 2);
   if (!$rec) {
-    info("pdfinfo: invalid regexp '$re': $err");
+    my $rulename = $pms->get_current_eval_rule_name();
+    warn "pdfinfo: invalid regexp for $rulename '$regex': $err";
     return 0;
   }
 
-  my $hit = 0;
-  foreach my $name (keys %{$pms->{'pdfinfo'}->{"names_pdf"}}) {
+  foreach my $name (keys %{$pms->{pdfinfo}->{names_pdf}}) {
     if ($name =~ $rec) {
       dbg("pdfinfo: pdf_name_regex hit on $name");
       return 1;
     }
   }
-  return 0;
 
+  return 0;
 }
 
-# -----------------------------------------
-
 sub pdf_is_encrypted {
-  my ($self,$pms,$body) = @_;
+  my ($self, $pms, $body) = @_;
 
-  # make sure we have image data read in.
-  if (!exists $pms->{'pdfinfo'}) {
-    $self->_find_pdf_mime_parts($pms);
-  }
-
-  return 0 if (exists $pms->{'pdfinfo'}->{'no_parts'});
-  return $pms->{'pdfinfo'}->{'encrypted'};
+  return $pms->{pdfinfo}->{encrypted} ? 1 : 0;
 }
 
-# -----------------------------------------
-
 sub pdf_count {
-  my ($self,$pms,$body,$min,$max) = @_;
-  return unless defined $min;
-
-  # make sure we have image data read in.
-  if (!exists $pms->{'pdfinfo'}) {
-    $self->_find_pdf_mime_parts($pms);
-  }
-
-  return 0 if (exists $pms->{'pdfinfo'}->{'no_parts'});
-  return 0 unless (exists $pms->{'pdfinfo'}->{"count_pdf"});
-  return result_check($min, $max, $pms->{'pdfinfo'}->{"count_pdf"});
+  my ($self, $pms, $body, $min, $max) = @_;
 
+  return _result_check($min, $max, $pms->{pdfinfo}->{count_pdf});
 }
 
-# -----------------------------------------
-
 sub pdf_image_count {
-  my ($self,$pms,$body,$min,$max) = @_;
-  return unless defined $min;
-
-  # make sure we have image data read in.
-  if (!exists $pms->{'pdfinfo'}) {
-    $self->_find_pdf_mime_parts($pms);
-  }
-
-  return 0 if (exists $pms->{'pdfinfo'}->{'no_parts'});
-  return 0 unless (exists $pms->{'pdfinfo'}->{"count_pdf_images"});
-  return result_check($min, $max, $pms->{'pdfinfo'}->{"count_pdf_images"});
+  my ($self, $pms, $body, $min, $max) = @_;
 
+  return _result_check($min, $max, $pms->{pdfinfo}->{count_pdf_images});
 }
 
-# -----------------------------------------
-
 sub pdf_pixel_coverage {
   my ($self,$pms,$body,$min,$max) = @_;
-  return unless (defined $min);
-
-  # make sure we have image data read in.
-  if (!exists $pms->{'pdfinfo'}) {
-    $self->_find_pdf_mime_parts($pms);
-  }
-
-  return 0 if (exists $pms->{'pdfinfo'}->{'no_parts'});
-  return 0 unless (exists $pms->{'pdfinfo'}->{"pc_pdf"});
 
-  # dbg("pdfinfo: pc_$type: $min, ".($max ? $max:'').", $type, ".$pms->{'pdfinfo'}->{"pc_pdf"});
-  return result_check($min, $max, $pms->{'pdfinfo'}->{"pc_pdf"});
+  return _result_check($min, $max, $pms->{pdfinfo}->{pc_pdf});
 }
 
-# -----------------------------------------
-
 sub pdf_image_to_text_ratio {
-  my ($self,$pms,$body,$min,$max) = @_;
-  return unless (defined $min && defined $max);
+  my ($self, $pms, $body, $min, $max) = @_;
 
-  # make sure we have image data read in.
-  if (!exists $pms->{'pdfinfo'}) {
-    $self->_find_pdf_mime_parts($pms);
-  }
-
-  return 0 if (exists $pms->{'pdfinfo'}->{'no_parts'});
-  return 0 unless (exists $pms->{'pdfinfo'}->{"pc_pdf"});
+  return 0 unless defined $max;
+  return 0 unless $pms->{pdfinfo}->{pc_pdf};
 
   # depending on how you call this eval (body vs rawbody),
   # the $textlen will differ.
-  my $textlen = length(join('',@$body));
-
-  return 0 unless ( $textlen > 0 && exists $pms->{'pdfinfo'}->{"pc_pdf"} && $pms->{'pdfinfo'}->{"pc_pdf"} > 0);
+  my $textlen = length(join('', @$body));
+  return 0 unless $textlen;
 
-  my $ratio = $textlen / $pms->{'pdfinfo'}->{"pc_pdf"};
+  my $ratio = $textlen / $pms->{pdfinfo}->{pc_pdf};
   dbg("pdfinfo: image ratio=$ratio, min=$min max=$max");
-  return result_check($min, $max, $ratio, 1);
-}
 
-# -----------------------------------------
+  return _result_check($min, $max, $ratio, 1);
+}
 
 sub pdf_is_empty_body {
-  my ($self,$pms,$body,$min) = @_;
+  my ($self, $pms, $body, $min) = @_;
 
+  return 0 unless $pms->{pdfinfo}->{count_pdf};
   $min ||= 0;  # default to 0 bytes
 
-  # make sure we have image data read in.
-  if (!exists $pms->{'pdfinfo'}) {
-    $self->_find_pdf_mime_parts($pms);
-  }
-
-  return 0 if (exists $pms->{'pdfinfo'}->{'no_parts'});
-  return 0 unless $pms->{'pdfinfo'}->{"count_pdf"};
-
-  # check for cached result
-  return 1 if $pms->{'pdfinfo'}->{"no_body_text"};
-
-  shift @$body;  # shift body array removes line #1 -> subject line.
-
   my $bytes = 0;
-  my $textlen = length(join('',@$body));
+  my $idx = 0;
   foreach my $line (@$body) {
-    next unless ($line =~ m/\S/);
-    next if ($line =~ m/^Subject/);
+    next if $idx++ == 0; # skip subject line
+    next unless $line =~ /\S/;
     $bytes += length($line);
+    # no hit if minimum already exceeded
+    return 0 if $bytes > $min;
   }
 
-  dbg("pdfinfo: is_empty_body = $bytes bytes");
-
-  if ($bytes == 0 || ($bytes <= $min)) {
-    $pms->{'pdfinfo'}->{"no_body_text"} = 1;
-    return 1;
-  }
-
-  # cache it and return 0
-  $pms->{'pdfinfo'}->{"no_body_text"} = 0;
-  return 0;
+  dbg("pdfinfo: pdf_is_empty_body matched ($bytes <= $min)");
+  return 1;
 }
 
-# -----------------------------------------
-
 sub pdf_image_size_exact {
-  my ($self,$pms,$body,$height,$width) = @_;
-  return unless (defined $height && defined $width);
+  my ($self, $pms, $body, $height, $width) = @_;
 
-  # make sure we have image data read in.
-  if (!exists $pms->{'pdfinfo'}) {
-    $self->_find_pdf_mime_parts($pms);
-  }
+  return 0 unless defined $width;
 
-  return 0 if (exists $pms->{'pdfinfo'}->{'no_parts'});
-  return 0 unless (exists $pms->{'pdfinfo'}->{"dems_pdf"});
-  return 1 if (exists $pms->{'pdfinfo'}->{"dems_pdf"}->{"${height}x${width}"});
+  return 1 if exists $pms->{pdfinfo}->{dems_pdf}->{"${height}x${width}"};
   return 0;
 }
 
-# -----------------------------------------
-
 sub pdf_image_size_range {
-  my ($self,$pms,$body,$minh,$minw,$maxh,$maxw) = @_;
-  return unless (defined $minh && defined $minw);
+  my ($self, $pms, $body, $minh, $minw, $maxh, $maxw) = @_;
 
-  # make sure we have image data read in.
-  if (!exists $pms->{'pdfinfo'}) {
-    $self->_find_pdf_mime_parts($pms);
-  }
-
-  return 0 if (exists $pms->{'pdfinfo'}->{'no_parts'});
-  return 0 unless (exists $pms->{'pdfinfo'}->{"dems_pdf"});
+  return 0 unless defined $minw;
+  return 0 unless exists $pms->{pdfinfo}->{dems_pdf};
 
-  foreach my $dem ( keys %{$pms->{'pdfinfo'}->{"dems_pdf"}}) {
-    my ($h,$w) = split(/x/,$dem);
+  foreach my $dem (keys %{$pms->{pdfinfo}->{dems_pdf}}) {
+    my ($h, $w) = split(/x/, $dem);
     next if ($h < $minh);  # height less than min height
     next if ($w < $minw);  # width less than min width
     next if (defined $maxh && $h > $maxh);  # height more than max height
     next if (defined $maxw && $w > $maxw);  # width more than max width
-
     # if we make it here, we have a match
     return 1;
   }
@@ -664,88 +578,54 @@ sub pdf_image_size_range {
   return 0;
 }
 
-# -----------------------------------------
-
 sub pdf_match_md5 {
+  my ($self, $pms, $body, $md5) = @_;
 
-  my ($self,$pms,$body,$md5) = @_;
-  return unless defined $md5;
+  return 0 unless defined $md5;
 
-  my $uc_md5 = uc($md5);  # uppercase matches only
-
-  # make sure we have pdf data read in.
-  if (!exists $pms->{'pdfinfo'}) {
-    $self->_find_pdf_mime_parts($pms);
-  }
-
-  return 0 if (exists $pms->{'pdfinfo'}->{'no_parts'});
-  return 0 unless (exists $pms->{'pdfinfo'}->{"md5"});
-  return 1 if (exists $pms->{'pdfinfo'}->{"md5"}->{$uc_md5});
+  return 1 if exists $pms->{pdfinfo}->{md5}->{uc $md5};
   return 0;
 }
 
-# -----------------------------------------
-
 sub pdf_match_fuzzy_md5 {
+  my ($self, $pms, $body, $md5) = @_;
 
-  my ($self,$pms,$body,$md5) = @_;
-  return unless defined $md5;
-
-  my $uc_md5 = uc($md5);  # uppercase matches only
+  return 0 unless defined $md5;
 
-  # make sure we have pdf data read in.
-  if (!exists $pms->{'pdfinfo'}) {
-    $self->_find_pdf_mime_parts($pms);
-  }
-
-  return 0 if (exists $pms->{'pdfinfo'}->{'no_parts'});
-  return 0 unless (exists $pms->{'pdfinfo'}->{"fuzzy_md5"});
-  return 1 if (exists $pms->{'pdfinfo'}->{"fuzzy_md5"}->{$uc_md5});
+  return 1 if exists $pms->{pdfinfo}->{fuzzy_md5}->{uc $md5};
   return 0;
 }
 
-# -----------------------------------------
-
 sub pdf_match_details {
   my ($self, $pms, $body, $detail, $regex) = @_;
-  return unless ($detail && $regex);
-
-  # make sure we have pdf data read in.
-  if (!exists $pms->{'pdfinfo'}) {
-    $self->_find_pdf_mime_parts($pms);
-  }
-
-  return 0 if (exists $pms->{'pdfinfo'}->{'no_parts'});
-  return 0 unless (exists $pms->{'pdfinfo'}->{'details'});
 
-  my $check_value = $pms->{pdfinfo}->{details}->{$detail};
-  return unless $check_value;
+  return 0 unless defined $regex;
+  return 0 unless exists $pms->{pdfinfo}->{details}->{$detail};
 
   my ($rec, $err) = compile_regexp($regex, 2);
   if (!$rec) {
-    info("pdfinfo: invalid regexp '$regex': $err");
+    my $rulename = $pms->get_current_eval_rule_name();
+    warn "pdfinfo: invalid regexp for $rulename '$regex': $err";
     return 0;
   }
 
-  if ($check_value =~ $rec) {
-    dbg("pdfinfo: pdf_match_details $detail $regex matches $check_value");
-    return 1;
+  foreach (keys %{$pms->{pdfinfo}->{details}->{$detail}}) {
+    if ($_ =~ $rec) {
+      dbg("pdfinfo: pdf_match_details $detail ($regex) match: $_");
+      return 1;
+    }
   }
+
   return 0;
 }
 
-# -----------------------------------------
-
-sub result_check {
+sub _result_check {
   my ($min, $max, $value, $nomaxequal) = @_;
-  return 0 unless defined $value;
-  return 0 if ($value < $min);
-  return 0 if (defined $max && $value > $max);
-  return 0 if (defined $nomaxequal && $nomaxequal && $value == $max);
+  return 0 unless defined $min && defined $value;
+  return 0 if $value < $min;
+  return 0 if defined $max && $value > $max;
+  return 0 if defined $nomaxequal && $nomaxequal && $value == $max;
   return 1;
 }
 
-# -----------------------------------------
-
 1;
-
index 0270930bf126485ff20c3cd51353b78c3d67478b..57bcab0e59732a97d52e4b9c5c3edfcda649f85d 100644 (file)
@@ -21,8 +21,10 @@ package Mail::SpamAssassin::Plugin::PhishTag;
 
 use strict;
 use warnings;
+use re 'taint';
 use Errno qw(EBADF);
-use Mail::SpamAssassin;
+
+use Mail::SpamAssassin::Plugin;
 use Mail::SpamAssassin::Logger;
 
 our @ISA = qw(Mail::SpamAssassin::Plugin);
@@ -220,7 +222,7 @@ PhishTag - SpamAssassin plugin for redirecting links in incoming emails.
 =head1 DESCRIPTION
 
 PhishTag enables administrators to rewrite links in emails that trigger certain
-tests, preferably anti-phishing blacklist tests. The plugin will inhibit the
+tests, preferably anti-phishing blocklist tests. The plugin will inhibit the
 blocking of a portion of the emails that trigger the test by SpamAssassin, and
 let them pass to the users' inbox after the rewrite. It is useful in providing
 training to email users about company policies and general email usage.
index 2edc0198a38dffaa2c88040bf2be7a4fd6f34e44..5957bed26532f2f936b1c61c243c46456a71495c 100644 (file)
@@ -1,6 +1,6 @@
 #
 # Author: Giovanni Bechis <gbechis@apache.org>
-# Copyright 2018,2019 Giovanni Bechis
+# Copyright 2018,2020 Giovanni Bechis
 #
 # <@LICENSE>
 # Licensed to the Apache Software Foundation (ASF) under one or more
@@ -31,28 +31,33 @@ Mail::SpamAssassin::Plugin::Phishing - check uris against phishing feed
   ifplugin Mail::SpamAssassin::Plugin::Phishing
     phishing_openphish_feed /etc/mail/spamassassin/openphish-feed.txt
     phishing_phishtank_feed /etc/mail/spamassassin/phishtank-feed.csv
+    phishing_phishstats_feed /etc/mail/spamassassin/phishstats-feed.csv
     body     URI_PHISHING      eval:check_phishing()
     describe URI_PHISHING      Url match phishing in feed
   endif
 
 =head1 DESCRIPTION
 
-This plugin finds uris used in phishing campaigns detected by 
-OpenPhish or PhishTank feeds.
+This plugin finds uris used in phishing campaigns detected by
+OpenPhish, PhishTank or PhishStats feeds.
 
 The Openphish free feed is updated every 6 hours and can be downloaded from
 https://openphish.com/feed.txt.
-The Premium Openphish feed is not currently supported.
 
 The PhishTank free feed is updated every 1 hours and can be downloaded from
 http://data.phishtank.com/data/online-valid.csv.
 To avoid download limits a registration is required.
 
+The PhishStats feed is updated every 90 minutes and can be downloaded from
+https://phishstats.info/phish_score.csv.
+
 =cut
 
 package Mail::SpamAssassin::Plugin::Phishing;
 use strict;
 use warnings;
+use re 'taint';
+
 my $VERSION = 1.1;
 
 use Errno qw(EBADF);
@@ -61,7 +66,7 @@ use Mail::SpamAssassin::PerMsgStatus;
 
 our @ISA = qw(Mail::SpamAssassin::Plugin);
 
-sub dbg { Mail::SpamAssassin::Plugin::dbg ("Phishing: @_"); }
+sub dbg { my $msg = shift; Mail::SpamAssassin::Plugin::dbg("Phishing: $msg", @_); }
 
 sub new {
     my ($class, $mailsa) = @_;
@@ -71,7 +76,7 @@ sub new {
     bless ($self, $class);
 
     $self->set_config($mailsa->{conf});
-    $self->register_eval_rule("check_phishing");
+    $self->register_eval_rule("check_phishing", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
 
     return $self;
 }
@@ -81,14 +86,98 @@ sub set_config {
     my @cmds;
     push(@cmds, {
         setting => 'phishing_openphish_feed',
+        is_admin => 1,
         type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
         }
     );
+
+=head1 ADMIN PREFERENCES
+
+The following options can be used in site-wide (C<local.cf>)
+configuration files to customize how the module handles phishing uris
+
+=cut
+
+=over 4
+
+=item phishing_openphish_feed
+
+Absolute path of the downloaded OpenPhish datafeed.
+
+=back
+
+=cut
     push(@cmds, {
         setting => 'phishing_phishtank_feed',
+        is_admin => 1,
         type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
         }
     );
+
+=over 4
+
+=item phishing_phishtank_feed
+
+Absolute path of the downloaded PhishTank datafeed.
+
+=back
+
+=cut
+    push(@cmds, {
+        setting => 'phishing_uri_noparam',
+        is_admin => 1,
+        default => 0,
+        type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
+        }
+    );
+
+=over 4
+
+=item phishing_uri_noparam ( 0 | 1 ) (default: 0)
+
+If this option is set uri parameters will not be take into consideration
+when parsing the phishing uris datafeed.
+If this option is enabled and the url without parameters is "generic"
+(like https://www.kisa.link/url_redirector.php?url=...) the url will be
+skipped.
+
+=back
+
+=cut
+    push(@cmds, {
+        setting => 'phishing_phishstats_feed',
+        is_admin => 1,
+        type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
+        }
+    );
+
+=over 4
+
+=item phishing_phishstats_feed
+
+Absolute path of the downloaded PhishStats datafeed.
+
+=back
+
+=cut
+    push(@cmds, {
+        setting => 'phishing_phishstats_minscore',
+        is_admin => 1,
+        default => 6,
+        type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
+        }
+    );
+
+=over 4
+
+=item phishing_phishstats_minscore ( 0 - 10 ) (default: 6)
+
+Minimum score to take into consideration for phishing uris downloaded
+from PhishStats datafeed.
+
+=back
+
+=cut
     $conf->{parser}->register_commands(\@cmds);
 }
 
@@ -100,7 +189,8 @@ sub finish_parsing_end {
 sub _read_configfile {
   my ($self) = @_;
   my $conf = $self->{main}->{registryboundaries}->{conf};
-  my @phtank_ln;
+  my (@phtank_ln, @phstats_ln);
+  my $stripped_cluri;
 
   local *F;
   if ( defined($conf->{phishing_openphish_feed}) && ( -f $conf->{phishing_openphish_feed} ) ) {
@@ -109,10 +199,14 @@ sub _read_configfile {
         chomp;
         #lines that start with pound are comments
         next if(/^\s*\#/);
+        $stripped_cluri = $_;
+       if ( $conf->{phishing_uri_noparam} eq 1 ) {
+          $stripped_cluri =~ s/\?.*//;
+       }
         my $phishdomain = $self->{main}->{registryboundaries}->uri_to_domain($_);
         if ( defined $phishdomain ) {
-          push @{$self->{PHISHING}->{$_}->{phishdomain}}, $phishdomain;
-          push @{$self->{PHISHING}->{$_}->{phishinfo}->{$phishdomain}}, "OpenPhish";
+          push @{$self->{PHISHING}->{$stripped_cluri}->{phishdomain}}, $phishdomain;
+          push @{$self->{PHISHING}->{$stripped_cluri}->{phishinfo}->{$phishdomain}}, "OpenPhish";
         }
     }
 
@@ -133,11 +227,47 @@ sub _read_configfile {
 
         @phtank_ln = split(/,/, $_);
         $phtank_ln[1] =~ s/\"//g;
-
+        $stripped_cluri = $phtank_ln[1];
+       if ( $conf->{phishing_uri_noparam} eq 1 ) {
+          $stripped_cluri =~ s/\?.*//;
+       }
         my $phishdomain = $self->{main}->{registryboundaries}->uri_to_domain($phtank_ln[1]);
         if ( defined $phishdomain ) {
-          push @{$self->{PHISHING}->{$phtank_ln[1]}->{phishdomain}}, $phishdomain;
-          push @{$self->{PHISHING}->{$phtank_ln[1]}->{phishinfo}->{$phishdomain}}, "PhishTank";
+          push @{$self->{PHISHING}->{$stripped_cluri}->{phishdomain}}, $phishdomain;
+          push @{$self->{PHISHING}->{$stripped_cluri}->{phishinfo}->{$phishdomain}}, "PhishTank";
+        }
+    }
+
+    defined $_ || $!==0  or
+      $!==EBADF ? dbg("PHISHING: error reading config file: $!")
+                : die "error reading config file: $!";
+    close(F) or die "error closing config file: $!";
+  }
+
+  if ( defined($conf->{phishing_phishstats_feed}) && (-f $conf->{phishing_phishstats_feed} ) ) {
+    open(F, '<', $conf->{phishing_phishstats_feed});
+    for ($!=0; <F>; $!=0) {
+        #skip first line
+        next if ( $. eq 1);
+        chomp;
+        #lines that start with pound are comments
+        next if(/^\s*\#/);
+
+       # CSV: Date,Score,URL,IP
+        @phstats_ln = split(/,/, $_);
+        $phstats_ln[1] =~ s/\"//g;
+        $phstats_ln[2] =~ s/\"//g;
+       if ( $conf->{phishing_phishstats_minscore} >= $phstats_ln[1] ) {
+         next;
+       }
+        $stripped_cluri = $phstats_ln[2];
+       if ( $conf->{phishing_uri_noparam} eq 1 ) {
+          $stripped_cluri =~ s/\?.*//;
+       }
+        my $phishdomain = $self->{main}->{registryboundaries}->uri_to_domain($phstats_ln[2]);
+        if ( defined $phishdomain ) {
+          push @{$self->{PHISHING}->{$stripped_cluri}->{phishdomain}}, $phishdomain;
+          push @{$self->{PHISHING}->{$stripped_cluri}->{phishinfo}->{$phishdomain}}, "PhishStats";
         }
     }
 
@@ -146,6 +276,7 @@ sub _read_configfile {
                 : die "error reading config file: $!";
     close(F) or die "error closing config file: $!";
   }
+
 }
 
 sub check_phishing {
@@ -153,10 +284,11 @@ sub check_phishing {
 
   my $feedname;
   my $domain;
-  my $uris = $pms->get_uri_detail_list();
+  my $stripped_cluri;
+  my $dcnt;
 
+  my $uris = $pms->get_uri_detail_list();
   my $rulename = $pms->get_current_eval_rule_name();
-
   while (my($uri, $info) = each %{$uris}) {
     # we want to skip mailto: uris
     next if ($uri =~ /^mailto:/i);
@@ -166,12 +298,21 @@ sub check_phishing {
     if (($info->{types}->{a}) || ($info->{types}->{parsed})) {
       # check url
       foreach my $cluri (@{$info->{cleaned}}) {
-        if ( exists $self->{PHISHING}->{$cluri} ) {
+        $stripped_cluri = $cluri;
+       if( $self->{main}->{conf}->{phishing_uri_noparam} eq 1 ) {
+          $stripped_cluri =~ s/\?.*//;
+          $dcnt = $stripped_cluri =~ tr/\///;
+       }
+       # If uri without parameters are considered, skip too short uris
+       # like https://www.google.com/url?sa=t&url=http://badsite.com
+        if( ($self->{main}->{conf}->{phishing_uri_noparam} eq 1) && ($dcnt <= 3) ) {
+          next;
+        }
+        if ( exists $self->{PHISHING}->{$stripped_cluri} ) {
           $domain = $self->{main}->{registryboundaries}->uri_to_domain($cluri);
-          $feedname = $self->{PHISHING}->{$cluri}->{phishinfo}->{$domain}[0];
-          dbg("HIT! $domain [$cluri] found in $feedname feed");
-          $pms->test_log("$feedname ($domain)");
-          $pms->got_hit($rulename, "", ruletype => 'eval');
+          $feedname = $self->{PHISHING}->{$stripped_cluri}->{phishinfo}->{$domain}[0];
+          dbg("HIT! $domain [$stripped_cluri] found in $feedname feed");
+          $pms->test_log("$feedname ($domain)", $rulename);
           return 1;
         }
       }
index e694bd62fdae387a5fa2763d41613ac7f4465894..97578d726db9f952d4509bb1d650c8a25f19fc7f 100644 (file)
@@ -37,13 +37,17 @@ package Mail::SpamAssassin::Plugin::Pyzor;
 use Mail::SpamAssassin::Plugin;
 use Mail::SpamAssassin::Logger;
 use Mail::SpamAssassin::Timeout;
-use Mail::SpamAssassin::Util qw(untaint_var untaint_file_path
-                                proc_status_ok exit_status_str);
+use Mail::SpamAssassin::SubProcBackChannel;
+use Mail::SpamAssassin::Util qw(untaint_var untaint_file_path am_running_on_windows
+                                proc_status_ok exit_status_str force_die);
 use strict;
 use warnings;
 # use bytes;
 use re 'taint';
 
+use Storable;
+use POSIX qw(PIPE_BUF WNOHANG);
+
 our @ISA = qw(Mail::SpamAssassin::Plugin);
 
 sub new {
@@ -64,7 +68,7 @@ sub new {
     dbg("pyzor: network tests on, attempting Pyzor");
   }
 
-  $self->register_eval_rule("check_pyzor");
+  $self->register_eval_rule("check_pyzor", $Mail::SpamAssassin::Conf::TYPE_FULL_EVALS);
 
   $self->set_config($mailsaobject->{conf});
 
@@ -87,11 +91,27 @@ Whether to use Pyzor, if it is available.
 
   push (@cmds, {
     setting => 'use_pyzor',
+    is_admin => 1,
     default => 1,
     type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL
   });
 
-=item pyzor_max NUMBER         (default: 5)
+=item pyzor_fork (0|1)         (default: 1)
+
+Instead of running Pyzor synchronously, fork separate process for it and
+read the results in later (similar to async DNS lookups).  Increases
+throughput. Considered experimental on Windows, where default is 0.
+
+=cut
+
+  push(@cmds, {
+    setting => 'pyzor_fork',
+    is_admin => 1,
+    default => am_running_on_windows()?0:1,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
+  });
+
+=item pyzor_count_min NUMBER   (default: 5)
 
 This option sets how often a message's body checksum must have been
 reported to the Pyzor server before SpamAssassin will consider the Pyzor
@@ -103,18 +123,69 @@ set this to a relatively low value, e.g. C<5>.
 =cut
 
   push (@cmds, {
-    setting => 'pyzor_max',
+    setting => 'pyzor_count_min',
+    is_admin => 1,
     default => 5,
     type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
   });
 
+  # Deprecated setting, the name makes no sense!
+  push (@cmds, {
+    setting => 'pyzor_max',
+    is_admin => 1,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      warn("deprecated setting used, change pyzor_max to pyzor_count_min\n");
+      if ($value !~ /^\d+$/) {
+        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+      }
+      $self->{pyzor_count_min} = $value;
+    }
+  });
+
+=item pyzor_welcomelist_min NUMBER     (default: 10)
+
+Previously pyzor_whitelist_min which will work interchangeably until 4.1.
+
+This option sets how often a message's body checksum must have been
+welcomelisted to the Pyzor server for SpamAssassin to consider ignoring the
+result.  Final decision is made by pyzor_welcomelist_factor.
+
+=cut
+
+  push (@cmds, {
+    setting => 'pyzor_welcomelist_min',
+    aliases => ['pyzor_whitelist_min'], # removed in 4.1
+    is_admin => 1,
+    default => 10,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
+  });
+
+=item pyzor_welcomelist_factor NUMBER  (default: 0.2)
+
+Previously pyzor_whitelist_factor which will work interchangeably until 4.1.
+
+Ignore Pyzor result if REPORTCOUNT x NUMBER >= pyzor_welcomelist_min.
+For default setting this means: 50 reports requires 10 welcomelistings.
+
+=cut
+
+  push (@cmds, {
+    setting => 'pyzor_welcomelist_factor',
+    aliases => ['pyzor_whitelist_factor'], # removed in 4.1
+    is_admin => 1,
+    default => 0.2,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
+  });
+
 =back
 
 =head1 ADMINISTRATOR OPTIONS
 
 =over 4
 
-=item pyzor_timeout n          (default: 3.5)
+=item pyzor_timeout n          (default: 5)
 
 How many seconds you wait for Pyzor to complete, before scanning continues
 without the Pyzor results. A numeric value is optionally suffixed by a
@@ -142,7 +213,7 @@ removing one of them.
   push (@cmds, {
     setting => 'pyzor_timeout',
     is_admin => 1,
-    default => 3.5,
+    default => 5,
     type => $Mail::SpamAssassin::Conf::CONF_TYPE_DURATION
   });
 
@@ -202,190 +273,352 @@ you should use this, as the current PATH will have been cleared.
 sub is_pyzor_available {
   my ($self) = @_;
 
-  my $pyzor = $self->{main}->{conf}->{pyzor_path} || '';
-  unless ($pyzor) {
-    $pyzor = Mail::SpamAssassin::Util::find_executable_in_env_path('pyzor');
-  }
+  my $pyzor = $self->{main}->{conf}->{pyzor_path} ||
+    Mail::SpamAssassin::Util::find_executable_in_env_path('pyzor');
+
   unless ($pyzor && -x $pyzor) {
-    dbg("pyzor: pyzor is not available: no pyzor executable found");
+    dbg("pyzor: no pyzor executable found");
+    $self->{pyzor_available} = 0;
     return 0;
   }
 
   # remember any found pyzor
   $self->{main}->{conf}->{pyzor_path} = $pyzor;
 
-  dbg("pyzor: pyzor is available: " . $self->{main}->{conf}->{pyzor_path});
+  dbg("pyzor: pyzor is available: $pyzor");
   return 1;
 }
 
-sub get_pyzor_interface {
-  my ($self) = @_;
+sub finish_parsing_start {
+  my ($self, $opts) = @_;
 
-  if (!$self->{main}->{conf}->{use_pyzor}) {
-    dbg("pyzor: use_pyzor option not enabled, disabling Pyzor");
-    $self->{pyzor_interface} = "disabled";
-    $self->{pyzor_available} = 0;
-  }
-  elsif ($self->is_pyzor_available()) {
-    $self->{pyzor_interface} = "pyzor";
-    $self->{pyzor_available} = 1;
-  }
-  else {
-    dbg("pyzor: no pyzor found, disabling Pyzor");
-    $self->{pyzor_available} = 0;
+  # If forking, hard adjust priority -100 to launch early
+  # Find rulenames from eval_to_rule mappings
+  if ($opts->{conf}->{pyzor_fork}) {
+    foreach (@{$opts->{conf}->{eval_to_rule}->{check_pyzor}}) {
+      dbg("pyzor: adjusting rule $_ priority to -100");
+      $opts->{conf}->{priority}->{$_} = -100;
+    }
   }
 }
 
 sub check_pyzor {
-  my ($self, $permsgstatus, $full) = @_;
+  my ($self, $pms, $full) = @_;
 
-  # initialize valid tags
-  $permsgstatus->{tag_data}->{PYZOR} = "";
+  return 0 if !$self->{pyzor_available};
+  return 0 if !$self->{main}->{conf}->{use_pyzor};
+
+  return 0 if $pms->{pyzor_running};
+  $pms->{pyzor_running} = 1;
+
+  return 0 if !$self->is_pyzor_available();
 
   my $timer = $self->{main}->time_method("check_pyzor");
 
-  $self->get_pyzor_interface();
-  return 0 unless $self->{pyzor_available};
+  # initialize valid tags
+  $pms->{tag_data}->{PYZOR} = '';
+
+  # create fulltext tmpfile now (before possible forking)
+  $pms->{pyzor_tmpfile} = $pms->create_fulltext_tmpfile();
+
+  ## non-forking method
+
+  if (!$self->{main}->{conf}->{pyzor_fork}) {
+    my @results = $self->pyzor_lookup($pms);
+    return $self->_check_result($pms, \@results);
+  }
+
+  ## forking method
+
+  $pms->{pyzor_rulename} = $pms->get_current_eval_rule_name();
+
+  # create socketpair for communication
+  $pms->{pyzor_backchannel} = Mail::SpamAssassin::SubProcBackChannel->new();
+  my $back_selector = '';
+  $pms->{pyzor_backchannel}->set_selector(\$back_selector);
+  eval {
+    $pms->{pyzor_backchannel}->setup_backchannel_parent_pre_fork();
+  } or do {
+    dbg("pyzor: backchannel pre-setup failed: $@");
+    delete $pms->{pyzor_backchannel};
+    return 0;
+  };
+
+  my $pid = fork();
+  if (!defined $pid) {
+    info("pyzor: child fork failed: $!");
+    delete $pms->{pyzor_backchannel};
+    return 0;
+  }
+  if (!$pid) {
+    $0 = "$0 (pyzor)";
+    $SIG{CHLD} = 'DEFAULT';
+    $SIG{PIPE} = 'IGNORE';
+    $SIG{$_} = sub {
+      eval { dbg("pyzor: child process $$ caught signal $_[0]"); };
+      force_die(6);  # avoid END and destructor processing
+      } foreach am_running_on_windows()?qw(INT HUP TERM QUIT):qw(INT HUP TERM TSTP QUIT USR1 USR2);
+    dbg("pyzor: child process $$ forked");
+    $pms->{pyzor_backchannel}->setup_backchannel_child_post_fork();
+    my @results = $self->pyzor_lookup($pms);
+    my $backmsg;
+    eval {
+      $backmsg = Storable::freeze(\@results);
+    };
+    if ($@) {
+      dbg("pyzor: child return value freeze failed: $@");
+      force_die(0); # avoid END and destructor processing
+    }
+    if (!syswrite($pms->{pyzor_backchannel}->{parent}, $backmsg)) {
+      dbg("pyzor: child backchannel write failed: $!");
+    }
+    force_die(0); # avoid END and destructor processing
+  }
 
-  return $self->pyzor_lookup($permsgstatus, $full);
+  $pms->{pyzor_pid} = $pid;
+
+  eval {
+    $pms->{pyzor_backchannel}->setup_backchannel_parent_post_fork($pid);
+  } or do {
+    dbg("pyzor: backchannel post-setup failed: $@");
+    delete $pms->{pyzor_backchannel};
+    return 0;
+  };
+
+  return; # return undef for async status
 }
 
 sub pyzor_lookup {
-  my ($self, $permsgstatus, $fulltext) = @_;
-  my @response;
-  my $pyzor_count;
-  my $pyzor_whitelisted;
-  my $timeout = $self->{main}->{conf}->{pyzor_timeout};
+  my ($self, $pms) = @_;
 
-  $pyzor_count = 0;
-  $pyzor_whitelisted = 0;
-  my $pid;
-
-  # use a temp file here -- open2() is unreliable, buffering-wise, under spamd
-  my $tmpf = $permsgstatus->create_fulltext_tmpfile($fulltext);
+  my $conf = $self->{main}->{conf};
+  my $timeout = $conf->{pyzor_timeout};
 
   # note: not really tainted, this came from system configuration file
-  my $path = untaint_file_path($self->{main}->{conf}->{pyzor_path});
-  my $opts = untaint_var($self->{main}->{conf}->{pyzor_options}) || '';
+  my $path = untaint_file_path($conf->{pyzor_path});
+  my $opts = untaint_var($conf->{pyzor_options}) || '';
 
-  $permsgstatus->enter_helper_run_mode();
+  $pms->enter_helper_run_mode();
 
+  my $pid;
+  my @resp;
   my $timer = Mail::SpamAssassin::Timeout->new(
-           { secs => $timeout, deadline => $permsgstatus->{master_deadline} });
+           { secs => $timeout, deadline => $pms->{master_deadline} });
   my $err = $timer->run_and_catch(sub {
-
     local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };
-    dbg("pyzor: opening pipe: " . join(' ', $path, $opts, "check", "< $tmpf"));
+
+    dbg("pyzor: opening pipe: ".
+      join(' ', $path, $opts, "check", "<".$pms->{pyzor_tmpfile}));
 
     $pid = Mail::SpamAssassin::Util::helper_app_pipe_open(*PYZOR,
-       $tmpf, 1, $path, split(' ', $opts), "check");
+       $pms->{pyzor_tmpfile}, 1, $path, split(' ', $opts), "check");
     $pid or die "$!\n";
 
     # read+split avoids a Perl I/O bug (Bug 5985)
-    my($inbuf,$nread,$resp); $resp = '';
-    while ( $nread=read(PYZOR,$inbuf,8192) ) { $resp .= $inbuf }
+    my($inbuf, $nread);
+    my $resp = '';
+    while ($nread = read(PYZOR, $inbuf, 8192)) { $resp .= $inbuf }
     defined $nread  or die "error reading from pipe: $!";
-    @response = split(/^/m, $resp, -1);  undef $resp;
+    @resp = split(/^/m, $resp, -1);
 
-    my $errno = 0;  close PYZOR or $errno = $!;
-    if (proc_status_ok($?,$errno)) {
+    my $errno = 0;
+    close PYZOR or $errno = $!;
+    if (proc_status_ok($?, $errno)) {
       dbg("pyzor: [%s] finished successfully", $pid);
-    } elsif (proc_status_ok($?,$errno, 0,1)) {  # sometimes it exits with 1
-      dbg("pyzor: [%s] finished: %s", $pid, exit_status_str($?,$errno));
+    } elsif (proc_status_ok($?, $errno, 0, 1)) {  # sometimes it exits with 1
+      dbg("pyzor: [%s] finished: %s", $pid, exit_status_str($?, $errno));
     } else {
-      info("pyzor: [%s] error: %s", $pid, exit_status_str($?,$errno));
-    }
-
-    if (!@response) {
-      # this exact string is needed below
-      warn("no response\n");   # yes, this is possible
-      return;
-    }
-    chomp for @response;
-
-    if ($response[0] =~ /^Traceback/) {
-      warn("internal error, python traceback seen in response: ".
-        join("\\n", @response));
-    } else {
-      dbg("pyzor: got response: ".join("\\n", @response));
+      info("pyzor: [%s] error: %s", $pid, exit_status_str($?, $errno));
     }
 
   });
 
   if (defined(fileno(*PYZOR))) {  # still open
     if ($pid) {
-      if (kill('TERM',$pid)) { dbg("pyzor: killed stale helper [$pid]") }
-      else { dbg("pyzor: killing helper application [$pid] failed: $!") }
+      if (kill('TERM', $pid)) {
+        dbg("pyzor: killed stale helper [$pid]");
+      } else {
+        dbg("pyzor: killing helper application [$pid] failed: $!");
+      }
     }
-    my $errno = 0;  close PYZOR or $errno = $!;
-    proc_status_ok($?,$errno)
-      or info("pyzor: [%s] error: %s", $pid, exit_status_str($?,$errno));
+    my $errno = 0;
+    close PYZOR or $errno = $!;
+    proc_status_ok($?, $errno)
+      or info("pyzor: [%s] error: %s", $pid, exit_status_str($?, $errno));
   }
-  $permsgstatus->leave_helper_run_mode();
+
+  $pms->leave_helper_run_mode();
 
   if ($timer->timed_out()) {
     dbg("pyzor: check timed out after $timeout seconds");
-    return 0;
+    return ();
+  } elsif ($err) {
+    chomp $err;
+    info("pyzor: check failed: $err");
+    return ();
   }
 
-  if ($err) {
-    chomp $err;
-    if ($err eq "__brokenpipe__ignore__") {
-      dbg("pyzor: check failed: broken pipe");
-    } elsif ($err eq "no response") {
-      dbg("pyzor: check failed: no response");
-    } else {
-      warn("pyzor: check failed: $err\n");
+  return @resp;
+}
+
+sub check_tick {
+  my ($self, $opts) = @_;
+  $self->_check_forked_result($opts->{permsgstatus}, 0);
+}
+
+sub check_cleanup {
+  my ($self, $opts) = @_;
+  $self->_check_forked_result($opts->{permsgstatus}, 1);
+}
+
+sub _check_forked_result {
+  my ($self, $pms, $finish) = @_;
+
+  return 0 if !$pms->{pyzor_backchannel};
+  return 0 if !$pms->{pyzor_pid};
+
+  my $timer = $self->{main}->time_method("check_pyzor");
+
+  $pms->{pyzor_abort} = $pms->{deadline_exceeded} || $pms->{shortcircuited};
+
+  my $kid_pid = $pms->{pyzor_pid};
+  # if $finish, force waiting for the child
+  my $pid = waitpid($kid_pid, $finish && !$pms->{pyzor_abort} ? 0 : WNOHANG);
+  if ($pid == 0) {
+    #dbg("pyzor: child process $kid_pid not finished yet, trying later");
+    if ($pms->{pyzor_abort}) {
+      dbg("pyzor: bailing out due to deadline/shortcircuit");
+      kill('TERM', $kid_pid);
+      if (waitpid($kid_pid, WNOHANG) == 0) {
+        sleep(1);
+        if (waitpid($kid_pid, WNOHANG) == 0) {
+          dbg("pyzor: child process $kid_pid still alive, KILL");
+          kill('KILL', $kid_pid);
+          waitpid($kid_pid, 0);
+        }
+      }
+      delete $pms->{pyzor_pid};
+      delete $pms->{pyzor_backchannel};
     }
     return 0;
+  } elsif ($pid == -1) {
+    # child does not exist?
+    dbg("pyzor: child process $kid_pid already handled?");
+    delete $pms->{pyzor_backchannel};
+    return 0;
   }
 
-  foreach my $one_response (@response) {
+  $pms->rule_ready($pms->{pyzor_rulename}); # mark rule ready for metas
+
+  dbg("pyzor: child process $kid_pid finished, reading results");
+
+  my $backmsg;
+  my $ret = sysread($pms->{pyzor_backchannel}->{latest_kid_fh}, $backmsg, am_running_on_windows()?512:PIPE_BUF);
+  if (!defined $ret || $ret == 0) {
+    dbg("pyzor: could not read result from child: ".($ret == 0 ? 0 : $!));
+    delete $pms->{pyzor_backchannel};
+    return 0;
+  }
+
+  delete $pms->{pyzor_backchannel};
+
+  my $results;
+  eval {
+    $results = Storable::thaw($backmsg);
+  };
+  if ($@) {
+    dbg("pyzor: child return value thaw failed: $@");
+    return;
+  }
+
+  $self->_check_result($pms, $results);
+}
+
+sub _check_result {
+  my ($self, $pms, $results) = @_;
+
+  if (!@$results) {
+    dbg("pyzor: no response from server");
+    return 0;
+  }
+
+  my $count = 0;
+  my $count_wl = 0;
+  foreach my $res (@$results) {
+    chomp($res);
+    if ($res =~ /^Traceback/) {
+      info("pyzor: internal error, python traceback seen in response: $res");
+      return 0;
+    }
+    dbg("pyzor: got response: $res");
     # this regexp is intended to be a little bit forgiving
-    if ($one_response =~ /^\S+\t.*?\t(\d+)\t(\d+)\s*$/) {
+    if ($res =~ /^\S+\t.*?\t(\d+)\t(\d+)\s*$/) {
       # until pyzor servers can sync their DBs,
       # sum counts obtained from all servers
-      $pyzor_whitelisted += $2+0;
-      $pyzor_count += $1+0;
-    }
-    else {
+      $count += untaint_var($1)+0; # crazy but needs untainting
+      $count_wl += untaint_var($2)+0;
+    } else {
       # warn on failures to parse
-      dbg("pyzor: failure to parse response \"$one_response\"");
+      info("pyzor: failure to parse response \"$res\"");
     }
   }
 
-  $permsgstatus->set_tag('PYZOR', $pyzor_whitelisted ? "Whitelisted."
-                                             : "Reported $pyzor_count times.");
+  my $conf = $self->{main}->{conf};
 
-  if ($pyzor_count >= $self->{main}->{conf}->{pyzor_max}) {
-    dbg("pyzor: listed: COUNT=$pyzor_count/$self->{main}->{conf}->{pyzor_max} WHITELIST=$pyzor_whitelisted");
-    return 1;
+  my $count_min = $conf->{pyzor_count_min};
+  my $wl_min = $conf->{pyzor_welcomelist_min};
+
+  my $wl_limit = $count_wl >= $wl_min ?
+    $count * $conf->{pyzor_welcomelist_factor} : 0;
+
+  dbg("pyzor: result: COUNT=$count/$count_min WELCOMELIST=$count_wl/$wl_min/%.1f",
+    $wl_limit);
+  $pms->set_tag('PYZOR', "Reported $count times, welcomelisted $count_wl times.");
+
+  # Empty body etc results in same hash, we should skip very large numbers..
+  if ($count >= 1000000 || $count_wl >= 10000) {
+    dbg("pyzor: result exceeded hardcoded limits, ignoring: count/wl 1000000/10000");
+    return 0;
   }
 
+  # Welcomelisted?
+  if ($wl_limit && $count_wl >= $wl_limit) {
+    dbg("pyzor: message welcomelisted");
+    return 0;
+  }
+
+  if ($count >= $count_min) {
+    if ($conf->{pyzor_fork}) {
+      # forked needs to run got_hit()
+      $pms->got_hit($pms->{pyzor_rulename}, "", ruletype => 'eval');
+      return 0;
+    }
+    return 1;
+  }
   return 0;
 }
 
 sub plugin_report {
   my ($self, $options) = @_;
 
-  return unless $self->{pyzor_available};
-  return unless $self->{main}->{conf}->{use_pyzor};
-
-  if (!$options->{report}->{options}->{dont_report_to_pyzor} && $self->is_pyzor_available())
-  {
-    # use temporary file: open2() is unreliable due to buffering under spamd
-    my $tmpf = $options->{report}->create_fulltext_tmpfile($options->{text});
-    if ($self->pyzor_report($options, $tmpf)) {
-      $options->{report}->{report_available} = 1;
-      info("reporter: spam reported to Pyzor");
-      $options->{report}->{report_return} = 1;
-    }
-    else {
-      info("reporter: could not report spam to Pyzor");
-    }
-    $options->{report}->delete_fulltext_tmpfile();
+  return if !$self->{pyzor_available};
+  return if !$self->{main}->{conf}->{use_pyzor};
+  return if $options->{report}->{options}->{dont_report_to_pyzor};
+  return if !$self->is_pyzor_available();
+
+  # use temporary file: open2() is unreliable due to buffering under spamd
+  my $tmpf = $options->{report}->create_fulltext_tmpfile($options->{text});
+  if ($self->pyzor_report($options, $tmpf)) {
+    $options->{report}->{report_available} = 1;
+    info("reporter: spam reported to Pyzor");
+    $options->{report}->{report_return} = 1;
+  }
+  else {
+    info("reporter: could not report spam to Pyzor");
   }
+  $options->{report}->delete_fulltext_tmpfile($tmpf);
+
+  return 1;
 }
 
 sub pyzor_report {
@@ -449,6 +682,9 @@ sub pyzor_report {
   return 1;
 }
 
+# Version features
+sub has_fork { 1 }
+
 1;
 
 =back
index 07f853d4f162d2182bc7f20b6785552066352192..184d8243d09ccade15d14b45d88ac4254b11c299 100644 (file)
@@ -43,11 +43,16 @@ package Mail::SpamAssassin::Plugin::Razor2;
 use Mail::SpamAssassin::Plugin;
 use Mail::SpamAssassin::Logger;
 use Mail::SpamAssassin::Timeout;
+use Mail::SpamAssassin::SubProcBackChannel;
+use Mail::SpamAssassin::Util qw(force_die am_running_on_windows);
 use strict;
 use warnings;
 # use bytes;
 use re 'taint';
 
+use Storable;
+use POSIX qw(PIPE_BUF WNOHANG);
+
 our @ISA = qw(Mail::SpamAssassin::Plugin);
 
 sub new {
@@ -73,8 +78,8 @@ sub new {
     }
   }
 
-  $self->register_eval_rule("check_razor2");
-  $self->register_eval_rule("check_razor2_range");
+  $self->register_eval_rule("check_razor2", $Mail::SpamAssassin::Conf::TYPE_FULL_EVALS);
+  $self->register_eval_rule("check_razor2_range", $Mail::SpamAssassin::Conf::TYPE_FULL_EVALS);
 
   $self->set_config($mailsaobject->{conf});
 
@@ -93,10 +98,26 @@ Whether to use Razor2, if it is available.
 
   push(@cmds, {
     setting => 'use_razor2',
+    is_admin => 1,
     default => 1,
     type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
   });
 
+=item razor_fork (0|1)         (default: 1)
+
+Instead of running Razor2 synchronously, fork separate process for it and
+read the results in later (similar to async DNS lookups). Increases
+throughput. Considered experimental on Windows, where default is 0.
+
+=cut
+
+  push(@cmds, {
+    setting => 'razor_fork',
+    is_admin => 1,
+    default => am_running_on_windows()?0:1,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
+  });
+
 =back
 
 =head1 ADMINISTRATOR SETTINGS
@@ -195,7 +216,7 @@ sub razor2_access {
       my $sigs = $rc->compute_sigs($objects)
          or die "$debug: error in compute_sigs";
 
-      # if mail isn't whitelisted, check it out
+      # if mail isn't welcomelisted, check it out
       # see 'man razor-whitelist'
       if ($type ne 'check' || ! $rc->local_check($objects->[0])) {
        # provide a better error message when servers are unavailable,
@@ -347,6 +368,8 @@ sub plugin_report {
   return unless $self->{main}->{conf}->{use_razor2};
   return if $options->{report}->{options}->{dont_report_to_razor};
 
+  my $timer = $self->{main}->time_method("razor2_report");
+
   if ($self->razor2_access($options->{text}, 'report', undef)) {
     $options->{report}->{report_available} = 1;
     info('reporter: spam reported to Razor');
@@ -360,6 +383,8 @@ sub plugin_report {
 sub plugin_revoke {
   my ($self, $options) = @_;
 
+  my $timer = $self->{main}->time_method("razor2_revoke");
+
   return unless $self->{razor2_available};
   return if $self->{main}->{local_tests_only};
   return unless $self->{main}->{conf}->{use_razor2};
@@ -375,34 +400,191 @@ sub plugin_revoke {
   }
 }
 
+sub finish_parsing_start {
+  my ($self, $opts) = @_;
+
+  # If forking, hard adjust priority -100 to launch early
+  # Find rulenames from eval_to_rule mappings
+  if ($opts->{conf}->{razor_fork}) {
+    foreach (@{$opts->{conf}->{eval_to_rule}->{check_razor2}}) {
+      dbg("razor2: adjusting rule $_ priority to -100");
+      $opts->{conf}->{priority}->{$_} = -100;
+    }
+    foreach (@{$opts->{conf}->{eval_to_rule}->{check_razor2_range}}) {
+      dbg("razor2: adjusting rule $_ priority to -100");
+      $opts->{conf}->{priority}->{$_} = -100;
+    }
+  }
+}
+
 sub check_razor2 {
-  my ($self, $permsgstatus, $full) = @_;
+  my ($self, $pms, $full) = @_;
 
-  return $permsgstatus->{razor2_result} if (defined $permsgstatus->{razor2_result});
-  $permsgstatus->{razor2_result} = 0;
-  $permsgstatus->{razor2_cf_score} = { '4' => 0, '8' => 0 };
+  return 0 unless $self->{razor2_available};
+  return 0 unless $self->{main}->{conf}->{use_razor2};
 
-  return unless $self->{razor2_available};
-  return unless $self->{main}->{conf}->{use_razor2};
+  return $pms->{razor2_result} if (defined $pms->{razor2_result});
+
+  return 0 if $pms->{razor2_running};
+  $pms->{razor2_running} = 1;
 
   my $timer = $self->{main}->time_method("check_razor2");
 
-  my $return;
-  my @results;
+  ## non-forking method
+
+  if (!$self->{main}->{conf}->{razor_fork}) {
+    # TODO: check for cache header, set results appropriately
+    # do it this way to make it easier to get out the results later from the
+    # netcache plugin ... what netcache plugin?
+    (undef, my @results) =
+      $self->razor2_access($full, 'check', $pms->{master_deadline});
+    return $self->_check_result($pms, \@results);
+  }
+
+  ## forking method
+
+  $pms->{razor2_rulename} = $pms->get_current_eval_rule_name();
+
+  # create socketpair for communication
+  $pms->{razor2_backchannel} = Mail::SpamAssassin::SubProcBackChannel->new();
+  my $back_selector = '';
+  $pms->{razor2_backchannel}->set_selector(\$back_selector);
+  eval {
+    $pms->{razor2_backchannel}->setup_backchannel_parent_pre_fork();
+  } or do {
+    dbg("razor2: backchannel pre-setup failed: $@");
+    delete $pms->{razor2_backchannel};
+    return 0;
+  };
+
+  my $pid = fork();
+  if (!defined $pid) {
+    info("razor2: child fork failed: $!");
+    delete $pms->{razor2_backchannel};
+    return 0;
+  }
+  if (!$pid) {
+    $0 = "$0 (razor2)";
+    $SIG{CHLD} = 'DEFAULT';
+    $SIG{PIPE} = 'IGNORE';
+    $SIG{$_} = sub {
+      eval { dbg("razor2: child process $$ caught signal $_[0]"); };
+      force_die(6);  # avoid END and destructor processing
+      } foreach am_running_on_windows()?qw(INT HUP TERM QUIT):qw(INT HUP TERM TSTP QUIT USR1 USR2);
+    dbg("razor2: child process $$ forked");
+    $pms->{razor2_backchannel}->setup_backchannel_child_post_fork();
+    (undef, my @results) =
+      $self->razor2_access($full, 'check', $pms->{master_deadline});
+    my $backmsg;
+    eval {
+      $backmsg = Storable::freeze(\@results);
+    };
+    if ($@) {
+      dbg("razor2: child return value freeze failed: $@");
+      force_die(0); # avoid END and destructor processing
+    }
+    if (!syswrite($pms->{razor2_backchannel}->{parent}, $backmsg)) {
+      dbg("razor2: child backchannel write failed: $!");
+    }
+    force_die(0); # avoid END and destructor processing
+  }
+
+  $pms->{razor2_pid} = $pid;
 
-  # TODO: check for cache header, set results appropriately
+  eval {
+    $pms->{razor2_backchannel}->setup_backchannel_parent_post_fork($pid);
+  } or do {
+    dbg("razor2: backchannel post-setup failed: $@");
+    delete $pms->{razor2_backchannel};
+    return 0;
+  };
+
+  return; # return undef for async status
+}
+
+sub check_tick {
+  my ($self, $opts) = @_;
+  $self->_check_forked_result($opts->{permsgstatus}, 0);
+}
+
+sub check_cleanup {
+  my ($self, $opts) = @_;
+  $self->_check_forked_result($opts->{permsgstatus}, 1);
+}
+
+sub _check_forked_result {
+  my ($self, $pms, $finish) = @_;
+
+  return 0 if !$pms->{razor2_backchannel};
+  return 0 if !$pms->{razor2_pid};
+
+  my $timer = $self->{main}->time_method("check_razor2");
+
+  $pms->{razor2_abort} = $pms->{deadline_exceeded} || $pms->{shortcircuited};
+
+  my $kid_pid = $pms->{razor2_pid};
+  # if $finish, force waiting for the child
+  my $pid = waitpid($kid_pid, $finish && !$pms->{razor2_abort} ? 0 : WNOHANG);
+  if ($pid == 0) {
+    #dbg("razor2: child process $kid_pid not finished yet, trying later");
+    if ($pms->{razor2_abort}) {
+      dbg("razor2: bailing out due to deadline/shortcircuit");
+      kill('TERM', $kid_pid);
+      if (waitpid($kid_pid, WNOHANG) == 0) {
+        sleep(1);
+        if (waitpid($kid_pid, WNOHANG) == 0) {
+          dbg("razor2: child process $kid_pid still alive, KILL");
+          kill('KILL', $kid_pid);
+          waitpid($kid_pid, 0);
+        }
+      }
+      delete $pms->{razor2_pid};
+      delete $pms->{razor2_backchannel};
+    }
+    return 0;
+  } elsif ($pid == -1) {
+    # child does not exist?
+    dbg("razor2: child process $kid_pid already handled?");
+    delete $pms->{razor2_backchannel};
+    return 0;
+  }
+
+  $pms->rule_ready($pms->{razor2_rulename}); # mark rule ready for metas
+
+  dbg("razor2: child process $kid_pid finished, reading results");
+
+  my $backmsg;
+  my $ret = sysread($pms->{razor2_backchannel}->{latest_kid_fh}, $backmsg, am_running_on_windows()?512:PIPE_BUF);
+  if (!defined $ret || $ret == 0) {
+    dbg("razor2: could not read result from child: ".($ret == 0 ? 0 : $!));
+    delete $pms->{razor2_backchannel};
+    return 0;
+  }
+
+  delete $pms->{razor2_backchannel};
+
+  my $results;
+  eval {
+    $results = Storable::thaw($backmsg);
+  };
+  if ($@) {
+    dbg("razor2: child return value thaw failed: $@");
+    return;
+  }
+
+  $self->_check_result($pms, $results);
+}
+
+sub _check_result {
+  my ($self, $pms, $results) = @_;
 
-  # do it this way to make it easier to get out the results later from the
-  # netcache plugin
-  ($return, @results) =
-    $self->razor2_access($full, 'check', $permsgstatus->{master_deadline});
   $self->{main}->call_plugins ('process_razor_result',
-       { results => \@results, permsgstatus => $permsgstatus }
+       { results => $results, permsgstatus => $pms }
   );
 
-  foreach my $result (@results) {
+  foreach my $result (@$results) {
     if (exists $result->{result}) {
-      $permsgstatus->{razor2_result} = $result->{result} if $result->{result};
+      $pms->{razor2_result} = $result->{result} if $result->{result};
     }
     elsif ($result->{noresponse}) {
       dbg('razor2: part=' . $result->{part} . ' noresponse');
@@ -415,46 +597,86 @@ sub check_razor2 {
 
       next if $result->{contested};
 
-      my $cf = $permsgstatus->{razor2_cf_score}->{$result->{engine}} || 0;
+      my $cf = $pms->{razor2_cf_score}->{$result->{engine}} || 0;
       if ($result->{confidence} > $cf) {
-        $permsgstatus->{razor2_cf_score}->{$result->{engine}} = $result->{confidence};
+        $pms->{razor2_cf_score}->{$result->{engine}} = $result->{confidence};
       }
     }
   }
 
-  dbg("razor2: results: spam? " . $permsgstatus->{razor2_result});
-  while(my ($engine, $cf) = each %{$permsgstatus->{razor2_cf_score}}) {
+  $pms->{razor2_result} ||= 0;
+  $pms->{razor2_cf_score} ||= {};
+
+  dbg("razor2: results: spam? " . $pms->{razor2_result});
+  while(my ($engine, $cf) = each %{$pms->{razor2_cf_score}}) {
     dbg("razor2: results: engine $engine, highest cf score: $cf");
   }
 
-  return $permsgstatus->{razor2_result};
+  if ($self->{main}->{conf}->{razor_fork}) {
+    # forked needs to run got_hit()
+    if ($pms->{razor2_rulename} && $pms->{razor2_result}) {
+      $pms->got_hit($pms->{razor2_rulename}, "", ruletype => 'eval');
+    }
+    # forked needs to run range callbacks
+    if ($pms->{razor2_range_callbacks}) {
+      foreach (@{$pms->{razor2_range_callbacks}}) {
+        $self->check_razor2_range($pms, '', @$_);
+      }
+    }
+  }
+
+  return $pms->{razor2_result};
 }
 
 # Check the cf value of a given message and return if it's within the
 # given range
 sub check_razor2_range {
-  my ($self, $permsgstatus, $body, $engine, $min, $max) = @_;
+  my ($self, $pms, $body, $engine, $min, $max, $rulename) = @_;
 
   # If Razor2 isn't available, or the general test is disabled, don't
   # continue.
-  return unless $self->{razor2_available};
-  return unless $self->{main}->{conf}->{use_razor2};
-  return unless $self->{main}->{conf}->{scores}->{'RAZOR2_CHECK'};
+  return 0 unless $self->{razor2_available};
+  return 0 unless $self->{main}->{conf}->{use_razor2};
 
-  # If Razor2 hasn't been checked yet, go ahead and run it.
-  unless (defined $permsgstatus->{razor2_result}) {
-    $self->check_razor2($permsgstatus, $body);
+  # Check if callback overriding rulename
+  if (!defined $rulename) {
+    $rulename = $pms->get_current_eval_rule_name();
   }
 
+  if ($pms->{razor2_abort}) {
+    $pms->rule_ready($rulename); # mark rule ready for metas
+    return;
+  }
+
+  # If forked, call back later unless results are in
+  if ($self->{main}->{conf}->{razor_fork}) {
+    if (!defined $pms->{razor2_result}) {
+      dbg("razor2: delaying check_razor2_range call for $rulename");
+      # array matches check_razor2_range() argument order
+      push @{$pms->{razor2_range_callbacks}},
+        [$engine, $min, $max, $rulename];
+      return; # return undef for async status
+    }
+  } else {
+    # If Razor2 hasn't been checked yet, go ahead and run it.
+    # (only if we are non-forking.. forking will handle these in
+    # callbacks)
+    if (!$pms->{razor2_running}) {
+      $self->check_razor2($pms, $body);
+    }
+  }
+
+  $pms->rule_ready($rulename); # mark rule ready for metas
+
   my $cf = 0;
   if ($engine) {
-    $cf = $permsgstatus->{razor2_cf_score}->{$engine};
-    return unless defined $cf;
+    $cf = $pms->{razor2_cf_score}->{$engine};
+    return unless defined $cf;
   }
   else {
     # If no specific engine was given to the rule, find the highest cf
     # determined and use that
-    while(my ($engine, $ecf) = each %{$permsgstatus->{razor2_cf_score}}) {
+    while(my ($engine, $ecf) = each %{$pms->{razor2_cf_score}}) {
       if ($ecf > $cf) {
         $cf = $ecf;
       }
@@ -462,13 +684,20 @@ sub check_razor2_range {
   }
 
   if ($cf >= $min && $cf <= $max) {
-    $permsgstatus->test_log(sprintf("cf: %3d", $cf));
+    my $cf_str = sprintf("cf: %3d", $cf);
+    $pms->test_log($cf_str, $rulename);
+    if ($self->{main}->{conf}->{razor_fork}) {
+      $pms->got_hit($rulename, "", ruletype => 'eval');
+    }
     return 1;
   }
 
-  return;
+  return 0;
 }
 
+# Version features
+sub has_fork { 1 }
+
 1;
 
 =back
index 38ec1e3bd3aa95a648718489e9b3453edb8ea66a..fc487ed001b1d394d777f8f68d2bfaeafc8917bc 100644 (file)
@@ -53,10 +53,8 @@ Following metadata headers and tags are added:
 
 =head1 REQUIREMENT
 
-This plugin requires the GeoIP2, Geo::IP, IP::Country::DB_File or 
-IP::Country::Fast module from CPAN.
-For backward compatibility IP::Country::Fast is used as fallback if no db_type
-is specified in the config file.
+This plugin uses Mail::SpamAssassin::GeoDB and requires a module supported
+by it, for example MaxMind::DB::Reader (GeoIP2).
 
 =cut
 
@@ -64,7 +62,6 @@ package Mail::SpamAssassin::Plugin::RelayCountry;
 
 use Mail::SpamAssassin::Plugin;
 use Mail::SpamAssassin::Logger;
-use Mail::SpamAssassin::Constants qw(:ip);
 use strict;
 use warnings;
 # use bytes;
@@ -72,6 +69,11 @@ use re 'taint';
 
 our @ISA = qw(Mail::SpamAssassin::Plugin);
 
+my $db;
+my $dbv6;
+my $db_info;  # will hold database info
+my $db_type;  # will hold database type
+
 # constructor: register the eval rule
 sub new {
   my $class = shift;
@@ -82,282 +84,39 @@ sub new {
   my $self = $class->SUPER::new($mailsaobject);
   bless ($self, $class);
 
-  $self->set_config($mailsaobject->{conf});
-  return $self;
-}
-
-sub set_config {
-  my ($self, $conf) = @_;
-  my @cmds;
-
-=head1 USER PREFERENCES
-
-The following options can be used in both site-wide (C<local.cf>) and
-user-specific (C<user_prefs>) configuration files to customize how
-SpamAssassin handles incoming email messages.
-
-=over 4
-
-=item country_db_type STRING
-
-This option tells SpamAssassin which type of Geo database to use.
-Valid database types are GeoIP, GeoIP2, DB_File and Fast.
-
-=back
-
-=cut
-
-  push (@cmds, {
-    setting => 'country_db_type',
-    default => "GeoIP",
-    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
-    code => sub {
-      my ($self, $key, $value, $line) = @_;
-      if ($value !~ /^(?:GeoIP|GeoIP2|DB_File|Fast)$/) {
-        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
-      }
-      $self->{country_db_type} = $value;
-    }
-  });
-
-=over 4
-
-=item country_db_path STRING
-
-This option tells SpamAssassin where to find MaxMind GeoIP2 or IP::Country::DB_File database.
-
-If not defined, GeoIP2 default search includes:
- /usr/local/share/GeoIP/GeoIP2-Country.mmdb
- /usr/share/GeoIP/GeoIP2-Country.mmdb
- /var/lib/GeoIP/GeoIP2-Country.mmdb
- /usr/local/share/GeoIP/GeoLite2-Country.mmdb
- /usr/share/GeoIP/GeoLite2-Country.mmdb
- /var/lib/GeoIP/GeoLite2-Country.mmdb
- (and same paths again for -City.mmdb, which also has country functionality)
-
-=back
-
-=cut
-
-  push (@cmds, {
-    setting => 'country_db_path',
-    default => "",
-    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
-    code => sub {
-      my ($self, $key, $value, $line) = @_;
-      if (!defined $value || !length $value) {
-        return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
-      }
-      if (!-e $value) {
-        info("config: country_db_path \"$value\" is not accessible");
-        $self->{country_db_path} = $value;
-        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
-      }
-      $self->{country_db_path} = $value;
-    }
-  });
-
-  push (@cmds, {
-    setting => 'geoip2_default_db_path',
-    default => [
-      '/usr/local/share/GeoIP/GeoIP2-Country.mmdb',
-      '/usr/share/GeoIP/GeoIP2-Country.mmdb',
-      '/var/lib/GeoIP/GeoIP2-Country.mmdb',
-      '/usr/local/share/GeoIP/GeoLite2-Country.mmdb',
-      '/usr/share/GeoIP/GeoLite2-Country.mmdb',
-      '/var/lib/GeoIP/GeoLite2-Country.mmdb',
-      '/usr/local/share/GeoIP/GeoIP2-City.mmdb',
-      '/usr/share/GeoIP/GeoIP2-City.mmdb',
-      '/var/lib/GeoIP/GeoIP2-City.mmdb',
-      '/usr/local/share/GeoIP/GeoLite2-City.mmdb',
-      '/usr/share/GeoIP/GeoLite2-City.mmdb',
-      '/var/lib/GeoIP/GeoLite2-City.mmdb',
-      ],
-    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRINGLIST,
-    code => sub {
-      my ($self, $key, $value, $line) = @_;
-      if ($value eq '') {
-        return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
-      }
-      push(@{$self->{geoip2_default_db_path}}, split(/\s+/, $value));
-    }
-  });
-  
-  $conf->{parser}->register_commands(\@cmds);
-}
-
-sub get_country {
-    my ($self, $ip, $db, $dbv6, $country_db_type) = @_;
-    my $cc;
-    my $IP_PRIVATE = IP_PRIVATE;
-    my $IPV4_ADDRESS = IPV4_ADDRESS;
-
-    # Private IPs will always be returned as '**'
-    if ($ip =~ /^$IP_PRIVATE$/o) {
-      $cc = "**";
-    }
-    elsif ($country_db_type eq "GeoIP") {
-      if ($ip =~ /^$IPV4_ADDRESS$/o) {
-        $cc = $db->country_code_by_addr($ip);
-      } elsif (defined $dbv6) {
-        $cc = $dbv6->country_code_by_addr_v6($ip);
-      }
-    }
-    elsif ($country_db_type eq "GeoIP2") {
-      my ($country, $country_rec);
-      eval {
-        if (index($db->metadata()->description()->{en}, 'City') != -1) {
-          $country = $db->city( ip => $ip );
-        } else {
-          $country = $db->country( ip => $ip );
-        }
-        $country_rec = $country->country();
-        $cc = $country_rec->iso_code();
-        1;
-      } or do {
-        $@ =~ s/\s+Trace begun.*//s;
-        dbg("metadata: RelayCountry: GeoIP2 failed: $@");
-      }
-    }
-    elsif ($country_db_type eq "DB_File") {
-      if ($ip =~ /^$IPV4_ADDRESS$/o ) {
-        $cc = $db->inet_atocc($ip);
-      } else {
-        $cc = $db->inet6_atocc($ip);
-      }
-    }
-    elsif ($country_db_type eq "Fast") {
-      $cc = $db->inet_atocc($ip);
-    }
-
-    $cc ||= 'XX';
+  # we need GeoDB country
+  $self->{main}->{geodb_wanted}->{country} = 1;
 
-    return $cc;
+  return $self;
 }
 
 sub extract_metadata {
   my ($self, $opts) = @_;
   my $pms = $opts->{permsgstatus};
+  
+  return if $self->{relaycountry_disabled};
 
-  my $db;
-  my $dbv6;
-  my $db_info;  # will hold database info
-  my $db_type;  # will hold database type
-
-  my $country_db_type = $opts->{conf}->{country_db_type};
-  my $country_db_path = $opts->{conf}->{country_db_path};
-
-  if ($country_db_type eq "GeoIP") {
-    eval {
-      require Geo::IP;
-      $db = Geo::IP->open_type(Geo::IP->GEOIP_COUNTRY_EDITION, Geo::IP->GEOIP_STANDARD);
-      die "GeoIP.dat not found" unless $db;
-      # IPv6 requires version Geo::IP 1.39+ with GeoIP C API 1.4.7+
-      if (Geo::IP->VERSION >= 1.39 && Geo::IP->api eq 'CAPI') {
-        $dbv6 = Geo::IP->open_type(Geo::IP->GEOIP_COUNTRY_EDITION_V6, Geo::IP->GEOIP_STANDARD);
-        if (!$dbv6) {
-          dbg("metadata: RelayCountry: GeoIP: IPv6 support not enabled, GeoIPv6.dat not found");
-        }
-      } else {
-        dbg("metadata: RelayCountry: GeoIP: IPv6 support not enabled, versions Geo::IP 1.39, GeoIP C API 1.4.7 required");
-      }
-      $db_info = sub { return "Geo::IP IPv4: " . ($db->database_info || '?')." / IPv6: ".($dbv6 ? $dbv6->database_info || '?' : '?') };
-      1;
-    } or do {
-      # Fallback to IP::Country::Fast
-      dbg("metadata: RelayCountry: GeoIP: GeoIP.dat not found, trying IP::Country::Fast as fallback");
-      $country_db_type = "Fast";
-    }
-  }
-  elsif ($country_db_type eq "GeoIP2") {
-    if (!$country_db_path) {
-      # Try some default locations
-      foreach (@{$opts->{conf}->{geoip2_default_db_path}}) {
-        if (-f $_) {
-          $country_db_path = $_;
-          last;
-        }
-      }
-    }
-    if (-f $country_db_path) {
-      eval {
-        require GeoIP2::Database::Reader;
-        $db = GeoIP2::Database::Reader->new(
-          file => $country_db_path,
-          locales => [ 'en' ]
-        );
-        die "unknown error" unless $db;
-        $db_info = sub {
-          my $m = $db->metadata();
-          return "GeoIP2 ".$m->description()->{en}." / ".localtime($m->build_epoch());
-        };
-        1;
-      } or do {
-        # Fallback to IP::Country::Fast
-        $@ =~ s/\s+Trace begun.*//s;
-        dbg("metadata: RelayCountry: GeoIP2: ${country_db_path} load failed: $@, trying IP::Country::Fast as fallback");
-        $country_db_type = "Fast";
-      }
-    } else {
-      # Fallback to IP::Country::Fast
-      my $err = $country_db_path ?
-        "$country_db_path not found" : "database not found from default locations";
-      dbg("metadata: RelayCountry: GeoIP2: $err, trying IP::Country::Fast as fallback");
-      $country_db_type = "Fast";
-    }
-  }
-  elsif ($country_db_type eq "DB_File") {
-    if (-f $country_db_path) {
-      eval {
-        require IP::Country::DB_File;
-        $db = IP::Country::DB_File->new($country_db_path);
-        die "unknown error" unless $db;
-        $db_info = sub { return "IP::Country::DB_File ".localtime($db->db_time()); };
-        1;
-      } or do {
-        # Fallback to IP::Country::Fast
-        dbg("metadata: RelayCountry: DB_File: ${country_db_path} load failed: $@, trying IP::Country::Fast as fallback");
-        $country_db_type = "Fast";
-      }
-    } else {
-      # Fallback to IP::Country::Fast
-      dbg("metadata: RelayCountry: DB_File: ${country_db_path} not found, trying IP::Country::Fast as fallback");
-      $country_db_type = "Fast";
-    }
-  } 
-
-  if ($country_db_type eq "Fast") {
-    my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
-    eval {
-      require IP::Country::Fast;
-      $db = IP::Country::Fast->new();
-      $db_info = sub { return "IP::Country::Fast ".localtime($db->db_time()); };
-      1;
-    } or do {
-      my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
-      dbg("metadata: RelayCountry: failed to load 'IP::Country::Fast', skipping: $eval_stat");
-      return 1;
-    }
-  }
-
-  if (!$db) {
-    return 1;
+  if (!$self->{main}->{geodb} ||
+        !$self->{main}->{geodb}->can('country')) {
+    dbg("metadata: RelayCountry: plugin disabled, GeoDB country not available");
+    $self->{relaycountry_disabled} = 1;
+    return;
   }
 
-  dbg("metadata: RelayCountry: Using database: ".$db_info->());
   my $msg = $opts->{msg};
+  my $geodb = $self->{main}->{geodb};
 
   my @cc_untrusted;
   foreach my $relay (@{$msg->{metadata}->{relays_untrusted}}) {
     my $ip = $relay->{ip};
-    my $cc = $self->get_country($ip, $db, $dbv6, $country_db_type);
+    my $cc = $geodb->get_country($ip);
     push @cc_untrusted, $cc;
   }
 
   my @cc_external;
   foreach my $relay (@{$msg->{metadata}->{relays_external}}) {
     my $ip = $relay->{ip};
-    my $cc = $self->get_country($ip, $db, $dbv6, $country_db_type);
+    my $cc = $geodb->get_country($ip);
     push @cc_external, $cc;
   }
 
@@ -369,7 +128,7 @@ sub extract_metadata {
     }
     if ($found_auth) {
       my $ip = $relay->{ip};
-      my $cc = $self->get_country($ip, $db, $dbv6, $country_db_type);
+      my $cc = $geodb->get_country($ip);
       push @cc_auth, $cc;
     }
   }
@@ -377,7 +136,7 @@ sub extract_metadata {
   my @cc_all;
   foreach my $relay (@{$msg->{metadata}->{relays_internal}}, @{$msg->{metadata}->{relays_external}}) {
     my $ip = $relay->{ip};
-    my $cc = $self->get_country($ip, $db, $dbv6, $country_db_type);
+    my $cc = $geodb->get_country($ip);
     push @cc_all, $cc;
   }
 
@@ -400,8 +159,6 @@ sub extract_metadata {
   $msg->put_metadata("X-Relay-Countries-All", $ccstr);
   dbg("metadata: X-Relay-Countries-All: $ccstr");
   $pms->set_tag("RELAYCOUNTRYALL", @cc_all == 1 ? $cc_all[0] : \@cc_all);
-
-  return 1;
 }
 
 1;
index f98905ef5347d00c92546aa043c6ebe14c05f1dd..e1137c3b994d719495c7c196cd58263a35f75885 100644 (file)
@@ -28,6 +28,8 @@ use re 'taint';
 
 our @ISA = qw(Mail::SpamAssassin::Plugin);
 
+my $IPV4_ADDRESS = IPV4_ADDRESS;
+
 # constructor: register the eval rule
 sub new {
   my $class = shift;
@@ -39,17 +41,17 @@ sub new {
   bless ($self, $class);
 
   # the important bit!
-  $self->register_eval_rule("check_for_numeric_helo");
-  $self->register_eval_rule("check_for_illegal_ip");
-  $self->register_eval_rule("check_all_trusted");
-  $self->register_eval_rule("check_no_relays");
-  $self->register_eval_rule("check_relays_unparseable");
-  $self->register_eval_rule("check_for_sender_no_reverse");
-  $self->register_eval_rule("check_for_from_domain_in_received_headers");
-  $self->register_eval_rule("check_for_forged_received_trail");
-  $self->register_eval_rule("check_for_forged_received_ip_helo");
-  $self->register_eval_rule("helo_ip_mismatch");
-  $self->register_eval_rule("check_for_no_rdns_dotcom_helo");
+  $self->register_eval_rule("check_for_numeric_helo"); # type does not matter
+  $self->register_eval_rule("check_for_illegal_ip"); # type does not matter
+  $self->register_eval_rule("check_all_trusted"); # type does not matter
+  $self->register_eval_rule("check_no_relays"); # type does not matter
+  $self->register_eval_rule("check_relays_unparseable"); # type does not matter
+  $self->register_eval_rule("check_for_sender_no_reverse"); # type does not matter
+  $self->register_eval_rule("check_for_from_domain_in_received_headers", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_for_forged_received_trail"); # type does not matter
+  $self->register_eval_rule("check_for_forged_received_ip_helo"); # type does not matter
+  $self->register_eval_rule("helo_ip_mismatch"); # type does not matter
+  $self->register_eval_rule("check_for_no_rdns_dotcom_helo"); # type does not matter
 
   return $self;
 }
@@ -72,7 +74,7 @@ sub hostname_to_domain {
   }
 }
 
-sub _helo_forgery_whitelisted {
+sub _helo_forgery_welcomelisted {
   my ($helo, $rdns) = @_;
   if ($helo eq 'msn.com' && $rdns eq 'hotmail.com') { return 1; }
   0;
@@ -84,12 +86,10 @@ sub check_for_numeric_helo {
   my $rcvd = $pms->{relays_untrusted_str};
 
   if ($rcvd) {
-    my $IP_ADDRESS = IPV4_ADDRESS;
-    my $IP_PRIVATE = IP_PRIVATE;
     local $1;
     # no re "strict";  # since perl 5.21.8: Ranges of ASCII printables...
-    if ($rcvd =~ /\bhelo=($IP_ADDRESS)(?=[\000-\040,;\[()<>]|\z)/i  # Bug 5878
-        && $1 !~ /$IP_PRIVATE/) {
+    if ($rcvd =~ /\bhelo=($IPV4_ADDRESS)(?=[\000-\040,;\[()<>]|\z)/i  # Bug 5878
+        && $1 !~ IS_IP_PRIVATE) {
       return 1;
     }
   }
@@ -108,23 +108,21 @@ sub check_for_illegal_ip {
 # due to bug in pure IPv6 address regular expression
 sub helo_ip_mismatch {
   my ($self, $pms) = @_;
-  my $IP_ADDRESS = IPV4_ADDRESS;
-  my $IP_PRIVATE = IP_PRIVATE;
 
   for my $relay (@{$pms->{relays_untrusted}}) {
     # is HELO usable?
-    next unless ($relay->{helo} =~ m/^$IP_ADDRESS$/ &&
-                $relay->{helo} !~ /$IP_PRIVATE/);
+    next unless ($relay->{helo} =~ IS_IPV4_ADDRESS &&
+                $relay->{helo} !~ IS_IP_PRIVATE);
     # compare HELO with IP
-    return 1 if ($relay->{ip} =~ m/^$IP_ADDRESS$/ &&
-                $relay->{ip} !~ m/$IP_PRIVATE/ &&
+    return 1 if ($relay->{ip} =~ IS_IPV4_ADDRESS &&
+                $relay->{ip} !~ IS_IP_PRIVATE &&
                 $relay->{helo} ne $relay->{ip} &&
                 # different IP is okay if in same /24
                 $relay->{helo} =~ /^(\d+\.\d+\.\d+\.)/ &&
                 index($relay->{ip}, $1) != 0);
   }
 
-  0;
+  return 0;
 }
 
 ###########################################################################
@@ -145,7 +143,7 @@ sub check_no_relays {
 
 sub check_relays_unparseable {
   my ($self, $pms) = @_;
-  return $pms->{num_relays_unparseable};
+  return $pms->{num_relays_unparseable} ? 1 : 0;
 }
 
 # Check if the apparent sender (in the last received header) had
@@ -215,7 +213,7 @@ sub check_for_from_domain_in_received_headers {
 sub check_for_no_rdns_dotcom_helo {
   my ($self, $pms) = @_;
   if (!exists $pms->{no_rdns_dotcom_helo}) { $self->_check_received_helos($pms); }
-  return $pms->{no_rdns_dotcom_helo};
+  return $pms->{no_rdns_dotcom_helo} ? 1 : 0;
 }
 
 # Bug 1133
@@ -346,7 +344,7 @@ sub _check_for_forged_received {
        # allow private IP addrs here, could be a legit screwup
        if ($hclassb && $fclassb && 
                $hclassb ne $fclassb &&
-               !($hlo =~ /$IP_PRIVATE/o))
+               $hlo !~ IS_IP_PRIVATE)
        {
          dbg2("eval: forged-HELO: massive mismatch on IP-addr HELO: '$hlo' != '$fip'");
          $pms->{mismatch_ip_helo}++;
@@ -357,7 +355,7 @@ sub _check_for_forged_received {
     my $prev = $from[$i-1];
     if (defined($prev) && $i > 0
                && $prev =~ /^\w+(?:[\w.-]+\.)+\w+$/
-               && $by ne $prev && !_helo_forgery_whitelisted($by, $prev))
+               && $by ne $prev && !_helo_forgery_welcomelisted($by, $prev))
     {
       dbg2("eval: forged-HELO: mismatch on from: '$prev' != '$by'");
       $pms->{mismatch_from}++;
index ffd31b362180bbdef846978c6f2b54f2bf3d11cb..572c923b27e0d7ba99187c5ea3ad904909439875 100644 (file)
@@ -49,7 +49,6 @@ SpamAssassin; it is not guaranteed to work with other versions of SpamAssassin.
 
 package Mail::SpamAssassin::Plugin::ReplaceTags;
 
-use Mail::SpamAssassin;
 use Mail::SpamAssassin::Plugin;
 use Mail::SpamAssassin::Logger;
 use Mail::SpamAssassin::Util qw(compile_regexp qr_to_string);
@@ -136,7 +135,7 @@ sub finish_parsing_end {
         my $pre = $conf->{replace_pre}->{$pre_name};
         if ($pre) {
           s{($start.+?$end)}{$pre$1}  for @re;
-         }
+        }
       }
       if ($post_name) {
         my $post = $conf->{replace_post}->{$post_name};
@@ -173,12 +172,15 @@ sub finish_parsing_end {
       # do the actual replacement
       my ($rec, $err) = compile_regexp($re, 0);
       if (!$rec) {
-        info("replacetags: regexp compilation failed '$re': $err");
+        info("replacetags: regexp compilation failed for $rule: '$re': $err");
         next;
       }
       $conf->{test_qrs}->{$rule} = $rec;
-      #dbg("replacetags: replaced $rule: '$origre' => '$re'");
-      dbg("replacetags: replaced $rule");
+      if (would_log('dbg','replacetags') > 1) {
+        dbg("replacetags: replaced $rule: '$origre' => '$re'");
+      } else {
+        dbg("replacetags: replaced $rule");
+      }
     } else {
       dbg("replacetags: nothing was replaced in $rule");
     }
index 9179b93bd6ec7b509a63c3eb4968be1833f8eebe..dc77fad4d234976604b9b068c27552744f8e92cc 100644 (file)
@@ -42,11 +42,11 @@ NOTE: Because this plugin uses BSD::Resource, it will not function on Windows.
 
 =over 4
 
-=item resource_limit_cpu 120   (default: 0 or no limit)
+=item resource_limit_cpu 120    (default: 0 or no limit)
 
 How many cpu cycles are allowed on this process before it dies.
 
-=item resource_limit_mem 536870912     (default: 0 or no limit)
+=item resource_limit_mem 536870912    (default: 0 or no limit)
 
 The maximum number of bytes of memory allowed both for:
 
@@ -68,76 +68,80 @@ resident set size
 
 package Mail::SpamAssassin::Plugin::ResourceLimits;
 
-use Mail::SpamAssassin::Plugin ();
-use Mail::SpamAssassin::Logger ();
-use Mail::SpamAssassin::Util   ();
-use Mail::SpamAssassin::Constants qw(:sa);
+use Mail::SpamAssassin::Plugin;
+use Mail::SpamAssassin::Logger;
 
 use strict;
 use warnings;
+use re 'taint';
 
-use BSD::Resource qw(RLIMIT_RSS RLIMIT_AS RLIMIT_CPU);
+use constant HAS_BSD_RESOURCE =>
+  eval 'use BSD::Resource qw(RLIMIT_CPU RLIMIT_RSS RLIMIT_AS); 1;';
 
 our @ISA = qw(Mail::SpamAssassin::Plugin);
 
 sub new {
-    my $class        = shift;
-    my $mailsaobject = shift;
+  my $class    = shift;
+  my $mailsaobject = shift;
 
-    $class = ref($class) || $class;
-    my $self = $class->SUPER::new($mailsaobject);
-    bless( $self, $class );
+  $class = ref($class) || $class;
+  my $self = $class->SUPER::new($mailsaobject);
+  bless ($self, $class);
 
-    $self->set_config( $mailsaobject->{conf} );
-    return $self;
+  if (!HAS_BSD_RESOURCE) {
+    warn "ResourceLimits not used, required module BSD::Resource missing\n";
+  }
+
+  $self->set_config($mailsaobject->{conf});
+  return $self;
 }
 
 sub set_config {
-    my ( $self, $conf ) = @_;
-    my @cmds = ();
-
-    push(
-        @cmds,
-        {
-            setting  => 'resource_limit_mem',
-            is_admin => 1,
-            default  => '0',
-            type     => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
-        }
-    );
-
-    push(
-        @cmds,
-        {
-            setting  => 'resource_limit_cpu',
-            is_admin => 1,
-            default  => '0',
-            type     => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
-        }
-    );
-
-    $conf->{parser}->register_commands( \@cmds );
+  my ($self, $conf) = @_;
+  my @cmds;
+
+  push(@cmds, {
+    setting  => 'resource_limit_mem',
+    is_admin => 1,
+    default  => 0,
+    type     => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
+  });
+
+  push(@cmds, {
+    setting  => 'resource_limit_cpu',
+    is_admin => 1,
+    default  => 0,
+    type     => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
+  });
+
+  $conf->{parser}->register_commands(\@cmds);
 }
 
+if (HAS_BSD_RESOURCE) { eval '
 sub spamd_child_init {
-    my ($self) = @_;
-
-    # Set CPU Resource limits if they were specified.
-    Mail::SpamAssassin::Util::dbg("resourcelimitplugin: In spamd_child_init");
-    Mail::SpamAssassin::Util::dbg( "resourcelimitplugin: cpu limit: " . $self->{main}->{conf}->{resource_limit_cpu} );
-    if ( $self->{main}->{conf}->{resource_limit_cpu} ) {
-        BSD::Resource::setrlimit( RLIMIT_CPU, $self->{main}->{conf}->{resource_limit_cpu}, $self->{main}->{conf}->{resource_limit_cpu} )
-          || info("resourcelimitplugin: Unable to set RLIMIT_CPU");
-    }
-
-    # Set  Resource limits if they were specified.
-    Mail::SpamAssassin::Util::dbg( "resourcelimitplugin: mem limit: " . $self->{main}->{conf}->{resource_limit_mem} );
-    if ( $self->{main}->{conf}->{resource_limit_mem} ) {
-        BSD::Resource::setrlimit( RLIMIT_RSS, $self->{main}->{conf}->{resource_limit_mem}, $self->{main}->{conf}->{resource_limit_mem} )
-          || info("resourcelimitplugin: Unable to set RLIMIT_RSS");
-        BSD::Resource::setrlimit( RLIMIT_AS, $self->{main}->{conf}->{resource_limit_mem}, $self->{main}->{conf}->{resource_limit_mem} )
-          || info("resourcelimitplugin: Unable to set RLIMIT_AS");
-    }
+  my ($self) = @_;
+
+  my $conf = $self->{main}->{conf};
+
+  # Set CPU Resource limits if they were specified.
+  dbg("resourcelimits: cpu limit: " . $conf->{resource_limit_cpu});
+  if ($conf->{resource_limit_cpu}) {
+    BSD::Resource::setrlimit( RLIMIT_CPU,
+        $conf->{resource_limit_cpu}, $conf->{resource_limit_cpu} )
+      or info("resourcelimits: Unable to set RLIMIT_CPU");
+  }
+
+  # Set Resource limits if they were specified.
+  dbg("resourcelimits: mem limit: " . $conf->{resource_limit_mem});
+  if ($conf->{resource_limit_mem}) {
+    BSD::Resource::setrlimit( RLIMIT_RSS,
+        $conf->{resource_limit_mem}, $conf->{resource_limit_mem} )
+      or info("resourcelimits: Unable to set RLIMIT_RSS");
+    BSD::Resource::setrlimit( RLIMIT_AS,
+        $conf->{resource_limit_mem}, $conf->{resource_limit_mem} )
+      or info("resourcelimits: Unable to set RLIMIT_AS");
+  }
 }
+'; }
 
 1;
index 9596ea262b84520e3c7701aa0c8dad2788f79f74..83666e882951e89bbb4be290ef7f511ae98ca188 100644 (file)
@@ -10,6 +10,8 @@ Mail::SpamAssassin::Plugin::Reuse - For reusing old rule hits during a mass-chec
 
   reuse NETWORK_RULE [ NETWORK_RULE_OLD_NAME ]
 
+  run_reuse_tests_only 0/1
+
   endif
 
 =head1 DESCRIPTION
@@ -18,6 +20,15 @@ The purpose of this plugin is to work in conjunction with B<mass-check
 --reuse> to map rules hit in input messages to rule hits in the
 mass-check output.
 
+run_reuse_tests_only 1 is special option for spamassassin/spamd use.
+Only reuse flagged tests will be run. It will also _enable_ network/DNS
+lookups. This is mainly intended for fast mass processing of corpus
+messages, so they can be properly reused later. For example:
+  spamd --pre="loadmodule Mail::SpamAssassin::Plugin::Reuse" \
+    --pre="run_reuse_tests_only 1" ...
+Such dedicated spamd could be scripted to add X-Spam-Status header to
+messages efficiently.
+
 =cut
 
 package Mail::SpamAssassin::Plugin::Reuse;
@@ -25,12 +36,16 @@ package Mail::SpamAssassin::Plugin::Reuse;
 # use bytes;
 use strict;
 use warnings;
+use re 'taint';
 
 use Mail::SpamAssassin::Conf;
 use Mail::SpamAssassin::Logger;
+use Mail::SpamAssassin::Constants qw(:sa);
 
 our @ISA = qw(Mail::SpamAssassin::Plugin);
 
+my $RULENAME_RE = RULENAME_RE;
+
 # constructor
 sub new {
   my $invocant = shift;
@@ -42,7 +57,7 @@ sub new {
   bless ($self, $class);
 
   $self->set_config($samain->{conf});
-  # make sure we run last (or close) of the finish_parsing_start since
+  # make sure we run last (or close) of the finish_parsing_end since
   # we need all other rules to be defined
   $self->register_method_priority("finish_parsing_start", 100);
   return $self;
@@ -56,28 +71,35 @@ sub set_config {
   # e.g.
   # reuse NET_TEST_V1 NET_TEST_V0
 
-  push (@cmds, { setting => 'reuse',
-                 code => sub {
-                   my ($conf, $key, $value, $line) = @_;
+  push (@cmds, {
+    setting => 'reuse',
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE,
+    code => sub {
+      my ($conf, $key, $value, $line) = @_;
 
-                   if ($value !~ /\s*(\w+)(?:\s+(\w+(?:\s+\w+)*))?\s*$/) {
-                     return $Mail::SpamAssassin::Conf::INVALID_VALUE;
-                   }
-
-                   my $new_name = $1;
-                  my @old_names = ($new_name);
-                  if ($2) {
-                    push @old_names, split (' ', $2);
-                  }
-
-                   dbg("reuse: read rule, old: @old_names new: $new_name");
+      if ($value !~ /^\s*(${RULENAME_RE})(?:\s+(${RULENAME_RE}(?:\s+${RULENAME_RE})*))?\s*$/) {
+        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+      }
 
-                   foreach my $old (@old_names) {
-                     push @{$conf->{reuse_tests}->{$new_name}}, $old;
-                   }
+      my $new_name = $1;
+      my @old_names = ($new_name);
+      if (defined $2) {
+        push @old_names, split (/\s+/, $2);
+      }
 
-               }});
+      dbg("reuse: read rule, old: %s new: %s", join(' ', @old_names), $new_name);
+  
+      foreach my $old (@old_names) {
+        push @{$conf->{reuse_tests}->{$new_name}}, $old;
+      }
+    }
+  });
 
+  push(@cmds, {
+    setting => 'run_reuse_tests_only',
+    default => 0,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
+  });
 
   $conf->{parser}->register_commands(\@cmds);
 }
@@ -86,11 +108,27 @@ sub finish_parsing_start {
   my ($self, $opts) = @_;
 
   my $conf = $opts->{conf};
+  my $tflags = $conf->{tflags};
 
-  dbg("reuse: finish_parsing_start called");
+  while (my($rulename,$tfl) = each %{$tflags}) {
+    if ($tfl =~ /\bnet\b/ && !exists $conf->{reuse_tests}->{$rulename}) {
+      dbg("reuse: forcing reuse of net rule $rulename");
+      push @{$conf->{reuse_tests}->{$rulename}}, $rulename;
+    }
+  }
 
   return 0 if (!exists $conf->{reuse_tests});
 
+  if ($conf->{run_reuse_tests_only}) {
+    # simply delete all rules not reuse
+    foreach (keys %{$conf->{tests}}) {
+      if (!defined $conf->{reuse_tests}->{$_}) {
+        delete $conf->{tests}->{$_};
+      }
+    }
+    return 0;
+  }
+
   foreach my $rule_name (keys %{$conf->{reuse_tests}}) {
 
     # If the rule does not exist, add a new EMPTY test, set default score
@@ -100,7 +138,7 @@ sub finish_parsing_start {
     }
     if (!exists $conf->{scores}->{$rule_name}) {
       my $set_score = ($rule_name =~/^T_/) ? 0.01 : 1.0;
-      $set_score = -$set_score if ( ($conf->{tflags}->{$rule_name}||'') =~ /\bnice\b/ );
+      $set_score = -$set_score if ( ($tflags->{$rule_name}||'') =~ /\bnice\b/ );
       foreach my $ss (0..3) {
         $conf->{scoreset}->[$ss]->{$rule_name} = $set_score;
       }
@@ -108,7 +146,7 @@ sub finish_parsing_start {
 
     # Figure out when to add any hits -- grab priority and "stage"
     my $priority = $conf->{priority}->{$rule_name} || 0;
-    my $stage = $self->_get_stage_from_rule($opts->{conf}, $rule_name);
+    my $stage = $self->_get_stage_from_rule($conf, $rule_name);
     $conf->{reuse_tests_order}->{$rule_name} = [ $priority, $stage ];
 
   }
@@ -118,6 +156,10 @@ sub check_start {
   my ($self, $opts) = @_;
 
   my $pms = $opts->{permsgstatus};
+  my $conf = $pms->{conf};
+  my $scoreset = $conf->{scoreset};
+
+  return 0 if $conf->{run_reuse_tests_only};
 
   # Can we reuse?
   my $msg = $pms->get_message();
@@ -130,30 +172,34 @@ sub check_start {
 
   # now go through the rules and priorities and figure out which ones
   # need to be disabled
-  foreach my $rule (keys %{$pms->{conf}->{reuse_tests}}) {
+  foreach my $rule (keys %{$conf->{reuse_tests}}) {
 
-    dbg("reuse: looking at rule $rule");
-    my ($priority, $stage) = @{$pms->{conf}->{reuse_tests_order}->{$rule}};
+    my ($priority, $stage) = @{$conf->{reuse_tests_order}->{$rule}};
 
     # score set could change after check_start but before we add hits,
     # so we need to disable the rule in all sets
+    my @dis;
     foreach my $ss (0..3) {
-      if (exists $pms->{conf}->{scoreset}->[$ss]->{$rule}) {
-       dbg("reuse: disabling rule $rule in score set $ss");
-       $pms->{reuse_old_scores}->{$rule}->[$ss] =
-         $pms->{conf}->{scoreset}->[$ss]->{$rule};
-       $pms->{conf}->{scoreset}->[$ss]->{$rule} = 0;
+      if (exists $scoreset->[$ss]->{$rule}) {
+        $pms->{reuse_old_scores}->{$rule}->[$ss] =
+          $scoreset->[$ss]->{$rule};
+        $scoreset->[$ss]->{$rule} = 0;
+        push @dis, $ss;
       }
     }
+    dbg("reuse: disabling rule $rule in score sets %s",
+      join(',', @dis)) if @dis;
 
     # now, check for hits
-  OLD: foreach my $old_test (@{$pms->{conf}->{reuse_tests}->{$rule}}) {
-      dbg("reuse: looking for rule $old_test");
+    foreach my $old_test (@{$conf->{reuse_tests}->{$rule}}) {
       if ($old_hash->{$old_test}) {
         push @{$pms->{reuse_hits_to_add}->{"$priority $stage"}}, $rule;
         dbg("reuse: rule $rule hit, will add at priority $priority, stage " .
-           "$stage");
-        last OLD;
+           "$stage");
+        last;
+      } else {
+        # Make sure rule is marked ready for meta rules
+        $pms->rule_ready($rule);
       }
     }
   }
@@ -163,11 +209,15 @@ sub check_end {
   my ($self, $opts) = @_;
 
   my $pms = $opts->{permsgstatus};
+  my $conf = $pms->{conf};
+  my $scoreset = $conf->{scoreset};
+
+  return 0 if $conf->{run_reuse_tests_only};
 
   foreach my $disabled_rule (keys %{$pms->{reuse_old_scores}}) {
     foreach my $ss (0..3) {
-      next unless exists $pms->{conf}->{scoreset}->[$ss]->{$disabled_rule};
-      $pms->{conf}->{scoreset}->[$ss]->{$disabled_rule} =
+      next unless exists $scoreset->[$ss]->{$disabled_rule};
+      $scoreset->[$ss]->{$disabled_rule} =
         $pms->{reuse_old_scores}->{$disabled_rule}->[$ss];
     }
   }
@@ -178,8 +228,11 @@ sub check_end {
 sub start_rules {
   my ($self, $opts) = @_;
 
-  return $self->_add_hits($opts->{permsgstatus}, $opts->{priority},
-                         $opts->{ruletype});
+  my $pms = $opts->{permsgstatus};
+
+  return 0 if $pms->{conf}->{run_reuse_tests_only};
+
+  return $self->_add_hits($pms, $opts->{priority}, $opts->{ruletype});
 }
 
 sub _add_hits {
@@ -194,7 +247,7 @@ sub _add_hits {
       $pms->{reuse_old_scores}->{$rule}->[$ss] || 0.001;
 
     dbg("reuse: registering hit for $rule: score: " .
-       $pms->{conf}->{scores}->{$rule});
+       $pms->{conf}->{scores}->{$rule});
     $pms->got_hit($rule);
 
     $pms->{conf}->{scores}->{$rule} = 0;
@@ -203,19 +256,19 @@ sub _add_hits {
 }
 
 my %type_to_stage = (
-                    $Mail::SpamAssassin::Conf::TYPE_HEAD_TESTS    => "head",
-                    $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS    => "eval",
-                    $Mail::SpamAssassin::Conf::TYPE_BODY_TESTS    => "body",
-                    $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS    => "eval",
-                    $Mail::SpamAssassin::Conf::TYPE_FULL_TESTS    => "full",
-                    $Mail::SpamAssassin::Conf::TYPE_FULL_EVALS    => "eval",
-                    $Mail::SpamAssassin::Conf::TYPE_RAWBODY_TESTS => "rawbody",
-                    $Mail::SpamAssassin::Conf::TYPE_RAWBODY_EVALS => "eval",
-                    $Mail::SpamAssassin::Conf::TYPE_URI_TESTS     => "uri",
-                    $Mail::SpamAssassin::Conf::TYPE_URI_EVALS     => "eval",
-                    $Mail::SpamAssassin::Conf::TYPE_META_TESTS    => "meta",
-                    $Mail::SpamAssassin::Conf::TYPE_RBL_EVALS     => "eval",
-                   );
+  $Mail::SpamAssassin::Conf::TYPE_HEAD_TESTS    => "head",
+  $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS    => "eval",
+  $Mail::SpamAssassin::Conf::TYPE_BODY_TESTS    => "body",
+  $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS    => "eval",
+  $Mail::SpamAssassin::Conf::TYPE_FULL_TESTS    => "full",
+  $Mail::SpamAssassin::Conf::TYPE_FULL_EVALS    => "eval",
+  $Mail::SpamAssassin::Conf::TYPE_RAWBODY_TESTS => "rawbody",
+  $Mail::SpamAssassin::Conf::TYPE_RAWBODY_EVALS => "eval",
+  $Mail::SpamAssassin::Conf::TYPE_URI_TESTS     => "uri",
+  $Mail::SpamAssassin::Conf::TYPE_URI_EVALS     => "eval",
+  $Mail::SpamAssassin::Conf::TYPE_META_TESTS    => "meta",
+  $Mail::SpamAssassin::Conf::TYPE_RBL_EVALS     => "eval",
+);
 
 sub _get_stage_from_rule {
   my  ($self, $conf, $rule) = @_;
@@ -239,4 +292,4 @@ sub _get_stage_from_rule {
   }
 }
 
-
+1;
index ab9a0d81c3953c8cf84f52cd3715b310c9092e0b..1b94412889982cb7e21beae6be1d6a41422ba522 100644 (file)
@@ -143,6 +143,16 @@ sub setup_test_set_pri {
       dbg "zoom: skipping rule $name, ReplaceTags";
       next;
     }
+    # ignore regex capture rules
+    if ($conf->{capture_rules}->{$name}) {
+      dbg "zoom: skipping rule $name, regex capture";
+      next;
+    }
+    # ignore regex capture template rules
+    if ($conf->{capture_template_rules}->{$name}) {
+      dbg "zoom: skipping rule $name, regex capture template";
+      next;
+    }
 
     # we have the rule, and its regexp matches.  zero out the body
     # rule, so that the module can do the work instead
@@ -197,6 +207,11 @@ sub check_rules_at_priority {
   $self->{one_line_body}->check_rules_at_priority($params);
 }
 
+sub check_cleanup {
+  my ($self, $params) = @_;
+  $self->{one_line_body}->check_cleanup($params);
+}
+
 ###########################################################################
 
 sub run_body_fast_scan {
index dfff1f1d2e31b7e39c2f47e5a9b0b12eb7e4bbc1..a5d0274bbbdd73d97955799ca6f791eae4459d3f 100644 (file)
@@ -29,6 +29,12 @@ This plugin checks a message against Sender Policy Framework (SPF)
 records published by the domain owners in DNS to fight email address
 forgery and make it easier to identify spams.
 
+It's recommended to use MTA filter (pypolicyd-spf / spf-engine etc), so this
+plugin can reuse the Received-SPF and/or Authentication-Results header results as is.
+Otherwise throughput could suffer, DNS lookups done by this plugin are not
+asynchronous.
+Those headers will also help when SpamAssassin is not able to correctly detect EnvelopeFrom.
+
 =cut
 
 package Mail::SpamAssassin::Plugin::SPF;
@@ -53,22 +59,25 @@ sub new {
   my $self = $class->SUPER::new($mailsaobject);
   bless ($self, $class);
 
-  $self->register_eval_rule ("check_for_spf_pass");
-  $self->register_eval_rule ("check_for_spf_neutral");
-  $self->register_eval_rule ("check_for_spf_none");
-  $self->register_eval_rule ("check_for_spf_fail");
-  $self->register_eval_rule ("check_for_spf_softfail");
-  $self->register_eval_rule ("check_for_spf_permerror");
-  $self->register_eval_rule ("check_for_spf_temperror");
-  $self->register_eval_rule ("check_for_spf_helo_pass");
-  $self->register_eval_rule ("check_for_spf_helo_neutral");
-  $self->register_eval_rule ("check_for_spf_helo_none");
-  $self->register_eval_rule ("check_for_spf_helo_fail");
-  $self->register_eval_rule ("check_for_spf_helo_softfail");
-  $self->register_eval_rule ("check_for_spf_helo_permerror");
-  $self->register_eval_rule ("check_for_spf_helo_temperror");
-  $self->register_eval_rule ("check_for_spf_whitelist_from");
-  $self->register_eval_rule ("check_for_def_spf_whitelist_from");
+  $self->register_eval_rule ("check_for_spf_pass", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule ("check_for_spf_neutral", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule ("check_for_spf_none", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule ("check_for_spf_fail", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule ("check_for_spf_softfail", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule ("check_for_spf_permerror", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule ("check_for_spf_temperror", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule ("check_for_spf_helo_pass", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule ("check_for_spf_helo_neutral", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule ("check_for_spf_helo_none", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule ("check_for_spf_helo_fail", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule ("check_for_spf_helo_softfail", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule ("check_for_spf_helo_permerror", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule ("check_for_spf_helo_temperror", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule ("check_for_spf_welcomelist_from", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule ("check_for_spf_whitelist_from", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS); # removed in 4.1
+  $self->register_eval_rule ("check_for_def_spf_welcomelist_from", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule ("check_for_def_spf_whitelist_from", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS); # removed in 4.1
+  $self->register_eval_rule ("check_spf_skipped_noenvfrom", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
 
   $self->set_config($mailsaobject->{conf});
 
@@ -85,37 +94,43 @@ sub set_config {
 
 =over 4
 
-=item whitelist_from_spf user@example.com
+=item welcomelist_from_spf user@example.com
+
+Previously whitelist_from_spf which will work interchangeably until 4.1.
 
-Works similarly to whitelist_from, except that in addition to matching
-a sender address, a check against the domain's SPF record must pass.
-The first parameter is an address to whitelist, and the second is a string
-to match the relay's rDNS.
+Works similarly to welcomelist_from, except that in addition to matching a
+sender address, a check against the domain's SPF record must pass.  The
+first parameter is an address to welcomelist, and the second is a string to
+match the relay's rDNS.
 
-Just like whitelist_from, multiple addresses per line, separated by spaces,
-are OK. Multiple C<whitelist_from_spf> lines are also OK.
+Just like welcomelist_from, multiple addresses per line, separated by
+spaces, are OK.  Multiple C<welcomelist_from_spf> lines are also OK.
 
-The headers checked for whitelist_from_spf addresses are the same headers
+The headers checked for welcomelist_from_spf addresses are the same headers
 used for SPF checks (Envelope-From, Return-Path, X-Envelope-From, etc).
 
-Since this whitelist requires an SPF check to be made, network tests must be
+Since this welcomelist requires an SPF check to be made, network tests must be
 enabled. It is also required that your trust path be correctly configured.
 See the section on C<trusted_networks> for more info on trust paths.
 
 e.g.
 
-  whitelist_from_spf joe@example.com fred@example.com
-  whitelist_from_spf *@example.com
+  welcomelist_from_spf joe@example.com fred@example.com
+  welcomelist_from_spf *@example.com
 
-=item def_whitelist_from_spf user@example.com
+=item def_welcomelist_from_spf user@example.com
 
-Same as C<whitelist_from_spf>, but used for the default whitelist entries
-in the SpamAssassin distribution.  The whitelist score is lower, because
+Previously def_whitelist_from_spf which will work interchangeably until 4.1.
+
+Same as C<welcomelist_from_spf>, but used for the default welcomelist entries
+in the SpamAssassin distribution.  The welcomelist score is lower, because
 these are often targets for spammer spoofing.
 
-=item unwhitelist_from_spf user@example.com
+=item unwelcomelist_from_spf user@example.com
+
+Previously unwhitelist_from_spf which will work interchangeably until 4.1.
 
-Used to remove a C<whitelist_from_spf> or C<def_whitelist_from_spf> entry. 
+Used to remove a C<welcomelist_from_spf> or C<def_welcomelist_from_spf> entry. 
 The specified email address has to match exactly the address previously used.
 
 Useful for removing undesired default entries from a distributed configuration
@@ -124,17 +139,20 @@ by a local or site-specific configuration or by C<user_prefs>.
 =cut
 
   push (@cmds, {
-    setting => 'whitelist_from_spf',
+    setting => 'welcomelist_from_spf',
+    aliases => ['whitelist_from_spf'], # removed in 4.1
     type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST
   });
 
   push (@cmds, {
-    setting => 'def_whitelist_from_spf',
+    setting => 'def_welcomelist_from_spf',
+    aliases => ['def_whitelist_from_spf'], # removed in 4.1
     type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST
   });
 
   push (@cmds, {
-    setting => 'unwhitelist_from_spf',
+    setting => 'unwelcomelist_from_spf',
+    aliases => ['unwhitelist_from_spf'], # removed in 4.1
     type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST,
     code => sub {
       my ($self, $key, $value, $line) = @_;
@@ -144,9 +162,9 @@ by a local or site-specific configuration or by C<user_prefs>.
       unless ($value =~ /^(?:\S+(?:\s+\S+)*)$/) {
         return $Mail::SpamAssassin::Conf::INVALID_VALUE;
       }
-      $self->{parser}->remove_from_addrlist('whitelist_from_spf',
+      $self->{parser}->remove_from_addrlist('welcomelist_from_spf',
                                         split (/\s+/, $value));
-      $self->{parser}->remove_from_addrlist('def_whitelist_from_spf',
+      $self->{parser}->remove_from_addrlist('def_welcomelist_from_spf',
                                         split (/\s+/, $value));
     }
   });
@@ -173,38 +191,6 @@ days, weeks).
     type => $Mail::SpamAssassin::Conf::CONF_TYPE_DURATION
   });
 
-=item do_not_use_mail_spf (0|1)                (default: 0)
-
-By default the plugin will try to use the Mail::SPF module for SPF checks if
-it can be loaded.  If Mail::SPF cannot be used the plugin will fall back to
-using the legacy Mail::SPF::Query module if it can be loaded.
-
-Use this option to stop the plugin from using Mail::SPF and cause it to try to
-use Mail::SPF::Query instead.
-
-=cut
-
-  push(@cmds, {
-    setting => 'do_not_use_mail_spf',
-    is_admin => 1,
-    default => 0,
-    type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
-  });
-
-=item do_not_use_mail_spf_query (0|1)  (default: 0)
-
-As above, but instead stop the plugin from trying to use Mail::SPF::Query and
-cause it to only try to use Mail::SPF.
-
-=cut
-
-  push(@cmds, {
-    setting => 'do_not_use_mail_spf_query',
-    is_admin => 1,
-    default => 0,
-    type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
-  });
-
 =item ignore_received_spf_header (0|1) (default: 0)
 
 By default, to avoid unnecessary DNS lookups, the plugin will try to use the
@@ -250,35 +236,64 @@ working downwards until results are successfully parsed.
     type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
   });
 
+  # Deprecated since 4.0.0, leave for backwards compatibility
+  push(@cmds, {
+    setting => 'do_not_use_mail_spf',
+    is_admin => 1,
+    default => 0,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
+  });
+  push(@cmds, {
+    setting => 'do_not_use_mail_spf_query',
+    is_admin => 1,
+    default => 1,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
+  });
+
   $conf->{parser}->register_commands(\@cmds);
 }
 
-
 =item has_check_for_spf_errors
 
 Adds capability check for "if can()" for check_for_spf_permerror, check_for_spf_temperror, check_for_spf_helo_permerror and check_for_spf_helo_permerror
 
-=cut 
+=cut
 
 sub has_check_for_spf_errors { 1 }
 
+=item has_check_spf_skipped_noenvfrom
+
+Adds capability check for "if can()" for check_spf_skipped_noenvfrom
+
+=cut
+
+sub has_check_spf_skipped_noenvfrom { 1 }
+
+sub parsed_metadata {
+  my ($self, $opts) = @_;
+
+  $self->_get_sender($opts->{permsgstatus});
+
+  return 1;
+}
+
 # SPF support
 sub check_for_spf_pass {
   my ($self, $scanner) = @_;
   $self->_check_spf ($scanner, 0) unless $scanner->{spf_checked};
-  $scanner->{spf_pass};
+  return $scanner->{spf_pass} ? 1 : 0;
 }
 
 sub check_for_spf_neutral {
   my ($self, $scanner) = @_;
   $self->_check_spf ($scanner, 0) unless $scanner->{spf_checked};
-  $scanner->{spf_neutral};
+  return $scanner->{spf_neutral} ? 1 : 0;
 }
 
 sub check_for_spf_none {
   my ($self, $scanner) = @_;
   $self->_check_spf ($scanner, 0) unless $scanner->{spf_checked};
-  $scanner->{spf_none};
+  return $scanner->{spf_none} ? 1 : 0;
 }
 
 sub check_for_spf_fail {
@@ -287,43 +302,43 @@ sub check_for_spf_fail {
   if ($scanner->{spf_failure_comment}) {
     $scanner->test_log ($scanner->{spf_failure_comment});
   }
-  $scanner->{spf_fail};
+  return $scanner->{spf_fail} ? 1 : 0;
 }
 
 sub check_for_spf_softfail {
   my ($self, $scanner) = @_;
   $self->_check_spf ($scanner, 0) unless $scanner->{spf_checked};
-  $scanner->{spf_softfail};
+  return $scanner->{spf_softfail} ? 1 : 0;
 }
 
 sub check_for_spf_permerror {
   my ($self, $scanner) = @_;
   $self->_check_spf ($scanner, 0) unless $scanner->{spf_checked};
-  $scanner->{spf_permerror};
+  return $scanner->{spf_permerror} ? 1 : 0;
 }
 
 sub check_for_spf_temperror {
   my ($self, $scanner) = @_;
   $self->_check_spf ($scanner, 0) unless $scanner->{spf_checked};
-  $scanner->{spf_temperror};
+  return $scanner->{spf_temperror} ? 1 : 0;
 }
 
 sub check_for_spf_helo_pass {
   my ($self, $scanner) = @_;
   $self->_check_spf ($scanner, 1) unless $scanner->{spf_helo_checked};
-  $scanner->{spf_helo_pass};
+  return $scanner->{spf_helo_pass} ? 1 : 0;
 }
 
 sub check_for_spf_helo_neutral {
   my ($self, $scanner) = @_;
   $self->_check_spf ($scanner, 1) unless $scanner->{spf_helo_checked};
-  $scanner->{spf_helo_neutral};
+  return $scanner->{spf_helo_neutral} ? 1 : 0;
 }
 
 sub check_for_spf_helo_none {
   my ($self, $scanner) = @_;
   $self->_check_spf ($scanner, 1) unless $scanner->{spf_helo_checked};
-  $scanner->{spf_helo_none};
+  return $scanner->{spf_helo_none} ? 1 : 0;
 }
 
 sub check_for_spf_helo_fail {
@@ -332,38 +347,60 @@ sub check_for_spf_helo_fail {
   if ($scanner->{spf_helo_failure_comment}) {
     $scanner->test_log ($scanner->{spf_helo_failure_comment});
   }
-  $scanner->{spf_helo_fail};
+  return $scanner->{spf_helo_fail} ? 1 : 0;
 }
 
 sub check_for_spf_helo_softfail {
   my ($self, $scanner) = @_;
   $self->_check_spf ($scanner, 1) unless $scanner->{spf_helo_checked};
-  $scanner->{spf_helo_softfail};
+  return $scanner->{spf_helo_softfail} ? 1 : 0;
 }
 
 sub check_for_spf_helo_permerror {
   my ($self, $scanner) = @_;
   $self->_check_spf ($scanner, 1) unless $scanner->{spf_helo_checked};
-  $scanner->{spf_helo_permerror};
+  return $scanner->{spf_helo_permerror} ? 1 : 0;
 }
 
 sub check_for_spf_helo_temperror {
   my ($self, $scanner) = @_;
   $self->_check_spf ($scanner, 1) unless $scanner->{spf_helo_checked};
-  $scanner->{spf_helo_temperror};
+  return $scanner->{spf_helo_temperror} ? 1 : 0;
+}
+
+=over 4
+
+=item check_spf_skipped_noenvfrom
+
+Checks if SPF checks have been skipped because EnvelopeFrom cannot be determined.
+
+=back
+
+=cut
+
+sub check_spf_skipped_noenvfrom {
+  my ($self, $scanner) = @_;
+  $self->_check_spf ($scanner, 0) unless $scanner->{spf_checked};
+  if (!exists $scanner->{spf_sender}) {
+    return 1;
+  } else {
+    return 0;
+  }
 }
 
-sub check_for_spf_whitelist_from {
+sub check_for_spf_welcomelist_from {
   my ($self, $scanner) = @_;
-  $self->_check_spf_whitelist($scanner) unless $scanner->{spf_whitelist_from_checked};
-  $scanner->{spf_whitelist_from};
+  $self->_check_spf_welcomelist($scanner) unless $scanner->{spf_welcomelist_from_checked};
+  return $scanner->{spf_welcomelist_from} ? 1 : 0;
 }
+*check_for_spf_whitelist_from = \&check_for_spf_welcomelist_from; # removed in 4.1
 
-sub check_for_def_spf_whitelist_from {
+sub check_for_def_spf_welcomelist_from {
   my ($self, $scanner) = @_;
-  $self->_check_def_spf_whitelist($scanner) unless $scanner->{def_spf_whitelist_from_checked};
-  $scanner->{def_spf_whitelist_from};
+  $self->_check_def_spf_welcomelist($scanner) unless $scanner->{def_spf_welcomelist_from_checked};
+  return $scanner->{def_spf_welcomelist_from} ? 1 : 0;
 }
+*check_for_def_spf_whitelist_from = \&check_for_def_spf_welcomelist_from; # removed in 4.1
 
 sub _check_spf {
   my ($self, $scanner, $ishelo) = @_;
@@ -387,7 +424,7 @@ sub _check_spf {
     $scanner->{checked_for_received_spf_header} = 1;
     dbg("spf: checking to see if the message has a Received-SPF header that we can use");
 
-    my @internal_hdrs = split("\n", $scanner->get('ALL-INTERNAL'));
+    my @internal_hdrs = $scanner->get('ALL-INTERNAL');
     unless ($scanner->{conf}->{use_newest_received_spf_header}) {
       # look for the LAST (earliest in time) header, it'll be the most accurate
       @internal_hdrs = reverse(@internal_hdrs);
@@ -459,7 +496,7 @@ sub _check_spf {
          dbg("spf: could not parse result from existing Received-SPF header");
        }
 
-      } elsif ($hdr =~ /^Authentication-Results:.*;\s*SPF\s*=\s*([^;]*)/i) {
+      } elsif ($hdr =~ /^(?:Arc\-)?Authentication-Results:.*;\s*SPF\s*=\s*([^;]*)/i) {
         dbg("spf: found an Authentication-Results header added by an internal host: $hdr");
 
         # RFC 5451 header parser - added by D. Stussy 2010-09-09:
@@ -524,8 +561,6 @@ sub _check_spf {
   unless (defined $self->{has_mail_spf}) {
     my $eval_stat;
     eval {
-      die("Mail::SPF disabled by admin setting\n") if $scanner->{conf}->{do_not_use_mail_spf};
-
       require Mail::SPF;
       if (!defined $Mail::SPF::VERSION || $Mail::SPF::VERSION < 2.001) {
        die "Mail::SPF 2.001 or later required, this is ".
@@ -547,43 +582,18 @@ sub _check_spf {
       dbg("spf: using Mail::SPF for SPF checks");
       $self->{has_mail_spf} = 1;
     } else {
-      # strip the @INC paths... users are going to see it and think there's a problem even though
-      # we're going to fall back to Mail::SPF::Query (which will display the same paths if it fails)
-      $eval_stat =~ s#^Can't locate Mail/SPFd.pm in \@INC .*#Can't locate Mail/SPFd.pm#;
-      dbg("spf: cannot load Mail::SPF module or create Mail::SPF::Server object: $eval_stat");
-      dbg("spf: attempting to use legacy Mail::SPF::Query module instead");
-
-      undef $eval_stat;
-      eval {
-       die("Mail::SPF::Query disabled by admin setting\n") if $scanner->{conf}->{do_not_use_mail_spf_query};
-
-       require Mail::SPF::Query;
-       if (!defined $Mail::SPF::Query::VERSION || $Mail::SPF::Query::VERSION < 1.996) {
-         die "Mail::SPF::Query 1.996 or later required, this is ".
-           (defined $Mail::SPF::Query::VERSION ? $Mail::SPF::Query::VERSION : 'unknown')."\n";
-       }
-        1;
-      } or do {
-        $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
-      };
-
-      if (!defined($eval_stat)) {
-       dbg("spf: using Mail::SPF::Query for SPF checks");
-       $self->{has_mail_spf} = 0;
-      } else {
-       dbg("spf: cannot load Mail::SPF::Query module: $eval_stat");
-       dbg("spf: one of Mail::SPF or Mail::SPF::Query is required for SPF checks, SPF checks disabled");
-       $self->{no_spf_module} = 1;
-       return;
-      }
+      dbg("spf: cannot load Mail::SPF: module: $eval_stat");
+      dbg("spf: Mail::SPF is required for SPF checks, SPF checks disabled");
+      $self->{no_spf_module} = 1;
+      return;
     }
   }
 
-
   # skip SPF checks if the A/MX records are nonexistent for the From
   # domain, anyway, to avoid crappy messages from slowing us down
   # (bug 3016)
-  return if $scanner->check_for_from_dns();
+  # TODO: this will only work if the queries are ready before SPF, so never?
+  return if $scanner->{sender_host_fail} && $scanner->{sender_host_fail} == 2;
 
   if ($ishelo) {
     # SPF HELO-checking variant
@@ -609,7 +619,7 @@ sub _check_spf {
     $scanner->{spf_failure_comment} = undef;
   }
 
-  my $lasthop = $self->_get_relay($scanner);
+  my $lasthop = $scanner->{relays_external}->[0];
   if (!defined $lasthop) {
     dbg("spf: no suitable relay for spf use found, skipping SPF%s check",
         $ishelo ? '-helo' : '');
@@ -618,7 +628,6 @@ sub _check_spf {
 
   my $ip = $lasthop->{ip};     # always present
   my $helo = $lasthop->{helo}; # could be missing
-  $scanner->{sender} = '' unless $scanner->{sender_got};
 
   if ($ishelo) {
     unless ($helo) {
@@ -627,19 +636,17 @@ sub _check_spf {
     }
     dbg("spf: checking HELO (helo=$helo, ip=$ip)");
   } else {
-    $self->_get_sender($scanner) unless $scanner->{sender_got};
-
     # TODO: we're supposed to use the helo domain as the sender identity (for
     # mfrom checks) if the sender is the null sender, however determining that
     # it's the null sender, and not just a failure to get the envelope isn't
     # exactly trivial... so for now we'll just skip the check
 
-    if (!$scanner->{sender}) {
+    if (!$scanner->{spf_sender}) {
       # we already dbg'd that we couldn't get an Envelope-From and can't do SPF
       return;
     }
     dbg("spf: checking EnvelopeFrom (helo=%s, ip=%s, envfrom=%s)",
-        ($helo ? $helo : ''), $ip, $scanner->{sender});
+        ($helo ? $helo : ''), $ip, $scanner->{spf_sender});
   }
 
   # this test could probably stand to be more strict, but try to test
@@ -657,81 +664,39 @@ sub _check_spf {
 
   my ($result, $comment, $text, $err);
 
-  # use Mail::SPF if it was available, otherwise use the legacy Mail::SPF::Query
-  if ($self->{has_mail_spf}) {
-
-    # TODO: currently we won't get to here for a mfrom check with a null sender
-    my $identity = $ishelo ? $helo : ($scanner->{sender}); # || $helo);
-
-    unless ($identity) {
-      dbg("spf: cannot determine %s identity, skipping %s SPF check",
-          ($ishelo ? 'helo' : 'mfrom'),  ($ishelo ? 'helo' : 'mfrom') );
-      return;
-    }
-    $helo ||= 'unknown';  # only used for macro expansion in the mfrom explanation
-
-    my $request;
-    eval {
-      $request = Mail::SPF::Request->new( scope         => $ishelo ? 'helo' : 'mfrom',
-                                         identity      => $identity,
-                                         ip_address    => $ip,
-                                         helo_identity => $helo );
-      1;
-    } or do {
-      my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
-      dbg("spf: cannot create Mail::SPF::Request object: $eval_stat");
-      return;
-    };
-
-    my $timeout = $scanner->{conf}->{spf_timeout};
-
-    my $timer = Mail::SpamAssassin::Timeout->new(
-                { secs => $timeout, deadline => $scanner->{master_deadline} });
-    $err = $timer->run_and_catch(sub {
-
-      my $query = $self->{spf_server}->process($request);
-
-      $result = $query->code;
-      $comment = $query->authority_explanation if $query->can("authority_explanation");
-      $text = $query->text;
-
-    });
-
-
-  } else {
-
-    if (!$helo) {
-      dbg("spf: cannot get HELO, cannot use Mail::SPF::Query, consider installing Mail::SPF");
-      return;
-    }
-
-    # TODO: if we start doing checks on the null sender using the helo domain
-    # be sure to fix this so that it uses the correct sender identity
-    my $query;
-    eval {
-      $query = Mail::SPF::Query->new (ip => $ip,
-                                   sender => $scanner->{sender},
-                                   helo => $helo,
-                                   debug => 0,
-                                   trusted => 0);
-      1;
-    } or do {
-      my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
-      dbg("spf: cannot create Mail::SPF::Query object: $eval_stat");
-      return;
-    };
-
-    my $timeout = $scanner->{conf}->{spf_timeout};
-
-    my $timer = Mail::SpamAssassin::Timeout->new(
-                { secs => $timeout, deadline => $scanner->{master_deadline} });
-    $err = $timer->run_and_catch(sub {
+  # TODO: currently we won't get to here for a mfrom check with a null sender
+  my $identity = $ishelo ? $helo : ($scanner->{spf_sender}); # || $helo);
 
-      ($result, $comment) = $query->result();
+  unless ($identity) {
+    dbg("spf: cannot determine %s identity, skipping %s SPF check",
+        ($ishelo ? 'helo' : 'mfrom'),  ($ishelo ? 'helo' : 'mfrom') );
+    return;
+  }
+  $helo ||= 'unknown';  # only used for macro expansion in the mfrom explanation
+
+  my $request;
+  eval {
+    $request = Mail::SPF::Request->new( scope => $ishelo ? 'helo' : 'mfrom',
+                         identity      => $identity,
+                         ip_address    => $ip,
+                         helo_identity => $helo );
+    1;
+  } or do {
+    my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
+    dbg("spf: cannot create Mail::SPF::Request object: $eval_stat");
+    return;
+  };
 
-    });
+  my $timeout = $scanner->{conf}->{spf_timeout};
 
-  } # end of differences between Mail::SPF and Mail::SPF::Query
+  my $timer_spf = Mail::SpamAssassin::Timeout->new(
+              { secs => $timeout, deadline => $scanner->{master_deadline} });
+  $err = $timer_spf->run_and_catch(sub {
+    my $query = $self->{spf_server}->process($request);
+    $result = $query->code;
+    $comment = $query->authority_explanation if $query->can("authority_explanation");
+    $text = $query->text;
+  });
 
   if ($err) {
     chomp $err;
@@ -739,7 +704,6 @@ sub _check_spf {
     return 0;
   }
 
-
   $result ||= 'timeout';       # bug 5077
   $comment ||= '';
   $comment =~ s/\s+/ /gs;      # no newlines please
@@ -774,138 +738,131 @@ sub _check_spf {
     }
   }
 
-  dbg("spf: query for $scanner->{sender}/$ip/$helo: result: $result, comment: $comment, text: $text");
-}
-
-sub _get_relay {
-  my ($self, $scanner) = @_;
-
-  # dos: first external relay, not first untrusted
-  return $scanner->{relays_external}->[0];
+  if ($ishelo) {
+    dbg("spf: query for $ip/$helo: result: $result, comment: $comment, text: $text");
+  } else {
+    dbg("spf: query for $scanner->{spf_sender}/$ip/$helo: result: $result, comment: $comment, text: $text");
+  }
 }
 
 sub _get_sender {
   my ($self, $scanner) = @_;
-  my $sender;
 
-  $scanner->{sender_got} = 1;
-  $scanner->{sender} = '';
-
-  my $relay = $self->_get_relay($scanner);
+  my $relay = $scanner->{relays_external}->[0];
   if (defined $relay) {
-    $sender = $relay->{envfrom};
+    my $sender = $relay->{envfrom};
+    if (defined $sender) {
+      dbg("spf: found EnvelopeFrom '$sender' in first external Received header");      
+      $scanner->{spf_sender} = lc $sender;
+    } else {
+      dbg("spf: EnvelopeFrom not found in first external Received header");
+    }
   }
 
-  if ($sender) {
-    dbg("spf: found Envelope-From in first external Received header");
-  }
-  else {
+  if (!exists $scanner->{spf_sender}) {
     # We cannot use the env-from data, since it went through 1 or more relays 
     # since the untrusted sender and they may have rewritten it.
-    if ($scanner->{num_relays_trusted} > 0 && !$scanner->{conf}->{always_trust_envelope_sender}) {
-      dbg("spf: relayed through one or more trusted relays, cannot use header-based Envelope-From, skipping");
-      return;
+    if ($scanner->{num_relays_trusted} > 0 &&
+          !$scanner->{conf}->{always_trust_envelope_sender}) {
+      dbg("spf: relayed through one or more trusted relays, ".
+            "cannot use header-based EnvelopeFrom");
+    } else {
+      # we can (apparently) use whatever the current EnvelopeFrom was,
+      # from the Return-Path, X-Envelope-From, or whatever header.
+      # it's better to get it from Received though, as that is updated
+      # hop-by-hop.
+      my $sender = ($scanner->get("EnvelopeFrom:addr"))[0];
+      if (defined $sender) {
+        dbg("spf: found EnvelopeFrom '$sender' from header");
+        $scanner->{spf_sender} = lc $sender;
+      } else {
+        dbg("spf: EnvelopeFrom header not found");
+      }
     }
-
-    # we can (apparently) use whatever the current Envelope-From was,
-    # from the Return-Path, X-Envelope-From, or whatever header.
-    # it's better to get it from Received though, as that is updated
-    # hop-by-hop.
-    $sender = $scanner->get("EnvelopeFrom:addr");
   }
 
-  if (!$sender) {
-    dbg("spf: cannot get Envelope-From, cannot use SPF");
-    return;  # avoid setting $scanner->{sender} to undef
+  if (!exists $scanner->{spf_sender}) {
+    dbg("spf: cannot get EnvelopeFrom, cannot use SPF by DNS");
   }
-
-  return $scanner->{sender} = lc $sender;
 }
 
-sub _check_spf_whitelist {
+sub _check_spf_welcomelist {
   my ($self, $scanner) = @_;
 
-  $scanner->{spf_whitelist_from_checked} = 1;
-  $scanner->{spf_whitelist_from} = 0;
+  $scanner->{spf_welcomelist_from_checked} = 1;
+  $scanner->{spf_welcomelist_from} = 0;
 
   # if we've already checked for an SPF PASS and didn't get it don't waste time
-  # checking to see if the sender address is in the spf whitelist
+  # checking to see if the sender address is in the spf welcomelist
   if ($scanner->{spf_checked} && !$scanner->{spf_pass}) {
-    dbg("spf: whitelist_from_spf: already checked spf and didn't get pass, skipping whitelist check");
+    dbg("spf: welcomelist_from_spf: already checked spf and didn't get pass, skipping welcomelist check");
     return;
   }
 
-  $self->_get_sender($scanner) unless $scanner->{sender_got};
-
-  unless ($scanner->{sender}) {
-    dbg("spf: spf_whitelist_from: could not find usable envelope sender");
+  if (!$scanner->{spf_sender}) {
+    dbg("spf: spf_welcomelist_from: no EnvelopeFrom available for welcomelist check");
     return;
   }
 
-  $scanner->{spf_whitelist_from} = $self->_wlcheck($scanner,'whitelist_from_spf');
-  if (!$scanner->{spf_whitelist_from}) {
-    $scanner->{spf_whitelist_from} = $self->_wlcheck($scanner, 'whitelist_auth');
-  }
+  $scanner->{spf_welcomelist_from} =
+    $self->_wlcheck($scanner, 'welcomelist_from_spf') ||
+    $self->_wlcheck($scanner, 'welcomelist_auth');
 
-  # if the message doesn't pass SPF validation, it can't pass an SPF whitelist
-  if ($scanner->{spf_whitelist_from}) {
+  # if the message doesn't pass SPF validation, it can't pass an SPF welcomelist
+  if ($scanner->{spf_welcomelist_from}) {
     if ($self->check_for_spf_pass($scanner)) {
-      dbg("spf: whitelist_from_spf: $scanner->{sender} is in user's WHITELIST_FROM_SPF and passed SPF check");
+      dbg("spf: welcomelist_from_spf: $scanner->{spf_sender} is in user's WELCOMELIST_FROM_SPF and passed SPF check");
     } else {
-      dbg("spf: whitelist_from_spf: $scanner->{sender} is in user's WHITELIST_FROM_SPF but failed SPF check");
-      $scanner->{spf_whitelist_from} = 0;
+      dbg("spf: welcomelist_from_spf: $scanner->{spf_sender} is in user's WELCOMELIST_FROM_SPF but failed SPF check");
+      $scanner->{spf_welcomelist_from} = 0;
     }
   } else {
-    dbg("spf: whitelist_from_spf: $scanner->{sender} is not in user's WHITELIST_FROM_SPF");
+    dbg("spf: welcomelist_from_spf: $scanner->{spf_sender} is not in user's WELCOMELIST_FROM_SPF");
   }
 }
 
-sub _check_def_spf_whitelist {
+sub _check_def_spf_welcomelist {
   my ($self, $scanner) = @_;
 
-  $scanner->{def_spf_whitelist_from_checked} = 1;
-  $scanner->{def_spf_whitelist_from} = 0;
+  $scanner->{def_spf_welcomelist_from_checked} = 1;
+  $scanner->{def_spf_welcomelist_from} = 0;
 
   # if we've already checked for an SPF PASS and didn't get it don't waste time
-  # checking to see if the sender address is in the spf whitelist
+  # checking to see if the sender address is in the spf welcomelist
   if ($scanner->{spf_checked} && !$scanner->{spf_pass}) {
-    dbg("spf: def_spf_whitelist_from: already checked spf and didn't get pass, skipping whitelist check");
+    dbg("spf: def_spf_welcomelist_from: already checked spf and didn't get pass, skipping welcomelist check");
     return;
   }
 
-  $self->_get_sender($scanner) unless $scanner->{sender_got};
-
-  unless ($scanner->{sender}) {
-    dbg("spf: def_spf_whitelist_from: could not find usable envelope sender");
+  if (!$scanner->{spf_sender}) {
+    dbg("spf: def_spf_welcomelist_from: could not find usable envelope sender");
     return;
   }
 
-  $scanner->{def_spf_whitelist_from} = $self->_wlcheck($scanner,'def_whitelist_from_spf');
-  if (!$scanner->{def_spf_whitelist_from}) {
-    $scanner->{def_spf_whitelist_from} = $self->_wlcheck($scanner, 'def_whitelist_auth');
-  }
+  $scanner->{def_spf_welcomelist_from} =
+    $self->_wlcheck($scanner, 'def_welcomelist_from_spf') ||
+    $self->_wlcheck($scanner, 'def_welcomelist_auth');
 
-  # if the message doesn't pass SPF validation, it can't pass an SPF whitelist
-  if ($scanner->{def_spf_whitelist_from}) {
+  # if the message doesn't pass SPF validation, it can't pass an SPF welcomelist
+  if ($scanner->{def_spf_welcomelist_from}) {
     if ($self->check_for_spf_pass($scanner)) {
-      dbg("spf: def_whitelist_from_spf: $scanner->{sender} is in DEF_WHITELIST_FROM_SPF and passed SPF check");
+      dbg("spf: def_welcomelist_from_spf: $scanner->{spf_sender} is in DEF_WELCOMELIST_FROM_SPF and passed SPF check");
     } else {
-      dbg("spf: def_whitelist_from_spf: $scanner->{sender} is in DEF_WHITELIST_FROM_SPF but failed SPF check");
-      $scanner->{def_spf_whitelist_from} = 0;
+      dbg("spf: def_welcomelist_from_spf: $scanner->{spf_sender} is in DEF_WELCOMELIST_FROM_SPF but failed SPF check");
+      $scanner->{def_spf_welcomelist_from} = 0;
     }
   } else {
-    dbg("spf: def_whitelist_from_spf: $scanner->{sender} is not in DEF_WHITELIST_FROM_SPF");
+    dbg("spf: def_welcomelist_from_spf: $scanner->{spf_sender} is not in DEF_WELCOMELIST_FROM_SPF");
   }
 }
 
 sub _wlcheck {
   my ($self, $scanner, $param) = @_;
-  if (defined ($scanner->{conf}->{$param}->{$scanner->{sender}})) {
+  if (defined ($scanner->{conf}->{$param}->{$scanner->{spf_sender}})) {
     return 1;
   } else {
-    study $scanner->{sender};  # study is a no-op since perl 5.16.0
     foreach my $regexp (values %{$scanner->{conf}->{$param}}) {
-      if ($scanner->{sender} =~ qr/$regexp/i) {
+      if ($scanner->{spf_sender} =~ $regexp) {
         return 1;
       }
     }
index 3d3372237220f318facb73c3838d20d27005467b..64213afac434dab2fc29521ac71f4358f86c936e 100644 (file)
@@ -58,7 +58,7 @@ sub new {
   my $self = $class->SUPER::new($mailsaobject);
   bless ($self, $class);
 
-  $self->register_eval_rule("check_shortcircuit");
+  $self->register_eval_rule("check_shortcircuit"); # type does not matter
   $self->set_config($mailsaobject->{conf});
 
   return $self;
@@ -88,6 +88,14 @@ that.
 To override a test that uses shortcircuiting, you can set the classification
 type to C<off>.
 
+Note that DNS and other network lookups are launched when SA reaches
+priority -100.  If you want to shortcircuit scanning before any network
+queries are sent, you need to set lower than -100 priority to any such rule,
+like -200 as in the examples below.
+
+Shortcircuited test will be automatically set to priority -200, but only if
+the original priority is unchanged at default 0.
+
 =over 4
 
 =item on
@@ -99,7 +107,7 @@ shortcircuited.  This would allow you, for example, to define a rule such as
   body TEST /test/
   describe TEST test rule that scores barely over spam threshold
   score TEST 5.5
-  priority TEST -100
+  priority TEST -200
   shortcircuit TEST on
 
 The result of a message hitting the above rule would be a final score of 5.5,
@@ -113,11 +121,11 @@ Disables shortcircuiting on said rule.
 
 Shortcircuit the rule using a set of defaults; override the default score of
 this rule with the score from C<shortcircuit_spam_score>, set the
-C<noautolearn> tflag, and set priority to C<-100>.  In other words,
+C<noautolearn> tflag, and set priority to C<-200>.  In other words,
 equivalent to:
 
   shortcircuit TEST on
-  priority TEST -100
+  priority TEST -200
   score TEST 100
   tflags TEST noautolearn
 
@@ -125,11 +133,11 @@ equivalent to:
 
 Shortcircuit the rule using a set of defaults; override the default score of
 this rule with the score from C<shortcircuit_ham_score>, set the C<noautolearn>
-and C<nice> tflags, and set priority to C<-100>.   In other words, equivalent
+and C<nice> tflags, and set priority to C<-200>.   In other words, equivalent
 to:
 
   shortcircuit TEST on
-  priority TEST -100
+  priority TEST -200
   score TEST -100
   tflags TEST noautolearn nice
 
@@ -141,23 +149,22 @@ to:
     setting => 'shortcircuit',
     code => sub {
       my ($self, $key, $value, $line) = @_;
-      my ($rule,$type);
       unless (defined $value && $value !~ /^$/) {
         return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
       }
-      if ($value =~ /^(\S+)\s+(\S+)$/) {
-        $rule=$1;
-        $type=$2;
-      } else {
+      local($1,$2);
+      unless ($value =~ /^(\w+)\s+(\w+)$/) {
         return $Mail::SpamAssassin::Conf::INVALID_VALUE;
       }
+      my ($rule, $type) = ($1, $2);
 
-      if ($type =~ m/^(?:spam|ham)$/) {
+      if ($type eq "ham" || $type eq "spam") {
         dbg("shortcircuit: adding $rule using abbreviation $type");
 
         # set the defaults:
         $self->{shortcircuit}->{$rule} = $type;
-        $self->{priority}->{$rule} = -100;
+        # don't override existing priority unless it's default 0
+        $self->{priority}->{$rule} ||= -200;
 
         my $tf = $self->{tflags}->{$rule};
         $self->{tflags}->{$rule} = ($tf ? $tf." " : "") .
@@ -227,7 +234,7 @@ sub hit_rule {
   my $rule = $params->{rulename};
 
   # don't s/c if we're linting
-  return if ($scan->{lint_rules});
+  return if ($self->{main}->{lint_rules});
 
   # don't s/c if we're in compile_now()
   return if ($self->{am_compiling});
@@ -256,6 +263,8 @@ sub hit_rule {
     $scscore = $score;
   }
 
+  $scan->{shortcircuited} = 1;
+
   # bug 5256: if we short-circuit, don't do auto-learning
   $scan->{disable_auto_learning} = 1;
   $scan->got_hit('SHORTCIRCUIT', '', score => $scscore);
@@ -306,6 +315,6 @@ sub compile_now_finish {
 
 =head1 SEE ALSO
 
-C<http://issues.apache.org/SpamAssassin/show_bug.cgi?id=3109>
+C<https://issues.apache.org/SpamAssassin/show_bug.cgi?id=3109>
 
 =cut
index 93281841c400fb4085590dfdc9393a02c01671a4..b4d12187e171ff46c3c2e1045c1c2cf38bb01ea4 100644 (file)
@@ -29,13 +29,13 @@ SpamCop is a service for reporting spam.  SpamCop determines the origin
 of unwanted email and reports it to the relevant Internet service
 providers.  By reporting spam, you have a positive impact on the
 problem.  Reporting unsolicited email also helps feed spam filtering
-systems, including, but not limited to, the SpamCop blacklist used in
+systems, including, but not limited to, the SpamCop blocklist used in
 SpamAssassin as a DNSBL.
 
 Note that spam reports sent by this plugin to SpamCop each include the
 entire spam message.
 
-See http://www.spamcop.net/ for more information about SpamCop.
+See https://www.spamcop.net/ for more information about SpamCop.
 
 =cut
 
@@ -43,13 +43,13 @@ package Mail::SpamAssassin::Plugin::SpamCop;
 
 use Mail::SpamAssassin::Plugin;
 use Mail::SpamAssassin::Logger;
+use Mail::SpamAssassin::Util qw(untaint_var);
 use IO::Socket;
 use strict;
 use warnings;
 # use bytes;
 use re 'taint';
 
-use constant HAS_NET_DNS => eval { require Net::DNS; };
 use constant HAS_NET_SMTP => eval { require Net::SMTP; };
 
 our @ISA = qw(Mail::SpamAssassin::Plugin);
@@ -63,7 +63,7 @@ sub new {
   bless ($self, $class);
 
   # are network tests enabled?
-  if (!$mailsaobject->{local_tests_only} && HAS_NET_DNS && HAS_NET_SMTP) {
+  if (!$mailsaobject->{local_tests_only} && HAS_NET_SMTP) {
     $self->{spamcop_available} = 1;
     dbg("reporter: network tests on, attempting SpamCop");
   }
@@ -114,7 +114,7 @@ guess will be used as the From: address in SpamCop reports.
 =item spamcop_to_address user@example.com   (default: generic reporting address)
 
 Your customized SpamCop report submission address.  You need to obtain
-this address by registering at C<http://www.spamcop.net/>.  If this is
+this address by registering at C<https://www.spamcop.net/>.  If this is
 not set, SpamCop reports will go to a generic reporting address for
 SpamAssassin users and your reports will probably have less weight in
 the SpamCop system.
@@ -153,6 +153,35 @@ size that SpamCop will accept at the time of release.
     type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
   });
 
+=item spamcop_relayhost server:port  (default: direct connection to SpamCop)
+
+Direct connection to SpamCop servers (port 587) is used for report
+submission by default.  If this is undesirable or blocked by local firewall
+policies, you can specify a local SMTP relayhost to forward reports. 
+Relayhost should be configured to not scan the report, for example by using
+a separate submission port.  SSL or authentication is not supported.
+
+=cut
+
+  push (@cmds, {
+    setting => 'spamcop_relayhost',
+    default => undef,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      if ($value =~ /^(\S+):(\d{2,5})$/) {
+       $self->{spamcop_relayhost} = untaint_var($1);
+       $self->{spamcop_relayport} = untaint_var($2);
+      }
+      elsif ($value =~ /^$/) {
+       return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
+      }
+      else {
+       return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+      }
+    }
+  });
+
   $conf->{parser}->register_commands(\@cmds);
 }
 
@@ -257,17 +286,31 @@ EOM
 
   # send message
   my $failure;
-  my $mx = $head{To};
   my $hello = Mail::SpamAssassin::Util::fq_hostname() || $from;
-  $mx =~ s/.*\@//;
   $hello =~ s/.*\@//;
-  for my $rr (Net::DNS::mx($mx)) {
-    my $exchange = Mail::SpamAssassin::Util::untaint_hostname($rr->exchange);
-    next unless $exchange;
-    my $smtp;
-    if ($smtp = Net::SMTP->new($exchange,
+
+  my @mxs;
+  if ($options->{report}->{conf}->{spamcop_relayhost}) {
+    push @mxs, $options->{report}->{conf}->{spamcop_relayhost};
+  } else {
+    my $mx = $head{To};
+    $mx =~ s/.*\@//;
+    foreach my $rr (Net::DNS::mx($mx)) {
+      if (defined $rr->exchange) {
+        push @mxs, Mail::SpamAssassin::Util::untaint_hostname($rr->exchange);
+      }
+    }
+    if (!@mxs) {
+      warn("reporter: failed to resolve SpamCop MX servers\n");
+      return 0;
+    }
+  }
+  my $port = $options->{report}->{conf}->{spamcop_relayport} || 587;
+
+  for my $exchange (@mxs) {
+    if (my $smtp = Net::SMTP->new($exchange,
                               Hello => $hello,
-                              Port => 587,
+                              Port => $port,
                               Timeout => 10))
     {
       if ($smtp->mail($from) && smtp_dbg("FROM $from", $smtp) &&
index 2427a09aaef3820a921ce9ce674ab87f4b69305c..797b19b8de439a356afa29d17f43ee2fd3de5e29 100644 (file)
@@ -87,8 +87,8 @@ sub new {
     }
   }
 
-  $self->register_eval_rule("check_language");
-  $self->register_eval_rule("check_body_8bits");
+  $self->register_eval_rule("check_language"); # type does not matter
+  $self->register_eval_rule("check_body_8bits", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
 
   $self->set_config($mailsaobject->{conf});
 
@@ -114,7 +114,7 @@ confidence. In that case, no action is taken.
 
 The rule C<UNWANTED_LANGUAGE_BODY> is triggered if none of the languages
 detected are in the "ok" list. Note that this is the only effect of the
-"ok" list. It does not act as a whitelist against any other form of spam
+"ok" list. It does not act as a welcomelist against any other form of spam
 scanning.
 
 In your configuration, you must use the two or three letter language
@@ -406,7 +406,7 @@ sub load_models {
   # create language ngram maps once
   for (@lm) {
     # look for end delimiter
-    if (/^0 (.+)/) {
+    if (index($_, '0 ') == 0 && /^0 (.+)/) {
       $ngram->{"language"} = $1;
       push(@nm, $ngram);
       # reset for next language
@@ -449,7 +449,14 @@ sub classify {
       $p += exists($ngram->{$_}) ? abs($ngram->{$_} - $i) : $maxp;
       $i++;
     }
-    $results{$language} = $p;
+    # Most latin1 languages have xx and xx.utf8 alternatives (those which
+    # don't have should be named xx.utf-8).  Always strip .utf8 from name,
+    # it will not be accurate as matching will depend on normalize_charset
+    # and mail encoding.  Keep track of the best score for alternatives.
+    $language = $short  if index($language, '.utf8') > 0;
+    if (!exists $results{$language} || $results{$language} > $p) {
+      $results{$language} = $p
+    }
   }
   my @results = sort { $results{$a} <=> $results{$b} } keys %results;
 
@@ -459,7 +466,11 @@ sub classify {
   my @results_tag;
   foreach (@results[0..19]) {
     last unless defined $_;
-    push @results_tag, sprintf "%s:%s(%.02f)", $_, $results{$_}, $results{$_} / $best;
+    if($best != 0) {
+      push @results_tag, sprintf "%s:%s(%.02f)", $_, $results{$_}, $results{$_} / $best;
+    } else {
+      push @results_tag, sprintf "%s:%s(unknown)", $_, $results{$_};
+    }
   }
   $opts->{permsgstatus}->set_tag('TEXTCATRESULTS', join(' ', @results_tag));
 
@@ -539,11 +550,14 @@ sub extract_metadata {
 
   my $body = $msg->get_rendered_body_text_array();
   $body = join("\n", @{$body});
-  $body =~ s/^Subject://i;
+
+  # Strip subject prefixes, enhances results
+  $body =~ s/^(?:[a-z]{2,12}:\s*){1,10}//i;
 
   # Strip anything that looks like url or email, enhances results
-  $body =~ s{https?://\S+}{ }gs;
-  $body =~ s{\S+?\@[a-zA-Z]\S+}{ }gs;
+  $body =~ s/https?(?:\:\/\/|:&#x2F;&#x2F;|%3A%2F%2F)\S{1,1024}/ /gs;
+  $body =~ s/\S{1,64}?\@[a-zA-Z]\S{1,128}/ /gs;
+  $body =~ s/\bwww\.\S{1,128}/ /gs;
 
   my $len = length($body);
   # truncate after 10k; that should be plenty to classify it
index f70a77e5f2352fcea23939cbe6a00e7036b7b7c1..c678d8fb8fb174cd6a86a1a78e6d068f9e0e5fc9 100644 (file)
@@ -23,7 +23,7 @@ Mail::SpamAssassin::Plugin::TxRep - Normalize scores with sender reputation reco
 =head1 SYNOPSIS
 
 The TxRep (Reputation) plugin is designed as an improved replacement of the AWL
-(Auto-Whitelist) plugin. It adjusts the final message spam score by looking up 
+(Auto-Welcomelist) plugin. It adjusts the final message spam score by looking up 
 and taking in consideration the reputation of the sender.
 
 To try TxRep out, you B<have to> first disable the AWL plugin (if enabled), and
@@ -48,7 +48,7 @@ Use the supplied 60_txreputation.cf file or add these lines to a .cf file:
 
 =head1 DESCRIPTION
 
-This plugin is intended to replace the former AWL - AutoWhiteList. Although the
+This plugin is intended to replace the former AWL - AutoWelcomeList. Although the
 concept and the scope differ, the purpose remains the same - the normalizing of spam
 score results based on previous sender's history. The name was intentionally changed
 from "whitelist" to "reputation" to avoid any confusion, since the result score can
@@ -70,7 +70,7 @@ respective sender, when calculating the corrective score at a new message, it do
 not take it in count in any way. So for example a sender who previously sent a single
 ham message with the score of -5, and then sends a second one with the score of +10,
 AWL will issue a corrective score bringing the score towards the -5. With the default
-C<auto_whitelist_factor> of 0.5, the resulting score would be only 2.5. And it would be
+C<auto_welcomelist_factor> of 0.5, the resulting score would be only 2.5. And it would be
 exactly the same even if the sender previously sent 1,000 messages with the average of
 -5. TxRep tries to take the maximal advantage of the collected data, and adjusts the
 final score not only with the mean reputation score stored in the database, but also
@@ -105,23 +105,23 @@ of past messages, and low recent frequency. It also turns to be particularly
 counterproductive when the administrator detects new patterns in certain messages, and
 applies new rules to better tag such messages as spam or ham. AWL will practically
 eliminate the effect of the new rules, by adjusting the score back towards the (wrong)
-historical average. Only setting the C<auto_whitelist_factor> lower would help, but in
+historical average. Only setting the C<auto_welcomelist_factor> lower would help, but in
 the same time it would also reduce the overall impact of AWL, and put doubts on its
-purpose. TxRep, besides the L</C<txrep_factor>> (replacement of the C<auto_whitelist_factor>),
+purpose. TxRep, besides the L</C<txrep_factor>> (replacement of the C<auto_welcomelist_factor>),
 introduces also the L</C<txrep_dilution_factor>> to help coping with this issue by
 progressively reducing the impact of past records. More details can be found in the
 description of the factor below.
 
-6. B<Blacklisting and Whitelisting> - when a whitelisting or blacklisting was requested
+6. B<Blocklisting and Welcomelisting> - when a welcomelisting or blocklisting was requested
 through SpamAssassin's API, AWL adjusts the historical total score of the plain email
 address without IP (and deleted records bound to an IP), but since during the reception 
-new records with IP will be added, the blacklisted entry would cease acting during 
+new records with IP will be added, the blocklisted entry would cease acting during 
 scanning. TxRep always uses the record of the plain email address without IP together 
 with the one bound to an IP address, DKIM signature, or SPF pass (unless the weight 
 factor for the EMAIL reputation is set to zero). AWL uses the score of 100 (resp. -100) 
-for the blacklisting (resp. whitelisting) purposes. TxRep increases the value 
+for the blocklisting (resp. welcomelisting) purposes. TxRep increases the value 
 proportionally to the weight factor of the EMAIL reputation. It is explained in details 
-in the section L<BLACKLISTING / WHITELISTING>. TxRep can blacklist or whitelist also
+in the section L<BLOCKLISTING / WELCOMELISTING>. TxRep can blocklist or welcomelist also
 IP addresses, domain names, and dotless HELO names.
 
 7. B<Sender Identification> - AWL identifies a sender on the basis of the email address
@@ -157,24 +157,24 @@ use dual storages: the global common storage, where all email processed by SpamA
 is recorded, and a local storage separate for each user, with reputation data from his
 email only. See more details at the setting L</C<txrep_user2global_ratio>>.
 
-10. B<Outbound Whitelisting> - when a local user sends messages to an email address, we
+10. B<Outbound Welcomelisting> - when a local user sends messages to an email address, we
 assume that he needs to see the eventual answer too, hence the recipient's address should
-be whitelisted. When SpamAssassin is used for scanning outgoing email too, when local
+be welcomelisted. When SpamAssassin is used for scanning outgoing email too, when local
 users use the SMTP server where SA is installed, for sending email, and when internal
 networks are defined, TxREP will improve the reputation of all 'To:' and 'CC' addresses
 from messages originating in the internal networks. Details can be found at the setting
-L</C<txrep_whitelist_out>>.
+L</C<txrep_welcomelist_out>>.
 
 Both plugins (AWL and TxREP) cannot coexist. It is necessary to disable the AWL to allow
 TxRep running. TxRep reuses the database handling of the original AWL module, and some
 its parameters bound to the database handler modules. By default, TxRep creates its own
-database, but the original auto-whitelist can be reused as a starting point. The AWL
+database, but the original auto-welcomelist can be reused as a starting point. The AWL
 database can be renamed to the name defined in TxRep settings, and TxRep will start
-using it. The original auto-whitelist database has to be backed up, to allow switching
+using it. The original auto-welcomelist database has to be backed up, to allow switching
 back to the original state.
 
 The spamassassin/Plugin/TxRep.pm file replaces both spamassassin/Plugin/AWL.pm and
-spamassassin/AutoWhitelist.pm. Another two AWL files, spamassassin/DBBasedAddrList.pm
+spamassassin/AutoWelcomelist.pm. Another two AWL files, spamassassin/DBBasedAddrList.pm
 and spamassassin/SQLBasedAddrList.pm are still needed.
 
 
@@ -225,7 +225,7 @@ sub new {                       # constructor: register the eval rule
   $self->{main}          = $main;
   $self->{conf}          = $main->{conf};
   $self->{factor}        = $main->{conf}->{txrep_factor};
-  $self->register_eval_rule("check_senders_reputation");
+  $self->register_eval_rule("check_senders_reputation", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
   $self->set_config($main->{conf});
 
   # only the default conf loaded here, do nothing here requiring
@@ -464,16 +464,18 @@ learned, or need to be relearned after modifying the learn penalty or bonus.
   });
 
 
-=item B<txrep_whitelist_out>
+=item B<txrep_welcomelist_out>
 
  range [0..200]         (default: 10)
 
+Previously txrep_whitelist_out which will work interchangeably until 4.1.
+
 When the value of this setting is greater than zero, recipients of messages sent from
-within the internal networks will be whitelisted through improving their total reputation
+within the internal networks will be welcomelisted through improving their total reputation
 score with the number of points defined by this setting. Since the IP address and other
 sender identificators are not known when sending the email, only the reputation of the
-standalone email is being whitelisted. The domain name is intentionally also left
-unaffected. The outbound whitelisting can only work when SpamAssassin is set up to scan
+standalone email is being welcomelisted. The domain name is intentionally also left
+unaffected. The outbound welcomelisting can only work when SpamAssassin is set up to scan
 also outgoing email, when local users use the SMTP server for sending email, and when
 C<internal_networks> are defined in SpamAssassin configuration. The improving of the
 reputation happens at every message sent from internal networks, so the more messages is
@@ -483,13 +485,14 @@ being sent to the recipient, the better reputation his email address will have.
 =cut
 
   push (@cmds, {
-    setting     => 'txrep_whitelist_out',
+    setting     => 'txrep_welcomelist_out',
+    aliases    => ['txrep_whitelist_out'], # removed in 4.1
     default     => 10,
     type        => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
     code        => sub {
         my ($self, $key, $value, $line) = @_;
         if ($value < 0 || $value > 200) {return $Mail::SpamAssassin::Conf::INVALID_VALUE;}
-        $self->{txrep_whitelist_out} = $value;
+        $self->{txrep_welcomelist_out} = $value;
     }
   });
 
@@ -588,7 +591,7 @@ global (server-wide) storages.
 
 User storage keeps only senders who send messages to the respective recipient,
 and will reflect also the corrected/learned scores, when some messages are marked
-by the user as spam or ham, or when the sender is whitelisted or blacklisted
+by the user as spam or ham, or when the sender is welcomelisted or blocklisted
 through the API of SpamAssassin.
 
 Global storage keeps the reputation data of all messages processed by SpamAssassin
@@ -610,7 +613,7 @@ available, the global storage is used fully, without applying the ratio.
 When the ratio is set to zero, only the default storage will be used. And it
 then depends whether you use the global, or the local user storage by default,
 which in turn is controlled either by the parameter user_awl_sql_override_username
-(in case of SQL storage), or the C</auto_whitelist_path> parameter (in case of
+(in case of SQL storage), or the C</auto_welcomelist_path> parameter (in case of
 Berkeley database).
 
 When this dual storage is enabled, and no global storage is defined by the
@@ -620,7 +623,7 @@ Berkeley database it uses the path defined by '__local_state_dir__/tx-reputation
 which typically renders into /var/db/spamassassin/tx-reputation. When the default
 storages are not available, or are not writable, you would have to set the global
 storage with the help of the C<user_awl_sql_override_username> resp.
-C<auto_whitelist_path settings>.
+C<auto_welcomelist_path settings>.
 
 Please note that some SpamAssassin installations run always under the same user
 ID. In such case it is pointless enabling the dual storage, because it would
@@ -642,9 +645,9 @@ This feature is disabled by default.
   });
 
 
-=item B<auto_whitelist_distinguish_signed>
+=item B<auto_welcomelist_distinguish_signed>  (default: 1 - enabled)
 
- (default: 1 - enabled)
+Previously auto_welcomelist_distinguish_signed which will work interchangeably until 4.1.
 
 Used by the SQLBasedAddrList storage implementation.
 
@@ -664,7 +667,8 @@ that is not possible you must set this option to 0 to avoid SQL errors.
 =cut
 
   push (@cmds, {
-    setting     => 'auto_whitelist_distinguish_signed',
+    setting     => 'auto_welcomelist_distinguish_signed',
+    aliases     => ['auto_whitelist_distinguish_signed'], # removed in 4.1
     default     => 1,
     type        => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL
   });
@@ -679,7 +683,7 @@ the same authorized identity, and will not associate any IP address with it.
 (The same happens with valid DKIM signatures. No option available for DKIM).
 
 Note: at domains that define the useless SPF +all (pass all), no IP would be
-ever associated with the email address, and all addresses (incl. the froged
+ever associated with the email address, and all addresses (incl. the forged
 ones) would be treated as coming from the authorized source. However, such
 domains are hopefully rare, and ask for this kind of treatment anyway.
 
@@ -775,7 +779,7 @@ At a user sending from multiple locations, diverse mail servers, or from a dynam
 IP range out of the masked block, his email address will have a separate reputation
 value for each of the different (partial) IP addresses.
 
-When the option auto_whitelist_distinguish_signed is enabled, in contrary to
+When the option auto_welcomelist_distinguish_signed is enabled, in contrary to
 the original AWL module, TxRep does not record the IP address when DKIM
 signature is detected. The email address is then not bound to any IP address, but
 rather just to the DKIM signature, since it is considered that it authenticates
@@ -921,10 +925,12 @@ Select alternative database factory module for the TxRep database.
   });
 
 
-=item B<auto_whitelist_path /path/filename>
+=item B<auto_welcomelist_path /path/filename>
 
  (default: ~/.spamassassin/tx-reputation)
 
+Previously auto_whitelist_path which will work interchangeably until 4.1.
+
 This is the TxRep directory and filename.  By default, each user
 has their own reputation database in their C<~/.spamassassin> directory with
 mode 0700.  For system-wide SpamAssassin use, you may want to share this
@@ -933,22 +939,25 @@ across all users.
 =cut
 
   push (@cmds, {
-    setting      => 'auto_whitelist_path',
+    setting      => 'auto_welcomelist_path',
+    aliases      => ['auto_whitelist_path'], # removed in 4.1
     is_admin     => 1,
     default      => '__userstate__/tx-reputation',
     type         => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
     code         => sub {
         my ($self, $key, $value, $line) = @_;
         unless (defined $value && $value !~ /^$/) {return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;}
-        $self->{auto_whitelist_path} = $value;
+        $self->{auto_welcomelist_path} = $value;
     }
   });
 
 
-=item B<auto_whitelist_db_modules Module ...>
+=item B<auto_welcomelist_db_modules Module ...>
 
  (default: see below)
 
+Previously auto_whitelist_db_modules which will work interchangeably until 4.1.
+
 What database modules should be used for the TxRep storage database
 file.   The first named module that can be loaded from the Perl include path
 will be used.  The format is:
@@ -964,17 +973,20 @@ NDBM_File is not supported (see SpamAssassin bug 4353).
 =cut
 
   push (@cmds, {
-    setting      => 'auto_whitelist_db_modules',
+    setting      => 'auto_welcomelist_db_modules',
+    aliases      => ['auto_whitelist_db_modules'], # removed in 4.1
     is_admin     => 1,
     default      => 'DB_File GDBM_File SDBM_File',
     type         => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING
   });
 
 
-=item B<auto_whitelist_file_mode>
+=item B<auto_welcomelist_file_mode>
 
  (default: 0700)
 
+Previously auto_whitelist_file_mode which will work interchangeably until 4.1.
+
 The file mode bits used for the TxRep directory or file.
 
 Make sure you specify this using the 'x' mode bits set, as it may also be used
@@ -984,7 +996,8 @@ not have any execute bits set (the umask is set to 0111).
 =cut
 
   push (@cmds, {
-    setting      => 'auto_whitelist_file_mode',
+    setting      => 'auto_welcomelist_file_mode',
+    aliases      => ['auto_whitelist_file_mode'], # removed in 4.1
     is_admin     => 1,
     default      => '0700',
     type         => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
@@ -993,7 +1006,8 @@ not have any execute bits set (the umask is set to 0111).
         if ($value !~ /^0?[0-7]{3}$/) {
             return $Mail::SpamAssassin::Conf::INVALID_VALUE;
         }
-        $self->{auto_whitelist_file_mode} = untaint_var($value);
+        $value = '0'.$value if length($value) == 3; # Bug 5771
+        $self->{auto_welcomelist_file_mode} = untaint_var($value);
     }
   });
 
@@ -1103,7 +1117,7 @@ sub _fn_envelope {
                $self->{conf}->{txrep_weight_helo};
   my $sign = $args->{signedby};
   my $id     = $args->{address};
-  if ($args->{address} =~ /,/) {
+  if (index($args->{address}, ',') >= 0) {
     $sign = $args->{address};
     $sign =~ s/^.*,//g;
     $id   =~ s/,.*$//g;
@@ -1114,9 +1128,9 @@ sub _fn_envelope {
        {$factor /= $self->{conf}->{txrep_weight_helo}; $sign = 'helo';}
   elsif ($id =~ /^[a-f\d\.:]+$/ && $self->{conf}->{txrep_weight_ip})
        {$factor /= $self->{conf}->{txrep_weight_ip};}
-  elsif ($id =~ /@/ && $self->{conf}->{txrep_weight_email})
+  elsif (index($id, '@') >= 0 && $self->{conf}->{txrep_weight_email})
        {$factor /= $self->{conf}->{txrep_weight_email};}
-  elsif ($id !~ /@/ && $self->{conf}->{txrep_weight_domain})
+  elsif (index($id, '@') == -1 && $self->{conf}->{txrep_weight_domain})
        {$factor /= $self->{conf}->{txrep_weight_domain};}
   else {$factor  = 1;}
 
@@ -1133,46 +1147,48 @@ sub _fn_envelope {
 }
 
 
-=head1 BLACKLISTING / WHITELISTING
+=head1 BLOCKLISTING / WELCOMELISTING
 
-When asked by SpamAssassin to blacklist or whitelist a user, the TxRep
-plugin adds a score of 100 (for blacklisting) or -100 (for whitelisting)
+When asked by SpamAssassin to blocklist or welcomelist a user, the TxRep
+plugin adds a score of 100 (for blocklisting) or -100 (for welcomelisting)
 to the given sender's email address. At a plain address without any IP
 address, the value is multiplied by the ratio of total reputation
 weight to the EMAIL reputation weight to account for the reduced impact
 of the standalone EMAIL reputation when calculating the overall reputation.
 
    total_weight = weight_email + weight_email_ip + weight_domain + weight_ip + weight_helo
-   blacklisted_reputation = 100 * total_weight / weight_email
+   blocklisted_reputation = 100 * total_weight / weight_email
 
-When a standalone email address is blacklisted/whitelisted, all records
+When a standalone email address is blocklisted/welcomelisted, all records
 of the email address bound to an IP address, DKIM signature, or a SPF pass
 will be removed from the database, and only the standalone record is kept.
 
-Besides blacklisting/whitelisting of standalone email addresses, the same
-method may be used also for blacklisting/whitelisting of IP addresses,
+Besides blocklisting/welcomelisting of standalone email addresses, the same
+method may be used also for blocklisting/welcomelisting of IP addresses,
 domain names, and HELO names (only dotless Netbios HELO names can be used).
 
-When whitelisting/blacklisting an email address or domain name, you can
+When welcomelisting/blocklisting an email address or domain name, you can
 bind them to a specified DKIM signature or SPF record by appending the 
 DKIM signing domain or the tag 'spf' after the ID in the following way:
 
- spamassassin --add-addr-to-blacklist=spamming.biz,spf
- spamassassin --add-addr-to-whitelist=friend@good.org,good.org
+ spamassassin --add-addr-to-blocklist=spamming.biz,spf
+ spamassassin --add-addr-to-welcomelist=friend@good.org,good.org
 
 When a message contains both a DKIM signature and an SPF pass, the DKIM
 signature takes the priority, so the record bound to the 'spf' tag won't 
 be checked. Only email addresses and domains can be bound to DKIM or SPF.
 Records of IP addresses and HELO names are always without DKIM/SPF.
 
-In case of dual storage, the black/whitelisting is performed only in the
+In case of dual storage, the block/welcomelisting is performed only in the
 default storage.
 
 =cut
 
 ######################################################## plugin hooks #####
-sub blacklist_address {my $self=shift; return $self->_fn_envelope(@_,  100, "blacklisting address");}
-sub whitelist_address {my $self=shift; return $self->_fn_envelope(@_, -100, "whitelisting address");}
+sub blocklist_address {my $self=shift; return $self->_fn_envelope(@_,  100, "blocklisting address");}
+*blacklist_address = \&blocklist_address; # removed in 4.1
+sub welcomelist_address {my $self=shift; return $self->_fn_envelope(@_, -100, "welcomelisting address");}
+*whitelist_address = \&welcomelist_address; # removed in 4.1
 sub remove_address    {my $self=shift; return $self->_fn_envelope(@_,undef, "removing address");}
 ###########################################################################
 
@@ -1227,8 +1243,8 @@ sub check_senders_reputation {
 
   # Cases where we would not be able to use TxRep
   return 0 unless ($self->{conf}->{use_txrep});
-  if ($self->{conf}->{use_auto_whitelist}) {
-    warn("TxRep: cannot run when Auto-Whitelist is enabled. Please disable it!\n");
+  if ($self->{conf}->{use_auto_welcomelist}) {
+    warn("TxRep: cannot run when Auto-Welcomelist is enabled. Please disable it!\n");
     return 0;
   }
   if ($autolearn && !$self->{conf}->{txrep_autolearn}) {
@@ -1245,9 +1261,7 @@ sub check_senders_reputation {
   my $timer    = $self->{main}->time_method("total_txrep");
   my $msgscore = (defined $self->{learning})? $self->{learning} : $pms->get_autolearn_points();
   my $date     = $pms->{msg}->receive_date() || $pms->{date_header_time};
-  my $msg_id   = $self->{msgid} ||
-                 Mail::SpamAssassin::Plugin::Bayes->get_msgid($pms->{msg}) ||
-                 $pms->get('Message-Id') || $pms->get('Message-ID') || $pms->get('MESSAGE-ID') || $pms->get('MESSAGEID');
+  my $msg_id   = $self->{msgid} || $pms->{msg}->generate_msgid();
 
   my $from   = lc $pms->get('From:addr') || $pms->get('EnvelopeFrom:addr');
   return 0 unless $from =~ /\S/;
@@ -1304,21 +1318,21 @@ sub check_senders_reputation {
     } else {dbg("TxRep: no message-id available, parsing forced");}
   }             # else no message tracking, go ahead with normal rep scan
 
-  # whitelists recipients at senders from internal networks after checking MSG_ID only
-  if ( $self->{conf}->{txrep_whitelist_out} &&
+  # welcomelists recipients at senders from internal networks after checking MSG_ID only
+  if ( $self->{conf}->{txrep_welcomelist_out} &&
           defined $pms->{relays_internal} &&  @{$pms->{relays_internal}} &&
         (!defined $pms->{relays_external} || !@{$pms->{relays_external}})
      ) {
     foreach my $rcpt ($pms->all_to_addrs()) {
         if ($rcpt) {
-            dbg("TxRep: internal sender, whitelisting recipient: $rcpt");
-            $self->modify_reputation($rcpt, -1*$self->{conf}->{txrep_whitelist_out}, undef);
+            dbg("TxRep: internal sender, welcomelisting recipient: $rcpt");
+            $self->modify_reputation($rcpt, -1*$self->{conf}->{txrep_welcomelist_out}, undef);
         }
     }
   }
 
   # Get the signing domain
-  my $signedby = ($self->{conf}->{auto_whitelist_distinguish_signed})? $pms->get_tag('DKIMDOMAIN') : undef;
+  my $signedby = ($self->{conf}->{auto_welcomelist_distinguish_signed})? $pms->get_tag('DKIMDOMAIN') : undef;
 
   # Summary of all information we've gathered so far
   dbg("TxRep: active, %s pre-score: %s, autolearn score: %s, IP: %s, address: %s %s",
@@ -1412,7 +1426,7 @@ sub check_reputation {
   my ($self, $storage, $pms, $key, $id, $ip, $signedby, $msgscore) = @_;
 
   my $delta  = 0;
-  my $weight = ($key eq 'MSG_ID')? 1 : eval('$pms->{main}->{conf}->{txrep_weight_'.lc($key).'}');
+  my $weight = ($key eq 'MSG_ID') ? 1 : $pms->{main}->{conf}->{'txrep_weight_'.lc($key)};
 
 #  {
 #    #Bug 7164, trying to find out reason for these: _WARN: Use of uninitialized value $msgscore in addition (+) at /usr/share/perl5/vendor_perl/Mail/SpamAssassin/Plugin/TxRep.pm line 1415.
@@ -1640,12 +1654,13 @@ sub open_storages {
     $factory = $self->{main}->{pers_addr_list_factory};
   } else {
     my $type = $self->{conf}->{txrep_factory};
-    if ($type =~ /^([_A-Za-z0-9:]+)$/) {
+    if ($type =~ /^[_A-Za-z0-9:]+$/) {
         $type = untaint_var($type);
-        eval 'require    '.$type.';
-            $factory = '.$type.'->new();
-            1;'
-        or do {
+        eval '
+          require '.$type.';
+          $factory = '.$type.'->new();
+          1;
+        ' or do {
             my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
             warn "TxRep: $eval_stat\n";
             undef $factory;
@@ -1661,33 +1676,33 @@ sub open_storages {
        # TODO: add an a method to the handler class instead
        my ($storage_type, $is_global);
        
-       if (ref($factory) =~ /SQLBasedAddrList/) {
+       if (index(ref($factory), 'SQLBasedAddrList') >= 0) {
            $is_global    = defined $self->{conf}->{user_awl_sql_override_username};
            $storage_type = 'SQL';
            if ($is_global && $self->{conf}->{user_awl_sql_override_username} eq $self->{main}->{username}) {
                # skip double storage if current user same as the global override
                $self->{user_storage} = $self->{global_storage} = $self->{default_storage};
            }
-       } elsif (ref($factory) =~ /DBBasedAddrList/) {
-           $is_global    = $self->{conf}->{auto_whitelist_path} !~ /__userstate__/;
+       } elsif (index(ref($factory), 'DBBasedAddrList') >= 0) {
+           $is_global    = index($self->{conf}->{auto_welcomelist_path}, '__userstate__') == -1;
            $storage_type = 'DB';
        }
        if (!defined $self->{global_storage}) {
            my $sql_override_orig = $self->{conf}->{user_awl_sql_override_username};
-           my $awl_path_orig     = $self->{conf}->{auto_whitelist_path};
+           my $awl_path_orig     = $self->{conf}->{auto_welcomelist_path};
            if ($is_global) {
                $self->{conf}->{user_awl_sql_override_username} = '';
-               $self->{conf}->{auto_whitelist_path}            = '__userstate__/tx-reputation';
+               $self->{conf}->{auto_welcomelist_path}            = '__userstate__/tx-reputation';
                $self->{global_storage} = $self->{default_storage};
                $self->{user_storage}   = $factory->new_checker($self->{main});
            } else {
                $self->{conf}->{user_awl_sql_override_username} = 'GLOBAL';
-               $self->{conf}->{auto_whitelist_path}            = '__local_state_dir__/tx-reputation';
+               $self->{conf}->{auto_welcomelist_path}            = '__local_state_dir__/tx-reputation';
                $self->{global_storage} = $factory->new_checker($self->{main});
                $self->{user_storage}   = $self->{default_storage};
            }
            $self->{conf}->{user_awl_sql_override_username} = $sql_override_orig;
-           $self->{conf}->{auto_whitelist_path}            = $awl_path_orig;
+           $self->{conf}->{auto_welcomelist_path}            = $awl_path_orig;
        
            # Another ugly hack to find out whether the user differs from
            # the global one. We need to add a method to the factory handlers
@@ -1753,7 +1768,7 @@ sub ip_to_awl_key {
         $result =~s/(\.0){1,3}\z//;                     # truncate zero tail
       }
     }
-  } elsif ($origip =~ /:/ &&                            # triage
+  } elsif (index($origip, ':') >= 0 &&                            # triage
            $origip =~
            /^ [0-9a-f]{0,4} (?: : [0-9a-f]{0,4} | \. [0-9]{1,3} ){2,9} $/xsi) {
     # looks like an IPv6 address
@@ -1928,10 +1943,10 @@ the processing time.
 4. Disabling the option L</C<txrep_autolearn>> will save the processing time
 at messages that trigger the auto-learning process.
 
-5. Disabling L</C<txrep_whitelist_out>> will reduce the processing time at
+5. Disabling L</C<txrep_welcomelist_out>> will reduce the processing time at
 outbound connections.
 
-6. Keeping the option L</C<auto_whitelist_distinguish_signed>> enabled may help
+6. Keeping the option L</C<auto_welcomelist_distinguish_signed>> enabled may help
 slightly reducing the size of the database, because at signed messages, the
 originating IP address is ignored, hence no additional database entries are
 needed for each separate IP address (resp. a masked block of IP addresses).
index e2c0640f997cb43137a50a130632c22147d21b33..57b7f5ab0d314b7e94ec559283dcc14bd42b78e5 100644 (file)
@@ -57,6 +57,9 @@ Specify a domain, or a number of domains, which should be skipped for the
 URIBL checks.  This is very useful to specify very common domains which are
 not going to be listed in URIBLs.
 
+In addition to trimmed domain, the full hostname is also checked from the
+list.
+
 =back
 
 =over 4
@@ -153,10 +156,11 @@ define a body-eval rule calling C<check_uridnsbl()> to use this.
 
 An RHSBL zone is one where the domain name is looked up, as a string; e.g. a
 URI using the domain C<foo.com> will cause a lookup of
-C<foo.com.uriblzone.net>.  Note that hostnames are stripped from the domain
-used in the URIBL lookup, so the domain C<foo.bar.com> will look up
+C<foo.com.uriblzone.net>.  Note that hostnames are trimmed to the domain
+portion in the URIBL lookup, so the domain C<foo.bar.com> will look up
 C<bar.com.uriblzone.net>, and C<foo.bar.co.uk> will look up
-C<bar.co.uk.uriblzone.net>.
+C<bar.co.uk.uriblzone.net>.  Using tflag C<notrim> will force full hostname
+lookup, but the specific uribl must support this method.
 
 If an URI consists of an IP address instead of a hostname, the IP address is
 looked up (using the standard reversed quads method) in each C<rhsbl_zone>.
@@ -186,8 +190,9 @@ for a single decimal or hex form the following must be true:
 Some typical examples of a sub-test are: 127.0.1.2, 127.0.1.20-127.0.1.39,
 127.2.3.0/255.255.255.0, 0.0.0.16/0.0.0.16, 0x10/0x10, 16, 0x10 .
 
-Note that, as with C<urirhsbl>, you must also define a body-eval rule calling
-C<check_uridnsbl()> to use this.
+Note that, as with C<urirhsbl>, you must also define a body-eval rule
+calling C<check_uridnsbl()> to use this.  Hostname to domain trimming is
+also done similarly.
 
 Example:
 
@@ -226,8 +231,7 @@ C<check_uridnsbl()> to use this.
 Perform a RHSBL-style domain lookup against the contents of the NS records for
 each URI.  In other words, a URI using the domain C<foo.com> will cause an NS
 lookup to take place; assuming that domain has an NS of C<ns0.bar.com>, that
-will cause a lookup of C<ns0.bar.com.uriblzone.net>.  Note that hostnames are
-stripped from the domain used in the URI.
+will cause a lookup of C<ns0.bar.com.uriblzone.net>.
 
 C<NAME_OF_RULE> is the name of the rule to be used, C<rhsbl_zone> is the zone
 to look up domain names in, and C<lookuptype> is the type of lookup (B<TXT> or
@@ -271,6 +275,12 @@ directives. Host names from URLs will be mapped to their IP addresses, which
 will be sent to blocklists. When both 'ns' and 'a' flags are specified,
 both queries will be performed.
 
+=item tflags NAME_OF_RULE notrim
+
+The full hostname component will be matched against the named
+"urirhsbl"/"urirhssub" rule, instead of using the trimmed domain.
+This works better, but the specific uribl must support this method.
+
 =back
 
 =head1 ADMINISTRATOR SETTINGS
@@ -299,7 +309,7 @@ package Mail::SpamAssassin::Plugin::URIDNSBL;
 
 use Mail::SpamAssassin::Plugin;
 use Mail::SpamAssassin::Constants qw(:ip);
-use Mail::SpamAssassin::Util;
+use Mail::SpamAssassin::Util qw(idn_to_ascii reverse_ip_address);
 use Mail::SpamAssassin::Logger;
 use strict;
 use warnings;
@@ -324,54 +334,56 @@ sub new {
 
   $self->{finished} = { };
 
-  $self->register_eval_rule ("check_uridnsbl");
+  $self->register_eval_rule ("check_uridnsbl"); # type does not matter
   $self->set_config($samain->{conf});
 
   return $self;
 }
 
-# this is just a placeholder; in fact the results are dealt with later
+# this is just a placeholder; in fact the results are dealt with later  
 sub check_uridnsbl {
-  return 0;
+  my ($self, $pms) = @_;
+  return; # return undef for async status
 }
 
 # ---------------------------------------------------------------------------
 
-# once the metadata is parsed, we can access the URI list.  So start off
-# the lookups here!
-sub parsed_metadata {
+# once the metadata is parsed, we can access the URI list.
+# Use check_dnsbl hook to launch lookups at correct time (priority -100)
+
+sub check_dnsbl {
   my ($self, $opts) = @_;
+
   my $pms = $opts->{permsgstatus};
   my $conf = $pms->{conf};
 
-  return 0  if $conf->{skip_uribl_checks};
-  return 0  if !$pms->is_dns_available();
+  return if $conf->{skip_uribl_checks};
+  return if !$pms->is_dns_available();
 
-  $pms->{'uridnsbl_activerules'} = { };
-  $pms->{'uridnsbl_hits'} = { };
-  $pms->{'uridnsbl_seen_lookups'} = { };
+  $pms->{uridnsbl_activerules} = [ ];
+  $pms->{uridnsbl_hits} = { };
+  $pms->{uridnsbl_seen_lookups} = { };
 
   # only hit DNSBLs for active rules (defined and score != 0)
-  $pms->{'uridnsbl_active_rules_rhsbl'} = { };
-  $pms->{'uridnsbl_active_rules_rhsbl_ipsonly'} = { };
-  $pms->{'uridnsbl_active_rules_rhsbl_domsonly'} = { };
-  $pms->{'uridnsbl_active_rules_nsrhsbl'} = { };
-  $pms->{'uridnsbl_active_rules_fullnsrhsbl'} = { };
-  $pms->{'uridnsbl_active_rules_nsrevipbl'} = { };
-  $pms->{'uridnsbl_active_rules_arevipbl'} = { };
+  $pms->{uridnsbl_active_rules_rhsbl} = { };
+  $pms->{uridnsbl_active_rules_rhsbl_ipsonly} = { };
+  $pms->{uridnsbl_active_rules_rhsbl_domsonly} = { };
+  $pms->{uridnsbl_active_rules_nsrhsbl} = { };
+  $pms->{uridnsbl_active_rules_fullnsrhsbl} = { };
+  $pms->{uridnsbl_active_rules_nsrevipbl} = { };
+  $pms->{uridnsbl_active_rules_arevipbl} = { };
 
   foreach my $rulename (keys %{$conf->{uridnsbls}}) {
-    next unless ($conf->is_rule_active('body_evals',$rulename));
+    next if !$conf->{scores}->{$rulename};
+    push @{$pms->{uridnsbl_activerules}}, $rulename;
 
     my $rulecf = $conf->{uridnsbls}->{$rulename};
-    my $tflags = $conf->{tflags}->{$rulename};
-    $tflags = ''  if !defined $tflags;
-    my %tfl = map { ($_,1) } split(' ',$tflags);
+    my %tfl = map { ($_,1) } split(/\s+/, $conf->{tflags}->{$rulename}||'');
 
     my $is_rhsbl = $rulecf->{is_rhsbl};
-    if (     $is_rhsbl && $tfl{'ips_only'}) {
+    if (     $is_rhsbl && $tfl{ips_only}) {
       $pms->{uridnsbl_active_rules_rhsbl_ipsonly}->{$rulename} = 1;
-    } elsif ($is_rhsbl && $tfl{'domains_only'}) {
+    } elsif ($is_rhsbl && $tfl{domains_only}) {
       $pms->{uridnsbl_active_rules_rhsbl_domsonly}->{$rulename} = 1;
     } elsif ($is_rhsbl) {
       $pms->{uridnsbl_active_rules_rhsbl}->{$rulename} = 1;
@@ -380,10 +392,10 @@ sub parsed_metadata {
     } elsif ($rulecf->{is_nsrhsbl}) {
       $pms->{uridnsbl_active_rules_nsrhsbl}->{$rulename} = 1;
     } else {  # just a plain dnsbl rule (IP based), not a RHS rule (name-based)
-      if ($tfl{'a'}) {  # tflag 'a' explicitly
+      if ($tfl{a}) {  # tflag 'a' explicitly
         $pms->{uridnsbl_active_rules_arevipbl}->{$rulename} = 1;
       }
-      if ($tfl{'ns'} || !$tfl{'a'}) {  # tflag 'ns' explicitly, or default
+      if ($tfl{ns} || !$tfl{a}) {  # tflag 'ns' explicitly, or default
         $pms->{uridnsbl_active_rules_nsrevipbl}->{$rulename} = 1;
       }
     }
@@ -392,8 +404,7 @@ sub parsed_metadata {
   # get all domains in message
 
   # don't keep dereferencing this
-  my $skip_domains = $conf->{uridnsbl_skip_domains};
-  $skip_domains = {}  if !$skip_domains;
+  my $skip_domains = $conf->{uridnsbl_skip_domains} || {};
 
   # list of hashes to use in order
   my @uri_ordered;
@@ -442,7 +453,11 @@ sub parsed_metadata {
     while (my($host,$domain) = each( %{$info->{hosts}} )) {
       if ($skip_domains->{$domain}) {
         dbg("uridnsbl: domain $domain in skip list, host $host");
-      } else {
+      }
+      elsif ($skip_domains->{$host}) {
+        dbg("uridnsbl: host $host in skip list, domain $domain");
+      }
+      else {
         # use hostname as a key, and drag along the stripped domain name part
         $uri_ordered[$entry]->{$host} = $domain;
       }
@@ -488,8 +503,6 @@ sub parsed_metadata {
 
   # and query
   $self->query_hosts_or_domains($pms, \%hostlist);
-
-  return 1;
 }
 
 # Accepts argument in one of the following forms: m, n1-n2, or n/m,
@@ -522,7 +535,7 @@ sub parse_and_canonicalize_subtest {
         # ok, already a decimal number
       } elsif (/^0x[0-9a-zA-Z]{1,8}\z/) {
         $_ = hex($_);  # hex -> number
-      } elsif (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/) {
+      } elsif ($_ =~ IS_IPV4_ADDRESS) {
         $_ = Mail::SpamAssassin::Util::my_inet_aton($_);  # quad-dot -> number
         $any_quad_dot = 1;
       } else {
@@ -558,7 +571,7 @@ sub set_config {
     code => sub {
       my ($self, $key, $value, $line) = @_;
       local($1,$2,$3);
-      if ($value =~ /^(\S+)\s+(\S+)\s+(\S+)$/) {
+      if ($value =~ /^(\w+)\s+(\S+)\s+(\S+)$/) {
         my $rulename = $1;
         my $zone = $2;
         my $type = $3;
@@ -583,7 +596,7 @@ sub set_config {
     code => sub {
       my ($self, $key, $value, $line) = @_;
       local($1,$2,$3,$4);
-      if ($value =~ /^(\S+)\s+(\S+)\s+(\S+)\s+(.*?)\s*$/) {
+      if ($value =~ /^(\w+)\s+(\S+)\s+(\S+)\s+(.*?)\s*$/) {
         my $rulename = $1;
         my $zone = $2;
         my $type = $3;
@@ -611,7 +624,7 @@ sub set_config {
     code => sub {
       my ($self, $key, $value, $line) = @_;
       local($1,$2,$3);
-      if ($value =~ /^(\S+)\s+(\S+)\s+(\S+)$/) {
+      if ($value =~ /^(\w+)\s+(\S+)\s+(\S+)$/) {
         my $rulename = $1;
         my $zone = $2;
         my $type = $3;
@@ -636,7 +649,7 @@ sub set_config {
     code => sub {
       my ($self, $key, $value, $line) = @_;
       local($1,$2,$3,$4);
-      if ($value =~ /^(\S+)\s+(\S+)\s+(\S+)\s+(.*?)\s*$/) {
+      if ($value =~ /^(\w+)\s+(\S+)\s+(\S+)\s+(.*?)\s*$/) {
         my $rulename = $1;
         my $zone = $2;
         my $type = $3;
@@ -664,7 +677,7 @@ sub set_config {
     code => sub {
       my ($self, $key, $value, $line) = @_;
       local($1,$2,$3);
-      if ($value =~ /^(\S+)\s+(\S+)\s+(\S+)$/) {
+      if ($value =~ /^(\w+)\s+(\S+)\s+(\S+)$/) {
         my $rulename = $1;
         my $zone = $2;
         my $type = $3;
@@ -689,7 +702,7 @@ sub set_config {
     code => sub {
       my ($self, $key, $value, $line) = @_;
       local($1,$2,$3,$4);
-      if ($value =~ /^(\S+)\s+(\S+)\s+(\S+)\s+(.*?)\s*$/) {
+      if ($value =~ /^(\w+)\s+(\S+)\s+(\S+)\s+(.*?)\s*$/) {
         my $rulename = $1;
         my $zone = $2;
         my $type = $3;
@@ -717,7 +730,7 @@ sub set_config {
     code => sub {
       my ($self, $key, $value, $line) = @_;
       local($1,$2,$3);
-      if ($value =~ /^(\S+)\s+(\S+)\s+(\S+)$/) {
+      if ($value =~ /^(\w+)\s+(\S+)\s+(\S+)$/) {
         my $rulename = $1;
         my $zone = $2;
         my $type = $3;
@@ -742,7 +755,7 @@ sub set_config {
     code => sub {
       my ($self, $key, $value, $line) = @_;
       local($1,$2,$3,$4);
-      if ($value =~ /^(\S+)\s+(\S+)\s+(\S+)\s+(.*?)\s*$/) {
+      if ($value =~ /^(\w+)\s+(\S+)\s+(\S+)\s+(.*?)\s*$/) {
         my $rulename = $1;
         my $zone = $2;
         my $type = $3;
@@ -814,7 +827,7 @@ sub set_config {
 sub query_hosts_or_domains {
   my ($self, $pms, $hosthash_ref) = @_;
   my $conf = $pms->{conf};
-  my $seen_lookups = $pms->{'uridnsbl_seen_lookups'};
+  my $seen_lookups = $pms->{uridnsbl_seen_lookups};
 
   my $rhsblrules = $pms->{uridnsbl_active_rules_rhsbl};
   my $rhsbliprules = $pms->{uridnsbl_active_rules_rhsbl_ipsonly};
@@ -824,95 +837,91 @@ sub query_hosts_or_domains {
   my $nsreviprules = $pms->{uridnsbl_active_rules_nsrevipbl};
   my $areviprules = $pms->{uridnsbl_active_rules_arevipbl};
 
+  my @nsrules = (
+    keys %$nsrhsblrules,
+    keys %$fullnsrhsblrules,
+    keys %$nsreviprules,
+  );
+
+  my %launched_rules;
+
   while (my($host,$domain) = each(%$hosthash_ref)) {
     $domain = lc $domain;  # just in case
     $host = lc $host;
     dbg("uridnsbl: considering host=$host, domain=$domain");
-    my $obj = { dom => $domain };
-
-    my ($is_ip, $single_dnsbl);
-    if ($host =~ /^\d+\.\d+\.\d+\.\d+$/) {
-      my $IPV4_ADDRESS = IPV4_ADDRESS;
-      my $IP_PRIVATE = IP_PRIVATE;
-      # only look up the IP if it is public and valid
-      if ($host =~ /^$IPV4_ADDRESS$/o && $host !~ /^$IP_PRIVATE$/o) {
-        my $obj = { dom => $host };
-        $self->lookup_dnsbl_for_ip($pms, $obj, $host);
-        # and check the IP in RHSBLs too
-        local($1,$2,$3,$4);
-        if ($host =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) {
-          $domain = "$4.$3.$2.$1";
-          $single_dnsbl = 1;
-          $is_ip = 1;
-        }
-      }
-    }
-    else {
-      $single_dnsbl = 1;
-    }
 
-    if ($single_dnsbl) {
-      # rule names which look up a domain in the basic RHSBL subset
-      my @rhsblrules = keys %{$rhsblrules};
+    # rule names which look up a domain in the basic RHSBL subset
+    my @rhsblrules = keys %$rhsblrules;
 
-      # and add the "domains_only" and "ips_only" subsets as appropriate
-      if ($is_ip) {
-        push @rhsblrules, keys %{$rhsbliprules};
+    # IPv4 look-a-like / IPv6 address literal?
+    if ($host =~ /^\d+\.\d+\.\d+\.\d+$/ || $host =~ /^\[/) {
+      # only look up the IPv4 if it is public and valid
+      if ($host =~ IS_IPV4_ADDRESS && $host !~ IS_IP_PRIVATE) {
+        # Use IP in RHSBL lookups
+        $domain = $host;
       } else {
-        push @rhsblrules, keys %{$rhsbldomrules};
-      }
-
-      foreach my $rulename (@rhsblrules) {
-        my $rulecf = $conf->{uridnsbls}->{$rulename};
-        $self->lookup_single_dnsbl($pms, $obj, $rulename,
-                                   $domain, $rulecf->{zone}, $rulecf->{type});
-
-        # note that these rules are now underway.   important: unless the
-        # rule hits, in the current design, these will not be considered
-        # "finished" until harvest_dnsbl_queries() completes
-        $pms->register_async_rule_start($rulename);
+        # Skip bogus/private/IPv6 completely
+        next;
       }
-
+      # Add ips_only rules to RHSBL checks
+      push @rhsblrules, keys %$rhsbliprules;
+    } else {
       # perform NS+A or A queries to look up the domain in the non-RHSBL subset,
       # but only if there are active reverse-IP-URIBL rules
-      if ($host !~ /^\d+\.\d+\.\d+\.\d+$/) {
-        if ( !$seen_lookups->{'NS:'.$domain} &&
-             (%$nsreviprules || %$nsrhsblrules || %$fullnsrhsblrules) ) {
-          $seen_lookups->{'NS:'.$domain} = 1;
-          $self->lookup_domain_ns($pms, $obj, $domain);
+      if (!$seen_lookups->{"NS:$domain"} && @nsrules > 0) {
+        $seen_lookups->{"NS:$domain"} = 1;
+        if ($self->lookup_domain_ns($pms, $domain, \@nsrules)) {
+          $launched_rules{$_} = 1  foreach (@nsrules);
         }
-        if (%$areviprules && !$seen_lookups->{'A:'.$host}) {
-          $seen_lookups->{'A:'.$host} = 1;
-          my $obj = { dom => $host, is_arevip => 1 };
-          $self->lookup_a_record($pms, $obj, $host);
-          $pms->register_async_rule_start($_)  for keys %$areviprules;
+      }
+      if (!$seen_lookups->{"A:$host"} && %$areviprules) {
+        $seen_lookups->{"A:$host"} = 1;
+        if ($self->lookup_a_record($pms, $host, [keys %$areviprules])) {
+          $launched_rules{$_} = 1  foreach (keys %$areviprules);
         }
       }
+      # Add domains_only rules to RHSBL checks
+      push @rhsblrules, keys %$rhsbldomrules;
     }
+
+    # Launch RHSBL checks
+    foreach my $rulename (@rhsblrules) {
+      my $rulecf = $conf->{uridnsbls}->{$rulename};
+      # Check notrim tflag to query full hostname (Bug 7835)
+      my $query = ($conf->{tflags}->{$rulename}||'') =~ /\bnotrim\b/ ? $host : $domain;
+      if ($self->lookup_single_dnsbl($pms, $query, $rulename,
+            $rulecf->{zone}, $rulecf->{type})) {
+        $launched_rules{$rulename} = 1;
+      }
+    }
+  }
+
+  # mark any rule that was not used ready for metas
+  foreach my $rulename (@{$pms->{uridnsbl_activerules}}) {
+    $pms->rule_ready($rulename)  unless $launched_rules{$rulename};
   }
 }
 
 # ---------------------------------------------------------------------------
 
 sub lookup_domain_ns {
-  my ($self, $pms, $obj, $dom) = @_;
+  my ($self, $pms, $lookup, $rules) = @_;
+
+  $lookup = idn_to_ascii($lookup);
 
-  my $key = "NS:" . $dom;
   my $ent = {
-    key => $key, zone => $dom, obj => $obj, type => "URI-NS",
+    rulename => [@$rules],
+    type => "URIBL",
+    lookup => $lookup,
+    domain => $lookup,
   };
-  # dig $dom ns
-  $ent = $pms->{async}->bgsend_and_start_lookup(
-    $dom, 'NS', undef, $ent,
-    sub { my ($ent2,$pkt) = @_;
-          $self->complete_ns_lookup($pms, $ent2, $pkt, $dom) },
-    master_deadline => $pms->{master_deadline} );
-
-  return $ent;
+  $pms->{async}->bgsend_and_start_lookup($lookup, 'NS', undef, $ent,
+    sub { my ($ent,$pkt) = @_; $self->complete_ns_lookup($pms, $ent, $pkt) },
+      master_deadline => $pms->{master_deadline} );
 }
 
 sub complete_ns_lookup {
-  my ($self, $pms, $ent, $pkt, $dom) = @_;
+  my ($self, $pms, $ent, $pkt) = @_;
 
   if (!$pkt) {
     # $pkt will be undef if the DNS query was aborted (e.g. timed out)
@@ -920,178 +929,177 @@ sub complete_ns_lookup {
     return;
   }
 
-  dbg("uridnsbl: complete_ns_lookup %s", $ent->{key});
+  dbg("uridnsbl: complete_ns_lookup %s %s", $ent->{key},
+    join(',', @{$ent->{rulename}}));
   my $conf = $pms->{conf};
   my @answer = $pkt->answer;
 
-  my $IPV4_ADDRESS = IPV4_ADDRESS;
-  my $IP_PRIVATE = IP_PRIVATE;
   my $nsrhsblrules = $pms->{uridnsbl_active_rules_nsrhsbl};
   my $fullnsrhsblrules = $pms->{uridnsbl_active_rules_fullnsrhsbl};
-  my $seen_lookups = $pms->{'uridnsbl_seen_lookups'};
+  my $areviprules = $pms->{uridnsbl_active_rules_arevipbl};
+  my $seen_lookups = $pms->{uridnsbl_seen_lookups};
 
   my $j = 0;
   foreach my $rr (@answer) {
     $j++;
     my $str = $rr->string;
-    next unless (defined($str) && defined($dom));
-    dbg("uridnsbl: got($j) NS for $dom: $str");
+    next unless defined $str && defined $ent->{lookup};
+    $str =~ s/.*\s//; # strip IN NS
+    dbg("uridnsbl: got($j) NS for $ent->{lookup}: $str");
 
     if ($rr->type eq 'NS') {
       my $nsmatch = lc $rr->nsdname;  # available since at least Net::DNS 0.14
       my $nsrhblstr = $nsmatch;
       my $fullnsrhblstr = $nsmatch;
 
-      if ($nsmatch =~ /^\d+\.\d+\.\d+\.\d+$/) {
+      # It would be very rare to receive IP as NS record, which is a
+      # misconfigure. Bind doesn't even allow that..
+      if ($nsmatch =~ /^\d+\.\d+\.\d+\.\d+$/ || index($nsmatch, ':') >= 0) {
        # only look up the IP if it is public and valid
-       if ($nsmatch =~ /^$IPV4_ADDRESS$/o && $nsmatch !~ /^$IP_PRIVATE$/o) {
-         $self->lookup_dnsbl_for_ip($pms, $ent->{obj}, $nsmatch);
-       }
-        $nsrhblstr = $nsmatch;
+       if ($nsmatch =~ IS_IPV4_ADDRESS && $nsmatch !~ IS_IP_PRIVATE) {
+          # Use IP in RHSBL lookups
+          #$nsrhblstr = $nsmatch; # already set
+        } else {
+          # Skip bogus/private/IPv6 completely
+          next;
+        }
       }
       else {
-        if (!$seen_lookups->{'A:'.$nsmatch}) {
-          $seen_lookups->{'A:'.$nsmatch} = 1;
-          $self->lookup_a_record($pms, $ent->{obj}, $nsmatch);
+        if (!$seen_lookups->{"A:$nsmatch"}) {
+          $seen_lookups->{"A:$nsmatch"} = 1;
+          $self->lookup_a_record($pms, $nsmatch, [keys %$areviprules]);
         }
         $nsrhblstr = $self->{main}->{registryboundaries}->trim_domain($nsmatch);
       }
 
       foreach my $rulename (keys %{$nsrhsblrules}) {
         my $rulecf = $conf->{uridnsbls}->{$rulename};
-        $self->lookup_single_dnsbl($pms, $ent->{obj}, $rulename,
-                                  $nsrhblstr, $rulecf->{zone}, $rulecf->{type});
-
-        $pms->register_async_rule_start($rulename);
+        $self->lookup_single_dnsbl($pms, $nsrhblstr, $rulename,
+          $rulecf->{zone}, $rulecf->{type});
       }
 
       foreach my $rulename (keys %{$fullnsrhsblrules}) {
         my $rulecf = $conf->{uridnsbls}->{$rulename};
-        $self->lookup_single_dnsbl($pms, $ent->{obj}, $rulename,
-                                  $fullnsrhblstr, $rulecf->{zone}, $rulecf->{type});
-
-        $pms->register_async_rule_start($rulename);
+        $self->lookup_single_dnsbl($pms, $fullnsrhblstr, $rulename,
+          $rulecf->{zone}, $rulecf->{type});
       }
     }
   }
+
+  # Make sure all finished rules are marked ready.  If foreach block above
+  # launched new lookups, rule_ready() simply ignores them.
+  foreach my $rulename (@{$ent->{rulename}}) {
+    $pms->rule_ready($rulename);
+  }
 }
 
 # ---------------------------------------------------------------------------
 
 sub lookup_a_record {
-  my ($self, $pms, $obj, $hname) = @_;
+  my ($self, $pms, $lookup, $rules) = @_;
+
+  $lookup = idn_to_ascii($lookup);
 
-  my $key = "A:" . $hname;
   my $ent = {
-    key => $key, zone => $hname, obj => $obj, type => "URI-A",
+    rulename => [@$rules],
+    type => "URIBL",
+    lookup => $lookup,
+    domain => $lookup,
   };
-  # dig $hname a
-  $ent = $pms->{async}->bgsend_and_start_lookup(
-    $hname, 'A', undef, $ent,
-    sub { my ($ent2,$pkt) = @_;
-          $self->complete_a_lookup($pms, $ent2, $pkt, $hname) },
-    master_deadline => $pms->{master_deadline} );
-
-  return $ent;
+  $pms->{async}->bgsend_and_start_lookup($lookup, 'A', undef, $ent,
+    sub { my ($ent,$pkt) = @_;
+          $self->complete_a_lookup($pms, $ent, $pkt) },
+    master_deadline => $pms->{master_deadline}
+  );
 }
 
 sub complete_a_lookup {
-  my ($self, $pms, $ent, $pkt, $hname) = @_;
+  my ($self, $pms, $ent, $pkt) = @_;
 
   if (!$pkt) {
     # $pkt will be undef if the DNS query was aborted (e.g. timed out)
     dbg("uridnsbl: complete_a_lookup aborted %s", $ent->{key});
     return;
   }
-  dbg("uridnsbl: complete_a_lookup %s", $ent->{key});
+
+  dbg("uridnsbl: complete_a_lookup %s %s", $ent->{key},
+    join(',', @{$ent->{rulename}}));
+
   my $j = 0;
   my @answer = $pkt->answer;
   foreach my $rr (@answer) {
     $j++;
-    my $str = $rr->string;
-    if (!defined $hname) {
-      warn "complete_a_lookup-1: $j, (hname is undef), $str";
-    } elsif (!defined $str) {
-      warn "complete_a_lookup-2: $j, $hname, (str is undef)";
-      next;
-    }
-    dbg("uridnsbl: complete_a_lookup got(%d) A for %s: %s", $j,$hname,$str);
+    next if $rr->type ne 'A';
+    my $ip_address = $rr->address;
+    dbg("uridnsbl: complete_a_lookup got(%d) A for %s: %s",
+        $j, $ent->{lookup}, $ip_address);
+    $self->lookup_dnsbl_for_ip($pms, $ip_address, $ent);
+  }
 
-    if ($rr->type eq 'A') {
-      my $ip_address = $rr->rdatastr;
-      $self->lookup_dnsbl_for_ip($pms, $ent->{obj}, $ip_address);
-    }
+  # Make sure all finished rules are marked ready.  If foreach block above
+  # launched new lookups, rule_ready() simply ignores them.
+  foreach my $rulename (@{$ent->{rulename}}) {
+    $pms->rule_ready($rulename);
   }
 }
 
 # ---------------------------------------------------------------------------
 
 sub lookup_dnsbl_for_ip {
-  my ($self, $pms, $obj, $ip) = @_;
-
-  local($1,$2,$3,$4);
-  $ip =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/;
-  my $revip = "$4.$3.$2.$1";
+  my ($self, $pms, $ip, $ent) = @_;
 
   my $conf = $pms->{conf};
-
-  my @rulenames;
-  if ($obj->{is_arevip}) {
-    @rulenames = keys %{$pms->{uridnsbl_active_rules_arevipbl}};
-  } else {
-    @rulenames = keys %{$pms->{uridnsbl_active_rules_nsrevipbl}};
-  }
-  foreach my $rulename (@rulenames) {
+  foreach my $rulename (@{$ent->{rulename}}) {
     my $rulecf = $conf->{uridnsbls}->{$rulename};
-
-    my $tflags = $conf->{tflags}->{$rulename} || '';
-    # ips_only/domains_only lookups should not act on this kind of BL
-    next if $tflags =~ /\b(?:ips_only|domains_only)\b/;
-
-    $self->lookup_single_dnsbl($pms, $obj, $rulename,
-                              $revip, $rulecf->{zone}, $rulecf->{type});
+    $self->lookup_single_dnsbl($pms, $ip, $rulename,
+      $rulecf->{zone}, $rulecf->{type}, $ent->{domain});
   }
 }
 
 sub lookup_single_dnsbl {
-  my ($self, $pms, $obj, $rulename, $lookupstr, $dnsbl, $qtype) = @_;
+  my ($self, $pms, $lookup, $rulename, $zone, $type, $orig_domain) = @_;
 
-  my $qkey = "$rulename:$lookupstr:$dnsbl:$qtype";
+  $lookup = idn_to_ascii($lookup);
+
+  my $qkey = "$rulename:$lookup:$zone:$type";
   return if exists $pms->{uridnsbl_seen_lookups}{$qkey};
   $pms->{uridnsbl_seen_lookups}{$qkey} = 1;
 
-  my $key = "DNSBL:" . $lookupstr . ':' . $dnsbl;
+  # IP queries need to be reversed
+  # Let's do it here, and only here..
+  my $domain = $lookup;
+  if ($lookup =~ /^\d+\.\d+\.\d+\.\d+$/) {
+    $lookup = reverse_ip_address($lookup);
+  }
+
   my $ent = {
-    key => $key, zone => $dnsbl, obj => $obj, type => 'URI-DNSBL',
     rulename => $rulename,
+    type => "URIBL",
+    lookup => $lookup,
+    domain => $domain,
+    orig_domain => $orig_domain,
   };
-  $ent = $pms->{async}->bgsend_and_start_lookup(
-    $lookupstr.".".$dnsbl, $qtype, undef, $ent,
-    sub { my ($ent2,$pkt) = @_;
-          $self->complete_dnsbl_lookup($pms, $ent2, $pkt) },
-    master_deadline => $pms->{master_deadline} );
-
-  return $ent;
+  $pms->{async}->bgsend_and_start_lookup("$lookup.$zone", $type, undef, $ent,
+    sub { my ($ent,$pkt) = @_; $self->complete_dnsbl_lookup($pms, $ent, $pkt) },
+    master_deadline => $pms->{master_deadline});
 }
 
 sub complete_dnsbl_lookup {
   my ($self, $pms, $ent, $pkt) = @_;
 
+  my $rulename = $ent->{rulename};
+
   if (!$pkt) {
     # $pkt will be undef if the DNS query was aborted (e.g. timed out)
     dbg("uridnsbl: complete_dnsbl_lookup aborted %s %s",
-        $ent->{rulename}, $ent->{key});
+        $rulename, $ent->{key});
     return;
   }
 
-  dbg("uridnsbl: complete_dnsbl_lookup %s %s", $ent->{rulename}, $ent->{key});
-  my $conf = $pms->{conf};
-
-  my $zone = $ent->{zone};
-  my $dom = $ent->{obj}->{dom};
-  my $rulename = $ent->{rulename};
-  my $rulecf = $conf->{uridnsbls}->{$rulename};
+  $pms->rule_ready($rulename); # mark rule ready for metas
+  dbg("uridnsbl: complete_dnsbl_lookup $ent->{key} $rulename");
 
+  my $rulecf = $pms->{conf}->{uridnsbls}->{$rulename};
   my @subtests;
   my @answer = $pkt->answer;
   foreach my $rr (@answer)
@@ -1100,17 +1108,16 @@ sub complete_dnsbl_lookup {
     my $rr_type = $rr->type;
 
     if ($rr_type eq 'A') {
-      # Net::DNS::RR::A::address() is available since Net::DNS 0.69
-      $rdatastr = $rr->UNIVERSAL::can('address') ? $rr->address
-                                                 : $rr->rdatastr;
-      if ($rdatastr =~ m/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) {
+      $rdatastr = $rr->address;
+      if ($rdatastr =~ IS_IPV4_ADDRESS) {
         $rdatanum = Mail::SpamAssassin::Util::my_inet_aton($rdatastr);
       }
     } elsif ($rr_type eq 'TXT') {
-      # txtdata returns a non- zone-file-format encoded result, unlike rdatastr;
+      # txtdata returns a non- zone-file-format encoded result, unlike rdstring;
       # avoid space-separated RDATA <character-string> fields if possible;
       # txtdata provides a list of strings in list context since Net::DNS 0.69
-      $rdatastr = join('',$rr->txtdata);
+      $rdatastr = join('', $rr->txtdata);
+      utf8::encode($rdatastr)  if utf8::is_utf8($rdatastr);
     } else {
       next;
     }
@@ -1118,7 +1125,7 @@ sub complete_dnsbl_lookup {
     my $subtest = $rulecf->{subtest};
 
     dbg("uridnsbl: %s . %s -> %s, %s%s",
-        $dom, $zone, $rdatastr, $rulename,
+        $ent->{domain}, $ent->{zone}, $rdatastr, $rulename,
         !defined $subtest ? '' : ', subtest:'.$subtest);
 
     my $match;
@@ -1126,7 +1133,7 @@ sub complete_dnsbl_lookup {
       # this zone is a simple rule, not a set of subrules
       # skip any A record that isn't on 127/8
       if ($rr_type eq 'A' && $rdatastr !~ /^127\./) {
-       warn("uridnsbl: bogus rr for domain=$dom, rule=$rulename, id=" .
+       warn("uridnsbl: bogus rr for domain=$ent->{domain}, rule=$rulename, id=" .
             $pkt->header->id." rr=".$rr->string);
        next;
       }
@@ -1143,44 +1150,32 @@ sub complete_dnsbl_lookup {
       : 0; # notice int($n1) to fix perl ~5.14 taint bug (Bug 7725)
 
       dbg("uridnsbl: %s . %s -> %s, %s, %08x %s %s",
-          $dom, $zone, $rdatastr, $rulename, $rdatanum,
+          $ent->{domain}, $ent->{zone}, $rdatastr, $rulename, $rdatanum,
           !defined $n2 ? sprintf('& %08x', $n1)
           : $n1 == $n2 ? sprintf('== %08x', $n1)
           :              sprintf('%08x%s%08x', $n1,$delim,$n2),
           $match ? 'match' : 'no');
     }
-    $self->got_dnsbl_hit($pms, $ent, $rdatastr, $dom, $rulename) if $match;
+    if ($match) {
+      $self->got_dnsbl_hit($pms, $ent, $rdatastr, $rulename);
+    }
   }
 }
 
 sub got_dnsbl_hit {
-  my ($self, $pms, $ent, $str, $dom, $rulename) = @_;
+  my ($self, $pms, $ent, $str, $rulename) = @_;
 
   $str =~ s/\s+/  /gs; # long whitespace => short
-  dbg("uridnsbl: domain \"$dom\" listed ($rulename): $str");
+  dbg("uridnsbl: domain \"$ent->{domain}\" listed ($rulename): $str");
 
-  if (!defined $pms->{uridnsbl_hits}->{$rulename}) {
-    $pms->{uridnsbl_hits}->{$rulename} = { };
-  };
-  $pms->{uridnsbl_hits}->{$rulename}->{$dom} = 1;
-
-  if ( $pms->{uridnsbl_active_rules_nsrevipbl}->{$rulename}
-    || $pms->{uridnsbl_active_rules_arevipbl}->{$rulename}
-    || $pms->{uridnsbl_active_rules_nsrhsbl}->{$rulename}
-    || $pms->{uridnsbl_active_rules_fullnsrhsbl}->{$rulename}
-    || $pms->{uridnsbl_active_rules_rhsbl}->{$rulename}
-    || $pms->{uridnsbl_active_rules_rhsbl_ipsonly}->{$rulename}
-    || $pms->{uridnsbl_active_rules_rhsbl_domsonly}->{$rulename})
-  {
-    # TODO: this needs to handle multiple domain hits per rule
-    $pms->clear_test_state();
-    my $uris = join (' ', keys %{$pms->{uridnsbl_hits}->{$rulename}});
-    $pms->test_log ("URIs: $uris");
-    $pms->got_hit ($rulename, "");
-
-    # note that this rule has completed (since it got at least 1 hit)
-    $pms->register_async_rule_finish($rulename);
+  $pms->{uridnsbl_hits}->{$rulename}->{$ent->{domain}} = 1;
+
+  if (defined $ent->{orig_domain}) {
+    $pms->test_log("URI: $ent->{orig_domain}/$ent->{domain}", $rulename);
+  } else {
+    $pms->test_log("URI: $ent->{domain}", $rulename);
   }
+  $pms->got_hit($rulename, '', ruletype => 'eval');
 }
 
 # ---------------------------------------------------------------------------
@@ -1191,5 +1186,6 @@ sub has_tflags_domains_only { 1 }
 sub has_subtest_for_ranges { 1 }
 sub has_uridnsbl_for_a { 1 }  # uridnsbl rules recognize tflags 'a' and 'ns'
 sub has_uridnsbl_a_ns { 1 }  # has an actually working 'a' flag, unlike above :-(
+sub has_tflags_notrim { 1 }  # Bug 7835
 
 1;
index af34e14a48cb3f2e07d11e72846962e7330b6875..863d3a6bd972402bd194b20364638d53f39bfdf9 100644 (file)
@@ -32,7 +32,7 @@ rules apply to all URIs found in the message.
 
 The format for defining a rule is as follows:
 
-  uri_detail SYMBOLIC_TEST_NAME key1 =~ /value1/  key2 !~ /value2/ ...
+  uri_detail SYMBOLIC_TEST_NAME key1 =~ /value1/i  key2 !~ /value2/ ...
 
 Supported keys are:
 
@@ -239,12 +239,10 @@ sub check_uri_detail {
       dbg("uri: criteria for $test met");
     }
     
-    $permsg->got_hit($test);
-
     # reset hash
     keys %uri_detail;
 
-    return 0;
+    return 1;
   }
 
   return 0;
index 06a1425fc6dfa14c5a5b0113246c0f2b7e9a347f..fc60150331b9a4910190bb663db0f3ec261e8d78 100644 (file)
@@ -38,9 +38,9 @@ sub new {
   bless ($self, $class);
 
   # the important bit!
-  $self->register_eval_rule("check_for_http_redirector");
-  $self->register_eval_rule("check_https_ip_mismatch");
-  $self->register_eval_rule("check_uri_truncated");
+  $self->register_eval_rule("check_for_http_redirector"); # type does not matter
+  $self->register_eval_rule("check_https_ip_mismatch"); # type does not matter
+  $self->register_eval_rule("check_uri_truncated"); # type does not matter
 
   return $self;
 }
@@ -54,7 +54,7 @@ sub check_for_http_redirector {
     while (s{^https?://([^/:\?]+).+?(https?:/{0,2}?([^/:\?]+).*)$}{$2}i) {
       my ($redir, $dest) = ($1, $3);
       foreach ($redir, $dest) {
-       $_ = $self->{main}->{registryboundaries}->uri_to_domain($_) || $_;
+        $_ = $self->{main}->{registryboundaries}->uri_to_domain($_) || $_;
       }
       next if ($redir eq $dest);
       dbg("eval: redirect: found $redir to $dest, flagging");
@@ -69,13 +69,15 @@ sub check_for_http_redirector {
 sub check_https_ip_mismatch {
   my ($self, $pms) = @_;
 
-  while (my($k,$v) = each %{$pms->{html}->{uri_detail}}) {
-    next if ($k !~ m%^https?:/*(?:[^\@/]+\@)?\d+\.\d+\.\d+\.\d+%i);
-    foreach (@{$v->{anchor_text}}) {
-      next if (m%^https:/*(?:[^\@/]+\@)?\d+\.\d+\.\d+\.\d+%i);
-      if (m%https:%i) {
-       keys %{$self->{html}->{uri_detail}}; # resets iterator, bug 4829
-       return 1;
+  foreach my $html (@{$pms->{html_all}}) {
+    foreach my $k (keys %{$html->{uri_detail}}) {
+      my $v = $html->{uri_detail}->{$k};
+      next if ($k !~ m%^https?:/*(?:[^\@/]+\@)?\d+\.\d+\.\d+\.\d+%i);
+      foreach (@{$v->{anchor_text}}) {
+        next if (m%^https:/*(?:[^\@/]+\@)?\d+\.\d+\.\d+\.\d+%i);
+        if (m%https:%i) {
+          return 1;
+        }
       }
     }
   }
@@ -88,7 +90,7 @@ sub check_https_ip_mismatch {
 # is there a better way to do this?
 sub check_uri_truncated {
   my ($self, $pms) = @_;
-  return $pms->{'uri_truncated'};
+  return $pms->{'uri_truncated'} ? 1 : 0;
 }
 
 1;
index 4def393d877541fa9fb9ab1792e965a9481111d8..7b7032e017437706a9579e02c6009a5ba38c480b 100644 (file)
@@ -17,7 +17,7 @@
 
 =head1 NAME
 
-URILocalBL - blacklist URIs using local information (ISP names, address lists, and country codes)
+URILocalBL - blocklist URIs using local information (ISP names, address lists, and country codes)
 
 =head1 SYNOPSIS
 
@@ -27,20 +27,20 @@ found in the HTML portion of a message, i.e. <a href=...> markup.
 
   loadplugin    Mail::SpamAssassin::Plugin::URILocalBL
 
-Why local blacklisting? There are a few excellent, effective, and
+Why local blocklisting? There are a few excellent, effective, and
 well-maintained DNSBL's out there. But they have several drawbacks:
 
 =over 2
 
-=item * blacklists can cover tens of thousands of entries, and you can't select which ones you use;
+=item * blocklists can cover tens of thousands of entries, and you can't select which ones you use;
 
 =item * verifying that it's correctly configured can be non-trivial;
 
-=item * new blacklisting entries may take a while to be detected and entered, so it's not instantaneous.
+=item * new blocklisting entries may take a while to be detected and entered, so it's not instantaneous.
 
 =back
 
-Sometimes all you want is a quick, easy, and very surgical blacklisting of
+Sometimes all you want is a quick, easy, and very surgical blocklisting of
 a particular site or a particular ISP. This plugin is defined for that
 exact usage case.
 
@@ -48,24 +48,33 @@ exact usage case.
 
 The format for defining a rule is as follows:
 
-  uri_block_cc SYMBOLIC_TEST_NAME cc1 cc2 cc3 cc4
+  uri_block_cc SYMBOLIC_TEST_NAME cc1 cc2 cc3 cc4 ..
+  uri_block_cc SYMBOLIC_TEST_NAME !cc1 !cc2 ..
 
 or:
 
-  uri_block_cont SYMBOLIC_TEST_NAME co1 co2 co3 co4
+  uri_block_cont SYMBOLIC_TEST_NAME co1 co2 co3 co4 ..
+  uri_block_cont SYMBOLIC_TEST_NAME !co1 !co2 ..
 
 or:
 
-  uri_block_cidr SYMBOLIC_TEST_NAME a.a.a.a b.b.b.b/cc d.d.d.d-e.e.e.e
+  uri_block_cidr SYMBOLIC_TEST_NAME a.a.a.a b.b.b.b/cc
 
 or:
 
-  uri_block_isp SYMBOLIC_TEST_NAME "DataRancid" "McCarrier" "Phishers-r-Us"
+  uri_block_isp SYMBOLIC_TEST_NAME "Data Rancid" McCarrier Phishers-r-Us
 
 Example rule for matching a URI in China:
 
   uri_block_cc TEST1 cn
 
+If you specify list of negations, such rule will match ANY country except
+the listed ones (Finland, Sweden):
+
+  uri_block_cc TEST1 !fi !se
+
+Continents uri_block_cont works exactly the same as uri_block_cc.
+
 This would block the URL http://www.baidu.com/index.htm.  Similarly, to
 match a Spam-haven netblock:
 
@@ -75,11 +84,12 @@ would match a netblock where several phishing sites were recently hosted.
 
 And to block all CIDR blocks registered to an ISP, one might use:
 
-  uri_block_isp TEST3 "ColoCrossing"
+  uri_block_isp TEST3 "Data Rancid" ColoCrossing
 
-if one didn't trust URL's pointing to that organization's clients.  Lastly,
-if there's a country that you want to block but there's an explicit host
-you wish to exempt from that blacklist, you can use:
+Quote ISP names containing spaces.
+
+Lastly, if there's a country that you want to block but there's an explicit
+host you wish to exempt from that blocklist, you can use:
 
   uri_block_exclude TEST1 www.baidu.com
 
@@ -88,21 +98,17 @@ applicable to CIDR and ISP blocks as well.
 
 =head1 DEPENDENCIES
 
-The Country-Code based filtering requires the Geo::IP or GeoIP2 module, 
-which uses either the fremium GeoLiteCountry database, or the commercial 
-version of it called GeoIP from MaxMind.com.
-
-The ISP based filtering requires the same module, plus the GeoIPISP database.
-There is no fremium version of this database, so commercial licensing is
-required.
+The Country-Code based filtering can use any Mail::SpamAssassin::GeoDB
+supported module like MaxMind::DB::Reader (GeoIP2) or Geo::IP.  ISP based
+filtering might require a paid subscription database like GeoIPISP.
 
 =cut
 
 package Mail::SpamAssassin::Plugin::URILocalBL;
 use Mail::SpamAssassin::Plugin;
-use Mail::SpamAssassin::Logger;
-use Mail::SpamAssassin::Constants qw(:ip);
-use Mail::SpamAssassin::Util qw(untaint_var);
+use Mail::SpamAssassin::Constants qw(:ip :sa);
+use Mail::SpamAssassin::Util qw(untaint_var idn_to_ascii);
+use Mail::SpamAssassin::NetSet;
 
 use Socket;
 
@@ -110,13 +116,13 @@ use strict;
 use warnings;
 # use bytes;
 use re 'taint';
-use version;
 
 our @ISA = qw(Mail::SpamAssassin::Plugin);
 
-use constant HAS_GEOIP => eval { require Geo::IP; };
-use constant HAS_GEOIP2 => eval { require GeoIP2::Database::Reader; };
-use constant HAS_CIDR => eval { require Net::CIDR::Lite; };
+sub dbg { my $msg = shift; Mail::SpamAssassin::Plugin::dbg ("URILocalBL: $msg", @_); }
+
+my $IP_ADDRESS = IP_ADDRESS;
+my $RULENAME_RE = RULENAME_RE;
 
 # constructor
 sub new {
@@ -128,15 +134,13 @@ sub new {
   my $self = $class->SUPER::new($mailsaobject);
   bless ($self, $class);
 
-  # how to handle failure to get the database handle?
-  # and we don't really have a valid return value...
-  # can we defer getting this handle until we actually see
-  # a uri_block_cc rule?
-
   $self->register_eval_rule("check_uri_local_bl");
-
   $self->set_config($mailsaobject->{conf});
 
+  # we need GeoDB country/isp
+  $self->{main}->{geodb_wanted}->{country} = 1;
+  $self->{main}->{geodb_wanted}->{isp} = 1;
+
   return $self;
 }
 
@@ -144,8 +148,6 @@ sub set_config {
   my ($self, $conf) = @_;
   my @cmds;
 
-  my $pluginobj = $self;        # allow use inside the closure below
-
   push (@cmds, {
     setting => 'uri_block_cc',
     type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE,
@@ -153,38 +155,38 @@ sub set_config {
     code => sub {
       my ($self, $key, $value, $line) = @_;
 
-      if ($value !~ /^(\S+)\s+(.+)$/) {
-       return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+      if ($value !~ /^(${RULENAME_RE})\s+(.+?)\s*$/) {
+        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
       }
       my $name = $1;
-      my $def = $2;
-      my $added_criteria = 0;
-
-      $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{countries} = {};
-
-      # this should match all country codes including satellite providers
-      while ($def =~ m/^\s*([a-z][a-z0-9])(\s+(.*)|)$/) {
-       my $cc = $1;
-       my $rest = $2;
-
-       #dbg("config: uri_block_cc adding %s to %s\n", $cc, $name);
-        $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{countries}->{uc($cc)} = 1;
-       $added_criteria = 1;
-
-        $def = $rest;
+      my $args = $2;
+      my @added;
+
+      foreach my $cc (split(/\s+/, uc($args))) {
+        # this should match all country codes including satellite providers
+        if ($cc =~ /^((\!)?([a-z][a-z0-9]))$/i) {
+          if (defined $2) {
+            $self->{urilocalbl}->{$name}{countries_neg} = 1;
+            $self->{urilocalbl}->{$name}{countries}{$3} = 0;
+          } else {
+            $self->{urilocalbl}->{$name}{countries}{$3} = 1;
+          }
+          push @added, $1;
+        } else {
+          return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+        }
       }
 
-      if ($added_criteria == 0) {
-        warn "config: no arguments";
-       return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
-      } elsif ($def ne '') {
-        warn "config: failed to add invalid rule $name";
-       return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+      my %checkneg = map { $_ => 1 } values %{$self->{urilocalbl}->{$name}{countries}};
+      if (scalar keys %checkneg > 1) {
+        dbg("config: uri_block_cc $name failed: trying to combine negations and non-negations");
+        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
       }
 
-      dbg("config: uri_block_cc added %s\n", $name);
-
-      $conf->{parser}->add_test($name, 'check_uri_local_bl()', $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+      dbg("config: uri_block_cc $name added: ".join(' ', @added));
+      $self->{parser}->add_test($name, 'check_uri_local_bl()',
+        $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+      $self->{parser}->{conf}->{priority}->{$name} = -100;
     }
   });
 
@@ -195,38 +197,38 @@ sub set_config {
     code => sub {
       my ($self, $key, $value, $line) = @_;
 
-      if ($value !~ /^(\S+)\s+(.+)$/) {
-       return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+      if ($value !~ /^(${RULENAME_RE})\s+(.+?)\s*$/) {
+        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
       }
       my $name = $1;
-      my $def = $2;
-      my $added_criteria = 0;
-
-      $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{continents} = {};
-
-      # this should match all continent codes
-      while ($def =~ m/^\s*([a-z]{2})(\s+(.*)|)$/) {
-       my $cont = $1;
-       my $rest = $2;
-
-       # dbg("config: uri_block_cont adding %s to %s\n", $cont, $name);
-        $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{continents}->{uc($cont)} = 1;
-       $added_criteria = 1;
-
-        $def = $rest;
+      my $args = $2;
+      my @added;
+
+      foreach my $cc (split(/\s+/, uc($args))) {
+        # this should match all continent codes
+        if ($cc =~ /^((\!)?([a-z]{2}))$/i) {
+          if (defined $2) {
+            $self->{urilocalbl}->{$name}{continents_neg} = 1;
+            $self->{urilocalbl}->{$name}{continents}{$3} = 0;
+          } else {
+            $self->{urilocalbl}->{$name}{continents}{$3} = 1;
+          }
+          push @added, $1;
+        } else {
+          return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+        }
       }
 
-      if ($added_criteria == 0) {
-        warn "config: no arguments";
-       return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
-      } elsif ($def ne '') {
-        warn "config: failed to add invalid rule $name";
-       return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+      my %checkneg = map { $_ => 1 } values %{$self->{urilocalbl}->{$name}{continents}};
+      if (scalar keys %checkneg > 1) {
+        dbg("config: uri_block_cont $name failed: trying to combine negations and non-negations");
+        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
       }
 
-      dbg("config: uri_block_cont added %s\n", $name);
-
-      $conf->{parser}->add_test($name, 'check_uri_local_bl()', $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+      dbg("config: uri_block_cont $name added: ".join(' ', @added));
+      $self->{parser}->add_test($name, 'check_uri_local_bl()',
+        $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+      $self->{parser}->{conf}->{priority}->{$name} = -100;
     }
   });
   
@@ -237,36 +239,30 @@ sub set_config {
     code => sub {
       my ($self, $key, $value, $line) = @_;
 
-      if ($value !~ /^(\S+)\s+(.+)$/) {
-       return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+      if ($value !~ /^(${RULENAME_RE})\s+(.+?)\s*$/) {
+        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
       }
       my $name = $1;
-      my $def = $2;
-      my $added_criteria = 0;
-
-      $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{isps} = {};
-
-      # gather up quoted strings
-      while ($def =~ m/^\s*"([^"]*)"(\s+(.*)|)$/) {
-       my $isp = $1;
-       my $rest = $2;
-
-       dbg("config: uri_block_isp adding \"%s\" to %s\n", $isp, $name);
-        $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{isps}->{$isp} = 1;
-       $added_criteria = 1;
-
-        $def = $rest;
+      my $args = $2;
+      my @added;
+
+      # gather up possibly quoted strings
+      while ($args =~ /("[^"]*"|(?<!")\S+(?!"))/g) {
+        my $isp = $1;
+        $isp =~ s/"//g;
+        my $ispkey = uc($isp); $ispkey =~ s/\s+//gs;
+        $self->{urilocalbl}->{$name}{isps}{$ispkey} = $isp;
+        push @added, "\"$isp\"";
       }
 
-      if ($added_criteria == 0) {
-        warn "config: no arguments";
-       return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
-      } elsif ($def ne '') {
-        warn "config: failed to add invalid rule $name";
-       return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+      if (!defined $self->{urilocalbl}->{$name}{isps}) {
+        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
       }
 
-      $conf->{parser}->add_test($name, 'check_uri_local_bl()', $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+      dbg("config: uri_block_isp $name added: ". join(', ', @added));
+      $self->{parser}->add_test($name, 'check_uri_local_bl()',
+        $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+      $self->{parser}->{conf}->{priority}->{$name} = -100;
     }
   });
 
@@ -277,47 +273,23 @@ sub set_config {
     code => sub {
       my ($self, $key, $value, $line) = @_;
 
-      if (!HAS_CIDR) {
-        warn "config: uri_block_cidr not supported, required module Net::CIDR::Lite missing\n";
+      if ($value !~ /^(${RULENAME_RE})\s+(.+?)\s*$/) {
         return $Mail::SpamAssassin::Conf::INVALID_VALUE;
       }
-
-      if ($value !~ /^(\S+)\s+(.+)$/) {
-       return $Mail::SpamAssassin::Conf::INVALID_VALUE;
-      }
       my $name = $1;
-      my $def = $2;
-      my $added_criteria = 0;
-
-      $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{cidr} = new Net::CIDR::Lite;
-
-      # match individual IP's, subnets, and ranges
-      while ($def =~ m/^\s*(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2}|-\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?)(\s+(.*)|)$/) {
-       my $addr = $1;
-       my $rest = $3;
-
-       dbg("config: uri_block_cidr adding %s to %s\n", $addr, $name);
-
-        eval { $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{cidr}->add_any($addr) };
-        last if ($@);
+      my $args = $2;
 
-       $added_criteria = 1;
-
-        $def = $rest;
-      }
-
-      if ($added_criteria == 0) {
-        warn "config: no arguments";
-       return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
-      } elsif ($def ne '') {
-        warn "config: failed to add invalid rule $name";
-       return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+      foreach my $addr (split(/\s+/, $args)) {
+        if ($addr =~ m!^$IP_ADDRESS(?:/\d{1,3})?$!o) {
+          $self->{urilocalbl}->{$name}{cidr}{$addr} = 1;
+        } else {
+          return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+        }
       }
 
-      # optimize the ranges
-      $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{cidr}->clean();
-
-      $conf->{parser}->add_test($name, 'check_uri_local_bl()', $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+      $self->{parser}->add_test($name, 'check_uri_local_bl()',
+        $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+      $self->{parser}->{conf}->{priority}->{$name} = -100;
     }
   });
 
@@ -328,378 +300,255 @@ sub set_config {
     code => sub {
       my ($self, $key, $value, $line) = @_;
 
-      if ($value !~ /^(\S+)\s+(.+)$/) {
-       return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+      if ($value !~ /^(${RULENAME_RE})\s+(.+?)\s*$/) {
+        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
       }
       my $name = $1;
-      my $def = $2;
-      my $added_criteria = 0;
-
-      $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{exclusions} = {};
-
-      # match individual IP's, or domain names
-      while ($def =~ m/^\s*((\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})|(([a-z0-9][-a-z0-9]*[a-z0-9](\.[a-z0-9][-a-z0-9]*[a-z0-9]){1,})))(\s+(.*)|)$/) {
-       my $addr = $1;
-       my $rest = $6;
-
-       dbg("config: uri_block_exclude adding %s to %s\n", $addr, $name);
-
-        $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{exclusions}->{$addr} = 1;
-
-       $added_criteria = 1;
+      my $args = $2;
 
-        $def = $rest;
+      foreach my $arg (split(/\s+/, $args)) {
+        $self->{urilocalbl}->{$name}{exclusions}{lc($arg)} = 1;
       }
 
-      if ($added_criteria == 0) {
-        warn "config: no arguments";
-       return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
-      } elsif ($def ne '') {
-        warn "config: failed to add invalid rule $name";
-       return $Mail::SpamAssassin::Conf::INVALID_VALUE;
-      }
-
-      $conf->{parser}->add_test($name, 'check_uri_local_bl()', $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+      $self->{parser}->add_test($name, 'check_uri_local_bl()',
+        $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
+      $self->{parser}->{conf}->{priority}->{$name} = -100;
     }
   });
 
-=over 2  
-
-=item uri_country_db_path STRING
-
-This option tells SpamAssassin where to find the MaxMind country GeoIP2 
-database. Country or City database are both supported.
+  $conf->{parser}->register_commands(\@cmds);
+}
 
-=back
+sub finish_parsing_end {
+  my ($self, $opts) = @_;
 
-=cut
+  my $conf = $opts->{conf};
 
-  push (@cmds, {
-    setting => 'uri_country_db_path',
-    is_priv => 1,
-    default => undef,
-    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
-    code => sub {
-      my ($self, $key, $value, $line) = @_;
-      if (!defined $value || !length $value) {
-        return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
-      }
-      if (!-f $value) {
-        info("config: uri_country_db_path \"$value\" is not accessible");
-        $self->{uri_country_db_path} = $value;
-        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+  # compile cidrs now
+  foreach my $rulename (keys %{$conf->{urilocalbl}}) {
+    my $ruleconf = $conf->{urilocalbl}->{$rulename};
+    next if defined $ruleconf->{netset};
+    next if !defined $ruleconf->{cidr};
+    my $netset = Mail::SpamAssassin::NetSet->new($rulename);
+    foreach my $addr (keys %{$ruleconf->{cidr}}) {
+      if ($netset->add_cidr($addr)) {
+        dbg("config: uri_block_cidr $rulename added: $addr");
+      } else {
+        dbg("config: uri_block_cidr $rulename add failed: $addr");
       }
-
-      $self->{uri_country_db_path} = $value;
     }
-  });
-
-=over 2
-
-=item uri_country_db_isp_path STRING
-
-This option tells SpamAssassin where to find the MaxMind isp GeoIP2 database.
-
-=back
-
-=cut
-
-  push (@cmds, {
-    setting => 'uri_country_db_isp_path',
-    is_priv => 1,
-    default => undef,
-    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
-    code => sub {
-      my ($self, $key, $value, $line) = @_;
-      if (!defined $value || !length $value) {
-        return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
-      }
-      if (!-f $value) {
-        info("config: uri_country_db_isp_path \"$value\" is not accessible");
-        $self->{uri_country_db_isp_path} = $value;
-        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
-      }
-
-      $self->{uri_country_db_isp_path} = $value;
+    if ($netset->get_num_nets()) {
+      $ruleconf->{netset} = $netset;
     }
-  });  
-  $conf->{parser}->register_commands(\@cmds);
-}  
+  }
+}
 
 sub check_uri_local_bl {
-  my ($self, $permsg) = @_;
-
-  my $cc;
-  my $cont;
-  my $db_info;
-  my $isp;
-  my $conf_country_db_path = $self->{'main'}{'resolver'}{'conf'}->{uri_country_db_path};
-  my $conf_country_db_isp_path = $self->{'main'}{'resolver'}{'conf'}->{uri_country_db_isp_path};
-  # If country_db_path is set I am using GeoIP2 api
-  if ( HAS_GEOIP2 and ( ( defined $conf_country_db_path ) or ( defined $conf_country_db_isp_path ) ) ) {
-
-   eval {
-    $self->{geoip} = GeoIP2::Database::Reader->new(
-               file    => $conf_country_db_path,
-               locales => [ 'en' ]
-    ) if (( defined $conf_country_db_path ) && ( -f $conf_country_db_path));
-    if ( defined ($conf_country_db_path) ) {
-      $db_info = sub { return "GeoIP2 " . ($self->{geoip}->metadata()->description()->{en} || '?') };
-      warn "$conf_country_db_path not found" unless $self->{geoip};
-    }
+  my ($self, $pms) = @_;
 
-    $self->{geoisp} = GeoIP2::Database::Reader->new(
-               file    => $conf_country_db_isp_path,
-               locales => [ 'en' ]
-    ) if (( defined $conf_country_db_isp_path ) && ( -f $conf_country_db_isp_path));
-    if ( defined ($conf_country_db_isp_path) ) {
-      warn "$conf_country_db_isp_path not found" unless $self->{geoisp};
-    }
-    $self->{use_geoip2} = 1;
-   };
-   if ($@ || !($self->{geoip} || $self->{geoisp})) {
-     $@ =~ s/\s+Trace begun.*//s;
-     warn "URILocalBL: GeoIP2 load failed: $@\n";
-     return 0;
-   }
-
-  } elsif ( HAS_GEOIP ) {
-    BEGIN {
-      Geo::IP->import( qw(GEOIP_MEMORY_CACHE GEOIP_CHECK_CACHE GEOIP_ISP_EDITION) );
-    }
-    $self->{use_geoip2} = 0;
-    # need GeoIP C library 1.6.3 and GeoIP perl API 1.4.4 or later to avoid messages leaking - Bug 7153
-    my $gic_wanted = version->parse('v1.6.3');
-    my $gic_have = version->parse(Geo::IP->lib_version());
-    my $gip_wanted = version->parse('v1.4.4');
-    my $gip_have = version->parse($Geo::IP::VERSION);
-
-    # this code burps an ugly message if it fails, but that's redirected elsewhere
-    my $flags = 0;
-    my $flag_isp = 0;
-    my $flag_silent = 0;
-    eval '$flags = GEOIP_MEMORY_CACHE | GEOIP_CHECK_CACHE' if ($gip_have >= $gip_wanted);
-    eval '$flag_silent = GEOIP_SILENCE' if ($gip_have >= $gip_wanted);
-    eval '$flag_isp = GEOIP_ISP_EDITION' if ($gip_have >= $gip_wanted);
-
-   eval {
-    if ($flag_silent && $gic_have >= $gic_wanted) {
-      $self->{geoip} = Geo::IP->new($flags | $flag_silent);
-      $self->{geoisp} = Geo::IP->open_type($flag_isp, $flag_silent | $flags);
-    } else {
-      open(OLDERR, ">&STDERR");
-      open(STDERR, ">", "/dev/null");
-      $self->{geoip} = Geo::IP->new($flags);
-      $self->{geoisp} = Geo::IP->open_type($flag_isp);
-      open(STDERR, ">&OLDERR");
-      close(OLDERR);
-    }
-   };
-    if ($@ || !($self->{geoip} || $self->{geoisp})) {
-      $@ =~ s/\s+Trace begun.*//s;
-      warn "URILocalBL: GeoIP load failed: $@\n";
-      return 0;
-    }
+  return 0 if $self->{urilocalbl_disabled};
 
-    $db_info = sub { return "Geo::IP " . ($self->{geoip}->database_info || '?') };
-  } else {
-    dbg("No GeoIP module available");
+  if (!$self->{main}->{geodb} ||
+        (!$self->{main}->{geodb}->can('country') &&
+         !$self->{main}->{geodb}->can('isp'))) {
+    dbg("plugin disabled, GeoDB country/isp not available");
+    $self->{urilocalbl_disabled} = 1;
     return 0;
   }
 
-  my %uri_detail = %{ $permsg->get_uri_detail_list() };
-  my $test = $permsg->{current_rule_name}; 
-  my $rule = $permsg->{conf}->{uri_local_bl}->{$test};
-
-  my %hit_tests;
-  my $got_hit = 0;
-  my @addrs;
-  my $IP_ADDRESS = IP_ADDRESS;
-  
-  if ( defined $self->{geoip} ) {
-    dbg("check: uri_local_bl evaluating rule %s using database %s\n", $test, $db_info->());
-  } else {
-    dbg("check: uri_local_bl evaluating rule %s\n", $test);
-  }
+  my $rulename = $pms->get_current_eval_rule_name();
+  my $ruleconf = $pms->{conf}->{urilocalbl}->{$rulename};
 
-  my $dns_available = $permsg->is_dns_available();
+  dbg("running $rulename");
 
-  while (my ($raw, $info) = each %uri_detail) {
+  my %found_hosts;
 
+  foreach my $info (values %{$pms->get_uri_detail_list()}) {
     next unless $info->{hosts};
 
     # look for W3 links only
-    next unless (defined $info->{types}->{a} || defined $info->{types}->{parsed});
+    next unless defined $info->{types}->{a} || defined $info->{types}->{parsed};
 
-    while (my($host, $domain) = each %{$info->{hosts}}) {
-
-      # skip if the domain name was matched
-      if (exists $rule->{exclusions} && exists $rule->{exclusions}->{$domain}) {
-        dbg("check: uri_local_bl excludes %s as *.%s\n", $host, $domain);
+    my %hosts = %{$info->{hosts}}; # evade hash reset by copy
+    while (my($host, $domain) = each %hosts) {
+      if (defined $ruleconf->{exclusions}{lc($domain)}) {
+        dbg("excluded $host, domain $domain matches");
         next;
       }
-
-      if($host !~ /^$IP_ADDRESS$/) {
-       if (!$dns_available) {
-         dbg("check: uri_local_bl skipping $host, dns not available");
-         next;
-       }
-       # this would be best cached from prior lookups
-       @addrs = gethostbyname($host);
-       # convert to string values address list
-       @addrs = map { inet_ntoa($_); } @addrs[4..$#addrs];
+      elsif ($host =~ IS_IP_ADDRESS) {
+        if ($self->_check_host($pms, $rulename, $host, [$host])) {
+          # if hit, rule is done
+          return 0;
+        }
       } else {
-       @addrs = ($host);
+        # do host lookups only after all IPs are checked, since they
+        # don't need resolving..
+        $found_hosts{$host} = 1;
       }
+    }
+  }
 
-      dbg("check: uri_local_bl %s addrs %s\n", $host, join(', ', @addrs));
-
-      for my $ip (@addrs) {
-        # skip if the address was matched
-        if (exists $rule->{exclusions} && exists $rule->{exclusions}->{$ip}) {
-          dbg("check: uri_local_bl excludes %s(%s)\n", $host, $ip);
-          next;
-        }
-
-        if (exists $rule->{countries}) {
-          dbg("check: uri_local_bl countries %s\n", join(' ', sort keys %{$rule->{countries}}));
-
-          if ( $self->{use_geoip2} == 1 ) {
-            my $country;
-            if (index($self->{geoip}->metadata()->description()->{en}, 'City') != -1) {
-              $country = $self->{geoip}->city( ip => $ip );
-            } else {
-              $country = $self->{geoip}->country( ip => $ip );
-            }
-            my $country_rec = $country->country();
-            $cc = $country_rec->iso_code();
-          } else {
-            $cc = $self->{geoip}->country_code_by_addr($ip);
-          }
-
-          dbg("check: uri_local_bl host %s(%s) maps to %s\n", $host, $ip, (defined $cc ? $cc : "(undef)"));
-
-          # handle there being no associated country (yes, there are holes in
-          # the database).
-          next unless defined $cc;
-
-          # not in blacklist
-          next unless (exists $rule->{countries}->{$cc});
-
-          dbg("check: uri_block_cc host %s(%s) matched\n", $host, $ip);
-
-          if (would_log('dbg', 'rules') > 1) {
-            dbg("check: uri_block_cc criteria for $test met");
-          }
-      
-          $permsg->test_log("Host: $host in $cc");
-          $hit_tests{$test} = 1;
-
-          # reset hash
-          keys %uri_detail;
-        }
-
-        if (exists $rule->{continents}) {
-          dbg("check: uri_local_bl continents %s\n", join(' ', sort keys %{$rule->{continents}}));
-
-          if ( $self->{use_geoip2} == 1 ) {
-            my $country = $self->{geoip}->country( ip => $ip );
-            my $cont_rec = $country->continent();
-            $cont = $cont_rec->{code};
-          } else {
-            $cc = $self->{geoip}->country_code_by_addr($ip);
-            $cont = $self->{geoip}->continent_code_by_country_code($cc);
-          }
-          
-          dbg("check: uri_local_bl host %s(%s) maps to %s\n", $host, $ip, (defined $cont ? $cont : "(undef)"));
-
-          # handle there being no associated continent (yes, there are holes in
-          # the database).
-          next unless defined $cont;
-
-          # not in blacklist
-          next unless (exists $rule->{continents}->{$cont});
-
-          dbg("check: uri_block_cont host %s(%s) matched\n", $host, $ip);
-
-          if (would_log('dbg', 'rules') > 1) {
-            dbg("check: uri_block_cont criteria for $test met");
-          }
-
-          $permsg->test_log("Host: $host in $cont");
-          $hit_tests{$test} = 1;
+  return 0 unless %found_hosts;
+
+  # bail out now if dns not available
+  return 0 if !$pms->is_dns_available();
+
+  my $queries;
+  foreach my $host (keys %found_hosts) {
+    $host = idn_to_ascii($host);
+    dbg("launching A/AAAA lookup for $host");
+    # launch dns
+    my $ret = $pms->{async}->bgsend_and_start_lookup($host, 'A', undef,
+      { rulename => $rulename, host => $host, type => 'URILocalBL' },
+      sub { my($ent, $pkt) = @_; $self->_finish_lookup($pms, $ent, $pkt); },
+      master_deadline => $pms->{master_deadline}
+    );
+    $queries++ if defined $ret;
+    # also IPv6 if database supports
+    if ($self->{main}->{geodb}->can('country_v6')) {
+      $ret = $pms->{async}->bgsend_and_start_lookup($host, 'AAAA', undef,
+        { rulename => $rulename, host => $host, type => 'URILocalBL' },
+        sub { my($ent, $pkt) = @_; $self->_finish_lookup($pms, $ent, $pkt); },
+        master_deadline => $pms->{master_deadline}
+      );
+      $queries++ if defined $ret;
+    }
+  }
 
-          # reset hash
-          keys %uri_detail;
-        }
+  return 0 if !$queries; # no query started
+  return; # return undef for async status
+}
 
-        if (exists $rule->{isps}) {
-          dbg("check: uri_local_bl isps %s\n", join(' ', map { '"' . $_ . '"'; } sort keys %{$rule->{isps}}));
+sub _finish_lookup {
+  my ($self, $pms, $ent, $pkt) = @_;
 
-          if ( $self->{use_geoip2} == 1 ) {
-            $isp = $self->{geoisp}->isp(ip => $ip);
-          } else {
-            $isp = $self->{geoisp}->isp_by_name($ip);
-          }
+  my $rulename = $ent->{rulename};
+  my $host = $ent->{host};
 
-          dbg("check: uri_local_bl isp %s(%s) maps to %s\n", $host, $ip, (defined $isp ? '"' . $isp . '"' : "(undef)"));
+  # Skip duplicate A / AAAA matches
+  return if $pms->{urilocalbl_finished}->{$rulename};
 
-          # handle there being no associated country
-          next unless defined $isp;
+  if (!$pkt) {
+      # $pkt will be undef if the DNS query was aborted (e.g. timed out)
+      dbg("host lookup failed: $rulename $host");
+      return;
+  }
 
-          # not in blacklist
-          next unless (exists $rule->{isps}->{$isp});
+  $pms->rule_ready($rulename); # mark rule ready for metas
 
-          dbg("check: uri_block_isp host %s(%s) matched\n", $host, $ip);
+  my @answer = $pkt->answer;
+  my @addrs;
+  foreach my $rr (@answer) {
+    if ($rr->type eq 'A' || $rr->type eq 'AAAA') {
+      push @addrs, $rr->address;
+    }
+  }
 
-          if (would_log('dbg', 'rules') > 1) {
-            dbg("check: uri_block_isp criteria for $test met");
-          }
-      
-          $permsg->test_log("Host: $host in \"$isp\"");
-          $hit_tests{$test} = 1;
+  if (@addrs) {
+    if ($self->_check_host($pms, $rulename, $host, \@addrs)) {
+      $pms->{urilocalbl_finished}->{$rulename} = 1;
+    }
+  }
+}
 
-          # reset hash
-          keys %uri_detail;
-        }
+sub _check_host {
+  my ($self, $pms, $rulename, $host, $addrs) = @_;
 
-        if (exists $rule->{cidr}) {
-          dbg("check: uri_block_cidr list %s\n", join(' ', $rule->{cidr}->list_range()));
+  my $ruleconf = $pms->{conf}->{urilocalbl}->{$rulename};
+  my $geodb = $self->{main}->{geodb};
 
-          next unless ($rule->{cidr}->find($ip));
+  if ($host ne $addrs->[0]) {
+    dbg("resolved $host: ".join(', ', @$addrs));
+  }
 
-          dbg("check: uri_block_cidr host %s(%s) matched\n", $host, $ip);
+  foreach my $ip (@$addrs) {
+    if (defined $ruleconf->{exclusions}{$ip}) {
+      dbg("excluded $host, IP $ip matches");
+      return 1;
+    }
+  }
 
-          if (would_log('dbg', 'rules') > 1) {
-            dbg("check: uri_block_cidr criteria for $test met");
-          }
+  if (defined $ruleconf->{countries}) {
+    my $neg = defined $ruleconf->{countries_neg};
+    my $testcc = join(' ', sort keys %{$ruleconf->{countries}});
+    if ($neg) {
+      dbg("checking $host for any country except: $testcc");
+    } else {
+      dbg("checking $host for countries: $testcc");
+    }
+    foreach my $ip (@$addrs) {
+      my $cc = $geodb->get_country($ip);
+      if ( (!$neg && defined $ruleconf->{countries}{$cc}) ||
+           ($neg && !defined $ruleconf->{countries}{$cc}) ) {
+        dbg("$host ($ip) country $cc - HIT");
+        $pms->test_log("Host: $host in country $cc", $rulename);
+        $pms->got_hit($rulename, "");
+        return 1;
+      } else {
+        dbg("$host ($ip) country $cc - ".($neg ? "excluded" : "no match"));
+      }
+    }
+  }
 
-          $permsg->test_log("Host: $host as $ip");
-          $hit_tests{$test} = 1;
+  if (defined $ruleconf->{continents}) {
+    my $neg = defined $ruleconf->{continents_neg};
+    my $testcont = join(' ', sort keys %{$ruleconf->{continents}});
+    if ($neg) {
+      dbg("checking $host for any continent except: $testcont");
+    } else {
+      dbg("checking $host for continents: $testcont");
+    }
+    foreach my $ip (@$addrs) {
+      my $cc = $geodb->get_continent($ip);
+      if ( (!$neg && defined $ruleconf->{continents}{$cc}) ||
+           ($neg && !defined $ruleconf->{continents}{$cc}) ) {
+        dbg("$host ($ip) continent $cc - HIT");
+        $pms->test_log("Host: $host in continent $cc", $rulename);
+        $pms->got_hit($rulename, "");
+        return 1;
+      } else {
+        dbg("$host ($ip) continent $cc - ".($neg ? "excluded" : "no match"));
+      }
+    }
+  }
 
-          # reset hash
-          keys %uri_detail;
+  if (defined $ruleconf->{isps}) {
+    if ($geodb->can('isp')) {
+      my $testisp = join(', ', map {"\"$_\""} sort values %{$ruleconf->{isps}});
+      dbg("checking $host for isps: $testisp");
+
+      foreach my $ip (@$addrs) {
+        my $isp = $geodb->get_isp($ip);
+        next unless defined $isp;
+        my $ispkey = uc($isp); $ispkey =~ s/\s+//gs;
+        if (defined $ruleconf->{isps}{$ispkey}) {
+          dbg("$host ($ip) isp \"$isp\" - HIT");
+          $pms->test_log("Host: $host in isp $isp", $rulename);
+          $pms->got_hit($rulename, "");
+          return 1;
+        } else {
+          dbg("$host ($ip) isp $isp - no match");
         }
       }
-    }
-    # cycle through all tests hitted by the uri
-    while((my $test_ok) = each %hit_tests) {
-      $permsg->got_hit($test_ok);
-      $got_hit = 1;
-    }
-    if($got_hit == 1) {
-      return 1;
     } else {
-      keys %hit_tests;
+      dbg("skipping ISP check, GeoDB database not loaded");
     }
   }
 
-  dbg("check: uri_local_bl %s no match\n", $test);
+  if (defined $ruleconf->{netset}) {
+    foreach my $ip (@$addrs) {
+      if ($ruleconf->{netset}->contains_ip($ip)) {
+        dbg("$host ($ip) matches cidr - HIT");
+        $pms->test_log("Host: $host in cidr", $rulename);
+        $pms->got_hit($rulename, "");
+        return 1;
+      } else {
+        dbg("$host ($ip) not matching cidr");
+      }
+    }
+  }
 
   return 0;
 }
 
 1;
-
index bf854a924f2ff4f331fce341d4ad59a67d29b8bf..6a2d49e0f187dbdffb5d1d02d4a9873b55b721ac 100644 (file)
@@ -43,8 +43,9 @@ sub new {
   my $self = $class->SUPER::new($mailsaobject);
   bless ($self, $class);
 
-  $self->register_eval_rule("have_any_bounce_relays");
-  $self->register_eval_rule("check_whitelist_bounce_relays");
+  $self->register_eval_rule("have_any_bounce_relays"); # type does not matter
+  $self->register_eval_rule("check_welcomelist_bounce_relays"); # type does not matter
+  $self->register_eval_rule("check_whitelist_bounce_relays"); # type does not matter - #Stub - Remove in SA 4.1
 
   $self->set_config($mailsaobject->{conf});
 
@@ -63,7 +64,9 @@ SpamAssassin handles incoming email messages.
 
 =over 4
 
-=item whitelist_bounce_relays hostname [hostname2 ...]
+=item welcomelist_bounce_relays hostname [hostname2 ...]
+
+Previously whitelist_bounce_relays which will work interchangeably until 4.1.
 
 This is used to 'rescue' legitimate bounce messages that were generated in
 response to mail you really *did* send. List the MTA relay hostnames that
@@ -76,14 +79,15 @@ Specifically, C<*> and C<?> are allowed, but all other metacharacters are not.
 Regular expressions are not used for security reasons.
 
 Multiple addresses per line, separated by spaces, is OK.  Multiple
-C<whitelist_bounce_relays> lines are also OK.
+C<welcomelist_bounce_relays> lines are also OK.
 
 =back
 
 =cut
 
   push (@cmds, {
-      setting => 'whitelist_bounce_relays',
+      setting => 'welcomelist_bounce_relays',
+      aliases => ['whitelist_bounce_relays'], # backward compatible - to be removed for 4.1
       type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST
     });
 
@@ -92,11 +96,11 @@ C<whitelist_bounce_relays> lines are also OK.
 
 sub have_any_bounce_relays {
   my ($self, $pms) = @_;
-  return $pms->{conf}->{whitelist_bounce_relays} &&
-         %{$pms->{conf}->{whitelist_bounce_relays}} ? 1 : 0;
+  return $pms->{conf}->{welcomelist_bounce_relays} &&
+         %{$pms->{conf}->{welcomelist_bounce_relays}} ? 1 : 0;
 }
 
-sub check_whitelist_bounce_relays {
+sub check_welcomelist_bounce_relays {
   my ($self, $pms) = @_;
 
   return 0  if !$self->have_any_bounce_relays($pms);
@@ -111,7 +115,7 @@ sub check_whitelist_bounce_relays {
   foreach my $line (@{$body}) {
     next unless ($line =~ /^[> ]*Received:/i);
     while ($line =~ / (\S+\.\S+) /g) {
-      return 1 if $self->_relay_is_in_whitelist_bounce_relays($pms, $1);
+      return 1 if $self->_relay_is_in_welcomelist_bounce_relays($pms, $1);
     }
   }
 
@@ -143,18 +147,19 @@ sub check_whitelist_bounce_relays {
 
     next unless ($fullhdr =~ /^[> ]*Received:/i);
     while ($fullhdr =~ /\s(\S+\.\S+)\s/gs) {
-      return 1 if $self->_relay_is_in_whitelist_bounce_relays($pms, $1);
+      return 1 if $self->_relay_is_in_welcomelist_bounce_relays($pms, $1);
     }
   }
 
   return 0;
 }
+*check_whitelist_bounce_relays = \&check_welcomelist_bounce_relays; # removed in 4.1
 
-sub _relay_is_in_whitelist_bounce_relays {
+sub _relay_is_in_welcomelist_bounce_relays {
   my ($self, $pms, $relay) = @_;
   return 1 if $self->_relay_is_in_list(
-        $pms->{conf}->{whitelist_bounce_relays}, $pms, $relay);
-  dbg("rules: relay $relay doesn't match any whitelist");
+        $pms->{conf}->{welcomelist_bounce_relays}, $pms, $relay);
+  dbg("rules: relay $relay doesn't match any welcomelist");
 
   return 0;
 }
@@ -167,7 +172,7 @@ sub _relay_is_in_list {
   if (defined $list->{$relay}) { return 1; }
 
   foreach my $regexp (values %{$list}) {
-    if ($relay =~ qr/$regexp/i) {
+    if ($relay =~ $regexp) {
       dbg("rules: relay $relay matches regexp: $regexp");
       return 1;
     }
index 232290937c9152bf8b2c4c3d9558689f9f69cc2c..9e97e2b663ef9fdb112a202b0799ea2dff6709b8 100644 (file)
@@ -40,83 +40,100 @@ sub new {
   bless ($self, $class);
 
   # the important bit!
-  $self->register_eval_rule("check_from_in_blacklist");
-  $self->register_eval_rule("check_to_in_blacklist");
-  $self->register_eval_rule("check_to_in_whitelist");
-  $self->register_eval_rule("check_to_in_more_spam");
-  $self->register_eval_rule("check_to_in_all_spam");
-  $self->register_eval_rule("check_from_in_list");
-  $self->register_eval_rule("check_replyto_in_list");
-  $self->register_eval_rule("check_to_in_list");
-  $self->register_eval_rule("check_from_in_whitelist");
-  $self->register_eval_rule("check_forged_in_whitelist");
-  $self->register_eval_rule("check_from_in_default_whitelist");
-  $self->register_eval_rule("check_forged_in_default_whitelist");
-  $self->register_eval_rule("check_mailfrom_matches_rcvd");
-  $self->register_eval_rule("check_uri_host_listed");
-  # same as: eval:check_uri_host_listed('BLACK') :
-  $self->register_eval_rule("check_uri_host_in_blacklist");
-  # same as: eval:check_uri_host_listed('WHITE') :
-  $self->register_eval_rule("check_uri_host_in_whitelist");
+  $self->register_eval_rule("check_from_in_blocklist", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_from_in_blacklist", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS); #Stub - Remove in SA 4.1
+  $self->register_eval_rule("check_to_in_blocklist", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_to_in_blacklist", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS); #Stub - Remove in SA 4.1
+  $self->register_eval_rule("check_to_in_welcomelist", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_to_in_whitelist", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS); #Stub - Remove in SA 4.1
+  $self->register_eval_rule("check_to_in_more_spam", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_to_in_all_spam", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_from_in_list", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_replyto_in_list", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_to_in_list", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_from_in_welcomelist", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_from_in_whitelist", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS); #Stub - Remove in SA 4.1
+  $self->register_eval_rule("check_forged_in_welcomelist", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_forged_in_whitelist", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS); #Stub - Remove in SA 4.1
+  $self->register_eval_rule("check_from_in_default_welcomelist", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_from_in_default_whitelist", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS); #Stub - Remove in SA 4.1
+  $self->register_eval_rule("check_forged_in_default_welcomelist", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_forged_in_default_whitelist", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS); #Stub - Remove in SA 4.1
+  $self->register_eval_rule("check_mailfrom_matches_rcvd", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule("check_uri_host_listed", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  # same as: eval:check_uri_host_listed('BLOCK') :
+  $self->register_eval_rule("check_uri_host_in_blocklist"); # type does not matter
+  $self->register_eval_rule("check_uri_host_in_blacklist"); # type does not matter #Stub - Remove in SA 4.1
+  # same as: eval:check_uri_host_listed('WELCOME') :
+  $self->register_eval_rule("check_uri_host_in_welcomelist"); # type does not matter
+  $self->register_eval_rule("check_uri_host_in_whitelist"); # type does not matter #Stub - Remove in SA 4.1
 
   return $self;
 }
 
-sub check_from_in_blacklist {
+sub check_from_in_blocklist {
   my ($self, $pms) = @_;
   foreach ($pms->all_from_addrs()) {
-    if ($self->_check_whitelist ($self->{main}->{conf}->{blacklist_from}, $_)) {
+    if ($self->_check_welcomelist ($self->{main}->{conf}->{blocklist_from}, $_)) {
       return 1;
     }
   }
+  return 0;
 }
+*check_from_in_blacklist = \&check_from_in_blocklist; # removed in 4.1
 
-sub check_to_in_blacklist {
+sub check_to_in_blocklist {
   my ($self, $pms) = @_;
   foreach ($pms->all_to_addrs()) {
-    if ($self->_check_whitelist ($self->{main}->{conf}->{blacklist_to}, $_)) {
+    if ($self->_check_welcomelist ($self->{main}->{conf}->{blocklist_to}, $_)) {
       return 1;
     }
   }
+  return 0;
 }
+*check_to_in_blacklist = \&check_to_in_blocklist; # removed in 4.1
 
-sub check_to_in_whitelist {
+sub check_to_in_welcomelist {
   my ($self, $pms) = @_;
   foreach ($pms->all_to_addrs()) {
-    if ($self->_check_whitelist ($self->{main}->{conf}->{whitelist_to}, $_)) {
+    if ($self->_check_welcomelist ($self->{main}->{conf}->{welcomelist_to}, $_)) {
       return 1;
     }
   }
+  return 0;
 }
+*check_to_in_whitelist = \&check_to_in_welcomelist; # removed in 4.1
 
 sub check_to_in_more_spam {
   my ($self, $pms) = @_;
   foreach ($pms->all_to_addrs()) {
-    if ($self->_check_whitelist ($self->{main}->{conf}->{more_spam_to}, $_)) {
+    if ($self->_check_welcomelist ($self->{main}->{conf}->{more_spam_to}, $_)) {
       return 1;
     }
   }
+  return 0;
 }
 
 sub check_to_in_all_spam {
   my ($self, $pms) = @_;
   foreach ($pms->all_to_addrs()) {
-    if ($self->_check_whitelist ($self->{main}->{conf}->{all_spam_to}, $_)) {
+    if ($self->_check_welcomelist ($self->{main}->{conf}->{all_spam_to}, $_)) {
       return 1;
     }
   }
+  return 0;
 }
 
 sub check_from_in_list {
   my ($self, $pms, $list) = @_;
-  my $list_ref = $self->{main}{conf}{$list};
+  my $list_ref = $pms->{conf}->{$list};
   unless (defined $list_ref) {
     warn "eval: could not find list $list";
-    return;
+    return 0;
   }
 
   foreach my $addr ($pms->all_from_addrs()) {
-    if ($self->_check_whitelist ($list_ref, $addr)) {
+    if ($self->_check_welcomelist ($list_ref, $addr)) {
       return 1;
     }
   }
@@ -126,16 +143,16 @@ sub check_from_in_list {
 
 sub check_replyto_in_list {
   my ($self, $pms, $list) = @_;
-  my $list_ref = $self->{main}{conf}{$list};
+  my $list_ref = $pms->{conf}->{$list};
   unless (defined $list_ref) {
     warn "eval: could not find list $list";
-    return;
+    return 0;
   }
 
   my $replyto = $pms->get("Reply-To:addr");
   return 0  if $replyto eq '';
 
-  if ($self->_check_whitelist ($list_ref, $replyto)) {
+  if ($self->_check_welcomelist ($list_ref, $replyto)) {
     return 1;
   }
 
@@ -163,14 +180,14 @@ sub check_wb_list {
 
 sub check_to_in_list {
   my ($self,$pms,$list) = @_;
-  my $list_ref = $self->{main}{conf}{$list};
+  my $list_ref = $pms->{conf}->{$list};
   unless (defined $list_ref) {
     warn "eval: could not find list $list";
-    return;
+    return 0;
   }
 
   foreach my $addr ($pms->all_to_addrs()) {
-    if ($self->_check_whitelist ($list_ref, $addr)) {
+    if ($self->_check_welcomelist ($list_ref, $addr)) {
       return 1;
     }
   }
@@ -179,46 +196,51 @@ sub check_to_in_list {
 }
 
 ###########################################################################
+#
 
-sub check_from_in_whitelist {
+sub check_from_in_welcomelist {
   my ($self, $pms) = @_;
-  $self->_check_from_in_whitelist($pms) unless exists $pms->{from_in_whitelist};
-  return ($pms->{from_in_whitelist} > 0);
+  $self->_check_from_in_welcomelist($pms) unless exists $pms->{from_in_welcomelist};
+  return ($pms->{from_in_welcomelist} > 0);
 }
+*check_from_in_whitelist = \&check_from_in_welcomelist; # removed in 4.1
 
-sub check_forged_in_whitelist {
+sub check_forged_in_welcomelist {
   my ($self, $pms) = @_;
-  $self->_check_from_in_whitelist($pms) unless exists $pms->{from_in_whitelist};
-  $self->_check_from_in_default_whitelist($pms) unless exists $pms->{from_in_default_whitelist};
-  return ($pms->{from_in_whitelist} < 0) && ($pms->{from_in_default_whitelist} == 0);
+  $self->_check_from_in_welcomelist($pms) unless exists $pms->{from_in_welcomelist};
+  $self->_check_from_in_default_welcomelist($pms) unless exists $pms->{from_in_default_welcomelist};
+  return ($pms->{from_in_welcomelist} < 0) && ($pms->{from_in_default_welcomelist} == 0);
 }
+*check_forged_in_whitelist = \&check_forged_in_welcomelist; # removed in 4.1
 
-sub check_from_in_default_whitelist {
+sub check_from_in_default_welcomelist {
   my ($self, $pms) = @_;
-  $self->_check_from_in_default_whitelist($pms) unless exists $pms->{from_in_default_whitelist};
-  return ($pms->{from_in_default_whitelist} > 0);
+  $self->_check_from_in_default_welcomelist($pms) unless exists $pms->{from_in_default_welcomelist};
+  return ($pms->{from_in_default_welcomelist} > 0);
 }
+*check_from_in_default_whitelist = \&check_from_in_default_welcomelist; # removed in 4.1
 
-sub check_forged_in_default_whitelist {
+sub check_forged_in_default_welcomelist {
   my ($self, $pms) = @_;
-  $self->_check_from_in_default_whitelist($pms) unless exists $pms->{from_in_default_whitelist};
-  $self->_check_from_in_whitelist($pms) unless exists $pms->{from_in_whitelist};
-  return ($pms->{from_in_default_whitelist} < 0) && ($pms->{from_in_whitelist} == 0);
+  $self->_check_from_in_default_welcomelist($pms) unless exists $pms->{from_in_default_welcomelist};
+  $self->_check_from_in_welcomelist($pms) unless exists $pms->{from_in_welcomelist};
+  return ($pms->{from_in_default_welcomelist} < 0) && ($pms->{from_in_welcomelist} == 0);
 }
+*check_forged_in_default_whitelist = \&check_forged_in_default_welcomelist; # removed in 4.1
 
 ###########################################################################
 
-sub _check_from_in_whitelist {
+sub _check_from_in_welcomelist {
   my ($self, $pms) = @_;
   my $found_match = 0;
   foreach ($pms->all_from_addrs()) {
-    if ($self->_check_whitelist ($self->{main}->{conf}->{whitelist_from}, $_)) {
-      $pms->{from_in_whitelist} = 1;
+    if ($self->_check_welcomelist ($self->{main}->{conf}->{welcomelist_from}, $_)) {
+      $pms->{from_in_welcomelist} = 1;
       return;
     }
-    my $wh = $self->_check_whitelist_rcvd ($pms, $self->{main}->{conf}->{whitelist_from_rcvd}, $_);
+    my $wh = $self->_check_welcomelist_rcvd ($pms, $self->{main}->{conf}->{welcomelist_from_rcvd}, $_);
     if ($wh == 1) {
-      $pms->{from_in_whitelist} = 1;
+      $pms->{from_in_welcomelist} = 1;
       return;
     }
     elsif ($wh == -1) {
@@ -226,19 +248,19 @@ sub _check_from_in_whitelist {
     }
   }
 
-  $pms->{from_in_whitelist} = $found_match;
+  $pms->{from_in_welcomelist} = $found_match;
   return;
 }
 
 ###########################################################################
 
-sub _check_from_in_default_whitelist {
+sub _check_from_in_default_welcomelist {
   my ($self, $pms) = @_;
   my $found_match = 0;
   foreach ($pms->all_from_addrs()) {
-    my $wh = $self->_check_whitelist_rcvd ($pms, $self->{main}->{conf}->{def_whitelist_from_rcvd}, $_);
+    my $wh = $self->_check_welcomelist_rcvd ($pms, $self->{main}->{conf}->{def_welcomelist_from_rcvd}, $_);
     if ($wh == 1) {
-      $pms->{from_in_default_whitelist} = 1;
+      $pms->{from_in_default_welcomelist} = 1;
       return;
     }
     elsif ($wh == -1) {
@@ -246,7 +268,7 @@ sub _check_from_in_default_whitelist {
     }
   }
 
-  $pms->{from_in_default_whitelist} = $found_match;
+  $pms->{from_in_default_welcomelist} = $found_match;
   return;
 }
 
@@ -305,9 +327,9 @@ sub _check_addr_matches_rcvd {
 
 ###########################################################################
 
-# look up $addr and trusted relays in a whitelist with rcvd
+# look up $addr and trusted relays in a welcomelist with rcvd
 # note if it appears to be a forgery and $addr is not in any-relay list
-sub _check_whitelist_rcvd {
+sub _check_welcomelist_rcvd {
   my ($self, $pms, $list, $addr) = @_;
 
   # we can only match this if we have at least 1 trusted or untrusted header
@@ -318,7 +340,7 @@ sub _check_whitelist_rcvd {
   if ($pms->{num_relays_untrusted} > 0) {
     @relays = $pms->{relays_untrusted}->[0];
   }
-  # then try the trusted ones; the user could have whitelisted a trusted
+  # then try the trusted ones; the user could have welcomelisted a trusted
   # relay, totally permitted
   # but do not do this if any untrusted relays, to avoid forgery -- bug 4425
   if ($pms->{num_relays_trusted} > 0 && !$pms->{num_relays_untrusted} ) {
@@ -327,13 +349,13 @@ sub _check_whitelist_rcvd {
 
   $addr = lc $addr;
   my $found_forged = 0;
-  foreach my $white_addr (keys %{$list}) {
-    my $regexp = qr/$list->{$white_addr}{re}/i;
-    foreach my $domain (@{$list->{$white_addr}{domain}}) {
-      # $domain is a second param in whitelist_from_rcvd: a domain name or an IP address
+  foreach my $welcome_addr (keys %{$list}) {
+    my $regexp = $list->{$welcome_addr}{re};
+    foreach my $domain (@{$list->{$welcome_addr}{domain}}) {
+      # $domain is a second param in welcomelist_from_rcvd: a domain name or an IP address
       
       if ($addr =~ $regexp) {
-        # From or sender address matching the first param in whitelist_from_rcvd
+        # From or sender address matching the first param in welcomelist_from_rcvd
         my $match;
         foreach my $lastunt (@relays) {
           local($1,$2);
@@ -346,7 +368,7 @@ sub _check_whitelist_rcvd {
               # relay's IP address not provided or unparseable
 
             } elsif ($wl_ip  =~ /^\d+\.\d+\.\d+\.\d+\z/s) {
-              # an IPv4 whitelist entry can only be matched by an IPv4 relay
+              # an IPv4 welcomelist entry can only be matched by an IPv4 relay
               if ($wl_ip eq $rly_ip) { $match = 1; last }  # exact match
 
             } elsif ($wl_ip =~ /^[\d\.]+\z/s) {  # an IPv4 classful subnet?
@@ -359,12 +381,12 @@ sub _check_whitelist_rcvd {
                 dbg("rules: bad IP address in relay: %s, sender: %s",
                     $rly_ip, $addr);
               } else {
-                my $wl_ip_obj = NetAddr::IP->new($wl_ip); # whitelist 2nd param
+                my $wl_ip_obj = NetAddr::IP->new($wl_ip); # welcomelist 2nd param
                 if (!defined $wl_ip_obj) {
-                  info("rules: bad IP address in whitelist: %s", $wl_ip);
+                  info("rules: bad IP address in welcomelist: %s", $wl_ip);
                 } elsif ($wl_ip_obj->contains($rly_ip_obj)) {
                   # note: an IPv4-compatible IPv6 address can match an IPv4 addr
-                  dbg("rules: relay addr %s matches whitelist %s, sender: %s",
+                  dbg("rules: relay addr %s matches welcomelist %s, sender: %s",
                       $rly_ip, $wl_ip_obj, $addr);
                   $match = 1; last;
                 } else {
@@ -380,8 +402,8 @@ sub _check_whitelist_rcvd {
           }
         }
         if ($match) {
-          dbg("rules: address %s matches (def_)whitelist_from_rcvd %s %s",
-              $addr, $list->{$white_addr}{re}, $domain);
+          dbg("rules: address %s matches (def_)welcomelist_from_rcvd %s %s",
+              $addr, $list->{$welcome_addr}{re}, $domain);
           return 1;
         }
         # found address match but no relay match. note as possible forgery
@@ -390,9 +412,9 @@ sub _check_whitelist_rcvd {
     }
   }
   if ($found_forged) { # might be forgery. check if in list of exempted
-    my $wlist = $self->{main}->{conf}->{whitelist_allows_relays};
-    foreach my $fuzzy_addr (values %{$wlist}) {
-      if ($addr =~ /$fuzzy_addr/i) {
+    my $wlist = $pms->{conf}->{welcomelist_allows_relays};
+    foreach my $regexp (values %{$wlist}) {
+      if ($addr =~ $regexp) {
         $found_forged = 0;
         last;
       }
@@ -403,14 +425,13 @@ sub _check_whitelist_rcvd {
 
 ###########################################################################
 
-sub _check_whitelist {
+sub _check_welcomelist {
   my ($self, $list, $addr) = @_;
   $addr = lc $addr;
   if (defined ($list->{$addr})) { return 1; }
-  study $addr;  # study is a no-op since perl 5.16.0, eliminating related bugs
   foreach my $regexp (values %{$list}) {
-    if ($addr =~ qr/$regexp/i) {
-      dbg("rules: address $addr matches whitelist or blacklist regexp: $regexp");
+    if ($addr =~ $regexp) {
+      dbg("rules: address $addr matches welcomelist or blocklist regexp: $regexp");
       return 1;
     }
   }
@@ -420,15 +441,17 @@ sub _check_whitelist {
 
 ###########################################################################
 
-sub check_uri_host_in_blacklist {
+sub check_uri_host_in_blocklist {
   my ($self, $pms) = @_;
-  $self->check_uri_host_listed($pms, 'BLACK');
+  $self->check_uri_host_listed($pms, 'BLOCK');
 }
+*check_uri_host_in_blacklist = \&check_uri_host_in_blocklist; # removed in 4.1
 
-sub check_uri_host_in_whitelist {
+sub check_uri_host_in_welcomelist {
   my ($self, $pms) = @_;
-  $self->check_uri_host_listed($pms, 'WHITE');
+  $self->check_uri_host_listed($pms, 'WELCOME');
 }
+*check_uri_host_in_whitelist = \&check_uri_host_in_welcomelist; # removed in 4.1
 
 sub check_uri_host_listed {
   my ($self, $pms, $subname) = @_;
@@ -451,7 +474,7 @@ sub _check_uri_host_listed {
     return $pms->{'uri_host_enlisted'};  # just provide a cached result
   }
 
-  my $uri_lists_href = $self->{main}{conf}{uri_host_lists};
+  my $uri_lists_href = $pms->{conf}->{uri_host_lists};
   if (!$uri_lists_href || !%$uri_lists_href) {
     $pms->{'uri_host_enlisted'} = {};  # no URI host lists
     return $pms->{'uri_host_enlisted'};
diff --git a/upstream/lib/Mail/SpamAssassin/Plugin/WelcomeListSubject.pm b/upstream/lib/Mail/SpamAssassin/Plugin/WelcomeListSubject.pm
new file mode 100644 (file)
index 0000000..c5eb4b9
--- /dev/null
@@ -0,0 +1,164 @@
+# <@LICENSE>
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to you under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at:
+# 
+#     http://www.apache.org/licenses/LICENSE-2.0
+# 
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# </@LICENSE>
+
+=head1 NAME
+
+Mail::SpamAssassin::Plugin::WelcomeListSubject - welcomelist by Subject header
+
+=head1 SYNOPSIS
+
+ loadplugin Mail::SpamAssassin::Plugin::WelcomeListSubject
+
+ header SUBJECT_IN_WELCOMELIST eval:check_subject_in_welcomelist()
+ header SUBJECT_IN_BLOCKLIST eval:check_subject_in_blocklist()
+
+ score SUBJECT_IN_WELCOMELIST -100
+ score SUBJECT_IN_BLOCKLIST 100
+
+ welcomelist_subject [Bug *]
+ blocklist_subject Make Money Fast
+
+=head1 DESCRIPTION
+
+This SpamAssassin plugin module provides eval tests for welcomelisting and
+blocklisting particular strings in the Subject header. String will match
+anywhere in the subject. The value for welcomelist_subject or blocklist_subject
+are strings which may contain file -glob -style patterns, similar to the
+other welcomelist_* config options. Note that each subject/string must be a
+separate *_subject command, all whitespace is included in the string.
+
+=cut
+
+package Mail::SpamAssassin::Plugin::WelcomeListSubject;
+
+use Mail::SpamAssassin::Plugin;
+use Mail::SpamAssassin::Util qw(compile_regexp);
+use strict;
+use warnings;
+# use bytes;
+use re 'taint';
+
+our @ISA = qw(Mail::SpamAssassin::Plugin);
+
+# constructor: register the eval rule
+sub new {
+  my $class = shift;
+  my $mailsaobject = shift;
+
+  $class = ref($class) || $class;
+  my $self = $class->SUPER::new($mailsaobject);
+  bless ($self, $class);
+
+  $self->register_eval_rule ("check_subject_in_welcomelist", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule ("check_subject_in_whitelist", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS); # removed in 4.1
+  $self->register_eval_rule ("check_subject_in_blocklist", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
+  $self->register_eval_rule ("check_subject_in_blacklist", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS); # removed in 4.1
+
+  $self->set_config($mailsaobject->{conf});
+
+  return $self;
+}
+
+sub set_config {
+  my ($self, $conf) = @_;
+
+  my @cmds;
+
+  push(@cmds, {
+              setting => 'welcomelist_subject',
+              aliases => ['whitelist_subject'], # removed in 4.1
+              default => {},
+               type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST,
+              code => sub {
+                my ($self, $key, $value, $line) = @_;
+
+                $value = lc $value;
+                my $re = $value;
+                $re =~ s/([^\*\?_a-zA-Z0-9])/\\$1/g;        # escape any possible metachars
+                $re =~ tr/?/./;                             # "?" -> "."
+                 $re =~ s/\*+/\.\*/g;                        # "*" -> "any string"
+                 my ($rec, $err) = compile_regexp($re, 0);
+                 if (!$rec) {
+                   warn "could not compile $key '$value': $err";
+                   return;
+                 }
+                $conf->{$key}->{$value} = $rec;
+              }});
+
+  push(@cmds, {
+              setting => 'blocklist_subject',
+              aliases => ['blacklist_subject'], # removed in 4.1
+              default => {},
+               type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST,
+              code => sub {
+                my ($self, $key, $value, $line) = @_;
+
+                $value = lc $value;
+                my $re = $value;
+                $re =~ s/([^\*\?_a-zA-Z0-9])/\\$1/g;        # escape any possible metachars
+                $re =~ tr/?/./;                             # "?" -> "."
+                 $re =~ s/\*+/\.\*/g;                        # "*" -> "any string"
+                 my ($rec, $err) = compile_regexp($re, 0);
+                 if (!$rec) {
+                   warn "could not compile $key '$value': $err";
+                   return;
+                 }
+                $conf->{$key}->{$value} = $rec;
+              }});
+
+  $conf->{parser}->register_commands(\@cmds);
+}
+
+sub check_subject_in_welcomelist {
+  my ($self, $permsgstatus) = @_;
+
+  my $subject = $permsgstatus->get('Subject');
+
+  return 0 unless $subject ne '';
+
+  return $self->_check_subject($permsgstatus->{conf}->{welcomelist_subject}, $subject);
+}
+*check_subject_in_whitelist = \&check_subject_in_welcomelist; # removed in 4.1
+
+sub check_subject_in_blocklist {
+  my ($self, $permsgstatus) = @_;
+
+  my $subject = $permsgstatus->get('Subject');
+
+  return 0 unless $subject ne '';
+
+  return $self->_check_subject($permsgstatus->{conf}->{blocklist_subject}, $subject);
+}
+*check_subject_in_blacklist = \&check_subject_in_blocklist; # removed in 4.1
+
+sub _check_subject {
+  my ($self, $list, $subject) = @_;
+
+  $subject = lc $subject;
+
+  return 1 if defined($list->{$subject});
+
+  foreach my $regexp (values %{$list}) {
+    if ($subject =~ $regexp) {
+      return 1;
+    }
+  }
+
+  return 0;
+}
+
+1;
diff --git a/upstream/lib/Mail/SpamAssassin/Plugin/WhiteListSubject.pm b/upstream/lib/Mail/SpamAssassin/Plugin/WhiteListSubject.pm
deleted file mode 100644 (file)
index 6f1fd91..0000000
+++ /dev/null
@@ -1,150 +0,0 @@
-# <@LICENSE>
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements.  See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to you under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License.  You may obtain a copy of the License at:
-# 
-#     http://www.apache.org/licenses/LICENSE-2.0
-# 
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-# </@LICENSE>
-
-=head1 NAME
-
-Mail::SpamAssassin::Plugin::WhiteListSubject - whitelist by Subject header
-
-=head1 SYNOPSIS
-
- loadplugin Mail::SpamAssassin::Plugin::WhiteListSubject
-
- header SUBJECT_IN_WHITELIST eval:check_subject_in_whitelist()
- header SUBJECT_IN_BLACKLIST eval:check_subject_in_blacklist()
-
- score SUBJECT_IN_WHITELIST -100
- score SUBJECT_IN_BLACKLIST 100
-
- whitelist_subject [Bug *]
- blacklist_subject Make Money Fast
-
-=head1 DESCRIPTION
-
-This SpamAssassin plugin module provides eval tests for whitelisting and
-blacklisting particular strings in the Subject header. String will match
-anywhere in the subject. The value for whitelist_subject or blacklist_subject
-are strings which may contain file -glob -style patterns, similar to the
-other whitelist_* config options. Note that each subject/string must be a
-separate *_subject command, all whitespace is included in the string.
-
-=cut
-
-package Mail::SpamAssassin::Plugin::WhiteListSubject;
-
-use Mail::SpamAssassin::Plugin;
-use strict;
-use warnings;
-# use bytes;
-use re 'taint';
-
-our @ISA = qw(Mail::SpamAssassin::Plugin);
-
-# constructor: register the eval rule
-sub new {
-  my $class = shift;
-  my $mailsaobject = shift;
-
-  $class = ref($class) || $class;
-  my $self = $class->SUPER::new($mailsaobject);
-  bless ($self, $class);
-
-  $self->register_eval_rule ("check_subject_in_whitelist");
-  $self->register_eval_rule ("check_subject_in_blacklist");
-
-  $self->set_config($mailsaobject->{conf});
-
-  return $self;
-}
-
-sub set_config {
-  my ($self, $conf) = @_;
-
-  my @cmds;
-
-  push(@cmds, {
-              setting => 'whitelist_subject',
-              default => {},
-               type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST,
-              code => sub {
-                my ($self, $key, $value, $line) = @_;
-
-                $value = lc $value;
-                my $re = $value;
-                $re =~ s/[\000\\\(]/_/gs;                   # paranoia
-                $re =~ s/([^\*\?_a-zA-Z0-9])/\\$1/g;        # escape any possible metachars
-                $re =~ tr/?/./;                             # "?" -> "."
-                 $re =~ s/\*+/\.\*/g;                        # "*" -> "any string"
-                $conf->{$key}->{$value} = ${re};
-              }});
-
-  push(@cmds, {
-              setting => 'blacklist_subject',
-              default => {},
-               type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST,
-              code => sub {
-                my ($self, $key, $value, $line) = @_;
-
-                $value = lc $value;
-                my $re = $value;
-                $re =~ s/[\000\\\(]/_/gs;                   # paranoia
-                $re =~ s/([^\*\?_a-zA-Z0-9])/\\$1/g;        # escape any possible metachars
-                $re =~ tr/?/./;                             # "?" -> "."
-                 $re =~ s/\*+/\.\*/g;                        # "*" -> "any string"
-                $conf->{$key}->{$value} = ${re};
-              }});
-
-  $conf->{parser}->register_commands(\@cmds);
-}
-
-sub check_subject_in_whitelist {
-  my ($self, $permsgstatus) = @_;
-
-  my $subject = $permsgstatus->get('Subject');
-
-  return 0 unless $subject ne '';
-
-  return $self->_check_subject($permsgstatus->{conf}->{whitelist_subject}, $subject);
-}
-
-sub check_subject_in_blacklist {
-  my ($self, $permsgstatus) = @_;
-
-  my $subject = $permsgstatus->get('Subject');
-
-  return 0 unless $subject ne '';
-
-  return $self->_check_subject($permsgstatus->{conf}->{blacklist_subject}, $subject);
-}
-
-sub _check_subject {
-  my ($self, $list, $subject) = @_;
-
-  $subject = lc $subject;
-
-  return 1 if defined($list->{$subject});
-
-  study $subject;  # study is a no-op since perl 5.16.0, eliminating bugs
-  foreach my $regexp (values %{$list}) {
-    if ($subject =~ qr/$regexp/i) {
-      return 1;
-    }
-  }
-
-  return 0;
-}
-
-1;
index 3c74fc4124ab2ab70279e976551938ab0f14115f..71115822d38ced8452c5d1cccf10d262324308fc 100644 (file)
@@ -81,6 +81,12 @@ sub load_plugin {
   }
   $package = Mail::SpamAssassin::Util::untaint_var($package);
 
+  # Bug 7728
+  if ($package eq 'Mail::SpamAssassin::Plugin::HashCash') {
+    warn "plugin: $package is deprecated, remove loadplugin clause from your configuration\n";
+    return;
+  }
+
   # Don't load the same plugin twice!
   # Do this *before* calling ->new(), otherwise eval rules will be
   # registered on a nonexistent object
index e6d7a35bda9765d7bda9ff87fe3f17e2fe6a4917..eabb710b022902cce8941d2308f9c95df1911f9e 100644 (file)
@@ -33,10 +33,8 @@ use re 'taint';
 our @ISA = qw();
 
 use Mail::SpamAssassin::Logger;
+use Mail::SpamAssassin::Util qw(idn_to_ascii is_fqdn_valid);
 use Mail::SpamAssassin::Constants qw(:ip);
-use Mail::SpamAssassin::Util qw(is_fqdn_valid);
-
-my $IP_ADDRESS = IP_ADDRESS;
 
 # called from SpamAssassin->init() to create $self->{util_rb}
 sub new {
@@ -89,7 +87,7 @@ our %US_STATES = qw(
 
 =over 4
 
-=item ($hostname, $domain) = split_domain ($fqdn)
+=item ($hostname, $domain) = split_domain ($fqdn, $is_ascii)
 
 Cut a fully-qualified hostname into the hostname part and the domain
 part, splitting at the DNS registry boundary.
@@ -99,11 +97,20 @@ Examples:
     "www.foo.com" => ( "www", "foo.com" )
     "www.foo.co.uk" => ( "www", "foo.co.uk" )
 
+If $is_ascii given and true, skip idn_to_ascii() conversion
+
 =cut
 
 sub split_domain {
-  my $self = shift;
-  my $domain = lc shift;
+  my ($self, $domain, $is_ascii) = @_;
+
+  if ($is_ascii) {
+    utf8::encode($domain)  if utf8::is_utf8($domain); # force octets
+    $domain = lc $domain;
+  } else {
+    # convert to ascii, handles Unicode dot normalization also
+    $domain = idn_to_ascii($domain);
+  }
 
   my $hostname = '';
 
@@ -120,16 +127,11 @@ sub split_domain {
     my @hostname;
 
     while (@domparts > 1) { # go until we find the TLD
-      if (@domparts == 4) {
-        if ($domparts[3] eq 'us' &&
-            (($domparts[0] eq 'pvt' && $domparts[1] eq 'k12') ||
-             ($domparts[0] =~ /^c[io]$/)))
-        {
-          # http://www.neustar.us/policies/docs/rfc_1480.txt
-          # "Fire-Dept.CI.Los-Angeles.CA.US"
-          # "<school-name>.PVT.K12.<state>.US"
-          last if ($US_STATES{$domparts[2]});
-        }
+      if (@domparts == 2) {
+        # co.uk, etc.
+        my $temp = join(".", @domparts);
+        # International domain names in ASCII-compatible encoding (ACE)
+        last if ($self->{conf}->{two_level_domains}{$temp});
       }
       elsif (@domparts == 3) {
         # http://www.neustar.us/policies/docs/rfc_1480.txt
@@ -141,13 +143,20 @@ sub split_domain {
         }
         else {
           my $temp = join(".", @domparts);
+          # International domain names in ASCII-compatible encoding (ACE)
           last if ($self->{conf}->{three_level_domains}{$temp});
         }
       }
-      elsif (@domparts == 2) {
-        # co.uk, etc.
-        my $temp = join(".", @domparts);
-        last if ($self->{conf}->{two_level_domains}{$temp});
+      elsif (@domparts == 4) {
+        if ($domparts[3] eq 'us' &&
+            (($domparts[0] eq 'pvt' && $domparts[1] eq 'k12') ||
+             ($domparts[0] =~ /^c[io]$/)))
+        {
+          # http://www.neustar.us/policies/docs/rfc_1480.txt
+          # "Fire-Dept.CI.Los-Angeles.CA.US"
+          # "<school-name>.PVT.K12.<state>.US"
+          last if ($US_STATES{$domparts[2]});
+        }
       }
       push(@hostname, shift @domparts);
     }
@@ -167,7 +176,7 @@ sub split_domain {
 
 ###########################################################################
 
-=item $domain = trim_domain($fqdn)
+=item $domain = trim_domain($fqdn, $is_ascii)
 
 Cut a fully-qualified hostname into the hostname part and the domain
 part, returning just the domain.
@@ -177,39 +186,51 @@ Examples:
     "www.foo.com" => "foo.com"
     "www.foo.co.uk" => "foo.co.uk"
 
+If $is_ascii given and true, skip idn_to_ascii() conversion
+
 =cut
 
 sub trim_domain {
-  my $self = shift;
-  my $domain = shift;
+  my ($self, $domain, $is_ascii) = @_;
 
-  my ($host, $dom) = $self->split_domain($domain);
+  my (undef, $dom) = $self->split_domain($domain, $is_ascii);
   return $dom;
 }
 
 ###########################################################################
 
-=item $ok = is_domain_valid($dom)
+=item $ok = is_domain_valid($dom, $is_ascii)
 
-Return C<1> if the domain is valid, C<undef> otherwise.  A valid domain
-(a) does not contain whitespace, (b) contains at least one dot, and (c)
-uses a valid TLD or ccTLD.
+Return C<1> if the domain/hostname uses valid known TLD, C<undef> otherwise.
+
+If $is_ascii given and true, skip idn_to_ascii() conversion.
+
+Note that this only checks the TLD validity and nothing else.  To verify
+that the complete fqdn is in a valid legal format, Util::is_fqdn_valid() can
+additionally be used.
 
 =back
 
 =cut
 
 sub is_domain_valid {
-  my ($self, $dom) = @_;
+  my ($self, $dom, $is_ascii) = @_;
 
   return 0 unless defined $dom;
+  if ($is_ascii) {
+    utf8::encode($dom)  if utf8::is_utf8($dom); # force octets
+    $dom = lc $dom;
+  } else {
+    # convert to ascii, handles Unicode dot normalization also
+    $dom = idn_to_ascii($dom);
+  }
 
   # domains don't have whitespace
   return 0 if ($dom =~ /\s/);
 
   # ensure it ends in a known-valid TLD, and has at least 1 dot
   return 0 unless ($dom =~ /\.([^.]+)$/);
-  return 0 unless ($self->{conf}->{valid_tlds}{lc $1});
+  return 0 unless exists $self->{conf}->{valid_tlds}{$1};
 
   return 1;     # nah, it's ok.
 }
@@ -245,20 +266,20 @@ sub uri_to_domain {
   # we'll see the decoded version as well.  see url_encode()
   return if $uri =~ /\%(?:2[1-9a-f]|[3-6][0-9a-f]|7[0-9a-e])/;
 
-  my $host = $uri;  # unstripped/full domain name
+  my $host = idn_to_ascii($uri);  # unstripped/full domain name
   my $domain = $host;
 
   # keep IPs intact
-  if ($host !~ /^$IP_ADDRESS$/) { 
+  if ($host !~ IS_IP_ADDRESS) {
     # check that it's a valid hostname/fqdn
-    return unless is_fqdn_valid($host);
+    return unless is_fqdn_valid($host, 1);
     # ignore invalid TLDs
-    return unless $self->is_domain_valid($host);
+    return unless $self->is_domain_valid($host, 1);
     # get rid of hostname part of domain, understanding delegation
-    $domain = $self->trim_domain($host);
+    $domain = $self->trim_domain($host, 1);
   }
   
-  # $uri is now the domain only, optionally return unstripped host name
+  # optionally return unstripped host name
   return !wantarray ? $domain : ($domain, $host);
 }
 
index 278f7924ed5c49251fe05ee10fe30b534ba1c05c..bc2dfa43d82c1e85eedcf7794368b21d6672c29c 100644 (file)
@@ -17,7 +17,7 @@
 
 =head1 NAME
 
-Mail::SpamAssassin::SQLBasedAddrList - SpamAssassin SQL Based Auto Whitelist
+Mail::SpamAssassin::SQLBasedAddrList - SpamAssassin SQL Based Auto Welcomelist
 
 =head1 SYNOPSIS
 
@@ -38,7 +38,7 @@ A SQL based persistent address list implementation.
 See C<Mail::SpamAssassin::PersistentAddrList> for more information.
 
 Uses DBI::DBD module access to your favorite database (tested with
-MySQL, SQLite and PostgreSQL) to store user auto-whitelists.
+MySQL, SQLite and PostgreSQL) to store user auto-welcomelists.
 
 The default table structure looks like this:
 CREATE TABLE awl (
@@ -48,6 +48,7 @@ CREATE TABLE awl (
   msgcount int(11) NOT NULL default '0',
   totscore float NOT NULL default '0',
   signedby varchar(255) NOT NULL default '',
+  last_hit timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
   PRIMARY KEY (username,email,signedby,ip)
 ) TYPE=MyISAM;
 
@@ -79,7 +80,7 @@ use warnings;
 use re 'taint';
 
 # Do this silliness to stop RPM from finding DBI as required
-BEGIN { require DBI;  import DBI; }
+BEGIN { require DBI;  DBI->import; }
 
 use Mail::SpamAssassin::PersistentAddrList;
 use Mail::SpamAssassin::Logger;
@@ -123,7 +124,7 @@ sub new_checker {
 
   if (!$main->{conf}->{user_awl_dsn} ||
       !$main->{conf}->{user_awl_sql_table}) {
-    dbg("auto-whitelist: sql-based invalid config");
+    dbg("auto-welcomelist: sql-based invalid config");
     return;
   }
 
@@ -134,12 +135,12 @@ sub new_checker {
   my $dbh = DBI->connect($dsn, $dbuser, $dbpass, {'PrintError' => 0});
 
   if(!$dbh) {
-    info("auto-whitelist: sql-based unable to connect to database (%s) : %s",
+    info("auto-welcomelist: sql-based unable to connect to database (%s) : %s",
          $dsn, DBI::errstr);
     return;
   }
 
-  dbg("auto-whitelist: sql-based connected to $dsn");
+  dbg("auto-welcomelist: sql-based connected to $dsn");
 
   $self = { 'main'      => $main,
             'dsn'       => $dsn,
@@ -161,9 +162,9 @@ sub new_checker {
     }
   }
   $self->{_with_awl_signer} =
-    $main->{conf}->{auto_whitelist_distinguish_signed};
+    $main->{conf}->{auto_welcomelist_distinguish_signed};
 
-  dbg("auto-whitelist: sql-based using username: ".$self->{_username});
+  dbg("auto-welcomelist: sql-based using username: ".$self->{_username});
 
   return bless ($self, $class);
 }
@@ -219,11 +220,19 @@ sub get_addr_entry {
     push(@args, @signedby);
   }
   $sql .= " ORDER BY last_hit";
+
   my $sth = $self->{dbh}->prepare($sql);
+
+  unless (defined($sth)) {
+    info("auto-welcomelist: sql-based get_addr_entry %s: SQL prepare error: %s",
+         join('|',@args), $self->{dbh}->errstr);
+    return $entry;
+  }
+
   my $rc = $sth->execute($self->{_username}, @args);
 
   if (!$rc) { # there was an error, but try to go on
-    info("auto-whitelist: sql-based get_addr_entry %s: SQL error: %s",
+    info("auto-welcomelist: sql-based get_addr_entry %s: SQL error: %s",
          join('|',@args), $sth->errstr);
     $entry->{msgcount} = 0;
     $entry->{totscore} = 0;
@@ -241,13 +250,14 @@ sub get_addr_entry {
       $entry->{exists_p} = 1;
       $cnt++;
     }
-    dbg("auto-whitelist: sql-based get_addr_entry: %s for %s",
+    dbg("auto-welcomelist: sql-based get_addr_entry: %s for %s",
         $cnt ? "found $cnt entries" : 'no entries found',
         join('|',@args) );
   }
   $sth->finish();
 
-  dbg("auto-whitelist: sql-based %s scores %s, msgcount %s",
+  # tests t/sql_based_w*.t look for this dbg line in this format
+  dbg("auto-welcomelist: sql-based %s scores %.1f, msgcount %s",
       join('|',@args), $entry->{totscore}, $entry->{msgcount});
 
   return $entry;
@@ -296,26 +306,48 @@ sub add_score {
     my @args = ($self->{_username}, $email, $ip, 1, $score);
     my $sql = sprintf("INSERT INTO %s (%s) VALUES (%s)", $self->{tablename},
                       join(',', @fields),  join(',', ('?') x @fields));
+    if ($self->{dsn} =~ /^DBI:(?:pg|SQLite)/i) {
+       $sql .= " ON CONFLICT (username, email, signedby, ip) DO UPDATE set msgcount = ?, totscore = totscore + ?";
+    } elsif ($self->{dsn} =~ /^DBI:(?:mysql|MariaDB)/i) {
+       $sql .= " ON DUPLICATE KEY UPDATE msgcount = ?, totscore = totscore + ?";
+    }
+
     my $sth = $self->{dbh}->prepare($sql);
 
+    unless (defined($sth)) {
+      info("auto-welcomelist: sql-based add_score/insert %s: SQL prepare error: %s",
+           join('|',@args), $self->{dbh}->errstr);
+      return $entry;
+    }
+
     if (!$self->{_with_awl_signer}) {
-      my $rc = $sth->execute(@args);
+      my $rc;
+      if ($self->{dsn} =~ /^DBI:(?:pg|SQLite|mysql|MariaDB)/i) {
+          $rc = $sth->execute(@args, $entry->{msgcount}, $score);
+      } else {
+          $rc = $sth->execute(@args);
+      }
       if (!$rc) {
-        dbg("auto-whitelist: sql-based add_score/insert %s: SQL error: %s",
+        dbg("auto-welcomelist: sql-based add_score/insert %s: SQL error: %s",
              join('|',@args), $sth->errstr);
       } else {
-        dbg("auto-whitelist: sql-based add_score/insert ".
+        dbg("auto-welcomelist: sql-based add_score/insert ".
             "score %s: %s", $score, join('|',@args));
         $inserted = 1; $entry->{exists_p} = 1;
       }
     } else {
       for my $s (@signedby) {
-        my $rc = $sth->execute(@args, $s);
+        my $rc;
+       if ($self->{dsn} =~ /^DBI:(?:pg|SQLite|mysql|MariaDB)/i) {
+          $rc = $sth->execute(@args, $s, $entry->{msgcount}, $score);
+       } else {
+         $rc = $sth->execute(@args, $s);
+       }
         if (!$rc) {
-          dbg("auto-whitelist: sql-based add_score/insert %s: SQL error: %s",
+          dbg("auto-welcomelist: sql-based add_score/insert %s: SQL error: %s",
               join('|',@args,$s), $sth->errstr);
         } else {
-          dbg("auto-whitelist: sql-based add_score/insert ".
+          dbg("auto-welcomelist: sql-based add_score/insert ".
               "score %s: %s", $score, join('|',@args,$s));
           $inserted = 1; $entry->{exists_p} = 1;
         }
@@ -323,7 +355,7 @@ sub add_score {
     }
   }
 
-  if (!$inserted) {
+  if (!$inserted && $self->{dsn} !~ /^DBI:(?:pg|SQLite|mysql|MariaDB)/i) {
     # insert failed, assume primary key constraint, so try the update
 
     my $sql = "UPDATE $self->{tablename} ".
@@ -345,19 +377,26 @@ sub add_score {
     push(@args, $ip);
 
     my $sth = $self->{dbh}->prepare($sql);
+
+    unless (defined($sth)) {
+      info("auto-welcomelist: sql-based add_score/update %s: SQL prepare error: %s",
+           join('|',@args), $self->{dbh}->errstr);
+      return $entry;
+    }
+
     my $rc = $sth->execute(@args);
-    
+
     if (!$rc) {
-      info("auto-whitelist: sql-based add_score/update %s: SQL error: %s",
+      info("auto-welcomelist: sql-based add_score/update %s: SQL error: %s",
            join('|',@args), $sth->errstr);
     } else {
-      dbg("auto-whitelist: sql-based add_score/update ".
+      dbg("auto-welcomelist: sql-based add_score/update ".
           "new msgcount: %s, new totscore: %s for %s",
           $entry->{msgcount}, $entry->{totscore}, join('|',@args));
       $entry->{exists_p} = 1;
     }
   }
-  
+
   return $entry;
 }
 
@@ -385,12 +424,12 @@ sub remove_entry {
   # when $ip is equal to none then attempt to delete all entries
   # associated with address
   if ($ip eq 'none') {
-    dbg("auto-whitelist: sql-based remove_entry: removing all entries matching $email");
+    dbg("auto-welcomelist: sql-based remove_entry: removing all entries matching $email");
   }
   else {
     $sql .= " AND ip = ?";
     push(@args, $ip);
-    dbg("auto-whitelist: sql-based remove_entry: removing single entry matching ".$entry->{addr});
+    dbg("auto-welcomelist: sql-based remove_entry: removing single entry matching ".$entry->{addr});
   }
   # if a key 'signedby' exists in the $entry, be selective on its value too
   my $signedby = $entry->{signedby};
@@ -405,10 +444,17 @@ sub remove_entry {
   }
 
   my $sth = $self->{dbh}->prepare($sql);
+
+  unless (defined($sth)) {
+    info("auto-welcomelist: sql-based remove_entry %s: SQL prepare error: %s",
+         join('|',@args), $self->{dbh}->errstr);
+    return;
+  }
+
   my $rc = $sth->execute(@args);
 
   if (!$rc) {
-    info("auto-whitelist: sql-based remove_entry %s: SQL error: %s",
+    info("auto-welcomelist: sql-based remove_entry %s: SQL error: %s",
          join('|',@args), $sth->errstr);
   }
   else {
@@ -429,7 +475,7 @@ This method provides the necessary cleanup for the address list.
 
 sub finish {
   my ($self) = @_;
-  dbg("auto-whitelist: sql-based finish: disconnected from " . $self->{dsn});
+  dbg("auto-welcomelist: sql-based finish: disconnected from " . $self->{dsn});
   $self->{dbh}->disconnect();
 }
 
@@ -438,7 +484,7 @@ sub finish {
 private instance (String, String) _unpack_addr(string $addr)
 
 Description:
-This method splits an autowhitelist address into it's two components,
+This method splits an autowelcomelist address into it's two components,
 email and ip address.
 
 =cut
@@ -449,7 +495,7 @@ sub _unpack_addr {
   my ($email, $ip) = split(/\|ip=/, $addr);
 
   unless ($email && $ip) {
-    dbg("auto-whitelist: sql-based _unpack_addr: unable to decode $addr");
+    dbg("auto-welcomelist: sql-based _unpack_addr: unable to decode $addr");
   }
 
   return ($email, $ip);
index 8dccbbc97eec37e096b1438dca2b7ce749f6459d..404eff24f9cf591f8d9584395fd29c7de9103049 100644 (file)
@@ -24,6 +24,7 @@ use warnings;
 # use bytes;
 use re 'taint';
 use Errno qw();
+use Scalar::Util qw(blessed);
 
 use Mail::SpamAssassin::Util qw(am_running_on_windows);
 use Mail::SpamAssassin::Logger;
@@ -528,6 +529,9 @@ sub wait_for_child_to_accept {
       return;
     }
     else {
+      if( Scalar::Util::blessed($self->{server_fh}[0]) eq 'IO::Socket::SSL' ) {
+        warn "prefork: SSL connection protocol error";
+      }
       warn "prefork: ordered child $kid to accept, but they reported state '$state', killing rogue";
       $self->child_error_kill($kid, $sock);
       $self->adapt_num_children();
index f3a6649c990a2cc5f1f5abeb7fa0a27fe6fa03f5..3bc5d341ecb13000a2c8ea5b4260e08755183ac3 100644 (file)
@@ -45,30 +45,32 @@ use warnings;
 # use bytes;
 use re 'taint';
 
-require 5.008001;  # needs utf8::is_utf8()
-
 use Mail::SpamAssassin::Logger;
 
+use version 0.77;
 use Exporter ();
 
 our @ISA = qw(Exporter);
 our @EXPORT = ();
-our @EXPORT_OK = qw(&local_tz &base64_decode &untaint_var &untaint_file_path
-                  &exit_status_str &proc_status_ok &am_running_on_windows
-                  &reverse_ip_address &decode_dns_question_entry &touch_file
-                  &get_my_locales &parse_rfc822_date &get_user_groups
-                  &secure_tmpfile &secure_tmpdir &uri_list_canonicalize
-                  &compile_regexp &qr_to_string &is_fqdn_valid);
+our @EXPORT_OK = qw(&local_tz &base64_decode &base64_encode &base32_encode
+                  &untaint_var &untaint_file_path &exit_status_str
+                  &proc_status_ok &am_running_on_windows &reverse_ip_address
+                  &decode_dns_question_entry &touch_file &secure_tmpfile
+                  &secure_tmpdir &uri_list_canonicalize &get_my_locales
+                  &parse_rfc822_date &idn_to_ascii &is_valid_utf_8
+                  &get_user_groups &compile_regexp &qr_to_string
+                  &is_fqdn_valid &parse_header_addresses &force_die
+                  &domain_to_search_list);
 
 our $AM_TAINTED;
 
 use Config;
+use Encode;
 use IO::Handle;
 use File::Spec;
 use File::Basename;
 use Time::Local;
-use Sys::Hostname (); # don't import hostname() into this namespace!
-use NetAddr::IP 4.000;
+use Scalar::Util qw(tainted);
 use Fcntl;
 use Errno qw(ENOENT EACCES EEXIST);
 use POSIX qw(:sys_wait_h WIFEXITED WIFSIGNALED WIFSTOPPED WEXITSTATUS
@@ -76,12 +78,14 @@ use POSIX qw(:sys_wait_h WIFEXITED WIFSIGNALED WIFSTOPPED WEXITSTATUS
 
 ###########################################################################
 
+use constant HAS_NETADDR_IP => eval { require NetAddr::IP; };
 use constant HAS_MIME_BASE64 => eval { require MIME::Base64; };
-use constant RUNNING_ON_WINDOWS => ($^O =~ /^(?:mswin|dos|os2)/oi);
+use constant RUNNING_ON_WINDOWS => ($^O =~ /^(?:mswin|dos|os2)/i);
 
 # These are only defined as stubs on Windows (see bugs 6798 and 6470).
 BEGIN {
   if (RUNNING_ON_WINDOWS) {
+    require Win32;
     no warnings 'redefine';
 
     # See the section on $? at
@@ -96,6 +100,45 @@ BEGIN {
 
 ###########################################################################
 
+our $ALT_FULLSTOP_UTF8_RE;
+BEGIN {
+  # Bug 6751:
+  # RFC 3490 (IDNA): Whenever dots are used as label separators, the
+  #   following characters MUST be recognized as dots: U+002E (full stop),
+  #   U+3002 (ideographic full stop), U+FF0E (fullwidth full stop),
+  #   U+FF61 (halfwidth ideographic full stop).
+  # RFC 5895: [...] the IDEOGRAPHIC FULL STOP character (U+3002)
+  #   can be mapped to the FULL STOP before label separation occurs.
+  #   [...] Only the IDEOGRAPHIC FULL STOP character (U+3002) is added in
+  #   this mapping because the authors have not fully investigated [...]
+  # Adding also 'SMALL FULL STOP' (U+FE52) as seen in the wild,
+  # and a 'ONE DOT LEADER' (U+2024).
+  #
+  no bytes;  # make sure there is no 'use bytes' in effect
+  my $dot_chars = "\x{2024}\x{3002}\x{FF0E}\x{FF61}\x{FE52}";  # \x{002E}
+  my $dot_bytes = join('|', split(//,$dot_chars));  utf8::encode($dot_bytes);
+  $ALT_FULLSTOP_UTF8_RE = qr/$dot_bytes/s;
+}
+
+###########################################################################
+
+our ($have_libidn, $have_libidn2);
+BEGIN {
+  my $sa_libidn = ($ENV{'SA_LIBIDN'}||'') =~ /(\d+)/ ? $1 : 0;
+  if (!$sa_libidn || $sa_libidn eq '2') {
+    eval { require Net::LibIDN2; } and do { $have_libidn2 = 1; };
+  }
+  if (!$have_libidn2 && (!$sa_libidn || $sa_libidn eq '1')) {
+    eval { require Net::LibIDN; } and do { $have_libidn = 1; };
+  }
+}
+
+$have_libidn||$have_libidn2
+  or info("util: module Net::LibIDN or Net::LibIDN2 not available, ".
+          "internationalized domain names with U-labels will not be recognized!");
+
+###########################################################################
+
 # find an executable in the current $PATH (or whatever for that platform)
 {
   # Show the PATH we're going to explore only once.
@@ -108,15 +151,28 @@ BEGIN {
     if ( !$displayed_path++ ) {
       dbg("util: current PATH is: ".join($Config{'path_sep'},File::Spec->path()));
     }
+
+    my @pathext = ('');
+    if (RUNNING_ON_WINDOWS) {
+      if ( $ENV{PATHEXT} ) {
+        push @pathext, split($Config{'path_sep'}, $ENV{PATHEXT});
+      } else {
+        push @pathext, qw{.exe .com .bat};
+      }
+    }
+
     foreach my $path (File::Spec->path()) {
-      my $fname = File::Spec->catfile ($path, $filename);
-      if ( -f $fname ) {
-        if (-x $fname) {
-          dbg("util: executable for $filename was found at $fname");
-          return $fname;
-        }
-        else {
-          dbg("util: $filename was found at $fname, but isn't executable");
+      my $base = File::Spec->catfile ($path, $filename);
+      for my $ext ( @pathext ) {
+        my $fname = $base.$ext;
+        if ( -f $fname ) {
+          if (-x $fname) {
+            dbg("util: executable for $filename was found at $fname");
+            return $fname;
+          }
+          else {
+            dbg("util: $filename was found at $fname, but isn't executable");
+          }
         }
       }
     }
@@ -138,12 +194,13 @@ BEGIN {
     dbg("util: taint mode: deleting unsafe environment variables, resetting PATH");
 
     if (RUNNING_ON_WINDOWS) {
-      dbg("util: running on Win32, skipping PATH cleaning");
-      return;
+      if ( $ENV{'PATHEXT'} ) { # clean and untaint
+        $ENV{'PATHEXT'} = join($Config{'path_sep'}, grep ($_, map( {$_ =~ m/^(\.[a-zA-Z]{1,10})$/; $1; } split($Config{'path_sep'}, $ENV{'PATHEXT'}))));
+      }
+    } else {
+      delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
     }
 
-    delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
-
     # Go through and clean the PATH out
     my @path;
     my @stat;
@@ -168,8 +225,8 @@ BEGIN {
        dbg("util: PATH included '$dir', which isn't a directory, dropping");
        next;
       }
-      elsif (($stat[2]&2) != 0) {
-        # World-Writable directories are considered insecure.
+      elsif (!RUNNING_ON_WINDOWS && (($stat[2]&2) != 0)) {
+        # World-Writable directories are considered insecure, but unavoidable on Windows
         # We could be more paranoid and check all of the parent directories as well,
         # but it's good for now.
        dbg("util: PATH included '$dir', which is world writable, dropping");
@@ -223,7 +280,7 @@ sub am_running_on_windows {
 ###########################################################################
 
 # untaint a path to a file, e.g. "/home/jm/.spamassassin/foo",
-# "C:\Program Files\SpamAssassin\tmp\foo", "/home/��t/etc".
+# "C:\Program Files\SpamAssassin\tmp\foo", "/home/õüt/etc".
 #
 # TODO: this does *not* handle locales well.  We cannot use "use locale"
 # and \w, since that will not detaint the data.  So instead just allow the
@@ -240,8 +297,8 @@ sub untaint_file_path {
   # Barry Jaspan: allow ~ and spaces, good for Windows.
   # Also return '' if input is '', as it is a safe path.
   # Bug 7264: allow also parenthesis, e.g. "C:\Program Files (x86)"
-  my $chars = '-_A-Za-z0-9.%=+,/:()\\@\\xA0-\\xFF\\\\';
-  my $re = qr{^\s*([$chars][${chars}~ ]*)\z}o;
+  my $chars = '-_A-Za-z0-9.#%=+,/:()\\@\\xA0-\\xFF\\\\';
+  my $re = qr{^\s*([$chars][${chars}~ ]*)\z};
 
   if ($path =~ $re) {
     $path = $1;
@@ -310,10 +367,13 @@ sub untaint_var {
           ${$arg}{untaint_var($k)} = untaint_var($v);
         }
       } else {
-        # hash keys are never tainted,
-        # although old version of perl had some quirks there
-        while (my($k, $v) = each %{$arg}) {
-          ${$arg}{untaint_var($k)} = untaint_var($v);
+        if($] < 5.020) {
+          # hash keys are never tainted,
+          # although old version of perl had some quirks there
+          # skip the check only for Perl > 5.020 to be on the safe side
+          while (my($k, $v) = each %{$arg}) {
+            ${$arg}{untaint_var($k)} = untaint_var($v);
+          }
         }
       }
       return %{$arg} if wantarray;
@@ -346,13 +406,22 @@ sub taint_var {
 ###########################################################################
 
 # Check for full hostname / FQDN / DNS name validity.  IP addresses must be
-# validated with other functions like $IP_ADDRESS.  Does not check for valid
-# TLD, use $self->{main}->{registryboundaries}->is_domain_valid()
-# additionally for that.
+# validated with other functions like Constants::IP_ADDRESS.  Does not check
+# for valid TLD, use $self->{main}->{registryboundaries}->is_domain_valid()
+# additionally for that.  If $is_ascii given and true, skip idn_to_ascii()
+# conversion.
 sub is_fqdn_valid {
-  my ($host) = @_;
+  my ($host, $is_ascii) = @_;
   return if !defined $host;
 
+  if ($is_ascii) {
+    utf8::encode($host)  if utf8::is_utf8($host); # force octets
+    $host = lc $host;
+  } else {
+    # convert to ascii, handles Unicode dot normalization also
+    $host = idn_to_ascii($host);
+  }
+
   # remove trailing dots
   $host =~ s/\.+\z//;
 
@@ -360,7 +429,7 @@ sub is_fqdn_valid {
   return if length($host) > 253;
 
   # validate dot separated components/labels
-  my @labels = split(/\./, lc $host);
+  my @labels = split(/\./, $host);
   my $cnt = scalar @labels;
   return unless $cnt > 1; # at least two labels required
   foreach my $label (@labels) {
@@ -369,6 +438,7 @@ sub is_fqdn_valid {
     return if length($label) > 63;
     # alphanumeric, - allowed only in middle part
     # underscores are allowed in DNS queries, so we allow here
+    # (idn_to_ascii made sure we are lowercase and pure ascii)
     return if $label !~ /^[a-z0-9_](?:[a-z0-9_-]*[a-z0-9_])?$/;
     # 1st-2nd level part can not contain _, only third+ can
     if ($cnt == 2 || $cnt == 1) {
@@ -383,6 +453,140 @@ sub is_fqdn_valid {
 
 ###########################################################################
 
+# returns true if the provided string of octets represents a syntactically
+# valid UTF-8 string, otherwise a false is returned
+#
+sub is_valid_utf_8 {
+# my $octets = $_[0];
+  return undef if !defined $_[0]; ## no critic (ProhibitExplicitReturnUndef)
+  #
+  # RFC 6532: UTF8-non-ascii = UTF8-2 / UTF8-3 / UTF8-4
+  # RFC 3629 section 4: Syntax of UTF-8 Byte Sequences
+  #   UTF8-char   = UTF8-1 / UTF8-2 / UTF8-3 / UTF8-4
+  #   UTF8-1      = %x00-7F
+  #   UTF8-2      = %xC2-DF UTF8-tail
+  #   UTF8-3      = %xE0 %xA0-BF UTF8-tail /
+  #                 %xE1-EC 2( UTF8-tail ) /
+  #                 %xED %x80-9F UTF8-tail /
+  #                   # U+D800..U+DFFF are utf16 surrogates, not legal utf8
+  #                 %xEE-EF 2( UTF8-tail )
+  #   UTF8-4      = %xF0 %x90-BF 2( UTF8-tail ) /
+  #                 %xF1-F3 3( UTF8-tail ) /
+  #                 %xF4 %x80-8F 2( UTF8-tail )
+  #   UTF8-tail   = %x80-BF
+  #
+  # loose variant:
+  #   [\x00-\x7F] | [\xC0-\xDF][\x80-\xBF] |
+  #   [\xE0-\xEF][\x80-\xBF]{2} | [\xF0-\xF4][\x80-\xBF]{3}
+  #
+  $_[0] =~ /^ (?: [\x00-\x7F] |
+                  [\xC2-\xDF] [\x80-\xBF] |
+                  \xE0 [\xA0-\xBF] [\x80-\xBF] |
+                  [\xE1-\xEC] [\x80-\xBF]{2} |
+                  \xED [\x80-\x9F] [\x80-\xBF] |
+                  [\xEE-\xEF] [\x80-\xBF]{2} |
+                  \xF0 [\x90-\xBF] [\x80-\xBF]{2} |
+                  [\xF1-\xF3] [\x80-\xBF]{3} |
+                  \xF4 [\x80-\x8F] [\x80-\xBF]{2} )* \z/xs ? 1 : 0;
+}
+
+# Given an international domain name with U-labels (UTF-8 or Unicode chars)
+# converts it to ASCII-compatible encoding (ACE).  If the argument is in
+# ASCII (or is an invalid IDN), returns it lowercased but otherwise unchanged.
+# The result is always in octets (utf8 flag off) even if the argument was in
+# Unicode characters.
+#
+#my $idn_cache = {};
+sub idn_to_ascii {
+  no bytes;  # make sure there is no 'use bytes' in effect
+  return undef  if !defined $_[0]; ## no critic (ProhibitExplicitReturnUndef)
+  my $s = "$_[0]";  # stringify
+
+  # encode chars to UTF-8, leave octets unchanged (not necessarily valid UTF-8)
+  utf8::encode($s)  if utf8::is_utf8($s); # i.e. remove utf-8 flag if set
+
+  # Rapid return for most common case, all-ASCII (including IP address literal),
+  # no conversion needed. Also if we don't have LibIDN, nothing more we can do.
+  if ($s !~ tr/a-zA-Z0-9_.:[]-//c || !($have_libidn||$have_libidn2)) {
+    return lc $s; # retains taintedness
+  }
+
+  #if (exists $idn_cache->{$s}) {
+  #  dbg("util: idn_to_ascii: converted to ACE: '$s' -> '$idn_cache->{$s}' (cached)");
+  #  return $idn_cache->{$s};
+  #}
+  #$idn_cache = {} if %$idn_cache > 1000;
+  #my $orig_s = $s; # save original for idn_cache
+
+  # propagate taintedness of the argument
+  my $t = tainted($s);
+  if ($t) {  # untaint $s, avoids taint-related bugs in LibIDN or in old perl
+    $s = untaint_var($s);
+  }
+
+  my $charset;
+
+  # Check for valid UTF-8
+  if (is_valid_utf_8($s)) {
+    # RFC 3490 (IDNA): Whenever dots are used as label separators, the
+    # following characters MUST be recognized as dots: U+002E (full stop),
+    # U+3002 (ideographic full stop), U+FF0E (fullwidth full stop),
+    # U+FF61 (halfwidth ideographic full stop).
+    if ($s =~ s/$ALT_FULLSTOP_UTF8_RE/./gs) {
+      dbg("util: idn_to_ascii: alternative dots normalized: '%s' -> '%s'",
+           $_[0], $s);
+    }
+    $charset = 'UTF-8';
+  }
+  # Check for valid extended ISO-8859-1 including diacritics
+  elsif ($s !~ tr/a-zA-Z0-9\xc0-\xd6\xd8-\xde\xe0-\xf6\xf8-\xfe_.-//c) {
+    $charset = 'ISO-8859-1';
+  }
+
+  if ($charset) {
+    # to ASCII-compatible encoding (ACE), lowercased
+    if ($have_libidn) {
+      my $sa = Net::LibIDN::idn_to_ascii($s, $charset);
+      if (!defined $sa) {
+        info("util: idn_to_ascii: conversion to ACE failed: '%s' (charset %s)",
+             $s, $charset);
+      } else {
+        dbg("util: idn_to_ascii: converted to ACE: '%s' -> '%s' (charset %s)",
+            $s, $sa, $charset)  if $s ne $sa;
+        $s = $sa;
+      }
+    } elsif ($have_libidn2) {
+      my $si = $s;
+      if ($charset eq 'ISO-8859-1') {
+        Encode::from_to($si, 'ISO-8859-1', 'UTF-8');
+      }
+      utf8::decode($si) unless utf8::is_utf8($si);
+      my $rc = 0;
+      my $sa = Net::LibIDN2::idn2_to_ascii_8($si,
+                 &Net::LibIDN2::IDN2_NFC_INPUT + &Net::LibIDN2::IDN2_NONTRANSITIONAL,
+                 $rc);
+      if (!defined $sa) {
+        info("util: idn_to_ascii: conversion to ACE failed, %s: '%s' (charset %s) (LibIDN2)",
+             Net::LibIDN2::idn2_strerror($rc), $s, $charset);
+      } else {
+        dbg("util: idn_to_ascii: converted to ACE: '%s' -> '%s' (charset %s) (LibIDN2)",
+            $s, $sa, $charset)  if $s ne $sa;
+        $s = $sa;
+      }
+    }
+  } else {
+    my($package, $filename, $line) = caller;
+    info("util: idn_to_ascii: valid charset not detected: '%s', called from %s line %d",
+         $s, $package, $line);
+    $s = lc $s;  # garbage-in / garbage-out
+  }
+
+  return $t ? taint_var($s) : $s;  # propagate taintedness of the argument
+  #return $idn_cache->{$orig_s} = $t ? taint_var($s) : $s;  # propagate taintedness of the argument
+}
+
+###########################################################################
+
 # map process termination status number to an informative string, and
 # append optional message (dual-valued errno or a string or a number),
 # returning the resulting string
@@ -685,18 +889,17 @@ sub wrap {
   my $pos = 0;
   my $pos_mod = 0;
   while ($#arr > $pos) {
-    my $tmpline = $arr[$pos] ;
-    $tmpline =~ s/\t/        /g;
-    my $len = length ($tmpline);
+    my $len = length($arr[$pos]);
+    $len += ($arr[$pos] =~ tr/\t//) * 7; # add tab lengths
+
     # if we don't want to have lines > $length (overflow==0), we
     # need to verify what will happen with the next line.  if we don't
     # care if a single line goes longer, don't care about the next
     # line.
     # we also want this to be true for the first entry on the line
     if ($pos_mod != 0 && $overflow == 0) {
-      my $tmpnext = $arr[$pos+1] ;
-      $tmpnext =~ s/\t/        /g;
-      $len += length ($tmpnext);
+      $len += length($arr[$pos+1]);
+      $len += ($arr[$pos+1] =~ tr/\t//) * 7; # add tab lengths
     }
 
     if ($len <= $length) {
@@ -786,6 +989,7 @@ sub qp_decode {
 
   # RFC 2045 explicitly prohibits lowercase characters a-f in QP encoding
   # do we really want to allow them???
+
   local $1;
   $str =~ s/=([0-9a-fA-F]{2})/chr(hex($1))/ge;
 
@@ -796,7 +1000,7 @@ sub base64_encode {
   local $_ = shift;
 
   if (HAS_MIME_BASE64) {
-    return MIME::Base64::encode_base64($_);
+    return MIME::Base64::encode_base64($_,'');
   }
 
   $_ = pack("u57", $_);
@@ -806,6 +1010,27 @@ sub base64_encode {
   return $_;
 }
 
+# Very basic Base32 encoder
+our %base32_bitchr = (
+  '00000'=>'A', '00001'=>'B', '00010'=>'C', '00011'=>'D', '00100'=>'E',
+  '00101'=>'F', '00110'=>'G', '00111'=>'H', '01000'=>'I', '01001'=>'J',
+  '01010'=>'K', '01011'=>'L', '01100'=>'M', '01101'=>'N', '01110'=>'O',
+  '01111'=>'P', '10000'=>'Q', '10001'=>'R', '10010'=>'S', '10011'=>'T',
+  '10100'=>'U', '10101'=>'V', '10110'=>'W', '10111'=>'X', '11000'=>'Y',
+  '11001'=>'Z', '11010'=>'2', '11011'=>'3', '11100'=>'4', '11101'=>'5',
+  '11110'=>'6', '11111'=>'7'
+);
+sub base32_encode {
+  my ($str) = @_;
+  return if !defined $str;
+  utf8::encode($str)  if utf8::is_utf8($str); # force octets
+  my $bits = unpack("B*", $str)."0000";
+  my $output;
+  local($1);
+  $output .= $base32_bitchr{$1} while ($bits =~ /(.{5})/g);
+  return $output;
+}
+
 ###########################################################################
 
 sub portable_getpwuid {
@@ -843,6 +1068,13 @@ sub _fake_getpwuid {
   );
 }
 
+###########################################################################
+# Get a platform specific directory for application data
+# Just used for Windows for now
+sub common_application_data_directory {
+  return Win32::GetFolderPath(Win32::CSIDL_COMMON_APPDATA()) if (RUNNING_ON_WINDOWS);
+}
+
 ###########################################################################
 
 # Given a string, extract an IPv4 address from it.  Required, since
@@ -855,11 +1087,11 @@ sub extract_ipv4_addr_from_string {
   return unless defined($str);
 
   if ($str =~ /\b(
-                       (?:1\d\d|2[0-4]\d|25[0-5]|[1-9]\d|\d)\.
-                       (?:1\d\d|2[0-4]\d|25[0-5]|[1-9]\d|\d)\.
-                       (?:1\d\d|2[0-4]\d|25[0-5]|[1-9]\d|\d)\.
-                       (?:1\d\d|2[0-4]\d|25[0-5]|[1-9]\d|\d)
-                     )\b/ix)
+                       (?:1\d\d|2[0-4]\d|25[0-5]|[1-9]\d|\d)\.
+                       (?:1\d\d|2[0-4]\d|25[0-5]|[1-9]\d|\d)\.
+                       (?:1\d\d|2[0-4]\d|25[0-5]|[1-9]\d|\d)\.
+                       (?:1\d\d|2[0-4]\d|25[0-5]|[1-9]\d|\d)
+                     )\b/ix)
   {
     if (defined $1) { return $1; }
   }
@@ -878,7 +1110,8 @@ sub extract_ipv4_addr_from_string {
 # Sys::Hostname thinks our hostname is, might also be a full qualified one)
   sub hostname {
     return $hostname if defined($hostname);
-
+    # Load only when required
+    require Sys::Hostname;
     # Sys::Hostname isn't taint safe and might fall back to `hostname`. So we've
     # got to clean PATH before we may call it.
     clean_path_in_taint_mode();
@@ -893,7 +1126,7 @@ sub extract_ipv4_addr_from_string {
     return $fq_hostname if defined($fq_hostname);
 
     $fq_hostname = hostname();
-    if ($fq_hostname !~ /\./) { # hostname doesn't contain a dot, so it can't be a FQDN
+    if (index($fq_hostname, '.') == -1) { # hostname doesn't contain a dot, so it can't be a FQDN
       my @names = grep(/^\Q${fq_hostname}.\E/o,                         # grep only FQDNs
                     map { split } (gethostbyname($fq_hostname))[0 .. 1] # from all aliases
                   );
@@ -955,10 +1188,10 @@ sub reverse_ip_address {
   local($1,$2,$3,$4);
   if ($ip =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\z/) {
     $revip = "$4.$3.$2.$1";
-  } elsif ($ip !~ /:/ || $ip !~ /^[0-9a-fA-F:.]{2,}\z/) {  # triage
+  } elsif (index($ip, ':') == -1 || $ip !~ /^[0-9a-fA-F:.]{2,}\z/) {  # triage
     # obviously unrecognized syntax
-  } elsif (!NetAddr::IP->can('full6')) {  # since NetAddr::IP 4.010
-    info("util: version of NetAddr::IP is too old, IPv6 not supported");
+  } elsif (!HAS_NETADDR_IP || !NetAddr::IP->can('full6')) {  # since NetAddr::IP 4.010
+    info("util: sufficiently new NetAddr::IP not found, IPv6 not supported");
   } else {
     # looks like an IPv6 address, let NetAddr::IP check the details
     my $ip_obj = NetAddr::IP->new6($ip);
@@ -991,8 +1224,8 @@ sub decode_dns_question_entry {
 
   local $1;
   # Net::DNS provides a query in encoded RFC 1035 zone file format, decode it!
-  $qname =~ s{ \\ ( [0-9]{3} | [^0-9] ) }
-             { length($1)==1 ? $1 : $1 <= 255 ? chr($1) : "\\$1" }xgse;
+  $qname =~ s{ \\ ( [0-9]{3} | (?![0-9]{3}) . ) }
+             { length($1)==3 && $1 <= 255 ? chr($1) : $1 }xgse;
   return ($q->qclass, $q->qtype, $qname);
 }
 
@@ -1005,7 +1238,8 @@ sub parse_content_type {
   # but it happens), MUAs seem to take the last one and so that's what we
   # should do here.
   #
-  my $ct = $_[-1] || 'text/plain; charset=us-ascii';
+  my $missing; # flag missing content-type, even though we force it text/plain
+  my $ct = $_[-1] || do { $missing = 1; 'text/plain; charset=us-ascii' };
 
   # This could be made a bit more rigid ...
   # the actual ABNF, BTW (RFC 1521, section 7.2.1):
@@ -1066,6 +1300,7 @@ sub parse_content_type {
   # bug 4298: If at this point we don't have a content-type, assume text/plain;
   # also, bug 5399: if the content-type *starts* with "text", and isn't in a 
   # list of known bad/non-plain formats, do likewise.
+  $missing = 1 if !$ct; # flag missing content-type
   if (!$ct ||
         ($ct =~ /^text\b/ && $ct !~ /^text\/(?:x-vcard|calendar|html)$/))
   {
@@ -1078,8 +1313,10 @@ sub parse_content_type {
   # Now that the header has been parsed, return the requested information.
   # In scalar context, just the MIME type, in array context the
   # four important data parts (type, boundary, charset, and filename).
+  # Added fifth array member $missing, if caller wants to know ct was
+  # missing/invalid, even though we forced it as text/plain.
   #
-  return wantarray ? ($ct,$boundary,$charset,$name) : $ct;
+  return wantarray ? ($ct,$boundary,$charset,$name,$missing) : $ct;
 }
 
 ###########################################################################
@@ -1188,6 +1425,15 @@ sub touch_file {
 
 ###########################################################################
 
+sub pseudo_random_string {
+  my $len = shift || 6;
+  my $str = '';
+  $str .= (0..9,'A'..'Z','a'..'z')[rand 62] for (1 .. $len);
+  return $str;
+}
+
+###########################################################################
+
 =item my ($filepath, $filehandle) = secure_tmpfile();
 
 Generates a filename for a temporary file, opens it exclusively and
@@ -1212,8 +1458,7 @@ sub secure_tmpfile {
   for (my $retries = 20; $retries > 0; $retries--) {
     # we do not rely on the obscurity of this name for security,
     # we use a average-quality PRG since this is all we need
-    my $suffix = join('', (0..9,'A'..'Z','a'..'z')[rand 62, rand 62, rand 62,
-                                                  rand 62, rand 62, rand 62]);
+    my $suffix = pseudo_random_string(6);
     $reportfile = File::Spec->catfile($tmpdir,".spamassassin${$}${suffix}tmp");
 
     # instead, we require O_EXCL|O_CREAT to guarantee us proper
@@ -1315,7 +1560,23 @@ sub secure_tmpdir {
 
 *uri_list_canonify = \&uri_list_canonicalize;  # compatibility alias
 sub uri_list_canonicalize {
-  my($redirector_patterns, @uris) = @_;
+  my $redirector_patterns = shift;
+
+  my @uris;
+  my $rb;
+  if (ref($_[0]) eq 'ARRAY') {
+    # New call style:
+    # - reference to array of redirector_patterns
+    # - reference to array of URIs
+    # - reference to $self->{main}->{registryboundaries}
+    @uris = @{$_[0]};
+    $rb = $_[1];
+  } else {
+    # Old call style:
+    # - reference to array of redirector_patterns
+    # - rest of the arguments is list of uris
+    @uris = @_;
+  }
 
   # make sure we catch bad encoding tricks
   my @nuris;
@@ -1348,7 +1609,7 @@ sub uri_list_canonicalize {
         push @nuris, $1
       }
       # Address must be trimmed of %20
-      if ($nuri =~ tr/%20// &&
+      if (index($nuri, '%20') >= 0 &&
           $nuri =~ /^(?:mailto:)?(?:\%20)*([^\@]+\@[^?&%]+)/) {
         push @nuris, "mailto:$1";
       }
@@ -1423,22 +1684,12 @@ sub uri_list_canonicalize {
         }
       }
 
-      # Bug 6751:
-      # RFC 3490 (IDNA): Whenever dots are used as label separators, the
-      #   following characters MUST be recognized as dots: U+002E (full stop),
-      #   U+3002 (ideographic full stop), U+FF0E (fullwidth full stop),
-      #   U+FF61 (halfwidth ideographic full stop).
-      # RFC 5895: [...] the IDEOGRAPHIC FULL STOP character (U+3002)
-      #   can be mapped to the FULL STOP before label separation occurs.
-      #   [...] Only the IDEOGRAPHIC FULL STOP character (U+3002) is added in
-      #   this mapping because the authors have not fully investigated [...]
-      # Adding also 'SMALL FULL STOP' (U+FE52) as seen in the wild.
-      # Parhaps also the 'ONE DOT LEADER' (U+2024).
-      if ($host =~ s{(?: \xE3\x80\x82 | \xEF\xBC\x8E | \xEF\xBD\xA1 |
-                         \xEF\xB9\x92 | \xE2\x80\xA4 )}{.}xgs) {
-        push(@nuris, join ('', $proto, $host, $rest));
+      my $nhost = idn_to_ascii($host);
+      if ($nhost ne lc($host)) {
+        push(@nuris, join('', $proto, $nhost, $rest));
         # Also add noport variant
-        push(@nuris, join('', $proto, $host, $rest_noport)) if $rest_noport;
+        push(@nuris, join('', $proto, $nhost, $rest_noport)) if $rest_noport;
+        $host = $nhost;
       }
 
       # bug 4146: deal with non-US ASCII 7-bit chars in the host portion
@@ -1535,9 +1786,12 @@ sub uri_list_canonicalize {
       # (do this here so we don't trip on those 0x123 IPs etc..)
       # https://hg.mozilla.org/mozilla-central/file/tip/docshell/base/nsDefaultURIFixup.cpp
       elsif ($proto eq 'http://' && $auth eq '' &&
-             $host ne 'localhost' && $port eq '80' &&
-             $host =~ /^(?:www\.)?([^.]+)$/) {
-        push(@nuris, join('', $proto, 'www.', $1, '.com', $rest));
+             $nhost ne 'localhost' && $port eq '80' &&
+             $nhost =~ /^(?:www\.)?([^.]+)$/) {
+        # Do not add .com to already valid schemelessly parsed domains (Bug 7891)
+        unless (defined $rb && $rb->is_domain_valid($nhost)) {
+          push(@nuris, join('', $proto, 'www.', $1, '.com', $rest));
+        }
       }
     }
   }
@@ -1622,10 +1876,10 @@ sub receive_date {
 sub get_user_groups {
   my $suid = shift;
   dbg("util: get_user_groups: uid is $suid\n");
-  my ( $user, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell, $expire ) = getpwuid($suid);
-  my $rgids="$gid ";
-  while ( my($name,$pw,$gid,$members) = getgrent() ) {
-    if ( $members =~ m/\b$user\b/ ) {
+  my ($user, $gid) = (getpwuid($suid))[0,3];
+  my $rgids = "$gid ";
+  while (my($name,$gid,$members) = (getgrent())[0,2,3]) {
+    if (grep { $_ eq $user } split(/ /, $members)) {
       $rgids .= "$gid ";
       dbg("util: get_user_groups: added $gid ($name) to group list which is now: $rgids\n");
     }
@@ -1645,16 +1899,25 @@ sub setuid_to_euid {
   my $gids = get_user_groups($touid);
   my ( $pgid, $supgs ) = split (' ',$gids,2);
   defined $supgs or $supgs=$pgid;
-  if ($( != $pgid) {
-    # Gotta be root for any of this to work
-    $> = 0 ;
+  my $prgid = 0 + $(; # bug 8043 - Only set rgid if it isn't already one of the euid's groups
+  if ( ($prgid == 0) or not (grep { $_ == $prgid } split(/ /, ${(}))) {
+    # setgid only works if euid is root, have to set that temporarily
+    $> = 0;
+    if ($> != 0) { warn("util: seteuid to 0 failed: $!"); }
     dbg("util: changing real primary gid from $( to $pgid and supplemental groups to $supgs to match effective uid $touid");
-    POSIX::setgid($pgid);
-    dbg("util: POSIX::setgid($pgid) set errno to $!");  
-    $! = 0;
-    $( = $pgid;
-    $) = "$pgid $supgs";
-    dbg("util: assignment  \$) = $pgid $supgs set errno to $!");  
+    $! = 0; POSIX::setgid($pgid);
+    if ($!) { warn("util: POSIX::setgid $pgid failed: $!\n"); }
+    $! = 0; $( = $pgid;
+    if ($!) { warn("util: failed to set gid $pgid: $!\n"); }
+    $! = 0; $) = "$pgid $supgs";
+    if ($!) {
+      # could be perl 5.30 bug #134169, let's be safe
+      if (grep { $_ eq '0' } split(/ /, ${)})) {
+        die("util: failed to set effective gid $pgid $supgs: $!\n");
+      } else {
+        warn("util: failed to set effective gid $pgid $supgs: $!\n");
+      }
+    }
   }
   if ($< != $touid) {
     dbg("util: changing real uid from $< to match effective uid $touid");
@@ -1684,23 +1947,38 @@ sub helper_app_pipe_open_windows {
   my ($fh, $stdinfile, $duperr2out, @cmdline) = @_;
 
   # use a traditional open(FOO, "cmd |")
+  $cmdline[0] = '"'.$cmdline[0].'"' if ($cmdline[0] !~ /^\".*\"$/);
   my $cmd = join(' ', @cmdline);
   if ($stdinfile) { $cmd .= qq/ < "$stdinfile"/; }
-  if ($duperr2out) { $cmd .= " 2>&1"; }
+  if ($duperr2out) {
+    # Support custom file target for STDERR, if ">file" specified
+    # Caller must make sure the destination is safe and untainted
+    if ($duperr2out =~ /^>/) {
+      $cmd .= " 2$duperr2out";
+    } else {
+      $cmd .= " 2>&1";
+    }
+  }
   return open ($fh, $cmd.'|');
 }
 
 sub force_die {
-  my ($msg) = @_;
+  my ($statrc, $msg) = @_;
 
   # note use of eval { } scope in logging -- paranoia to ensure that a broken
   # $SIG{__WARN__} implementation will not interfere with the flow of control
   # here, where we *have* to die.
-  eval { warn $msg };  # hmm, STDERR may no longer be open
-  eval { dbg("util: force_die: $msg") };
+  if ($msg) {
+    eval { warn $msg };  # hmm, STDERR may no longer be open
+    eval { dbg("util: force_die: $msg") };
+  }
 
-  POSIX::_exit(6);  # avoid END and destructor processing 
-  kill('KILL',$$);  # still kicking? die! 
+  if (am_running_on_windows()) {
+    exit($statrc); # on Windows _exit would terminate parent too BUG 8007
+  } else {
+    POSIX::_exit($statrc);  # avoid END and destructor processing 
+    kill('KILL',$$) if ($statrc);  # somehow this breaks those places that are calling it to exit(0)
+  }
 }
 
 sub helper_app_pipe_open_unix {
@@ -1730,7 +2008,7 @@ sub helper_app_pipe_open_unix {
   eval {
     # go setuid...
     setuid_to_euid();
-    info("util: setuid: ruid=$< euid=$> rgid=$( egid=$) ");
+    dbg("util: setuid: ruid=$< euid=$> rgid=$( egid=$)");
 
     # now set up the fds.  due to some weirdness, we may have to ensure that
     # we *really* close the correct fd number, since some other code may have
@@ -1782,7 +2060,15 @@ sub helper_app_pipe_open_unix {
         POSIX::close(2);
       }
 
-      open (STDERR, ">&STDOUT") or die "dup STDOUT failed: $!";
+      # Support custom file target for STDERR, if ">file" specified
+      # Caller must make sure the destination is safe and untainted
+      my $errout;
+      if ($duperr2out =~ /^>/) {
+        $errout = $duperr2out;
+      } else {
+        $errout = ">&STDOUT";
+      }
+      open (STDERR, $errout) or die "dup $errout failed: $!";
       STDERR->autoflush(1);  # make sure not to lose diagnostics if exec fails
 
       # STDERR must be fd 2 to be useful to subprocesses! (bug 3649)
@@ -1797,7 +2083,7 @@ sub helper_app_pipe_open_unix {
   my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
 
   # bug 4370: we really have to exit here; break any eval traps
-  force_die(sprintf('util: failed to spawn a process "%s": %s',
+  force_die(6, sprintf('util: failed to spawn a process "%s": %s',
                     join(", ",@cmdline), $eval_stat));
   die;  # must be a die() otherwise -w will complain
 }
@@ -1822,12 +2108,20 @@ sub trap_sigalrm_fully {
     $SIG{ALRM} = $handler;
   } else {
     # may be using "safe" signals with %SIG; use POSIX to avoid it
-    POSIX::sigaction POSIX::SIGALRM(), new POSIX::SigAction $handler;
+    POSIX::sigaction POSIX::SIGALRM(), POSIX::SigAction->new($handler);
   }
 }
 
 ###########################################################################
 
+# Bug 6802 helper function, use /aa for perl 5.16+
+my $qr_sa;
+if ($] >= 5.016) {
+  eval '$qr_sa = sub { return qr/$_[0]/aa; }';
+} else {
+  eval '$qr_sa = sub { return qr/$_[0]/; }';
+}
+
 # returns ($compiled_re, $error)
 # if any errors, $compiled_re = undef, $error has string
 # args:
@@ -1839,8 +2133,8 @@ sub compile_regexp {
   local($1);
 
   # Do not allow already compiled regexes or other funky refs
-  if (ref($re)) {
-    return (undef, 'ref passed');
+  if (ref($re) ne '') {
+    return (undef, 'ref passed: '.ref($re));
   }
 
   # try stripping by default
@@ -1886,7 +2180,7 @@ sub compile_regexp {
 
   # paranoid check for eval exec (?{foo}), in case someone
   # actually put "use re 'eval'" somewhere..
-  if ($re =~ /\(\?\??\{/) {
+  if (index($re, '?{') >= 0 && $re =~ /\(\?\??\{/) {
     return (undef, 'eval (?{}) found');
   }
 
@@ -1896,7 +2190,7 @@ sub compile_regexp {
   if ($delim_end && $delim_end !~ tr/\}\)\]//) {
     # first we remove all escaped backslashes "\\"
     my $dbs_stripped = $re;
-    $dbs_stripped =~ s/\\\\//g;
+    $dbs_stripped =~ s/\\\\//g if index($dbs_stripped, '\\\\') >= 0;
     # now we can properly check if something is unescaped
     if ($dbs_stripped =~ /(?<!\\)\Q${delim_end}\E/) {
       return (undef, "unquoted delimiter '$delim_end' found");
@@ -1925,7 +2219,7 @@ sub compile_regexp {
         die "$_[0]\n";
       }
     };
-    $compiled_re = qr/$re/;
+    $compiled_re = $qr_sa->($re);
     1;
   };
   if ($ok && ref($compiled_re) eq 'Regexp') {
@@ -1948,11 +2242,8 @@ sub is_always_matching_regexp {
   elsif ($re =~ /(?<!\\)\|\|/) {
     return "contains '||'";
   }
-  elsif ($re =~ /^\|/) {
-    return "starts with '|'";
-  }
-  elsif ($re =~ /\|(?<!\\\|)$/) {
-    return "ends with '|'";
+  elsif ($re =~ /^\||\|(?<!\\\|)$/) {
+    return "starts or ends with '|'";
   }
 
   return "";
@@ -2055,6 +2346,8 @@ sub make_qr {
 
 ###########################################################################
 
+###########################################################################
+
 sub get_my_locales {
   my ($ok_locales) = @_;
 
@@ -2104,6 +2397,34 @@ sub fisher_yates_shuffle {
 
 ###########################################################################
 
+# Given a domain name, produces a listref of successively stripped down
+# parent domains, e.g. a domain '2.10.Example.COM' would produce a list:
+# '2.10.example.com', '10.example.com', 'example.com', 'com'
+#
+sub domain_to_search_list {
+  my ($domain) = @_;
+
+  $domain =~ s/^\.+//; $domain =~ s/\.+\z//;  # strip leading and trailing dots
+  return [] unless $domain;                   # no domain left
+  return [$domain] if index($domain, '[') == 0; # don't split address literals
+
+  # initialize
+  $domain = lc $domain;
+  my @search_keys = ($domain);
+  my $pos = 0;
+
+  # split domain into search keys
+  while (($pos = index($domain, '.', $pos+1)) != -1) {
+    push @search_keys, substr($domain, $pos+1);
+  }
+
+  # enforce some sanity limit
+  if (@search_keys > 20) {
+    @search_keys = @search_keys[$#search_keys-19 .. $#search_keys];
+  }
+
+  return \@search_keys;
+}
 
 ###########################################################################
 
@@ -2137,6 +2458,330 @@ sub get_tag_value_for_score {
 
 ###########################################################################
 
+# RFC 5322 (+IDN?) parsing of addresses and names from To/From/Cc.. headers
+#
+# Return array of hashes, containing at minimum name,address,user,host
+#
+# Override parser with SA_HEADER_ADDRESS_PARSER environment variable
+
+our $header_address_parser;
+our $email_address_xs;
+our $email_address_xs_fix_address;
+BEGIN {
+  # SA_HEADER_ADDRESS_PARSER=1 only use internal parser
+  # SA_HEADER_ADDRESS_PARSER=2 only use Email::Address::XS
+  # By default internal is preferred, will defer for some cases
+  $header_address_parser = untaint_var($ENV{'SA_HEADER_ADDRESS_PARSER'});
+  if ((!defined $header_address_parser || $header_address_parser eq '2') &&
+       eval 'use Email::Address::XS; 1;') {
+    $email_address_xs = 1;
+    if (version->parse(Email::Address::XS->VERSION) < version->parse(1.02)) {
+      $email_address_xs_fix_address = 1;
+    }
+  }
+}
+
+# Helper for internal parser
+our $header_address_mailre = qr/
+  # user
+  (?:
+    # quoted localpart
+    " (?:|(?:[^"\\]++|\\.)*+) " |
+    # or un-quoted localpart
+    [^\@\s\<\>\(\)\[\]\,\:\;]+
+  )
+  # domain
+  \@ (?: [^\"\s\<\>\(\)\[\]\,\:\;]+ | \[ [\d:.]+ \] )
+/ix;
+
+# Very relaxed internal parser
+# Only handles non-nested comments in some places
+our $header_address_re = qr/^
+  \s*
+  (?:
+    # optional phrase, quoted or non-quoted
+    (?:
+      ( (?: " (?:|(?:[^"\\]++|\\.)*+) " | [^",;<]++ )+ )
+      \s*
+    )?
+    # and enclosed email (or empty)
+    # ... allow whitespace in localpart
+    < \s* ( [^>\@]* \S+ | \s* ) \s* >
+    # some output duplicate enclosures..
+    (?: \s* < \s* (?: (?: " (?:|(?:[^"\\]++|\\.)*+) " )? \S+ | \s* ) \s* > )*
+  |
+    # or standalone email or phrase
+    (?:
+      ( $header_address_mailre ) |
+      ( (?: " (?:|(?:[^"\\]++|\\.)*+) " | [^",;<]++ )+ )
+    )
+  )
+  # possible comment after (no nested support here)
+  (?: \s* \( ( (?:|(?:[^()\\]++|\\.)*+) ) \) )?
+  # Followed by comma (semi-colon sometimes) or finish
+  \s* (?: [,;] | \z )
+/ix;
+
+#
+# Main public function
+# expected input is header contents without Header: itself
+#
+sub parse_header_addresses {
+  my ($str) = @_;
+
+  return if !defined $str || $str !~ /\S/;
+
+  my @results;
+
+  # Internal parser
+  if (!$header_address_parser || $header_address_parser eq '1') {
+    @results = _parse_header_addresses($str);
+  }
+
+  # Email::Address::XS
+  if ($email_address_xs) {
+    if (!$header_address_parser || $header_address_parser eq '2') {
+      # Only consulted if no internal results, or there doesn't
+      # seem to have enough results, or possible nested comments ( (
+      my $maybe_nested = scalar($str =~ /\(/) >= 2;
+      if (!@results || $maybe_nested || @results < scalar($str =~ tr/,//)+1) {
+        my @results_xs = _parse_header_addresses_xs($str);
+        # If we have more results than internal, use it, or nested
+        if (@results_xs > @results || $maybe_nested) {
+          return @results_xs;
+        }
+      }
+    }
+  }
+
+  return @results;
+}
+
+# Check some basic parsing mistakes
+sub _valid_parsed_address {
+  return 0 if !defined $_[0];
+  return 0 if index($_[0], '""@') == 0;
+  return 0 if scalar($_[0] =~ tr/"//) == 1;
+  return 1;
+}
+
+#
+# v0.1, improved internal parser, no support for comments in strange
+# places or nested comments, but handled a large corpus atleast 99% the
+# same as Email::Address::XS and in some cases even better (retains some
+# more name/addr info, even when not fully valid).
+#
+sub _parse_header_addresses {
+  local $_ = shift;
+  local ($1, $2, $3, $4, $5);
+
+  # Clear trailing whitespace
+  s/\s+\z//s;
+
+  # Strip away all escaped blackslashes, simplifies processing a lot
+  s/\\\\//g;
+
+  # Reduce group address
+  s/^[^"()<>]+:\s*(.*?)\s*(?:;.*)?/$1/gs;
+
+  # Skip empty
+  return unless /\S/;
+
+  my @results;
+  while (s/$header_address_re//igs) {
+    my $phrase = defined $1 ? $1 :
+                 defined $4 ? $4 : undef;
+    my $address = defined $2 ? $2 :
+                defined $3 ? $3 : undef;
+    my $comment = defined $5 ? $5 : undef;
+
+    my ($user, $host, $invalid);
+
+    # Check relaxed <> capture
+    if (defined $2) {
+      # Remove comments (no nested support here)
+      $address =~ s/\((?:|(?:[^()\\]++|\\.)*+)\)//gs;
+      # Validate as somewhat email looking
+      if ($address !~ /^$header_address_mailre$/) {
+        $address = undef;
+      }
+    }
+
+    # Validate some other address oddities
+    if (!_valid_parsed_address($address)) {
+      $address = undef;
+    }
+
+    if (defined $phrase) {
+      my $newphrase;
+      # Parse phrase as quoted and unquoted parts
+      while ($phrase =~ /(?:"(|(?:[^"\\]++|\\.)*+)"|([^"]++))/igs) {
+        my $qs = $1;
+        my $nqs = $2;
+        if (defined $qs) {
+          # Unescape things inside quoted string
+          $qs =~ s/\\(?!\\)//g;
+          $qs =~ s/\\\\/\\/g;
+          #$qs =~ s/\\//g;
+          $newphrase .= $qs;
+        } else {
+          # Remove comments (no nested support here)
+          $nqs =~ s/\((?:|(?:[^()\\]++|\\.)*+)\)//gs;
+          $newphrase .= $nqs;
+        }
+      }
+      $phrase = $newphrase;
+
+      # If we only have phrase which looks email, swap when valid
+      # Check all in one if, either swap or don't
+      if (!defined $address &&
+          $phrase =~ /^$header_address_mailre$/i &&
+          _valid_parsed_address($phrase) &&
+          $phrase =~ /^[^\@]*\@([^\@]*)/ &&
+          is_fqdn_valid(idn_to_ascii($1), 1)) {
+        $address = $phrase;
+        $phrase = undef;
+      } else {
+        # Remove redundant phrase==email?
+        if (defined $address && $phrase eq $address) {
+          $phrase = undef;
+        } elsif ($phrase eq '') {
+          $phrase = undef;
+        }
+      }
+    }
+
+    # Copy comment to phrase if not defined
+    if (!defined $phrase && defined $comment) {
+      $phrase = $comment;
+    }
+
+    if (defined $address) {
+      # Unescape quoted localpart
+      #if ($address =~ /^"(.*?)"\@(.*)/) {
+      #  $user = $1;
+      #  $host = $2;
+      #  $user =~ s/\\//g;
+      #  $user =~ s/\s+//gs;
+      #  $address = "$user\@$host";
+      #}
+      # Strip sometimes seen quotes
+      #$address =~ s/^'(.*?)'$/$1/;
+      $address =~ s/^(([^\@]*)\@([^\@]*)).*/$1/;
+      ($user, $host) = ($2, $3);
+    }
+
+    $invalid = !defined $host || !is_fqdn_valid(idn_to_ascii($host), 1);
+    push @results, {
+      'phrase' => $phrase,
+      'user' => $user,
+      'host' => $host,
+      'address' => $address,
+      'comment' => $comment,
+      'invalid' => $invalid
+    };
+  }
+
+  # Was something left unparsed?
+  if (index($_, '@') != -1) {
+    # Last ditch effort, examples:
+    # =?UTF-8?Q?"Foobar"_<noreply@foobar.com>?=
+    # =?utf-8?Q?"Foobar"?=<info=foobar.com@mlsend.com>
+    while (/<($header_address_mailre)>/igs) {
+      my $address = $1;
+      next if !_valid_parsed_address($address);
+      $address =~ s/^(([^\@]*)\@([^\@]*)).*/$1/;
+      my ($user, $host) = ($2, $3);
+      my $invalid = !is_fqdn_valid(idn_to_ascii($host), 1);
+      push @results, {
+        'phrase' => undef,
+        'user' => $user,
+        'host' => $host,
+        'address' => $address,
+        'comment' => undef,
+        'invalid' => $invalid
+      };
+    }
+  }
+
+  return if !@results;
+  return @results;
+}
+
+sub _parse_header_addresses_xs {
+  my ($str) = @_;
+
+  # Strip away all escaped blackslashes, simplifies processing a lot
+  $str =~ s/\\\\//g;
+
+  my @results;
+  my @addrs = Email::Address::XS->parse($str);
+
+  local ($1, $2);
+  foreach my $addr (@addrs) {
+    my $name = $addr->name;
+    my $address = $addr->address;
+    my $user = $addr->user;
+    my $host = $addr->host;
+    my $phrase = $addr->phrase;
+    my $comment = $addr->comment;
+    my $invalid;
+
+    # Workaround Bug 5201 for Email::Address::XS
+    # From: "joe+foobar@example.com"
+    # If everything else is missing but phrase looks like
+    # an email, let's assume it is (hostname verifies)
+    if (!defined $address && !defined $user &&
+        !defined $comment && defined $phrase &&
+        _valid_parsed_address($phrase) &&
+        $phrase =~ /^([^\s\@]+)\@([^\s\@]+)$/ &&
+        is_fqdn_valid(idn_to_ascii($2), 1))
+    {
+      $user = $1;
+      $host = $2;
+      $address = $phrase;
+      $name = $user;
+      $invalid = 0;
+      $phrase = undef;
+    }
+    else {
+      $invalid = !$addr->is_valid;
+    }
+
+    # Version <1.02 borks address if both user+host are UTF-8
+    if ($email_address_xs_fix_address) {
+      if (defined $user && defined $host) {
+        # <"Another User"@foo> loses quotes in user, add back
+        if (index($user, ' ') != -1 &&
+            index($user, '"') == -1) {
+          $user = '"'.$user.'"';
+        }
+        $address = $user.'@'.$host;
+      }
+    }
+
+    # Copy comment to phrase if not defined
+    if (!defined $phrase && defined $comment) {
+      $phrase = $comment;
+    }
+
+    # Use input as name if nothing found
+    if (!defined $phrase && !defined $address) {
+      $phrase = $str;
+    }
+
+    push @results, {
+      'phrase' => $phrase,
+      'user' => $user,
+      'host' => $host,
+      'address' => $address,
+      'comment' => $comment,
+      'invalid' => $invalid
+    };
+  }
+
+  return @results;
+}
 
 1;
 
index e55c8635c7c77a6bdbe8701a37d9be997e64bc78..90f28a41a11b0386d1bdeb59e3ceee37f6824e0d 100644 (file)
@@ -31,10 +31,11 @@ package Mail::SpamAssassin::Util::DependencyInfo;
 
 use strict;
 use warnings;
-# use bytes;
 use re 'taint';
 use version 0.77;
 
+use Mail::SpamAssassin::Util;
+
 our ( $EXIT_STATUS, $WARNINGS );
 
 our @MODULES = (
@@ -52,15 +53,10 @@ our @MODULES = (
 },
 {
   module => 'Net::DNS',
-  version => ($^O =~ /^(mswin|dos|os2)/oi ? '0.46' : '0.34'),
+  version => '0.69',
   desc => 'Used for all DNS-based tests (SBL, XBL, SpamCop, DSBL, etc.),
   perform MX checks, and is also used when manually reporting spam to
-  SpamCop.
-
-  You need to make sure the Net::DNS version is sufficiently up-to-date:
-
-  - version 0.34 or higher on Unix systems
-  - version 0.46 or higher on Windows systems',
+  SpamCop.',
 },
 {
   'module' => 'NetAddr::IP',
@@ -110,7 +106,7 @@ our @OPTIONAL_MODULES = (
   module => 'DB_File',
   version => 0,
   desc => 'Used to store data on-disk, for the Bayes-style logic and
-  auto-whitelist.  *Much* more efficient than the other standard Perl
+  auto-welcomelist.  *Much* more efficient than the other standard Perl
   database packages.  Strongly recommended.',
 },
 {
@@ -119,6 +115,23 @@ our @OPTIONAL_MODULES = (
   version => 0,
   desc => 'Used when manually reporting spam to SpamCop with "spamassassin -r".',
 },
+{
+  module => 'Net::LibIDN2',
+  version => 0,
+  desc => "Newer version of the optional Net::LibIDN module.
+  Provides mapping between Internationalized Domain Names (IDN) in
+  Unicode and ASCII-compatible encoding (ACE) for use in DNS and comparisions.
+  The module is optional, but without it Unicode IDN names found in mail will
+  not be suitable for DNS queries and welcome/blocklisting.",
+},
+{
+  module => 'Net::LibIDN',
+  version => 0,
+  desc => "Provides mapping between Internationalized Domain Names (IDN) in
+  Unicode and ASCII-compatible encoding (ACE) for use in DNS and comparisions.
+  The module is optional, but without it Unicode IDN names found in mail will
+  not be suitable for DNS queries and welcome/blocklisting.",
+},
 {
   module => 'Mail::SPF',
   version => 0,
@@ -126,19 +139,27 @@ our @OPTIONAL_MODULES = (
   address forgery and make it easier to identify spams.',
 },
 {
-  module => 'GeoIP2::Database::Reader',
+  module => 'MaxMind::DB::Reader',
   version => 0,
   desc => 'Used by the RelayCountry plugin (not enabled by default) to
   determine the domain country codes of each relay in the path of an email. 
   Also used by the URILocalBL plugin (not enabled by default) to provide ISP
   and Country code based filtering.',
 },
+{
+  module => 'MaxMind::DB::Reader::XS',
+  version => 0,
+
+  desc => 'Recommended much faster version of the optional MaxMind::DB::Reader module,
+  used by RelayCountry / URILocalBL plugins.',
+},
 {
   module => 'Geo::IP',
   version => 0,
-  desc => 'Used by the RelayCountry plugin (not enabled by default) to determine
-  the domain country codes of each relay in the path of an email.  Also used by 
-  the URILocalBL plugin to provide ISP and Country code based filtering.',
+  desc => 'Used by the RelayCountry plugin (not enabled by default) to
+  determine the domain country codes of each relay in the path of an email. 
+  Also used by the URILocalBL plugin (not enabled by default) to provide ISP
+  and Country code based filtering.',
 },
 {
   module => 'IP::Country::DB_File',
@@ -149,9 +170,12 @@ our @OPTIONAL_MODULES = (
   Country code based filtering.',
 },
 {
-  module => 'Net::CIDR::Lite',
+  module => 'IP::Country::Fast',
   version => 0,
-  desc => 'Used by the URILocalBL plugin to process IP address ranges.',
+  desc => 'Used by the RelayCountry plugin (not enabled by default) to
+  determine the domain country codes of each relay in the path of an email. 
+  Also used by the URILocalBL plugin (not enabled by default) to provide
+  Country code based filtering.',
 },
 {
   module => 'Razor2::Client::Agent',
@@ -165,12 +189,6 @@ our @OPTIONAL_MODULES = (
   More info on installing and using Razor can be found
   at http://wiki.apache.org/spamassassin/InstallingRazor .',
 },
-#{
-# module => 'Net::Ident',
-# version => 0,
-# desc => 'If you plan to use the --auth-ident option to spamd, you will need
-# to install this module.',
-#},
 {
   module => 'IO::Socket::IP',
   version => 0.09,
@@ -184,7 +202,7 @@ our @OPTIONAL_MODULES = (
 {
   module => 'IO::Socket::INET6',
   version => 0,
-  desc => 'This module is an older alternative to IO::Socket::IP.
+  desc => 'This module is a deprecated alternative to IO::Socket::IP.
   Spamd, as well some underlying modules, will fall back to using
   IO::Socket::INET6 if IO::Socket::IP is unavailable. One or the other
   module is required to support IPv6 (e.g. in spamd/spamc protocol,
@@ -227,44 +245,45 @@ our @OPTIONAL_MODULES = (
   your database.',
 },
 {
-  module => 'Getopt::Long',
-  version => '2.32',        # min version was included in 5.8.0, which works
-  desc => 'The "sa-stats.pl" program included in "tools", used to generate
-  summary reports from spamd\'s syslog messages, requires this version
-  of Getopt::Long or newer.',
+  module => 'DBD::SQLite',
+  version => 1.59,
+  desc => 'If you intend to use SpamAssassin with SQLite as the SQL database
+  backend for the DBI module, this is the DBD driver required. Version 1.59_01
+  or later is needed to provide SQLite 3.25.0 or later.',
 },
 {
   module => 'LWP::UserAgent',
   version => 0,
-  desc => 'The "sa-update" program requires this module to make HTTP requests.',
-},
-{
-  module => 'HTTP::Date',
-  version => 0,
-  desc => 'The "sa-update" program requires this module to make HTTP
-  If-Modified-Since GET requests.',
+  desc => 'The "sa-update" program can use this module to make HTTP requests.
+  Also used by DecodeShortURLs plugin.',
 },
 {
   module => 'Encode::Detect::Detector',
   version => 0,
-  desc => 'If you plan to use the normalize_charset config setting to
-  decode message parts from their declared character set into Unicode, and
-  such decoding fails, the Encode::Detect::Detector module (when available)
-  may be consulted to provide an alternative guess on a character set of a
-  problematic message part.',
+  desc => 'If normalize_charset decoding of message parts from their
+  declared character set into Unicode fails, the Encode::Detect::Detector
+  module (when available) may be consulted to provide an alternative guess
+  on a character set of a problematic message part.',
 },
 {
   module => 'Net::Patricia',
   version => 1.16,
-  desc => 'If this module is available, it will be used for IP address lookups
-  in tables internal_networks, trusted_networks, and msa_networks. Recommended
-  when a number of entries in these tables is large, i.e. in hundreds
-  or thousands. However, in case of overlapping (or conflicting) networks
-  in these tables, lookup results may differ as Net::Patricia finds a
-  tightest-matching entry, while a sequential NetAddr::IP search finds
-  a first-matching entry. So when overlapping network ranges are given,
-  specifying more specific subnets (longest netmask) first, followed by
-  wider subnets ensures predictable results.',
+  desc => 'If this module is available, it will be used for IP address
+  lookups in tables internal_networks, trusted_networks, msa_networks and
+  uri_local_cidr.  Recommended when a number of entries in these tables is
+  large, i.e.  in hundreds or thousands.  However, in case of overlapping
+  (or conflicting) networks in these tables, lookup results may differ as
+  Net::Patricia finds a tightest-matching entry, while a sequential
+  NetAddr::IP search finds a first-matching entry.  So when overlapping
+  network ranges are given, specifying more specific subnets (longest
+  netmask) first, followed by wider subnets ensures predictable results.',
+},
+{
+  module => 'Net::CIDR::Lite',
+  version => 0,
+  desc => 'If this module is available, then dash separated IP range format
+  "192.168.1.1-192.168.255.255" can be used for internal_networks,
+  trusted_networks, msa_networks and uri_local_cidr.',
 },
 {
   module => 'Net::DNS::Nameserver',
@@ -292,6 +311,18 @@ our @OPTIONAL_MODULES = (
   desc => 'IO::String emulates file interface for in-core strings.
   It is used by the optional OLEVBMacro Plugin.',
 },
+{
+  module => 'Email::Address::XS',
+  version => 0,
+  desc => 'Email::Address::XS is used to parse email addresses from header
+  fields like To/From/cc, per RFC 5322. If installed, it may additionally
+  be used by internal parser to process complex lists.',
+},
+{
+  module => 'Mail::DMARC',
+  version => 0,
+  desc => 'Mail::DMARC is used by the optional DMARC plugin.',
+},
 );
 
 our @BINARIES = ();
@@ -332,14 +363,6 @@ our @OPTIONAL_BINARIES = (
   version_check_regex => 'curl ([\d\.]*)',
   desc => $lwp_note,
 },
-#Fetch is a FreeBSD Product. We do not believe it has any way to check the version from
-#the command line.  It has been tested with FreeBSD version 8 through 9.1.
-{
-  binary => 'fetch',
-  version => '0',
-  
-  desc => $lwp_note,
-},
 {
   binary => 're2c',
   version => '0',
@@ -349,6 +372,16 @@ our @OPTIONAL_BINARIES = (
 }
 );
 
+#Fetch is a FreeBSD Product. We do not believe it has any way to check the version from
+#the command line.  It has been tested with FreeBSD version 8 through 9.1.
+if ($^O eq 'freebsd') {
+  push @OPTIONAL_BINARIES, {
+    binary => 'fetch',
+    version => '0',
+    desc => $lwp_note,
+  };
+}
+
 ###########################################################################
 
 =head1 METHODS
@@ -367,14 +400,6 @@ problems.
 sub debug_diagnostics {
   my $out = "diag: perl platform: $] $^O\n";
 
-# # this avoids an unsightly warning due to a shortcoming of Net::Ident;
-# # "Net::Ident::_export_hooks() called too early to check prototype at
-# # /usr/share/perl5/Net/Ident.pm line 29."   It only needs to be
-# # called here.
-# eval '
-#   sub Net::Ident::_export_hooks;
-# ';
-
   my $prefix = '';
   foreach my $moddef (@MODULES, 'optional', @OPTIONAL_MODULES) {
     if ($moddef eq 'optional') { $prefix = 'optional '; next; }
@@ -391,7 +416,10 @@ sub debug_diagnostics {
   return $out;
 }
 
+# When called from Makefile.PL use optional argument so it distinguishes between missing required modules
+# that CPAN will install before continuing, and missing required binaries that can't be fixed by CPAN install
 sub long_diagnostics {
+  my ($missing_modules_are_continuable) = @_;
   my $summary = "";
 
   print "checking module dependencies and their versions...\n";
@@ -405,6 +433,11 @@ sub long_diagnostics {
     try_module(0, $moddef, \$summary);
   }
 
+  if ($missing_modules_are_continuable) {
+    $WARNINGS += $EXIT_STATUS;
+    $EXIT_STATUS = 0;
+  }
+
   print "checking binary dependencies and their versions...\n";
 
   foreach my $bindef (@BINARIES) {
@@ -436,51 +469,20 @@ sub try_binary {
   my $required_version = $bindef->{version};
   my $recommended_version = $bindef->{recommended_min_version};
   my $errtype;
-  my ($command, $output);
-
-
-  # only viable on unix based systems, so exclude windows, etc. here
-  if ($^O =~ /^(mswin|dos|os2)/i) {
-    $$summref .= "Warning: Unable to test on this platform for the optional \"$bindef->{'binary'}\" binary\n";
-    $errtype = 'is unknown for this platform';
-  } else {
-    $command = "which $bindef->{'binary'} 2>&1";
-    #print "DEBUG: running $command\n";
-    $output = `$command`;
-
-    if (!defined $output || $output eq '') {
-      $installed = 0;
-    } elsif ($output =~ /which: no \Q$bindef->{'binary'}\E in/i) {
-      $installed = 0;
-    } else {
-      #COMMAND APPEARS TO EXIST
-      $command = $output;
-      chomp ($command);
-
-      $installed = 1;
-    }
-    #print "DEBUG: $command completed and output parsed\n";
-  }
-
-
-  if ($installed) {
-    #SANITIZE THE RETURNED COMMAND JUST IN CASE
-    $command =~ s/[^a-z0-9\/]//ig;
 
+  my $command = Mail::SpamAssassin::Util::find_executable_in_env_path($bindef->{'binary'});
+  if (defined $command) {
     #GET THE VERSION
-    $command .= " ";
     if (defined $bindef->{'version_check_params'}) {
-      $command .= $bindef->{'version_check_params'};
+      $command .= " ".$bindef->{'version_check_params'};
     }
-    $command .= " 2>&1";
 
     #print "DEBUG: running $command to check the version\n";
-    $output = `$command`;
+    my $output = `$command 2>&1`;
 
-    if (!defined $output) {
-      $installed = 0;
+    if (defined $output && $output ne '') {
+      $installed = 1;
 
-    } else {
       if (defined $bindef->{'version_check_regex'}) {
         $output =~ m/$bindef->{'version_check_regex'}/;
         $binary_version = $1;
index 0ebad33b5519805cb5963fcfb28cb00cebd34a29..eb383ce3e01418b9557a5325a6493ae1aa5507e4 100644 (file)
@@ -184,7 +184,7 @@ sub init_bar {
 
   my @chars = (' ') x $self->{bar_size};
 
-  print $fh sprintf("\r%3d%% [%s] %6.2f %s/sec %sm%ss LEFT",
+  print $fh sprintf("\r%3d%% [%s] %6.2f %s/sec %s-%s- LEFT",
                    0, join('', @chars), 0, $self->{itemtype}, '--', '--');
 
   return;
@@ -235,14 +235,12 @@ sub update {
       
       # using the overall_rate here seems to provide much smoother eta numbers
       my $eta = ($self->{total} - $num_done)/$overall_rate;
-      
-      # we make the assumption that we will never run > 1 hour, maybe this is bad
-      my $min = int($eta/60) % 60;
-      my $sec = int($eta % 60);
-      
-      print $fh sprintf("\r%3d%% [%s] %6.2f %s/sec %02dm%02ds LEFT",
-                       $percentage, join('', @chars), $self->{avg_msgs_per_sec},
-                        $self->{itemtype}, $min, $sec);
+
+      my($t1, $v1, $t2, $v2) = seconds_to_values($eta);
+
+      print $fh sprintf("\r%3d%% [%s] %6.2f %s/sec %2d${t1}%2d${t2} LEFT",
+                      $percentage, join('', @chars), $self->{avg_msgs_per_sec},
+                      $self->{itemtype}, $v1, $v2);
     }
     else { # we have no term, so fake it
       print $fh '.' x $msgs_since;
@@ -287,8 +285,7 @@ sub final {
 
   my $msgs_per_sec = $num_done / $time_taken;
 
-  my $min = int($time_taken/60) % 60;
-  my $sec = $time_taken % 60;
+  my($t1, $v1, $t2, $v2) = seconds_to_values($time_taken);
 
   if ($self->{term}) {
     my @chars = (' ') x $self->{bar_size};
@@ -297,17 +294,36 @@ sub final {
       $chars[$_] = '=';
     }
 
-    print $fh sprintf("\r%3d%% [%s] %6.2f %s/sec %02dm%02ds DONE\n",
+    print $fh sprintf("\r%3d%% [%s] %6.2f %s/sec %2d${t1}%2d${t2} DONE\n",
                      $percentage, join('', @chars), $msgs_per_sec,
-                      $self->{itemtype}, $min, $sec);
+                      $self->{itemtype}, $v1, $v2);
   }
   else {
-    print $fh sprintf("\n%3d%% Completed %6.2f %s/sec in %02dm%02ds\n",
+    print $fh sprintf("\n%3d%% Completed %6.2f %s/sec in %2d${t1}%2d${t2}\n",
                      $percentage, $msgs_per_sec,
-                      $self->{itemtype}, $min, $sec);
+                      $self->{itemtype}, $v1, $v2);
   }
 
   return;
 }
 
+sub seconds_to_values {
+  my $isec = shift;
+
+  my $day = int($isec/86400); $isec -= $day*86400;
+  my $hour = int($isec/3600); $isec -= $hour*3600;
+  my $min = int($isec/60); $isec -= $min*60;
+  my $sec = int($isec);
+
+  if ($day > 0) {
+    return ('d', $day, 'h', $hour);
+  }
+  elsif ($hour > 0) {
+    return ('h', $hour, 'm', $min);
+  }
+  else {
+    return ('m', $min, 's', $sec);
+  }
+}
+
 1;
index 5000767b53993ae67b4b4b6eb622b28a27829bd5..e81defa308c36f25f3ffb80a111d47936c9f2c7b 100644 (file)
@@ -43,6 +43,7 @@ Options:
  --siteconfigpath=path             Path for site configs
                                    (def: /etc/mail/spamassassin)
  --cf='config line'                Additional line of configuration
+ --pre='config line'               Additional line of ".pre" (prepended to configuration)
  -x, --nocreate-prefs              Don't create user preferences file
  -e, --exit-code                   Exit with a non-zero exit code if the
                                    tested message was spam
@@ -51,17 +52,17 @@ Options:
  -t, --test-mode                   Pipe message through and add extra
                                    report to the bottom
  --lint                            Lint the rule set: report syntax errors
- -W, --add-to-whitelist            Add addresses in mail to persistent address whitelist
- --add-to-blacklist                Add addresses in mail to persistent address blacklist
- -R, --remove-from-whitelist       Remove all addresses found in mail from
+ -W, --add-to-welcomelist          Add addresses in mail to persistent address welcomelist
+ --add-to-blocklist                Add addresses in mail to persistent address blocklist
+ -R, --remove-from-welcomelist     Remove all addresses found in mail from
                                    persistent address list
- --add-addr-to-whitelist=addr      Add addr to persistent address whitelist
- --add-addr-to-blacklist=addr      Add addr to persistent address blacklist
- --remove-addr-from-whitelist=addr Remove addr from persistent address list
+ --add-addr-to-welcomelist=addr    Add addr to persistent address welcomelist
+ --add-addr-to-blocklist=addr      Add addr to persistent address blocklist
+ --remove-addr-from-welcomelist=addr Remove addr from persistent address list
  -4 --ipv4only, --ipv4-only, --ipv4 Use IPv4, disable use of IPv6 for DNS etc.
  -6                                Use IPv6, disable use of IPv4 where possible
  --progress                        Print progress bar
- -D, --debug [area=n,...]          Print debugging messages
+ -D, --debug [area,...]            Print debugging messages
  -V, --version                     Print version
  -h, --help                        Print usage message
 
@@ -86,9 +87,12 @@ containing whitespace or beginning with "." or "," are skipped).
 The options I<--mbox> and I<--mbx> can override the assumed format,
 see the appropriate OPTION information below.
 
-Please note that SpamAssassin is not designed to scan large
-messages. Don't feed messages larger than about 500 KB to
-SpamAssassin, as this will consume a huge amount of memory.
+Files compressed with gzip/bzip2/xz/lz4/lzip/lzo are uncompressed
+automatically.  See C<Mail::SpamAssassin::ArchiveIterator> for more details.
+
+Please note that SpamAssassin is not designed to scan huge messages. 
+Messages larger than ~10-20MB should not be fed to SpamAssassin, as memory
+consumption will increase rapidly.
 
 =head1 OPTIONS
 
@@ -122,8 +126,8 @@ Report this message as manually-verified spam.  This will submit the mail
 message read from STDIN to various spam-blocker databases.  Currently,
 these are the Distributed Checksum Clearinghouse
 C<https://www.dcc-servers.net/dcc/>, Pyzor
-C<http://pyzor.org/>, Vipul's Razor
-C<http://razor.sourceforge.net/>, and SpamCop C<http://www.spamcop.net/>.
+C<https://www.pyzor.org/>, Vipul's Razor
+C<http://razor.sourceforge.net/>, and SpamCop C<https://www.spamcop.net/>.
 
 If the message contains SpamAssassin markup, the markup will be stripped
 out automatically before submission.  The support modules for DCC, Pyzor,
@@ -161,45 +165,57 @@ Syntax check (lint) the rule set and configuration files, reporting
 typos and rules that do not compile correctly.  Exits with 0 if there
 are no errors, or greater than 0 if any errors are found.
 
-=item B<-W>, B<--add-to-whitelist>
+=item B<-W>, B<--add-to-welcomelist>
+
+Previously --add-to-whitelist which will work interchangeably until 4.1.
 
 Add all email addresses, in the headers and body of the mail message read
-from STDIN, to a persistent address whitelist.  Note that you must be running
+from STDIN, to a persistent address welcomelist.  Note that you must be running
 C<spamassassin> or C<spamd> with a persistent address list plugin enabled for
 this to work.
 
-=item B<--add-to-blacklist>
+=item B<--add-to-blocklist>
+
+Previously --add-to-blacklist which will work interchangeably until 4.1.
 
 Add all email addresses, in the headers and body of the mail message read
-from STDIN, to the persistent address blacklist.  Note that you must be
+from STDIN, to the persistent address blocklist.  Note that you must be
 running C<spamassassin> or C<spamd> with a persistent address list plugin
 enabled for this to work.
 
-=item B<-R>, B<--remove-from-whitelist>
+=item B<-R>, B<--remove-from-welcomelist>
+
+Previously --remove-from-whitelist which will work interchangeably until 4.1.
 
 Remove all email addresses, in the headers and body of the mail message read
 from STDIN, from a persistent address list. STDIN must contain a full email
 message, so to remove a single address you should use
-B<--remove-addr-from-whitelist> instead.
+B<--remove-addr-from-welcomelist> instead.
 
 Note that you must be running C<spamassassin> or C<spamd> with a persistent
 address list plugin enabled for this to work.
 
-=item B<--add-addr-to-whitelist>
+=item B<--add-addr-to-welcomelist>
+
+Previously --add-addr-to-whitelist which will work interchangeably until 4.1.
 
-Add the named email address to a persistent address whitelist.  Note that you
+Add the named email address to a persistent address welcomelist.  Note that you
 must be running C<spamassassin> or C<spamd> with a persistent address list
 plugin enabled for this to work.
 
-=item B<--add-addr-to-blacklist>
+=item B<--add-addr-to-blocklist>
+
+Previously --add-addr-to-blacklist which will work interchangeably until 4.1.
 
-Add the named email address to a persistent address blacklist.  Note that you
+Add the named email address to a persistent address blocklist.  Note that you
 must be running C<spamassassin> or C<spamd> with a persistent address list
 plugin enabled for this to work.
 
-=item B<--remove-addr-from-whitelist>
+=item B<--remove-addr-from-welcomelist>
 
-Remove the named email address from a persistent address whitelist.  Note that
+Previously --remove-addr-from-whitelist which will work interchangeably until 4.1.
+
+Remove the named email address from a persistent address welcomelist.  Note that
 you must be running C<spamassassin> or C<spamd> with a persistent address
 list plugin enabled for this to work.
 
@@ -251,6 +267,15 @@ example:
 
         spamassassin -t --cf="body NEWRULE /text/" --cf="score NEWRULE 3.0"
 
+=item B<--pre='config line'>
+
+Add additional lines of .pre configuration directly from the command-line,
+parsed before the configuration files are read.  Multiple B<--pre> arguments
+can be used, and each will be considered a separate line of configuration. 
+For example:
+
+        spamassassin -t --pre="loadplugin Mail::SpamAssassin::Plugin::Foobar"
+
 =item B<-p> I<prefs>, B<--prefspath>=I<prefs>, B<--prefs-file>=I<prefs>
 
 Read user score preferences from I<prefs> (usually C<$HOME/.spamassassin/user_prefs>).
@@ -271,13 +296,16 @@ diagnostic output on bayes, learn, and dns, use:
 
         spamassassin -D bayes,learn,dns
 
+Use an empty string (-D '') to indicate no areas when the next item on the
+command line is a path, to prevent the path from being parsed as an area.
+
 Higher priority informational messages that are suitable for logging in normal
 circumstances are available with an area of "info".
 
 For more information about which areas (also known as channels) are available,
 please see the documentation at:
 
-       L<http://wiki.apache.org/spamassassin/DebugChannels>
+       L<https://wiki.apache.org/spamassassin/DebugChannels>
 
 =item B<-x>, B<--nocreate-prefs>
 
@@ -292,7 +320,7 @@ Unix message folder format.
 
 Specify that the input message(s) are in UW .mbx format.  mbx is
 the mailbox format used within the University of Washington's IMAP
-implementation; see C<http://www.washington.edu/imap/>.
+implementation; see C<https://en.wikipedia.org/wiki/UW_IMAP>.
 
 =back
 
@@ -310,7 +338,7 @@ C<Mail::SpamAssassin>
 
 =head1 BUGS
 
-See <http://issues.apache.org/SpamAssassin/>
+See <https://issues.apache.org/SpamAssassin/>
 
 =head1 AUTHORS
 
diff --git a/upstream/rules-extras/10_uridnsbl_skip_financial.cf b/upstream/rules-extras/10_uridnsbl_skip_financial.cf
new file mode 100644 (file)
index 0000000..84dbaa6
--- /dev/null
@@ -0,0 +1,625 @@
+
+#
+# Last update: 2016-08-29-axb
+# Phished financial domains
+ifplugin Mail::SpamAssassin::Plugin::URIDNSBL
+
+uridnsbl_skip_domain 1stnationalbank.com
+uridnsbl_skip_domain 365online.com
+uridnsbl_skip_domain 53.com
+uridnsbl_skip_domain abl.com.pk
+uridnsbl_skip_domain abnamro.nl
+uridnsbl_skip_domain accessbankplc.com
+uridnsbl_skip_domain adib.ae
+uridnsbl_skip_domain aib.ie
+uridnsbl_skip_domain aibgb.co.uk
+uridnsbl_skip_domain airdriesavingsbank.com
+uridnsbl_skip_domain aldermore.co.uk
+uridnsbl_skip_domain alliancebank.com.my
+uridnsbl_skip_domain alliancefg.com
+uridnsbl_skip_domain alliantcreditunion.com
+uridnsbl_skip_domain alliantcreditunion.org
+uridnsbl_skip_domain allianz.de
+uridnsbl_skip_domain allybank.com
+uridnsbl_skip_domain alterna.ca
+uridnsbl_skip_domain americanexpress.ch
+uridnsbl_skip_domain americanexpress.com
+uridnsbl_skip_domain anadolubank.nl
+uridnsbl_skip_domain anz.co.nz
+uridnsbl_skip_domain anz.com
+uridnsbl_skip_domain anz.com.au
+uridnsbl_skip_domain arbuthnotlatham.co.uk
+uridnsbl_skip_domain asb.co.nz
+uridnsbl_skip_domain authorize.net
+uridnsbl_skip_domain axisbank.co.in
+uridnsbl_skip_domain axisbank.com
+uridnsbl_skip_domain b2bbank.com
+uridnsbl_skip_domain baaderbank.de
+uridnsbl_skip_domain baloise.ch
+uridnsbl_skip_domain baml.com
+uridnsbl_skip_domain banamex.com
+uridnsbl_skip_domain bancanetbsc.do
+uridnsbl_skip_domain bancanetsantacruz.com.do
+uridnsbl_skip_domain bancapulia.it
+uridnsbl_skip_domain bancarios.com
+uridnsbl_skip_domain bancastato.ch
+uridnsbl_skip_domain bancatransilvania.ro
+uridnsbl_skip_domain banco.bradesco
+uridnsbl_skip_domain bancobase.com
+uridnsbl_skip_domain bancobic.ao
+uridnsbl_skip_domain bancobic.pt
+uridnsbl_skip_domain bancobpi.pt
+uridnsbl_skip_domain bancobrasil.com.br
+uridnsbl_skip_domain bancochile.cl
+uridnsbl_skip_domain bancochile.com
+uridnsbl_skip_domain bancoestado.cl
+uridnsbl_skip_domain bancofalabella.cl
+uridnsbl_skip_domain bancofalabella.com.co
+uridnsbl_skip_domain bancofalabella.pe
+uridnsbl_skip_domain bancomer.com
+uridnsbl_skip_domain bancopopolare.it
+uridnsbl_skip_domain bancoposta.it
+uridnsbl_skip_domain bancopostaclick.it
+uridnsbl_skip_domain bancosantander.es
+uridnsbl_skip_domain bancovotorantimcartoes.com.br
+uridnsbl_skip_domain bank-of-ireland.co.uk
+uridnsbl_skip_domain bank.barclays.co.uk
+uridnsbl_skip_domain bank24.ru
+uridnsbl_skip_domain bankalhabib.com
+uridnsbl_skip_domain bankaustria.at
+uridnsbl_skip_domain bankbgzbnpparibas.pl
+uridnsbl_skip_domain bankcardservices.co.uk
+uridnsbl_skip_domain bankcomm.com
+uridnsbl_skip_domain bankcoop.ch
+uridnsbl_skip_domain bankia.com
+uridnsbl_skip_domain bankia.es
+uridnsbl_skip_domain bankiabancapersonal.es
+uridnsbl_skip_domain bankinter.com
+uridnsbl_skip_domain bankinter.es
+uridnsbl_skip_domain bankmutual.com
+uridnsbl_skip_domain bankofamerica.com
+uridnsbl_skip_domain bankofcanada.ca
+uridnsbl_skip_domain bankofchina.com
+uridnsbl_skip_domain bankofcyprus.com
+uridnsbl_skip_domain bankofindia.co.nz
+uridnsbl_skip_domain bankofireland.com
+uridnsbl_skip_domain bankofirelanduk.com
+uridnsbl_skip_domain bankofoklahoma.com
+uridnsbl_skip_domain bankofscotland.co.uk
+uridnsbl_skip_domain bankofsingapore.com
+uridnsbl_skip_domain banksinarmas.com
+uridnsbl_skip_domain bankvonroll.ch
+uridnsbl_skip_domain bankwest.com.au
+uridnsbl_skip_domain banque-casino.fr
+uridnsbl_skip_domain banquepopulaire.fr
+uridnsbl_skip_domain banquescotia.com
+uridnsbl_skip_domain barclaycard.co.uk
+uridnsbl_skip_domain barclaycard.de
+uridnsbl_skip_domain barclaycard.es
+uridnsbl_skip_domain barclays.co.uk
+uridnsbl_skip_domain barclays.com
+uridnsbl_skip_domain barclays.sc
+uridnsbl_skip_domain barclayspartnerfinance.com
+uridnsbl_skip_domain barodanzltd.co.nz
+uridnsbl_skip_domain basler.ch
+uridnsbl_skip_domain bba.org.uk
+uridnsbl_skip_domain bbandt.com
+uridnsbl_skip_domain bci.cl
+uridnsbl_skip_domain bcp.com.pe
+uridnsbl_skip_domain bcv.ch
+uridnsbl_skip_domain bcvs.ch
+uridnsbl_skip_domain bekb.ch
+uridnsbl_skip_domain bellevue.ch
+uridnsbl_skip_domain bendigobank.com.au
+uridnsbl_skip_domain berliner-bank.de
+uridnsbl_skip_domain berliner-sparkasse.de
+uridnsbl_skip_domain bfanet.ao
+uridnsbl_skip_domain bgfi.com
+uridnsbl_skip_domain bgfionline.com
+uridnsbl_skip_domain bgzbnpparibas.pl
+uridnsbl_skip_domain billmelater.com
+uridnsbl_skip_domain bk.rw
+uridnsbl_skip_domain bkb.ch
+uridnsbl_skip_domain bks.at
+uridnsbl_skip_domain blkb.ch
+uridnsbl_skip_domain bmo.com
+uridnsbl_skip_domain bmocm.com
+uridnsbl_skip_domain bmogam.com
+uridnsbl_skip_domain bmoharris.com
+uridnsbl_skip_domain bmoharrisprivatebankingonline.com
+uridnsbl_skip_domain bmoinvestorline.com
+uridnsbl_skip_domain bmonesbittburns.com
+uridnsbl_skip_domain bnl.it
+uridnsbl_skip_domain bnpparibas.com
+uridnsbl_skip_domain bnpparibas.fr
+uridnsbl_skip_domain bnpparibasfortis.be
+uridnsbl_skip_domain boc.cnnz
+uridnsbl_skip_domain bonuscard.ch
+uridnsbl_skip_domain bpe-gruposantander.com
+uridnsbl_skip_domain bpi.pt
+uridnsbl_skip_domain bpostbank.be
+uridnsbl_skip_domain bradescardonline.com.br
+uridnsbl_skip_domain bradesco.com.br
+uridnsbl_skip_domain bradescoseguranca.com.br
+uridnsbl_skip_domain bridgewaterbank.ca
+uridnsbl_skip_domain bsibank.com
+uridnsbl_skip_domain bt-trade.ro
+uridnsbl_skip_domain btrl.ro
+uridnsbl_skip_domain businessonline-boi.com
+uridnsbl_skip_domain bzbank.ch
+uridnsbl_skip_domain ca-cib.com
+uridnsbl_skip_domain ca-egypt.com
+uridnsbl_skip_domain ca-suisse.com
+uridnsbl_skip_domain cafbank.org
+uridnsbl_skip_domain cafonline.org
+uridnsbl_skip_domain caisse-epargne.com
+uridnsbl_skip_domain caisse-epargne.fr
+uridnsbl_skip_domain caixa.gov.br
+uridnsbl_skip_domain caixabank.com
+uridnsbl_skip_domain cajasur.es
+uridnsbl_skip_domain camsonline.com
+uridnsbl_skip_domain canadiandirect.com
+uridnsbl_skip_domain capitalone.com
+uridnsbl_skip_domain capitalone360.com
+uridnsbl_skip_domain capitaloneonline.co.uk
+uridnsbl_skip_domain capitecbank.co.za
+uridnsbl_skip_domain cariparma.it
+uridnsbl_skip_domain carrefour-banque.fr
+uridnsbl_skip_domain cartabcc.it
+uridnsbl_skip_domain cartabccpos.it
+uridnsbl_skip_domain cartasi.it
+uridnsbl_skip_domain catalunyacaixa.com
+uridnsbl_skip_domain cbg.gm
+uridnsbl_skip_domain cbonline.co.uk
+uridnsbl_skip_domain cembra.ch
+uridnsbl_skip_domain cenbank.org
+uridnsbl_skip_domain centralbank.ae
+uridnsbl_skip_domain charitybank.org
+uridnsbl_skip_domain chase.com
+uridnsbl_skip_domain chebanca.it
+uridnsbl_skip_domain chinatrust.com.tw
+uridnsbl_skip_domain cial.ch
+uridnsbl_skip_domain cibc.com
+uridnsbl_skip_domain cic.ch
+uridnsbl_skip_domain cimbclicks.com.my
+uridnsbl_skip_domain citi.co.nz
+uridnsbl_skip_domain citi.com
+uridnsbl_skip_domain citi.eu
+uridnsbl_skip_domain citibank.ae
+uridnsbl_skip_domain citibank.co.in
+uridnsbl_skip_domain citibank.co.uk
+uridnsbl_skip_domain citibank.com
+uridnsbl_skip_domain citibankonline.com
+uridnsbl_skip_domain citibusiness.com
+uridnsbl_skip_domain citicards.com
+uridnsbl_skip_domain citigroup.com
+uridnsbl_skip_domain citizensbank.ca
+uridnsbl_skip_domain citizensbank.com
+uridnsbl_skip_domain citizensbankonline.com
+uridnsbl_skip_domain civibank.com
+uridnsbl_skip_domain civibank.it
+uridnsbl_skip_domain closebrothers.co.uk
+uridnsbl_skip_domain closebrothers.com
+uridnsbl_skip_domain clubsc.ch
+uridnsbl_skip_domain co-operativebank.co.uk
+uridnsbl_skip_domain colpatria.com
+uridnsbl_skip_domain colpatria.com.co
+uridnsbl_skip_domain commbank.com
+uridnsbl_skip_domain commbank.com.au
+uridnsbl_skip_domain commerzbank.com
+uridnsbl_skip_domain commerzbank.de
+uridnsbl_skip_domain coopbank.dk
+uridnsbl_skip_domain corner.ch
+uridnsbl_skip_domain cornerbanca.ch
+uridnsbl_skip_domain cornercard.ch
+uridnsbl_skip_domain cornercard.com
+uridnsbl_skip_domain cosycard.ch
+uridnsbl_skip_domain coutts.com
+uridnsbl_skip_domain credit-agricole.com
+uridnsbl_skip_domain credit-agricole.fr
+uridnsbl_skip_domain credit-suisse.com
+uridnsbl_skip_domain creditagricole.rs
+uridnsbl_skip_domain cs.com
+uridnsbl_skip_domain css.ch
+uridnsbl_skip_domain ctbcbank.com
+uridnsbl_skip_domain ctfs.com
+uridnsbl_skip_domain cwbank.com
+uridnsbl_skip_domain cwbankgroup.com
+uridnsbl_skip_domain cwt.ca
+uridnsbl_skip_domain cybg.com
+uridnsbl_skip_domain danskebank.co.uk
+uridnsbl_skip_domain danskebank.com
+uridnsbl_skip_domain danskebank.de
+uridnsbl_skip_domain danskebank.dk
+uridnsbl_skip_domain danskebank.ee
+uridnsbl_skip_domain danskebank.fi
+uridnsbl_skip_domain danskebank.ie
+uridnsbl_skip_domain danskebank.no
+uridnsbl_skip_domain danskebankas.lt
+uridnsbl_skip_domain datatrans.biz
+uridnsbl_skip_domain datatrans.ch
+uridnsbl_skip_domain db.com
+uridnsbl_skip_domain dbs.com
+uridnsbl_skip_domain demirbank.kg
+uridnsbl_skip_domain denizbank.com
+uridnsbl_skip_domain desjardins.ca
+uridnsbl_skip_domain desjardins.com
+uridnsbl_skip_domain deutsche-bank.de
+uridnsbl_skip_domain deutschebank.be
+uridnsbl_skip_domain deutschebank.co.nz
+uridnsbl_skip_domain deutschebank.de
+uridnsbl_skip_domain diamondbank.com
+uridnsbl_skip_domain dibpak.com
+uridnsbl_skip_domain discover.com
+uridnsbl_skip_domain discovercard.com
+uridnsbl_skip_domain discovery.co.za
+uridnsbl_skip_domain dnbnord.lt
+uridnsbl_skip_domain dresdner-bank.de
+uridnsbl_skip_domain dsbbank.sr
+uridnsbl_skip_domain duncanlawrie.com
+uridnsbl_skip_domain e-gulfbank.com
+uridnsbl_skip_domain easybank.at
+uridnsbl_skip_domain ecobank.com
+uridnsbl_skip_domain edwardjones.com
+uridnsbl_skip_domain esunbank.com.tw
+uridnsbl_skip_domain fednetbank.com
+uridnsbl_skip_domain fidelity.com
+uridnsbl_skip_domain fidor.de
+uridnsbl_skip_domain finance.com
+uridnsbl_skip_domain finansbank.com.tr
+uridnsbl_skip_domain finasta.lt
+uridnsbl_skip_domain fineco.it
+uridnsbl_skip_domain firstbankcard.com
+uridnsbl_skip_domain firstmerit.com
+uridnsbl_skip_domain firstnational.com
+uridnsbl_skip_domain firstnationalmerchantsolutions.com
+uridnsbl_skip_domain firsttrustbank.co.uk
+uridnsbl_skip_domain fnb-online.com
+uridnsbl_skip_domain fnb.co.za
+uridnsbl_skip_domain fnbc.ca
+uridnsbl_skip_domain friuladria.it
+uridnsbl_skip_domain garanti.com.tr
+uridnsbl_skip_domain garantibank.eu
+uridnsbl_skip_domain garantibank.nl
+uridnsbl_skip_domain gazprombank.ch
+uridnsbl_skip_domain gazprombank.ru
+uridnsbl_skip_domain generali.es
+uridnsbl_skip_domain genevoise.ch
+uridnsbl_skip_domain gkb.ch
+uridnsbl_skip_domain granitbank.hu
+uridnsbl_skip_domain gtbank.com
+uridnsbl_skip_domain halifax.co.uk
+uridnsbl_skip_domain handelsbanken.se
+uridnsbl_skip_domain harrodsbank.co.uk
+uridnsbl_skip_domain hbl.com
+uridnsbl_skip_domain hblibank.com
+uridnsbl_skip_domain hblibank.com.pk
+uridnsbl_skip_domain hdfcbank.com
+uridnsbl_skip_domain heartland.co.nz
+uridnsbl_skip_domain hellenicbank.com
+uridnsbl_skip_domain hkbea.com
+uridnsbl_skip_domain hlb.com.kh
+uridnsbl_skip_domain hlb.com.my
+uridnsbl_skip_domain hoaresbank.co.uk
+uridnsbl_skip_domain home.barclays
+uridnsbl_skip_domain hongleongconnect.com.kh
+uridnsbl_skip_domain hongleongconnect.com.vn
+uridnsbl_skip_domain hongleongconnect.my
+uridnsbl_skip_domain hsbc.co.nz
+uridnsbl_skip_domain hsbc.co.uk
+uridnsbl_skip_domain hsbc.com
+uridnsbl_skip_domain hsbc.com.ar
+uridnsbl_skip_domain hsbc.com.hk
+uridnsbl_skip_domain hypovereinsbank.co.uk
+uridnsbl_skip_domain hypovereinsbank.de
+uridnsbl_skip_domain icbcnz.com
+uridnsbl_skip_domain icicibank.co.in
+uridnsbl_skip_domain icicibank.com
+uridnsbl_skip_domain icicibankprivatebanking.com
+uridnsbl_skip_domain icorner.ch
+uridnsbl_skip_domain icscards.de
+uridnsbl_skip_domain icscards.nl
+uridnsbl_skip_domain ing-diba.de
+uridnsbl_skip_domain ing.be
+uridnsbl_skip_domain ing.com
+uridnsbl_skip_domain ing.lu
+uridnsbl_skip_domain ing.nl
+uridnsbl_skip_domain ingdirect.ca
+uridnsbl_skip_domain ingdirect.fr
+uridnsbl_skip_domain ingvysyabank.com
+uridnsbl_skip_domain interac.ca
+uridnsbl_skip_domain iobnet.co.in
+uridnsbl_skip_domain isbank.com.tr
+uridnsbl_skip_domain isbank.de
+uridnsbl_skip_domain isbank.ge
+uridnsbl_skip_domain isbank.iq
+uridnsbl_skip_domain isbankkosova.com
+uridnsbl_skip_domain itau.com.br
+uridnsbl_skip_domain jpmchase.com
+uridnsbl_skip_domain jpmorgan.com
+uridnsbl_skip_domain jsafrasarasin.com
+uridnsbl_skip_domain julianhodgebank.com
+uridnsbl_skip_domain juliusbaer.com
+uridnsbl_skip_domain jyskebank.dk
+uridnsbl_skip_domain kantonalbank.ch
+uridnsbl_skip_domain key.com
+uridnsbl_skip_domain kiwibank.co.nz
+uridnsbl_skip_domain kotak.com
+uridnsbl_skip_domain kredytbank.pl
+uridnsbl_skip_domain kreissparkasse-schwalm-eder.de
+uridnsbl_skip_domain ksklb.de
+uridnsbl_skip_domain kutxabank.es
+uridnsbl_skip_domain laboralkutxa.com
+uridnsbl_skip_domain lacaixa.cat
+uridnsbl_skip_domain lacaixa.es
+uridnsbl_skip_domain laurentianbank.ca
+uridnsbl_skip_domain lbb.de
+uridnsbl_skip_domain lcl.com
+uridnsbl_skip_domain lcl.fr
+uridnsbl_skip_domain lloydsbank.com
+uridnsbl_skip_domain lloydsbankcommercial.com
+uridnsbl_skip_domain lloydsbankinggroup.com
+uridnsbl_skip_domain lloydstsb.ch
+uridnsbl_skip_domain lloydstsb.co.uk
+uridnsbl_skip_domain lombardodier.com
+uridnsbl_skip_domain loydsbank.com
+uridnsbl_skip_domain maerki-baumann.ch
+uridnsbl_skip_domain mandtbank.com
+uridnsbl_skip_domain manulife.com
+uridnsbl_skip_domain manulifebank.ca
+uridnsbl_skip_domain manulifebankselect.ca
+uridnsbl_skip_domain manulifeone.ca
+uridnsbl_skip_domain mashreqbank.com
+uridnsbl_skip_domain mastercard.com
+uridnsbl_skip_domain maybank2u.com
+uridnsbl_skip_domain maybank2u.com.my
+uridnsbl_skip_domain mdmbank.com
+uridnsbl_skip_domain mechanicsbank.com
+uridnsbl_skip_domain medbank.lt
+uridnsbl_skip_domain metrobankdirect.com
+uridnsbl_skip_domain metrobankonline.co.uk
+uridnsbl_skip_domain migbank.com
+uridnsbl_skip_domain migrosbank.ch
+uridnsbl_skip_domain mizuhobank.co.jp
+uridnsbl_skip_domain mmwarburg.lu
+uridnsbl_skip_domain montepio.pt
+uridnsbl_skip_domain morganstanley.com
+uridnsbl_skip_domain mps.it
+uridnsbl_skip_domain ms.com
+uridnsbl_skip_domain mufg.jp
+uridnsbl_skip_domain myonlineresourcecenter.com
+uridnsbl_skip_domain myonlineservices.ch
+uridnsbl_skip_domain nab.com.au
+uridnsbl_skip_domain nationalesuisse.ch
+uridnsbl_skip_domain nationwide-communications.co.uk
+uridnsbl_skip_domain nationwide-service.co.uk
+uridnsbl_skip_domain nationwide.co.uk
+uridnsbl_skip_domain natwest.com
+uridnsbl_skip_domain navyfederal.org
+uridnsbl_skip_domain nbc.ca
+uridnsbl_skip_domain newyorkfed.org
+uridnsbl_skip_domain nibl.com.np
+uridnsbl_skip_domain nordea.fi
+uridnsbl_skip_domain nordea.lt
+uridnsbl_skip_domain nordfynsbank.dk
+uridnsbl_skip_domain norisbank.de
+uridnsbl_skip_domain notenstein.ch
+uridnsbl_skip_domain nuvisionfederal.com
+uridnsbl_skip_domain oceanbank.com
+uridnsbl_skip_domain onlinesbi.com
+uridnsbl_skip_domain orchardbank.com
+uridnsbl_skip_domain ostsaechsische-sparkasse-dresden.de
+uridnsbl_skip_domain paylife.at
+uridnsbl_skip_domain paypal-brasil.com.br
+uridnsbl_skip_domain paypal-communication.com
+uridnsbl_skip_domain paypal-community.com
+uridnsbl_skip_domain paypal-customerfeedback.com
+uridnsbl_skip_domain paypal-deutschland.de
+uridnsbl_skip_domain paypal-exchanges.com
+uridnsbl_skip_domain paypal-marketing.co.uk
+uridnsbl_skip_domain paypal-marketing.pl
+uridnsbl_skip_domain paypal-notify.com
+uridnsbl_skip_domain paypal-now.com
+uridnsbl_skip_domain paypal-opwaarderen.nl
+uridnsbl_skip_domain paypal-pages.com
+uridnsbl_skip_domain paypal-search.com
+uridnsbl_skip_domain paypal-shopping.co.uk
+uridnsbl_skip_domain paypal-techsupport.com
+uridnsbl_skip_domain paypal.be
+uridnsbl_skip_domain paypal.ca
+uridnsbl_skip_domain paypal.ch
+uridnsbl_skip_domain paypal.co.il
+uridnsbl_skip_domain paypal.co.uk
+uridnsbl_skip_domain paypal.com
+uridnsbl_skip_domain paypal.com.au
+uridnsbl_skip_domain paypal.com.br
+uridnsbl_skip_domain paypal.com.mx
+uridnsbl_skip_domain paypal.com.pt
+uridnsbl_skip_domain paypal.de
+uridnsbl_skip_domain paypal.dk
+uridnsbl_skip_domain paypal.es
+uridnsbl_skip_domain paypal.fr
+uridnsbl_skip_domain paypal.it
+uridnsbl_skip_domain paypal.net
+uridnsbl_skip_domain paypal.nl
+uridnsbl_skip_domain paypal.no
+uridnsbl_skip_domain paypal.pt
+uridnsbl_skip_domain paypal.ru
+uridnsbl_skip_domain paypal.se
+uridnsbl_skip_domain paypalobjects.com
+uridnsbl_skip_domain pbebank.com
+uridnsbl_skip_domain pcfinancial.ca
+uridnsbl_skip_domain permanenttsb.ie
+uridnsbl_skip_domain pnc.com
+uridnsbl_skip_domain popolarevicenza.it
+uridnsbl_skip_domain postbank.de
+uridnsbl_skip_domain postepay.it
+uridnsbl_skip_domain postfinance.ch
+uridnsbl_skip_domain postfinance.info
+uridnsbl_skip_domain postfinancearena.ch
+uridnsbl_skip_domain publicislamicbank.com.my
+uridnsbl_skip_domain rabobank.co.nz
+uridnsbl_skip_domain rabobank.com
+uridnsbl_skip_domain rabobank.nl
+uridnsbl_skip_domain rahnbodmer.ch
+uridnsbl_skip_domain raiffeisen.ch
+uridnsbl_skip_domain raiffeisen.hu
+uridnsbl_skip_domain raiffeisen.li
+uridnsbl_skip_domain raiffeisen.ru
+uridnsbl_skip_domain raiffeisenbank.rs
+uridnsbl_skip_domain raphaelsbank.com
+uridnsbl_skip_domain rbc.com
+uridnsbl_skip_domain rbcroyalbank.com
+uridnsbl_skip_domain rbs.co.uk
+uridnsbl_skip_domain rbssecure.co.uk
+uridnsbl_skip_domain rbsworldpay.com
+uridnsbl_skip_domain rcb.at rcb.at
+uridnsbl_skip_domain recordbank.be
+uridnsbl_skip_domain regiobank.nl
+uridnsbl_skip_domain regions.com
+uridnsbl_skip_domain regionsnet.com
+uridnsbl_skip_domain renasantbank.com
+uridnsbl_skip_domain rhbgroup.com
+uridnsbl_skip_domain rogersbank.com
+uridnsbl_skip_domain rothschild.com
+uridnsbl_skip_domain rothschildbank.com
+uridnsbl_skip_domain royalbank.com
+uridnsbl_skip_domain s.de
+uridnsbl_skip_domain sagepay.co.uk
+uridnsbl_skip_domain sagepay.com
+uridnsbl_skip_domain sainsburysbank.co.uk
+uridnsbl_skip_domain samba.com
+uridnsbl_skip_domain santander.cl
+uridnsbl_skip_domain santander.co.uk
+uridnsbl_skip_domain santander.com
+uridnsbl_skip_domain santander.com.br
+uridnsbl_skip_domain santander.com.mx
+uridnsbl_skip_domain santandercorretora.com.br
+uridnsbl_skip_domain santanderesfera.com.br
+uridnsbl_skip_domain santandersantiago.cl
+uridnsbl_skip_domain sarasin.ch
+uridnsbl_skip_domain sberbank.ch
+uridnsbl_skip_domain sbs.net.nz
+uridnsbl_skip_domain sc.com
+uridnsbl_skip_domain schoellerbank.at
+uridnsbl_skip_domain scotiabank.ca
+uridnsbl_skip_domain scotiabank.com
+uridnsbl_skip_domain scotiamocatta.com
+uridnsbl_skip_domain scotiaonline.com
+uridnsbl_skip_domain securetrustbank.com
+uridnsbl_skip_domain service-sparkasse.de
+uridnsbl_skip_domain serviciobancomer.com
+uridnsbl_skip_domain shawbrook.co.uk
+uridnsbl_skip_domain shkb.ch
+uridnsbl_skip_domain six-group.com
+uridnsbl_skip_domain six-payment-services.com
+uridnsbl_skip_domain skrill.com
+uridnsbl_skip_domain sls-direkt.de
+uridnsbl_skip_domain snb.ch snb.ch
+uridnsbl_skip_domain snsbank.nl
+uridnsbl_skip_domain societegenerale.fr
+uridnsbl_skip_domain sparda-a.de
+uridnsbl_skip_domain sparda-b.de
+uridnsbl_skip_domain sparda-bank-hamburg.de
+uridnsbl_skip_domain sparda-bw.de
+uridnsbl_skip_domain sparda-h.de
+uridnsbl_skip_domain sparda-hessen.de
+uridnsbl_skip_domain sparda-m.de
+uridnsbl_skip_domain sparda-ms.de
+uridnsbl_skip_domain sparda-n.de
+uridnsbl_skip_domain sparda-ostbayern.de
+uridnsbl_skip_domain sparda-sw.de
+uridnsbl_skip_domain sparda-verband.de
+uridnsbl_skip_domain sparda-west.de
+uridnsbl_skip_domain sparkasse-bank-malta.com
+uridnsbl_skip_domain sparkasse-bielefeld.de
+uridnsbl_skip_domain sparkasse-bochum.de
+uridnsbl_skip_domain sparkasse-gera-greiz.de
+uridnsbl_skip_domain sparkasse-hamm.de
+uridnsbl_skip_domain sparkasse-heidelberg.de
+uridnsbl_skip_domain sparkasse-ingolstadt.de
+uridnsbl_skip_domain sparkasse-mittelthueringen.de
+uridnsbl_skip_domain sparkasse.at
+uridnsbl_skip_domain sparkasse.ch
+uridnsbl_skip_domain sparkasse.de
+uridnsbl_skip_domain sparkasseblog.de
+uridnsbl_skip_domain standardbank.co.za
+uridnsbl_skip_domain standardbank.com
+uridnsbl_skip_domain standardchartered.com.gh
+uridnsbl_skip_domain standardchartered.com.my
+uridnsbl_skip_domain suncorpbank.com.au
+uridnsbl_skip_domain suntrust.com
+uridnsbl_skip_domain swedbank.com
+uridnsbl_skip_domain swedbank.ee
+uridnsbl_skip_domain swedbank.lt
+uridnsbl_skip_domain swedbank.lu
+uridnsbl_skip_domain swedbank.se
+uridnsbl_skip_domain swisscanto.ch
+uridnsbl_skip_domain swisscaution.ch
+uridnsbl_skip_domain swissquote.ch
+uridnsbl_skip_domain sydbank.dk
+uridnsbl_skip_domain tangerine.ca
+uridnsbl_skip_domain tcb-bank.com.tw
+uridnsbl_skip_domain tdbank.com
+uridnsbl_skip_domain tdcommercialbanking.com
+uridnsbl_skip_domain tescobank.com
+uridnsbl_skip_domain tsb.co.nz
+uridnsbl_skip_domain tsb.co.uk
+uridnsbl_skip_domain tsbbank.co.nz
+uridnsbl_skip_domain ubibanca.com
+uridnsbl_skip_domain ubs.com
+uridnsbl_skip_domain ulsterbank.co.uk
+uridnsbl_skip_domain ulsterbankanytimebanking.co.uk
+uridnsbl_skip_domain unibanco.pt
+uridnsbl_skip_domain unibancoconnect.pt
+uridnsbl_skip_domain unicredit.eu
+uridnsbl_skip_domain unicredit.it
+uridnsbl_skip_domain unicreditbank.lt
+uridnsbl_skip_domain unicreditgroup.eu
+uridnsbl_skip_domain unionbank.com
+uridnsbl_skip_domain unionbankcameroon.com
+uridnsbl_skip_domain unity.co.uk
+uridnsbl_skip_domain uob.com.sg
+uridnsbl_skip_domain uobgroup.com
+uridnsbl_skip_domain usbank.com
+uridnsbl_skip_domain valianttrust.com
+uridnsbl_skip_domain vaudoise.ch
+uridnsbl_skip_domain venetobanca.it
+uridnsbl_skip_domain venetobanka.al
+uridnsbl_skip_domain versabank.com
+uridnsbl_skip_domain virginmoney.com
+uridnsbl_skip_domain visa.com.ar
+uridnsbl_skip_domain visa.com.br
+uridnsbl_skip_domain visaeurope.ch
+uridnsbl_skip_domain visaeurope.com
+uridnsbl_skip_domain viseca.ch
+uridnsbl_skip_domain volksbank.de
+uridnsbl_skip_domain volkswagenbank.de
+uridnsbl_skip_domain vpbank.com
+uridnsbl_skip_domain vr.de
+uridnsbl_skip_domain vwbank.de
+uridnsbl_skip_domain wachovia.com
+uridnsbl_skip_domain weatherbys.co.uk
+uridnsbl_skip_domain wegelin.ch
+uridnsbl_skip_domain wellsfargo.com
+uridnsbl_skip_domain wellsfargoemail.com
+uridnsbl_skip_domain westernunion.ca
+uridnsbl_skip_domain westernunion.com
+uridnsbl_skip_domain westernunion.fr
+uridnsbl_skip_domain westernunion.se
+uridnsbl_skip_domain westpac.co.nz
+uridnsbl_skip_domain westpac.com.au
+uridnsbl_skip_domain westpac.com.nz
+uridnsbl_skip_domain wir.ch
+uridnsbl_skip_domain worldbank.org
+uridnsbl_skip_domain worldpay.com
+uridnsbl_skip_domain wvb.de
+uridnsbl_skip_domain yacht.nl
+uridnsbl_skip_domain ybonline.co.uk
+uridnsbl_skip_domain yorkshirebank.co.uk
+uridnsbl_skip_domain yourbankcard.com
+uridnsbl_skip_domain zagbank.ca
+uridnsbl_skip_domain zenithbank.com
+uridnsbl_skip_domain zkb.ch
+uridnsbl_skip_domain zugerkb.ch
+endif   # Mail::SpamAssassin::Plugin::URIDNSBL
diff --git a/upstream/rules-extras/README.txt b/upstream/rules-extras/README.txt
new file mode 100644 (file)
index 0000000..030088f
--- /dev/null
@@ -0,0 +1,3 @@
+Rules in this directory are NOT processed by masschecks or sa-update
+Use at your own risk.
+
diff --git a/upstream/rules.README b/upstream/rules.README
deleted file mode 100644 (file)
index 8ae717a..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-From build/README:
-
-- Rule Source is only in trunk.  If you are building a branch, checkout
-  trunk as well and symlink it, i.e. rulesrc -> ../trunk/rulesrc/
-
-- t.rule Source is only in trunk.  If you are building a branch, checkout
-  trunk as well and symlink it, i.e. t.rules -> ../trunk/t.rules/
-
-- Rules are ONLY published from trunk.  Rule development should use plugin
-  and version conditions to make it so one ruleset works on all modern
-  versions of SA. If you are building a branch, checkout trunk as well and
-  symlink the rules from trunk, i.e. rules -> ../trunk/rules/
-
-  With the rules in trunk symlinked, you can expect MANIFEST warnings when
-  running things such as make distclean such as:
-
-    No such file: rules/20_aux_tlds.cf
-    No such file: rules/active.list
-    No such file: rules/init.pre
-    No such file: rules/languages
-    No such file: rules/local.cf
-    No such file: rules/regression_tests.cf
-    No such file: rules/sa-update-pubkey.txt
-    No such file: rules/user_prefs.template
-    No such file: rules/v310.pre
-    No such file: rules/v312.pre
-    No such file: rules/v320.pre
-    No such file: rules/v330.pre
-    No such file: rules/v340.pre
-    No such file: rules/v341.pre
-    No such file: rules/v342.pre
-
-  NOTE: Don't remove the lines from the MANIFEST though!
index 215b01702097d2b64ef62ab456441025a423c322..77c22da42088aad6d736ae479ae4e00e24584ea3 100644 (file)
@@ -51,149 +51,148 @@ endif
 # this block
 #
 # For an up to date list of IDN TLDs that can be pasted into this block, run this command:
-#  wget https://data.iana.org/TLD/tlds-alpha-by-domain.txt -q -O - | grep -i '^xn--' | tr '\n' ' ' | fold -w 80 -s | perl -pe 'chomp; s/.*/util_rb_tld \L$_\n/'
+#  wget https://data.iana.org/TLD/tlds-alpha-by-domain.txt -q -O - | grep -i '^xn--' | tr '\n' ' ' | fold -w 80 -s | perl -pe 's/\s+$//; s/.*/util_rb_tld \L$_\n/'
 # Since version 4.0 the util_rb_tld also accepts Unicode IDN labels (encoded as UTF-8), e.g.:
-#  wget https://data.iana.org/TLD/tlds-alpha-by-domain.txt -q -O - | grep -i '^xn--' | idn -u | tr '\n' ' ' | fold -w 80 -s | perl -pe 'chomp; s/.*/util_rb_tld \L$_\n/'
+#  wget https://data.iana.org/TLD/tlds-alpha-by-domain.txt -q -O - | grep -i '^xn--' | idn -u | tr '\n' ' ' | fold -w 80 -s | perl -pe 's/\s+$//; s/.*/util_rb_tld \L$_\n/'
 
 if can(Mail::SpamAssassin::Conf::feature_registryboundaries)
-util_rb_tld xn--11b4c3d xn--1ck2e1b xn--1qqw23a xn--2scrj9c xn--30rr7y xn--3bst00m 
-util_rb_tld xn--3ds443g xn--3e0b707e xn--3hcrj9c xn--3oq18vl8pn36a xn--3pxu8k xn--42c2d9a 
-util_rb_tld xn--45br5cyl xn--45brj9c xn--45q11c xn--4gbrim xn--54b7fta0cc xn--55qw42g 
-util_rb_tld xn--55qx5d xn--5su34j936bgsg xn--5tzm5g xn--6frz82g xn--6qq986b3xl xn--80adxhks 
-util_rb_tld xn--80ao21a xn--80aqecdr1a xn--80asehdb xn--80aswg xn--8y0a063a xn--90a3ac 
-util_rb_tld xn--90ae xn--90ais xn--9dbq2a xn--9et52u xn--9krt00a xn--b4w605ferd 
-util_rb_tld xn--bck1b9a5dre4c xn--c1avg xn--c2br7g xn--cck2b3b xn--cg4bki 
-util_rb_tld xn--clchc0ea0b2g2a9gcd xn--czr694b xn--czrs0t xn--czru2d xn--d1acj3b xn--d1alf 
-util_rb_tld xn--e1a4c xn--eckvdtc9d xn--efvy88h xn--estv75g xn--fct429k xn--fhbei 
-util_rb_tld xn--fiq228c5hs xn--fiq64b xn--fiqs8s xn--fiqz9s xn--fjq720a xn--flw351e 
-util_rb_tld xn--fpcrj9c3d xn--fzc2c9e2c xn--fzys8d69uvgm xn--g2xx48c xn--gckr3f0f 
-util_rb_tld xn--gecrj9c xn--gk3at1e xn--h2breg3eve xn--h2brj9c xn--h2brj9c8c xn--hxt814e 
-util_rb_tld xn--i1b6b1a6a2e xn--imr513n xn--io0a7i xn--j1aef xn--j1amh xn--j6w193g 
-util_rb_tld xn--jlq61u9w7b xn--jvr189m xn--kcrx77d1x4a xn--kprw13d xn--kpry57d xn--kpu716f 
-util_rb_tld xn--kput3i xn--l1acc xn--lgbbat1ad8j xn--mgb9awbf xn--mgba3a3ejt 
-util_rb_tld xn--mgba3a4f16a xn--mgba7c0bbn0a xn--mgbaakc7dvf xn--mgbaam7a8h xn--mgbab2bd 
-util_rb_tld xn--mgbai9azgqp6j xn--mgbayh7gpa xn--mgbb9fbpob xn--mgbbh1a xn--mgbbh1a71e 
-util_rb_tld xn--mgbc0a9azcg xn--mgbca7dzdo xn--mgberp4a5d4ar xn--mgbgu82a xn--mgbi4ecexp 
-util_rb_tld xn--mgbpl2fh xn--mgbt3dhd xn--mgbtx2b xn--mgbx4cd0ab xn--mix891f xn--mk1bu44c 
-util_rb_tld xn--mxtq1m xn--ngbc5azd xn--ngbe9e0a xn--ngbrx xn--node xn--nqv7f 
-util_rb_tld xn--nqv7fs00ema xn--nyqy26a xn--o3cw4h xn--ogbpf8fl xn--otu796d xn--p1acf 
-util_rb_tld xn--p1ai xn--pbt977c xn--pgbs0dh xn--pssy2u xn--q9jyb4c xn--qcka1pmc xn--qxam 
-util_rb_tld xn--rhqv96g xn--rovu88b xn--rvc1e0am3e xn--s9brj9c xn--ses554g xn--t60b56a 
-util_rb_tld xn--tckwe xn--tiq49xqyj xn--unup4y xn--vermgensberater-ctb 
-util_rb_tld xn--vermgensberatung-pwb xn--vhquv xn--vuq861b xn--w4r85el8fhu5dnra xn--w4rs40l 
-util_rb_tld xn--wgbh1c xn--wgbl6a xn--xhq521b xn--xkc2al3hye2a xn--xkc2dl3a5ee0h xn--y9a3aq 
+# Updated 2022-10-18
+util_rb_tld xn--11b4c3d xn--1ck2e1b xn--1qqw23a xn--2scrj9c xn--30rr7y xn--3bst00m
+util_rb_tld xn--3ds443g xn--3e0b707e xn--3hcrj9c xn--3pxu8k xn--42c2d9a xn--45br5cyl
+util_rb_tld xn--45brj9c xn--45q11c xn--4dbrk0ce xn--4gbrim xn--54b7fta0cc xn--55qw42g
+util_rb_tld xn--55qx5d xn--5su34j936bgsg xn--5tzm5g xn--6frz82g xn--6qq986b3xl xn--80adxhks
+util_rb_tld xn--80ao21a xn--80aqecdr1a xn--80asehdb xn--80aswg xn--8y0a063a xn--90a3ac
+util_rb_tld xn--90ae xn--90ais xn--9dbq2a xn--9et52u xn--9krt00a xn--b4w605ferd
+util_rb_tld xn--bck1b9a5dre4c xn--c1avg xn--c2br7g xn--cck2b3b xn--cckwcxetd xn--cg4bki
+util_rb_tld xn--clchc0ea0b2g2a9gcd xn--czr694b xn--czrs0t xn--czru2d xn--d1acj3b xn--d1alf
+util_rb_tld xn--e1a4c xn--eckvdtc9d xn--efvy88h xn--fct429k xn--fhbei xn--fiq228c5hs
+util_rb_tld xn--fiq64b xn--fiqs8s xn--fiqz9s xn--fjq720a xn--flw351e xn--fpcrj9c3d
+util_rb_tld xn--fzc2c9e2c xn--fzys8d69uvgm xn--g2xx48c xn--gckr3f0f xn--gecrj9c xn--gk3at1e
+util_rb_tld xn--h2breg3eve xn--h2brj9c xn--h2brj9c8c xn--hxt814e xn--i1b6b1a6a2e
+util_rb_tld xn--imr513n xn--io0a7i xn--j1aef xn--j1amh xn--j6w193g xn--jlq480n2rg
+util_rb_tld xn--jlq61u9w7b xn--jvr189m xn--kcrx77d1x4a xn--kprw13d xn--kpry57d xn--kput3i
+util_rb_tld xn--l1acc xn--lgbbat1ad8j xn--mgb9awbf xn--mgba3a3ejt xn--mgba3a4f16a
+util_rb_tld xn--mgba7c0bbn0a xn--mgbaakc7dvf xn--mgbaam7a8h xn--mgbab2bd xn--mgbah1a3hjkrd
+util_rb_tld xn--mgbai9azgqp6j xn--mgbayh7gpa xn--mgbbh1a xn--mgbbh1a71e xn--mgbc0a9azcg
+util_rb_tld xn--mgbca7dzdo xn--mgbcpq6gpa1a xn--mgberp4a5d4ar xn--mgbgu82a xn--mgbi4ecexp
+util_rb_tld xn--mgbpl2fh xn--mgbt3dhd xn--mgbtx2b xn--mgbx4cd0ab xn--mix891f xn--mk1bu44c
+util_rb_tld xn--mxtq1m xn--ngbc5azd xn--ngbe9e0a xn--ngbrx xn--node xn--nqv7f
+util_rb_tld xn--nqv7fs00ema xn--nyqy26a xn--o3cw4h xn--ogbpf8fl xn--otu796d xn--p1acf
+util_rb_tld xn--p1ai xn--pgbs0dh xn--pssy2u xn--q7ce6a xn--q9jyb4c xn--qcka1pmc xn--qxa6a
+util_rb_tld xn--qxam xn--rhqv96g xn--rovu88b xn--rvc1e0am3e xn--s9brj9c xn--ses554g
+util_rb_tld xn--t60b56a xn--tckwe xn--tiq49xqyj xn--unup4y xn--vermgensberater-ctb
+util_rb_tld xn--vermgensberatung-pwb xn--vhquv xn--vuq861b xn--w4r85el8fhu5dnra xn--w4rs40l
+util_rb_tld xn--wgbh1c xn--wgbl6a xn--xhq521b xn--xkc2al3hye2a xn--xkc2dl3a5ee0h xn--y9a3aq
 util_rb_tld xn--yfro4i67o xn--ygbi2ammx xn--zfr164b
 endif
 
 # Standard List
 # For an up to date list of TLDs that can be pasted into this block, run this command:
-#  wget https://data.iana.org/TLD/tlds-alpha-by-domain.txt -q -O - | tail -n+2 | grep -vi '^xn--' | tr '\n' ' ' | fold -w 80 -s | perl -pe 'chomp; s/.*/util_rb_tld \L$_\n/'
+#  wget https://data.iana.org/TLD/tlds-alpha-by-domain.txt -q -O - | tail -n+2 | grep -vi '^xn--' | tr '\n' ' ' | fold -w 80 -s | perl -pe 's/\s+$//; s/.*/util_rb_tld \L$_\n/'
 
-util_rb_tld aaa aarp abarth abb abbott abbvie abc able abogado abudhabi ac academy 
-util_rb_tld accenture accountant accountants aco actor ad adac ads adult ae aeg aero aetna 
-util_rb_tld af afamilycompany afl africa ag agakhan agency ai aig airbus airforce airtel 
-util_rb_tld akdn al alfaromeo alibaba alipay allfinanz allstate ally alsace alstom am 
-util_rb_tld amazon americanexpress americanfamily amex amfam amica amsterdam analytics 
-util_rb_tld android anquan anz ao aol apartments app apple aq aquarelle ar arab aramco 
-util_rb_tld archi army arpa art arte as asda asia associates at athleta attorney au auction 
-util_rb_tld audi audible audio auspost author auto autos avianca aw aws ax axa az azure ba 
-util_rb_tld baby baidu banamex bananarepublic band bank bar barcelona barclaycard barclays 
-util_rb_tld barefoot bargains baseball basketball bauhaus bayern bb bbc bbt bbva bcg bcn bd 
-util_rb_tld be beats beauty beer bentley berlin best bestbuy bet bf bg bh bharti bi bible 
-util_rb_tld bid bike bing bingo bio biz bj black blackfriday blockbuster blog bloomberg 
-util_rb_tld blue bm bms bmw bn bnpparibas bo boats boehringer bofa bom bond boo book 
-util_rb_tld booking bosch bostik boston bot boutique box br bradesco bridgestone broadway 
-util_rb_tld broker brother brussels bs bt budapest bugatti build builders business buy buzz 
-util_rb_tld bv bw by bz bzh ca cab cafe cal call calvinklein cam camera camp cancerresearch 
-util_rb_tld canon capetown capital capitalone car caravan cards care career careers cars 
-util_rb_tld casa case caseih cash casino cat catering catholic cba cbn cbre cbs cc cd ceb 
-util_rb_tld center ceo cern cf cfa cfd cg ch chanel channel charity chase chat cheap 
-util_rb_tld chintai christmas chrome church ci cipriani circle cisco citadel citi citic 
-util_rb_tld city cityeats ck cl claims cleaning click clinic clinique clothing cloud club 
-util_rb_tld clubmed cm cn co coach codes coffee college cologne com comcast commbank 
-util_rb_tld community company compare computer comsec condos construction consulting 
-util_rb_tld contact contractors cooking cookingchannel cool coop corsica country coupon 
-util_rb_tld coupons courses cpa cr credit creditcard creditunion cricket crown crs cruise 
-util_rb_tld cruises csc cu cuisinella cv cw cx cy cymru cyou cz dabur dad dance data date 
-util_rb_tld dating datsun day dclk dds de deal dealer deals degree delivery dell deloitte 
-util_rb_tld delta democrat dental dentist desi design dev dhl diamonds diet digital direct 
-util_rb_tld directory discount discover dish diy dj dk dm dnp do docs doctor dog domains 
-util_rb_tld dot download drive dtv dubai duck dunlop dupont durban dvag dvr dz earth eat ec 
-util_rb_tld eco edeka edu education ee eg email emerck energy engineer engineering 
-util_rb_tld enterprises epson equipment er ericsson erni es esq estate et etisalat eu 
-util_rb_tld eurovision eus events exchange expert exposed express extraspace fage fail 
-util_rb_tld fairwinds faith family fan fans farm farmers fashion fast fedex feedback 
-util_rb_tld ferrari ferrero fi fiat fidelity fido film final finance financial fire 
-util_rb_tld firestone firmdale fish fishing fit fitness fj fk flickr flights flir florist 
-util_rb_tld flowers fly fm fo foo food foodnetwork football ford forex forsale forum 
-util_rb_tld foundation fox fr free fresenius frl frogans frontdoor frontier ftr fujitsu 
-util_rb_tld fujixerox fun fund furniture futbol fyi ga gal gallery gallo gallup game games 
-util_rb_tld gap garden gay gb gbiz gd gdn ge gea gent genting george gf gg ggee gh gi gift 
-util_rb_tld gifts gives giving gl glade glass gle global globo gm gmail gmbh gmo gmx gn 
-util_rb_tld godaddy gold goldpoint golf goo goodyear goog google gop got gov gp gq gr 
-util_rb_tld grainger graphics gratis green gripe grocery group gs gt gu guardian gucci guge 
-util_rb_tld guide guitars guru gw gy hair hamburg hangout haus hbo hdfc hdfcbank health 
-util_rb_tld healthcare help helsinki here hermes hgtv hiphop hisamitsu hitachi hiv hk hkt 
-util_rb_tld hm hn hockey holdings holiday homedepot homegoods homes homesense honda horse 
-util_rb_tld hospital host hosting hot hoteles hotels hotmail house how hr hsbc ht hu hughes 
-util_rb_tld hyatt hyundai ibm icbc ice icu id ie ieee ifm ikano il im imamat imdb immo 
-util_rb_tld immobilien in inc industries infiniti info ing ink institute insurance insure 
-util_rb_tld int intel international intuit investments io ipiranga iq ir irish is ismaili 
-util_rb_tld ist istanbul it itau itv iveco jaguar java jcb jcp je jeep jetzt jewelry jio 
-util_rb_tld jll jm jmp jnj jo jobs joburg jot joy jp jpmorgan jprs juegos juniper kaufen 
-util_rb_tld kddi ke kerryhotels kerrylogistics kerryproperties kfh kg kh ki kia kim kinder 
-util_rb_tld kindle kitchen kiwi km kn koeln komatsu kosher kp kpmg kpn kr krd kred 
-util_rb_tld kuokgroup kw ky kyoto kz la lacaixa lamborghini lamer lancaster lancia land 
-util_rb_tld landrover lanxess lasalle lat latino latrobe law lawyer lb lc lds lease leclerc 
-util_rb_tld lefrak legal lego lexus lgbt li lidl life lifeinsurance lifestyle lighting like 
-util_rb_tld lilly limited limo lincoln linde link lipsy live living lixil lk llc llp loan 
-util_rb_tld loans locker locus loft lol london lotte lotto love lpl lplfinancial lr ls lt 
-util_rb_tld ltd ltda lu lundbeck lupin luxe luxury lv ly ma macys madrid maif maison makeup 
-util_rb_tld man management mango map market marketing markets marriott marshalls maserati 
-util_rb_tld mattel mba mc mckinsey md me med media meet melbourne meme memorial men menu 
-util_rb_tld merckmsd metlife mg mh miami microsoft mil mini mint mit mitsubishi mk ml mlb 
-util_rb_tld mls mm mma mn mo mobi mobile moda moe moi mom monash money monster mormon 
-util_rb_tld mortgage moscow moto motorcycles mov movie mp mq mr ms msd mt mtn mtr mu museum 
-util_rb_tld mutual mv mw mx my mz na nab nagoya name nationwide natura navy nba nc ne nec 
-util_rb_tld net netbank netflix network neustar new newholland news next nextdirect nexus 
-util_rb_tld nf nfl ng ngo nhk ni nico nike nikon ninja nissan nissay nl no nokia 
-util_rb_tld northwesternmutual norton now nowruz nowtv np nr nra nrw ntt nu nyc nz obi 
-util_rb_tld observer off office okinawa olayan olayangroup oldnavy ollo om omega one ong 
-util_rb_tld onl online onyourside ooo open oracle orange org organic origins osaka otsuka 
-util_rb_tld ott ovh pa page panasonic paris pars partners parts party passagens pay pccw pe 
-util_rb_tld pet pf pfizer pg ph pharmacy phd philips phone photo photography photos physio 
-util_rb_tld pics pictet pictures pid pin ping pink pioneer pizza pk pl place play 
-util_rb_tld playstation plumbing plus pm pn pnc pohl poker politie porn post pr pramerica 
-util_rb_tld praxi press prime pro prod productions prof progressive promo properties 
-util_rb_tld property protection pru prudential ps pt pub pw pwc py qa qpon quebec quest qvc 
-util_rb_tld racing radio raid re read realestate realtor realty recipes red redstone 
-util_rb_tld redumbrella rehab reise reisen reit reliance ren rent rentals repair report 
-util_rb_tld republican rest restaurant review reviews rexroth rich richardli ricoh 
-util_rb_tld rightathome ril rio rip rmit ro rocher rocks rodeo rogers room rs rsvp ru rugby 
-util_rb_tld ruhr run rw rwe ryukyu sa saarland safe safety sakura sale salon samsclub 
-util_rb_tld samsung sandvik sandvikcoromant sanofi sap sarl sas save saxo sb sbi sbs sc sca 
-util_rb_tld scb schaeffler schmidt scholarships school schule schwarz science scjohnson 
-util_rb_tld scot sd se search seat secure security seek select sener services ses seven sew 
-util_rb_tld sex sexy sfr sg sh shangrila sharp shaw shell shia shiksha shoes shop shopping 
-util_rb_tld shouji show showtime shriram si silk sina singles site sj sk ski skin sky skype 
-util_rb_tld sl sling sm smart smile sn sncf so soccer social softbank software sohu solar 
-util_rb_tld solutions song sony soy space sport spot spreadbetting sr srl ss st stada 
-util_rb_tld staples star statebank statefarm stc stcgroup stockholm storage store stream 
-util_rb_tld studio study style su sucks supplies supply support surf surgery suzuki sv 
-util_rb_tld swatch swiftcover swiss sx sy sydney symantec systems sz tab taipei talk taobao 
-util_rb_tld target tatamotors tatar tattoo tax taxi tc tci td tdk team tech technology tel 
-util_rb_tld temasek tennis teva tf tg th thd theater theatre tiaa tickets tienda tiffany 
-util_rb_tld tips tires tirol tj tjmaxx tjx tk tkmaxx tl tm tmall tn to today tokyo tools 
-util_rb_tld top toray toshiba total tours town toyota toys tr trade trading training travel 
-util_rb_tld travelchannel travelers travelersinsurance trust trv tt tube tui tunes tushu tv 
-util_rb_tld tvs tw tz ua ubank ubs ug uk unicom university uno uol ups us uy uz va 
-util_rb_tld vacations vana vanguard vc ve vegas ventures verisign versicherung vet vg vi 
-util_rb_tld viajes video vig viking villas vin vip virgin visa vision viva vivo vlaanderen 
-util_rb_tld vn vodka volkswagen volvo vote voting voto voyage vu vuelos wales walmart 
-util_rb_tld walter wang wanggou watch watches weather weatherchannel webcam weber website 
-util_rb_tld wed wedding weibo weir wf whoswho wien wiki williamhill win windows wine 
-util_rb_tld winners wme wolterskluwer woodside work works world wow ws wtc wtf xbox xerox 
-util_rb_tld xfinity xihuan xin xxx xyz yachts yahoo yamaxun yandex ye yodobashi yoga 
+# Updated 2022-10-18
+util_rb_tld aaa aarp abarth abb abbott abbvie abc able abogado abudhabi ac academy
+util_rb_tld accenture accountant accountants aco actor ad adac ads adult ae aeg aero aetna
+util_rb_tld af afl africa ag agakhan agency ai aig airbus airforce airtel akdn al alfaromeo
+util_rb_tld alibaba alipay allfinanz allstate ally alsace alstom am amazon americanexpress
+util_rb_tld americanfamily amex amfam amica amsterdam analytics android anquan anz ao aol
+util_rb_tld apartments app apple aq aquarelle ar arab aramco archi army arpa art arte as
+util_rb_tld asda asia associates at athleta attorney au auction audi audible audio auspost
+util_rb_tld author auto autos avianca aw aws ax axa az azure ba baby baidu banamex
+util_rb_tld bananarepublic band bank bar barcelona barclaycard barclays barefoot bargains
+util_rb_tld baseball basketball bauhaus bayern bb bbc bbt bbva bcg bcn bd be beats beauty
+util_rb_tld beer bentley berlin best bestbuy bet bf bg bh bharti bi bible bid bike bing
+util_rb_tld bingo bio biz bj black blackfriday blockbuster blog bloomberg blue bm bms bmw
+util_rb_tld bn bnpparibas bo boats boehringer bofa bom bond boo book booking bosch bostik
+util_rb_tld boston bot boutique box br bradesco bridgestone broadway broker brother
+util_rb_tld brussels bs bt build builders business buy buzz bv bw by bz bzh ca cab cafe cal
+util_rb_tld call calvinklein cam camera camp canon capetown capital capitalone car caravan
+util_rb_tld cards care career careers cars casa case cash casino cat catering catholic cba
+util_rb_tld cbn cbre cbs cc cd center ceo cern cf cfa cfd cg ch chanel channel charity
+util_rb_tld chase chat cheap chintai christmas chrome church ci cipriani circle cisco
+util_rb_tld citadel citi citic city cityeats ck cl claims cleaning click clinic clinique
+util_rb_tld clothing cloud club clubmed cm cn co coach codes coffee college cologne com
+util_rb_tld comcast commbank community company compare computer comsec condos construction
+util_rb_tld consulting contact contractors cooking cookingchannel cool coop corsica country
+util_rb_tld coupon coupons courses cpa cr credit creditcard creditunion cricket crown crs
+util_rb_tld cruise cruises cu cuisinella cv cw cx cy cymru cyou cz dabur dad dance data
+util_rb_tld date dating datsun day dclk dds de deal dealer deals degree delivery dell
+util_rb_tld deloitte delta democrat dental dentist desi design dev dhl diamonds diet
+util_rb_tld digital direct directory discount discover dish diy dj dk dm dnp do docs doctor
+util_rb_tld dog domains dot download drive dtv dubai dunlop dupont durban dvag dvr dz earth
+util_rb_tld eat ec eco edeka edu education ee eg email emerck energy engineer engineering
+util_rb_tld enterprises epson equipment er ericsson erni es esq estate et etisalat eu
+util_rb_tld eurovision eus events exchange expert exposed express extraspace fage fail
+util_rb_tld fairwinds faith family fan fans farm farmers fashion fast fedex feedback
+util_rb_tld ferrari ferrero fi fiat fidelity fido film final finance financial fire
+util_rb_tld firestone firmdale fish fishing fit fitness fj fk flickr flights flir florist
+util_rb_tld flowers fly fm fo foo food foodnetwork football ford forex forsale forum
+util_rb_tld foundation fox fr free fresenius frl frogans frontdoor frontier ftr fujitsu fun
+util_rb_tld fund furniture futbol fyi ga gal gallery gallo gallup game games gap garden gay
+util_rb_tld gb gbiz gd gdn ge gea gent genting george gf gg ggee gh gi gift gifts gives
+util_rb_tld giving gl glass gle global globo gm gmail gmbh gmo gmx gn godaddy gold
+util_rb_tld goldpoint golf goo goodyear goog google gop got gov gp gq gr grainger graphics
+util_rb_tld gratis green gripe grocery group gs gt gu guardian gucci guge guide guitars
+util_rb_tld guru gw gy hair hamburg hangout haus hbo hdfc hdfcbank health healthcare help
+util_rb_tld helsinki here hermes hgtv hiphop hisamitsu hitachi hiv hk hkt hm hn hockey
+util_rb_tld holdings holiday homedepot homegoods homes homesense honda horse hospital host
+util_rb_tld hosting hot hoteles hotels hotmail house how hr hsbc ht hu hughes hyatt hyundai
+util_rb_tld ibm icbc ice icu id ie ieee ifm ikano il im imamat imdb immo immobilien in inc
+util_rb_tld industries infiniti info ing ink institute insurance insure int international
+util_rb_tld intuit investments io ipiranga iq ir irish is ismaili ist istanbul it itau itv
+util_rb_tld jaguar java jcb je jeep jetzt jewelry jio jll jm jmp jnj jo jobs joburg jot joy
+util_rb_tld jp jpmorgan jprs juegos juniper kaufen kddi ke kerryhotels kerrylogistics
+util_rb_tld kerryproperties kfh kg kh ki kia kids kim kinder kindle kitchen kiwi km kn
+util_rb_tld koeln komatsu kosher kp kpmg kpn kr krd kred kuokgroup kw ky kyoto kz la
+util_rb_tld lacaixa lamborghini lamer lancaster lancia land landrover lanxess lasalle lat
+util_rb_tld latino latrobe law lawyer lb lc lds lease leclerc lefrak legal lego lexus lgbt
+util_rb_tld li lidl life lifeinsurance lifestyle lighting like lilly limited limo lincoln
+util_rb_tld linde link lipsy live living lk llc llp loan loans locker locus loft lol london
+util_rb_tld lotte lotto love lpl lplfinancial lr ls lt ltd ltda lu lundbeck luxe luxury lv
+util_rb_tld ly ma macys madrid maif maison makeup man management mango map market marketing
+util_rb_tld markets marriott marshalls maserati mattel mba mc mckinsey md me med media meet
+util_rb_tld melbourne meme memorial men menu merckmsd mg mh miami microsoft mil mini mint
+util_rb_tld mit mitsubishi mk ml mlb mls mm mma mn mo mobi mobile moda moe moi mom monash
+util_rb_tld money monster mormon mortgage moscow moto motorcycles mov movie mp mq mr ms msd
+util_rb_tld mt mtn mtr mu museum music mutual mv mw mx my mz na nab nagoya name natura navy
+util_rb_tld nba nc ne nec net netbank netflix network neustar new news next nextdirect
+util_rb_tld nexus nf nfl ng ngo nhk ni nico nike nikon ninja nissan nissay nl no nokia
+util_rb_tld northwesternmutual norton now nowruz nowtv np nr nra nrw ntt nu nyc nz obi
+util_rb_tld observer office okinawa olayan olayangroup oldnavy ollo om omega one ong onl
+util_rb_tld online ooo open oracle orange org organic origins osaka otsuka ott ovh pa page
+util_rb_tld panasonic paris pars partners parts party passagens pay pccw pe pet pf pfizer
+util_rb_tld pg ph pharmacy phd philips phone photo photography photos physio pics pictet
+util_rb_tld pictures pid pin ping pink pioneer pizza pk pl place play playstation plumbing
+util_rb_tld plus pm pn pnc pohl poker politie porn post pr pramerica praxi press prime pro
+util_rb_tld prod productions prof progressive promo properties property protection pru
+util_rb_tld prudential ps pt pub pw pwc py qa qpon quebec quest racing radio re read
+util_rb_tld realestate realtor realty recipes red redstone redumbrella rehab reise reisen
+util_rb_tld reit reliance ren rent rentals repair report republican rest restaurant review
+util_rb_tld reviews rexroth rich richardli ricoh ril rio rip ro rocher rocks rodeo rogers
+util_rb_tld room rs rsvp ru rugby ruhr run rw rwe ryukyu sa saarland safe safety sakura
+util_rb_tld sale salon samsclub samsung sandvik sandvikcoromant sanofi sap sarl sas save
+util_rb_tld saxo sb sbi sbs sc sca scb schaeffler schmidt scholarships school schule
+util_rb_tld schwarz science scot sd se search seat secure security seek select sener
+util_rb_tld services ses seven sew sex sexy sfr sg sh shangrila sharp shaw shell shia
+util_rb_tld shiksha shoes shop shopping shouji show showtime si silk sina singles site sj
+util_rb_tld sk ski skin sky skype sl sling sm smart smile sn sncf so soccer social softbank
+util_rb_tld software sohu solar solutions song sony soy spa space sport spot sr srl ss st
+util_rb_tld stada staples star statebank statefarm stc stcgroup stockholm storage store
+util_rb_tld stream studio study style su sucks supplies supply support surf surgery suzuki
+util_rb_tld sv swatch swiss sx sy sydney systems sz tab taipei talk taobao target
+util_rb_tld tatamotors tatar tattoo tax taxi tc tci td tdk team tech technology tel temasek
+util_rb_tld tennis teva tf tg th thd theater theatre tiaa tickets tienda tiffany tips tires
+util_rb_tld tirol tj tjmaxx tjx tk tkmaxx tl tm tmall tn to today tokyo tools top toray
+util_rb_tld toshiba total tours town toyota toys tr trade trading training travel
+util_rb_tld travelchannel travelers travelersinsurance trust trv tt tube tui tunes tushu tv
+util_rb_tld tvs tw tz ua ubank ubs ug uk unicom university uno uol ups us uy uz va
+util_rb_tld vacations vana vanguard vc ve vegas ventures verisign versicherung vet vg vi
+util_rb_tld viajes video vig viking villas vin vip virgin visa vision viva vivo vlaanderen
+util_rb_tld vn vodka volkswagen volvo vote voting voto voyage vu vuelos wales walmart
+util_rb_tld walter wang wanggou watch watches weather weatherchannel webcam weber website
+util_rb_tld wed wedding weibo weir wf whoswho wien wiki williamhill win windows wine
+util_rb_tld winners wme wolterskluwer woodside work works world wow ws wtc wtf xbox xerox
+util_rb_tld xfinity xihuan xin xxx xyz yachts yahoo yamaxun yandex ye yodobashi yoga
 util_rb_tld yokohama you youtube yt yun za zappos zara zero zip zm zone zuerich zw
 
 #
@@ -450,7 +449,7 @@ util_rb_2tld nextmail.ru
 util_rb_2tld nightmail.ru
 util_rb_2tld nm.ru
 util_rb_2tld notlong.com
-util_rb_2tld page.tl
+util_rb_2tld page.tl page.link
 util_rb_2tld pochta.ru
 util_rb_2tld pochtamt.ru
 util_rb_2tld pop3.ru
@@ -740,6 +739,7 @@ util_rb_3tld blogspot.com.es
 util_rb_3tld no-ip.co.uk
 #
 util_rb_3tld mobile.web.tr
+util_rb_3tld ct.sendgrid.net
 
 endif
 
index 1046cba46592deda622fbc0c96f0e152eaf5c893..18cef69b6b7746ac59ca0c5b225e306d22ed5ab8 100644 (file)
@@ -1,9 +1,6 @@
 # DO NOT EDIT: file generated by build/mkupdates/listpromotable
 # active ruleset list, automatically generated from https://ruleqa.spamassassin.org/
-# with results from: last-net: net-axb-coi-bulk net-axb-generic net-axb-ham-misc net-darxus net-ena-week0 net-ena-week1 net-ena-week2 net-ena-week3 net-ena-week4 net-grenier net-jhardin net-llanga net-mmiroslaw-mails-ham net-mmiroslaw-mails-spam net-pds net-sihde net-spamsponge net-thendrikx; day 1: axb-coi-bulk axb-generic axb-ham-misc darxus ena-week0 ena-week1 ena-week2 ena-week3 ena-week4 giovanni-ham giovanni-spam giovanni-spammy grenier jhardin llanga mmiroslaw-mails-ham mmiroslaw-mails-spam pds sihde spamsponge thendrikx; day 2: axb-coi-bulk axb-generic axb-ham-misc darxus ena-week0 ena-week1 ena-week2 ena-week3 ena-week4 giovanni-ham giovanni-spam giovanni-spammy grenier jhardin llanga mmiroslaw-mails-ham mmiroslaw-mails-spam pds sihde spamsponge thendrikx; day 3: axb-coi-bulk axb-generic axb-ham-misc darxus ena-week0 ena-week1 ena-week2 ena-week3 ena-week4 giovanni-ham giovanni-spam giovanni-spammy grenier jhardin llanga mmiroslaw-mails-ham mmiroslaw-mails-spam pds sihde spamsponge thendrikx; day 4: axb-coi-bulk axb-generic axb-ham-misc darxus ena-week0 ena-week1 ena-week2 ena-week3 ena-week4 giovanni-ham giovanni-spam giovanni-spammy grenier jhardin llanga mmiroslaw-mails-ham mmiroslaw-mails-spam pds sihde spamsponge thendrikx; day 5: axb-coi-bulk axb-generic axb-ham-misc darxus ena-week0 ena-week1 ena-week2 ena-week3 ena-week4 giovanni-ham giovanni-spam giovanni-spammy grenier jhardin llanga mmiroslaw-mails-ham mmiroslaw-mails-spam pds sihde spamsponge thendrikx
-
-# good enough
-ACCT_PHISHING_MANY
+# with results from: last-net: net-ena-week0 net-ena-week1 net-ena-week2 net-ena-week3 net-ena-week4 net-giovanni-ham net-giovanni-spam net-giovanni-spammy net-grenier net-hege net-jhardin net-llanga net-mmiroslaw-mails-ham net-mmiroslaw-mails-spam net-spamsponge net-thendrikx net-tsz- corpus; day 1: ena-week0 ena-week1 ena-week2 ena-week3 ena-week4 giovanni-ham giovanni-spam giovanni-spammy grenier hege jhardin llanga mmiroslaw-mails-ham mmiroslaw-mails-spam spamsponge thendrikx tsz- corpus; day 2: ena-week0 ena-week1 ena-week2 ena-week3 ena-week4 giovanni-ham giovanni-spam giovanni-spammy grenier hege jhardin llanga mmiroslaw-mails-ham mmiroslaw-mails-spam spamsponge thendrikx tsz- corpus; day 3: ena-week0 ena-week1 ena-week2 ena-week3 ena-week4 giovanni-ham giovanni-spam giovanni-spammy grenier hege jhardin llanga mmiroslaw-mails-ham mmiroslaw-mails-spam spamsponge thendrikx tsz- corpus; day 4: ena-week0 ena-week1 ena-week2 ena-week3 ena-week4 giovanni-ham giovanni-spam giovanni-spammy grenier hege jhardin llanga mmiroslaw-mails-ham mmiroslaw-mails-spam spamsponge thendrikx tsz- corpus; day 5: ena-week0 ena-week1 ena-week2 ena-week3 ena-week4 giovanni-ham giovanni-spam giovanni-spammy grenier hege jhardin llanga mmiroslaw-mails-ham mmiroslaw-mails-spam spamsponge thendrikx tsz- corpus
 
 # tflags publish
 AC_BR_BONANZA
@@ -53,6 +50,9 @@ ADMAIL
 # tflags publish
 ADMITS_SPAM
 
+# tflags publish
+ADULT_DATING_COMPANY
+
 # tflags publish
 ADVANCE_FEE_2_NEW_FORM
 
@@ -116,11 +116,23 @@ APP_DEVELOPMENT_FREEM
 # tflags publish
 APP_DEVELOPMENT_NORDNS
 
+# tflags net
+ARC_INVALID
+
+# tflags net
+ARC_SIGNED
+
+# tflags net
+ARC_VALID
+
 # good enough
 AXB_XMAILER_MIMEOLE_OL_024C2
 
 # good enough
-AXB_XMAILER_MIMEOLE_OL_1ECD5
+AXB_X_FF_SEZ_S
+
+# good enough
+BAT_BDRY_TO_MALF
 
 # tflags learn
 BAYES_00
@@ -152,6 +164,9 @@ BAYES_99
 # tflags learn
 BAYES_999
 
+# tflags publish
+BEBEE_IMG_NOT_RCVD_BB
+
 # tflags publish
 BIGNUM_EMAILS_FREEM
 
@@ -176,7 +191,7 @@ BITCOIN_IMGUR
 # good enough
 BITCOIN_MALF_HTML
 
-# tflags net
+# tflags publish
 BITCOIN_MALWARE
 
 # tflags publish
@@ -236,13 +251,7 @@ BITCOIN_XPRIO
 # tflags publish
 BITCOIN_YOUR_INFO
 
-# good enough
-BODY_SINGLE_URI
-
-# good enough
-BODY_SINGLE_WORD
-
-# tflags net
+# tflags publish
 BODY_URI_ONLY
 
 # tflags publish
@@ -272,6 +281,9 @@ CHARSET_FARAWAY
 # tflags userconf
 CHARSET_FARAWAY_HEADER
 
+# good enough
+CK_HELO_GENERIC
+
 # tflags publish
 CN_B2B_SPAMMER
 
@@ -284,6 +296,9 @@ COMPENSATION
 # tflags publish
 CONTENT_AFTER_HTML
 
+# tflags publish
+CONTENT_AFTER_HTML_WEAK
+
 # tflags publish
 CORRUPT_FROM_LINE_IN_HDRS
 
@@ -291,10 +306,7 @@ CORRUPT_FROM_LINE_IN_HDRS
 CTE_8BIT_MISMATCH
 
 # good enough
-CTYPE_NULL
-
-# good enough
-DATE_IN_FUTURE_96_Q
+DATE_IN_FUTURE_Q_PLUS
 
 # tflags publish
 DAY_I_EARNED
@@ -302,6 +314,9 @@ DAY_I_EARNED
 # good enough
 DEAR_BENEFICIARY
 
+# good enough
+DEAR_WINNER
+
 # tflags net
 DIGEST_MULTIPLE
 
@@ -353,6 +368,21 @@ DKIM_VALID_AU
 # tflags net
 DKIM_VALID_EF
 
+# tflags net
+DMARC_MISSING
+
+# tflags net
+DMARC_NONE
+
+# tflags net
+DMARC_PASS
+
+# tflags net
+DMARC_QUAR
+
+# tflags net
+DMARC_REJECT
+
 # tflags publish
 DOS_ANAL_SPAM_MAILER
 
@@ -362,15 +392,15 @@ DOS_DEREK_AUG08
 # good enough
 DOS_OE_TO_MX
 
+# good enough
+DOS_OE_TO_MX_IMAGE
+
 # good enough
 DOS_OUTLOOK_TO_MX
 
 # tflags publish
 DOTGOV_IMAGE
 
-# good enough
-DSN_NO_MIMEVERSION
-
 # tflags publish
 DX_TEXT_02
 
@@ -398,14 +428,8 @@ ENVFROM_GOOG_TRIX
 # tflags net
 ENV_AND_HDR_SPF_MATCH
 
-# good enough
-FAKE_REPLY_A1
-
-# good enough
-FAKE_REPLY_B
-
-# good enough
-FAKE_REPLY_C
+# tflags publish
+FACEBOOK_IMG_NOT_RCVD_FB
 
 # tflags publish
 FBI_MONEY
@@ -443,18 +467,15 @@ FONT_INVIS_POSTEXTRAS
 # tflags net
 FORGED_SPF_HELO
 
-# tflags net
+# tflags publish
 FORM_FRAUD
 
-# tflags net
+# tflags publish
 FORM_FRAUD_3
 
 # tflags publish
 FORM_FRAUD_5
 
-# tflags net
-FORM_LOW_CONTRAST
-
 # tflags publish
 FOUND_YOU
 
@@ -533,9 +554,6 @@ FROM_MISSP_REPLYTO
 # tflags net
 FROM_MISSP_SPF_FAIL
 
-# good enough
-FROM_MISSP_TO_UNDISC
-
 # good enough
 FROM_MISSP_USER
 
@@ -554,9 +572,6 @@ FROM_NTLD_REPLY_FREEMAIL
 # tflags net
 FROM_NUMBERO_NEWDOMAIN
 
-# good enough
-FROM_NUMERIC_TLD
-
 # tflags net
 FROM_PAYPAL_SPOOF
 
@@ -647,6 +662,15 @@ FUZZY_WALLET
 # tflags publish
 GAPPY_SALES_LEADS_FREEM
 
+# good enough
+GB_BITCOIN_CP
+
+# good enough
+GB_BITCOIN_NH
+
+# tflags publish
+GB_CUSTOM_HTM_URI
+
 # tflags publish
 GB_FAKE_RF_SHORT
 
@@ -662,6 +686,15 @@ GB_FREEMAIL_DISPTO_NOTFREEM
 # tflags publish
 GB_GOOGLE_OBFUR
 
+# tflags net
+GB_HASHBL_BTC
+
+# tflags publish
+GB_STORAGE_GOOGLE_EMAIL
+
+# good enough
+GB_URI_FLEEK_STO_HTM
+
 # tflags publish
 GOOGLE_DOCS_PHISH
 
@@ -680,12 +713,18 @@ GOOG_MALWARE_DNLD
 # tflags publish
 GOOG_REDIR_DOCUSIGN
 
+# good enough
+GOOG_REDIR_HTML_ONLY
+
 # good enough
 GOOG_REDIR_NORDNS
 
 # tflags publish
 GOOG_REDIR_SHORT
 
+# tflags publish
+GOOG_STO_EMAIL_PHISH
+
 # tflags publish
 GOOG_STO_HTML_PHISH
 
@@ -710,9 +749,6 @@ HAS_X_NO_RELAY
 # tflags publish
 HAS_X_OUTGOING_SPAM_STAT
 
-# tflags net
-HDRS_LCASE
-
 # good enough
 HDRS_LCASE_IMGONLY
 
@@ -731,9 +767,6 @@ HEADER_FROM_DIFFERENT_DOMAINS
 # tflags userconf
 HEAD_LONG
 
-# good enough
-HELO_LH_HOME
-
 # good enough
 HELO_LOCALHOST
 
@@ -747,10 +780,10 @@ HEXHASH_WORD
 HK_CTE_RAW
 
 # good enough
-HK_NAME_DRUGS
+HK_LOTTO
 
 # good enough
-HK_NAME_FM_MR_MRS
+HK_NAME_DRUGS
 
 # good enough
 HK_NAME_MR_MRS
@@ -770,9 +803,6 @@ HK_RCVD_IP_MULTICAST
 # tflags publish
 HK_SCAM
 
-# good enough
-HK_WIN
-
 # tflags publish
 HOSTED_IMG_DIRECT_MX
 
@@ -797,15 +827,21 @@ HTML_ENTITY_ASCII
 # tflags publish
 HTML_ENTITY_ASCII_TINY
 
+# good enough
+HTML_FONT_TINY_NORDNS
+
 # tflags publish
 HTML_OFF_PAGE
 
 # tflags publish
 HTML_SHRT_CMNT_OBFU_MANY
 
-# tflags net
+# tflags publish
 HTML_SINGLET_MANY
 
+# good enough
+HTML_TAG_BALANCE_CENTER
+
 # tflags net
 HTML_TEXT_INVISIBLE_FONT
 
@@ -824,18 +860,24 @@ JH_SPAMMY_PATTERN01
 # tflags publish
 JH_SPAMMY_PATTERN02
 
-# tflags net
-KHOP_FAKE_EBAY
-
 # tflags net
 KHOP_HELO_FCRDNS
 
+# good enough
+KHOP_JS_OBFUSCATION
+
+# tflags publish
+LINKEDIN_IMG_NOT_RCVD_LNKN
+
 # tflags publish
 LIST_PRTL_PUMPDUMP
 
 # tflags publish
 LIST_PRTL_SAME_USER
 
+# good enough
+LONGLN_LOW_CONTRAST
+
 # tflags publish
 LONG_HEX_URI
 
@@ -848,15 +890,15 @@ LONG_INVISIBLE_TEXT
 # tflags publish
 LOTS_OF_MONEY
 
+# good enough
+LOTTO_AGENT
+
 # good enough
 LOTTO_DEPT
 
 # tflags publish
 LUCRATIVE
 
-# good enough
-MALFORMED_FREEMAIL
-
 # tflags publish
 MALF_HTML_B64
 
@@ -869,9 +911,6 @@ MALWARE_PASSWORD
 # tflags publish
 MALW_ATTACH
 
-# tflags net
-MANY_HDRS_LCASE
-
 # tflags publish
 MANY_SPAN_IN_TEXT
 
@@ -902,9 +941,6 @@ MIXED_AREA_CASE
 # tflags publish
 MIXED_CENTER_CASE
 
-# good enough
-MIXED_CTYPE_CASE
-
 # tflags publish
 MIXED_ES
 
@@ -956,18 +992,12 @@ MONEY_FROM_41
 # good enough
 MONEY_FROM_MISSP
 
-# good enough
-MONEY_NOHTML
-
 # tflags publish
 MSGID_DOLLARS_URI_IMG
 
 # tflags publish
 MSGID_HDR_MALF
 
-# good enough
-MSGID_NOFQDN1
-
 # good enough
 MSMAIL_PRI_ABNORMAL
 
@@ -986,6 +1016,9 @@ NA_DOLLARS
 # tflags publish
 NEWEGG_IMG_NOT_RCVD_NEGG
 
+# tflags publish
+NEW_PRODUCTS
+
 # good enough
 NICE_REPLY_A
 
@@ -998,7 +1031,7 @@ NML_ADSP_CUSTOM_LOW
 # tflags net
 NML_ADSP_CUSTOM_MED
 
-# tflags net
+# good enough
 NORDNS_LOW_CONTRAST
 
 # tflags publish
@@ -1023,7 +1056,7 @@ NSL_RCVD_FROM_USER
 NSL_RCVD_HELO_USER
 
 # good enough
-NUMBEREND_LINKBAIT
+NUMBERONLY_BITCOIN_EXP
 
 # tflags publish
 OBFU_BITCOIN
@@ -1040,12 +1073,6 @@ OBFU_UNSUB_UL
 # tflags publish
 ODD_FREEM_REPTO
 
-# good enough
-OFFER_ONLY_AMERICA
-
-# good enough
-ORDER_TODAY
-
 # good enough
 PDS_BAD_THREAD_QP_64
 
@@ -1056,26 +1083,14 @@ PDS_BTC_ID
 PDS_BTC_MSGID
 
 # good enough
-PDS_DBL_URL_TNB_RUNON
-
-# good enough
-PDS_EMPTYSUBJ_URISHRT
+PDS_BTC_NTLD
 
 # good enough
-PDS_FRNOM_TODOM_DBL_URL
+PDS_DBL_URL_TNB_RUNON
 
 # good enough
-PDS_FRNOM_TODOM_NAKED_TO
-
-# tflags net
 PDS_FROM_2_EMAILS
 
-# good enough
-PDS_FROM_2_EMAILS_SHRTNER
-
-# good enough
-PDS_FROM_NAME_TO_DOMAIN
-
 # tflags net
 PDS_HELO_SPF_FAIL
 
@@ -1086,10 +1101,7 @@ PDS_NAKED_TO_NUMERO
 PDS_NO_FULL_NAME_SPOOFED_URL
 
 # good enough
-PDS_OTHER_BAD_TLD
-
-# good enough
-PDS_SHORTFWD_URISHRT_FP
+PDS_RDNS_DYNAMIC_FP
 
 # good enough
 PDS_SHORT_SPOOFED_URL
@@ -1100,18 +1112,6 @@ PDS_TINYSUBJ_URISHRT
 # good enough
 PDS_TONAME_EQ_TOLOCAL_FREEM_FORGE
 
-# good enough
-PDS_TONAME_EQ_TOLOCAL_HDRS_LCASE
-
-# good enough
-PDS_TONAME_EQ_TOLOCAL_SHORT
-
-# good enough
-PDS_TONAME_EQ_TOLOCAL_VSHORT
-
-# good enough
-PDS_TO_EQ_FROM_NAME
-
 # tflags publish
 PHISH_ATTACH
 
@@ -1121,12 +1121,6 @@ PHISH_AZURE_CLOUDAPP
 # tflags publish
 PHISH_FBASEAPP
 
-# good enough
-PHOTO_EDITING_DIRECT
-
-# good enough
-PHOTO_EDITING_FREEM
-
 # tflags publish
 PHP_NOVER_MUA
 
@@ -1139,6 +1133,18 @@ PHP_SCRIPT
 # tflags publish
 PHP_SCRIPT_MUA
 
+# tflags publish
+POSSIBLE_APPLE_PHISH_02
+
+# tflags publish
+POSSIBLE_EBAY_PHISH_02
+
+# tflags publish
+POSSIBLE_PAYPAL_PHISH_01
+
+# tflags publish
+POSSIBLE_PAYPAL_PHISH_02
+
 # tflags publish
 PP_MIME_FAKE_ASCII_TEXT
 
@@ -1343,15 +1349,6 @@ RCVD_IN_PBL
 # tflags net
 RCVD_IN_PSBL
 
-# tflags net
-RCVD_IN_RP_CERTIFIED
-
-# tflags net
-RCVD_IN_RP_RNBL
-
-# tflags net
-RCVD_IN_RP_SAFE
-
 # tflags net
 RCVD_IN_SBL
 
@@ -1454,8 +1451,32 @@ REPTO_419_FRAUD_YJ
 # tflags publish
 REPTO_419_FRAUD_YN
 
+# tflags publish
+REPTO_INFONUMSCOM
+
+# tflags publish
+SCC_BOGUS_CTE_1
+
 # good enough
-RISK_FREE
+SCC_CANSPAM_1
+
+# good enough
+SCC_CANSPAM_2
+
+# tflags publish
+SCC_CTMPP
+
+# tflags publish
+SCC_ISEMM_LID_1
+
+# tflags publish
+SCC_ISEMM_LID_1A
+
+# tflags publish
+SCC_ISEMM_LID_1B
+
+# tflags publish
+SCC_SPECIAL_GUID
 
 # tflags publish
 SENDGRID_REDIR
@@ -1475,18 +1496,12 @@ SHOPIFY_IMG_NOT_RCVD_SFY
 # tflags publish
 SHORTENER_SHORT_IMG
 
-# good enough
-SHORTENER_SHORT_SUBJ
-
 # tflags publish
 SHORT_IMG_SUSP_NTLD
 
 # good enough
 SHORT_SHORTNER
 
-# tflags net
-SINGLETS_LOW_CONTRAST
-
 # tflags net
 SPF_FAIL
 
@@ -1538,17 +1553,17 @@ SPOOF_GMAIL_MID
 # tflags publish
 STATIC_XPRIO_OLE
 
-# tflags net
-STOCK_LOW_CONTRAST
-
 # tflags publish
 STOCK_TIP
 
+# good enough
+STOX_BOUND_090909_B
+
 # tflags userconf
-SUBJECT_IN_BLACKLIST
+SUBJECT_IN_BLOCKLIST
 
 # tflags userconf
-SUBJECT_IN_WHITELIST
+SUBJECT_IN_WELCOMELIST
 
 # good enough
 SUBJ_ATTENTION
@@ -1556,19 +1571,25 @@ SUBJ_ATTENTION
 # tflags net
 SUBJ_BRKN_WORDNUMS
 
-# tflags net
-SUBJ_UNNEEDED_HTML
-
 # tflags net
 SURBL_BLOCKED
 
+# good enough
+SUSP_UTF8_WORD_SUBJ
+
 # tflags publish
 SYSADMIN
 
-# tflags net
+# tflags publish
+TAGSTAT_IMG_NOT_RCVD_TGST
+
+# tflags publish
+TARINGANET_IMG_NOT_RCVD_TN
+
+# tflags publish
 TEQF_USR_IMAGE
 
-# tflags net
+# tflags publish
 TEQF_USR_MSGID_HEX
 
 # tflags net
@@ -1580,30 +1601,15 @@ THIS_AD
 # tflags publish
 THIS_IS_ADV_SUSP_NTLD
 
-# good enough
-THREAD_INDEX_HEX
-
 # tflags publish
 TONLINE_FAKE_DKIM
 
-# good enough
-TONOM_EQ_TOLOC_SHRT_SHRTNER
-
 # tflags publish
 TO_EQ_FM_DIRECT_MX
 
-# tflags net
-TO_EQ_FM_DOM_HTML_IMG
-
-# tflags net
-TO_EQ_FM_DOM_HTML_ONLY
-
 # tflags net
 TO_EQ_FM_DOM_SPF_FAIL
 
-# tflags net
-TO_EQ_FM_HTML_ONLY
-
 # tflags net
 TO_EQ_FM_SPF_FAIL
 
@@ -1622,7 +1628,7 @@ TO_NO_BRKTS_HTML_IMG
 # tflags publish
 TO_NO_BRKTS_HTML_ONLY
 
-# tflags net
+# good enough
 TO_NO_BRKTS_MSFT
 
 # tflags publish
@@ -1635,13 +1641,7 @@ TO_NO_BRKTS_PCNT
 TO_TOO_MANY_WFH_01
 
 # good enough
-TVD_IP_HEX
-
-# good enough
-TVD_IP_SING_HEX
-
-# good enough
-TVD_QUAL_MEDS
+TVD_PH_BODY_META
 
 # good enough
 TVD_RCVD_SPACE_BRACKET
@@ -1652,12 +1652,6 @@ TVD_SPACE_ENCODED
 # tflags net
 TVD_SPACE_RATIO_MINFP
 
-# tflags net
-TVD_SUBJ_NUM_OBFU_MINFP
-
-# good enough
-TVD_VISIT_PHARMA
-
 # tflags publish
 TW_GIBBERISH_MANY
 
@@ -1679,8 +1673,8 @@ UNICODE_OBFU_ZW
 # tflags userconf
 UNPARSEABLE_RELAY
 
-# tflags net
-UPGRADE_MAILBOX
+# tflags publish
+UNSUB_GOOG_FORM
 
 # tflags net
 URIBL_ABUSE_SURBL
@@ -1778,9 +1772,6 @@ URI_DASHGOVEDU
 # tflags publish
 URI_DATA
 
-# good enough
-URI_DOTDOT_LOW_CNTRST
-
 # tflags publish
 URI_DOTEDU
 
@@ -1806,10 +1797,10 @@ URI_GOOG_STO_SPAMMY
 URI_HEX_IP
 
 # tflags userconf
-URI_HOST_IN_BLACKLIST
+URI_HOST_IN_BLOCKLIST
 
 # tflags userconf
-URI_HOST_IN_WHITELIST
+URI_HOST_IN_WELCOMELIST
 
 # tflags publish
 URI_IMG_WP_REDIR
@@ -1823,7 +1814,10 @@ URI_MALWARE_SCMS
 # tflags userconf
 URI_NOVOWEL
 
-# tflags net
+# good enough
+URI_OBFU_DOM
+
+# tflags publish
 URI_ONLY_MSGID_MALF
 
 # tflags publish
@@ -1853,7 +1847,7 @@ URI_WP_DIRINDEX
 # tflags net
 URI_WP_HACKED
 
-# tflags net
+# tflags publish
 URI_WP_HACKED_2
 
 # tflags publish
@@ -1878,13 +1872,13 @@ USER_IN_DEF_SPF_WL
 USER_IN_DEF_WELCOMELIST
 
 # tflags net
-USER_IN_DKIM_WHITELIST
+USER_IN_DKIM_WELCOMELIST
 
 # tflags userconf
 USER_IN_MORE_SPAM_TO
 
 # tflags net
-USER_IN_SPF_WHITELIST
+USER_IN_SPF_WELCOMELIST
 
 # tflags userconf
 USER_IN_WELCOMELIST
@@ -1901,12 +1895,6 @@ VPS_NO_NTLD
 # tflags publish
 WALMART_IMG_NOT_RCVD_WAL
 
-# good enough
-WANT_TO_ORDER
-
-# good enough
-WIKI_IMG
-
 # tflags publish
 WORD_INVIS
 
@@ -1925,9 +1913,6 @@ XM_PHPMAILER_FORGED
 # tflags publish
 XM_RANDOM
 
-# good enough
-XM_RECPTID
-
 # tflags net
 XPRIO
 
index f9ee06a00430f6f446db70677bad1d024c4713aa..dc900a4797d4313fd38be815ad3fec65bf1798ce 100644 (file)
 # added to new files, named according to the release they're added in.
 ###########################################################################
 
+# Version compatibility - Welcomelist/Blocklist
+# In SpamAssassin 4.0, rules containing "whitelist" or "blacklist" have been
+# renamed to contain more racially neutral "welcomelist" and "blocklist"
+# terms.  When this compatibility flag is enabled, old rule names from stock
+# rules will not hit anymore alongside the new ones.  For more information,
+# see: https://wiki.apache.org/spamassassin/WelcomelistBlocklist
+#
+enable_compat welcomelist_blocklist
+
 # RelayCountry - add metadata for Bayes learning, marking the countries
 # a message was relayed through
 #
index a53ef67cbfca1affa487ff439fdfdab0806c13c9..b3c4a9524f0d016c7a83d77d8b245c19f72c91d3 100644 (file)
Binary files a/upstream/rules/languages and b/upstream/rules/languages differ
index a9a7c87b232acd94b988a271535c0064614526ab..fb1d11116fade01022801b49b3886f4733398639 100644 (file)
 # 
 ifplugin Mail::SpamAssassin::Plugin::Shortcircuit
 #
-#   default: strongly-whitelisted mails are *really* whitelisted now, if the
-#   shortcircuiting plugin is active, causing early exit to save CPU load.
-#   Uncomment to turn this on
+#   default: strongly-welcomelisted mails are *really* welcomelisted now, if
+#   the shortcircuiting plugin is active, causing early exit to save CPU
+#   load.  Uncomment to turn this on
 #
 #   SpamAssassin tries hard not to launch DNS queries before priority -100. 
 #   If you want to shortcircuit without launching unneeded queries, make
 #   sure such rule priority is below -100. These examples are already:
 #
-# shortcircuit USER_IN_WHITELIST       on
-# shortcircuit USER_IN_DEF_WHITELIST   on
+# shortcircuit USER_IN_WELCOMELIST       on
+# shortcircuit USER_IN_DEF_WELCOMELIST   on
 # shortcircuit USER_IN_ALL_SPAM_TO     on
-# shortcircuit SUBJECT_IN_WHITELIST    on
 
-#   the opposite; blacklisted mails can also save CPU
+#   the opposite; blocklisted mails can also save CPU
 #
-# shortcircuit USER_IN_BLACKLIST       on
-# shortcircuit USER_IN_BLACKLIST_TO    on
-# shortcircuit SUBJECT_IN_BLACKLIST    on
+# shortcircuit USER_IN_BLOCKLIST       on
+# shortcircuit USER_IN_BLOCKLIST_TO    on
 
 #   if you have taken the time to correctly specify your "trusted_networks",
 #   this is another good way to save CPU
index 56e5b2ff2bcccf731a408311d36e7d3782dba51b..2b80e775ccd20824545ed12b93db5e3e3ff41bba 100644 (file)
@@ -40,7 +40,7 @@ loadplugin Mail::SpamAssassin::Plugin::SpamCop
 #
 #loadplugin Mail::SpamAssassin::Plugin::AntiVirus
 
-# AWL - do auto-whitelist checks
+# AWL - do auto-welcomelist checks
 #
 #loadplugin Mail::SpamAssassin::Plugin::AWL
 
@@ -56,17 +56,9 @@ loadplugin Mail::SpamAssassin::Plugin::AutoLearnThreshold
 #
 #loadplugin Mail::SpamAssassin::Plugin::AccessDB
 
-# WhitelistSubject - Whitelist/Blacklist certain subject regular expressions
+# WelcomelistSubject - Welcomelist/Blocklist certain subject regular expressions
 #
-loadplugin Mail::SpamAssassin::Plugin::WhiteListSubject
-
-###########################################################################
-# experimental plugins
-
-# DomainKeys - perform DomainKeys verification
-#
-# This plugin has been removed as of v3.3.0.  Use the DKIM plugin instead,
-# which supports both Domain Keys and DKIM.
+loadplugin Mail::SpamAssassin::Plugin::WelcomeListSubject
 
 # MIMEHeader - apply regexp rules against MIME headers in the message
 #
index cba5d7dbcadfb2ba60cda0fe7a8d6d5afc11d6f5..2ae42a70030e0368069878bf147a3606280a1291 100644 (file)
 # added to new files, named according to the release they're added in.
 
 ###########################################################################
-# experimental plugins
 
 # DKIM - perform DKIM verification
 #
 # Mail::DKIM module required for use, see INSTALL for more information.
 # 
-# Note that if C<Mail::DKIM> version 0.20 or later is installed, this
+# Note that if Mail::DKIM version 0.20 or later is installed, this
 # renders the DomainKeys plugin redundant.
 #
 loadplugin Mail::SpamAssassin::Plugin::DKIM
index 489dd4c37c4671148dfd45ec882ece60fbee004b..4de1691dbddaf210f0e3f4fc446ea34a80dce3a5 100644 (file)
 ###########################################################################
 
 # TxRep - Reputation database that replaces AWL
+#
 # loadplugin Mail::SpamAssassin::Plugin::TxRep
 
 # URILocalBL - Provides ISP and Country code based filtering as well as
 # quick IP based blocks without a full RBL implementation - Bug 7060
-
+#
 # loadplugin Mail::SpamAssassin::Plugin::URILocalBL
 
 # PDFInfo - Use several methods to detect a PDF file's ham/spam traits
+#
 # loadplugin Mail::SpamAssassin::Plugin::PDFInfo
+
index 8e0fb073ee9b7340f7eddb7a72bab8eb81382a4c..38255bb2e1e7e84956aadea10fd90323f9243f52 100644 (file)
 ###########################################################################
 
 # HashBL - Query hashed/unhashed strings, emails, uris etc from DNS lists
-# loadplugin Mail::SpamAssassin::Plugin::HashBL
+#
+loadplugin Mail::SpamAssassin::Plugin::HashBL
 
 # ResourceLimits - assure your spamd child processes
 # do not exceed specified CPU or memory limit
+#
 # loadplugin Mail::SpamAssassin::Plugin::ResourceLimits
 
 # FromNameSpoof - help stop spam that tries to spoof other domains using 
 # the from name
+#
 # loadplugin Mail::SpamAssassin::Plugin::FromNameSpoof
 
 # Phishing - finds uris used in phishing campaigns detected by
 # OpenPhish or PhishTank feeds.
+#
 # loadplugin Mail::SpamAssassin::Plugin::Phishing
 
index e9bb767b6211e993f4de7aef335176a04f22d6e7..107ee4b56db806daf1aa92962a0388e4a1a6e763 100644 (file)
@@ -22,4 +22,5 @@
 # macros present to security, many places block these type of documents outright.
 #
 # For this plugin to work, Archive::Zip and IO::String modules are required.
+#
 # loadplugin Mail::SpamAssassin::Plugin::OLEVBMacro
diff --git a/upstream/rules/v400.pre b/upstream/rules/v400.pre
new file mode 100644 (file)
index 0000000..47515d2
--- /dev/null
@@ -0,0 +1,37 @@
+# This is the right place to customize your installation of SpamAssassin.
+#
+# See 'perldoc Mail::SpamAssassin::Conf' for details of what can be
+# tweaked.
+#
+# This file was installed during the installation of SpamAssassin 4.0.0,
+# and contains plugin loading commands for the new plugins added in that
+# release.  It will not be overwritten during future SpamAssassin installs,
+# so you can modify it to enable some disabled-by-default plugins below,
+# if you so wish.
+#
+# There are now multiple files read to enable plugins in the
+# /etc/mail/spamassassin directory; previously only one, "init.pre" was
+# read.  Now both "init.pre", "v310.pre", and any other files ending in
+# ".pre" will be read.  As future releases are made, new plugins will be
+# added to new files, named according to the release they're added in.
+###########################################################################
+
+# ExtractText - Extract text from documents or images for matching
+#
+# Requires manual configuration, see plugin documentation.
+#
+# loadplugin Mail::SpamAssassin::Plugin::ExtractText
+
+# DecodeShortUrl - Check for shortened URLs
+#
+# Note that this plugin will send HTTP requests to different URL shortener
+# services.  Enabling caching is recommended, see plugin documentation.
+#
+# loadplugin Mail::SpamAssassin::Plugin::DecodeShortURLs
+
+# DMARC - Check DMARC compliance
+#
+# Requires Mail::DMARC module and working SPF and DKIM Plugins.
+#
+loadplugin Mail::SpamAssassin::Plugin::DMARC
+
index d99163afb66e2de274b7a2fd0734931750bc9110..d2d4e7b347a4708abf0962e531047956c43e1f0b 100755 (executable)
@@ -67,7 +67,13 @@ use AnyDBM_File ;
 
 my $db;
 if ($#ARGV == -1) {
-  $db = $ENV{HOME}."/.spamassassin/auto-whitelist";
+  # Check for existing file from <4.0, compatibility to be removed in 4.1
+  if (!-e $ENV{HOME}."/.spamassassin/auto-welcomelist" &&
+       -e $ENV{HOME}."/.spamassassin/auto-whitelist") {
+    $db = $ENV{HOME}."/.spamassassin/auto-whitelist";
+  } else {
+    $db = $ENV{HOME}."/.spamassassin/auto-welcomelist";
+  }
 } else {
   $db = $ARGV[0];
 }
@@ -120,7 +126,7 @@ untie %h;
 
 =head1 NAME
 
-sa-awl - examine and manipulate SpamAssassin's auto-whitelist db
+sa-awl - examine and manipulate SpamAssassin's auto-welcomelist db
 
 =head1 SYNOPSIS
 
@@ -128,10 +134,10 @@ B<sa-awl> [--clean] [--min n] [dbfile]
 
 =head1 DESCRIPTION
 
-Check or clean a SpamAssassin auto-whitelist (AWL) database file.
+Check or clean a SpamAssassin auto-welcomelist (AWL) database file.
 
 The name of the file is specified after any options, as C<dbfile>.
-The default is C<$HOME/.spamassassin/auto-whitelist>.
+The default is C<$HOME/.spamassassin/auto-welcomelist>.
 
 =head1 OPTIONS
 
index 674db805fec522d566bf05addc44589c7644aa01..7ec56797140cdccd986cc798596b81a9c5126df3 100755 (executable)
@@ -213,10 +213,10 @@ foreach my $optkey (keys %opt) {
 # only uses the first line of output.
 my $client;
 if (defined $opt{'port'}) {
- $client = new Mail::SpamAssassin::Client({port => $opt{'port'},
-                                          host => $opt{'hostname'}});
+ $client = Mail::SpamAssassin::Client->new({port => $opt{'port'},
+                                           host => $opt{'hostname'}});
 } else {
- $client = new Mail::SpamAssassin::Client({socketpath => $opt{'socketpath'}});
+ $client = Mail::SpamAssassin::Client->new({socketpath => $opt{'socketpath'}});
 }
 
 # this'd be weird, but totally dependent on the client
index 017536b29dab7c99621796d2eadd84f7bcb56c7e..842cc999e07454b46d9582b1dbe6368839e1495d 100755 (executable)
@@ -149,7 +149,7 @@ my $post_config = q(
 
 ).join("\n", @{$opt{'cf'}})."\n";
 
-my $spamtest = new Mail::SpamAssassin(
+my $spamtest = Mail::SpamAssassin->new(
   {
     rules_filename      => $opt{'configpath'},
     site_rules_filename => $opt{'siteconfigpath'},
@@ -560,7 +560,7 @@ scan(psv)
        AV *results;
 
   CODE:
-       pstart = (unsigned char *) SvPVutf8(psv, plen);
+       pstart = (unsigned char *) SvPV(psv, plen);
        pend = pstart + plen;
        results = (AV *) sv_2mortal((SV *) newAV());
 
@@ -700,7 +700,7 @@ C<re2c> can match strings much faster than perl code, by constructing a DFA to
 match many simple strings in parallel, and compiling that to native object
 code.  Not all SpamAssassin rules are amenable to this conversion, however.
 
-This requires C<re2c> (see C<http://re2c.org/>), and the C
+This requires C<re2c> (see C<https://re2c.org/>), and the C
 compiler used to build Perl XS modules, be installed.
 
 Note that running this, and creating a compiled ruleset, will have no
@@ -777,7 +777,7 @@ I<area> is the area of the code to instrument.
 
 For more information about which areas (also known as channels) are
 available, please see the documentation at
-L<http://wiki.apache.org/spamassassin/DebugChannels>.
+L<https://wiki.apache.org/spamassassin/DebugChannels>.
 
 =item B<-h>, B<--help>
 
@@ -803,7 +803,7 @@ C<Mail::SpamAssassin::Plugin::Rule2XSBody>
 
 =head1 BUGS
 
-See <http://issues.apache.org/SpamAssassin/>
+See <https://issues.apache.org/SpamAssassin/>
 
 =head1 AUTHORS
 
index 3105908c0aabb0e9702fae1dd8a2860c2f2d03a5..5d69ea5f0320ebb26006d32db3aa48af17448420 100755 (executable)
@@ -173,15 +173,14 @@ if ( !defined $isspam
   && !defined $opt{'folders'} )
 {
   usage( 0,
-"Please select either --spam, --ham, --folders, --forget, --sync, --import,\n--dump, --clear, --backup or --restore"
+    "Please select either --spam, --ham, --folders, --forget, --sync, --import,\n--dump, --clear, --backup or --restore"
   );
 }
 
 # We need to make sure the journal syncs pre-forget...
 if ( defined $forget && $opt{'nosync'} ) {
   $opt{'nosync'} = 0;
-  warn
-"sa-learn warning: --forget requires read/write access to the database, and is incompatible with --no-sync\n";
+  warn "sa-learn warning: --forget requires read/write access to the database, and is incompatible with --no-sync\n";
 }
 
 if ( defined $opt{'old_format'} ) {
@@ -217,7 +216,7 @@ if (defined $opt{'dump'} || defined $opt{'import'} || defined $opt{'clear'} ||
 $post_config .= join("\n", @{$opt{'cf'}})."\n";
 
 # create the tester factory
-$spamtest = new Mail::SpamAssassin(
+$spamtest = Mail::SpamAssassin->new(
   {
     rules_filename      => $opt{'configpath'},
     site_rules_filename => $opt{'siteconfigpath'},
@@ -459,10 +458,10 @@ eval {
 
   ###########################################################################
 
-  my $iter = new Mail::SpamAssassin::ArchiveIterator(
+  my $iter = Mail::SpamAssassin::ArchiveIterator->new(
     {
         # skip messages larger than max-size bytes,
-        # 0 for no limit, undef defaults to 256 KB
+        # 0 for no limit, undef defaults to 500 KB
       'opt_max_size' => $opt{'max-size'},
       'opt_want_date' => 0,
       'opt_from_regex' => $spamtest->{conf}->{mbox_format_from_regex},
@@ -517,6 +516,12 @@ sub killed {
 sub target {
   my ($target) = @_;
 
+  if (!defined $isspam && !$forget)
+  {
+    usage( 0,
+      "Please select either --spam or --ham or --forget before the first target"
+    );
+  }
   my $class = ( $isspam ? "spam" : "ham" );
   my $format = ( defined( $opt{'format'} ) ? $opt{'format'} : "detect" );
 
@@ -618,9 +623,9 @@ B<sa-learn> [options] --dump [ all | data | magic ]
 
 Options:
 
- --ham                 Learn messages as ham (non-spam)
- --spam                Learn messages as spam
- --forget              Forget a message
+ --ham                 Learn the following messages as ham (non-spam)
+ --spam                Learn the following messages as spam
+ --forget              Forget the following messages
  --use-ignores         Use bayes_ignore_from and bayes_ignore_to
  --sync                Synchronize the database and the journal if needed
  --force-expire        Force a database sync and expiry run
@@ -636,12 +641,13 @@ Options:
  --mbox                Input sources are in mbox format
  --mbx                 Input sources are in mbx format
  --max-size <b>        Skip messages larger than b bytes;
-                       defaults to 256 KB, 0 implies no limit
+                       defaults to 500 KB, 0 implies no limit
  --showdots            Show progress using dots
  --progress            Show progress using progress bar
  --no-sync             Skip synchronizing the database and journal
                        after learning
- -L, --local           Operate locally, no network accesses
+ -L, --local           Operate locally, no network accesses. Use
+                       of this is recommended, see documentation.
  --import              Migrate data from older version/non DB_File
                        based databases
  --clear               Wipe out existing database
@@ -657,7 +663,7 @@ Options:
  --siteconfigpath=path Path for site configs
                        (default:  @@PREFIX@@/etc/mail/spamassassin)
  --cf='config line'    Additional line of configuration
- -D, --debug [area=n,...]  Print debugging messages
+ -D, --debug [area,...]  Print debugging messages
  -V, --version         Print version
  -h, --help            Print usage message
 
@@ -678,6 +684,9 @@ that matches.  See C<Mail::SpamAssassin::ArchiveIterator> for more details.
 If you are using mail boxes in format other than maildir you should use
 the B<--mbox> or B<--mbx> parameters.
 
+Files compressed with gzip/bzip2/xz/lz4/lzip/lzo are uncompressed
+automatically.  See C<Mail::SpamAssassin::ArchiveIterator> for more details.
+
 SpamAssassin remembers which mail messages it has learnt already, and will not
 re-learn those messages again, unless you use the B<--forget> option. Messages
 learnt as spam will have SpamAssassin markup removed, on the fly.
@@ -696,21 +705,21 @@ should investigate the C<spamc -L> switch.
 
 =item B<--ham>
 
-Learn the input message(s) as ham.   If you have previously learnt any of the
-messages as spam, SpamAssassin will forget them first, then re-learn them as
-ham.  Alternatively, if you have previously learnt them as ham, it'll skip them
-this time around.  If the messages have already been filtered through
-SpamAssassin, the learner will ignore any modifications SpamAssassin may have
-made.
+Learn the input message(s) in the files following the option as ham.
+If you have previously learnt any of the messages as spam, SpamAssassin will
+forget them first, then re-learn them as ham.  Alternatively, if you have
+previously learnt them as ham, it'll skip them this time around.
+If the messages have already been filtered through SpamAssassin, the learner
+will ignore any modifications SpamAssassin may have made.
 
 =item B<--spam>
 
-Learn the input message(s) as spam.   If you have previously learnt any of the
-messages as ham, SpamAssassin will forget them first, then re-learn them as
-spam.  Alternatively, if you have previously learnt them as spam, it'll skip
-them this time around.  If the messages have already been filtered through
-SpamAssassin, the learner will ignore any modifications SpamAssassin may have
-made.
+Learn the input message(s) in the files following the option as spam.
+If you have previously learnt any of the messages as ham, SpamAssassin will
+forget them first, then re-learn them as spam.  Alternatively, if you have
+previously learnt them as spam, it'll skip them this time around.
+If the messages have already been filtered through SpamAssassin, the learner
+will ignore any modifications SpamAssassin may havemmade.
 
 =item B<--folders>=I<filename>, B<-f> I<filename>
 
@@ -757,7 +766,8 @@ into the Bayes databases.
 
 =item B<--forget>
 
-Forget a given message previously learnt.
+Forget the input message(s) in the files following the option as previously
+learnt.
 
 =item B<--dbpath>
 
@@ -842,10 +852,13 @@ diagnostic output on bayes, learn, and dns, use:
 
         spamassassin -D bayes,learn,dns
 
+Use an empty string (-D '') to indicate no areas when the next item on the
+command line is a path, to prevent the path from being parsed as an area.
+
 For more information about which areas (also known as channels) are available,
 please see the documentation at:
 
-        C<http://wiki.apache.org/spamassassin/DebugChannels>
+        C<https://wiki.apache.org/spamassassin/DebugChannels>
 
 Higher priority informational messages that are suitable for logging in normal
 circumstances are available with an area of "info".
@@ -870,11 +883,10 @@ ignored since there is no learn operation.
 =item B<-L>, B<--local>
 
 Do not perform any network accesses while learning details about the mail
-messages.  This will speed up the learning process, but may result in a
-slightly lower accuracy.
-
-Note that this is currently ignored, as current versions of SpamAssassin will
-not perform network access while learning; but future versions may.
+messages.  This should be normally used, as there really isn't anything
+Bayes can learn from network lookup results.  Official SpamAssassin plugins
+do not currently do any network lookups when learning, but it's possible
+that third party ones might.
 
 =item B<--import>
 
@@ -986,7 +998,7 @@ With Bayesian analysis, it's all probabilities - "because the past says
 it is likely as this falls into a probabilistic distribution common to past
 spam in your systems". Tell that to your users!  Tell that to the client
 when he asks "what can I do to change this". (By the way, the answer in
-this case is "use whitelisting".)
+this case is "use welcomelisting".)
 
 =item It will take disk space and memory.
 
@@ -1015,6 +1027,7 @@ Otherwise, the results may be pretty skewed.
 
        sa-learn --spam /path/to/spam/folder
        sa-learn --ham /path/to/ham/folder
+       sa-learn --ham hampath1 hampath2 --spam spampath1 spampath2
        ...
 
 Let SpamAssassin proceed, learning stuff. When it finds ham and spam
@@ -1344,7 +1357,7 @@ Paul Graham's "A Plan For Spam" paper
 E<lt>http://www.linuxjournal.com/article/6467E<gt>
 Gary Robinson's f(x) and combining algorithms, as used in SpamAssassin
 
-E<lt>http://www.bgl.nu/~glouis/bogofilter/E<gt>
+E<lt>http://web.archive.org/web/20120512230723/http://www.bgl.nu/~glouis/bogofilter/E<gt>
 'Training on error' page.  A discussion of various Bayes training regimes,
 including 'train on error' and unsupervised training.
 
index 0c1fb4131e5d100ce4124c60be1ad3a24ca407a5..1302abcb0b710646cd1752732cc05ec7e89bbed0 100755 (executable)
@@ -22,11 +22,11 @@ use warnings;
 use re 'taint';
 
 my $VERSION = 'svnunknown';
-if ('$Id: sa-update.raw 1881784 2020-09-17 07:17:40Z gbechis $' =~ ':') {
-  # Subversion keyword "$Id: sa-update.raw 1881784 2020-09-17 07:17:40Z gbechis $" has been successfully expanded.
+if ('$Id: sa-update.raw 1900642 2022-05-07 06:01:02Z hege $' =~ ':') {
+  # Subversion keyword "$Id: sa-update.raw 1900642 2022-05-07 06:01:02Z hege $" has been successfully expanded.
   # Doesn't happen with automated launchpad builds:
   # https://bugs.launchpad.net/launchpad/+bug/780916
-  $VERSION = &Mail::SpamAssassin::Version . ' / svn' . (split(/\s+/, '$Id: sa-update.raw 1881784 2020-09-17 07:17:40Z gbechis $'))[2];
+  $VERSION = &Mail::SpamAssassin::Version . ' / svn' . (split(/\s+/, '$Id: sa-update.raw 1900642 2022-05-07 06:01:02Z hege $'))[2];
 }
 
 my $PREFIX          = '@@PREFIX@@';             # substituted at 'make' time
@@ -86,7 +86,6 @@ BEGIN {                          # see comments in "spamassassin.raw" for doco
 
 # These are the non-standard required modules
 use Net::DNS;
-use HTTP::Date qw(time2str);
 use Archive::Tar 1.23;
 use IO::Zlib 1.04;
 use Mail::SpamAssassin::Logger qw(:DEFAULT info log_message);
@@ -96,7 +95,7 @@ our ($have_lwp, $io_socket_module_name, $have_inet4, $use_inet4, $have_inet6, $u
 BEGIN {
   # Deal with optional modules
 
-  eval { require Digest::SHA; import Digest::SHA qw(sha256_hex sha512_hex); 1 } and do { $have_sha256=1; $have_sha512=1 }
+  eval { require Digest::SHA; Digest::SHA->import(qw(sha256_hex sha512_hex)); 1 } and do { $have_sha256=1; $have_sha512=1 }
   or die "Unable to verify file hashes! You must install a modern version of Digest::SHA.";
   
     $have_lwp = eval {
@@ -190,7 +189,10 @@ GetOptions(
   'allowplugins'                        => \$opt{'allowplugins'},
   'reallyallowplugins'                  => \$opt{'reallyallowplugins'},
   'refreshmirrors'                      => \$opt{'refreshmirrors'},
+  'forcemirror=s'                       => \$opt{'forcemirror'},
   'httputil=s'                          => \$opt{'httputil'},
+  'score-multiplier=s'                  => \$opt{'score-multiplier'},
+  'score-limit=s'                       => \$opt{'score-limit'},
 
   # allow multiple of these on the commandline
   'gpgkey=s'                           => $opt{'gpgkey'},
@@ -243,6 +245,13 @@ if ( defined $opt{'httputil'} && $opt{'httputil'} !~ /^(curl|wget|fetch|lwp)$/ )
   warn "Invalid parameter for --httputil, curl|wget|fetch|lwp wanted\n";
 }
 
+if ( defined $opt{'score-multiplier'} && $opt{'score-multiplier'} !~ /^\d+(?:\.\d+)?$/ ) {
+  die "Invalid parameter for --score-multiplier, integer or float expected.\n";
+}
+if ( defined $opt{'score-limit'} && $opt{'score-limit'} !~ /^\d+(?:\.\d+)?$/ ) {
+  die "Invalid parameter for --score-limit, integer or float expected.\n";
+}
+
 # Figure out what version of SpamAssassin we're using, and also figure out the
 # reverse of it for the DNS query.  Handle x.yyyzzz as well as x.yz.
 my $SAVersion = $Mail::SpamAssassin::VERSION;
@@ -258,13 +267,7 @@ else {
 my $RevSAVersion = join(".", reverse split(/\./, $SAVersion));
 
 # set debug areas, if any specified (only useful for command-line tools)
-$SAVersion =~ /^(\d+\.\d+)/;
-if ($1+0 > 3.0) {
-  $opt{'debug'} ||= 'all' if (defined $opt{'debug'});
-}
-else {
-  $opt{'debug'} = defined $opt{'debug'};
-}
+$opt{'debug'} ||= 'all' if (defined $opt{'debug'});
 
 # Find the default site rule directory, also setup debugging and other M::SA bits
 my $SA = Mail::SpamAssassin->new({
@@ -617,10 +620,10 @@ foreach my $channel (@channels) {
   my $GPG;
 
   if ($instfile) {
-    dbg("channel: using --install files $instfile\{,.sha256,.sha512,.asc\}");
+    dbg("channel: using --install files $instfile\{,.asc,.sha512,.sha256\}");
     $content = read_install_file($instfile);
-    if ( -s "$instfile.sha512" ) { $SHA512 = read_install_file($instfile.".sha512"); }
-    if ( -s "$instfile.sha256" ) { $SHA256 = read_install_file($instfile.".sha256"); }
+    if ( -f "$instfile.sha512" ) { $SHA512 = read_install_file($instfile.".sha512"); }
+    if ( -f "$instfile.sha256" ) { $SHA256 = read_install_file($instfile.".sha256"); }
     $GPG = read_install_file($instfile.".asc") if $GPG_ENABLED;
 
   } else {  # not an install file, obtain fresh rules from network
@@ -667,6 +670,8 @@ foreach my $channel (@channels) {
         channel_failed("channel '$channel': MIRRORED.BY file URL was not in DNS");
         next;
       }
+      # make sure requests spread randomly
+      Mail::SpamAssassin::Util::fisher_yates_shuffle(\@mirrors);
       foreach my $mirror (@mirrors) {
         my ($result_fname, $http_ok) =
           http_get($mirror, $UPDDir, $mirby_path, $mirby_force_reload);
@@ -720,6 +725,12 @@ foreach my $channel (@channels) {
     my @mirrors = split(/^/, $mirby);
     while(my $mirror = shift @mirrors) {
       chomp $mirror;
+      if ( defined $opt{'forcemirror'} ) {
+        $mirror = $opt{'forcemirror'};
+        $mirrors{$mirror}->{"weight"} = 1;
+        dbg("channel: found mirror $mirror (forced)");
+       last;
+      }
 
       $mirror =~ s/#.*$//;   # remove comments
       $mirror =~ s/^\s+//;   # remove leading whitespace
@@ -798,19 +809,6 @@ foreach my $channel (@channels) {
         next;
       }
 
-      # SHA512 of the archive file
-      ($result_fname, $http_ok) = http_get("$mirror/$newV.tar.gz.sha512", $UPDDir);
-      if (!$http_ok || !-s $result_fname) {
-        # If not found, try SHA256 instead
-        ($result_fname, $http_ok) = http_get("$mirror/$newV.tar.gz.sha256", $UPDDir);
-        if (!$http_ok || !-s $result_fname) {
-          dbg("channel: No sha512 or sha256 file available from $mirror, %s",
-            %mirrors ? "sleeping $sleep_sec sec and trying next" : 'no mirrors left');
-          sleep($sleep_sec) if %mirrors;
-          next;
-        }
-      }
-
       # if GPG is enabled, the GPG detached signature of the archive file
       if ($GPG_ENABLED) {
         ($result_fname, $http_ok) = http_get("$mirror/$newV.tar.gz.asc", $UPDDir);
@@ -821,6 +819,20 @@ foreach my $channel (@channels) {
           next;
         }
       }
+      else {
+        # SHA512 of the archive file
+        ($result_fname, $http_ok) = http_get("$mirror/$newV.tar.gz.sha512", $UPDDir);
+        if (!$http_ok || !-s $result_fname) {
+          # If not found, try SHA256 instead
+          ($result_fname, $http_ok) = http_get("$mirror/$newV.tar.gz.sha256", $UPDDir);
+          if (!$http_ok || !-s $result_fname) {
+            dbg("channel: No sha512 or sha256 file available from $mirror, %s",
+              %mirrors ? "sleeping $sleep_sec sec and trying next" : 'no mirrors left');
+            sleep($sleep_sec) if %mirrors;
+            next;
+          }
+        }
+      }
 
       $download_ok = 1;
       last;
@@ -846,8 +858,12 @@ foreach my $channel (@channels) {
     }
   }
 
-  unless ($content && ( $SHA512 || $SHA256 ) && (!$GPG_ENABLED || $GPG)) {
-    channel_failed("channel '$channel': could not find working mirror");
+  unless ($content && (($GPG_ENABLED && $GPG) || (!$GPG_ENABLED && ($SHA512 || $SHA256)))) {
+    if ($instfile) {
+      channel_failed("channel '$channel': missing checksum files $instfile\{,.sha512,.sha256\}");
+    } else {
+      channel_failed("channel '$channel': could not find working mirror");
+    }
     next;
   }
 
@@ -1281,6 +1297,7 @@ sub read_install_file {
   my $all;
   { local $/ = undef; $all = <IN> }
   close IN or die "cannot close $file: $!";
+  defined $all && $all ne '' or die "empty file $file\n";
   return $all;
 }
 
@@ -1349,12 +1366,49 @@ sub taint_safe_archive_extract {
         # also, if --allowplugins is not specified, comment out
         # all loadplugin or tryplugin lines (and others that can load code)
         if ( !$opt{'allowplugins'} ) {
-          $content =~ s{^\s*((?:load|try)plugin|\S+_modules?|\S+_factory)\s}
+          $content =~ s{^\s*(
+              loadplugin |
+              tryplugin |
+              \S+_modules? |
+              \S+_factory |
+              dcc_(?:path|options) |
+              pyzor_(?:path|options) |
+              extracttext_external
+            )\s}
             {#(commented by sa-update, no --allowplugins switch specified)# $1}gmx;
         }
 
         # other stuff never allowed for safety
         $content =~ s/^\s*(dns_server)/#(commented by sa-update, not allowed)# $1/gm;
+
+        # adjust scores
+        if ($opt{'score-multiplier'} || $opt{'score-limit'}) {
+          my $adjust_score = sub {
+            my @scores = split(/\s+/, $_[1]);
+            my $touched = 0;
+            foreach (@scores) {
+              next if $_ == 0; # Can't adjust if zero..
+              my $old = $_;
+              $_ = $_ * $opt{'score-multiplier'} if $opt{'score-multiplier'};
+              $_ = $opt{'score-limit'} if $opt{'score-limit'} && $_ > $opt{'score-limit'};
+              if ($old != $_) {
+                if ($_ == 0) { # Prevent zeroing scores
+                  $_ = $old < 0 ? "-0.001" : "0.001"
+                } else {
+                  $_ = sprintf("%.3f", $_);
+                }
+                $touched++ if $old != $_;
+              }
+            }
+            if ($touched) {
+              return $_[0].join(' ', @scores)." #(score adjusted by sa-update, $_[1])#".$_[2];
+            } else {
+              return $_[0].$_[1].$_[2];
+            }
+          };
+          $content =~ s/^(\s*score\s+\w+\s+)(-?\d+(?:\.\d+)?(?:\s+-?\d+(?:\.\d+)?)*)(.*)$
+            /$adjust_score->($1,$2,$3)/igmex;
+        }
       }
 
       print OUT $content
@@ -1481,7 +1535,11 @@ sub http_get_lwp {
     $request->url($url);
 
     if (defined $ims) {
-      my $str = time2str($ims);
+      my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) = gmtime($ims);
+      my $str = sprintf("%s, %02d %s %04d %02d:%02d:%02d GMT",
+          qw(Sun Mon Tue Wed Thu Fri Sat)[$wday], $mday,
+          qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)[$mon],
+          $year + 1900, $hour, $min, $sec);
       $request->header('If-Modified-Since', $str);
       dbg("http: IMS GET request, $url, $str");
     }
@@ -1530,7 +1588,6 @@ sub http_get_lwp {
 
 # Do a GET request via HTTP for a given URL using an external program,
 # or fall back to LWP if no external downloading program is available.
-# Use the optional time_t value to do an IMS GET
 sub http_get {
   my($url, $dir, $suggested_out_fname, $force_reload) = @_;
   my $content;
@@ -1917,7 +1974,7 @@ sub lint_check_dir {
   # due to the Logger module's globalness (all M::SA objects share the same
   # Logger setup), we can't change the debug level here to only include
   # "config" or otherwise be more terse. :(
-  my $spamtest = new Mail::SpamAssassin( {
+  my $spamtest = Mail::SpamAssassin->new( {
     rules_filename       => $dir,
     site_rules_filename  => $LOCAL_RULES_DIR,
     ignore_site_cf_files => 1,
@@ -1958,9 +2015,9 @@ Options:
                           Use multiple times for multiple channels
   --channelfile file      Retrieve updates from the channels in the file
   --checkonly             Check for update availability, do not install
-  --install filename      Install updates directly from this file. Signature
-                          verification will use "file.asc", "file.sha256",
-                          and "file.sha512".
+  --install file          Install updates directly from this file. Signature
+                          verification will use "file.asc", or "file.sha512"
+                          or "file.sha256".
   --allowplugins          Allow updates to load plugin code (DANGEROUS)
   --gpgkey key            Trust the key id to sign releases
                           Use multiple times for multiple keys
@@ -1975,8 +2032,15 @@ Options:
                           SpamAssassin site rules directory
                           (default: @@LOCAL_STATE_DIR@@/@@VERSION@@)
   --refreshmirrors        Force the MIRRORED.BY file to be updated
+  --forcemirror url       Use a specific mirror instead of downloading from
+                          official mirrors
   --httputil util         Force used download tool. By default first found
                           from these is used: curl, wget, fetch, lwp
+  --score-multiplier x.x  Adjust all scores from update channel, multiply
+                          with given value (integer or float).
+  --score-limit x.x       Adjust all scores from update channel, limit
+                          to given value (integer or float). Limiting
+                          is done after possible multiply operation.
   -D, --debug [area=n,...]  Print debugging messages
   -v, --verbose           Be verbose, like print updated channel names;
                           For more verbosity specify multiple times
@@ -1992,8 +2056,9 @@ configuration, based on channels.  The default channel is
 I<updates.spamassassin.org>, which has updated rules since the previous
 release.
 
-Update archives are verified using SHA256 and SHA512 hashes and GPG signatures,
-by default.
+Update archives are verified using GPG signatures by default.  If GPG is
+disabled (not recommended), file integrity is checked with SHA512 or SHA256
+checksums.
 
 Note that C<sa-update> will not restart C<spamd> or otherwise cause
 a scanner to reload the now-updated ruleset automatically.  Instead,
@@ -2041,10 +2106,10 @@ The exit code will be C<0> or C<1> as described below.
 Install updates "offline", from the named tar.gz file, instead of performing
 DNS lookups and HTTP invocations.
 
-Files named B<file>.sha256, B<file>.sha512, and B<file>.asc will be used for
-the SHA256 and SHA512 hashes and the GPG signature, respectively.  The filename
-provided must contain a version number of at least 3 digits, which will be used
-as the channel's update version number.
+Files named B<file>.asc, B<file>.sha512, or B<file>.sha256 will be used for
+GPG signature, and the SHA256 and SHA512 checksums, respectively.  The
+filename provided must contain a version number of at least 3 digits, which
+will be used as the channel's update version number.
 
 Multiple B<--channel> switches cannot be used with B<--install>.  To install
 multiple channels from tarballs, run C<sa-update> multiple times with different
@@ -2068,19 +2133,18 @@ Use --reallyallowplugins option to bypass warnings and make it work.
 
 =item B<--gpg>, B<--nogpg>
 
-sa-update by default will verify update archives by use of SHA256 and SHA512
-checksums and GPG signature.  SHA* hashes can verify whether or not the
-downloaded archive has been corrupted, but it does not offer any form of
-security regarding whether or not the downloaded archive is legitimate
-(aka: non-modifed by evildoers).  GPG verification of the archive is used to
-solve that problem.
+sa-update by default will verify update archives by use of GPG signature. 
 
-If you wish to skip GPG verification, you can use the B<--nogpg> option
-to disable its use.  Use of the following gpgkey-related options will
-override B<--nogpg> and keep GPG verification enabled.
+If you wish to skip GPG verification (very unsafe), you can use the
+B<--nogpg> option to disable its use.  Use of the following gpgkey-related
+options will override B<--nogpg> and keep GPG verification enabled.
 
-Note: Currently, only GPG itself is supported (ie: not PGP).  v1.2 has been
-tested, although later versions ought to work as well.
+If GPG is disabled, only SHA512 or SHA256 checksums are used to verify
+whether or not the downloaded archive has been corrupted, but it does not
+offer any form of security regarding whether or not the downloaded archive
+is legitimate (aka: non-modifed by evildoers).
+
+Note: Only GnuPG is supported (ie: not any other PGP software).
 
 =item B<--gpgkey>
 
@@ -2135,6 +2199,11 @@ Force the list of sa-update mirrors for each channel, stored in the MIRRORED.BY
 file, to be updated.  By default, the MIRRORED.BY file will be cached for up to
 7 days after each time it is downloaded.
 
+=item B<--forcemirror>
+
+Force the download from a specific host instead of relying on mirrors listed
+in MIRRORED.BY.
+
 =item B<--updatedir>
 
 By default, C<sa-update> will use the system-wide rules update directory:
@@ -2159,7 +2228,7 @@ diagnostic output on channel, gpg, and http, use:
 
 For more information about which areas (also known as channels) are
 available, please see the documentation at
-L<http://wiki.apache.org/spamassassin/DebugChannels>.
+L<https://wiki.apache.org/spamassassin/DebugChannels>.
 
 =item B<-h>, B<--help>
 
@@ -2196,7 +2265,7 @@ Mail::SpamAssassin(3)
 Mail::SpamAssassin::Conf(3)
 spamassassin(1)
 spamd(1)
-<http://wiki.apache.org/spamassassin/RuleUpdates>
+<https://wiki.apache.org/spamassassin/RuleUpdates>
 
 =head1 PREREQUISITES
 
@@ -2204,7 +2273,7 @@ C<Mail::SpamAssassin>
 
 =head1 BUGS
 
-See <http://issues.apache.org/SpamAssassin/>
+See <https://issues.apache.org/SpamAssassin/>
 
 =head1 AUTHORS
 
index 4b52ef9b421b07a9104ea06db096995f74f00da8..92ef1a6f29addcb1ac079281ee6d4dd9885478ba 100755 (executable)
@@ -158,9 +158,9 @@ if ($Mail::SpamAssassin::VERSION ne '@@VERSION@@' && '@@VERSION@@' ne "\@\@VERSI
 # - create user preference files
 # - have ArchiveIterator detect the input message format (file vs dir)
 #
-my %opt = ( 'create-prefs' => 1, 'format' => 'detect', cf => [] );
+my %opt = ( 'create-prefs' => 1, 'format' => 'detect', pre => [], cf => [] );
 
-my $doing_whitelist_operation = 0;
+my $doing_welcomelist_operation = 0;
 my $count                     = 0;
 my @targets                   = ();
 my $exitvalue;
@@ -174,12 +174,17 @@ my $total_messages            = 0;
 Getopt::Long::Configure(
   qw(bundling no_getopt_compat no_auto_abbrev no_ignore_case));
 GetOptions(
-  'add-addr-to-blacklist=s'                 => \$opt{'add-addr-to-blacklist'},
-  'add-addr-to-whitelist=s'                 => \$opt{'add-addr-to-whitelist'},
-  'add-to-blacklist'                        => \$opt{'add-to-blacklist'},
-  'add-to-whitelist|W'                      => \$opt{'add-to-whitelist'},
+  'add-addr-to-blocklist=s'                 => \$opt{'add-addr-to-blocklist'},
+  'add-addr-to-welcomelist=s'               => \$opt{'add-addr-to-welcomelist'},
+  'add-addr-to-blacklist=s'                 => \$opt{'add-addr-to-blocklist'}, # removed in 4.1
+  'add-addr-to-whitelist=s'                 => \$opt{'add-addr-to-welcomelist'}, # removed in 4.1
+  'add-to-blocklist'                        => \$opt{'add-to-blocklist'},
+  'add-to-welcomelist|W'                    => \$opt{'add-to-welcomelist'},
+  'add-to-blacklist'                        => \$opt{'add-to-blocklist'}, # removed in 4.1
+  'add-to-whitelist'                        => \$opt{'add-to-welcomelist'}, # removed in 4.1
   'configpath|config-file|config-dir|c|C=s' => \$opt{'configpath'},
   'create-prefs!'                           => \$opt{'create-prefs'},
+  'pre=s'                                   => \@{$opt{'pre'}},
   'cf=s'                                    => \@{$opt{'cf'}},
   'debug|D:s'                               => \$opt{'debug'},
   'error-code|exit-code|e:i'                => \$opt{'error-code'},
@@ -189,12 +194,15 @@ GetOptions(
   '6'                                       => sub { $opt{'force_ipv6'} = 1;
                                                      $opt{'force_ipv4'} = 0; },
   'lint'                                    => \$opt{'lint'},
+  'net'                                     => \$opt{'net'},
   'local-only|local|L'                      => \$opt{'local'},
   'mbox'                                    => sub { $opt{'format'} = 'mbox'; },
   'mbx'                                     => sub { $opt{'format'} = 'mbx'; },
   'prefspath|prefs-file|p=s'                => \$opt{'prefspath'},
-  'remove-addr-from-whitelist=s'            => \$opt{'remove-addr-from-whitelist'},
-  'remove-from-whitelist|R'                 => \$opt{'remove-from-whitelist'},
+  'remove-addr-from-welcomelist=s'          => \$opt{'remove-addr-from-welcomelist'},
+  'remove-from-welcomelist|R'               => \$opt{'remove-from-welcomelist'},
+  'remove-addr-from-whitelist=s'            => \$opt{'remove-addr-from-welcomelist'}, # removed in 4.1
+  'remove-from-whitelist'                   => \$opt{'remove-from-welcomelist'}, # removed in 4.1
   'remove-markup|despamassassinify|d'       => \$opt{'remove-markup'},
   'report|r'                                => \$opt{'report'},
   'revoke|k'                                => \$opt{'revoke'},
@@ -248,10 +256,11 @@ if (Mail::SpamAssassin::Util::am_running_on_windows()) {
 }
 
 # bug 5048: --lint should not cause network accesses
-if ($opt{'lint'}) { $opt{'local'} = 1; }
+# allow --net to override for testing
+if ($opt{'lint'} && !$opt{'net'}) { $opt{'local'} = 1; }
 
 # create the tester factory
-my $spamtest = new Mail::SpamAssassin(
+my $spamtest = Mail::SpamAssassin->new(
   {
     rules_filename      => $opt{'configpath'},
     site_rules_filename => $opt{'siteconfigpath'},
@@ -261,6 +270,7 @@ my $spamtest = new Mail::SpamAssassin(
     local_tests_only    => $opt{'local'},
     debug               => $opt{'debug'},
     dont_copy_prefs     => ( $opt{'create-prefs'} ? 0 : 1 ),
+    pre_config_text     => join("\n", @{$opt{'pre'}})."\n",
     post_config_text    => join("\n", @{$opt{'cf'}})."\n",
     require_rules       => 1,
     PREFIX              => $PREFIX,
@@ -273,6 +283,7 @@ my $spamtest = new Mail::SpamAssassin(
 if ($opt{'lint'}) {
   $spamtest->debug_diagnostics();
   my $res = $spamtest->lint_rules();
+  $spamtest->finish();
   warn "lint: $res issues detected, please rerun with debug enabled for more information\n" if ($res and !$opt{'debug'});
   # make sure we notice any write errors while flushing output buffer
   close STDOUT  or die "error closing STDOUT: $!";
@@ -280,23 +291,23 @@ if ($opt{'lint'}) {
   exit $res ? 1 : 0;
 }
 
-if ($opt{'remove-addr-from-whitelist'} ||
-    $opt{'add-addr-to-whitelist'} ||
-    $opt{'add-addr-to-blacklist'})
+if ($opt{'remove-addr-from-welcomelist'} ||
+    $opt{'add-addr-to-welcomelist'} ||
+    $opt{'add-addr-to-blocklist'})
 {
   $spamtest->init(1);
 
-  if ( $opt{'add-addr-to-whitelist'} ) {
-    $spamtest->add_address_to_whitelist($opt{'add-addr-to-whitelist'}, 1);
+  if ( $opt{'add-addr-to-welcomelist'} ) {
+    $spamtest->add_address_to_welcomelist($opt{'add-addr-to-welcomelist'}, 1);
   }
-  elsif ( $opt{'remove-addr-from-whitelist'} ) {
-    $spamtest->remove_address_from_whitelist($opt{'remove-addr-from-whitelist'}, 1);
+  elsif ( $opt{'remove-addr-from-welcomelist'} ) {
+    $spamtest->remove_address_from_welcomelist($opt{'remove-addr-from-welcomelist'}, 1);
   }
-  elsif ( $opt{'add-addr-to-blacklist'} ) {
-    $spamtest->add_address_to_blacklist($opt{'add-addr-to-blacklist'}, 1);
+  elsif ( $opt{'add-addr-to-blocklist'} ) {
+    $spamtest->add_address_to_blocklist($opt{'add-addr-to-blocklist'}, 1);
   }
   else {
-    die "spamassassin: oops! unhandled whitelist operation";
+    die "spamassassin: oops! unhandled welcomelist operation";
   }
 
   $spamtest->finish();
@@ -306,21 +317,21 @@ if ($opt{'remove-addr-from-whitelist'} ||
   exit(0);
 }
 
-# if we're going to do white/black-listing, let's prep now...
-if ( $opt{'remove-from-whitelist'}
-  or $opt{'add-to-whitelist'}
-  or $opt{'add-to-blacklist'} )
+# if we're going to do welcome/block-listing, let's prep now...
+if ( $opt{'remove-from-welcomelist'}
+  or $opt{'add-to-welcomelist'}
+  or $opt{'add-to-blocklist'} )
 {
-  $doing_whitelist_operation = 1;
+  $doing_welcomelist_operation = 1;
   $spamtest->init(1);
 }
 
 # if we're doing things in test mode, force disable long-term memory
-# functions like autowhitelist and bayes autolearn.
+# functions like autowelcomelist and bayes autolearn.
 # XXX - feels like we need a plugin hook here so plugins can be made
 # aware and take appropriate action.
 if ($opt{'test-mode'}) {
-  $spamtest->{'conf'}->{'use_auto_whitelist'} = 0;
+  $spamtest->{'conf'}->{'use_auto_welcomelist'} = 0;
   $spamtest->{'conf'}->{'bayes_auto_learn'} = 0;
 }
 
@@ -376,7 +387,7 @@ for(my $elem = 0; $elem <= $#targets; $elem++) {
 setup_sig_handlers();
 
 # Everything below here needs ArchiveIterator ...
-my $iter = new Mail::SpamAssassin::ArchiveIterator(
+my $iter = Mail::SpamAssassin::ArchiveIterator->new(
   {
     'opt_max_size' => 0,  # no limit
     'opt_want_date' => 0
@@ -406,7 +417,7 @@ if (defined $tempfile) {
 
 # Let folks know how many messages were handled, as long as the handling
 # didn't produce output (ala: check, test, or remove_markup ...)
-if ( $opt{'report'} || $opt{'revoke'} || $doing_whitelist_operation ) {
+if ( $opt{'report'} || $opt{'revoke'} || $doing_welcomelist_operation ) {
   print "$count message(s) examined.\n"  or die "error writing: $!";
 }
 
@@ -455,19 +466,19 @@ sub wanted {
   $mail       = $spamtest->parse($dataref);
   $count++;
 
-  # This is a short cut -- doing white/black-list?  Do it and return quickly.
-  if ($doing_whitelist_operation) {
-    if ( $opt{'add-to-whitelist'} ) {
-      $spamtest->add_all_addresses_to_whitelist($mail, 1);
+  # This is a short cut -- doing welcome/block-list?  Do it and return quickly.
+  if ($doing_welcomelist_operation) {
+    if ( $opt{'add-to-welcomelist'} ) {
+      $spamtest->add_all_addresses_to_welcomelist($mail, 1);
     }
-    elsif ( $opt{'remove-from-whitelist'} ) {
-      $spamtest->remove_all_addresses_from_whitelist($mail, 1);
+    elsif ( $opt{'remove-from-welcomelist'} ) {
+      $spamtest->remove_all_addresses_from_welcomelist($mail, 1);
     }
-    elsif ( $opt{'add-to-blacklist'} ) {
-      $spamtest->add_all_addresses_to_blacklist($mail, 1);
+    elsif ( $opt{'add-to-blocklist'} ) {
+      $spamtest->add_all_addresses_to_blocklist($mail, 1);
     }
     else {
-      warn "spamassassin: oops! unhandled whitelist operation";
+      warn "spamassassin: oops! unhandled welcomelist operation";
     }
 
     $mail->finish();
@@ -627,8 +638,8 @@ program or perldoc(1).
 
 =head1 WEB SITES
 
-    SpamAssassin web site:     http://spamassassin.apache.org/
-    Wiki-based documentation:  http://wiki.apache.org/spamassassin/
+    SpamAssassin web site:     https://spamassassin.apache.org/
+    Wiki-based documentation:  https://wiki.apache.org/spamassassin/
 
 =head1 USER MAILING LIST
 
@@ -827,8 +838,8 @@ from the SpamAssassin distribution.
     Mail::SpamAssassin::ArchiveIterator
        find and process messages one at a time
 
-    Mail::SpamAssassin::AutoWhitelist
-       auto-whitelist handler for SpamAssassin
+    Mail::SpamAssassin::AutoWelcomelist
+       auto-welcomelist handler for SpamAssassin
 
     Mail::SpamAssassin::Bayes
        determine spammishness using a Bayesian classifier
@@ -869,9 +880,6 @@ from the SpamAssassin distribution.
     Mail::SpamAssassin::Plugin
        SpamAssassin plugin base class
 
-    Mail::SpamAssassin::Plugin::Hashcash
-       perform hashcash verification tests
-
     Mail::SpamAssassin::Plugin::RelayCountry
        add message metadata indicating the country code of each relay
 
@@ -882,15 +890,15 @@ from the SpamAssassin distribution.
        look up URLs against DNS blocklists
 
     Mail::SpamAssassin::SQLBasedAddrList
-       SpamAssassin SQL Based Auto Whitelist
+       SpamAssassin SQL Based Auto Welcomelist
 
 =head1 BUGS
 
-See <http://issues.apache.org/SpamAssassin/>
+See <https://issues.apache.org/SpamAssassin/>
 
 =head1 AUTHORS
 
-The SpamAssassin(tm) Project <http://spamassassin.apache.org/>
+The SpamAssassin(tm) Project <https://spamassassin.apache.org/>
 
 =head1 COPYRIGHT AND LICENSE
 
index d8e5dcf8a21d6a3d91269fbb12b365a3815a48b2..c3478d7ba32d2cd5cf45a921eabc792c1c82a2cd 100755 (executable)
@@ -943,7 +943,7 @@ esac
     else
       echo "$as_me: WARNING: no configuration information is in $ac_dir" >&2
     fi
-    cd "$ac_popdir"
+    cd $ac_popdir
   done
 fi
 
@@ -3553,6 +3553,7 @@ _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
+#include <stdio.h>
 #include <sys/types.h>
 #include <sys/socket.h>
 int
@@ -4252,6 +4253,7 @@ _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
+#include <stdio.h>
 #include <netdb.h>
 int
 main ()
index 42cc998412075086be342e7644dcfc58cafedd5c..96769105ef640bc8b22561bc126171a84fb1ea8b 100644 (file)
@@ -47,7 +47,8 @@ dnl ----------------------------------------------------------------------
 
 AC_CACHE_CHECK([for SHUT_RD],
        shutrd, [
-                AC_TRY_COMPILE([#include <sys/types.h>
+                AC_TRY_COMPILE([#include <stdio.h>
+#include <sys/types.h>
 #include <sys/socket.h>],
                         [printf ("%d", SHUT_RD); return 0;],
                                         [shutrd=yes],
@@ -89,7 +90,8 @@ dnl ----------------------------------------------------------------------
 
 AC_CACHE_CHECK([for h_errno],
         herrno, [
-                AC_TRY_COMPILE([#include <netdb.h>],
+                AC_TRY_COMPILE([#include <stdio.h>
+#include <netdb.h>],
                         [printf ("%d", h_errno); return 0;],
                                         [herrno=yes],
                                         [herrno=no]),
index f9028de7468bd70690a1936786091264353caa5e..ed5f66a77357d0edd67c1aba9604cfd071efec63 100644 (file)
@@ -607,6 +607,120 @@ static int _try_to_connect_tcp(const struct transport *tp, int *sockptr)
     return _translate_connect_errno(origerr);
 }
 
+#ifdef SPAMC_SSL
+static char * _ssl_err_as_string (void) {
+    BIO *bio = BIO_new(BIO_s_mem());
+    ERR_print_errors(bio);
+    char *buf = NULL;
+    size_t len = BIO_get_mem_data(bio, &buf);
+    char *ret = (char *)calloc(1, 1 + len);
+    if (!ret) {
+        BIO_free(bio);
+        char *err = "(could not get SSL error)";
+        return err;
+    }
+    memcpy(ret, buf, len);
+    BIO_free(bio);
+    /* Only return up to first newline */
+    char *lf = strchr(ret, '\n');
+    if (lf)
+        *lf = '\0';
+    return ret;
+}
+
+static SSL_CTX * _try_ssl_ctx_init(int flags)
+{
+    const SSL_METHOD *meth;
+    SSL_CTX *ctx;
+
+    SSLeay_add_ssl_algorithms();
+    SSL_load_error_strings();
+    /* this method allows negotiation of version */
+    meth = SSLv23_client_method();
+    ctx = SSL_CTX_new(meth);
+    if (ctx == NULL) {
+        libspamc_log(flags, LOG_ERR, "cannot create SSL CTX context: %s",
+                     _ssl_err_as_string());
+        return NULL;
+    }
+    if (flags & SPAMC_TLSV1) {
+       /* allow TLSv1.0 or better */
+       SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2|SSL_OP_NO_SSLv3);
+    } else {
+       /* allow SSLv3 or better */
+       SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2);
+    }
+    SSL_CTX_set_mode(ctx, SSL_MODE_AUTO_RETRY);
+    return ctx;
+}
+
+static int _try_ssl_connect(SSL_CTX *ctx, struct transport *tp,
+                           SSL **pssl, int flags, int sock)
+{
+    SSL *ssl;
+    int ssl_rtn;
+    if (tp->ssl_ca_file || tp->ssl_ca_path) {
+       if (!SSL_CTX_load_verify_locations(ctx, tp->ssl_ca_file,
+                                          tp->ssl_ca_path)) {
+           libspamc_log(flags, LOG_ERR,
+                        "error loading CA file %s or path %s: %s",
+                        tp->ssl_ca_file ? tp->ssl_ca_file : "(void)",
+                        tp->ssl_ca_path ? tp->ssl_ca_path : "(void)",
+                        _ssl_err_as_string());
+           return EX_OSERR;
+       }
+       SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
+    } else {
+        SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
+    }
+    if (flags & SPAMC_CLIENT_SSL_CERT) {
+       /* libspamc_log(flags, LOG_ERR, "loading client cert %s key %s",
+                    tp->ssl_cert_file, tp->ssl_key_file); */
+       if (!SSL_CTX_use_certificate_file(ctx, tp->ssl_cert_file,
+                                         SSL_FILETYPE_PEM)) {
+           libspamc_log(flags, LOG_ERR,
+                        "unable to load certificate file %s: %s",
+                        tp->ssl_cert_file, _ssl_err_as_string());
+           return EX_OSERR;
+       }
+       if (!SSL_CTX_use_PrivateKey_file(ctx, tp->ssl_key_file,
+                                        SSL_FILETYPE_PEM)) {
+           libspamc_log(flags, LOG_ERR,
+                        "unable to load key file %s: %s",
+                        tp->ssl_key_file, _ssl_err_as_string());
+           return EX_OSERR;
+       }
+       if (!SSL_CTX_check_private_key(ctx)) {
+           libspamc_log(flags, LOG_ERR,
+                        "key file %s and cert file %s do not match: %s",
+                        tp->ssl_key_file, tp->ssl_cert_file,
+                        _ssl_err_as_string());
+           return EX_OSERR;
+       }
+    }
+    ssl = SSL_new(ctx);
+    if (ssl == NULL) {
+        libspamc_log(flags, LOG_ERR,
+                    "SSL_new failed: %s", _ssl_err_as_string());
+        return EX_OSERR;
+    }
+    *pssl = ssl;
+    if (!SSL_set_fd(ssl, sock)) {
+       libspamc_log(flags, LOG_ERR,
+                    "SSL_set_fd failed: %s", _ssl_err_as_string());
+       return EX_OSERR;
+    }
+    ssl_rtn = SSL_connect(ssl);
+    if (ssl_rtn != 1) {
+       int ssl_err = SSL_get_error(ssl, ssl_rtn);
+       libspamc_log(flags, LOG_ERR,
+                    "SSL_connect error: %s", _ssl_err_as_string());
+       return EX_UNAVAILABLE;
+    }
+    return EX_OK;
+}
+#endif
+
 /* Aug 14, 2002 bj: Reworked things. Now we have message_read, message_write,
  * message_dump, lookup_host, message_filter, and message_process, and a bunch
  * of helper functions.
@@ -1213,13 +1327,13 @@ int message_filter(struct transport *tp, const char *username,
 
     if (flags & SPAMC_USE_SSL) {
 #ifdef SPAMC_SSL
-       SSLeay_add_ssl_algorithms();
-       meth = SSLv23_client_method();
-       SSL_load_error_strings();
-       ctx = SSL_CTX_new(meth);
+        ctx = _try_ssl_ctx_init(flags);
+        if (ctx == NULL) {
+           failureval = EX_OSERR;
+           goto failure;
+        }
 #else
        UNUSED_VARIABLE(ssl);
-       UNUSED_VARIABLE(meth);
        UNUSED_VARIABLE(ctx);
        libspamc_log(flags, LOG_ERR, "spamc not built with SSL support");
        return EX_SOFTWARE;
@@ -1372,17 +1486,33 @@ int message_filter(struct transport *tp, const char *username,
     
         if (flags & SPAMC_USE_SSL) {
 #ifdef SPAMC_SSL
-            ssl = SSL_new(ctx);
-            SSL_set_fd(ssl, sock);
-            SSL_connect(ssl);
+            rc = _try_ssl_connect(ctx, tp, &ssl, flags, sock);
+            if (rc != EX_OK) {
+                failureval = rc;
+                goto failure;
+            }
 #endif
         }
     
         /* Send to spamd */
         if (flags & SPAMC_USE_SSL) {
 #ifdef SPAMC_SSL
-            SSL_write(ssl, buf, len);
-            SSL_write(ssl, towrite_buf, towrite_len);
+            rc = SSL_write(ssl, buf, len);
+            if (rc <= 0) {
+                libspamc_log(flags, LOG_ERR, "SSL write failed (%d)",
+                             SSL_get_error(ssl, rc));
+                failureval = EX_IOERR;
+                goto failure;
+            }
+            rc = SSL_write(ssl, towrite_buf, towrite_len);
+            if (rc <= 0) {
+                libspamc_log(flags, LOG_ERR, "SSL write failed (%d)",
+                             SSL_get_error(ssl, rc));
+                failureval = EX_IOERR;
+                goto failure;
+            }
+            SSL_shutdown(ssl);
+            shutdown(sock, SHUT_WR);
 #endif
         }
         else {
@@ -1593,20 +1723,19 @@ int message_tell(struct transport *tp, const char *username, int flags,
     int failureval;
     SSL_CTX *ctx = NULL;
     SSL *ssl = NULL;
-    const SSL_METHOD *meth;
 
     assert(tp != NULL);
     assert(m != NULL);
 
     if (flags & SPAMC_USE_SSL) {
 #ifdef SPAMC_SSL
-       SSLeay_add_ssl_algorithms();
-       meth = SSLv23_client_method();
-       SSL_load_error_strings();
-       ctx = SSL_CTX_new(meth);
+        ctx = _try_ssl_ctx_init(flags);
+        if (ctx == NULL) {
+            failureval = EX_OSERR;
+            goto failure;
+        }
 #else
        UNUSED_VARIABLE(ssl);
-       UNUSED_VARIABLE(meth);
        UNUSED_VARIABLE(ctx);
        libspamc_log(flags, LOG_ERR, "spamc not built with SSL support");
        return EX_SOFTWARE;
@@ -1720,17 +1849,33 @@ int message_tell(struct transport *tp, const char *username, int flags,
 
     if (flags & SPAMC_USE_SSL) {
 #ifdef SPAMC_SSL
-       ssl = SSL_new(ctx);
-       SSL_set_fd(ssl, sock);
-       SSL_connect(ssl);
+        rc = _try_ssl_connect(ctx, tp, &ssl, flags, sock);
+        if (rc != EX_OK) {
+            failureval = rc;
+            goto failure;
+        }
 #endif
     }
 
     /* Send to spamd */
     if (flags & SPAMC_USE_SSL) {
 #ifdef SPAMC_SSL
-       SSL_write(ssl, buf, len);
-       SSL_write(ssl, m->msg, m->msg_len);
+       rc = SSL_write(ssl, buf, len);
+        if (rc <= 0) {
+            libspamc_log(flags, LOG_ERR, "SSL write failed (%d)",
+                         SSL_get_error(ssl, rc));
+            failureval = EX_IOERR;
+            goto failure;
+        }
+       rc = SSL_write(ssl, m->msg, m->msg_len);
+        if (rc <= 0) {
+            libspamc_log(flags, LOG_ERR, "SSL write failed (%d)",
+                         SSL_get_error(ssl, rc));
+            failureval = EX_IOERR;
+            goto failure;
+        }
+       SSL_shutdown(ssl);
+       shutdown(sock, SHUT_WR);
 #endif
     }
     else {
index 7a6fc1209753156620db8ff2a8d4cef926ccdfbd..269e57464d1068f583acca617c3d5d26a6ee3b2b 100644 (file)
  * */
 #define SPAMC_UNAVAIL_TEMPFAIL (1<<13)
 
+/* April 2022, add SSL client certificate support, bug 7267 */
+#define SPAMC_CLIENT_SSL_CERT (1<<12)
+
 #define SPAMC_MESSAGE_CLASS_SPAM 1
 #define SPAMC_MESSAGE_CLASS_HAM  2
 
@@ -249,6 +252,13 @@ struct transport
     /* Added for filterloop */
     int filter_retries;
     int filter_retry_sleep;
+
+#ifdef SPAMC_SSL
+    const char *ssl_cert_file;
+    const char *ssl_key_file;
+    const char *ssl_ca_file;
+    const char *ssl_ca_path;
+#endif
 };
 
 /* Initialise and setup transport-specific context for the connection
index dc660ee9f12a87156f2e68ef9f1ab437c2eb628b..6f14e2512cb0a71309a88eb6ce9dcda99a9df5df 100644 (file)
@@ -90,7 +90,7 @@ char *__progname = "spamc";
 
 
 /* safe fallback defaults to on now - CRH */
-int flags = SPAMC_RAW_MODE | SPAMC_SAFE_FALLBACK;
+int flags = SPAMC_RAW_MODE | SPAMC_SAFE_FALLBACK | SPAMC_TLSV1;
 
 /* global to control whether we should exit(0)/exit(1) on ham/spam */
 int use_exit_code = 0;
@@ -146,6 +146,10 @@ print_usage(void)
         "                      [default: 783]\n");
 #ifdef SPAMC_SSL
     usg("  -S, --ssl           Use SSL to talk to spamd.\n");
+    usg("  --ssl-cert cert     Authenticate using SSL client certificate.\n");
+    usg("  --ssl-key key       Specify an SSL client key PEM file.\n");
+    usg("  --ssl-ca-file file  Specify the location of the CA PEM file.\n");
+    usg("  --ssl-ca-path path  Specify a directory containin CA files.\n");
 #endif
 #ifndef _WIN32
     usg("  -U, --socket path   Connect to spamd via UNIX domain sockets.\n");
@@ -248,6 +252,10 @@ read_args(int argc, char **argv,
        { "randomize", no_argument, 0, 'H' },
        { "port", required_argument, 0, 'p' },
        { "ssl", optional_argument, 0, 'S' },
+       { "ssl-cert", optional_argument, 0, 5 },
+       { "ssl-key", optional_argument, 0, 6 },
+       { "ssl-ca-file", optional_argument, 0, 7 },
+       { "ssl-ca-path", optional_argument, 0, 8 },
        { "socket", required_argument, 0, 'U' },
        { "config", required_argument, 0, 'F' },
        { "timeout", required_argument, 0, 't' },
@@ -277,6 +285,13 @@ read_args(int argc, char **argv,
        { 0, 0, 0, 0} /* last element _must_ be all zeroes */
     };
     
+#ifdef SPAMC_SSL
+    ptrn->ssl_cert_file = 0;
+    ptrn->ssl_key_file = 0;
+    ptrn->ssl_ca_file = 0;
+    ptrn->ssl_ca_path = 0;
+#endif
+
     while ((opt = spamc_getopt_long(argc, argv, opts, longoptions, 
                 &longind)) != -1)
     {
@@ -522,9 +537,42 @@ read_args(int argc, char **argv,
                 ptrn->filter_retry_sleep = atoi(spamc_optarg);
                 break;
             }
+#ifdef SPAMC_SSL
+            case 5:
+            {
+                flags |= SPAMC_CLIENT_SSL_CERT;
+                ptrn->ssl_cert_file = spamc_optarg;
+                break;
+            }
+            case 6:
+            {
+                flags |= SPAMC_CLIENT_SSL_CERT;
+                ptrn->ssl_key_file = spamc_optarg;
+                break;
+            }
+            case 7:
+            {
+                ptrn->ssl_ca_file = spamc_optarg;
+                break;
+            }
+            case 8:
+            {
+                ptrn->ssl_ca_path = spamc_optarg;
+                break;
+            }
+#endif
         }
     }
 
+#ifdef SPAMC_SSL
+    if ((flags & SPAMC_CLIENT_SSL_CERT)
+       && !(ptrn->ssl_cert_file && ptrn->ssl_key_file)) {
+       libspamc_log(flags, LOG_ERR,
+                    "--ssl-cert and --ssl-key must be used together");
+       ret = EX_USAGE;
+    }
+#endif
+
     if (*max_size > SPAMC_MAX_MESSAGE_LEN) {
         libspamc_log(flags, LOG_ERR, "-s parameter is beyond max of %d",
                         SPAMC_MAX_MESSAGE_LEN);
@@ -728,7 +776,7 @@ get_output_fd(int *fd)
  * If the program's caller didn't identify the user to run as, use the
  * current user for this. Note that we're not talking about UNIX perm-
  * issions, but giving SpamAssassin a username so it can do per-user
- * configuration (whitelists & the like).
+ * configuration (welcomelists & the like).
  *
  * Allocates memory for the username, returns EX_OK if successful.
  */
index dcc602142f30c9522667954f5d3107cd7bfd0023..016182e0541689748adb90ec031c512ad9951992 100644 (file)
@@ -189,11 +189,33 @@ The default is 1 time (ie. one attempt and no retries).
 Sleep for I<sleep> seconds between failed spamd filtering attempts.
 The default is 1 second.
 
-=item B<-S>, B<--ssl>, B<--ssl>
+=item B<-S>, B<--ssl>
 
 If spamc was built with support for SSL, encrypt data to and from the
 spamd process with SSL; spamd must support SSL as well.
 
+=item B<--ssl-cert>=I<certfile>
+
+Authenticate to spamd server with a SSL client certificate.  Specify the
+certificate file to use.
+
+=item B<--ssl-key>=I<keyfile>
+
+Authenticate to spamd server with a SSL client certificate.  Specify the
+certificate key file to use.
+
+=item B<--ssl-ca-file>=I<cafile>
+
+Use the specified Certificate Authority certificate to verify the server
+certificate.  The server certificate must be signed by this certificate.
+
+=item B<--ssl-ca-path>=I<capath>
+
+Use the Certificate Authority certificate files in the specified set of
+directories to verify the server certificate.  The server certificate must
+be signed by one of these Certificate Authorities.  See the man page for
+B<IO::Socket::SSL> for additional details.
+
 =item B<-t> I<timeout>, B<--timeout>=I<timeout>
 
 Set the timeout for spamc-to-spamd communications (default: 600, 0 disables).
index f78e95ae66565f728920b02f7a4f09e38e57af4d..3f3555417addfed2a5b1d678113ba8ccde556213 100644 (file)
@@ -46,7 +46,7 @@ available separately on CPAN.
 
 BUGS
   
-See <http://issues.apache.org/SpamAssassin/> to report a bug.
+See <https://issues.apache.org/SpamAssassin/> to report a bug.
 
 Please include perl, Apache and mod_perl versions. 
 
index 9e4008349265f7e90d27f85eacbbb21087c9bdbb..5ab04f4208e00318b6403cadecb6450661275504 100644 (file)
@@ -255,7 +255,7 @@ sub _validate {
     if (exists $self->{'ident-timeout'} && $self->{'ident-timeout'} <= 0) {
       die "ERROR: --ident-timeout must be > 0\n";
     }
-    ##import Net::Ident qw(ident_lookup);
+    ##Net::Ident->import(qw(ident_lookup));
   }
 
   # let's not modify %ENV here...
index 13cf6915bae6c6f901e81aa552694f24f82a0892..cf9b6d5a07c92d999214b7fca0579f05b86c8d8d 100644 (file)
@@ -83,9 +83,9 @@ use mysql or LDAP for per-user configuration there is no reason in the world
 to run as root, and this remains fully functional.
 
 If you do not need to let your users define their own rules, maintain
-their own whitelists, or have non-world-readable home and ~/.spamassassin
+their own welcomelists, or have non-world-readable home and ~/.spamassassin
 directories, then just set spamd up to run with the "-u username" option.
-Since spamd can use auto-whitelisting, which requires it maintain a
+Since spamd can use auto-welcomelisting, which requires it maintain a
 database of email addresses on-disk, you should use a non-"root" but
 non-"nobody" user: "mailnull" or "mail" are good choices, or even create a
 "spamd" user.
@@ -112,7 +112,7 @@ you will need to either
 
   2. Alternatively, let the users train their individual Bayes database.
 
-http://wiki.apache.org/spamassassin/SiteWideBayesFeedback can be very
+https://wiki.apache.org/spamassassin/SiteWideBayesFeedback can be very
 helpful here.
 
 We have implemented an auto-learning algorithm (option 'bayes_auto_learn', on
@@ -129,7 +129,7 @@ spamc, it is easily possible for malicious users invoking modified
 spamc clients to make spamd:
 
  (1.)  read (and hence determine) the contents of other users configurations
- (2.)  change the contents of other users configurations (whitelisting)
+ (2.)  change the contents of other users configurations (welcomelisting)
  (3.)  grab CPU time as that user -- this is an issue on ulimit'd systems
 
 If users do not have the opportunity to invoke spamc themselves, and
@@ -194,7 +194,7 @@ There are no known bugs with this setup.  Several reasonable sized sites
 are now running it on their production mail systems.  However, you should
 still test it completely in *your environment* before trusting all your
 mail to it.  If you discover compilation, runtime, or load-performance
-bugs, please open a ticket at http://issues.apache.org/SpamAssassin/
+bugs, please open a ticket at https://issues.apache.org/SpamAssassin/
 
 There is an issue if you run spamd using the standard perl installation
 on Mac OS X and certain *BSD-flavored UNIX platforms.  spamd will change
index e158cc85be7698076bd6e0f4785f08d3922851b6..1024ec0064dd34e3c8040158fc154f4812c911c8 100755 (executable)
@@ -22,6 +22,20 @@ use strict;
 use warnings;
 use re 'taint';
 
+my @ORIG_INC_OPTS;
+BEGIN {
+  # bug 8030 - Save what is in @INC to capture any -I arguments passed in to use at SIGHUP restart
+  # This is done before any use lib statements add anything else to @INC
+  my %orig_inc;
+  for (my $i = $#INC; $i >=0; $i--) {
+    my $path = $INC[$i];
+    if (!$orig_inc{$path}) { # more stringent checking will done later after more modules are loaded
+       $orig_inc{$path} = 1;
+       unshift(@ORIG_INC_OPTS, $path);
+    }
+  }
+}
+
 my $PREFIX          = '@@PREFIX@@';             # substituted at 'make' time
 my $DEF_RULES_DIR   = '@@DEF_RULES_DIR@@';      # substituted at 'make' time
 my $LOCAL_RULES_DIR = '@@LOCAL_RULES_DIR@@';    # substituted at 'make' time
@@ -53,7 +67,7 @@ BEGIN {
   # Socket->VERSION(1.95);  # provides AI_ADDRCONFIG
     Socket->VERSION(1.96);  # provides NIx_NOSERV, and Exporter tag :addrinfo
   # Socket->VERSION(1.97);  # IO::Socket::IP depends on Socket 1.97
-    import Socket qw(/^(?:AI|NI|NIx|EAI)_/);
+    Socket->import(qw(/^(?:AI|NI|NIx|EAI)_/));
 
     # AUTOLOADing 'constants' here enables inlining - see Exporter man page
     &AI_ADDRCONFIG; &AI_PASSIVE;
@@ -64,13 +78,13 @@ BEGIN {
     require Socket6;
   # Socket6->VERSION(0.13);  # provides NI_NAMEREQD
     Socket6->VERSION(0.18);  # provides AI_NUMERICSERV
-    import Socket6 qw(/^(?:AI|NI|NIx|EAI)_/);
+    Socket6->import(qw(/^(?:AI|NI|NIx|EAI)_/));
     &AI_ADDRCONFIG; &AI_PASSIVE;  # enable inlining
     &NI_NUMERICHOST; &NI_NUMERICSERV; &NI_NAMEREQD; 1;
   };
 
   require Socket;
-  import Socket qw(:DEFAULT IPPROTO_TCP);
+  Socket->import(qw(:DEFAULT IPPROTO_TCP));
 
   &SOCK_STREAM; &IPPROTO_TCP; &SOMAXCONN; # enable inlining
 
@@ -249,12 +263,12 @@ use Mail::SpamAssassin::SpamdForkScaling qw(:pfstates);
 use Mail::SpamAssassin::Logger qw(:DEFAULT log_message);
 use Mail::SpamAssassin::Util qw(untaint_var untaint_file_path secure_tmpdir
                                 exit_status_str am_running_on_windows
-                                get_user_groups);
+                                get_user_groups force_die);
 use Mail::SpamAssassin::Timeout;
 
 use Getopt::Long;
 use POSIX qw(:sys_wait_h);
-use POSIX qw(locale_h setsid sigprocmask _exit);
+use POSIX qw(locale_h setsid sigprocmask);
 use Errno;
 use Fcntl qw(:flock);
 
@@ -315,7 +329,7 @@ sub print_usage_and_exit {
   }
 
   require Pod::Usage;
-  import Pod::Usage;
+  Pod::Usage->import;
   pod2usage(
     -verbose => 0,
     -message => $message,
@@ -326,12 +340,12 @@ sub print_usage_and_exit {
 # defaults
 my %opt = (
   'user-config'   => 1,
-  'ident-timeout' => 5.0,
   # scaling settings; some of these aren't actually settable via cmdline
   'server-scale-period' => 2,   # how often to scale the # of kids, secs
   'min-children'  => 1,         # min kids to have running
   'min-spare'     => 1,         # min kids that must be spare
   'max-spare'     => 2,         # max kids that should be spare
+  'pre'           => [],        # extra .pre lines
   'cf'            => [],        # extra config lines
 );
 
@@ -366,17 +380,16 @@ Getopt::Long::Configure("bundling");
 GetOptions(
   'allow-tell'               => \$opt{'tell'},
   'allowed-ips|A=s'          => \@{ $opt{'allowed-ip'} },
-  'auth-ident'               => \$opt{'auth-ident'},
   'configpath|C=s'           => \$opt{'configpath'},
   'c'                        => \$opt{'create-prefs'},
   'create-prefs!'            => \$opt{'create-prefs'},
   'daemonize!'               => \$opt{'daemonize'},
   'debug|D:s'                => \$opt{'debug'},
+  'default-user|U=s'         => \$opt{'default-user'},
   'd'                        => \$opt{'daemonize'},
   'groupname|g=s'            => \$opt{'groupname'},
   'helper-home-dir|H:s'      => \$opt{'home_dir_for_helpers'},
   'help|h'                   => \$opt{'help'},
-  'ident-timeout=f'          => \$opt{'ident-timeout'},
   '4|ipv4only|ipv4-only|ipv4'=> sub { $opt{'force_ipv4'} = 1;
                                       $opt{'force_ipv6'} = 0; },
   '6'                        => sub { $opt{'force_ipv6'} = 1;
@@ -404,6 +417,7 @@ GetOptions(
   'setuid-with-ldap'         => \$opt{'setuid-with-ldap'},
   'setuid-with-sql'          => \$opt{'setuid-with-sql'},
   'siteconfigpath=s'         => \$opt{'siteconfigpath'},
+  'pre=s'                    => \@{$opt{'pre'}},
   'cf=s'                     => \@{$opt{'cf'}},
   'socketgroup=s'            => \$opt{'socketgroup'},
   'socketmode=s'             => \$opt{'socketmode'},
@@ -411,6 +425,9 @@ GetOptions(
   'socketpath=s'             => \$opt{'socketpath'},
   'sql-config!'              => \$opt{'sql-config'},
   'ssl'                      => \$opt{'ssl'},
+  'ssl-verify'               => \$opt{'ssl-verify'},
+  'ssl-ca-file=s'            => \$opt{'ssl-ca-file'},
+  'ssl-ca-path=s'            => \$opt{'ssl-ca-path'},
   'ssl-port=s'               => \$opt{'ssl-port'},
   'syslog-socket=s'          => \$opt{'syslog-socket'},
   'syslog|s=s'               => \$opt{'syslog'},
@@ -452,6 +469,10 @@ if ($opt{'version'}) {
   exit($resphash{'EX_OK'});
 }
 
+if (!defined $opt{'default-user'}) {
+  $opt{'default-user'} = 'nobody';
+}
+
 my $log_timestamp_fmt = $opt{'log-timestamp-fmt'};
 if (defined $log_timestamp_fmt && lc($log_timestamp_fmt) eq 'default') {
   undef $log_timestamp_fmt;  # undefined implies per-logger's default
@@ -460,7 +481,8 @@ if (defined $log_timestamp_fmt) {
   # a nondefault timestamp format was specified, need to reopen stderr logger
   Mail::SpamAssassin::Logger::remove('stderr');
   Mail::SpamAssassin::Logger::add(method => 'stderr',
-                                 timestamp_fmt => $log_timestamp_fmt);
+                                 timestamp_fmt => $log_timestamp_fmt,
+                                 escape => 1);
 }
 
 # Enable debugging, if any areas were specified.  We do this already here,
@@ -499,16 +521,6 @@ if ( @{ $opt{'allowed-ip'} } ) {
   set_allowed_ip('127.0.0.1', '::1');
 }
 
-# ident-based spamc user authentication
-if ( $opt{'auth-ident'} ) {
-  eval { require Net::Ident }
-  or die "spamd: ident-based authentication requested, ".
-         "but Net::Ident is unavailable: $@\n";
-
-  $opt{'ident-timeout'} = undef if $opt{'ident-timeout'} <= 0.0;
-  import Net::Ident qw(ident_lookup);
-}
-
 ### Begin initialization of logging ########################
 
 # The syslog facility can be changed on the command line with the
@@ -600,7 +612,8 @@ if ($log_socket ne 'file' && $log_facility ne 'null') {
                                       socket => $log_socket,
                                       facility => $log_facility,
                                       ident => 'spamd',
-                                      timestamp_fmt => $log_timestamp_fmt))
+                                      timestamp_fmt => $log_timestamp_fmt,
+                                      escape => 1))
   {
     # syslog method failed 
     $log_facility = 'stderr';
@@ -610,7 +623,8 @@ if ($log_socket ne 'file' && $log_facility ne 'null') {
 elsif ($log_facility eq 'file') {
   if (!Mail::SpamAssassin::Logger::add(method => 'file',
                                       filename => $log_file,
-                                      timestamp_fmt => $log_timestamp_fmt))
+                                      timestamp_fmt => $log_timestamp_fmt,
+                                      escape => 1))
   {
     # file method failed
     $log_facility = 'stderr';
@@ -748,11 +762,22 @@ if ( defined $ENV{'HOME'} ) {
   delete $ENV{'HOME'};         # we do not want to use this when running spamd
 }
 
-# Do whitelist later in tmp dir. Side effect: this will be done as -u user.
+# Do welcomelist later in tmp dir. Side effect: this will be done as -u user.
+
+# Initialize SSL options
 
 $opt{'server-key'}  ||= "$LOCAL_RULES_DIR/certs/server-key.pem";
 $opt{'server-cert'} ||= "$LOCAL_RULES_DIR/certs/server-cert.pem";
 
+$opt{'ssl-verify'} = 1  if $opt{'ssl-ca-file'} || $opt{'ssl-ca-path'};
+$opt{'ssl'} ||= $opt{'ssl-verify'};
+if ($opt{'ssl-ca-file'} && !-e $opt{'ssl-ca-file'}) {
+  die "spamd: ssl-ca-file $opt{'ssl-ca-file'} does not exist\n";
+}
+if ($opt{'ssl-ca-path'} && !-e $opt{'ssl-ca-path'}) {
+  die "spamd: ssl-ca-path $opt{'ssl-ca-path'} does not exist\n";
+}
+
 # ---------------------------------------------------------------------------
 # Server (listening) socket setup for the various supported types
 
@@ -1071,21 +1096,41 @@ sub server_sock_setup_inet {
     );
     $sockopt{V6Only} = 1  if $io_socket_module_name eq 'IO::Socket::IP'
                              && IO::Socket::IP->VERSION >= 0.09;
-    %sockopt = (%sockopt, (
-      SSL_verify_mode => 0x00,
-      SSL_key_file    => $opt{'server-key'},
-      SSL_cert_file   => $opt{'server-cert'},
-    ))  if $ssl;
+    if ($ssl) {
+      if (!$have_ssl_module) {
+       eval { require IO::Socket::SSL; }
+       or die "spamd: SSL encryption requested, ".
+           "but IO::Socket::SSL is unavailable ($@)\n";
+       $have_ssl_module = 1;
+      }
+      %sockopt = (%sockopt, (
+        SSL_server      => 1,
+        SSL_key_file    => $opt{'server-key'},
+        SSL_cert_file   => $opt{'server-cert'},
+      ));
+      my $ssl_mode;
+      if ($opt{'ssl-verify'}) {
+       $ssl_mode = Net::SSLeay::VERIFY_PEER()
+           | Net::SSLeay::VERIFY_FAIL_IF_NO_PEER_CERT();
+       if ($opt{'ssl-ca-file'}) {
+         $sockopt{SSL_ca_file} = $opt{'ssl-ca-file'};
+       }
+       if ($opt{'ssl-ca-path'}) {
+         $sockopt{SSL_ca_path} = $opt{'ssl-ca-path'};
+       }
+       $sockopt{SSL_check_crl} = 0;
+       $sockopt{SSL_verifycn_scheme} = 'none';
+       $sockopt{SSL_verifycn_publicsuffix} = '';
+      } else {
+       $ssl_mode = Net::SSLeay::VERIFY_NONE()
+           | Net::SSLeay::VERIFY_FAIL_IF_NO_PEER_CERT();
+      }
+      $sockopt{SSL_verify_mode} = $ssl_mode;
+    }
     dbg("spamd: creating %s socket: %s",
         $ssl ? 'IO::Socket::SSL' : $io_socket_module_name,
         join(', ', map("$_: ".(defined $sockopt{$_} ? $sockopt{$_} : "(undef)"),
                        sort keys %sockopt)));
-    if ($ssl && !$have_ssl_module) {
-      eval { require IO::Socket::SSL }
-        or die "spamd: SSL encryption requested, ".
-               "but IO::Socket::SSL is unavailable ($@)\n";
-      $have_ssl_module = 1;
-    }
     my $server_inet = $ssl ? IO::Socket::SSL->new(%sockopt)
                            : $io_socket_module_name->new(%sockopt);
     my $diag;
@@ -1153,6 +1198,7 @@ my $spamtest = Mail::SpamAssassin->new(
     dont_copy_prefs      => $dontcopy,
     rules_filename       => ( $opt{'configpath'} || 0 ),
     site_rules_filename  => ( $opt{'siteconfigpath'} || 0 ),
+    pre_config_text      => join("\n", @{$opt{'pre'}})."\n",
     post_config_text     => join("\n", @{$opt{'cf'}})."\n",
     force_ipv4           => ( $opt{'force_ipv4'} || 0 ),
     local_tests_only     => ( $opt{'local'} || 0 ),
@@ -1214,7 +1260,9 @@ setup_parent_sig_handlers();
 
 # should be done post-daemonize such that any files created by this
 # process are written with the right ownership and everything.
+seteuid_to_user();
 preload_modules_with_tmp_homedir();
+restore_euid();
 
 # this must be after preload_modules_with_tmp_homedir(), for bug 5606
 $spamtest->init_learner({
@@ -1350,8 +1398,6 @@ sub spawn {
     }
 
     srand;  # reseed pseudorandom number generator soon for each child process
-    $spamtest->call_plugins("spamd_child_init");
-
     if ($sockets_access_lock_tempfile) {
       # A lock will be required across select+accept in a child processes,
       # Bug 6996. Need to have a per-child filehandle on the same lock file
@@ -1376,22 +1422,39 @@ sub spawn {
       # bug 3900: assignments to $> and $< problems with BSD perl bug
       # use the POSIX functions to hide the platform specific workarounds 
       dbg("spamd: Privilege de-escalation from user $< and groups $(\n");
-      $! = 0;
-      POSIX::setgid($ugid);  # set effective and real gid
-      dbg("spamd: setgid ERRNO is $!\n");
-      $( = $ugid;
-      $) = "$ugid ".(get_user_groups($uuid));  # set effective and real gid/grouplist another way because we lack initgroups in Perl
-      dbg("spamd: group assignment ERRNO is $!\n");
-      POSIX::setuid($uuid);  # set effective and real UID
-      dbg("spamd: setuid ERRNO is $!\n");
-      $< = $uuid; $> = $uuid;   # bug 5574
-      dbg("spamd: uid assignment ERRNO is $!\n");
-      dbg("spamd: real user is $< \neff user is $> \nreal groups are $( \neff groups are $) \n");
-
-
-      # keep the sanity check to catch problems like bug 3900 just in case
-      if ( $> != $uuid and $> != ( $uuid - 2**32 ) ) {
-        die "spamd: setuid to uid $uuid failed (> = $>, < = $<)\n";
+      my $togids = "$ugid ".get_user_groups($uuid);
+      if ($( ne $togids || $) ne $togids) {
+        $! = 0; POSIX::setgid($ugid);  # set effective and real gid
+        if ($!) { warn("spamd: POSIX::setgid $ugid failed: $!\n"); }
+        $! = 0; $( = $ugid;
+        if ($!) { warn("spamd: failed to set gid $ugid: $!\n"); }
+        # set effective and real gid/grouplist another way because we lack initgroups in Perl
+        $! = 0; $) = $togids;
+        if ($!) {
+          # could be perl 5.30 bug #134169, let's be safe
+          if (grep { $_ eq '0' } split(/ /, ${)})) {
+            die("spamd: failed to set effective gid $togids: $!\n");
+          } else {
+            warn("spamd: failed to set effective gid $togids: $!\n");
+          }
+        }
+      } else {
+        dbg("spamd: Group already set to $(");
+      }
+      if ($< != $uuid || $> != $uuid) {
+        $! = 0; POSIX::setuid($uuid);  # set effective and real UID
+        if ($!) { warn("spamd: POSIX::setuid $uuid failed: $!\n"); }
+        $! = 0; $< = $uuid; $> = $uuid;   # bug 5574
+        if ($!) { warn("spamd: setuid $uuid failed: $!\n"); }
+        dbg("spamd: now running as: ruid=$< euid=$> rgid=$( egid=$)");
+
+        # keep the sanity check to catch problems like bug 3900 just in case
+        if ( $> != $uuid and $> != ( $uuid - 2**32 ) ) {
+          sleep(1); # prevent spamd fork flooding
+          die "spamd: setuid to uid $uuid failed (ruid=$<, euid=$>), not started as root?\n";
+        }
+      } else {
+        dbg("spamd: Uid already set to $<");
       }
     }
 
@@ -1399,6 +1462,10 @@ sub spawn {
     # this will help make it clear via process listing which is child/parent
     $0 = 'spamd child';
 
+    # Let's call spamd_child_init only after root privs are dropped
+    # Mail::SpamAssassin::main() will also run this to set global_state_dir
+    $spamtest->call_plugins("spamd_child_init");
+
     $backchannel->setup_backchannel_child_post_fork();
     if ($scaling) {     # only do this once, for efficiency; $$ is a syscall
       $scaling->set_my_pid($$);
@@ -1421,12 +1488,12 @@ sub spawn {
       my $evalret = eval { accept_a_conn($scaling ? 0.5 : undef); };
 
       if (!defined $evalret) {
-        warn("spamd: error: $@ $!, continuing");
+        warn("spamd: error: $@, continuing\n");
         if ($client) { $client->close(); }  # avoid fd leaks
       }
       elsif ($evalret == -1) {
         # serious error; used for accept() failure
-        die("spamd: respawning server");
+        die("spamd: respawning server\n");
       }
 
       $spamtest->call_plugins("spamd_child_post_connection_close");
@@ -1546,18 +1613,21 @@ sub accept_from_any_server_socket {
       $socket or die "no socket???, impossible";
       dbg("spamd: accept() on fd %d", $selected_socket_info->{fd});
       $client = $socket->accept;
+      if (!defined $client) {
+        if (defined $socket) {
+          die sprintf("%s accept failed: %s\n", ref $socket,
+                       $socket->isa('IO::Socket::SSL') ?
+                         $socket->errstr : $@);
+        } else {
+          die "accept failed: no socket available: $!\n";
+        }
+      }
     }
     1;  # end eval with success
 
   } or do {
     my $err = $@ ne '' ? $@ : "errno=$!";  chomp $err;
-    if ($locked) {
-      dbg("spamd: releasing a lock over select+accept");
-      flock($sockets_access_lock_fh, LOCK_UN)
-        or die "Can't release sockets-access lock: $!";
-      $locked = 0;
-    }
-    die "accept_a_conn: $err";
+    info("spamd: accept_a_conn: $err");
   };
 
   if ($locked) {
@@ -1565,16 +1635,7 @@ sub accept_from_any_server_socket {
     flock($sockets_access_lock_fh, LOCK_UN)
       or die "Can't release sockets-access lock: $!";
   }
-  if(!defined $client) {
-    if(defined($socket)) {
-      die sprintf("accept_a_conn: %s accept failed: %s",
-                         ref $socket,
-                         !$socket->isa('IO::Socket::SSL') ? $!
-                           : $socket->errstr.", $!");
-    } else {
-      die sprintf("accept_a_conn: no socket available");
-    }
-  }
+
   return ($client, $selected_socket_info);
 }
 
@@ -1597,8 +1658,12 @@ sub accept_a_conn {
     if ( $! == &Errno::EINTR ) {
       return 0;
     }
+    elsif ( $@ =~ /ssl3_get_record:wrong version number/ ||
+            $@ =~ /peer did not return a certificate/ ) {
+      # Handshake error, not speaking SSL? No need to respawn
+      return 0;
+    }
     else {
-      warn("spamd: accept failed: $!");
       return -1;
     }
   }
@@ -1622,9 +1687,27 @@ sub accept_a_conn {
       peer_info_from_socket($client);
     $remote_hostaddr or die 'failed to obtain port and ip from socket';
 
-    my $msg = sprintf("connection from %s [%s]:%s to port %d, fd %d",
+    my $ssl_info = '';
+    if ($client->isa('IO::Socket::SSL')) {
+      $ssl_info = ', ';
+      my $ssl_version = $client->get_sslversion();
+      if (defined $ssl_version) {
+        $ssl_info .= $ssl_version.'/';
+      } else {
+        $ssl_version = $client->get_sslversion_int();
+        if    ($ssl_version == 0x0304) { $ssl_info .= 'TLSv1.3/'; }
+        elsif ($ssl_version == 0x0303) { $ssl_info .= 'TLSv1.2/'; }
+        elsif ($ssl_version == 0x0302) { $ssl_info .= 'TLSv1.1/'; }
+        elsif ($ssl_version == 0x0301) { $ssl_info .= 'TLSv1.0/'; }
+        elsif ($ssl_version == 0x0300) { $ssl_info .= 'SSLv3/'; }
+        elsif ($ssl_version == 0x0002) { $ssl_info .= 'SSLv2/'; }
+      }
+      $ssl_info .= $client->get_cipher();
+    }
+
+    my $msg = sprintf("connection from %s [%s]:%s to port %d, fd %d%s",
                       $remote_hostname, $remote_hostaddr, $remote_port,
-                      $local_port, $socket_info->{fd});
+                      $local_port, $socket_info->{fd}, $ssl_info);
     if (ip_is_allowed($remote_hostaddr)) {
       info("spamd: $msg");
     }
@@ -1748,15 +1831,15 @@ sub handle_setuid_to_user {
   }
   if (!am_running_on_windows()) {
     warn("spamd: still running as root: user not specified with -u, "
-         . "not found, or set to root, falling back to nobody\n");
+         . "not found, or set to root, falling back to $opt{'default-user'}\n");
 
     my ($name, $pwd, $uid, $gid, $quota, $comment, $gcos, $dir, $etc) =
-        getpwnam('nobody');
+        getpwnam($opt{'default-user'});
   
     $) = (get_user_groups($uid));       # eGID
     $> = $uid;                          # eUID
     if (!defined($uid) || ($> != $uid and $> != ($uid - 2**32))) {
-      die("spamd: setuid to nobody failed");
+      die("spamd: setuid to $opt{'default-user'} failed");
   }
 
   $spamtest->signal_user_changed(
@@ -1818,7 +1901,7 @@ sub zlib_inflate_read {
 
     # TODO: inflate in smaller buffers instead of at EOF
     while (1) {
-      my $numbytes = $client->read($buf, (1024 * 64) + $red, $red);
+      my $numbytes = $client->read($buf, (1024 * 64), $red);
       if (!defined $numbytes) {
         die "read of zlib data failed: $!";
         return -1;
@@ -1828,13 +1911,13 @@ sub zlib_inflate_read {
     }
 
     if ($red > $expected_length) {
-      warn "hmm, zlib read $red > expected_length $expected_length";
+      warn "spamd: zlib read $red > expected_length $expected_length\n";
       substr ($buf, $expected_length) = '';
     }
 
     ($out, $status) = $zlib->inflate($buf);
     if ($status != Compress::Zlib::Z_STREAM_END()) {
-      die "failed to find end of zlib stream";
+      die "failed to find end of zlib stream\n";
     }
   };
 
@@ -2077,7 +2160,7 @@ sub check {
   # ensure we didn't accidentally fork (bug 4370)
   if ($starting_self_pid != $$) {
     eval { warn("spamd: accidental fork: $$ != $starting_self_pid"); };
-    POSIX::_exit(1);        # avoid END and dtor processing
+    force_die(0);        # avoid END and dtor processing
   }
 
   return 1;
@@ -2159,18 +2242,23 @@ sub dotell {
   my @did_set;
   my @did_remove;
 
-  if ($hdrs->{set_local}) {
-    my $status = $spamtest->learn($mail, undef, ($hdrs->{message_class} eq 'spam' ? 1 : 0), 0);
+  # bug 5740 Don't bayes learn if global configs disabkle bayes,
+  #  also give user some control with userprefs bayes_learn_during_report option
 
-    push(@did_set, 'local') if ($status->did_learn());
-    $status->finish();
-  }
+  if (defined $spamtest->{bayes_scanner} && $spamtest->{conf}->{bayes_learn_during_report}) {
+    if ($hdrs->{set_local}) {
+      my $status = $spamtest->learn($mail, undef, ($hdrs->{message_class} eq 'spam' ? 1 : 0), 0);
+
+      push(@did_set, 'local') if ($status->did_learn());
+      $status->finish();
+    }
 
-  if ($hdrs->{remove_local}) {
-    my $status = $spamtest->learn($mail, undef, undef, 1);
+    if ($hdrs->{remove_local}) {
+      my $status = $spamtest->learn($mail, undef, undef, 1);
 
-    push(@did_remove, 'local') if ($status->did_learn());
-    $status->finish();
+      push(@did_remove, 'local') if ($status->did_learn());
+      $status->finish();
+    }
   }
 
   if ($hdrs->{set_remote}) {
@@ -2274,10 +2362,6 @@ sub parse_headers {
     $line =~ s/\r\n$//;
 
     if (!length $line) {    # end of headers
-      if (!$got_user_header && $opt{'auth-ident'}) {
-        service_unavailable_error('User header required');
-        return 0;
-      }
       return 1;
     }
 
@@ -2304,7 +2388,7 @@ sub parse_headers {
       return 0 unless got_remove_header($hdrs, $header, $value);
     }
     elsif ($header eq 'Compress') {
-      return 0 unless &got_compress_header($hdrs, $header, $value);
+      return 0 unless got_compress_header($hdrs, $header, $value);
     }
   }
 
@@ -2334,10 +2418,6 @@ sub got_user_header {
     $current_user = $1;
   }
 
-  if ($opt{'auth-ident'} && !auth_ident($current_user)) {
-    return 0;
-  }
-
   if ( !$opt{'user-config'} ) {
     if ( $opt{'sql-config'} ) {
       unless ( handle_user_sql($current_user) ) {
@@ -2444,10 +2524,10 @@ sub got_compress_header {
       return 0;
     }
     $hdrs->{compress_zlib} = 1;
-    dbg("spamd: compress header received\n");
+    dbg("spamd: compress header received: $value");
   }
   else {
-    protocol_error("(compression type not supported)");
+    protocol_error("(compression type not supported: $value)");
     return 0;
   }
 
@@ -2478,17 +2558,36 @@ sub service_timeout {
 
 ###########################################################################
 
-sub auth_ident {
-  my $username = shift;
-  my $ident_username = ident_lookup( $client, $opt{'ident-timeout'} );
-  my $dn = $ident_username || 'NONE';    # display name
-  dbg("ident: ident_username = $dn, spamc_username = $username\n");
-  if ( !defined($ident_username) || $username ne $ident_username ) {
-    info("spamd: ident username ($dn) does not match "
-        . "spamc username ($username)" );
-    return 0;
+sub seteuid_to_user {
+  return if (am_running_on_windows() || $> != 0);
+  
+  my $suidto = $opt{'username'} || $opt{'default-user'};
+  my ($name, $pwd, $uid, $gid, $quota, $comment, $gcos, $suiddir, $etc) = getpwnam($suidto);
+
+  if (!defined $uid) {
+      die "spamd: seteuid_to_user (getpwnam) unable to find user: '$suidto'\n";
+  }
+
+  $) = (get_user_groups($uid));     # change eGID
+  $> = $uid;                        # change eUID
+  if ( !defined($uid) || ( $> != $uid and $> != ( $uid - 2**32 ) ) ) {
+    # make it fatal to avoid security breaches
+    die("spamd: fatal error: setuid to $suidto failed");
+  }
+}
+
+sub restore_euid {
+  return if (am_running_on_windows());
+  
+  if (($> != $<) && ($> != ($< - 2**32))) {
+    $) = "$( $(";    # change eGID
+    $> = $<;         # change eUID
+    # check again; ensure the change happened
+    if ($> != $< && ($> != ( $< - 2**32))) {
+      # make it fatal to avoid security breaches
+      die("spamd: return setuid failed");
+    }
   }
-  return 1;
 }
 
 sub handle_user_setuid_basic {
@@ -3006,8 +3105,8 @@ sub serverstarted {
 }
 
 sub daemonize {
-  # removed bug 7594 # Pretty command line in ps
-    #$0 = join (' ', $ORIG_ARG0, @ORIG_ARGV) unless would_log("dbg");
+  # bug 8036 - ensure ps legacy name shows up as spamd even if command line call was perl path_to_spamd
+  $0 = 'spamd' unless would_log("dbg");
 
   # be a nice daemon and chdir to the root so we don't block any
   # unmount attempts
@@ -3175,15 +3274,12 @@ sub map_server_sockets {
 # is received
 my $perl_from_hashbang_line;
 sub prepare_for_sighup_restart {
-  # it'd be great if we could introspect the interpreter to figure this
-  # out, but bizarrely it seems unavailable.
-  if (open (IN, "<$ORIG_ARG0")) {
-    my $l = <IN>;
-    close IN;
-    if ($l && $l =~ /^#!\s*(\S+)\s*.*?$/) {
-      $perl_from_hashbang_line = $1;
-    }
-  }
+  @ORIG_INC_OPTS =
+    map {
+          my $path = untaint_var($_);
+          (File::Spec->file_name_is_absolute($path) and (-d $path))?("-I", $path):()
+        }
+        @ORIG_INC_OPTS;
 }
 
 sub do_sighup_restart {
@@ -3200,15 +3296,14 @@ sub do_sighup_restart {
 
   # ensure we re-run spamd using the right perl interpreter, and
   # with the right switches (taint mode and warnings) (bug 5255)
+  # Also need -I options (bug 8030) because there is no way
+  # to determine if everything in @INC came from this perl's defaults
   my $perl = untaint_var($^X);
-  my @execs = ( $perl, "-T", "-w", $ORIG_ARG0, @ORIG_ARGV );
-
-  if ($perl eq $perl_from_hashbang_line) {
-    # we're using the same perl as the script uses on the #! line;
-    # we can safely just exec the script
-    @execs = ( $ORIG_ARG0, @ORIG_ARGV );
-  }
+  my @execs = ( $perl, "-T", "-w", @ORIG_INC_OPTS, $ORIG_ARG0, @ORIG_ARGV );
 
+  # bug 8030 - removed code that in some cases just exec'd the script
+  # Can't ever exec the script in case the perl -I options are necessary
+  
   warn "spamd: restarting using '" . join (' ', @execs) . "'\n";
   exec @execs;
 
@@ -3233,6 +3328,7 @@ Options:
  -C path, --configpath=path        Path for default config files
  --siteconfigpath=path             Path for site configs
  --cf='config line'                Additional line of configuration
+ --pre='config line'               Additional line of ".pre" (prepended to configuration)
  -d, --daemonize                   Daemonize
  -h, --help                        Print usage message
  -i [ip_or_name[:port]], --listen=[ip_or_name[:port]] Listen on IP addr and port
@@ -3266,13 +3362,16 @@ Options:
  -g groupname, --groupname=groupname  Run as groupname
  -v, --vpopmail                    Enable vpopmail config
  -x, --nouser-config               Disable user config files
- --auth-ident                      Use ident to identify spamc user (deprecated)
- --ident-timeout=timeout           Timeout for ident connections
+ -U username, --default-user=username  Fall back to this username if spamc user
+                                   is not found (default: nobody)
  -D, --debug[=areas]               Print debugging messages (for areas)
  -L, --local                       Use local tests only (no DNS)
  -P, --paranoid                    Die upon user errors
  -H [dir], --helper-home-dir[=dir] Specify a different HOME directory
  --ssl                             Enable SSL on TCP connections
+ --ssl-verify                      Request a client certificate and verify it
+ --ssl-ca-file cafile              Certificate Authority certificate file
+ --ssl-ca-path capath              Certificate Authority directory
  --ssl-port port                   Override --port setting for SSL connections
  --server-key keyfile              Specify an SSL keyfile
  --server-cert certfile            Specify an SSL certificate
@@ -3333,12 +3432,11 @@ command to tell what type of message is being processed and whether
 local (learn/forget) or remote (report/revoke) databases should be
 updated.
 
-Note that spamd always trusts the username passed in (unless
-B<--auth-ident> is used) so clients could maliciously learn messages
-for other users. (This is not usually a concern with an SQL Bayes
-store as users will typically have read-write access directly to the
-database, and can also use C<sa-learn> with the B<-u> option to
-achieve the same result.)
+Note that spamd always trusts the username passed in so clients could
+maliciously learn messages for other users.  (This is not usually a concern
+with an SQL Bayes store as users will typically have read-write access
+directly to the database, and can also use C<sa-learn> with the B<-u> option
+to achieve the same result.)
 
 =item B<-c>, B<--create-prefs>
 
@@ -3360,6 +3458,12 @@ Add additional lines of configuration directly from the command-line, parsed
 after the configuration files are read.   Multiple B<--cf> arguments can be
 used, and each will be considered a separate line of configuration.
 
+=item B<--pre='config line'>
+
+Add additional lines of .pre configuration directly from the command-line,
+parsed before the configuration files are read.  Multiple B<--pre> arguments
+can be used, and each will be considered a separate line of configuration.
+
 =item B<-d>, B<--daemonize>
 
 Detach from starting process and run in background (daemonize).
@@ -3591,23 +3695,11 @@ configuration from the user's home directory (B<--user-config>).
 This option does not disable or otherwise influence the SQL, LDAP or
 Virtual Config Dir settings.
 
-=item B<--auth-ident>
-
-Verify the username provided by spamc using ident.  This is only
-useful if connections are only allowed from trusted hosts (because an
-identd that lies is trivial to create) and if spamc REALLY SHOULD be
-running as the user it represents.  Connections are terminated
-immediately if authentication fails.  In this case, spamc will pass
-the mail through unchecked.  Failure to connect to an ident server,
-and response timeouts are considered authentication failures.  This
-requires that Net::Ident be installed. Deprecated.
+=item B<-U> I<username>, B<--default-user>=I<username>
 
-=item B<--ident-timeout>=I<timeout>
-
-Wait at most I<timeout> seconds for a response to ident queries.
-Ident query that takes longer that I<timeout> seconds will fail, and
-mail will not be processed.  Setting this to 0.0 or less results in no
-timeout, which is STRONGLY discouraged.  The default is 5 seconds.
+Fall back to this username, if the username provided by spamc is not found.
+Default is I<nobody>, which might not exist or not have a usable home
+directory, use this setting to define a suitable user if needed.
 
 =item B<-A> I<host,...>, B<--allowed-ips>=I<host,...>
 
@@ -3651,7 +3743,7 @@ circumstances are available with an area of "info".
 For more information about which areas (also known as channels) are available,
 please see the documentation at:
 
-       C<http://wiki.apache.org/spamassassin/DebugChannels>
+       C<https://wiki.apache.org/spamassassin/DebugChannels>
 
 =item B<-4>, B<--ipv4only>, B<--ipv4-only>, B<--ipv4>
 
@@ -3673,8 +3765,8 @@ network tests.  Works the same as the C<-L> flag to C<spamassassin(1)>.
 
 =item B<-P>, B<--paranoid>
 
-Die on user errors (for the user passed from spamc) instead of falling back to
-user I<nobody> and using the default configuration.
+Die on user errors (for the user passed from spamc) instead of falling back
+to user C<--default-user> and using the default configuration.
 
 =item B<-m> I<number> , B<--max-children>=I<number>
 
@@ -3761,6 +3853,24 @@ connections.  If the B<--ssl> switch is used, and B<--ssl-port> is set, then
 unencrypted connections will be accepted on the B<--port>, at the same time as
 encrypted connections are accepted at B<--ssl-port>.
 
+=item B<--ssl-verify>
+
+Implies B<--ssl>.  Request a client certificate and verify the certificate. 
+Requires B<--ssl-ca-file> or B<--ssl-ca-path>.
+
+=item B<--ssl-ca-file>=I<cafile>
+
+Implies B<--ssl-verify>.  Use the specified Certificate Authority
+certificate to verify the client certificate.  The client certificate must
+be signed by this certificate.
+
+=item B<--ssl-ca-path>=I<capath>
+
+Implies B<--ssl-verify>.  Use the Certificate Authority certificate files in
+the specified set of directories to verify the client certificate.  The
+client certificate must be signed by one of these Certificate Authorities. 
+See the man page for B<IO::Socket::SSL> for additional details.
+
 =item B<--ssl-port>=I<port>
 
 Optionally specifies the port number for the server to listen on for
index 35815d7a927403e87abd00dd17ec1cea516a405c..62e7ff3cddbffb5f40ed18042649a52aa3f3cdb9 100644 (file)
@@ -6,13 +6,13 @@ SpamAssassin can now load users' score files from an SQL database.  The concept
 here is to have a web application (PHP/perl/ASP/etc.) that will allow users to
 be able to update their local preferences on how SpamAssassin will filter their
 e-mail.  The most common use for a system like this would be for users to be
-able to update the white list of addresses (whitelist_from) without the need
+able to update the white list of addresses (welcomelist_from, previously whitelist_from) without the need
 for them to update their $HOME/.spamassassin/user_prefs file.  It is also quite
 common for users listed in /etc/passwd to not have a home directory, therefore,
 the only way to have their own local settings would be through an RDBMS system.
 
 Note that this will NOT look for test rules, only local scores,
-whitelist_from(s), and required_score.
+welcomelist_from(s) (previously whitelist_from), and required_score.
 
 In addition, any config options marked as Admin Only will NOT be parsed from
 SQL preferences.
@@ -75,23 +75,16 @@ If you have a table layout that differs from the default, please
 review the documentation for user_scores_sql_custom_query for
 information on how deal with a custom layout.
 
+
 Requirements
 ------------
 
-In order for SpamAssassin to work with your SQL database, you must have
-the perl DBI module installed, AS WELL AS the DBD driver/module for your
-specific database.  For example, if using MySQL as your RDBMS, you must have
-the Msql-Mysql module installed.  Check CPAN for the latest versions of DBI 
-and your database driver/module. 
-
-We are currently using:
-
-  DBI-1.20
-  Msql-Mysql-modules-1.2219
-  perl v5.6.1
-
-But older and newer versions should work fine as the SQL code in SpamAssassin
-is as simple as could be.
+In order for SpamAssassin to work with your SQL database, you must have the
+perl DBI module installed, AS WELL AS the DBD driver/module for your
+specific database.  For example, if using MySQL/MariaDB as your RDBMS, you
+must have the DBD::mysql or DBD::MariaDB module installed.  For PostgreSQL
+use the DBD::Pg module.  Check CPAN for the latest versions of DBI and your
+database driver/module.
 
 
 Database Schema
@@ -101,7 +94,7 @@ The database must contain a table, default name "userpref", with at
 least three fields:
 
   username varchar(100)          # this is the username whose e-mail is being filtered
-  preference varchar(50)  # the preference (whitelist_from, required_score, etc.)
+  preference varchar(50)  # the preference (welcomelist_from (previously whitelist_from), required_score, etc.)
   value varchar(100)     # the value of the named preference
 
 You can add as many other fields you wish as long as the above three fields are
index 68de4a146bbf758a52e75e0a5e3a80677fbb8835..19d06c07c16d8542f65f88283e9e29ab6696ccce 100644 (file)
@@ -1,18 +1,18 @@
 
-Using SpamAssassin Auto-Whitelists With An SQL Database
+Using SpamAssassin Auto-Welcomelists With An SQL Database
 -------------------------------------------------------
 
-SpamAssassin can now load users' auto-whitelists from a SQL database.
+SpamAssassin can now load users' auto-welcomelists from a SQL database.
 The most common use for a system like this would be for users to be
-able to have per user auto-whitelists on systems where users may not
-have a home directory to store the whitelist DB files.
+able to have per user auto-welcomelists on systems where users may not
+have a home directory to store the welcomelist DB files.
 
-In order to activate the SQL based auto-whitelist you have to
-configure spamassassin and spamd to use a different whitelist factory.
-This is done with the auto_whitelist_factory config variable, like
+In order to activate the SQL based auto-welcomelist you have to
+configure spamassassin and spamd to use a different welcomelist factory.
+This is done with the auto_welcomelist_factory config variable, like
 so:
 
-auto_whitelist_factory Mail::SpamAssassin::SQLBasedAddrList
+auto_welcomelist_factory Mail::SpamAssassin::SQLBasedAddrList
 
 SpamAssassin will check the global configuration file (ie. any file
 matching /etc/mail/spamassassin/*.cf) for the following settings:
@@ -27,7 +27,7 @@ in the format as listed above.  <driver> should be the DBD driver that
 you have installed to access your database (initially tested with
 MySQL (driver is 'mysql'), PostgreSQL ('Pg') and SQLite ('SQLite')).
 <database> must be the name of the database that you created to store
-the auto-whitelist table. <hostname> is the name of the host that contains
+the auto-welcomelist table. <hostname> is the name of the host that contains
 the SQL database server.  <port> is the optional port number where your
 database server is listening.
 
@@ -39,10 +39,10 @@ default port number.  The other two required options tells SpamAssassin to use
 the defined username and password to establish the connection.
 
 If the user_awl_dsn option does not exist, SpamAssassin will not attempt
-to use SQL for the auto-whitelist.
+to use SQL for the auto-welcomelist.
 
 One additional configuration option exists that allows you to set the
-table name for the auto-whitelist table.
+table name for the auto-welcomelist table.
 
 user_awl_sql_table           awl
 
@@ -83,8 +83,8 @@ You can add as many other fields you wish as long as the above fields are
 contained in the table.
 
 The 'signedby' field was introduced in version 3.3.0 and is only needed
-if auto_whitelist_distinguish_signed is true, e.g. (in local.cf):
-  auto_whitelist_distinguish_signed 1
+if auto_welcomelist_distinguish_signed is true, e.g. (in local.cf):
+  auto_welcomelist_distinguish_signed 1
 and is only useful if a plugin DKIM is enabled. If the setting is off
 the field is not used, but it does no harm to have it in a table.
 The new field makes AWL keep separate records for author addresses with
@@ -134,7 +134,7 @@ under PostgreSQL:
     ADD PRIMARY KEY (username,email,signedby,ip);
 then add the following to local.cf to let SpamAssassin start using the
 newly added field 'signedby' :
-  auto_whitelist_distinguish_signed 1
+  auto_welcomelist_distinguish_signed 1
 
 To extend a field awl.ip on an existing table to be able to fit
 an IPv6 addresses (39 characters would suffice) or an IPv4 address:
@@ -146,7 +146,7 @@ under PostgreSQL:
 
 Once you have created the database and added the table, just add the
 required lines to your global configuration file (local.cf).  Note that
-you must specify the proper whitelist factory in the config file in order
+you must specify the proper welcomelist factory in the config file in order
 for this to work and the current username must be passed to spamd.
 
 Testing SpamAssassin/SQL
index 3e837bfec4489162ca8240cae0280af4c21298d4..7093060a2a04879ebc0113fcaaaec03bd755117c 100644 (file)
@@ -2,7 +2,7 @@
 CREATE TABLE bayes_expire (
   id int(11) NOT NULL default '0',
   runtime int(11) NOT NULL default '0',
-  KEY bayes_expire_idx1 (id)
+  PRIMARY KEY  (id)
 ) ENGINE=InnoDB;
 
 CREATE TABLE bayes_global_vars (
diff --git a/upstream/sql/decodeshorturl_mysql.sql b/upstream/sql/decodeshorturl_mysql.sql
new file mode 100644 (file)
index 0000000..719ac3f
--- /dev/null
@@ -0,0 +1,10 @@
+CREATE TABLE `short_url_cache`
+( `short_url` VARCHAR(255) NOT NULL,
+  `decoded_url` VARCHAR(512) NOT NULL,
+  `hits` INT NOT NULL DEFAULT 1,
+  `created` INT(11) NOT NULL,
+  `modified` INT(11) NOT NULL,
+  PRIMARY KEY (`short_url`)
+) ENGINE = InnoDB;
+-- Maintaining index for cleaning is likely more expensive than occasional full table scan
+-- ALTER TABLE `short_url_cache` ADD INDEX `short_url_created` (`created`);
diff --git a/upstream/sql/decodeshorturl_pg.sql b/upstream/sql/decodeshorturl_pg.sql
new file mode 100644 (file)
index 0000000..d14dfcd
--- /dev/null
@@ -0,0 +1,10 @@
+CREATE TABLE short_url_cache (
+  short_url VARCHAR(256) NOT NULL,
+  decoded_url VARCHAR(512) NOT NULL,
+  hits INT NOT NULL DEFAULT 1,
+  created INT NOT NULL,
+  modified INT NOT NULL,
+  PRIMARY KEY (short_url)
+);
+-- Maintaining index for cleaning is likely more expensive than occasional full table scan
+-- ALTER TABLE short_url_cache ADD INDEX short_url_created (created);
diff --git a/upstream/sql/decodeshorturl_sqlite.sql b/upstream/sql/decodeshorturl_sqlite.sql
new file mode 100644 (file)
index 0000000..965d522
--- /dev/null
@@ -0,0 +1,2 @@
+-- Manual database creation for SQLite is not necessary,
+-- DecodeShortURLs plugin will create and clean database automatically.
index 191074cf1eb9eab15b637bd3f43f8e8c62be7680..ea2bb4d7a766141b54fadddc706249562630f137 100644 (file)
@@ -19,7 +19,7 @@ BEGIN
 END;
 $$ language 'plpgsql';
 
-create TRIGGER update_txrep_update_last_hit BEFORE UPDATE
+create TRIGGER update_txrep_update_last_hit BEFORE UPDATE 
 ON txrep FOR EACH ROW EXECUTE PROCEDURE
 update_txrep_last_hit();
 
index e799f8d8c10c37a6b5b9714660ed26d28951b881..a5aaac4fcee98bbe12002afb6ef101f3f06d5d83 100644 (file)
@@ -15,7 +15,7 @@ CREATE TRIGGER [UpdateLastHit]
     AFTER UPDATE
     ON txrep
     FOR EACH ROW
-    WHEN NEW.last_hot < OLD.last_hit
+    WHEN NEW.last_hit < OLD.last_hit
 BEGIN
     UPDATE txrep SET last_hit=CURRENT_TIMESTAMP 
     WHERE (username=OLD.username AND email=OLD.email AND signedby=OLD.signedby AND ip=OLD.ip);
index f94c6268886c44faa77f124397b423c6eb5a018b..ae894ea153db8e8dff99a7eba04f1c4ff854dc92 100644 (file)
@@ -16,6 +16,11 @@ that includes the testing step would look like
 See the INSTALL file for details on the build and installation
 process.
 
+For faster testing with multiple CPUs, you can activate parallel processing
+with HARNESS_OPTIONS=j<x>.  For example, this would run 8 tests in parallel:
+
+ make test HARNESS_OPTIONS=j8
+
 Regression Test Options
 -----------------------
 
@@ -80,9 +85,9 @@ values.
 SPAMD_HOST
 SPAMD_PORT
 SPAMD_LOCALHOST
+SPAMD_SCRIPT
 SPAMASSASSIN_SCRIPT
 SPAMC_SCRIPT
-SPAMD_SCRIPT
 SAAWL_SCRIPT
 SACHECKSPAMD_SCRIPT
 SALEARN_SCRIPT
index f9c7b5bbe68173ed61ccddf2d66c0deb1bcd7495..98fba44534958c4398b3647499044f90bb47e02f 100644 (file)
@@ -2,6 +2,8 @@
 # imported into main for ease of use.
 package main;
 
+require v5.14.0;
+
 # use strict;
 # use warnings;
 # use re 'taint';
@@ -12,6 +14,7 @@ use File::Basename;
 use File::Copy;
 use File::Path;
 use File::Spec;
+use File::Temp qw(tempdir);
 
 use Test::Builder ();
 use Test::More    ();
@@ -21,8 +24,11 @@ use POSIX qw(WIFEXITED WIFSIGNALED WIFSTOPPED WEXITSTATUS WTERMSIG WSTOPSIG);
 use vars qw($RUNNING_ON_WINDOWS $SSL_AVAILABLE
             $SKIP_SPAMD_TESTS $SKIP_SPAMC_TESTS $NO_SPAMC_EXE
             $SKIP_SETUID_NOBODY_TESTS $SKIP_DNSBL_TESTS
-            $have_inet4 $have_inet6 $spamdhost $spamdport);
+            $have_inet4 $have_inet6 $spamdhost $spamdport
+            $workdir $siterules $localrules $userrules $userstate
+            $keep_workdir $mainpid $spamd_pidfile);
 
+my $sa_code_dir;
 BEGIN {
   require Exporter;
   use vars qw(@ISA @EXPORT @EXPORT_OK);
@@ -45,24 +51,56 @@ BEGIN {
 
   $have_inet4 = eval {
     require IO::Socket::INET;
-    my $sock = IO::Socket::INET->new(LocalAddr => '0.0.0.0', Proto => 'udp');
+    my $sock = IO::Socket::INET->new(LocalAddr => '127.0.0.1', Proto => 'udp');
     $sock->close or die "error closing inet socket: $!"  if $sock;
     $sock ? 1 : undef;
   };
 
   $have_inet6 = eval {
     require IO::Socket::INET6;
-    my $sock = IO::Socket::INET6->new(LocalAddr => '::', Proto => 'udp');
+    my $sock = IO::Socket::INET6->new(LocalAddr => '::1', Proto => 'udp');
     $sock->close or die "error closing inet6 socket: $!"  if $sock;
     $sock ? 1 : undef;
   };
 
   # Clean PATH so taint doesn't complain
-  $ENV{'PATH'} = '/bin:/usr/bin:/usr/local/bin';
-  # Remove tainted envs, at least ENV used in FreeBSD
-  delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
+  if (!$RUNNING_ON_WINDOWS) {
+    $ENV{'PATH'} = '/bin:/usr/bin:/usr/local/bin';
+    # Remove tainted envs, at least ENV used in FreeBSD
+    delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
+  } else {
+    # Windows might need non-system directories in PATH to run a Perl installation
+    # The best we can do is clean out obviously bad stuff such as relative paths or \..\
+    my @pathdirs = split(';', $ENV{'PATH'});
+    $ENV{'PATH'} =
+      join(';', # filter for only dirs that are canonical absolute paths that exist
+        map {
+              my $pathdir = $_;
+              $pathdir =~ s/\\*\z//;
+              my $abspathdir = File::Spec->canonpath(Cwd::realpath($pathdir)) if (-d $pathdir);
+              if (defined $abspathdir) {
+                $abspathdir  =~ /^(.*)\z/s;
+                $abspathdir = $1; # untaint it
+              }
+              ((defined $abspathdir) and (lc $pathdir eq lc $abspathdir))?($abspathdir):()
+            }
+          @pathdirs);
+  }
+  
+  # Fix INC to point to absolute path of built SA
+  if (-e 't/test_dir') { $sa_code_dir = 'blib/lib'; }
+  elsif (-e 'test_dir') { $sa_code_dir = '../blib/lib'; }
+  else { die "FATAL: not in or below test directory?\n"; }
+  File::Spec->rel2abs($sa_code_dir) =~ /^(.*)\z/s;
+  $sa_code_dir = $1;
+  if (not -d $sa_code_dir) {
+    die "FATAL: not in expected directory relative to built code tree?\n";
+  }
 }
 
+# use is run at compile time, but after the variable has been computed in the BEGIN block
+use lib $sa_code_dir;
+
 # Set up for testing. Exports (as global vars):
 # out: $home: $HOME env variable
 # out: $cwd: here
@@ -72,6 +110,7 @@ BEGIN {
 #
 sub sa_t_init {
   my $tname = shift;
+  $mainpid = $$;
 
   if ($config{PERL_PATH}) {
     $perl_path = $config{PERL_PATH};
@@ -100,6 +139,31 @@ sub sa_t_init {
   $perl_cmd .= " -T" if !defined($ENV{'TEST_PERL_TAINT'}) or $ENV{'TEST_PERL_TAINT'} ne 'no';
   $perl_cmd .= " -w" if !defined($ENV{'TEST_PERL_WARN'})  or $ENV{'TEST_PERL_WARN'}  ne 'no';
 
+  # Copy directories in PERL5LIB into -I options in perl_cmd because -T suppresses use of PERL5LIB in call to ./spamassassin
+  # If PERL5LIB is empty copy @INC instead because on some platforms like FreeBSD MakeMaker clears PER5LIB and sets @INC
+  # Filter out relative paths, and canonicalize so no symlinks or /../ will be left in untainted result as a nod to security
+  # Since this is only used to run tests, the security considerations are not as strict as with more general situations.
+  my @pathdirs = @INC;
+  if ($ENV{'PERL5LIB'}) {
+    @pathdirs = split($Config{path_sep}, $ENV{'PERL5LIB'});
+  }
+  my $inc_opts =
+    join(' -I', # filter for only dirs that are absolute paths that exist, then canonicalize them
+      map {
+            my $pathdir = $_;
+            my $canonpathdir = File::Spec->canonpath(Cwd::realpath($pathdir)) if ((-d $pathdir) and File::Spec->file_name_is_absolute($pathdir));
+            if (defined $canonpathdir) {
+               $canonpathdir =~ /^(.*)\z/s;
+               $canonpathdir = $1; # untaint it
+            }
+            ((defined $canonpathdir))?($canonpathdir):()
+          }
+         @pathdirs);
+  $perl_cmd .= " -I$inc_opts" if ($inc_opts);
+  
+  # To work in Windows, the perl scripts have to be launched by $perl_cmd and
+  # the ones that are exe files have to be directly called in the command lines
+  
   $scr = $ENV{'SPAMASSASSIN_SCRIPT'};
   $scr ||= "$perl_cmd ../spamassassin.raw";
 
@@ -113,10 +177,10 @@ sub sa_t_init {
   $salearn ||= "$perl_cmd ../sa-learn.raw";
 
   $saawl = $ENV{'SAAWL_SCRIPT'};
-  $saawl ||= "../sa-awl";
+  $saawl ||= "$perl_cmd ../sa-awl";
 
   $sacheckspamd = $ENV{'SACHECKSPAMD_SCRIPT'};
-  $sacheckspamd ||= "../sa-check_spamd";
+  $sacheckspamd ||= "$perl_cmd ../sa-check_spamd";
 
   $spamdlocalhost = $ENV{'SPAMD_LOCALHOST'};
   if (!$spamdlocalhost) {
@@ -124,36 +188,74 @@ sub sa_t_init {
   }
   $spamdhost = $ENV{'SPAMD_HOST'};
   $spamdhost ||= $spamdlocalhost;
-  $spamdport = $ENV{'SPAMD_PORT'};
-  $spamdport ||= probably_unused_spamd_port();
 
   # optimisation -- don't setup spamd test parameters unless we're
   # not skipping all spamd tests and this particular test is called
   # called "spamd_something" or "spamc_foo"
   # We still run spamc tests when there is an external SPAMD_HOST, but don't have to set up the spamd parameters for it
-  if ($SKIP_SPAMD_TESTS or ($tname !~ /spam[cd]/)) {
-    $NO_SPAMD_REQUIRED = 1;
+  if ($tname !~ /spam[cd]/) {
+    $TEST_DOES_NOT_RUN_SPAMC_OR_D = 1;
+  } else {
+    $spamdport = $ENV{'SPAMD_PORT'};
+    $spamdport ||= probably_unused_spamd_port();
   }
 
-  $spamd_cf_args = "-C log/test_rules_copy";
-  $spamd_localrules_args = " --siteconfigpath log/localrules.tmp";
-  $scr_localrules_args =   " --siteconfigpath log/localrules.tmp";
-  $salearn_localrules_args =   " --siteconfigpath log/localrules.tmp";
+  (-f "t/test_dir") && chdir("t");        # run from ..
+  -f "test_dir"  or die "FATAL: not in test directory?\n";
 
-  $scr_cf_args = "-C log/test_rules_copy";
-  $scr_pref_args = "-p log/test_default.cf";
-  $salearn_cf_args = "-C log/test_rules_copy";
-  $salearn_pref_args = "-p log/test_default.cf";
+  unless (-d "log") {
+    mkdir ("log", 0755) or die ("Error creating log dir: $!");
+  }
+  chmod (0755, "log"); # set in case log already exists with wrong permissions
+
+  if (!$RUNNING_ON_WINDOWS) {
+    untaint_system("chacl -B log 2>/dev/null || setfacl -b log 2>/dev/null"); # remove acls that confuse test
+  }
+
+  # clean old workdir if sa_t_init called multiple times
+  if (defined $workdir) {
+    if (!$keep_workdir) {
+      rmtree($workdir);
+    }
+  }
+
+  # individual work directory to make parallel tests possible
+  $workdir = tempdir("$tname.XXXXXX", DIR => "log");
+  die "FATAL: failed to create workdir: $!" unless -d $workdir;
+  $keep_workdir = 0;
+  # $siterules contains all stock *.pre files
+  $siterules = "$workdir/siterules";
+  # $localrules contains all stock *.cf files
+  $localrules = "$workdir/localrules";
+  # $userrules contains user rules
+  $userrules = "$workdir/user.cf";
+  # user_state directory
+  $userstate = "$workdir/user_state";
+
+  mkdir($siterules) or die "FATAL: failed to create $siterules\n";
+  mkdir($localrules) or die "FATAL: failed to create $localrules\n";
+  open(OUT, ">$userrules") or die "FATAL: failed to create $userrules\n";
+  close(OUT);
+  mkdir($userstate) or die "FATAL: failed to create $userstate\n";
+
+  $spamd_pidfile = "$workdir/spamd.pid";
+  $spamd_cf_args = "-C $localrules";
+  $spamd_localrules_args = " --siteconfigpath $siterules";
+  $scr_localrules_args =   " --siteconfigpath $siterules";
+  $salearn_localrules_args =   " --siteconfigpath $siterules";
+
+  $scr_cf_args = "-C $localrules";
+  $scr_pref_args = "-p $userrules";
+  $salearn_cf_args = "-C $localrules";
+  $salearn_pref_args = "-p $userrules";
   $scr_test_args = "";
   $salearn_test_args = "";
-  $set_test_prefs = 0;
+  $set_user_prefs = 0;
   $default_cf_lines = "
-    bayes_path ./log/user_state/bayes
-    auto_whitelist_path ./log/user_state/auto-whitelist
+    bayes_path ./$userstate/bayes
+    auto_welcomelist_path ./$userstate/auto-welcomelist
   ";
 
-  (-f "t/test_dir") && chdir("t");        # run from ..
-
   read_config();
 
   # if running as root, ensure "nobody" can write to it too
@@ -168,77 +270,45 @@ sub sa_t_init {
     $tmp_dir_mode = 0755;
   }
 
-  if (!$NO_SPAMD_REQUIRED) {
-    $NO_SPAMC_EXE = ($RUNNING_ON_WINDOWS &&
+  $NO_SPAMC_EXE = $TEST_DOES_NOT_RUN_SPAMC_OR_D ||
+                  ($RUNNING_ON_WINDOWS &&
                    !$ENV{'SPAMC_SCRIPT'} &&
                    !(-e "../spamc/spamc.exe"));
-    $SKIP_SPAMC_TESTS = ($NO_SPAMC_EXE ||
-                       ($RUNNING_ON_WINDOWS && !$ENV{'SPAMD_HOST'})); 
-    $SSL_AVAILABLE = ((!$SKIP_SPAMC_TESTS) &&  # no SSL test if no spamc
-                    (!$SKIP_SPAMD_TESTS) &&  # or if no local spamd
-                    (untaint_cmd("$spamc -V") =~ /with SSL support/) &&
-                    (untaint_cmd("$spamd --version") =~ /with SSL support/));
-  }
-  # do not remove prior test results!
-  # rmtree ("log");
-
-  unless (-d "log") {
-    mkdir ("log", 0755) or die ("Error creating log dir: $!");
-  }
-  chmod (0755, "log"); # set in case log already exists with wrong permissions
-
-  if (!$RUNNING_ON_WINDOWS) {
-    untaint_system("chacl -B log 2>/dev/null || setfacl -b log 2>/dev/null"); # remove acls that confuse test
-  }
-
-  rmtree ("log/user_state");
-  rmtree ("log/outputdir.tmp");
-
-  rmtree ("log/test_rules_copy");
-  mkdir ("log/test_rules_copy", 0755);
-
-  for $tainted (<../rules/*.cf>, <../rules/*.pm>, <../rules/*.pre>) {
+  $SKIP_SPAMC_TESTS = ($NO_SPAMC_EXE ||
+                     ($RUNNING_ON_WINDOWS && !$ENV{'SPAMD_HOST'})); 
+  $SSL_AVAILABLE = (!$TEST_DOES_NOT_RUN_SPAMC_OR_D) &&
+                  (!$SKIP_SPAMC_TESTS) &&  # no SSL test if no spamc
+                  (!$SKIP_SPAMD_TESTS) &&  # or if no local spamd
+                  (untaint_cmd("$spamc -V") =~ /with SSL support/) &&
+                  (untaint_cmd("$spamd --version") =~ /with SSL support/);
+
+  for $tainted (<../rules/*.pm>, <../rules/*.pre>, <../rules/languages>) {
     $tainted =~ /(.*)/;
     my $file = $1;
     $base = basename $file;
-    copy ($file, "log/test_rules_copy/$base")
-      or warn "cannot copy $file to log/test_rules_copy/$base: $!";
+    copy ($file, "$siterules/$base")
+      or warn "cannot copy $file to $siterules/$base: $!";
   }
 
-  copy ("data/01_test_rules.pre", "log/test_rules_copy/01_test_rules.pre")
-    or warn "cannot copy data/01_test_rules.cf to log/test_rules_copy/01_test_rules.pre: $!";
-  copy ("data/01_test_rules.cf", "log/test_rules_copy/01_test_rules.cf")
-    or warn "cannot copy data/01_test_rules.cf to log/test_rules_copy/01_test_rules.cf: $!";
-
-  rmtree ("log/localrules.tmp");
-  mkdir ("log/localrules.tmp", 0755);
-
-  for $tainted (<../rules/*.pm>, <../rules/*.pre>) {
+  for $tainted (<../rules/*.cf>) {
     $tainted =~ /(.*)/;
     my $file = $1;
     $base = basename $file;
-    copy ($file, "log/localrules.tmp/$base")
-      or warn "cannot copy $file to log/localrules.tmp/$base: $!";
+    copy ($file, "$localrules/$base")
+      or warn "cannot copy $file to $localrules/$base: $!";
   }
 
-  copy ("../rules/user_prefs.template", "log/test_rules_copy/99_test_default.cf")
-    or die "user prefs copy failed: $!";
+  copy ("data/01_test_rules.pre", "$localrules/01_test_rules.pre")
+    or warn "cannot copy data/01_test_rules.cf to $localrules/01_test_rules.pre: $!";
+  copy ("data/01_test_rules.cf", "$localrules/01_test_rules.cf")
+    or warn "cannot copy data/01_test_rules.cf to $localrules/01_test_rules.cf: $!";
 
-  open (PREFS, ">>log/test_rules_copy/99_test_default.cf")
-    or die "cannot append to log/test_rules_copy/99_test_default.cf: $!";
+  open (PREFS, ">>$localrules/99_test_default.cf")
+    or die "cannot append to $localrules/99_test_default.cf: $!";
   print PREFS $default_cf_lines
-    or die "error writing to log/test_rules_copy/99_test_default.cf: $!";
-  close PREFS
-    or die "error closing log/test_rules_copy/99_test_default.cf: $!";
-
-  # create an empty .prefs file
-  open (PREFS, ">>log/test_default.cf")
-    or die "cannot append to log/test_default.cf: $!";
+    or die "error writing to $localrules/99_test_default.cf: $!";
   close PREFS
-    or die "error closing log/test_default.cf: $!";
-
-  mkdir("log/user_state",$tmp_dir_mode);
-  chmod ($tmp_dir_mode, "log/user_state");  # unaffected by umask
+    or die "error closing $localrules/99_test_default.cf: $!";
 
   $home = $ENV{'HOME'};
   $home ||= $ENV{'WINDIR'} if (defined $ENV{'WINDIR'});
@@ -250,10 +320,26 @@ sub sa_t_init {
   $spamd_run_as_user = ($RUNNING_ON_WINDOWS || ($> == 0)) ? "nobody" : (getpwuid($>))[0] ;
 }
 
-# a port number between 32768 and 65535; used to allow multiple test
+# remove all rules - $localrules/*.cf
+# when you want to only use rules declared inside a specific *.t
+sub clear_localrules {
+  for $tainted (<$localrules/*.cf>) {
+    $tainted =~ /(.*)/;
+    my $file = $1;
+    # Keep some useful, should not contain any rules
+    next if $file =~ /10_default_prefs.cf$/;
+    next if $file =~ /20_aux_tlds.cf$/;
+    # Keep our own tstprefs() or tstlocalrules()
+    next if $file =~ /99_test_prefs.cf$/;
+    next if $file =~ /99_test_rules.cf$/;
+    unlink $file;
+  }
+}
+
+# a port number between 40000 and 65520; used to allow multiple test
 # suite runs on the same machine simultaneously
 sub probably_unused_spamd_port {
-  return 0 if $NO_SPAMD_REQUIRED;
+  return 0 if $SKIP_SPAMD_TESTS;
 
   my $port;
   my @nstat;
@@ -263,11 +349,9 @@ sub probably_unused_spamd_port {
     @nstat = grep(/^\s*tcp/i, <NSTAT>);
     close(NSTAT);
   }
-  my $delta = ($$ % 32768) || int(rand(32768));
-  for (1..10) {
-    $port = 32768 + $delta;
+  for (1..20) {
+    $port = 40000 + int(rand(65500-40000));
     last unless (getservbyport($port, "tcp") || grep(/[:.]$port\s/, @nstat));
-    $delta = int(rand(32768));
   }
   return $port;
 }
@@ -291,31 +375,35 @@ sub sa_t_finish {
 
 sub tstfile {
   my $file = shift;
-  open (OUT, ">log/mail.txt") or die;
+  open (OUT, ">$workdir/mail.txt") or die;
   print OUT $file; close OUT;
 }
 
-sub tstlocalrules {
+sub tstprefs {
   my $lines = shift;
 
-  $set_local_rules = 1;
+  open (OUT, ">$localrules/99_test_prefs.cf") or die;
+  print OUT $lines; close OUT;
+}
+
+sub tstlocalrules {
+  my $lines = shift;
 
-  open (OUT, ">log/localrules.tmp/00test.cf") or die;
+  open (OUT, ">$localrules/99_test_rules.cf") or die;
   print OUT $lines; close OUT;
 }
 
-sub tstprefs {
+sub tstuserprefs {
   my $lines = shift;
 
-  $set_test_prefs = 1;
+  $set_user_prefs = 1;
 
   # TODO: should we use -p, or modify the test_rules_copy/99_test_default.cf?
   # for now, I'm taking the -p route, since we have to be able to test
   # the operation of user-prefs in general, itself.
 
-  open (OUT, ">log/tst.cf") or die;
+  open (OUT, ">$userrules") or die;
   print OUT $lines; close OUT;
-  $scr_pref_args = "-p log/tst.cf";
 }
 
 # creates a .pre file in the localrules dir to be parsed alongside init.pre
@@ -324,10 +412,25 @@ sub tstprefs {
 sub tstpre {
   my $lines = shift;
 
-  open (OUT, ">log/localrules.tmp/zz_tst.pre") or die;
+  open (OUT, ">$siterules/zz_test.pre") or die;
   print OUT $lines; close OUT;
 }
 
+# remove default compatibility option
+sub disable_compat {
+  my $compat = shift;
+  return unless defined $compat;
+  open (IN, "$siterules/init.pre") or die;
+  open (OUT, ">$siterules/init.pre.new") or die;
+  while (<IN>) {
+    next if $_ =~ /^\s*enable_compat\s+\Q$compat\E(?:\s|$)/i;
+    print OUT $_;
+  }
+  close OUT or die;
+  close IN or die;
+  rename("$siterules/init.pre.new", "$siterules/init.pre");
+}
+
 # Run spamassassin. Calls back with the output.
 # in $args: arguments to run with
 # in $read_sub: callback for the output (should read from <IN>).
@@ -355,14 +458,15 @@ sub sarun {
   my $scrargs = "$scr $args";
   $scrargs =~ s!/!\\!g if ($^O =~ /^MS(DOS|Win)/i);
   print ("\t$scrargs\n");
-  (-d "log/d.$testname") or mkdir ("log/d.$testname", 0755);
+  (-d "$workdir/d.$testname") or mkdir ("$workdir/d.$testname", 0755);
   
   my $test_number = test_number();
-
-  untaint_system("$scrargs > log/d.$testname/$test_number $post_redir");
+  $current_checkfile = "$workdir/d.$testname/$test_number";
+#print STDERR "RUN: $scrargs\n";
+  untaint_system("$scrargs > $workdir/d.$testname/$test_number $post_redir");
   $sa_exitcode = ($?>>8);
   if ($sa_exitcode != 0) { return undef; }
-  &checkfile ("d.$testname/$test_number", $read_sub) if (defined $read_sub);
+  &checkfile ("$workdir/d.$testname/$test_number", $read_sub) if (defined $read_sub);
   1;
 }
 
@@ -392,14 +496,15 @@ sub salearnrun {
   my $salearnargs = "$salearn $args";
   $salearnargs =~ s!/!\\!g if ($^O =~ /^MS(DOS|Win)/i);
   print ("\t$salearnargs\n");
-  (-d "log/d.$testname") or mkdir ("log/d.$testname", 0755);
+  (-d "$workdir/d.$testname") or mkdir ("$workdir/d.$testname", 0755);
 
   my $test_number = test_number();
+  $current_checkfile = "$workdir/d.$testname/$test_number";
 
-  untaint_system("$salearnargs > log/d.$testname/$test_number");
+  untaint_system("$salearnargs > $workdir/d.$testname/$test_number");
   $salearn_exitcode = ($?>>8);
   if ($salearn_exitcode != 0) { return undef; }
-  &checkfile ("d.$testname/$test_number", $read_sub) if (defined $read_sub);
+  &checkfile ("$workdir/d.$testname/$test_number", $read_sub) if (defined $read_sub);
   1;
 }
 
@@ -451,14 +556,14 @@ sub spamcrun {
   $spamcargs =~ s!/!\\!g if ($^O =~ /^MS(DOS|Win)/i);
 
   print ("\t$spamcargs\n");
-  (-d "log/d.$testname") or mkdir ("log/d.$testname", 0755);
+  (-d "$workdir/d.$testname") or mkdir ("$workdir/d.$testname", 0755);
 
   my $test_number = test_number();
 
   if ($capture_stderr) {
-    untaint_system ("$spamcargs > log/d.$testname/out.$test_number 2>&1");
+    untaint_system ("$spamcargs > $workdir/d.$testname/out.$test_number 2>&1");
   } else {
-    untaint_system ("$spamcargs > log/d.$testname/out.$test_number");
+    untaint_system ("$spamcargs > $workdir/d.$testname/out.$test_number");
   }
 
   $sa_exitcode = ($?>>8);
@@ -468,7 +573,7 @@ sub spamcrun {
 
   %found = ();
   %found_anti = ();
-  &checkfile ("d.$testname/out.$test_number", $read_sub) if (defined $read_sub);
+  &checkfile ("$workdir/d.$testname/out.$test_number", $read_sub) if (defined $read_sub);
 
   if ($expect_failure) {
     ($sa_exitcode != 0);
@@ -497,10 +602,10 @@ sub spamcrun_background {
   $spamcargs =~ s!/!\\!g if ($^O =~ /^MS(DOS|Win)/i);
 
   print ("\t$spamcargs &\n");
-  (-d "log/d.$testname") or mkdir ("log/d.$testname", 0755);
+  (-d "$workdir/d.$testname") or mkdir ("$workdir/d.$testname", 0755);
   
   my $test_number = test_number();
-  untaint_system ("$spamcargs > log/d.$testname/bg.$test_number &") and return 0;
+  untaint_system ("$spamcargs > $workdir/d.$testname/bg.$test_number &") and return 0;
 
   1;
 }
@@ -518,15 +623,15 @@ sub sdrun {
 }
 
 sub recreate_outputdir_tmp {
-  rmtree ("log/outputdir.tmp"); # some tests use this
-  mkdir ("log/outputdir.tmp", $tmp_dir_mode);
-  chmod ($tmp_dir_mode, "log/outputdir.tmp");  # unaffected by umask
+  rmtree ("$workdir/outputdir.tmp"); # some tests use this
+  mkdir ("$workdir/outputdir.tmp", $tmp_dir_mode);
+  chmod ($tmp_dir_mode, "$workdir/outputdir.tmp");  # unaffected by umask
 }
 
 # out: $spamd_stderr
 sub start_spamd {
   return if $SKIP_SPAMD_TESTS;
-  die "NO_SPAMD_REQUIRED in start_spamd! oops" if $NO_SPAMD_REQUIRED;
+  die "TEST_DOES_NOT_RUN_SPAMC_OR_D; in start_spamd! oops" if $TEST_DOES_NOT_RUN_SPAMC_OR_D;
 
   my $spamd_extra_args = shift;
 
@@ -571,13 +676,12 @@ sub start_spamd {
     warn "oops! SATest.pm: a test prefs file was created, but spamd isn't reading it\n";
   }
 
-  (-d "log/d.$testname") or mkdir ("log/d.$testname", 0755);
+  (-d "$workdir/d.$testname") or mkdir ("$workdir/d.$testname", 0755);
   
   my $test_number = test_number();
-  my $spamd_stdout = "log/d.$testname/spamd.out.$test_number";
-     $spamd_stderr = "log/d.$testname/spamd.err.$test_number";    #  global
-  my $spamd_stdlog = "log/d.$testname/spamd.log.$test_number";
-  my $spamd_pidfile = "log/spamd.pid";
+  my $spamd_stdout = "$workdir/d.$testname/spamd.out.$test_number";
+     $spamd_stderr = "$workdir/d.$testname/spamd.err.$test_number";    #  global
+  my $spamd_stdlog = "$workdir/d.$testname/spamd.log.$test_number";
   my $spamd_forker = $ENV{'SPAMD_FORKER'}   ?
                        $ENV{'SPAMD_FORKER'} :
                      $RUNNING_ON_WINDOWS    ?
@@ -599,7 +703,7 @@ sub start_spamd {
 
   # DEBUG instrumentation to trace spamd processes. See bug 5731 for history
   # if (-f "/home/jm/capture_spamd_straces") {
-  # $spamd_cmd = "strace -ttt -fo log/d.$testname/spamd.strace.$test_number $spamd_cmd";
+  # $spamd_cmd = "strace -ttt -fo $workdir/d.$testname/spamd.strace.$test_number $spamd_cmd";
   # }
 
   unlink ($spamd_stdout, $spamd_stderr, $spamd_stdlog, $spamd_pidfile);
@@ -616,7 +720,11 @@ sub start_spamd {
   sleep $wait ;
   while ($spamd_pid <= 0) {
     my $spamdlog = '';
-    my $pidstr = untaint_cmd("cat $spamd_pidfile 2>/dev/null");
+    my $pidstr;
+    if (open(PID, $spamd_pidfile)) {
+      $pidstr = <PID>;
+      close PID;
+    }
     if ($pidstr) {
        chomp $pidstr;
        $spamd_pid = $pidstr;
@@ -641,7 +749,7 @@ sub start_spamd {
     }
 
     my $sleep = (int($wait++ / 4) + 1);
-    warn "spam_pid not found: Sleeping $sleep - Retry # $retries\n";
+    warn "spam_pid not found: Sleeping $sleep - Retry # $retries\n" if $retries && $retries < 20;
 
     sleep $sleep if $retries > 0;
 
@@ -658,7 +766,7 @@ sub start_spamd {
 
 sub stop_spamd {
   return 0 if ( defined($spamd_already_killed) || $SKIP_SPAMD_TESTS);
-  die "NO_SPAMD_REQUIRED in stop_spamd! oops" if $NO_SPAMD_REQUIRED;
+  die "TEST_DOES_NOT_RUN_SPAMC_OR_D; in stop_spamd! oops" if $TEST_DOES_NOT_RUN_SPAMC_OR_D;
 
   $spamd_pid ||= 0;
   $spamd_pid = untaint_var($spamd_pid);
@@ -688,10 +796,10 @@ sub create_saobj {
 
   # YUCK, these file/dir names should be some sort of variable, at
   # least we keep their definition in the same file for the moment.
-  my %setup_args = ( rules_filename => 'log/test_rules_copy',
-                    site_rules_filename => 'log/localrules.tmp',
-                    userprefs_filename => 'log/test_default.cf',
-                    userstate_dir => 'log/user_state',
+  my %setup_args = ( rules_filename => $localrules,
+                    site_rules_filename => $siterules,
+                    userprefs_filename => $userrules,
+                    userstate_dir => $userstate,
                     local_tests_only => 1,
                      # debug => 'all',
                   );
@@ -701,7 +809,6 @@ sub create_saobj {
     $setup_args{$arg} = $args->{$arg};
   }
 
-  # We'll assume that the test has setup INC correctly
   require Mail::SpamAssassin;
 
   my $sa = Mail::SpamAssassin->new(\%setup_args);
@@ -712,7 +819,6 @@ sub create_saobj {
 sub create_clientobj {
   my $args = shift;
 
-  # We'll assume that the test has setup INC correctly
   require Mail::SpamAssassin::Client;
 
   my $client = Mail::SpamAssassin::Client->new($args);
@@ -727,15 +833,11 @@ sub checkfile {
   my $read_sub = shift;
 
   # print "Checking $filename\n";
-  if (!open (IN, "< log/$filename")) {
-    # could be it already contains the "log/" prefix?
-    if (!open (IN, "< $filename")) {
-      warn "cannot open log/$filename or $filename"; return undef;
-    } else {
-      push @files_checked, "$filename";
-    }
+  if (!open (IN, "< $filename")) {
+    warn "cannot open $filename";
+    return undef;
   } else {
-    push @files_checked, "log/$filename";
+    push @files_checked, "$filename";
   }
   &$read_sub();
   close IN;
@@ -743,53 +845,64 @@ sub checkfile {
 
 # ---------------------------------------------------------------------------
 
-sub pattern_to_re {
-  my $pat = shift;
-
-  if ($pat =~ /^\/(.*)\/$/) {
-    return $1;
-  }
-
-  $pat = quotemeta($pat);
-
-  # make whitespace irrelevant; match any amount as long as the
-  # non-whitespace chars are OK.
-  $pat =~ s/\\\s/\\s\*/gs;
-  $pat;
-}
-
-# ---------------------------------------------------------------------------
-
 sub patterns_run_cb {
-  local ($_);
   my $string = shift;
 
-  if (defined $string) {
-    $_ = $string;
-  } else {
-    $_ = join ('', <IN>);
+  if (!defined $string) {
+    $string = join ('', <IN>);
   }
-  $matched_output = $_;
+  $matched_output = $string;
 
   # create default names == the pattern itself, if not specified
+  my %seen;
   foreach my $pat (keys %patterns) {
     if ($patterns{$pat} eq '') {
       $patterns{$pat} = $pat;
     }
+    if ($seen{$patterns{$pat}}++) {
+      die "ERROR: duplicate pattern name found: '$patterns{$pat}'\n";
+    }
+  }
+  %seen = ();
+  foreach my $pat (keys %anti_patterns) {
+    if ($anti_patterns{$pat} eq '') {
+      $anti_patterns{$pat} = $pat;
+    }
+    if ($seen{$anti_patterns{$pat}}++) {
+      die "ERROR: duplicate anti_pattern name found: '$anti_patterns{$pat}'\n";
+    }
   }
 
   foreach my $pat (sort keys %patterns) {
-    my $safe = pattern_to_re ($pat);
-    # print "JMD $patterns{$pat}\n";
-    if ($_ =~ /${safe}/s) {
-      $found{$patterns{$pat}}++;
+    if (index($pat, '(?^') == 0) { # Detect qr// regex, it's a string now
+      if ($string =~ $pat) {
+        $found{$patterns{$pat}}++;
+      }
+    } else {
+      my $re = $pat;
+      $re =~ s/([^A-Za-z_0-9\s])/\\$1/gs; # quotemeta
+      $re =~ s/\s+/\\s+/gs; # normalize whitespace
+      eval { $re = qr/$re/; 1; };
+      if ($@) { die "ERROR: failed to compile regex: '$re'\n"; }
+      if ($string =~ $re) {
+        $found{$patterns{$pat}}++;
+      }
     }
   }
   foreach my $pat (sort keys %anti_patterns) {
-    my $safe = pattern_to_re ($pat);
-    # print "JMD $patterns{$pat}\n";
-    if ($_ =~ /${safe}/s) {
-      $found_anti{$anti_patterns{$pat}}++;
+    if (index($pat, '(?^') == 0) { # Detect qr// regex, it's a string now
+      if ($string =~ $pat) {
+        $found_anti{$anti_patterns{$pat}}++;
+      }
+    } else {
+      my $re = $pat;
+      $re =~ s/([^A-Za-z_0-9\s])/\\$1/gs; # quotemeta
+      $re =~ s/\s+/\\s+/gs; # normalize whitespace
+      eval { $re = qr/$re/; 1; };
+      if ($@) { die "ERROR: failed to compile regex: '$re'\n"; }
+      if ($string =~ $re) {
+        $found_anti{$anti_patterns{$pat}}++;
+      }
     }
   }
 }
@@ -806,8 +919,10 @@ sub ok_all_patterns {
         ok ($found{$type} == 1) or warn "Found more than once: $type at $file line $line.\n";
       }
     } else {
-      warn "\tNot found: $type = $pat at $file line $line.\n";
+      my $typestr = $type eq $pat ? "" : "$type = ";
+      warn "\tNot found: $typestr$pat at $file line $line.\n";
       if (!$dont_ok) {
+        $keep_workdir = 1;
         ok (0);                     # keep the right # of tests
       }
       $wasfailure++;
@@ -817,7 +932,8 @@ sub ok_all_patterns {
     my $type = $anti_patterns{$pat};
     print "\tChecking for anti-pattern $type at $file line $line.\n";
     if (defined $found_anti{$type}) {
-      warn "\tFound anti-pattern: $type = $pat at $file line $line.\n";
+      my $typestr = $type eq $pat ? "" : "$type = ";
+      warn "\tFound anti-pattern: $typestr$pat at $file line $line.\n";
       if (!$dont_ok) { ok (0); }
       $wasfailure++;
     }
@@ -830,6 +946,7 @@ sub ok_all_patterns {
   if ($wasfailure) {
     warn "Output can be examined in: ".
          join(' ', @files_checked)."\n"  if @files_checked;
+    $keep_workdir = 1;
     return 0;
   } else {
     return 1;
@@ -849,7 +966,8 @@ sub skip_all_patterns {
       if ($skip) {
         warn "\tTest skipped: $skip at $file line $line.\n";
       } else {
-        warn "\tNot found: $type = $pat at $file line $line.\n";
+        my $typestr = $type eq $pat ? "" : "$type = ";
+        warn "\tNot found: $typestr$pat at $file line $line.\n";
       }
       skip ($skip, 0);                     # keep the right # of tests
     }
@@ -858,7 +976,8 @@ sub skip_all_patterns {
     my $type = $anti_patterns{$pat};
     print "\tChecking for anti-pattern $type\n";
     if (defined $found_anti{$type}) {
-      warn "\tFound anti-pattern: $type = $pat at $file line $line.\n";
+      my $typestr = $type eq $pat ? "" : "$type = ";
+      warn "\tFound anti-pattern: $typestr$pat at $file line $line.\n";
       skip ($skip, 0);
     }
     else
@@ -937,40 +1056,10 @@ sub conf_bool {
   return 0;                                 # n or 0
 }
 
-sub mk_safe_tmpdir {
-  return $safe_tmpdir if defined($safe_tmpdir);
-
-  my $dir = File::Spec->tmpdir() || 'log';
-
-  # be a little paranoid, since we're using a public tmp dir and
-  # are exposed to race conditions
-  my $retries = 10;
-  my $tmp;
-  while (1) {
-    $tmp = "$dir/satest.$$.".rand(99999);
-    if (!-d $tmp && mkdir ($tmp, 0755)) {
-      if (-d $tmp && -o $tmp) {     # check we own it
-        lstat($tmp);
-        if (-d _ && -o _) {         # double-check, ignoring symlinks
-          last;                     # we got it safely
-        }
-      }
-    }
-
-    die "cannot get tmp dir, giving up" if ($retries-- < 0);
-
-    warn "failed to create tmp dir '$tmp' safely, retrying...";
-    sleep 1;
-  }
-
-  $safe_tmpdir = $tmp;
-  return $tmp;
-}
-
-sub cleanup_safe_tmpdir {
-  if ($safe_tmpdir) {
-    rmtree($safe_tmpdir) or warn "cannot rmtree $safe_tmpdir";
-  }
+sub mk_socket_tempdir {
+  my $dir = tempdir(CLEANUP => 1);
+  die "FATAL: failed to create socket_tempdir: $!" unless -d $dir;
+  return $dir;
 }
 
 sub wait_for_file_to_change_or_disappear {
@@ -1157,4 +1246,11 @@ sub untaint_cmd {
     }
 }
 
+END {
+  # Cleanup workdir (but not if inside forked process)
+  if (defined $workdir && !$keep_workdir && $$ == $mainpid) {
+    rmtree($workdir);
+  }
+}
+
 1;
index 855395f7524f86f09a1342e79c66767117a8f46f..b64750c2e5ae4b16b8624eabaa52a14d2bde3a32 100755 (executable)
@@ -1,76 +1,74 @@
 #!/usr/bin/perl -T
 
-use lib '.'; 
-use lib 't';
-use SATest; 
-sa_t_init("all_modules");
+use lib '.'; use lib 't';
+use SATest; sa_t_init("all_modules");
 
 use Test::More;
-plan tests => 5;
+plan tests => 6;
 
 # ---------------------------------------------------------------------------
 
-my $plugins = '';
-
-if (eval { require BSD::Resource; }) {
-    $plugins .= "loadplugin Mail::SpamAssassin::Plugin::ResourceLimits\n"
-}
-if (eval { require Net::CIDR::Lite; }) {
-    $plugins .= "loadplugin Mail::SpamAssassin::Plugin::URILocalBL\n";
-}
-
 tstpre ("
-loadplugin Mail::SpamAssassin::Plugin::RelayCountry
-loadplugin Mail::SpamAssassin::Plugin::URIDNSBL
-loadplugin Mail::SpamAssassin::Plugin::Hashcash
-loadplugin Mail::SpamAssassin::Plugin::SPF
-loadplugin Mail::SpamAssassin::Plugin::DCC
-loadplugin Mail::SpamAssassin::Plugin::Pyzor
-loadplugin Mail::SpamAssassin::Plugin::Razor2
-loadplugin Mail::SpamAssassin::Plugin::SpamCop
-loadplugin Mail::SpamAssassin::Plugin::AntiVirus
-loadplugin Mail::SpamAssassin::Plugin::AWL
-loadplugin Mail::SpamAssassin::Plugin::AutoLearnThreshold
-# TODO fix finding languages file..
-#loadplugin Mail::SpamAssassin::Plugin::TextCat
-loadplugin Mail::SpamAssassin::Plugin::AccessDB
-loadplugin Mail::SpamAssassin::Plugin::WhiteListSubject
-loadplugin Mail::SpamAssassin::Plugin::MIMEHeader
-loadplugin Mail::SpamAssassin::Plugin::ReplaceTags
-loadplugin Mail::SpamAssassin::Plugin::DKIM
-loadplugin Mail::SpamAssassin::Plugin::Check
-loadplugin Mail::SpamAssassin::Plugin::HTTPSMismatch
-loadplugin Mail::SpamAssassin::Plugin::URIDetail
-loadplugin Mail::SpamAssassin::Plugin::Shortcircuit
-loadplugin Mail::SpamAssassin::Plugin::Bayes
-loadplugin Mail::SpamAssassin::Plugin::BodyEval
-loadplugin Mail::SpamAssassin::Plugin::DNSEval
-loadplugin Mail::SpamAssassin::Plugin::HTMLEval
-loadplugin Mail::SpamAssassin::Plugin::HeaderEval
-loadplugin Mail::SpamAssassin::Plugin::MIMEEval
-loadplugin Mail::SpamAssassin::Plugin::RelayEval
-loadplugin Mail::SpamAssassin::Plugin::URIEval
-loadplugin Mail::SpamAssassin::Plugin::WLBLEval
-loadplugin Mail::SpamAssassin::Plugin::VBounce
-loadplugin Mail::SpamAssassin::Plugin::Rule2XSBody
-loadplugin Mail::SpamAssassin::Plugin::ASN
-loadplugin Mail::SpamAssassin::Plugin::ImageInfo
-loadplugin Mail::SpamAssassin::Plugin::PhishTag
-loadplugin Mail::SpamAssassin::Plugin::FreeMail
-loadplugin Mail::SpamAssassin::Plugin::AskDNS
-loadplugin Mail::SpamAssassin::Plugin::TxRep
-loadplugin Mail::SpamAssassin::Plugin::PDFInfo
-loadplugin Mail::SpamAssassin::Plugin::HashBL
-loadplugin Mail::SpamAssassin::Plugin::FromNameSpoof
-loadplugin Mail::SpamAssassin::Plugin::Phishing
-$plugins
+  loadplugin Mail::SpamAssassin::Plugin::ResourceLimits
+  loadplugin Mail::SpamAssassin::Plugin::RelayCountry
+  loadplugin Mail::SpamAssassin::Plugin::URIDNSBL
+  loadplugin Mail::SpamAssassin::Plugin::SPF
+  loadplugin Mail::SpamAssassin::Plugin::DCC
+  loadplugin Mail::SpamAssassin::Plugin::Pyzor
+  loadplugin Mail::SpamAssassin::Plugin::Razor2
+  loadplugin Mail::SpamAssassin::Plugin::SpamCop
+  loadplugin Mail::SpamAssassin::Plugin::AntiVirus
+  loadplugin Mail::SpamAssassin::Plugin::AWL
+  loadplugin Mail::SpamAssassin::Plugin::AutoLearnThreshold
+  loadplugin Mail::SpamAssassin::Plugin::TextCat
+  loadplugin Mail::SpamAssassin::Plugin::AccessDB
+  loadplugin Mail::SpamAssassin::Plugin::WelcomeListSubject
+  loadplugin Mail::SpamAssassin::Plugin::MIMEHeader
+  loadplugin Mail::SpamAssassin::Plugin::ReplaceTags
+  loadplugin Mail::SpamAssassin::Plugin::DKIM
+  loadplugin Mail::SpamAssassin::Plugin::Check
+  loadplugin Mail::SpamAssassin::Plugin::HTTPSMismatch
+  loadplugin Mail::SpamAssassin::Plugin::URIDetail
+  loadplugin Mail::SpamAssassin::Plugin::Shortcircuit
+  loadplugin Mail::SpamAssassin::Plugin::Bayes
+  loadplugin Mail::SpamAssassin::Plugin::BodyEval
+  loadplugin Mail::SpamAssassin::Plugin::DNSEval
+  loadplugin Mail::SpamAssassin::Plugin::HTMLEval
+  loadplugin Mail::SpamAssassin::Plugin::HeaderEval
+  loadplugin Mail::SpamAssassin::Plugin::MIMEEval
+  loadplugin Mail::SpamAssassin::Plugin::RelayEval
+  loadplugin Mail::SpamAssassin::Plugin::URIEval
+  loadplugin Mail::SpamAssassin::Plugin::WLBLEval
+  loadplugin Mail::SpamAssassin::Plugin::VBounce
+  loadplugin Mail::SpamAssassin::Plugin::Rule2XSBody
+  loadplugin Mail::SpamAssassin::Plugin::ASN
+  loadplugin Mail::SpamAssassin::Plugin::ImageInfo
+  loadplugin Mail::SpamAssassin::Plugin::PhishTag
+  loadplugin Mail::SpamAssassin::Plugin::FreeMail
+  loadplugin Mail::SpamAssassin::Plugin::AskDNS
+  loadplugin Mail::SpamAssassin::Plugin::TxRep
+  loadplugin Mail::SpamAssassin::Plugin::URILocalBL
+  loadplugin Mail::SpamAssassin::Plugin::PDFInfo
+  loadplugin Mail::SpamAssassin::Plugin::HashBL
+  loadplugin Mail::SpamAssassin::Plugin::FromNameSpoof
+  loadplugin Mail::SpamAssassin::Plugin::Phishing
+  loadplugin Mail::SpamAssassin::Plugin::AuthRes
+  loadplugin Mail::SpamAssassin::Plugin::ExtractText
+  loadplugin Mail::SpamAssassin::Plugin::DecodeShortURLs
+  loadplugin Mail::SpamAssassin::Plugin::DMARC
 ");
 
 tstprefs("
-use_razor2 1
-use_dcc 1
-use_pyzor 1
-use_bayes 1
+  use_bayes 1
+  spf_timeout 2
+  use_razor2 1
+  razor_timeout 2
+  razor_fork 1
+  use_dcc 1
+  dcc_timeout 2
+  use_pyzor 1
+  pyzor_timeout 2
+  pyzor_fork 1
 ");
 
 %patterns = (
@@ -78,10 +76,13 @@ use_bayes 1
             );
 
 %anti_patterns = (
-        q{ Insecure dependency }, 'tainted',
-        q{ Syntax error }, 'syntax',
-        q{ Use of uninitialized }, 'uninitialized',
-        q{ warn: }, 'warn',
+        # sometimes trips on URIBL_BLOCKED, ignore..
+        # also ignore ResourceLimits/OLEVBMacro missing required modules
+        qr/ warn: (?![^\n]*(?:dns_block_rule|ResourceLimits not used|OLEVBMacro:.*required module))/, 'warn',
+        qr/Insecure dependency/i, 'tainted',
+        qr/Syntax error/i, 'syntax',
+        qr/Use of uninitialized/i, 'uninitialized',
+        qr/failed to parse/i, 'parse',
             );
 
 if (conf_bool('run_net_tests')) {
diff --git a/upstream/t/arc.t b/upstream/t/arc.t
new file mode 100755 (executable)
index 0000000..af13383
--- /dev/null
@@ -0,0 +1,34 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("arc");
+
+use Test::More;
+plan skip_all => "Net tests disabled" unless conf_bool('run_net_tests');
+plan skip_all => "Needs Mail::DKIM::ARC::Verifier >= 0.50" unless HAS_DKIM_VERIFIER ;
+plan tests => 2;
+
+tstlocalrules (q{
+  loadplugin Mail::SpamAssassin::Plugin::DKIM
+
+  full     ARC_SIGNED eval:check_arc_signed()
+  score    ARC_SIGNED 0.1
+
+  full     ARC_VALID eval:check_arc_valid()
+  score    ARC_VALID 0.1
+});
+
+
+%patterns = (
+  q{ 0.1 ARC_SIGNED }, 'ARC_SIGNED',
+);
+sarun ("-t < data/dkim/arc/ok01.eml", \&patterns_run_cb);
+ok_all_patterns();
+clear_pattern_counters();
+
+%patterns = ();
+%anti_patterns = (
+  q{ 0.1 ARC_SIGNED }, 'ARC_SIGNED',
+);
+sarun ("-t < data/dkim/arc/ko01.eml", \&patterns_run_cb);
+ok_all_patterns();
diff --git a/upstream/t/askdns.t b/upstream/t/askdns.t
new file mode 100755 (executable)
index 0000000..077813b
--- /dev/null
@@ -0,0 +1,59 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("askdns");
+use version 0.77;
+
+use constant HAS_DKIM_VERIFIER => eval {
+  require Mail::DKIM::Verifier;
+  version->parse(Mail::DKIM::Verifier->VERSION) >= version->parse(0.31);
+};
+
+use Test::More;
+plan skip_all => "Net tests disabled"          unless conf_bool('run_net_tests');
+plan skip_all => "Can't use Net::DNS Safely"   unless can_use_net_dns_safely();
+
+my $tests = 4;
+$tests += 3 if (HAS_DKIM_VERIFIER);
+
+plan tests => $tests;
+
+# ---------------------------------------------------------------------------
+
+#
+# some DKIM stuff
+#
+
+if (HAS_DKIM_VERIFIER) {
+  tstlocalrules(q{
+    full   DKIM_SIGNED           eval:check_dkim_signed()
+    askdns  ASKDNS_DKIM_AUTHORDOMAIN  _AUTHORDOMAIN_.askdnstest.spamassassin.org. A /^127\.0\.0\.8$/
+    askdns  ASKDNS_DKIM_DKIMDOMAIN  _DKIMDOMAIN_.askdnstest.spamassassin.org. A /^127\.0\.0\.8$/
+    # Bug 7897 - test that meta rules depending on net rules hit
+    meta ASKDNS_META_AUTHORDOMAIN ASKDNS_DKIM_AUTHORDOMAIN
+  });
+  %patterns = (
+    q{ ASKDNS_DKIM_AUTHORDOMAIN } => 'ASKDNS_DKIM_AUTHORDOMAIN',
+    q{ ASKDNS_DKIM_DKIMDOMAIN } => 'ASKDNS_DKIM_DKIMDOMAIN',
+    q{ ASKDNS_META_AUTHORDOMAIN } => 'ASKDNS_META_AUTHORDOMAIN',
+  );
+  ok sarun ("-t < data/dkim/test-pass-01.msg 2>&1", \&patterns_run_cb);
+  ok_all_patterns();
+  clear_pattern_counters();
+}
+
+#
+# TXT
+#
+
+tstlocalrules(q{
+  askdns  ASKDNS_TXT_SPF spamassassin.org TXT /^v=spf1 -all$/
+});
+%patterns = (
+  q{ ASKDNS_TXT_SPF } => 'ASKDNS_TXT_SPF',
+  '[spamassassin.org TXT:v=spf1 -all]' => 'ASKDNS_TXT_SPF_LOG',
+);
+ok sarun ("-t -D < data/nice/001 2>&1", \&patterns_run_cb);
+ok_all_patterns();
+clear_pattern_counters();
+
diff --git a/upstream/t/authres.t b/upstream/t/authres.t
new file mode 100755 (executable)
index 0000000..0720dbd
--- /dev/null
@@ -0,0 +1,144 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("authres");
+
+use Test::More;
+plan tests => 44;
+
+# ---------------------------------------------------------------------------
+
+tstpre ("
+loadplugin Mail::SpamAssassin::Plugin::AuthRes
+");
+
+## with internal networks
+
+tstprefs("
+  clear_internal_networks
+  clear_trusted_networks
+  internal_networks 212.17.35.15
+  trusted_networks 212.17.35.15
+  trusted_networks 141.154.95.22
+");
+
+%patterns = (
+        'parsing Authentication-Results: authrestest1int', 'hdr1',
+        'parsing Authentication-Results: authrestest2int', 'hdr2',
+        'parsing Authentication-Results: authrestest3int', 'hdr3',
+        'parsing Authentication-Results: authrestest4int', 'hdr4',
+        'parsing Authentication-Results: authrestest5int', 'hdr5',
+        'parsing Authentication-Results: authrestest6int', 'hdr6',
+        'authres: results: dkim=pass dmarc=none spf=pass', 'results',
+            );
+
+%anti_patterns = (
+        'parsing Authentication-Results: authrestest7tru', 'hdr7',
+        'parsing Authentication-Results: authrestest8ext', 'hdr8',
+        'authres: no Authentication-Results headers found', 'nohdr',
+        'authres: skipping header,', 'skipping',
+            );
+
+sarun ("-D authres -L -t < data/nice/authres 2>&1", \&patterns_run_cb);
+ok_all_patterns();
+
+
+## with trusted networks included
+
+tstprefs("
+  clear_internal_networks
+  clear_trusted_networks
+  internal_networks 212.17.35.15
+  trusted_networks 212.17.35.15
+  trusted_networks 141.154.95.22
+
+  authres_networks trusted
+");
+
+%patterns = (
+        'parsing Authentication-Results: authrestest1int', 'hdr1',
+        'parsing Authentication-Results: authrestest2int', 'hdr2',
+        'parsing Authentication-Results: authrestest3int', 'hdr3',
+        'parsing Authentication-Results: authrestest4int', 'hdr4',
+        'parsing Authentication-Results: authrestest5int', 'hdr5',
+        'parsing Authentication-Results: authrestest6int', 'hdr6',
+        'parsing Authentication-Results: authrestest7tru', 'hdr7',
+        'authres: results: dkim=pass dmarc=none spf=pass', 'results',
+            );
+
+%anti_patterns = (
+        'parsing Authentication-Results: authrestest8ext', 'hdr8',
+        'authres: no Authentication-Results headers found', 'nohdr',
+        'authres: skipping header,', 'skipping',
+            );
+
+sarun ("-D authres -L -t < data/nice/authres 2>&1", \&patterns_run_cb);
+ok_all_patterns();
+
+
+## with all networks (test ignore also)
+
+tstprefs("
+  clear_internal_networks
+  clear_trusted_networks
+  internal_networks 212.17.35.15
+  trusted_networks 212.17.35.15
+  trusted_networks 141.154.95.22
+
+  authres_networks all
+  authres_ignored_authserv authrestest3int authrestest4int
+");
+
+%patterns = (
+        'parsing Authentication-Results: authrestest1int', 'hdr1',
+        'parsing Authentication-Results: authrestest2int', 'hdr2',
+        'parsing Authentication-Results: authrestest3int', 'hdr3',
+        'parsing Authentication-Results: authrestest4int', 'hdr4',
+        'parsing Authentication-Results: authrestest5int', 'hdr5',
+        'parsing Authentication-Results: authrestest6int', 'hdr6',
+        'parsing Authentication-Results: authrestest7tru', 'hdr7',
+        'parsing Authentication-Results: authrestest8ext', 'hdr8',
+        'authres: results: dkim=pass dmarc=none spf=pass', 'results',
+        'authres: skipping header, ignored authserv: authrestest3int', 'skip3',
+        'authres: skipping header, ignored authserv: authrestest4int', 'skip4',
+            );
+
+%anti_patterns = (
+        'authres: no Authentication-Results headers found', 'nohdr',
+            );
+
+sarun ("-D authres -L -t < data/nice/authres 2>&1", \&patterns_run_cb);
+ok_all_patterns();
+
+## with all networks (test trusted also)
+
+tstprefs("
+  clear_internal_networks
+  clear_trusted_networks
+  internal_networks 212.17.35.15
+  trusted_networks 212.17.35.15
+  trusted_networks 141.154.95.22
+
+  authres_networks all
+  authres_trusted_authserv authrestest6int
+");
+
+%patterns = (
+        'dbg: authres: skipping header, authserv not trusted: authrestest1int', 'skip1',
+        'dbg: authres: skipping header, authserv not trusted: authrestest2int', 'skip2',
+        'dbg: authres: skipping header, authserv not trusted: authrestest3int', 'skip3',
+        'dbg: authres: skipping header, authserv not trusted: authrestest4int', 'skip4',
+        'dbg: authres: skipping header, authserv not trusted: authrestest5int', 'skip5',
+        'dbg: authres: skipping header, authserv not trusted: authrestest7tru', 'skip6',
+        'dbg: authres: skipping header, authserv not trusted: authrestest8ext', 'skip7',
+        'parsing Authentication-Results: authrestest6int', 'parsing',
+        'authres: results: dkim=fail', 'results',
+            );
+
+%anti_patterns = (
+        'authres: no Authentication-Results headers found', 'nohdr',
+            );
+
+sarun ("-D authres -L -t < data/nice/authres 2>&1", \&patterns_run_cb);
+ok_all_patterns();
+
index ad32559c3f6546542f7a7c4edd6c14b6f24f8816..a1d83e49e447a7c58545e0e4f4230abd6b3b19ae 100755 (executable)
@@ -13,15 +13,13 @@ plan tests => 2;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ autolearn=spam } => 'autolearned as spam'
-
+  q{ autolearn=spam } => 'autolearned as spam'
 );
 
 %anti_patterns = (
 );
 
-tstprefs ('
+tstprefs ("
 
 body   AUTOLEARNTEST_BODY      /EVOLUTION PREVIEW RELEASE/
 score  AUTOLEARNTEST_BODY      1.5
@@ -45,7 +43,8 @@ use_bayes 1
 bayes_auto_learn 1
 bayes_auto_learn_threshold_spam 6.0
 
-');
+");
 
 ok (sarun ("-L -t < data/nice/001", \&patterns_run_cb));
 ok_all_patterns();
+
index 3176c0be6f15c0df8a3091b9b7be0b520d952ac8..7e3c71337ddf776de4a5d8723672c824e244fc69 100755 (executable)
@@ -13,15 +13,13 @@ plan tests => 2;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ autolearn=spam autolearn_force=yes } => 'autolearned as spam with autolearn_force'
-
+  q{ autolearn=spam autolearn_force=yes } => 'autolearned as spam with autolearn_force'
 );
 
 %anti_patterns = (
 );
 
-tstprefs ('
+tstprefs ("
 
 body   AUTOLEARNTEST_BODY      /EVOLUTION PREVIEW RELEASE/
 score  AUTOLEARNTEST_BODY      7.0
@@ -31,7 +29,8 @@ use_bayes 1
 bayes_auto_learn 1
 bayes_auto_learn_threshold_spam 6.0
 
-');
+");
 
 ok (sarun ("-L -t < data/nice/001", \&patterns_run_cb));
 ok_all_patterns();
+
index 5712700d822a29479dab7f51170a965472c19be7..e6b612a4260deb125b1db426da145aa6db9fc8f3 100755 (executable)
@@ -13,14 +13,14 @@ plan tests => 3;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-q{ autolearn=no } => 'autolearn no',
+  q{ autolearn=no } => 'autolearn no',
 );
 
 %anti_patterns = (
-q{ autolearn=spam } => 'autolearned as spam',
+  'autolearn=spam' => 'autolearned as spam',
 );
 
-tstprefs ('
+tstprefs ("
 
 header  AUTOLEARNTEST_FROM_HEADER      From =~ /@/
 score   AUTOLEARNTEST_FROM_HEADER      13.0
@@ -30,7 +30,7 @@ use_bayes 1
 bayes_auto_learn 1
 bayes_auto_learn_threshold_spam 12.0
 
-');
+");
 
 ok (sarun ("-L -t < data/nice/001", \&patterns_run_cb));
 ok_all_patterns();
index 5ee2a053c06bf354e46b5904b6aab2530deb7755..143dfb7d25e0aaca8e41d821aa88b0c9d2d47c3b 100755 (executable)
@@ -2,18 +2,33 @@
 
 use lib '.'; use lib 't';
 use SATest; sa_t_init("basic_lint");
-use Test::More tests => 1;
 
-# ---------------------------------------------------------------------------
+use Test::More;
 
-%patterns = (
+@test_locales = qw(C);
+
+if (!$RUNNING_ON_WINDOWS) {
+  # Test with few random additional locales if available
+  my $locales = untaint_cmd("locale -a");
+  while ($locales =~ /^((?:C|en_US|fr_FR|zh_CN)\.(?:utf|iso|gb).*)$/gmi) {
+    push @test_locales, $1;
+  }
+}
 
-q{  }, 'anything',
+plan tests => scalar(@test_locales);
 
+# ---------------------------------------------------------------------------
+
+%patterns = (
+  qr/^/, 'anything',
 );
 
-# override locale for this test!
-$ENV{'LANGUAGE'} = $ENV{'LC_ALL'} = 'C';
+foreach my $locale (@test_locales) {
+  my $language = $locale;
+  $language =~ s/[._].*//;
+  $ENV{'LANGUAGE'} = $language;
+  $ENV{'LC_ALL'} = $locale;
+  sarun ("-L --lint", \&patterns_run_cb);
+  ok_all_patterns();
+}
 
-sarun ("-L --lint", \&patterns_run_cb);
-ok_all_patterns();
diff --git a/upstream/t/basic_lint_net.t b/upstream/t/basic_lint_net.t
new file mode 100755 (executable)
index 0000000..7ec2b9f
--- /dev/null
@@ -0,0 +1,25 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("basic_lint_net");
+use Test::More;
+
+plan skip_all => "Net tests disabled" unless conf_bool('run_net_tests');
+
+plan tests => 2;
+
+# ---------------------------------------------------------------------------
+
+%patterns = (
+  qr/^/, 'anything',
+);
+%anti_patterns = (
+  q{ warn: }, 'warning',
+);
+
+# override locale for this test!
+$ENV{'LANGUAGE'} = $ENV{'LC_ALL'} = 'C';
+
+sarun ("--lint --net", \&patterns_run_cb);
+ok_all_patterns();
+
index 6ca3b242bb21935d67e8753de4349a3083b7fcbb..63c7e39668a34de28b7255ca645e4c5bb3c9bc59 100755 (executable)
@@ -9,16 +9,14 @@ use Test::More tests => 3;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{  }, 'anything',
-
+  qr/^/, 'anything',
 );
 
 # override locale for this test!
 $ENV{'LANGUAGE'} = $ENV{'LC_ALL'} = 'C';
 
-my $scoresfile  = "log/test_rules_copy/50_scores.cf";
-my $sandboxfile = "log/test_rules_copy/70_sandbox.cf";
+my $scoresfile  = "$localrules/50_scores.cf";
+my $sandboxfile = "$localrules/70_sandbox.cf";
 
 # when running from the built tarball or make disttest, we will not have a full
 # rules dir -- therefore no 70_sandbox.cf.  We will also have no 50_scores.cf,
@@ -32,3 +30,4 @@ SKIP: {
 
 sarun ("-L --lint", \&patterns_run_cb);
 ok_all_patterns();
+
index d9f2923b8a54874e7aba0465f6f58e5852efe6ea..0a46e89ded83ef398d3e6702fd597bf605f5d805 100755 (executable)
@@ -1,23 +1,8 @@
 #!/usr/bin/perl -w -T
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_names.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
-
 use strict;
 use lib '.'; use lib 't';
-use SATest; sa_t_init("meta");
+use SATest; sa_t_init("basic_meta");
 
 use Mail::SpamAssassin;
 
@@ -37,18 +22,18 @@ my $meta_dependency_disabled = 0;
 my $meta_dependency_nonexistent = 0;
 
 for (my $scoreset = 0; $scoreset < 4; $scoreset++) {
-  my $output = "log/rules-$scoreset.pl";
+  my $output = "$workdir/rules-$scoreset.pl";
   unlink $output || die;
   %rules = ();
   %scores = ();
-  if (untaint_system("$perl_path $prefix/build/parse-rules-for-masses -o $output -d \"$prefix/rules\" -s $scoreset -x")) {
+  if (untaint_system("$perl_path ../build/parse-rules-for-masses -o $output -d \"../rules\" -s $scoreset -x")) {
     warn "parse-rules-for-masses failed!";
   }
   eval {
-    require "log/rules-$scoreset.pl";
+    require "$workdir/rules-$scoreset.pl";
   };
   if ($@) {
-    warn "log/rules-$scoreset.pl is unparseable: $@";
+    warn "$workdir/rules-$scoreset.pl is unparseable: $@";
     warn "giving up on test.";
     ok(1);
     ok(1);
old mode 100644 (file)
new mode 100755 (executable)
index 0171085..1433c99
@@ -2,30 +2,47 @@
 
 use lib '.'; 
 use lib 't';
-use SATest; 
-sa_t_init("meta2");
+use SATest; sa_t_init("basic_meta2");
 
 use Test::More;
-plan tests => 5;
+
+# run many times to catch some random natured failures
+my $iterations = 5;
+plan tests => 24 * $iterations;
 
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-  q{ TEST_FOO_1 }     => '',
-  q{ TEST_FOO_2 }     => '',
-  q{ TEST_FOO_3 }     => '',
-  q{ TEST_META_1 }     => '',
-
+  q{ 1.0 TEST_FOO_1 }     => '',
+  q{ 1.0 TEST_FOO_2 }     => '',
+  q{ 1.0 TEST_FOO_3 }     => '',
+  q{ 1.0 TEST_META_1 }    => '',
+  q{ 1.0 TEST_META_2 }    => '',
+  q{ 1.0 TEST_META_3 }    => '',
+  q{ 1.0 TEST_META_4 }    => '',
+  q{ 1.0 TEST_META_5 }    => '',
+  q{ 1.0 TEST_META_6 }    => '',
+  q{ 1.0 TEST_META_7 }    => '',
+  q{ 1.0 TEST_META_9 }    => '',
+  q{ 1.0 TEST_META_A }    => '',
+  q{ 1.0 TEST_META_B }    => '',
+  q{ 1.0 TEST_META_C }    => '',
+  q{ 1.0 TEST_META_D }    => '',
+  q{ 1.0 TEST_META_E }    => '',
+  q{ 1.0 TEST_META_F }    => '',
+  q{ 1.0 TEST_META_G }    => '',
+  q{ 1.0 TEST_META_H }    => '',
+  q{ 1.0 TEST_META_I }    => '',
+  q{ 1.0 TEST_META_J }    => '',
+  q{ 1.0 TEST_META_K }    => '',
 );
 
 %anti_patterns = (
-
   q{ TEST_NEG_1 }     => '',
-
+  q{ TEST_META_8 }    => '',
 );
 
-tstprefs (qq{
+tstlocalrules (qq{
 
    body __FOO_1 /a/
    body __FOO_2 /b/
@@ -40,8 +57,75 @@ tstprefs (qq{
 
    meta TEST_META_1 (TEST_FOO_1 + TEST_FOO_2 + TEST_NEG_1) == 2
 
+   ##
+   ## Unrun rule dependencies (Bug 7735)
+   ##
+
+   # Non-existing rule, should hit as !0
+   meta TEST_META_2 !NONEXISTINGRULE
+   # Should hit as !0 || 0
+   meta TEST_META_3 !NONEXISTINGRULE || NONEXISTINGRULE
+
+   # Disabled rule, same as above
+   body TEST_DISABLED /a/
+   score TEST_DISABLED 0
+   # Should hit as !0
+   meta TEST_META_4 !TEST_DISABLED
+   # Should hit as !0 || 0
+   meta TEST_META_5 !TEST_DISABLED || TEST_DISABLED
+
+   # Unrun rule (due to local tests only), same as above
+   askdns TEST_DISABLED2 spamassassin.org TXT /./
+   # Should hit as !0
+   meta TEST_META_6 !TEST_DISABLED2
+   # Should hit as !0 || 0
+   meta TEST_META_7 !TEST_DISABLED2 || TEST_DISABLED2
+
+   # Other way of "disabling" a rule, with meta 0.
+   meta TEST_DISABLED3 0
+   # Should hit
+   meta TEST_META_I !TEST_DISABLED3
+   # Should hit
+   meta TEST_META_J !TEST_DISABLED3 && __FOO_1
+
+   # Should not hit
+   meta TEST_META_8 __FOO_1 + NONEXISTINGRULE == 2
+   # Should hit as 1 + 0 + 1 == 2
+   meta TEST_META_9 __FOO_1 + NONEXISTINGRULE + __FOO_2 == 2
+   # Should hit as above
+   meta TEST_META_A __FOO_1 + NONEXISTINGRULE + __FOO_2 > 1
+
+   # local_tests_only
+   meta TEST_META_B NONEXISTINGRULE || local_tests_only
+
+   # complex metas with different priorities
+   body __BAR_5 /a/
+   priority __BAR_5 -1000
+   body __BAR_6 /b/
+   priority __BAR_6 0
+   body __BAR_7 /c/
+   priority __BAR_7 1000
+   meta TEST_META_C __BAR_5 && __BAR_6 && __BAR_7
+   meta TEST_META_D __BAR_5 && __BAR_6 && TEST_META_C
+   priority TEST_META_D -2000
+   meta TEST_META_E __BAR_6 && __BAR_7 && TEST_META_D
+   meta TEST_META_F __BAR_5 && __BAR_7 && TEST_META_E
+   priority TEST_META_F 2000
+   meta TEST_META_G TEST_META_C && TEST_META_D && TEST_META_E && TEST_META_F
+
+   # metas without dependencies
+   meta __TEST_META_H1  6
+   meta __TEST_META_H2  2
+   meta __TEST_META_H3  1
+   meta TEST_META_H   (__TEST_META_H1 > 2) && (__TEST_META_H2 > 1) && __TEST_META_H3
+
+   # bug 7735, comment 87
+   meta __TEST_META_K  (1 || TEST_DISABLED || TEST_DISABLED2 || TEST_DISABLED3)
+   meta TEST_META_K  __TEST_META_K
 });
 
-sarun ("-L -t < data/nice/001 2>&1", \&patterns_run_cb);
-ok_all_patterns();
+for (1 .. $iterations) {
+  sarun ("-L -t < data/nice/001 2>&1", \&patterns_run_cb);
+  ok_all_patterns();
+}
 
index 81d3b970220c0d63d6ee31388b09b02d5f9cdb4b..8742f3db44b0f9cca194b5e7035a3460e19e202f 100755 (executable)
@@ -1,21 +1,5 @@
 #!/usr/bin/perl -T
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_tests.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-    unshift(@INC, '../lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
-
 use lib '.'; use lib 't';
 use SATest; sa_t_init("basic_obj_api");
 use Test::More tests => 4;
index a9812f69cbf9ae91012e5f58fe1e2b3a61ae9818..1f2f9556bd841df2a7de83bf0211710a5e378280 100755 (executable)
@@ -1,21 +1,12 @@
 #!/usr/bin/perl -T
 
-use Data::Dumper;
+use File::Find qw(find);
 use lib '.'; use lib 't';
-use SATest; sa_t_init("bayes");
+use SATest; sa_t_init("bayesbdb");
 
 use constant HAS_BDB => eval { require BerkeleyDB };
 
 use Test::More;
-BEGIN { 
-  if (-e 't/test_dir') {
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {
-    unshift(@INC, '../blib/lib');
-  }
-}
 
 plan skip_all => "Long running tests disabled" unless conf_bool('run_long_tests');
 plan skip_all => "BerkeleyDB is unavailable" unless HAS_BDB;
@@ -25,11 +16,11 @@ plan skip_all => "BerkeleyDB is unavailable" unless HAS_BDB;
   plan skip_all => "BerkeleyDB >= 4.6 is required" unless $BerkeleyDB::db_version >= 4.6;
 }
 
-plan tests => 42;
+plan tests => 48;
 
 
-tstlocalrules ("
-        bayes_store_module Mail::SpamAssassin::BayesStore::BDB
+tstprefs ("
+  bayes_store_module Mail::SpamAssassin::BayesStore::BDB
 ");
 
 use Mail::SpamAssassin;
@@ -69,11 +60,12 @@ my $toks = getimpl->tokenize($mail, $body);
 
 ok(scalar(keys %{$toks}) > 0);
 
-my($msgid,$msgid_hdr) = getimpl->get_msgid($mail);
+my $msgid = $mail->generate_msgid();
+my $msgid_hdr = $mail->get_msgid();
 
 # $msgid is the generated hash messageid
 # $msgid_hdr is the Message-Id header
-ok($msgid eq '4cf5cc4d53b22e94d3e55932a606b18641a54041@sa_generated')
+ok($msgid eq '71f849915d7e469ddc1890cd8175f6876843f99e@sa_generated')
     or warn "got: [$msgid]";
 ok($msgid_hdr eq '9PS291LhupY');
 
@@ -148,28 +140,45 @@ ok(!getimpl->{store}->seen_get($msgid));
 
 getimpl->{store}->untie_db();
 
+getimpl->{store}->_close_db(); # on Windows the following sa_t_init can't delete the old files without this close
+
 undef $sa;
 
-sa_t_init('bayes'); # this wipes out what is there and begins anew
+sa_t_init('bayesbdb'); # this wipes out what is there and begins anew
 
 # make sure we learn to a journal
-tstlocalrules ("
-bayes_store_module Mail::SpamAssassin::BayesStore::BDB
-bayes_min_spam_num 10
-bayes_min_ham_num 10
+tstprefs ("
+  bayes_store_module Mail::SpamAssassin::BayesStore::BDB
+  bayes_min_spam_num 10
+  bayes_min_ham_num 10
 ");
 
 # we get to bastardize the existing pattern matching code here.  It lets us provide
 # our own checking callback and keep using the existing ok_all_patterns call
 %patterns = ( 1 => 'Acted on message' );
 
+$wanted_examined = count_files("data/spam");
 ok(salearnrun("--spam data/spam", \&check_examined));
 ok_all_patterns();
 
+$wanted_examined = count_files("data/nice");
 ok(salearnrun("--ham data/nice", \&check_examined));
 ok_all_patterns();
 
-ok(salearnrun("--ham data/whitelists", \&check_examined));
+$wanted_examined = count_files("data/welcomelists");
+ok(salearnrun("--ham data/welcomelists", \&check_examined));
+ok_all_patterns();
+
+$wanted_examined = 3;
+ok(salearnrun("--ham --mbox data/nice.mbox", \&check_examined));
+ok_all_patterns();
+
+$wanted_examined = 3;
+ok(salearnrun("--ham --mbox < data/nice.mbox", \&check_examined));
+ok_all_patterns();
+
+$wanted_examined = 3;
+ok(salearnrun("--forget --mbox data/nice.mbox", \&check_examined));
 ok_all_patterns();
 
 %patterns = ( 'non-token data: bayes db version' => 'db version' );
@@ -237,9 +246,9 @@ ok($score =~ /\d/ && $score <= 1.0 && $score != .5);
 
 ok(getimpl->{store}->clear_database());
 
-ok(!-e 'log/user_state/bayes/vars.db');
-ok(!-e 'log/user_state/bayes/seen.db');
-ok(!-e 'log/user_state/bayes/toks.db');
+ok(!-e "$userstate/bayes/vars.db");
+ok(!-e "$userstate/bayes/seen.db");
+ok(!-e "$userstate/bayes/toks.db");
 
 sub check_examined {
   local ($_);
@@ -251,7 +260,17 @@ sub check_examined {
     $_ = join ('', <IN>);
   }
 
-  if ($_ =~ /(?:Forgot|Learned) tokens from \d+ message\(s\) \(\d+ message\(s\) examined\)/) {
-    $found{'Acted on message'}++;
+  if ($_ =~ /(?:Forgot|Learned) tokens from \d+ message\(s\) \((\d+) message\(s\) examined\)/) {
+    #print STDERR "examined $1 messages\n";
+    if (defined $wanted_examined && $wanted_examined == $1) {
+      $found{'Acted on message'}++;
+    }
   }
 }
+
+sub count_files {
+  my $cnt = 0;
+  find({wanted => sub { $cnt++ if -f $_; }, no_chdir => 1}, $_[0]);
+  return $cnt;
+}
+
index 645d78d4269309a54945b97b3e025cb906eb3797..51cef29fc0b4a822a48ec874dd06d75976604870 100755 (executable)
@@ -1,28 +1,19 @@
 #!/usr/bin/perl -T
 
-use Data::Dumper;
+use File::Find qw(find);
 use lib '.'; use lib 't';
-use SATest; sa_t_init("bayes");
+use SATest; sa_t_init("bayesdbm");
 
 use constant HAS_DB_FILE => eval { require DB_File };
  
 use Test::More;
-BEGIN { 
-  if (-e 't/test_dir') {
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {
-    unshift(@INC, '../blib/lib');
-  }
-}
 
 plan skip_all => "Long running tests disabled" unless conf_bool('run_long_tests');
 plan skip_all => "DB_File is unavailable" unless HAS_DB_FILE;
-plan tests => 48;
+plan tests => 54;
 
-tstlocalrules ("
-        bayes_learn_to_journal 0
+tstprefs ("
+  bayes_learn_to_journal 0
 ");
 
 use Mail::SpamAssassin;
@@ -62,11 +53,12 @@ my $toks = getimpl->tokenize($mail, $body);
 
 ok(scalar(keys %{$toks}) > 0);
 
-my($msgid,$msgid_hdr) = getimpl->get_msgid($mail);
+my $msgid = $mail->generate_msgid();
+my $msgid_hdr = $mail->get_msgid();
 
 # $msgid is the generated hash messageid
 # $msgid_hdr is the Message-Id header
-ok($msgid eq '4cf5cc4d53b22e94d3e55932a606b18641a54041@sa_generated')
+ok($msgid eq '71f849915d7e469ddc1890cd8175f6876843f99e@sa_generated')
     or warn "got: [$msgid]";
 ok($msgid_hdr eq '9PS291LhupY');
 
@@ -143,53 +135,68 @@ getimpl->{store}->untie_db();
 
 undef $sa;
 
-sa_t_init('bayes'); # this wipes out what is there and begins anew
+sa_t_init('bayesdbm'); # this wipes out what is there and begins anew
 
 # make sure we learn to a journal
-tstlocalrules ("
-        bayes_learn_to_journal 1
+tstprefs ("
+  bayes_learn_to_journal 1
 ");
 
 $sa = create_saobj();
 
 $sa->init();
 
-ok(!-e 'log/user_state/bayes_journal');
+ok(!-e "$userstate/bayes_journal");
 
 ok($sa->{bayes_scanner}->learn(1, $mail));
 
-ok(-e 'log/user_state/bayes_journal');
+ok(-e "$userstate/bayes_journal");
 
 $sa->{bayes_scanner}->sync(1); # always returns 0, so no need to check return
 
-ok(!-e 'log/user_state/bayes_journal');
+ok(!-e "$userstate/bayes_journal");
 
-ok(-e 'log/user_state/bayes_seen');
+ok(-e "$userstate/bayes_seen");
 
-ok(-e 'log/user_state/bayes_toks');
+ok(-e "$userstate/bayes_toks");
 
 undef $sa;
 
-sa_t_init('bayes'); # this wipes out what is there and begins anew
+sa_t_init('bayesdbm'); # this wipes out what is there and begins anew
 
 # make sure we learn to a journal
-tstlocalrules ("
-bayes_learn_to_journal 0
-bayes_min_spam_num 10
-bayes_min_ham_num 10
+tstprefs ("
+  bayes_learn_to_journal 0
+  bayes_min_spam_num 10
+  bayes_min_ham_num 10
 ");
 
 # we get to bastardize the existing pattern matching code here.  It lets us provide
 # our own checking callback and keep using the existing ok_all_patterns call
 %patterns = ( 1 => 'Acted on message' );
 
+$wanted_examined = count_files("data/spam");
 ok(salearnrun("--spam data/spam", \&check_examined));
 ok_all_patterns();
 
+$wanted_examined = count_files("data/nice");
 ok(salearnrun("--ham data/nice", \&check_examined));
 ok_all_patterns();
 
-ok(salearnrun("--ham data/whitelists", \&check_examined));
+$wanted_examined = count_files("data/welcomelists");
+ok(salearnrun("--ham data/welcomelists", \&check_examined));
+ok_all_patterns();
+
+$wanted_examined = 3;
+ok(salearnrun("--ham --mbox data/nice.mbox", \&check_examined));
+ok_all_patterns();
+
+$wanted_examined = 3;
+ok(salearnrun("--ham --mbox < data/nice.mbox", \&check_examined));
+ok_all_patterns();
+
+$wanted_examined = 3;
+ok(salearnrun("--forget --mbox data/nice.mbox", \&check_examined));
 ok_all_patterns();
 
 %patterns = ( 'non-token data: bayes db version' => 'db version' );
@@ -257,9 +264,9 @@ ok($score =~ /\d/ && $score <= 1.0 && $score != .5);
 
 ok(getimpl->{store}->clear_database());
 
-ok(!-e 'log/user_state/bayes_journal');
-ok(!-e 'log/user_state/bayes_seen');
-ok(!-e 'log/user_state/bayes_toks');
+ok(!-e "$userstate/bayes_journal");
+ok(!-e "$userstate/bayes_seen");
+ok(!-e "$userstate/bayes_toks");
 
 sub check_examined {
   local ($_);
@@ -271,9 +278,17 @@ sub check_examined {
     $_ = join ('', <IN>);
   }
 
-  if ($_ =~ /(?:Forgot|Learned) tokens from \d+ message\(s\) \(\d+ message\(s\) examined\)/) {
-    $found{'Acted on message'}++;
+  if ($_ =~ /(?:Forgot|Learned) tokens from \d+ message\(s\) \((\d+) message\(s\) examined\)/) {
+    #print STDERR "examined $1 messages\n";
+    if (defined $wanted_examined && $wanted_examined == $1) {
+      $found{'Acted on message'}++;
+    }
   }
 }
 
+sub count_files {
+  my $cnt = 0;
+  find({wanted => sub { $cnt++ if -f $_; }, no_chdir => 1}, $_[0]);
+  return $cnt;
+}
 
index d105a8ac9aeefc1c5c8ef0a7bdeda3a75a023f5a..e8df0481ca6689da9c9d978c76ba5c4be4936c09 100755 (executable)
@@ -7,24 +7,15 @@ use SATest; sa_t_init("bayesdbm_flock");
 use constant HAS_DB_FILE => eval { require DB_File };
 
 use Test::More;
-BEGIN { 
-  if (-e 't/test_dir') {
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {
-    unshift(@INC, '../blib/lib');
-  }
-}
 
 plan skip_all => "Long running tests disabled" unless conf_bool('run_long_tests');
 plan skip_all => "Tests don't work on windows" if $RUNNING_ON_WINDOWS;
 plan skip_all => "DB_File is unavailable" unless HAS_DB_FILE;
 plan tests => 48;
 
-tstlocalrules ("
-        bayes_learn_to_journal 0
-        lock_method flock
+tstprefs ("
+  bayes_learn_to_journal 0
+  lock_method flock
 ");
 
 use Mail::SpamAssassin;
@@ -65,11 +56,12 @@ my $toks = getimpl->tokenize($mail, $body);
 
 ok(scalar(keys %{$toks}) > 0);
 
-my($msgid,$msgid_hdr) = getimpl->get_msgid($mail);
+my $msgid = $mail->generate_msgid();
+my $msgid_hdr = $mail->get_msgid();
 
 # $msgid is the generated hash messageid
 # $msgid_hdr is the Message-Id header
-ok($msgid eq '4cf5cc4d53b22e94d3e55932a606b18641a54041@sa_generated');
+ok($msgid eq '71f849915d7e469ddc1890cd8175f6876843f99e@sa_generated');
 ok($msgid_hdr eq '9PS291LhupY');
 
 ok(getimpl->{store}->tie_db_writable());
@@ -151,42 +143,42 @@ alarm(0);
 
 undef $sa;
 
-sa_t_init('bayes'); # this wipes out what is there and begins anew
+sa_t_init('bayesdbm_flock'); # this wipes out what is there and begins anew
 
 # make sure we learn to a journal
-tstlocalrules ("
-        bayes_learn_to_journal 1
+tstprefs ("
+  bayes_learn_to_journal 1
 ");
 
 $sa = create_saobj();
 
 $sa->init();
 
-ok(!-e 'log/user_state/bayes_journal');
+ok(!-e "$userstate/bayes_journal");
 
 ok($sa->{bayes_scanner}->learn(1, $mail));
 
-ok(-e 'log/user_state/bayes_journal');
+ok(-e "$userstate/bayes_journal");
 
 $sa->{bayes_scanner}->sync(1); # always returns 0, so no need to check return
 
-ok(!-e 'log/user_state/bayes_journal');
+ok(!-e "$userstate/bayes_journal");
 
-ok(-e 'log/user_state/bayes_seen');
+ok(-e "$userstate/bayes_seen");
 
-ok(-e 'log/user_state/bayes_toks');
+ok(-e "$userstate/bayes_toks");
 
 undef $sa;
 
-sa_t_init('bayes'); # this wipes out what is there and begins anew
+sa_t_init('bayesdbm_flock'); # this wipes out what is there and begins anew
 
 alarm(0);  # cancel timer - make sure that alarm is off
 
 # make sure we learn to a journal
-tstlocalrules ("
-bayes_learn_to_journal 0
-bayes_min_spam_num 10
-bayes_min_ham_num 10
+tstprefs ("
+  bayes_learn_to_journal 0
+  bayes_min_spam_num 10
+  bayes_min_ham_num 10
 ");
 
 # we get to bastardize the existing pattern matching code here.  It lets us provide
@@ -199,7 +191,7 @@ ok_all_patterns();
 ok(salearnrun("--ham data/nice", \&check_examined));
 ok_all_patterns();
 
-ok(salearnrun("--ham data/whitelists", \&check_examined));
+ok(salearnrun("--ham data/welcomelists", \&check_examined));
 ok_all_patterns();
 
 %patterns = ( 'non-token data: bayes db version' => 'db version' );
@@ -267,9 +259,9 @@ ok($score =~ /\d/ && $score <= 1.0 && $score != .5);
 
 ok(getimpl->{store}->clear_database());
 
-ok(!-e 'log/user_state/bayes_journal');
-ok(!-e 'log/user_state/bayes_seen');
-ok(!-e 'log/user_state/bayes_toks');
+ok(!-e "$userstate/bayes_journal");
+ok(!-e "$userstate/bayes_seen");
+ok(!-e "$userstate/bayes_toks");
 
 sub check_examined {
   local ($_);
index 5c6fba6074f0e1dcdf54d864bb41f78754816b34..1eefa4c4084843947503fc88ea4211582c366352 100755 (executable)
@@ -2,28 +2,19 @@
 
 use Data::Dumper;
 use lib '.'; use lib 't';
-use SATest; sa_t_init("bayes");
+use SATest; sa_t_init("bayessdbm");
 
 use constant HAS_SDBM_FILE => eval { require SDBM_File };
 
 use Test::More;
-BEGIN {
-  if (-e 't/test_dir') {
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {
-    unshift(@INC, '../blib/lib');
-  }
-}
 
 plan skip_all => "Long running tests disabled" unless conf_bool('run_long_tests');
 plan skip_all => "No SDBM_File" unless HAS_SDBM_FILE;
 plan tests => 52;
 
-tstlocalrules ("
-        bayes_store_module Mail::SpamAssassin::BayesStore::SDBM
-        bayes_learn_to_journal 0
+tstprefs ("
+  bayes_store_module Mail::SpamAssassin::BayesStore::SDBM
+  bayes_learn_to_journal 0
 ");
 
 use Mail::SpamAssassin;
@@ -64,11 +55,12 @@ my $toks = getimpl->tokenize($mail, $body);
 
 ok(scalar(keys %{$toks}) > 0);
 
-my($msgid,$msgid_hdr) = getimpl->get_msgid($mail);
+my $msgid = $mail->generate_msgid();
+my $msgid_hdr = $mail->get_msgid();
 
 # $msgid is the generated hash messageid
 # $msgid_hdr is the Message-Id header
-ok($msgid eq '4cf5cc4d53b22e94d3e55932a606b18641a54041@sa_generated');
+ok($msgid eq '71f849915d7e469ddc1890cd8175f6876843f99e@sa_generated');
 ok($msgid_hdr eq '9PS291LhupY');
 
 ok(getimpl->{store}->tie_db_writable());
@@ -144,44 +136,44 @@ getimpl->{store}->untie_db();
 
 undef $sa;
 
-sa_t_init('bayes'); # this wipes out what is there and begins anew
+sa_t_init('bayessdbm'); # this wipes out what is there and begins anew
 
 # make sure we learn to a journal
-tstlocalrules ("
-        bayes_store_module Mail::SpamAssassin::BayesStore::SDBM
-        bayes_learn_to_journal 1
+tstprefs ("
+  bayes_store_module Mail::SpamAssassin::BayesStore::SDBM
+  bayes_learn_to_journal 1
 ");
 
 $sa = create_saobj();
 
 $sa->init();
 
-ok(!-e 'log/user_state/bayes_journal');
+ok(!-e "$userstate/bayes_journal");
 
 ok($sa->{bayes_scanner}->learn(1, $mail));
 
-ok(-e 'log/user_state/bayes_journal');
+ok(-e "$userstate/bayes_journal");
 
 $sa->{bayes_scanner}->sync(1); # always returns 0, so no need to check return
 
-ok(!-e 'log/user_state/bayes_journal');
+ok(!-e "$userstate/bayes_journal");
 
-ok(-e 'log/user_state/bayes_seen.pag');
-ok(-e 'log/user_state/bayes_seen.dir');
+ok(-e "$userstate/bayes_seen.pag");
+ok(-e "$userstate/bayes_seen.dir");
 
-ok(-e 'log/user_state/bayes_toks.pag');
-ok(-e 'log/user_state/bayes_toks.dir');
+ok(-e "$userstate/bayes_toks.pag");
+ok(-e "$userstate/bayes_toks.dir");
 
 undef $sa;
 
-sa_t_init('bayes'); # this wipes out what is there and begins anew
+sa_t_init('bayessdbm'); # this wipes out what is there and begins anew
 
 # make sure we learn to a journal
-tstlocalrules ("
-        bayes_store_module Mail::SpamAssassin::BayesStore::SDBM
-bayes_learn_to_journal 0
-bayes_min_spam_num 10
-bayes_min_ham_num 10
+tstprefs ("
+  bayes_store_module Mail::SpamAssassin::BayesStore::SDBM
+  bayes_learn_to_journal 0
+  bayes_min_spam_num 10
+  bayes_min_ham_num 10
 ");
 
 # we get to bastardize the existing pattern matching code here.  It lets us provide
@@ -194,7 +186,7 @@ ok_all_patterns();
 ok(salearnrun("--ham data/nice", \&check_examined));
 ok_all_patterns();
 
-ok(salearnrun("--ham data/whitelists", \&check_examined));
+ok(salearnrun("--ham data/welcomelists", \&check_examined));
 ok_all_patterns();
 
 %patterns = ( 'non-token data: bayes db version' => 'db version' );
@@ -262,11 +254,11 @@ ok($score =~ /\d/ && $score <= 1.0 && $score != .5);
 
 ok(getimpl->{store}->clear_database());
 
-ok(!-e 'log/user_state/bayes_journal');
-ok(!-e 'log/user_state/bayes_seen.pag');
-ok(!-e 'log/user_state/bayes_seen.dir');
-ok(!-e 'log/user_state/bayes_toks.pag');
-ok(!-e 'log/user_state/bayes_toks.dir');
+ok(!-e "$userstate/bayes_journal");
+ok(!-e "$userstate/bayes_seen.pag");
+ok(!-e "$userstate/bayes_seen.dir");
+ok(!-e "$userstate/bayes_toks.pag");
+ok(!-e "$userstate/bayes_toks.dir");
 
 sub check_examined {
   local ($_);
index 20da115a001d1df91053bb1a4de0f097cf70d524..7a2f0ff32e3e2cea5a37a90822e89bc52060f985 100755 (executable)
@@ -11,19 +11,9 @@ plan skip_all => "Long running tests disabled" unless conf_bool('run_long_tests'
 plan skip_all => "No SDBM_File" unless HAS_SDBM_FILE;
 plan tests => 54;
 
-BEGIN {
-  if (-e 't/test_dir') {
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {
-    unshift(@INC, '../blib/lib');
-  }
-}
-
-tstlocalrules ("
-        bayes_store_module Mail::SpamAssassin::BayesStore::SDBM
-        bayes_learn_to_journal 0
+tstprefs ("
+  bayes_store_module Mail::SpamAssassin::BayesStore::SDBM
+  bayes_learn_to_journal 0
 ");
 
 use Mail::SpamAssassin;
@@ -64,11 +54,12 @@ my $toks = getimpl->tokenize($mail, $body);
 
 ok(scalar(keys %{$toks}) > 0);
 
-my($msgid,$msgid_hdr) = getimpl->get_msgid($mail);
+my $msgid = $mail->generate_msgid();
+my $msgid_hdr = $mail->get_msgid();
 
 # $msgid is the generated hash messageid
 # $msgid_hdr is the Message-Id header
-ok($msgid eq '4cf5cc4d53b22e94d3e55932a606b18641a54041@sa_generated');
+ok($msgid eq '71f849915d7e469ddc1890cd8175f6876843f99e@sa_generated');
 ok($msgid_hdr eq '9PS291LhupY');
 
 ok(getimpl->{store}->tie_db_writable());
@@ -144,44 +135,44 @@ getimpl->{store}->untie_db();
 
 undef $sa;
 
-sa_t_init('bayes'); # this wipes out what is there and begins anew
+sa_t_init('bayessdbm_seen_delete'); # this wipes out what is there and begins anew
 
 # make sure we learn to a journal
-tstlocalrules ("
-        bayes_store_module Mail::SpamAssassin::BayesStore::SDBM
-        bayes_learn_to_journal 1
+tstprefs ("
+  bayes_store_module Mail::SpamAssassin::BayesStore::SDBM
+  bayes_learn_to_journal 1
 ");
 
 $sa = create_saobj();
 
 $sa->init();
 
-ok(!-e 'log/user_state/bayes_journal');
+ok(!-e "$userstate/bayes_journal");
 
 ok($sa->{bayes_scanner}->learn(1, $mail));
 
-ok(-e 'log/user_state/bayes_journal');
+ok(-e "$userstate/bayes_journal");
 
 $sa->{bayes_scanner}->sync(1); # always returns 0, so no need to check return
 
-ok(!-e 'log/user_state/bayes_journal');
+ok(!-e "$userstate/bayes_journal");
 
-ok(-e 'log/user_state/bayes_seen.pag');
-ok(-e 'log/user_state/bayes_seen.dir');
+ok(-e "$userstate/bayes_seen.pag");
+ok(-e "$userstate/bayes_seen.dir");
 
-ok(-e 'log/user_state/bayes_toks.pag');
-ok(-e 'log/user_state/bayes_toks.dir');
+ok(-e "$userstate/bayes_toks.pag");
+ok(-e "$userstate/bayes_toks.dir");
 
 undef $sa;
 
-sa_t_init('bayes'); # this wipes out what is there and begins anew
+sa_t_init('bayessdbm_seen_delete'); # this wipes out what is there and begins anew
 
 # make sure we learn to a journal
-tstlocalrules ("
-        bayes_store_module Mail::SpamAssassin::BayesStore::SDBM
-bayes_learn_to_journal 0
-bayes_min_spam_num 10
-bayes_min_ham_num 10
+tstprefs ("
+  bayes_store_module Mail::SpamAssassin::BayesStore::SDBM
+  bayes_learn_to_journal 0
+  bayes_min_spam_num 10
+  bayes_min_ham_num 10
 ");
 
 # we get to bastardize the existing pattern matching code here.  It lets us provide
@@ -194,7 +185,7 @@ ok_all_patterns();
 ok(salearnrun("--ham data/nice", \&check_examined));
 ok_all_patterns();
 
-ok(salearnrun("--ham data/whitelists", \&check_examined));
+ok(salearnrun("--ham data/welcomelists", \&check_examined));
 ok_all_patterns();
 
 %patterns = ( 'non-token data: bayes db version' => 'db version' );
@@ -203,9 +194,9 @@ ok_all_patterns();
 
 # now delete the journal and bayes_seen -- should still be possible
 # for Bayes to continue...
-unlink 'log/user_state/bayes_journal';
-ok(unlink 'log/user_state/bayes_seen.pag');
-ok(unlink 'log/user_state/bayes_seen.dir');
+unlink "$userstate/bayes_journal";
+ok(unlink "$userstate/bayes_seen.pag");
+ok(unlink "$userstate/bayes_seen.dir");
 
 use constant SCAN_USING_PERL_CODE_TEST => 1;
 
@@ -266,11 +257,11 @@ ok($score =~ /\d/ && $score <= 1.0 && $score != .5);
 
 ok(getimpl->{store}->clear_database());
 
-ok(!-e 'log/user_state/bayes_journal');
-ok(!-e 'log/user_state/bayes_seen.pag');
-ok(!-e 'log/user_state/bayes_seen.dir');
-ok(!-e 'log/user_state/bayes_toks.pag');
-ok(!-e 'log/user_state/bayes_toks.dir');
+ok(!-e "$userstate/bayes_journal");
+ok(!-e "$userstate/bayes_seen.pag");
+ok(!-e "$userstate/bayes_seen.dir");
+ok(!-e "$userstate/bayes_toks.pag");
+ok(!-e "$userstate/bayes_toks.dir");
 
 sub check_examined {
   local ($_);
index d6b94be9fa969e4d69fb79010b0fe7237771176d..be39a1f6e051794d3316ec5776a16fe64a8a9655 100755 (executable)
 #!/usr/bin/perl -T
 
+use File::Find qw(find);
 use lib '.'; use lib 't';
-use SATest;
+use SATest; sa_t_init("bayessql");
+
+use Test::More;
+use Mail::SpamAssassin;
 
 use constant HAS_DBI => eval { require DBI; }; # for our cleanup stuff
+use constant SQLITE => eval { require DBD::SQLite; DBD::SQLite->VERSION(1.59_01); };
+use constant SQL => conf_bool('run_bayes_sql_tests');
 
-use Test::More;
-plan skip_all => "Bayes SQL tests are disabled" unless conf_bool('run_bayes_sql_tests');
-plan skip_all => "DBI is unavailable on this system" unless HAS_DBI;
-plan tests => 53;
+plan skip_all => "Long running tests disabled" unless conf_bool('run_long_tests');
+plan skip_all => "DBI is unavailable on this system" unless (HAS_DBI);
+plan skip_all => "Bayes SQL tests are disabled or DBD::SQLite not found" unless (SQLITE || SQL);
 
-BEGIN {
-  if (-e 't/test_dir') {
-    chdir 't';
-  }
+my $tests = 0;
+$tests += 59 if (SQLITE);
+$tests += 59 if (SQL);
+plan tests => $tests;
 
-  if (-e 'test_dir') {
-    unshift(@INC, '../blib/lib');
-  }
+diag "Note: If there is a failure it may be due to an incorrect SQL configuration." if (SQL);
+
+my ($dbconfig, $dbdsn, $dbusername, $dbpassword);
+
+if (SQLITE) {
+  my $dbdir = tempdir("bayessql.XXXXXX", DIR => "log");
+  die "FATAL: failed to create dbdir: $!" unless -d $dbdir;
+  # Bug 8033 - undocumented extension to dsn format we added for this test
+  $dbdsn = "dbi:SQLite:dbname=$dbdir/bayes.db;synchronous=OFF";
+  $dbusername = "";
+  $dbpassword = "";
+  my $dbh = DBI->connect($dbdsn,$dbusername,$dbpassword);
+  $dbh->do("PRAGMA synchronous = OFF");
+  $dbh->do("
+  CREATE TABLE bayes_expire (
+    id int(11) NOT NULL default '0',
+    runtime int(11) NOT NULL default '0',
+    PRIMARY KEY (id)
+  );
+  ") or die "Failed to create $dbfile";
+  $dbh->do("
+  CREATE TABLE bayes_global_vars (
+    variable varchar(30) NOT NULL default '',
+    value varchar(200) NOT NULL default '',
+    PRIMARY KEY (variable)
+  );
+  ") or die "Failed to create $dbfile";
+  $dbh->do("
+  INSERT INTO bayes_global_vars VALUES ('VERSION','3');
+  ") or die "Failed to create $dbfile";
+  $dbh->do("
+  CREATE TABLE bayes_seen (
+    id int(11) NOT NULL default '0',
+    msgid varchar(200) NOT NULL default '' COLLATE binary,
+    flag char(1) NOT NULL default '',
+    PRIMARY KEY (id,msgid)
+  );
+  ") or die "Failed to create $dbfile";
+  $dbh->do("
+  CREATE TABLE bayes_token (
+    id int(11) NOT NULL default '0',
+    token char(5) NOT NULL default '' COLLATE binary,
+    spam_count int(11) NOT NULL default '0',
+    ham_count int(11) NOT NULL default '0',
+    atime int(11) NOT NULL default '0',
+    PRIMARY KEY (id, token)
+  );
+  ") or die "Failed to create $dbfile";
+  $dbh->do("
+  CREATE INDEX idx_id_atime ON bayes_token (id, atime);
+  ") or die "Failed to create $dbfile";
+  $dbh->do("
+  CREATE TABLE bayes_vars (
+    id INTEGER PRIMARY KEY AUTOINCREMENT,
+    username varchar(200) NOT NULL default '',
+    spam_count int(11) NOT NULL default '0',
+    ham_count int(11) NOT NULL default '0',
+    token_count int(11) NOT NULL default '0',
+    last_expire int(11) NOT NULL default '0',
+    last_atime_delta int(11) NOT NULL default '0',
+    last_expire_reduce int(11) NOT NULL default '0',
+    oldest_token_age int(11) NOT NULL default '2147483647',
+    newest_token_age int(11) NOT NULL default '0'
+  );
+  ") or die "Failed to create $dbfile";
+  $dbh->do("
+  CREATE UNIQUE INDEX idx_username ON bayes_vars (username);
+  ") or die "Failed to create $dbfile";
+
+  $dbh->disconnect;
+  undef $dbh;
+
+  $dbconfig = "
+    bayes_store_module Mail::SpamAssassin::BayesStore::SQL
+    bayes_sql_dsn $dbdsn
+  ";
+
+  run_bayes();
+  rmtree($dbdir);
 }
 
-diag "Note: Failure may be due to an incorrect config.";
-
-my $dbdsn = conf('bayes_sql_dsn');
-my $dbusername = conf('bayes_sql_username');
-my $dbpassword = conf('bayes_sql_password');
-
-my $dbconfig = '';
-foreach my $setting (qw(
-                  bayes_store_module
-                  bayes_sql_dsn
-                  bayes_sql_username
-                  bayes_sql_password
-                ))
-{
-  $val = conf($setting);
-  $dbconfig .= "$setting $val\n" if $val;
+if (SQL) {
+  $dbdsn = conf('bayes_sql_dsn');
+  $dbusername = conf('bayes_sql_username');
+  $dbpassword = conf('bayes_sql_password');
+
+  $dbconfig = '';
+  foreach my $setting (qw(
+    bayes_store_module
+    bayes_sql_dsn
+    bayes_sql_username
+    bayes_sql_password
+    ))
+  {
+    my $val = conf($setting);
+    $dbconfig .= "$setting $val\n" if $val;
+  }
+
+  run_bayes();
 }
 
-my $testuser = 'tstusr.'.$$.'.'.time();
 
-sa_t_init("bayes");
+#---------------------------------------------------------------------------
+sub run_bayes {
 
-tstlocalrules ("
-$dbconfig
-bayes_sql_override_username $testuser
-loadplugin validuserplugin ../../data/validuserplugin.pm
-bayes_sql_username_authorized 1
-");
+my $testuser = 'tstusr.'.$$.'.'.time();
 
-use Mail::SpamAssassin;
+tstprefs ("
+  $dbconfig
+  bayes_sql_override_username $testuser
+  loadplugin validuserplugin ../../../data/validuserplugin.pm
+  bayes_sql_username_authorized 1
+");
 
 my $sa = create_saobj();
 
@@ -57,21 +140,19 @@ $sa->init();
 
 ok($sa);
 
-sub getimpl {
-  return $sa->call_plugins("learner_get_implementation");
-}
+my $learner = $sa->call_plugins("learner_get_implementation");
 
-ok($sa->{bayes_scanner} && getimpl);
+ok($sa->{bayes_scanner} && $learner);
 
-ok(getimpl->{store}->tie_db_writable());
+ok($learner->{store}->tie_db_writable());
 
 # This bit breaks abstraction a bit, the userid is an implementation detail,
 # but is necessary to perform some of the tests.  Perhaps in the future we
 # can add some sort of official API for this sort of thing.
-my $testuserid = getimpl->{store}->{_userid};
+my $testuserid = $learner->{store}->{_userid};
 ok(defined($testuserid));
 
-ok(getimpl->{store}->clear_database());
+ok($learner->{store}->clear_database());
 
 ok(database_clear_p($testuser, $testuserid));
 
@@ -79,13 +160,13 @@ $sa->finish_learner();
 
 undef $sa;
 
-sa_t_init("bayes");
+sa_t_init("bayessql");
 
-tstlocalrules ("
-$dbconfig
-bayes_sql_override_username iwillfail
-loadplugin validuserplugin ../../data/validuserplugin.pm
-bayes_sql_username_authorized 1
+tstprefs ("
+  $dbconfig
+  bayes_sql_override_username iwillfail
+  loadplugin validuserplugin ../../../data/validuserplugin.pm
+  bayes_sql_username_authorized 1
 ");
 
 $sa = create_saobj();
@@ -94,19 +175,21 @@ $sa->init();
 
 ok($sa);
 
+$learner = $sa->call_plugins("learner_get_implementation");
+
 ok($sa->{bayes_scanner});
 
-ok(!getimpl->{store}->tie_db_writable());
+ok(!$learner->{store}->tie_db_writable());
 
 $sa->finish_learner();
 
 undef $sa;
 
-sa_t_init("bayes");
+sa_t_init("bayessql");
 
-tstlocalrules ("
-$dbconfig
-bayes_sql_override_username $testuser
+tstprefs ("
+  $dbconfig
+  bayes_sql_override_username $testuser
 ");
 
 $sa = create_saobj();
@@ -115,6 +198,8 @@ $sa->init();
 
 ok($sa);
 
+$learner = $sa->call_plugins("learner_get_implementation");
+
 ok($sa->{bayes_scanner});
 
 ok(!$sa->{bayes_scanner}->is_scan_available());
@@ -139,49 +224,50 @@ my $mail = $sa->parse( \@msg );
 
 ok($mail);
 
-my $body = getimpl->get_body_from_msg($mail);
+my $body = $learner->get_body_from_msg($mail);
 
 ok($body);
 
-my $toks = getimpl->tokenize($mail, $body);
+my $toks = $learner->tokenize($mail, $body);
 
 ok(scalar(keys %{$toks}) > 0);
 
-my($msgid,$msgid_hdr) = getimpl->get_msgid($mail);
+my $msgid = $mail->generate_msgid();
+my $msgid_hdr = $mail->get_msgid();
 
 # $msgid is the generated hash messageid
 # $msgid_hdr is the Message-Id header
-ok($msgid eq '4cf5cc4d53b22e94d3e55932a606b18641a54041@sa_generated');
+ok($msgid eq '71f849915d7e469ddc1890cd8175f6876843f99e@sa_generated');
 ok($msgid_hdr eq '9PS291LhupY');
 
-ok(getimpl->{store}->tie_db_writable());
+ok($learner->{store}->tie_db_writable());
 
-ok(!getimpl->{store}->seen_get($msgid));
+ok(!$learner->{store}->seen_get($msgid));
 
-getimpl->{store}->untie_db();
+$learner->{store}->untie_db();
 
 ok($sa->{bayes_scanner}->learn(1, $mail));
 
 ok(!$sa->{bayes_scanner}->learn(1, $mail));
 
-ok(getimpl->{store}->tie_db_writable());
+ok($learner->{store}->tie_db_writable());
 
-ok(getimpl->{store}->seen_get($msgid) eq 's');
+ok($learner->{store}->seen_get($msgid) eq 's');
 
-getimpl->{store}->untie_db();
+$learner->{store}->untie_db();
 
-ok(getimpl->{store}->tie_db_writable());
+ok($learner->{store}->tie_db_writable());
 
 my $tokerror = 0;
 foreach my $tok (keys %{$toks}) {
-  my ($spam, $ham, $atime) = getimpl->{store}->tok_get($tok);
+  my ($spam, $ham, $atime) = $learner->{store}->tok_get($tok);
   if ($spam == 0 || $ham > 0) {
     $tokerror = 1;
   }
 }
 ok(!$tokerror);
 
-my $tokens = getimpl->{store}->tok_get_all(keys %{$toks});
+my $tokens = $learner->{store}->tok_get_all(keys %{$toks});
 
 ok($tokens);
 
@@ -195,44 +281,44 @@ foreach my $tok (@{$tokens}) {
 
 ok(!$tokerror);
 
-getimpl->{store}->untie_db();
+$learner->{store}->untie_db();
 
 ok($sa->{bayes_scanner}->learn(0, $mail));
 
-ok(getimpl->{store}->tie_db_writable());
+ok($learner->{store}->tie_db_writable());
 
-ok(getimpl->{store}->seen_get($msgid) eq 'h');
+ok($learner->{store}->seen_get($msgid) eq 'h');
 
-getimpl->{store}->untie_db();
+$learner->{store}->untie_db();
 
-ok(getimpl->{store}->tie_db_writable());
+ok($learner->{store}->tie_db_writable());
 
 $tokerror = 0;
 foreach my $tok (keys %{$toks}) {
-  my ($spam, $ham, $atime) = getimpl->{store}->tok_get($tok);
+  my ($spam, $ham, $atime) = $learner->{store}->tok_get($tok);
   if ($spam  > 0 || $ham == 0) {
     $tokerror = 1;
   }
 }
 ok(!$tokerror);
 
-getimpl->{store}->untie_db();
+$learner->{store}->untie_db();
 
 ok($sa->{bayes_scanner}->forget($mail));
 
-ok(getimpl->{store}->tie_db_writable());
+ok($learner->{store}->tie_db_writable());
 
-ok(!getimpl->{store}->seen_get($msgid));
+ok(!$learner->{store}->seen_get($msgid));
 
-getimpl->{store}->untie_db();
+$learner->{store}->untie_db();
 
 # This bit breaks abstraction a bit, the userid is an implementation detail,
 # but is necessary to perform some of the tests.  Perhaps in the future we
 # can add some sort of official API for this sort of thing.
-$testuserid = getimpl->{store}->{_userid};
+$testuserid = $learner->{store}->{_userid};
 ok(defined($testuserid));
 
-ok(getimpl->{store}->clear_database());
+ok($learner->{store}->clear_database());
 
 ok(database_clear_p($testuser, $testuserid));
 
@@ -240,27 +326,42 @@ $sa->finish_learner();
 
 undef $sa;
 
-sa_t_init('bayes'); # this wipes out what is there and begins anew
+sa_t_init("bayessql"); # this wipes out what is there and begins anew
 
 # make sure we learn to a journal
-tstlocalrules ("
-$dbconfig
-bayes_min_spam_num 10
-bayes_min_ham_num 10
-bayes_sql_override_username $testuser
+tstprefs ("
+  $dbconfig
+  bayes_min_spam_num 10
+  bayes_min_ham_num 10
+  bayes_sql_override_username $testuser
 ");
 
 # we get to bastardize the existing pattern matching code here.  It lets us provide
 # our own checking callback and keep using the existing ok_all_patterns call
 %patterns = ( 1 => 'Acted on message' );
 
+$wanted_examined = count_files("data/spam");
 ok(salearnrun("--spam data/spam", \&check_examined));
 ok_all_patterns();
 
+$wanted_examined = count_files("data/nice");
 ok(salearnrun("--ham data/nice", \&check_examined));
 ok_all_patterns();
 
-ok(salearnrun("--ham data/whitelists", \&check_examined));
+$wanted_examined = count_files("data/welcomelists");
+ok(salearnrun("--ham data/welcomelists", \&check_examined));
+ok_all_patterns();
+
+$wanted_examined = 3;
+ok(salearnrun("--ham --mbox data/nice.mbox", \&check_examined));
+ok_all_patterns();
+
+$wanted_examined = 3;
+ok(salearnrun("--ham --mbox < data/nice.mbox", \&check_examined));
+ok_all_patterns();
+
+$wanted_examined = 3;
+ok(salearnrun("--forget --mbox data/nice.mbox", \&check_examined));
 ok_all_patterns();
 
 %patterns = ( 'non-token data: bayes db version' => 'db version' );
@@ -277,6 +378,8 @@ $sa = create_saobj();
 
 $sa->init();
 
+$learner = $sa->call_plugins("learner_get_implementation");
+
 open(MAIL,"< ../sample-nonspam.txt");
 
 $raw_message = do {
@@ -294,13 +397,13 @@ foreach my $line (split(/^/m,$raw_message)) {
 
 $mail = $sa->parse( \@msg );
 
-$body = getimpl->get_body_from_msg($mail);
+$body = $learner->get_body_from_msg($mail);
 
 my $msgstatus = Mail::SpamAssassin::PerMsgStatus->new($sa, $mail);
 
 ok($msgstatus);
 
-my $score = getimpl->scan($msgstatus, $mail, $body);
+my $score = $learner->scan($msgstatus, $mail, $body);
 
 # Pretty much we can't count on the data returned with such little training
 # so just make sure that the score wasn't equal to .5 which is the default
@@ -325,11 +428,11 @@ foreach my $line (split(/^/m,$raw_message)) {
 
 $mail = $sa->parse( \@msg );
 
-$body = getimpl->get_body_from_msg($mail);
+$body = $learner->get_body_from_msg($mail);
 
 $msgstatus = Mail::SpamAssassin::PerMsgStatus->new($sa, $mail);
 
-$score = getimpl->scan($msgstatus, $mail, $body);
+$score = $learner->scan($msgstatus, $mail, $body);
 
 # Pretty much we can't count on the data returned with such little training
 # so just make sure that the score wasn't equal to .5 which is the default
@@ -341,15 +444,18 @@ ok($score =~ /\d/ && $score <= 1.0 && $score != .5);
 # This bit breaks abstraction a bit, the userid is an implementation detail,
 # but is necessary to perform some of the tests.  Perhaps in the future we
 # can add some sort of official API for this sort of thing.
-$testuserid = getimpl->{store}->{_userid};
+$testuserid = $learner->{store}->{_userid};
 ok(defined($testuserid));
 
-ok(getimpl->{store}->clear_database());
+ok($learner->{store}->clear_database());
 
 ok(database_clear_p($testuser, $testuserid));
 
 $sa->finish_learner();
 
+}
+#---------------------------------------------------------------------------
+
 sub check_examined {
   local ($_);
   my $string = shift;
@@ -360,11 +466,20 @@ sub check_examined {
     $_ = join ('', <IN>);
   }
 
-  if ($_ =~ /(?:Forgot|Learned) tokens from \d+ message\(s\) \(\d+ message\(s\) examined\)/) {
-    $found{'Acted on message'}++;
+  if ($_ =~ /(?:Forgot|Learned) tokens from \d+ message\(s\) \((\d+) message\(s\) examined\)/) {
+    #print STDERR "examined $1 messages\n";
+    if (defined $wanted_examined && $wanted_examined == $1) {
+      $found{'Acted on message'}++;
+    }
   }
 }
 
+sub count_files {
+  my $cnt = 0;
+  find({wanted => sub { $cnt++ if -f $_; }, no_chdir => 1}, $_[0]);
+  return $cnt;
+}
+
 # WARNING! Do not use this as an example, this breaks abstraction
 # and is here strictly to help the regression tests.
 sub database_clear_p {
@@ -399,4 +514,3 @@ sub database_clear_p {
   return 1;
 }
 
-  
index 4649b6cbdfe3710adb3353e26001a789cb425b64..6a42e141c2d5eb45edc6ee4d864bb1b9fef86a3b 100755 (executable)
@@ -6,22 +6,26 @@ use Test::More tests => 3;
 
 # ---------------------------------------------------------------------------
 
-%patterns = (
-
-q{ USER_IN_BLACKLIST }, 'blacklisted',
-
+disable_compat "welcomelist_blocklist";
 
+%patterns = (
+  q{ 100 USER_IN_BLACKLIST }, 'blacklisted',
 );
 
 %anti_patterns = (
-q{ autolearn=ham } => 'autolearned as ham'
+  'autolearn=ham' => 'autolearned as ham'
 );
 
 tstprefs ('
-
-blacklist_from *@ximian.com
-
+  header USER_IN_BLOCKLIST             eval:check_from_in_blocklist()
+  tflags USER_IN_BLOCKLIST             userconf nice noautolearn
+  meta USER_IN_BLACKLIST               (USER_IN_BLOCKLIST)
+  tflags USER_IN_BLACKLIST             userconf nice noautolearn
+  score USER_IN_BLACKLIST              100
+  score USER_IN_BLOCKLIST              0.01
+  blacklist_from *@ximian.com
 ');
 
 ok (sarun ("-L -t < data/nice/001", \&patterns_run_cb));
 ok_all_patterns();
+
diff --git a/upstream/t/blocklist_autolearn.t b/upstream/t/blocklist_autolearn.t
new file mode 100755 (executable)
index 0000000..ca05847
--- /dev/null
@@ -0,0 +1,26 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("blocklist_autolearn");
+use Test::More tests => 3;
+
+# ---------------------------------------------------------------------------
+
+%patterns = (
+  q{ 100 USER_IN_BLOCKLIST }, 'blocklisted',
+);
+
+%anti_patterns = (
+  'autolearn=ham' => 'autolearned as ham'
+);
+
+tstprefs ('
+  header USER_IN_BLOCKLIST             eval:check_from_in_blocklist()
+  tflags USER_IN_BLOCKLIST             userconf nice noautolearn
+  score USER_IN_BLOCKLIST              100
+  blacklist_from *@ximian.com
+');
+
+ok (sarun ("-L -t < data/nice/001", \&patterns_run_cb));
+ok_all_patterns();
+
index b768222a7d4333572c94b8cf4bc6a5734a44d8d7..73fe747b74441382ded1ba8c556a3c8d644534d5 100755 (executable)
@@ -1,20 +1,5 @@
 #!/usr/bin/perl -w -T
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/body_mod.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
-
 use strict;
 use lib '.'; use lib 't';
 use SATest; sa_t_init("body_mod");
old mode 100644 (file)
new mode 100755 (executable)
index 41206fd..76d80c5
@@ -2,21 +2,6 @@
 
 # test URIs with UTF8 IDNA-equivalent dots between domains instead of ordinary '.'
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_names.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
-
 use strict;
 use lib '.'; use lib 't';
 use SATest; sa_t_init("body_str.t");
index af4e5f5a763d52cad55014423d0e4ce1fc10efac..689c978045171fecdabd51333fab6ca610bb00df 100755 (executable)
@@ -1,24 +1,5 @@
 #!/usr/bin/perl -T
 
-# detect use of dollar-ampersand somewhere in the perl interpreter;
-# once it is used once, it slows down every regexp match thereafter.
-
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_tests.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-    unshift(@INC, '../lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
-
 use lib '.'; use lib 't';
 use SATest; sa_t_init("check_implemented");
 
@@ -33,14 +14,14 @@ require Mail::SpamAssassin;
 
 # kill all 'loadplugin' lines
 foreach my $file 
-        (<log/localrules.tmp/*.pre>, <log/test_rules_copy/*.pre>) #*/
+        (<$localrules/*.pre>, <$siterules/*.pre>) #*/
 {
   $file = main::untaint_var($file);
   rename $file, "$file.bak" or die "rename $file failed";
   open IN, "<$file.bak" or die "cannot read $file.bak";
   open OUT, ">$file" or die "cannot write $file";
   while (<IN>) {
-    s/^loadplugin/###loadplugin/g;
+    s/^\s*loadplugin/###loadplugin/g;
     print OUT;
   }
   close IN;
index 5e487ed90ae1202688bc6beea49a07716be9f38e..22577cd3fd5850763ea7cf0eaa84fb6d12e1a11a 100755 (executable)
@@ -1,29 +1,22 @@
 #!/usr/bin/perl -T
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_tests.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
+use lib '.'; use lib 't';
+use SATest; sa_t_init("cidrs");
 
 use strict;
+use Test::More;
 
-use Test::More tests => 51;
+use constant HAS_NET_CIDR => eval { require Net::CIDR::Lite; };
+
+my $tests = 72;
+$tests += 4 if (HAS_NET_CIDR);
+plan tests => $tests;
 
 use Mail::SpamAssassin;
 use Mail::SpamAssassin::NetSet;
 
 my $sa = Mail::SpamAssassin->new({
-    rules_filename => "$prefix/rules",
+    rules_filename => $localrules,
 });
 
 sub tryone ($@) {
@@ -61,8 +54,34 @@ ok tryone "127.0.0.254", "127.";
 ok tryone "127.0.0.1", "127/8";
 ok tryone "127.0.0.1", "127.0/16";
 ok tryone "127.0.0.1", "127.0.0/24";
+ok tryone "127.0.0.0", "127.0.0.0/24";
+ok tryone "127.0.0.255", "127.0.0.0/24";
+
+ok !tryone "127.0.0.0", "127.0.0.1/32";
 ok tryone "127.0.0.1", "127.0.0.1/32";
+ok !tryone "127.0.0.2", "127.0.0.1/32";
+
+ok tryone "127.0.0.0", "127.0.0.0/31";
+ok tryone "127.0.0.1", "127.0.0.0/31";
+ok !tryone "127.0.0.2", "127.0.0.0/31";
+ok !tryone "127.0.0.3", "127.0.0.0/31";
+
+# This probably misbehaves because it's not an "even" CIDR
+ok tryone "127.0.0.0", "127.0.0.1/31"; # NetAddr::IP bug? Should NOT match?
 ok tryone "127.0.0.1", "127.0.0.1/31";
+ok !tryone "127.0.0.2", "127.0.0.1/31"; # NetAddr::IP bug? Should match?
+ok !tryone "127.0.0.3", "127.0.0.1/31";
+
+ok !tryone "127.0.0.1", "127.0.0.2/31";
+ok tryone "127.0.0.2", "127.0.0.2/31";
+ok tryone "127.0.0.3", "127.0.0.2/31";
+ok !tryone "127.0.0.4", "127.0.0.2/31";
+
+ok !tryone "127.0.0.15", "127.0.0.16/31";
+ok tryone "127.0.0.16", "127.0.0.16/31";
+ok tryone "127.0.0.17", "127.0.0.16/31";
+ok !tryone "127.0.0.18", "127.0.0.16/31";
+
 ok tryone "127.0.0.1", "10.", "11.", "127.0.0.1";
 ok tryone "127.0.0.1", "127.0.";
 ok tryone "127.0.0.1", "127.0.0.";
@@ -120,3 +139,13 @@ ok trynet "DEAD:BEEF:0000:0102:0304:0506:0:0/96",
 ok !trynet "DEAD:BEEF:0000:0102:0304:0506:1:1/90",
           "DEAD:BEEF:0000:0102:0304:0506:0:0/96";
 
+# NetSet does not parse leading zeroes as octal number, it strips them
+ok tryone "010.010.10.10", "10.10.10.10";
+ok !tryone "8.8.10.10", "010.010.10.10";
+
+if (HAS_NET_CIDR) {
+  ok tryone "127.0.0.1", "127.0.0.0-127.0.0.255";
+  ok trynet "127.0.0.16/30", "127.0.0.0-127.0.000.255";
+  ok !tryone "127.0.0.1", "127.0.0.8-127.0.0.20";
+  ok tryone "010.50.60.1", "0.0.0.0-010.255.255.255";
+}
index db6e7b112c5dbc63eb9032db76f3b045255a1c50..6074959aaea8a6b0563adec459499e4c6aa903f8 100644 (file)
@@ -14,16 +14,21 @@ run_long_tests=y
 
 run_net_tests=n
 
+# If you have resolver capable of returning IPv6/AAAA addresses
+run_ipv6_dns_tests=n
+
 # Run DCC Tests
 run_dcc_tests=n
 
 # ---------------------------------------------------------------------------
-# Run SQL-based user pref tests during 'make test' REQUIRES DBD::SQLite
+# Run SQL-based user pref tests during 'make test' REQUIRES DBD::SQLite 1.59_01 or later
 run_sql_pref_tests=n
 
 # ---------------------------------------------------------------------------
-# Run SQL-based Auto-whitelist tests during 'make test' (additional
-# information required, below:)
+# Run SQL-based Auto-whitelist tests during 'make test'
+# NOTE: AWL test is always run with DBD::SQLite when available, only enable
+# this when you want to additionally test for example MySQL or PostgresSQL
+# (for which database needs to be created manually and configured below).
 run_awl_sql_tests=n
 
 # SQL AWL DSN
@@ -36,12 +41,14 @@ user_awl_sql_password=
 user_awl_sql_table=awl
 
 # ---------------------------------------------------------------------------
-# Run Bayes SQL storage tests during 'make test' (additional
-# information required, below:)
+# Run Bayes SQL storage tests during 'make test'
+# NOTE: Bayes test is always run with DBD::SQLite when available, only enable
+# this when you want to additionally test for example MySQL or PostgresSQL
+# (for which database needs to be created manually and configured below).
 run_bayes_sql_tests=n
 
 # Bayes Store Module (bayes_store_module)
-bayes_store_module=Mail::SpamAssassin::BayesStore::SQL
+bayes_store_module=Mail::SpamAssassin::BayesStore::MySQL
 # Bayes SQL DSN (bayes_sql_dsn)
 bayes_sql_dsn=dbi:mysql:spamassassin:localhost
 # Bayes SQL DB username (bayes_sql_username)
@@ -64,12 +71,6 @@ run_rule_name_tests=n
 
 # ---------------------------------------------------------------------------
 
-# This test requires the Devel::SawAmpersand CPAN module, and may also fail
-# depending on changes in the third-party modules we import.
-run_saw_ampersand_test=n
-
-# ---------------------------------------------------------------------------
-
 # The "root_*.t" tests require root privileges, and may create files in
 # the filesystem as part of the test.  Disabled by default.
 run_root_tests=n
index 82bc4a8aa1c774515db89e7c3e9c22588e93c2e0..6202ff1e39b429ff1f0db31eb28c60cfd418dcbb 100755 (executable)
@@ -22,21 +22,6 @@ $/ox;
 
 # ---------------------------------------------------------------------------
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_names.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
-
 use strict;
 use lib '.'; use lib 't';
 use SATest; sa_t_init("config_errs");
index 6fc56a3437388ca9e7de37ca05d90a1ddaa53e27..951e9aac1f3567db3c889b629927aae1f6c5ab40 100755 (executable)
@@ -1,28 +1,7 @@
 #!/usr/bin/perl -T
-#
-# Test that config_tree_recurse works ok in taint mode; bug 6019
-
-delete @ENV{'PATH', 'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
-$ENV{PATH}='/bin:/usr/bin:/usr/local/bin';
-
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_tests.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-    unshift(@INC, '../lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
 
 use lib '.'; use lib 't';
-use SATest; sa_t_init("basic_obj_api");
+use SATest; sa_t_init("config_tree_recurse.t");
 use Test::More tests => 4;
 
 # ---------------------------------------------------------------------------
old mode 100644 (file)
new mode 100755 (executable)
index ad2f3bd..5cd39cb
@@ -1,21 +1,5 @@
 #!/usr/bin/perl -T
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_tests.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-    unshift(@INC, '../lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
-
 use lib '.'; use lib 't';
 use SATest; sa_t_init("cross_user_config_leak");
 use Test::More tests => 6;
@@ -43,6 +27,9 @@ use warnings;
 require Mail::SpamAssassin;
 
 my $sa = create_saobj({
+    rules_filename => $localrules,
+    site_rules_filename => $siterules,
+    userprefs_filename  => $userrules,
     require_rules        => 1,
     local_tests_only     => 1,
     dont_copy_prefs      => 1,
@@ -77,11 +64,11 @@ my %expected_val;
 my %ignored_command;
 foreach my $k (@ignored_commands) { $ignored_command{$k}++; }
 
-print "Reading log/user_prefs1\n";
-$sa->read_scoreonly_config("log/user_prefs1");
+print "Reading $workdir/user_prefs1\n";
+$sa->read_scoreonly_config("$workdir/user_prefs1");
 set_all_confs($sa->{conf});
 
-$sa->signal_user_changed( { username => "user1", user_dir => "log/user1" });
+$sa->signal_user_changed( { username => "user1", user_dir => "$workdir/user1" });
 ok validate_all_confs($sa->{conf}, 1, 'after first user config read');
 
 print "Restoring config from backup\n";
@@ -89,9 +76,9 @@ $sa->copy_config(\%conf_backup, undef) or die "copy_config failed";
 ok validate_all_confs($sa->{conf}, 0, 'after restoring from backup');
 
 
-print "Reading log/user_prefs2\n";
-$sa->read_scoreonly_config("log/user_prefs2");
-$sa->signal_user_changed( { username => "user2", user_dir => "log/user2" });
+print "Reading $workdir/user_prefs2\n";
+$sa->read_scoreonly_config("$workdir/user_prefs2");
+$sa->signal_user_changed( { username => "user2", user_dir => "$workdir/user2" });
 ok validate_all_confs($sa->{conf}, 0, 'after second user config read');
 
 print "Restoring config from backup, second time\n";
@@ -205,6 +192,9 @@ sub validate_all_confs {
     # if the default value is undef, it's a permitted value, obvs
     next if ($settings_should_exist && !defined $cmd->{default});
 
+    # ignore use_dcc etc changed default from data/01_test_rules.cf
+    next if $k =~ /^use_(?:dcc|razor2|pyzor)$/;
+
     $setting_details = "key='$k' when=$stage";
     if (!defined $cmd->{type}) {
       # warn "undef config type for $k";                # already done this
@@ -261,10 +251,12 @@ sub assert_validation {
         " wanted=".(defined $expected_val ? "'$expected_val'" : "(none)").
         " $setting_details";
     $validation_passed = 0;
+    $keep_workdir = 1;
   }
   if (!$settings_should_exist && defined($val) && "".$val eq "".$expected_val) {
     warn "found=".(defined $val ? "'$val'" : "(none)")." wanted=(none)".
         " $setting_details";
     $validation_passed = 0;
+    $keep_workdir = 1;
   }
 }
index a7c3a8da95b31ca37444c15acf301e3d0469f73f..aa0394e14b9c0ee247070c985836f79c714e7078 100644 (file)
 # limitations under the License.
 # </@LICENSE>
 
-# copied from 10_default_prefs.cf
-clear_report_template
-report Spam detection software, running on the system "_HOSTNAME_", has
-report identified this incoming email as possible spam.  The original message
-report has been attached to this so you can view it (if it isn't spam) or label
-report similar future email.  If you have any questions, see
-report _CONTACTADDRESS_ for details.
-report
-report Content preview:  _PREVIEW_
-report
-report Content analysis details:   (_SCORE_ points, _REQD_ required)
-report
-report " pts rule name              description"
-report  ---- ---------------------- --------------------------------------------------
-report _SUMMARY_
-
-report_contact  @@CONTACT_ADDRESS@@
-
-clear_unsafe_report_template
-unsafe_report The original message was not completely plain text, and may be unsafe to
-unsafe_report open with some email clients; in particular, it may contain a virus,
-unsafe_report or confirm that your address can receive spam.  If you wish to view
-unsafe_report it, it may be safer to save it to a file and open it with an editor.
-
-clear_headers
-add_header all Checker-Version SpamAssassin _VERSION_ (_SUBVERSION_) on _HOSTNAME_
-add_header spam Flag _YESNOCAPS_
-add_header all Level _STARS(*)_
-add_header all Status "_YESNO_, score=_SCORE_ required=_REQD_ tests=_TESTS_ autolearn=_AUTOLEARN_ version=_VERSION_"
-
-clear_originating_ip_headers
-originating_ip_headers X-Yahoo-Post-IP X-Originating-IP X-Apparently-From
-originating_ip_headers X-SenderIP
-
-required_score           5
-ok_locales              all
-ifplugin Mail::SpamAssassin::Plugin::TextCat
-ok_languages            all
-endif # Mail::SpamAssassin::Plugin::TextCat
-
-ifplugin Mail::SpamAssassin::Plugin::AutoLearnThreshold
-bayes_auto_learn_threshold_nonspam      0.1
-bayes_auto_learn_threshold_spam         12.0
-endif # Mail::SpamAssassin::Plugin::AutoLearnThreshold
-
-bayes_auto_learn                        1
-
-report_safe           1
-
+# This file defines a subset of the default rules that tests can count on
+# If you need something else in a test, use tstpre, tstlocalrules, or tstprefs in the test
+# instead of adding it here, so tests can be more self-contained and maintainable.
 
 header TEST_NOREALNAME  From =~ /^["\s]*\<?\S+\@\S+\>?\s*$/
 describe TEST_NOREALNAME From: does not include a real name
@@ -100,33 +54,6 @@ header TEST_MSGID_OUTLOOK_INVALID    eval:check_outlook_message_id()
 describe TEST_MSGID_OUTLOOK_INVALID  Message-Id is fake (in Outlook Express format)
 score TEST_MSGID_OUTLOOK_INVALID     5
 
-ifplugin Mail::SpamAssassin::Plugin::Hashcash
-header HASHCASH_20            eval:check_hashcash_value('20', '21')
-header HASHCASH_21            eval:check_hashcash_value('21', '22')
-header HASHCASH_22            eval:check_hashcash_value('22', '23')
-header HASHCASH_23            eval:check_hashcash_value('23', '24')
-header HASHCASH_24            eval:check_hashcash_value('24', '25')
-header HASHCASH_25            eval:check_hashcash_value('25', '26')
-header HASHCASH_HIGH          eval:check_hashcash_value('26', '9999')
-tflags HASHCASH_20            nice userconf
-tflags HASHCASH_21            nice userconf
-tflags HASHCASH_22            nice userconf
-tflags HASHCASH_23            nice userconf
-tflags HASHCASH_24            nice userconf
-tflags HASHCASH_25            nice userconf
-tflags HASHCASH_HIGH          nice userconf
-describe HASHCASH_20          Contains valid Hashcash token (20 bits)
-describe HASHCASH_21          Contains valid Hashcash token (21 bits)
-describe HASHCASH_22          Contains valid Hashcash token (22 bits)
-describe HASHCASH_23          Contains valid Hashcash token (23 bits)
-describe HASHCASH_24          Contains valid Hashcash token (24 bits)
-describe HASHCASH_25          Contains valid Hashcash token (25 bits)
-describe HASHCASH_HIGH        Contains valid Hashcash token (>25 bits)
-header HASHCASH_2SPEND        eval:check_hashcash_double_spend()
-describe HASHCASH_2SPEND      Hashcash token already spent in another mail
-tflags HASHCASH_2SPEND        userconf
-endif # Mail::SpamAssassin::Plugin::Hashcash
-
 header MISSING_HB_SEP            eval:check_msg_parse_flags('missing_head_body_separator')
 describe MISSING_HB_SEP          Missing blank line between message header and body
 tflags MISSING_HB_SEP            userconf
@@ -145,38 +72,6 @@ describe GTUBE           Generic Test for Unsolicited Bulk Email
 score GTUBE              1000
 tflags GTUBE             userconf noautolearn
 
-body BAYES_99         eval:check_bayes('0.99', '1.00')
-tflags BAYES_99               learn
-describe BAYES_99     Bayes spam probability is 99 to 100%
-score BAYES_99 0 0 3.5 3.5
-
-ifplugin Mail::SpamAssassin::Plugin::WLBLEval
-header USER_IN_WHITELIST       eval:check_from_in_whitelist()
-tflags USER_IN_WHITELIST       userconf nice noautolearn
-score  USER_IN_WHITELIST -100.000
-header USER_IN_DEF_WHITELIST   eval:check_from_in_default_whitelist()
-tflags USER_IN_DEF_WHITELIST   userconf nice noautolearn
-score  USER_IN_DEF_WHITELIST -15.000
-header USER_IN_WHITELIST_TO     eval:check_to_in_whitelist()
-tflags USER_IN_WHITELIST_TO     userconf nice noautolearn
-score  USER_IN_WHITELIST_TO -6.000
-header USER_IN_BLACKLIST       eval:check_from_in_blacklist()
-tflags USER_IN_BLACKLIST       userconf noautolearn
-score  USER_IN_BLACKLIST 100.000
-header USER_IN_BLACKLIST_TO     eval:check_to_in_blacklist()
-tflags USER_IN_BLACKLIST_TO     userconf noautolearn
-score  USER_IN_BLACKLIST_TO 10.000
-endif # Mail::SpamAssassin::Plugin::WLBLEval
-
-ifplugin Mail::SpamAssassin::Plugin::WhiteListSubject
-header SUBJECT_IN_WHITELIST    eval:check_subject_in_whitelist()
-tflags SUBJECT_IN_WHITELIST    userconf nice noautolearn
-header SUBJECT_IN_BLACKLIST    eval:check_subject_in_blacklist()
-tflags SUBJECT_IN_BLACKLIST    userconf noautolearn
-score  SUBJECT_IN_BLACKLIST 100
-score  SUBJECT_IN_WHITELIST -100
-endif # Mail::SpamAssassin::Plugin::WhiteListSubject
-
 header __HAS_MSGID               MESSAGEID =~ /\S/
 header __SANE_MSGID              MESSAGEID =~ /^<[^<>\\ \t\n\r\x0b\x80-\xff]+\@[^<>\\ \t\n\r\x0b\x80-\xff]+>\s*$/m
 header __MSGID_COMMENT           MESSAGEID =~ /\(.*\)/m
@@ -191,36 +86,6 @@ header INVALID_DATE              Date !~ /^\s*(?:(?i:Mon|Tue|Wed|Thu|Fri|Sat|Sun
 describe INVALID_DATE            Invalid Date: header (not RFC 2822)
 score INVALID_DATE               2.303 1.651 1.329 1.245
 
-ifplugin Mail::SpamAssassin::Plugin::SPF
-header SPF_PASS                        eval:check_for_spf_pass()
-header SPF_NEUTRAL             eval:check_for_spf_neutral()
-header SPF_FAIL                        eval:check_for_spf_fail()
-header SPF_SOFTFAIL            eval:check_for_spf_softfail()
-header SPF_HELO_PASS           eval:check_for_spf_helo_pass()
-header SPF_HELO_NEUTRAL                eval:check_for_spf_helo_neutral()
-header SPF_HELO_FAIL           eval:check_for_spf_helo_fail()
-header SPF_HELO_SOFTFAIL       eval:check_for_spf_helo_softfail()
-tflags SPF_PASS                        nice userconf net
-tflags SPF_HELO_PASS           nice userconf net
-tflags SPF_NEUTRAL             net
-tflags SPF_FAIL                        net
-tflags SPF_SOFTFAIL            net
-tflags SPF_HELO_NEUTRAL                net
-tflags SPF_HELO_FAIL           net
-tflags SPF_HELO_SOFTFAIL       net
-header USER_IN_SPF_WHITELIST   eval:check_for_spf_whitelist_from()
-tflags USER_IN_SPF_WHITELIST   userconf nice noautolearn
-header USER_IN_DEF_SPF_WL      eval:check_for_def_spf_whitelist_from()
-tflags USER_IN_DEF_SPF_WL      userconf nice noautolearn
-endif # Mail::SpamAssassin::Plugin::SPF
-
-ifplugin Mail::SpamAssassin::Plugin::AWL
-header AWL                   eval:check_from_in_auto_whitelist()
-describe AWL                 From: address is in the auto white-list
-tflags AWL                   userconf noautolearn
-priority AWL                    1000
-endif # Mail::SpamAssassin::Plugin::AWL
-
 redirector_pattern      /^http:\/\/chkpt\.zdnet\.com\/chkpt\/\w+\/(.*)$/i
 redirector_pattern      /^http:\/\/www(?:\d+)?\.nate\.com\/r\/\w+\/(.*)$/i
 redirector_pattern      /^http:\/\/.+\.gov\/(?:.*\/)?externalLink\.jhtml\?.*url=(.*?)(?:&.*)?$/i
@@ -228,43 +93,3 @@ redirector_pattern      /^http:\/\/redir\.internet\.com\/.+?\/.+?\/(.*)$/i
 redirector_pattern      /^http:\/\/(?:.*?\.)?adtech\.de\/.*(?:;|\|)link=(.*?)(?:;|$)/i
 redirector_pattern      m'^http.*?/redirect\.php\?.*(?<=[?&])goto=(.*?)(?:$|[&\#])'i
 redirector_pattern      m'^https?:/*(?:[^/]+\.)?emf\d\.com/r\.cfm.*?&r=(.*)'i
-
-ifplugin Mail::SpamAssassin::Plugin::DCC
-full DCC_CHECK          eval:check_dcc()
-describe DCC_CHECK      Detected as bulk mail by DCC (dcc-servers.net)
-tflags DCC_CHECK        net
-reuse DCC_CHECK
-endif
-
-ifplugin Mail::SpamAssassin::Plugin::DKIM
-full   DKIM_SIGNED           eval:check_dkim_signed()
-full   DKIM_VALID            eval:check_dkim_valid()
-full   DKIM_VALID_AU         eval:check_dkim_valid_author_sig()
-meta   DKIM_INVALID          DKIM_SIGNED && !DKIM_VALID
-header DKIM_ADSP_NXDOMAIN    eval:check_dkim_adsp('N')
-header DKIM_ADSP_DISCARD     eval:check_dkim_adsp('D')
-header DKIM_ADSP_ALL         eval:check_dkim_adsp('A')
-header DKIM_ADSP_CUSTOM_LOW  eval:check_dkim_adsp('1')
-header DKIM_ADSP_CUSTOM_MED  eval:check_dkim_adsp('2')
-header DKIM_ADSP_CUSTOM_HIGH eval:check_dkim_adsp('3')
-adsp_override sa-test-nxd.spamassassin.org  nxdomain
-adsp_override sa-test-unk.spamassassin.org  unknown
-adsp_override sa-test-all.spamassassin.org  all
-adsp_override sa-test-dis.spamassassin.org  discardable
-adsp_override sa-test-di2.spamassassin.org
-endif
-
-ifplugin Mail::SpamAssassin::Plugin::Shortcircuit
-header SHORTCIRCUIT             eval:check_shortcircuit()
-describe SHORTCIRCUIT           Not all rules were run, due to a shortcircuited rule
-tflags SHORTCIRCUIT             userconf noautolearn
-endif
-
-
-ifplugin Mail::SpamAssassin::Plugin::Razor2
-
-full RAZOR2_CHECK      eval:check_razor2()
-describe RAZOR2_CHECK  Listed in Razor2 (http://razor.sf.net/)
-tflags RAZOR2_CHECK    net
-
-endif
index c4681ca3ef9c4406aa8fa03ffa9e4c23e6de5229..f789493f7a901e0f763cc1e82b4916a6fde2b6af 100644 (file)
@@ -1,41 +1,39 @@
-loadplugin Mail::SpamAssassin::Plugin::Check
-loadplugin Mail::SpamAssassin::Plugin::URIDNSBL
-loadplugin Mail::SpamAssassin::Plugin::Hashcash
-loadplugin Mail::SpamAssassin::Plugin::SPF
-loadplugin Mail::SpamAssassin::Plugin::AWL
-loadplugin Mail::SpamAssassin::Plugin::WhiteListSubject
-loadplugin Mail::SpamAssassin::Plugin::MIMEHeader
-loadplugin Mail::SpamAssassin::Plugin::ReplaceTags
-loadplugin Mail::SpamAssassin::Plugin::DKIM
-loadplugin Mail::SpamAssassin::Plugin::URIDetail
-loadplugin Mail::SpamAssassin::Plugin::HTTPSMismatch
-loadplugin Mail::SpamAssassin::Plugin::Bayes
-loadplugin Mail::SpamAssassin::Plugin::BodyEval
-loadplugin Mail::SpamAssassin::Plugin::DNSEval
-loadplugin Mail::SpamAssassin::Plugin::HTMLEval
-loadplugin Mail::SpamAssassin::Plugin::HeaderEval
-loadplugin Mail::SpamAssassin::Plugin::MIMEEval
-loadplugin Mail::SpamAssassin::Plugin::RelayEval
-loadplugin Mail::SpamAssassin::Plugin::URIEval
-loadplugin Mail::SpamAssassin::Plugin::WLBLEval
-loadplugin Mail::SpamAssassin::Plugin::VBounce
 
-# Try to load some non-default plugins also
+# Allow DNS queries only to our test zone
+dns_query_restriction deny *
+dns_query_restriction allow spamassassin.org
+
+# Load selection of non-default plugins for all tests
+loadplugin Mail::SpamAssassin::Plugin::AWL
 loadplugin Mail::SpamAssassin::Plugin::RelayCountry
 loadplugin Mail::SpamAssassin::Plugin::DCC
-loadplugin Mail::SpamAssassin::Plugin::AntiVirus
-loadplugin Mail::SpamAssassin::Plugin::AWL
-#loadplugin Mail::SpamAssassin::Plugin::TextCat
-#loadplugin Mail::SpamAssassin::Plugin::AccessDB
+loadplugin Mail::SpamAssassin::Plugin::TextCat
 loadplugin Mail::SpamAssassin::Plugin::Shortcircuit
-#loadplugin Mail::SpamAssassin::Plugin::Rule2XSBody
 loadplugin Mail::SpamAssassin::Plugin::ASN
-#loadplugin Mail::SpamAssassin::Plugin::PhishTag
-#loadplugin Mail::SpamAssassin::Plugin::TxRep
+loadplugin Mail::SpamAssassin::Plugin::PhishTag
 loadplugin Mail::SpamAssassin::Plugin::URILocalBL
 loadplugin Mail::SpamAssassin::Plugin::PDFInfo
 loadplugin Mail::SpamAssassin::Plugin::HashBL
-#loadplugin Mail::SpamAssassin::Plugin::ResourceLimits
 loadplugin Mail::SpamAssassin::Plugin::FromNameSpoof
 loadplugin Mail::SpamAssassin::Plugin::Phishing
-#loadplugin Mail::SpamAssassin::Plugin::OLEVBMacro
+loadplugin Mail::SpamAssassin::Plugin::ExtractText
+
+clear_report_template
+report _SUMMARY_
+
+clear_headers
+
+add_header spam Flag _YESNOCAPS_
+add_header all Level _STARS(*)_
+add_header all Status "_YESNO_, score=_SCORE_ required=_REQD_ tests=_TESTS_ autolearn=_AUTOLEARN_ version=_VERSION_"
+
+ifplugin Mail::SpamAssassin::Plugin::DCC
+use_dcc 0
+endif
+ifplugin Mail::SpamAssassin::Plugin::Razor2
+use_razor2 0
+endif
+ifplugin Mail::SpamAssassin::Plugin::Pyzor
+use_pyzor 0
+endif
+
index 14024752a5d9cbb0fd5534602e39ca64e4408004..75134668f10208cbf6d3213422cdd505efdccddc 100644 (file)
@@ -16,29 +16,81 @@ sub check_end {
   my ($self, $opts) = @_;
 
   local $_;
-  $_ = $opts->{permsgstatus}->get("ALL:raw");
-  s/\n/[\\n]/gs; s/\t/[\\t]/gs; s/\n+//gs;
 
   # ignore the M:SpamAssassin:compile() test message
-  return if /I need to make this message body somewhat long so TextCat preloads/;
-  print STDOUT "text-all-raw: $_\n";
+  return if $self->{linting};
+  #return if /I need to make this message body somewhat long so TextCat preloads/;
+
+  ## pre-4.0 scalar context calls
+
+  $_ = $opts->{permsgstatus}->get("ALL:raw");
+  s/\n/[\\n]/gs; s/\t/[\\t]/gs; s/\n+//gs;
+  print STDOUT "scalar-text-all-raw: $_"."[END]\n";
 
   $_ = $opts->{permsgstatus}->get("ALL");
   s/\n/[\\n]/gs; s/\t/[\\t]/gs; s/\n+//gs;
-  print STDOUT "text-all-noraw: $_\n";
+  print STDOUT "scalar-text-all-noraw: $_"."[END]\n";
 
   $_ = $opts->{permsgstatus}->get("From:raw");
   s/\n/[\\n]/gs; s/\t/[\\t]/gs; s/\n+//gs;
-  print STDOUT "text-from-raw: $_\n";
+  print STDOUT "scalar-text-from-raw: $_"."[END]\n";
 
   $_ = $opts->{permsgstatus}->get("From");
   s/\n/[\\n]/gs; s/\t/[\\t]/gs; s/\n+//gs;
-  print STDOUT "text-from-noraw: $_\n";
+  print STDOUT "scalar-text-from-noraw: $_"."[END]\n";
 
   $_ = $opts->{permsgstatus}->get("From:addr");
   s/\n/[\\n]/gs; s/\t/[\\t]/gs; s/\n+//gs;
-  print STDOUT "text-from-addr: $_\n";
+  print STDOUT "scalar-text-from-addr: $_"."[END]\n";
+
+  ## 4.0 list context tests
+
+  my @l;
+  my $s;
+
+  @l = $opts->{permsgstatus}->get("ALL:raw");
+  foreach (@l) { s/\n/[\\n]/gs; s/\t/[\\t]/gs; s/\n+//gs; }
+  print STDOUT "list-text-all-raw: ".join("[LIST]", @l)."[END]\n";
+
+  @l = $opts->{permsgstatus}->get("ALL");
+  foreach (@l) { s/\n/[\\n]/gs; s/\t/[\\t]/gs; s/\n+//gs; }
+  print STDOUT "list-text-all-noraw: ".join("[LIST]", @l)."[END]\n";
+
+  @l = $opts->{permsgstatus}->get("From:raw");
+  foreach (@l) { s/\n/[\\n]/gs; s/\t/[\\t]/gs; s/\n+//gs; }
+  print STDOUT "list-text-from-raw: ".join("[LIST]", @l)."[END]\n";
+
+  @l = $opts->{permsgstatus}->get("From");
+  foreach (@l) { s/\n/[\\n]/gs; s/\t/[\\t]/gs; s/\n+//gs; }
+  print STDOUT "list-text-from-noraw: ".join("[LIST]", @l)."[END]\n";
+
+  @l = $opts->{permsgstatus}->get("From:addr");
+  foreach (@l) { s/\n/[\\n]/gs; s/\t/[\\t]/gs; s/\n+//gs; }
+  print STDOUT "list-text-from-addr: ".join("[LIST]", @l)."[END]\n";
+
+  @l = $opts->{permsgstatus}->get("From:first:addr");
+  foreach (@l) { s/\n/[\\n]/gs; s/\t/[\\t]/gs; s/\n+//gs; }
+  print STDOUT "list-text-from-first-addr: ".join("[LIST]", @l)."[END]\n";
+
+  @l = $opts->{permsgstatus}->get("From:last:addr");
+  foreach (@l) { s/\n/[\\n]/gs; s/\t/[\\t]/gs; s/\n+//gs; }
+  print STDOUT "list-text-from-last-addr: ".join("[LIST]", @l)."[END]\n";
+
+  @l = $opts->{permsgstatus}->get("MESSAGEID:host");
+  foreach (@l) { s/\n/[\\n]/gs; s/\t/[\\t]/gs; s/\n+//gs; }
+  print STDOUT "list-text-msgid-host: ".join("[LIST]", @l)."[END]\n";
+
+  @l = $opts->{permsgstatus}->get("MESSAGEID:domain");
+  foreach (@l) { s/\n/[\\n]/gs; s/\t/[\\t]/gs; s/\n+//gs; }
+  print STDOUT "list-text-msgid-domain: ".join("[LIST]", @l)."[END]\n";
+
+  @l = $opts->{permsgstatus}->get("Received:ip");
+  foreach (@l) { s/\n/[\\n]/gs; s/\t/[\\t]/gs; s/\n+//gs; }
+  print STDOUT "list-text-received-ip: ".join("[LIST]", @l)."[END]\n";
 
+  @l = $opts->{permsgstatus}->get("Received:revip");
+  foreach (@l) { s/\n/[\\n]/gs; s/\t/[\\t]/gs; s/\n+//gs; }
+  print STDOUT "list-text-received-revip: ".join("[LIST]", @l)."[END]\n";
 }
 
 1;
diff --git a/upstream/t/data/dkim/arc/ko01.eml b/upstream/t/data/dkim/arc/ko01.eml
new file mode 100644 (file)
index 0000000..100fa8e
--- /dev/null
@@ -0,0 +1,16 @@
+Authentication-Results: sa-test.spamassassin.org; header.From=test@sa-test.spamassassin.org; dkim=pass (\r
+        message from spamassassin.org verified; );\r
+DKIM-Signature: v=1; a=rsa-sha256; d=sa-test.spamassassin.org; h=from:to\r
+       :subject:message-id:date:mime-version:content-type; s=t0768; bh=\r
+       15pFrAvOGi+eHKJgB6psh6iIBCbvYSuhPj+wQn6C7Ss=; b=ZFopU9lJ/WFWddnO\r
+       1nrYuptGphxfk2c4Tl0w/5HP0LhDMXX2KQRKHDh8p/AXxCERk6esOtX+BjME/ZOF\r
+       PnFrSh7naSjaT22YrT91gLD548OK73YUxR3Zh5nVOmSfn0TM\r
+From: SpamAssassin Test <test@sa-test.spamassassin.org>\r
+To: undisclosed-recipients:;\r
+Subject: test message 2\r
+Message-ID: <4A294538.10002@spamassassin.org>\r
+Date: Mon, 08 Jun 2009 12:00:00 +0000\r
+MIME-Version: 1.0\r
+Content-Type: text/plain; charset=us-ascii\r
+\r
+testing\r
diff --git a/upstream/t/data/dkim/arc/ok01.eml b/upstream/t/data/dkim/arc/ok01.eml
new file mode 100644 (file)
index 0000000..5c36849
--- /dev/null
@@ -0,0 +1,20 @@
+ARC-Seal: i=1; a=rsa-sha256; cv=none; d=sa-test.spamassassin.org; s=t0768; t=12345; b=GCLxX6NFV3/REpxEmzeKIRip5xJVP55GQTgOYndidGhYC+iXTNTm3xJf5zKQSaEikmtHgzL92QpgdpNcXGg+XvUI3UmQEOuyMCzJRw4hX0W3MFPSZ2xQr3hBKOnRpd96fAzGbDWJ9FjCwyloL+Uaylu+UNbfg1vcMv6/8NbMsF2gRSzJjhs8xQPMSZgqE0lWPkU1rmWmKbkx91txRNNrpKNQc0SlEIB1VdAsNWnnqLSp1B+EoGKsRJ1n55hpXRB6ytf+W+Edoi8Pkeb9IjNaoG8Zunwwpx59EP5iBcmGwdkYsS1eOu+92IbxihKUOMyRG9av1eJs0bSvPKS5OEs8dw==
+ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=sa-test.spamassassin.org; h=from:to:subject:message-id:date:mime-version:content-type; s=t0768; t=12345; bh=15pFrAvOGi+eHKJgB6psh6iIBCbvYSuhPj+wQn6C7Ss=; b=hqOPs3BhWVPBH2RfcQm5HGhfsqbaof3LXv1QAH4rmONbxuZXw4Rf4lDUrhWQB3UhkIsunu3TjslsODCkwPdDtSSEfbpRa7VHuq4O6tI4Ufinm5FGXflfY/o0sjx8S1gX9VFI/z3K1A2KFM/r1YnsgmoEjC7pLwgPWWyji3k0nUdaaYVzKSGktvkjMkfjgLPN/zlw0oN9ZUlfzEy6pFQdXjOuoYDQDHcq7AVm34grcj/8Mh1oNv0fUm3kAHSobebZxZb9jwp93WZPeAH/AnpaDg11U7k2IdSvbvis4qUt3SiLUYoGzCOkNywKPd8uqOilAGherx3aVpAuSwC8gdKiOw==
+ARC-Authentication-Results: i=1; sa-test.spamassassin.org; header.From=test@sa-test.spamassassin.org; dkim=pass (
+        message from spamassassin.org verified; )
+Authentication-Results: sa-test.spamassassin.org; header.From=test@sa-test.spamassassin.org; dkim=pass (
+        message from spamassassin.org verified; );
+DKIM-Signature: v=1; a=rsa-sha256; d=sa-test.spamassassin.org; h=from:to
+       :subject:message-id:date:mime-version:content-type; s=t0768; bh=
+       15pFrAvOGi+eHKJgB6psh6iIBCbvYSuhPj+wQn6C7Ss=; b=ZFopU9lJ/WFWddnO
+       1nrYuptGphxfk2c4Tl0w/5HP0LhDMXX2KQRKHDh8p/AXxCERk6esOtX+BjME/ZOF
+       PnFrSh7naSjaT22YrT91gLD548OK73YUxR3Zh5nVOmSfn0TM
+From: SpamAssassin Test <test@sa-test.spamassassin.org>
+To: undisclosed-recipients:;
+Subject: test message 2
+Message-ID: <4A294538.10002@spamassassin.org>
+Date: Mon, 08 Jun 2009 12:00:00 +0000
+MIME-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+
+testing
diff --git a/upstream/t/data/geodb/GeoIP2-City.mmdb b/upstream/t/data/geodb/GeoIP2-City.mmdb
new file mode 100644 (file)
index 0000000..11b9846
Binary files /dev/null and b/upstream/t/data/geodb/GeoIP2-City.mmdb differ
diff --git a/upstream/t/data/geodb/GeoIP2-Country.mmdb b/upstream/t/data/geodb/GeoIP2-Country.mmdb
new file mode 100644 (file)
index 0000000..96cefc5
Binary files /dev/null and b/upstream/t/data/geodb/GeoIP2-Country.mmdb differ
diff --git a/upstream/t/data/geodb/GeoIP2-ISP.mmdb b/upstream/t/data/geodb/GeoIP2-ISP.mmdb
new file mode 100644 (file)
index 0000000..1bb8191
Binary files /dev/null and b/upstream/t/data/geodb/GeoIP2-ISP.mmdb differ
diff --git a/upstream/t/data/geodb/GeoIPCity.dat b/upstream/t/data/geodb/GeoIPCity.dat
new file mode 100644 (file)
index 0000000..aad5d68
Binary files /dev/null and b/upstream/t/data/geodb/GeoIPCity.dat differ
diff --git a/upstream/t/data/geodb/GeoIPISP.dat b/upstream/t/data/geodb/GeoIPISP.dat
new file mode 100644 (file)
index 0000000..82a74c4
Binary files /dev/null and b/upstream/t/data/geodb/GeoIPISP.dat differ
diff --git a/upstream/t/data/geodb/create_GeoIPCity.README b/upstream/t/data/geodb/create_GeoIPCity.README
new file mode 100644 (file)
index 0000000..0d8b122
--- /dev/null
@@ -0,0 +1,16 @@
+
+Instructions:
+
+git clone https://github.com/mteodoro/mmutils.git
+cd mmutils
+virtualenv env
+source env/bin/activate
+pip install -r requirements.txt
+
+python -c 'exec("import sys,socket\nfor ip in sys.argv[1:]:\n print ip, int(socket.inet_aton(ip).encode(\"hex\"),16)\n")' 8.8.8.8
+
+echo 'startIP,endIP,country,region,city,postalCode,latitude,longitude,metroCode,areaCode
+134744072,134744072,"US","CA","Redwood City","94063",37.4914,-122.2110,807,650' >GeoIPCity.csv
+
+python csv2dat.py -w GeoIPCity.dat mmcity GeoIPCity.csv
+
diff --git a/upstream/t/data/geodb/create_GeoIPISP.README b/upstream/t/data/geodb/create_GeoIPISP.README
new file mode 100644 (file)
index 0000000..88157b3
--- /dev/null
@@ -0,0 +1,17 @@
+
+Instructions:
+
+git clone https://github.com/mteodoro/mmutils.git
+cd mmutils
+virtualenv env
+source env/bin/activate
+pip install -r requirements.txt
+
+python -c 'exec("import sys,socket\nfor ip in sys.argv[1:]:\n print ip, int(socket.inet_aton(ip).encode(\"hex\"),16)\n")' 8.8.8.8
+
+echo 'startIP,endIP,isp
+0,0,"SpamAssassin test data"
+134744072,134744072,"Level 3 Communications"' >GeoIPISP.csv
+
+python csv2dat.py -w GeoIPISP.dat mmisp GeoIPISP.csv
+
diff --git a/upstream/t/data/geodb/create_ipcc.sh b/upstream/t/data/geodb/create_ipcc.sh
new file mode 100644 (file)
index 0000000..0caf8bb
--- /dev/null
@@ -0,0 +1,12 @@
+#!/bin/sh
+
+# IP::Country::DB_File ipcc.db
+echo '2.3|arin|1537592415823|142286|19700101|20180922|-0400
+arin|US|ipv4|8.0.0.0|8388608|19921201|allocated|e5e3b9c13678dfc483fb1f819d70883c
+arin|US|ipv6|2001:4860::|32|20050314|allocated|9d99e3f7d38d1b8026f2ebbea4017c9f' >delegated-arin
+true >delegated-ripencc
+true >delegated-afrinic
+true >delegated-apnic
+true >delegated-lacnic
+build_ipcc.pl -b -d .
+
diff --git a/upstream/t/data/geodb/ipcc.db b/upstream/t/data/geodb/ipcc.db
new file mode 100644 (file)
index 0000000..437bb0f
Binary files /dev/null and b/upstream/t/data/geodb/ipcc.db differ
diff --git a/upstream/t/data/nice.mbox b/upstream/t/data/nice.mbox
new file mode 100644 (file)
index 0000000..ab3b673
--- /dev/null
@@ -0,0 +1,217 @@
+From jm@dogma.slashnull.org  Wed May 16 00:40:34 2001
+Return-Path: <jm@dogma.slashnull.org>
+Delivered-To: jm@netnoteinc.com
+Received: from dogma.slashnull.org (dogma.slashnull.org [212.17.35.15]) by
+    mail.netnoteinc.com (Postfix) with ESMTP id 830E5115158 for
+    <jm@netnoteinc.com>; Tue, 15 May 2001 23:40:33 +0000 (Eire)
+Received: (from jm@localhost) by dogma.slashnull.org (8.9.3/8.9.3) id
+    AAA30873 for jm@netnoteinc.com; Wed, 16 May 2001 00:40:33 +0100
+Received: from trna.ximian.com ([141.154.95.22]) by dogma.slashnull.org
+    (8.9.3/8.9.3) with ESMTP id AAA30867 for <jm-ximian@jmason.org>;
+    Wed, 16 May 2001 00:40:31 +0100
+Received: from trna.ximian.com (IDENT:nobody@localhost [127.0.0.1]) by
+    trna.ximian.com (8.9.3/8.9.3) with ESMTP id SAA19408; Tue, 15 May 2001
+    18:26:07 -0400
+Received: from milkplus (62-122-4-47.flat.galactica.it [62.122.4.47]) by
+    trna.ximian.com (8.9.3/8.9.3) with ESMTP id RAA19544; Tue, 15 May 2001
+    17:31:24 -0400
+Received: by milkplus (Postfix, from userid 1000) id D3FDD10B051;
+    Tue, 15 May 2001 17:31:22 -0400 (EDT)
+From: "Ximian, Inc." <evolve@ximian.com>
+To: announce@ximian.com
+Content-Type: text/plain
+X-Mailer: Evolution/0.10 (Preview Release)
+X-Loop: just so a test passes
+Date: 15 May 2001 17:31:22 -0400
+Message-Id: <989962282.546.27.camel@milkplus>
+MIME-Version: 1.0
+Subject: [HC Announce] Ximian Evolution 0.10 "Tasmanian Devil" is Now
+    Available!
+Sender: announce-admin@helixcode.com
+Errors-To: announce-admin@helixcode.com
+X-Mailman-Version: 1.1
+Precedence: bulk
+X-Hashcash: 0:040315:test@example.com:69781c87bae95c03
+X-hashcash: 1:20:040806:test1@example.com:test=foo:482b788d12eb9b56:2a3349
+List-Id: Announcements about Ximian. <announce.helixcode.com>
+X-Beenthere: announce@helixcode.com
+X-Spam-Status: No, hits=2 required=5
+
+A new preview release of Ximian Evolution is now available.  Evolution
+is a personal and workgroup information management tool that seamlessly
+combines email, calendar, address book and more.  Its extensive network
+support lets you connect to a wide range of services.  Release 0.10
+includes a host of new features and fixes.
+
+TO GET THE EVOLUTION PREVIEW RELEASE
+
+-   For those of you using Ximian GNOME, this version can be installed
+    by subscribing to the Evolution channel in Red Carpet (System -> Get
+    Software).
+
+-   To download the preview release from the Ximian web site, go to:
+    http://www.ximian.com/apps/evolution-preview/index.php3
+
+TO GET SOURCE CODE
+
+You can also get the Evolution source tarball here:
+
+        ftp://ftp.gnome.org/pub/GNOME/unstable/sources/evolution
+
+    Evolution also requires Gal (0.7), GtkHTML (0.8.2), Bonobo (1.0),
+    OAF (0.6.2), GNOME VFS (1.0), libunicode (0.4.gnome), GNOME Print
+    (0.25) and ORBit (0.5.6).
+
+        ftp://ftp.gnome.org/pub/GNOME/unstable/sources/gal
+        ftp://ftp.gnome.org/pub/GNOME/unstable/sources/gtkhtml
+        ftp://ftp.gnome.org/pub/GNOME/unstable/sources/bonobo
+        ftp://ftp.gnome.org/pub/GNOME/unstable/sources/oaf
+        ftp://ftp.gnome.org/pub/GNOME/unstable/sources/gnome-vfs
+        ftp://ftp.gnome.org/pub/GNOME/unstable/sources/libunicode
+        ftp://ftp.gnome.org/pub/GNOME/stable/sources/gnome-print
+        ftp://ftp.gnome.org/pub/GNOME/stable/sources/ORBit
+
+TO VIEW 0.10 RELEASE NOTES
+
+The 0.10 release notes are available at:
+http://www.ximian.com/newsitems/evolution-0.10-announce.php3
+
+
+
+_______________________________________________
+Announce maillist  -  Announce@helixcode.com
+http://lists.helixcode.com/mailman/listinfo/announce
+
+
+
+From nsb@thumper.bellcore.com Tue Oct  8 10:29:39 1991
+Received: from Tomobiki-Cho.CAC.Washington. (Tomobiki-Cho.CAC.Washington.EDU) by Ikkoku-Kan.Panda.COM
+       (NeXT-1.0 (From Sendmail 5.52)/UW-NDC Revision: 2.22 ) id AA12299; Tue, 8 Oct 91 07:29:39 PDT
+Return-Path: <nsb@thumper.bellcore.com>
+Received: from thumper.bellcore.com by Tomobiki-Cho.CAC.Washington.EDU
+       (NeXT-1.0 (From Sendmail 5.52)/UW-NDC Revision: 1.60.MRC ) id AA27545; Tue, 8 Oct 91 07:28:25 PDT
+Received: from greenbush.bellcore.com by thumper.bellcore.com (4.1/4.7)
+       id <AA08355> for mrc@panda.com; Tue, 8 Oct 91 10:25:41 EDT
+Received: by greenbush.bellcore.com (4.1/4.7)
+       id <AA00616> for mrc@panda.com; Tue, 8 Oct 91 10:25:36 EDT
+Date: Tue, 8 Oct 91 10:25:36 EDT
+From: nsb@thumper.bellcore.com (Nathaniel Borenstein)
+Message-Id: <9110081425.AA00616@greenbush.bellcore.com>
+To: mrc@panda.com
+Subject:  Re: multipart mail
+MIME-Version: 1.0
+Content-Type: audio/basic
+Content-Transfer-Encoding: base64
+Content-Description: Hi Mark
+Status: RO
+
++Pv/d3RydWlnbGtnYWJiaWxqaWxucHrx7erk7X709Pfx6eXo5uXe3Ojw+Px1cXZra2dsbGdr
+
+From julliard@winehq.com Wed Sep  4 19:07:34 2002
+Received: (qmail 27859 invoked by uid 1001); 5 Sep 2002 02:25:41 -0000
+Received: from pop.pi.sbcglobal.net [207.115.63.84]
+       by localhost with POP3 (fetchmail-5.9.11 polling pop.sbcglobal.net account matt_relay)
+       for matt@localhost (single-drop); Wed, 04 Sep 2002 19:25:41 -0700 (PDT)
+Received: from vm4-ext.prodigy.net by vm4 with SMTP; Wed,  4 Sep 2002 22:26:20 -0400
+X-Originating-IP: [64.49.198.145]
+Received: from mail1.mailwizards.com (mail1.mailwizards.com [64.49.198.145])
+       by vm4-ext.prodigy.net (8.12.3 da nor stuldap/8.12.3) with ESMTP id g852QJix196066
+       for <matt_relay@sbcglobal.net>; Wed, 4 Sep 2002 22:26:19 -0400
+Received: from wine.codeweavers.com (wine.codeweavers.com [198.144.4.3])
+       by mail1.mailwizards.com (8.11.4/MW-2.03) with ESMTP id g852QIu06714
+       for <matt@nightrealms.com>; Wed, 4 Sep 2002 21:26:18 -0500 (CDT)
+Received: from localhost.localdomain (wine [127.0.0.1])
+       by wine.codeweavers.com (8.11.6/8.11.6) with ESMTP id g852ClF25431;
+       Wed, 4 Sep 2002 21:12:47 -0500
+Received: from mail.wine.dyndns.org (12-235-88-76.client.attbi.com [12.235.88.76])
+       by wine.codeweavers.com (8.11.6/8.11.6) with ESMTP id g8527bF25126
+       for <wine-announce@winehq.com>; Wed, 4 Sep 2002 21:07:37 -0500
+Received: from mail.wine.dyndns.org (julliard@localhost [127.0.0.1])
+       by mail.wine.dyndns.org (8.12.3/8.12.3/Debian -4) with ESMTP id g8527Z0a029784
+       for <wine-announce@winehq.com>; Wed, 4 Sep 2002 19:07:35 -0700
+Received: (from julliard@localhost)
+       by mail.wine.dyndns.org (8.12.3/8.12.3/Debian -4) id g8527Zmq029780;
+       Wed, 4 Sep 2002 19:07:35 -0700
+To: wine-announce@winehq.com
+Subject: Wine release 20020904
+From: Alexandre Julliard <julliard@winehq.com>
+Message-ID: <87elc9xk7t.fsf@mail.wine.dyndns.org>
+Lines: 48
+User-Agent: Gnus/5.0808 (Gnus v5.8.8) XEmacs/21.4 (Common Lisp)
+MIME-Version: 1.0
+Content-Type: text/plain;
+  charset=us-ascii
+Sender: wine-announce-admin@winehq.com
+Errors-To: wine-announce-admin@winehq.com
+X-BeenThere: wine-announce@winehq.com
+X-Mailman-Version: 2.0
+Precedence: bulk
+Reply-To: wine-devel@winehq.com
+List-Help: <mailto:wine-announce-request@winehq.com?subject=help>
+List-Post: <mailto:wine-announce@winehq.com>
+List-Subscribe: <http://www.winehq.com/mailman/listinfo/wine-announce>,
+       <mailto:wine-announce-request@winehq.com?subject=subscribe>
+List-Id: Wine Announcements <wine-announce.winehq.com>
+List-Unsubscribe: <http://www.winehq.com/mailman/listinfo/wine-announce>,
+       <mailto:wine-announce-request@winehq.com?subject=unsubscribe>
+List-Archive: <http://www.winehq.com/hypermail/wine-announce/>
+Date: 04 Sep 2002 19:07:34 -0700
+X-DCC-wanadoo-be-Metrics: kagome 1016; Body=1 Fuz1=1 Fuz2=1
+X-Spam-Status: No, hits=-2.9 required=5.0
+       tests=KNOWN_MAILING_LIST,SPAM_PHRASE_03_05,SUBJ_HAS_UNIQ_ID,
+             USER_AGENT
+       version=2.50-cvs
+X-Spam-Level: 
+Status: R 
+X-Status: N
+
+This is release 20020904 of Wine, a free implementation of Windows on
+Unix.  This is still a developers only release.  There are many bugs
+and unimplemented features.  Most applications still do not work
+correctly.
+
+Patches should be submitted to "wine-patches@winehq.com".  Please don't
+forget to include a ChangeLog entry.
+
+WHAT'S NEW with Wine-20020904: (see ChangeLog for details)
+       - Much improved PowerPC support.
+       - More correct locale definitions.
+       - Progress on the conversion of handle types to pointers.
+       - Many Visio and Quicken fixes merged from Crossover.
+       - Lots of bug fixes.
+
+See the README file in the distribution for installation instructions.
+
+Because of lags created by using mirror, this message may reach you before
+the release is available at the ftp sites.  The sources will be available
+from the following locations:
+
+  http://www.ibiblio.org/pub/Linux/ALPHA/wine/development/Wine-20020904.tar.gz
+  ftp://ftp.infomagic.com/pub/mirrors/linux/sunsite/ALPHA/wine/development/Wine-20020904.tar.gz
+  ftp://ftp.fu-berlin.de/unix/linux/mirrors/sunsite.unc.edu/ALPHA/wine/development/Wine-20020904.tar.gz
+  ftp://orcus.progsoc.uts.edu.au/pub/Wine/development/Wine-20020904.tar.gz
+
+It should also be available from any other site that mirrors ibiblio.org.
+For more download locations, see http://ftpsearch.lycos.com. These
+locations also hold pre-built documentation packages in various
+formats: wine-doc-html.tar.gz, wine-doc-txt.tar.gz, wine-doc.pdf.gz
+and wine-doc.ps.gz.
+
+You can also get the current source directly from the CVS tree. Check
+http://www.winehq.com/development/ for details.
+
+If you submitted a patch, please check to make sure it has been
+included in the new release.
+
+If you want to receive by mail a patch against the previous release
+when a new one is released, you can subscribe to the mailing list at
+http://tiger.informatik.hu-berlin.de/cgi-bin/mailman/listinfo/wine-patches.
+
+Wine is available thanks to the work of many people. See the file
+AUTHORS in the distribution for the complete list.
+
+--
+Alexandre Julliard
+julliard@winehq.com
+
+
diff --git a/upstream/t/data/nice/authres b/upstream/t/data/nice/authres
new file mode 100644 (file)
index 0000000..2f950b0
--- /dev/null
@@ -0,0 +1,99 @@
+From jm@dogma.slashnull.org  Wed May 16 00:40:34 2001
+Return-Path: <jm@dogma.slashnull.org>
+Delivered-To: jm@netnoteinc.com
+Received: from dogma.slashnull.org (dogma.slashnull.org [212.17.35.15]) by
+    mail.netnoteinc.com (Postfix) with ESMTP id 830E5115158 for
+    <jm@netnoteinc.com>; Tue, 15 May 2001 23:40:33 +0000 (Eire)
+Authentication-Results: authrestest1int;
+    spf=pass smtp.mailfrom=bounce.example.org;
+    dkim=pass header.i=@example.org;
+    dkim=fail header.i=@another.signing.domain.example
+Authentication-Results: authrestest2int; dmarc=none (p=none dis=none) header.from=ximian.com
+authentication-Results: authrestest3int; spf=fail smtp.mailfrom=dogma.slashnull.org
+Authentication-Results: authrestest4int 2; spf=fail reason (BAH) = "just some (<>.%&! reason" smtp.mailfrom=dogma.slashnull.org
+Authentication-RESULTS:  authrestest5int;
+        dkim=pass (2048-bit key; unprotected) header.d=gmail.com header.i=@gmail.com header.b=SI+iqkld;
+Authentication-Results: authrestest6int;
+        dkim=fail (2048-bit key; unprotected) header.d=gmail.com header.i=" foo bar \"xyz\"@gmail.com" header.b=SI+iqkld;
+Received: (from jm@localhost) by dogma.slashnull.org (8.9.3/8.9.3) id
+    AAA30873 for jm@netnoteinc.com; Wed, 16 May 2001 00:40:33 +0100
+Received: from trna.ximian.com ([141.154.95.22]) by dogma.slashnull.org
+    (8.9.3/8.9.3) with ESMTP id AAA30867 for <jm-ximian@jmason.org>;
+    Wed, 16 May 2001 00:40:31 +0100
+Authentication-Results: authrestest7tru; spf=fail smtp.mailfrom=last.trusted@example.com
+Received: from trna.ximian.com (IDENT:nobody@localhost [127.0.0.1]) by
+    trna.ximian.com (8.9.3/8.9.3) with ESMTP id SAA19408; Tue, 15 May 2001
+    18:26:07 -0400
+Received: from milkplus (62-122-4-47.flat.galactica.it [62.122.4.47]) by
+    trna.ximian.com (8.9.3/8.9.3) with ESMTP id RAA19544; Tue, 15 May 2001
+    17:31:24 -0400
+Received: by milkplus (Postfix, from userid 1000) id D3FDD10B051;
+    Tue, 15 May 2001 17:31:22 -0400 (EDT)
+Authentication-Results: authrestest8ext; spf=pass smtp.mailfrom=untrusted@example.com
+From: "Ximian, Inc." <evolve@ximian.com>
+To: announce@ximian.com
+Content-Type: text/plain
+X-Mailer: Evolution/0.10 (Preview Release)
+X-Loop: just so a test passes
+Date: 15 May 2001 17:31:22 -0400
+Message-Id: <989962282.546.27.camel@milkplus>
+MIME-Version: 1.0
+Subject: [HC Announce] Ximian Evolution 0.10 "Tasmanian Devil" is Now
+    Available!
+Sender: announce-admin@helixcode.com
+Errors-To: announce-admin@helixcode.com
+X-Mailman-Version: 1.1
+Precedence: bulk
+X-Hashcash: 0:040315:test@example.com:69781c87bae95c03
+X-hashcash: 1:20:040806:test1@example.com:test=foo:482b788d12eb9b56:2a3349
+List-Id: Announcements about Ximian. <announce.helixcode.com>
+X-Beenthere: announce@helixcode.com
+X-Spam-Status: No, hits=2 required=5
+
+A new preview release of Ximian Evolution is now available.  Evolution
+is a personal and workgroup information management tool that seamlessly
+combines email, calendar, address book and more.  Its extensive network
+support lets you connect to a wide range of services.  Release 0.10
+includes a host of new features and fixes.
+
+TO GET THE EVOLUTION PREVIEW RELEASE
+
+-   For those of you using Ximian GNOME, this version can be installed
+    by subscribing to the Evolution channel in Red Carpet (System -> Get
+    Software).
+
+-   To download the preview release from the Ximian web site, go to:
+    http://www.ximian.com/apps/evolution-preview/index.php3
+
+TO GET SOURCE CODE
+
+You can also get the Evolution source tarball here:
+
+        ftp://ftp.gnome.org/pub/GNOME/unstable/sources/evolution
+
+    Evolution also requires Gal (0.7), GtkHTML (0.8.2), Bonobo (1.0),
+    OAF (0.6.2), GNOME VFS (1.0), libunicode (0.4.gnome), GNOME Print
+    (0.25) and ORBit (0.5.6).
+
+        ftp://ftp.gnome.org/pub/GNOME/unstable/sources/gal
+        ftp://ftp.gnome.org/pub/GNOME/unstable/sources/gtkhtml
+        ftp://ftp.gnome.org/pub/GNOME/unstable/sources/bonobo
+        ftp://ftp.gnome.org/pub/GNOME/unstable/sources/oaf
+        ftp://ftp.gnome.org/pub/GNOME/unstable/sources/gnome-vfs
+        ftp://ftp.gnome.org/pub/GNOME/unstable/sources/libunicode
+        ftp://ftp.gnome.org/pub/GNOME/stable/sources/gnome-print
+        ftp://ftp.gnome.org/pub/GNOME/stable/sources/ORBit
+
+TO VIEW 0.10 RELEASE NOTES
+
+The 0.10 release notes are available at:
+http://www.ximian.com/newsitems/evolution-0.10-announce.php3
+
+
+
+_______________________________________________
+Announce maillist  -  Announce@helixcode.com
+http://lists.helixcode.com/mailman/listinfo/announce
+
+
+
diff --git a/upstream/t/data/nice/dmarc/noneok.eml b/upstream/t/data/nice/dmarc/noneok.eml
new file mode 100644 (file)
index 0000000..965a921
--- /dev/null
@@ -0,0 +1,23 @@
+Return-Path: <test@dmarc1.spamassassin.org>
+Received: from dmarc1.spamassassin.org (dmarc1.spamassassin.org [64.142.3.173])
+       by dmarc1.spamassassin.org (8.14.9/8.14.9) with ESMTP id 13DFe22R006047
+       (version=TLSv1/SSLv3 cipher=AES256-GCM-SHA384 bits=256 verify=NO);
+       Tue, 13 Apr 2021 11:40:02 -0400
+DKIM-Signature: v=1; a=rsa-sha1; c=relaxed/simple; d=
+        dmarc1.spamassassin.org; h=from:to:subject:message-id:date
+        :mime-version:content-type; s=selector1; bh=AaYQYg3XpWgPk5P7okfW
+        fbeh0V4=; b=jxH2H2N0++4M6VJ/tebY0f+GOMteGMsjcHkP1S+oTE8657JmosdR
+        S1VXtcJ5CwrXNTxe+9oW1bdU6QVL8fe7I1i2fXIEShaw6js+l5ymbvWts8o9kHZH
+        Jv8+ZfwaSkmr6onD679oTxBFGOT0PkI33kQOoZnVQY9xF73vZXEA7NoWg0rmaGcT
+        up8zinkgQV6BhdqJGzzi3je4QOdDgVmp1Pj42aaliurC0HlFZT/xAF0OZKVzwm3I
+        J2dlpC84zKIlqa9vGnx16N1wIyA+/GnpJ13s4hg9N7PrAi7iotanBh0W+v/ujLnr
+        MTjXJ11pMzp8xvoXtt3c+Ptxbf7TW4BxKA==
+From: SpamAssassin Test <test@dmarc1.spamassassin.org>
+To: undisclosed-recipients:;
+Subject: test message 1
+Message-ID: <4A294538.10002@dmarc1.spamassassin.org>
+Date: Sun, 11 Apr 2021 02:00:04 +0000
+MIME-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+
+test message
diff --git a/upstream/t/data/nice/dmarc/quarok.eml b/upstream/t/data/nice/dmarc/quarok.eml
new file mode 100644 (file)
index 0000000..7075137
--- /dev/null
@@ -0,0 +1,23 @@
+Return-Path: <test@dmarc2.spamassassin.org>
+Received: from dmarc2.spamassassin.org (dmarc2.spamassassin.org [64.142.3.173])
+       by dmarc2.spamassassin.org (8.14.9/8.14.9) with ESMTP id 13DFe22R006047
+       (version=TLSv1/SSLv3 cipher=AES256-GCM-SHA384 bits=256 verify=NO);
+       Tue, 13 Apr 2021 11:40:02 -0400
+DKIM-Signature: v=1; a=rsa-sha1; c=relaxed/simple; d=
+        dmarc2.spamassassin.org; h=from:to:subject:message-id:date
+        :mime-version:content-type; s=selector1; bh=AaYQYg3XpWgPk5P7okfW
+        fbeh0V4=; b=2T58S24T2pMH5xPY2FB3YYH9qvdyXg6KZUIhNnj0bHFkmLbZWsYN
+        lMdfQojRifSwD28tN8tljiKE9tdwNJeWj8sy6hGzvw5ksGjvAHjb46ZifWi9oD+7
+        2ddAvVgKSV/wtVhg5dZCimNdDq3irKOQ881mPHuzdcXxEsNxYJUMR/989HTvdYLA
+        eJAcT00hum1LdL+wdxZiG/JyC0G5mThARi/b3KdC7MV8DukO3pSRJjWsIgnEJNna
+        +F7YCCJbp6Rm32HyUayYbov3ZMZ6MFzN9sYUkej/gUTl8LMacW3ibCcleeVD4oKN
+        ksOe+bE0CAevco5lyqSEjSAXigBAGoV2ow==
+From: SpamAssassin Test <test@dmarc2.spamassassin.org>
+To: undisclosed-recipients:;
+Subject: test message 1
+Message-ID: <4A294538.10002@dmarc2.spamassassin.org>
+Date: Sun, 11 Apr 2021 02:00:04 +0000
+MIME-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+
+test message
diff --git a/upstream/t/data/nice/dmarc/rejectok.eml b/upstream/t/data/nice/dmarc/rejectok.eml
new file mode 100644 (file)
index 0000000..abf3603
--- /dev/null
@@ -0,0 +1,23 @@
+Return-Path: <test@dmarc3.spamassassin.org>
+Received: from dmarc3.spamassassin.org (dmarc3.spamassassin.org [64.142.3.173])
+       by dmarc3.spamassassin.org (8.14.9/8.14.9) with ESMTP id 13DFe22R006047
+       (version=TLSv1/SSLv3 cipher=AES256-GCM-SHA384 bits=256 verify=NO);
+       Tue, 13 Apr 2021 11:40:02 -0400
+DKIM-Signature: v=1; a=rsa-sha1; c=relaxed/simple; d=
+        dmarc3.spamassassin.org; h=from:to:subject:message-id:date
+        :mime-version:content-type; s=selector1; bh=AaYQYg3XpWgPk5P7okfW
+        fbeh0V4=; b=SfeRUmdB+35RaFj+etCrogC358LU4jiyF7Oa7Qsp+kp3rV++gfSG
+        6NKWuGAAbY+sA3M1m8KBXZXavPzmLRcZaorgVuHdmnsF+/5Fzmz6DBOSKhcM54p2
+        1CfeiJAz0Rcudbxq9c3OJYlu1iSXDw1YwflRDgWv+Sed9T0jWmti1//N66NTZEKc
+        2O6EyI6KuBPUvRHRD04GBCAUweiM9HR4rVIDA9H7HFFPlVfB6Gm6iNhHy1tsuDSJ
+        +1wMJcojXrdRje8QC6bIyQLsY7/H4X0tUbjXNHhC4d2oA0WQQ7mvVGWhtFQFDfIx
+        G30/NRr8NICgPhjp91rAIXU9dKgdohdnWg==
+From: SpamAssassin Test <test@dmarc3.spamassassin.org>
+To: undisclosed-recipients:;
+Subject: test message 1
+Message-ID: <4A294538.10002@dmarc3.spamassassin.org>
+Date: Sun, 11 Apr 2021 02:00:04 +0000
+MIME-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+
+test message
diff --git a/upstream/t/data/nice/dmarc/strictrejectok.eml b/upstream/t/data/nice/dmarc/strictrejectok.eml
new file mode 100644 (file)
index 0000000..f1edb89
--- /dev/null
@@ -0,0 +1,23 @@
+Return-Path: <test@dmarc4.spamassassin.org>
+Received: from dmarc4.spamassassin.org (dmarc4.spamassassin.org [64.142.3.173])
+       by dmarc4.spamassassin.org (8.14.9/8.14.9) with ESMTP id 13DFe22R006047
+       (version=TLSv1/SSLv3 cipher=AES256-GCM-SHA384 bits=256 verify=NO);
+       Tue, 13 Apr 2021 11:40:02 -0400
+DKIM-Signature: v=1; a=rsa-sha1; c=relaxed/simple; d=
+        dmarc4.spamassassin.org; h=from:to:subject:message-id:date
+        :mime-version:content-type; s=selector1; bh=AaYQYg3XpWgPk5P7okfW
+        fbeh0V4=; b=0KPynqQB29CX6l5kp6+/x/HT7iTSC1G/u6yZJP0n/JpoqPVOvmbJ
+        l3/U36gxCHPxz1D7dFWBgU2chDkAlTcz/+TkKF4jcta8pPsLTsbaJH6egS0krT+4
+        ydMeck2W98pj2zgh2yz25VqAP418y0EP/1QqlSDckjUayVRz3xakPVGX8fp4iIB4
+        lQ08239wyqHua0mJxjQuqi/Xr6qDxaPPJOs/U9+ToKrLlKuLw0LC2VGlzsttTt5z
+        8OBBIGfda0srAASmuwpyeilyFieaMAnpBuI1RAW0H5Ol8mqUjy8rjNKvoIPfZSUN
+        hSDpuUAc9u58kcZZ0TSmodXazPYne4JlLw=
+From: SpamAssassin Test <test@dmarc4.spamassassin.org>
+To: undisclosed-recipients:;
+Subject: test message 1
+Message-ID: <4A294538.10002@dmarc4.spamassassin.org>
+Date: Sun, 11 Apr 2021 02:00:04 +0000
+MIME-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+
+test message
index 796cec52a886a46ed0359bfc579dd27ddfabe1a5..5b1563d9c89e6b3a0c048e0a099f85d25fdafe2a 100644 (file)
@@ -1,4 +1,5 @@
-Received: from 1.2.3.4 by probeer.bokxing.nl
+Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org
+  [64.142.3.173]) by probeer.bokxing.nl
   (probeer.alt001.com [87.253.148.98]) with ESMTP id YN8t6r6y41Ly
   for <rolek@example.nl>; Mon, 11 Oct 2010 14:21:26 +0200 (CEST)
 X-Originating-Ip: [198.51.100.1]
index 0b5463c023220605780f2d271d321dc006adeccc..142bb1490f91b5bbd4c172eb231ea062b4527578 100644 (file)
@@ -1,7 +1,11 @@
 Return-Path: <newsalerts-noreply@dnsbltest.spamassassin.org>
-Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org [64.142.3.173]) by amgod.boxhost.net (Postfix) with SMTP id B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 18:18:49 +0000 (GMT)
-Received: by proxy.google.com with SMTP id so1951389 for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 10:14:01 -0800 (PST)
-Received: by abbulk2 with SMTP id mr733125; Tue, 10 Feb 2004 10:14:01 -0800 (PST)
+Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org
+ [64.142.3.173]) by amgod.boxhost.net (Postfix) with SMTP id B9B2931016D for
+ <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 18:18:49 +0000 (GMT)
+Received: by proxy.google.com with SMTP id so1951389 for
+ <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 10:14:01 -0800 (PST)
+Received: by abbulk2 with SMTP id mr733125; Tue,
+ 10 Feb 2004 10:14:01 -0800 (PST)
 Message-ID: <1076436841.67074.8fa05ccdc458abe5.1446041b@persist.google.com>
 Date: Tue, 10 Feb 2004 10:14:01 -0800 (PST)
 From: newsalerts-noreply@dnsbltest.spamassassin.org
index 688899dbf29a8dc2d6ffe212919400f191da341a..e87df1cdadde8f6414328502f0f9372c42079889 100644 (file)
@@ -1,8 +1,15 @@
 Return-Path: <newsalerts-noreply@dnsbltest.spamassassin.org>
-Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org [65.214.43.157]) by dnsbltest.spamassassin.org (Postfix) with SMTP id B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 18:18:49 +0000 (GMT)
-Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org [64.142.3.173]) by amgod.boxhost.net (Postfix) with SMTP id B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 18:18:49 +0000 (GMT)
-Received: by proxy.google.com with SMTP id so1951389 for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 10:14:01 -0800 (PST)
-Received: by abbulk2 with SMTP id mr733125; Tue, 10 Feb 2004 10:14:01 -0800 (PST)
+Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org
+ [65.214.43.157]) by dnsbltest.spamassassin.org (Postfix) with SMTP id
+ B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue,
+ 10 Feb 2004 18:18:49 +0000 (GMT)
+Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org
+ [64.142.3.173]) by amgod.boxhost.net (Postfix) with SMTP id B9B2931016D for
+ <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 18:18:49 +0000 (GMT)
+Received: by proxy.google.com with SMTP id so1951389 for
+ <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 10:14:01 -0800 (PST)
+Received: by abbulk2 with SMTP id mr733125; Tue,
+ 10 Feb 2004 10:14:01 -0800 (PST)
 Message-ID: <1076436841.67074.8fa05ccdc458abe5.1446041b@persist.google.com>
 Date: Tue, 10 Feb 2004 10:14:01 -0800 (PST)
 From: newsalerts-noreply@dnsbltest.spamassassin.org
index 422a6ea776958b8e77c30e6566f975177c178a39..a4a162f47dff85f0d2cd9e7a589727039ad6bb10 100644 (file)
@@ -1,11 +1,27 @@
 Return-Path: <newsalerts-noreply@dnsbltest.spamassassin.org>
-Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org [65.214.43.158]) by dnsbltest.spamassassin.org (Postfix) with SMTP id B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 18:18:49 +0000 (GMT)
-Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org [64.142.3.173]) by dnsbltest.spamassassin.org (Postfix) with SMTP id B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 18:18:49 +0000 (GMT)
-Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org [65.214.43.155]) by dnsbltest.spamassassin.org (Postfix) with SMTP id B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 18:18:49 +0000 (GMT)
-Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org [65.214.43.156]) by dnsbltest.spamassassin.org (Postfix) with SMTP id B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 18:18:49 +0000 (GMT)
-Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org [65.214.43.157]) by amgod.boxhost.net (Postfix) with SMTP id B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 18:18:49 +0000 (GMT)
-Received: by proxy.google.com with SMTP id so1951389 for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 10:14:01 -0800 (PST)
-Received: by abbulk2 with SMTP id mr733125; Tue, 10 Feb 2004 10:14:01 -0800 (PST)
+Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org
+ [65.214.43.158]) by dnsbltest.spamassassin.org (Postfix) with SMTP id
+ B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue,
+ 10 Feb 2004 18:18:49 +0000 (GMT)
+Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org
+ [64.142.3.173]) by dnsbltest.spamassassin.org (Postfix) with SMTP id
+ B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue,
+ 10 Feb 2004 18:18:49 +0000 (GMT)
+Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org
+ [65.214.43.155]) by dnsbltest.spamassassin.org (Postfix) with SMTP id
+ B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue,
+ 10 Feb 2004 18:18:49 +0000 (GMT)
+Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org
+ [65.214.43.156]) by dnsbltest.spamassassin.org (Postfix) with SMTP id
+ B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue,
+ 10 Feb 2004 18:18:49 +0000 (GMT)
+Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org
+ [65.214.43.157]) by amgod.boxhost.net (Postfix) with SMTP id B9B2931016D for
+ <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 18:18:49 +0000 (GMT)
+Received: by proxy.google.com with SMTP id so1951389 for
+ <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 10:14:01 -0800 (PST)
+Received: by abbulk2 with SMTP id mr733125; Tue,
+ 10 Feb 2004 10:14:01 -0800 (PST)
 Message-ID: <1076436841.67074.8fa05ccdc458abe5.1446041b@persist.google.com>
 Date: Tue, 10 Feb 2004 10:14:01 -0800 (PST)
 From: newsalerts-noreply@dnsbltest.spamassassin.org
index 5ae7c9537d49e23c833d1c72e2f0e1b445f538ea..08b1cedc5fece62df69fcf87e4635103e9ead4a6 100644 (file)
@@ -1,15 +1,39 @@
 Return-Path: <newsalerts-noreply@dnsbltest.spamassassin.org>
-X-Comment: Yeah, the Received-SPF headers make no sense, there just there to test that the SPF plugin will parse the results from them... the IPs and comments are bogus
-Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org [65.214.43.158]) by dnsbltest.spamassassin.org (Postfix) with SMTP id B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 18:18:49 +0000 (GMT)
-Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org [64.142.3.173]) by dnsbltest.spamassassin.org (Postfix) with SMTP id B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 18:18:49 +0000 (GMT)
-Received-SPF: fail (dostech.ca: 69.61.78.188 is authorized to use 'spamassassin@dostech.ca' in 'mfrom' identity (mechanism 'mx' matched)) receiver=FC5-VPC; identity=mfrom; envelope-from="spamassassin@dostech.ca"; helo=smtp.dostech.net; client-ip=69.61.78.188
-Received-SPF: softfail (dostech.ca: 69.61.78.188 is authorized to use 'dostech.ca' in 'helo' identity (mechanism 'mx' matched)) receiver=FC5-VPC; identity=helo; helo=dostech.ca; client-ip=69.61.78.188
-Received-SPF: neutral (herse.apache.org: domain of spamassassin@dostech.ca designates 69.61.78.188 as permitted sender)
-Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org [65.214.43.155]) by dnsbltest.spamassassin.org (Postfix) with SMTP id B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 18:18:49 +0000 (GMT)
-Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org [65.214.43.156]) by dnsbltest.spamassassin.org (Postfix) with SMTP id B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 18:18:49 +0000 (GMT)
-Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org [65.214.43.157]) by amgod.boxhost.net (Postfix) with SMTP id B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 18:18:49 +0000 (GMT)
-Received: by proxy.google.com with SMTP id so1951389 for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 10:14:01 -0800 (PST)
-Received: by abbulk2 with SMTP id mr733125; Tue, 10 Feb 2004 10:14:01 -0800 (PST)
+X-Comment: Yeah, the Received-SPF headers make no sense,
+ there just there to test that the SPF plugin will parse the results from
+ them... the IPs and comments are bogus
+Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org
+ [65.214.43.158]) by dnsbltest.spamassassin.org (Postfix) with SMTP id
+ B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue,
+ 10 Feb 2004 18:18:49 +0000 (GMT)
+Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org
+ [64.142.3.173]) by dnsbltest.spamassassin.org (Postfix) with SMTP id
+ B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue,
+ 10 Feb 2004 18:18:49 +0000 (GMT)
+Received-SPF: fail (dostech.ca: 69.61.78.188 is authorized to use
+ 'spamassassin@dostech.ca' in 'mfrom' identity (mechanism 'mx' matched))
+ receiver=FC5-VPC; identity=mfrom; envelope-from="spamassassin@dostech.ca";
+ helo=smtp.dostech.net; client-ip=69.61.78.188
+Received-SPF: softfail (dostech.ca: 69.61.78.188 is authorized to use
+ 'dostech.ca' in 'helo' identity (mechanism 'mx' matched)) receiver=FC5-VPC;
+ identity=helo; helo=dostech.ca; client-ip=69.61.78.188
+Received-SPF: neutral (herse.apache.org: domain of spamassassin@dostech.ca
+ designates 69.61.78.188 as permitted sender)
+Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org
+ [65.214.43.155]) by dnsbltest.spamassassin.org (Postfix) with SMTP id
+ B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue,
+ 10 Feb 2004 18:18:49 +0000 (GMT)
+Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org
+ [65.214.43.156]) by dnsbltest.spamassassin.org (Postfix) with SMTP id
+ B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue,
+ 10 Feb 2004 18:18:49 +0000 (GMT)
+Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org
+ [65.214.43.157]) by amgod.boxhost.net (Postfix) with SMTP id B9B2931016D for
+ <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 18:18:49 +0000 (GMT)
+Received: by proxy.google.com with SMTP id so1951389 for
+ <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 10:14:01 -0800 (PST)
+Received: by abbulk2 with SMTP id mr733125; Tue,
+ 10 Feb 2004 10:14:01 -0800 (PST)
 Message-ID: <1076436841.67074.8fa05ccdc458abe5.1446041b@persist.google.com>
 Date: Tue, 10 Feb 2004 10:14:01 -0800 (PST)
 From: newsalerts-noreply@dnsbltest.spamassassin.org
diff --git a/upstream/t/data/nice/spf4-received-spf-nofold b/upstream/t/data/nice/spf4-received-spf-nofold
new file mode 100644 (file)
index 0000000..5ae7c95
--- /dev/null
@@ -0,0 +1,32 @@
+Return-Path: <newsalerts-noreply@dnsbltest.spamassassin.org>
+X-Comment: Yeah, the Received-SPF headers make no sense, there just there to test that the SPF plugin will parse the results from them... the IPs and comments are bogus
+Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org [65.214.43.158]) by dnsbltest.spamassassin.org (Postfix) with SMTP id B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 18:18:49 +0000 (GMT)
+Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org [64.142.3.173]) by dnsbltest.spamassassin.org (Postfix) with SMTP id B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 18:18:49 +0000 (GMT)
+Received-SPF: fail (dostech.ca: 69.61.78.188 is authorized to use 'spamassassin@dostech.ca' in 'mfrom' identity (mechanism 'mx' matched)) receiver=FC5-VPC; identity=mfrom; envelope-from="spamassassin@dostech.ca"; helo=smtp.dostech.net; client-ip=69.61.78.188
+Received-SPF: softfail (dostech.ca: 69.61.78.188 is authorized to use 'dostech.ca' in 'helo' identity (mechanism 'mx' matched)) receiver=FC5-VPC; identity=helo; helo=dostech.ca; client-ip=69.61.78.188
+Received-SPF: neutral (herse.apache.org: domain of spamassassin@dostech.ca designates 69.61.78.188 as permitted sender)
+Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org [65.214.43.155]) by dnsbltest.spamassassin.org (Postfix) with SMTP id B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 18:18:49 +0000 (GMT)
+Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org [65.214.43.156]) by dnsbltest.spamassassin.org (Postfix) with SMTP id B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 18:18:49 +0000 (GMT)
+Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org [65.214.43.157]) by amgod.boxhost.net (Postfix) with SMTP id B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 18:18:49 +0000 (GMT)
+Received: by proxy.google.com with SMTP id so1951389 for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 10:14:01 -0800 (PST)
+Received: by abbulk2 with SMTP id mr733125; Tue, 10 Feb 2004 10:14:01 -0800 (PST)
+Message-ID: <1076436841.67074.8fa05ccdc458abe5.1446041b@persist.google.com>
+Date: Tue, 10 Feb 2004 10:14:01 -0800 (PST)
+From: newsalerts-noreply@dnsbltest.spamassassin.org
+To: jm-google-news-alerts@jmason.org
+Subject: Google News Alert - spamassassin
+MIME-Version: 1.0
+Content-Type: text/plain; charset="ISO-8859-1";
+
+SWSOFT Unveils Plesk 7, Deployed by 1&1
+Web Host Industry Review - USA
+... The software also features a newly designed Windows XP-like user interface,
+is equipped SpamAssassin, an open source anti-spam tool, and includes
+"Application ...
+<http://thewhir.com/marketwatch/sws021004.cfm>
+See all stories on this topic:
+<http://news.google.com/news?hl=en&lr=&ie=UTF-8&oe=utf8&client=google&num=30&newsc
+lusterurl=http://thewhir.com/marketwatch/sws021004.cfm>
+
+"v=spf1 ip4:64.142.3.173 -ip4:65.214.43.155 ~ip4:65.214.43.156 ?ip4:65.214.43.157 -all"
+
diff --git a/upstream/t/data/nice/spf5-received-spf-crlf b/upstream/t/data/nice/spf5-received-spf-crlf
new file mode 100644 (file)
index 0000000..99bd882
--- /dev/null
@@ -0,0 +1,56 @@
+Return-Path: <newsalerts-noreply@dnsbltest.spamassassin.org>\r
+X-Comment: Yeah, the Received-SPF headers make no sense,\r
+ there just there to test that the SPF plugin will parse the results from\r
+ them... the IPs and comments are bogus\r
+Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org\r
+ [65.214.43.158]) by dnsbltest.spamassassin.org (Postfix) with SMTP id\r
+ B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue,\r
+ 10 Feb 2004 18:18:49 +0000 (GMT)\r
+Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org\r
+ [64.142.3.173]) by dnsbltest.spamassassin.org (Postfix) with SMTP id\r
+ B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue,\r
+ 10 Feb 2004 18:18:49 +0000 (GMT)\r
+Received-SPF: fail (dostech.ca: 69.61.78.188 is authorized to use\r
+ 'spamassassin@dostech.ca' in 'mfrom' identity (mechanism 'mx' matched))\r
+ receiver=FC5-VPC; identity=mfrom; envelope-from="spamassassin@dostech.ca";\r
+ helo=smtp.dostech.net; client-ip=69.61.78.188\r
+Received-SPF: softfail (dostech.ca: 69.61.78.188 is authorized to use\r
+ 'dostech.ca' in 'helo' identity (mechanism 'mx' matched)) receiver=FC5-VPC;\r
+ identity=helo; helo=dostech.ca; client-ip=69.61.78.188\r
+Received-SPF: neutral (herse.apache.org: domain of spamassassin@dostech.ca\r
+ designates 69.61.78.188 as permitted sender)\r
+Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org\r
+ [65.214.43.155]) by dnsbltest.spamassassin.org (Postfix) with SMTP id\r
+ B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue,\r
+ 10 Feb 2004 18:18:49 +0000 (GMT)\r
+Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org\r
+ [65.214.43.156]) by dnsbltest.spamassassin.org (Postfix) with SMTP id\r
+ B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue,\r
+ 10 Feb 2004 18:18:49 +0000 (GMT)\r
+Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org\r
+ [65.214.43.157]) by amgod.boxhost.net (Postfix) with SMTP id B9B2931016D for\r
+ <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 18:18:49 +0000 (GMT)\r
+Received: by proxy.google.com with SMTP id so1951389 for\r
+ <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 10:14:01 -0800 (PST)\r
+Received: by abbulk2 with SMTP id mr733125; Tue,\r
+ 10 Feb 2004 10:14:01 -0800 (PST)\r
+Message-ID: <1076436841.67074.8fa05ccdc458abe5.1446041b@persist.google.com>\r
+Date: Tue, 10 Feb 2004 10:14:01 -0800 (PST)\r
+From: newsalerts-noreply@dnsbltest.spamassassin.org\r
+To: jm-google-news-alerts@jmason.org\r
+Subject: Google News Alert - spamassassin\r
+MIME-Version: 1.0\r
+Content-Type: text/plain; charset="ISO-8859-1";\r
+\r
+SWSOFT Unveils Plesk 7, Deployed by 1&1\r
+Web Host Industry Review - USA\r
+... The software also features a newly designed Windows XP-like user interface,\r
+is equipped SpamAssassin, an open source anti-spam tool, and includes\r
+"Application ...\r
+<http://thewhir.com/marketwatch/sws021004.cfm>\r
+See all stories on this topic:\r
+<http://news.google.com/news?hl=en&lr=&ie=UTF-8&oe=utf8&client=google&num=30&newsc\r
+lusterurl=http://thewhir.com/marketwatch/sws021004.cfm>\r
+\r
+"v=spf1 ip4:64.142.3.173 -ip4:65.214.43.155 ~ip4:65.214.43.156 ?ip4:65.214.43.157 -all"\r
+\r
diff --git a/upstream/t/data/nice/spf6-received-spf-crlf2 b/upstream/t/data/nice/spf6-received-spf-crlf2
new file mode 100644 (file)
index 0000000..45c068d
--- /dev/null
@@ -0,0 +1,56 @@
+Return-Path: <newsalerts-noreply@dnsbltest.spamassassin.org>\r
+X-Comment: Yeah, the Received-SPF headers make no sense,
+ there just there to test that the SPF plugin will parse the results from
+ them... the IPs and comments are bogus\r
+Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org
+ [65.214.43.158]) by dnsbltest.spamassassin.org (Postfix) with SMTP id
+ B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue,
+ 10 Feb 2004 18:18:49 +0000 (GMT)\r
+Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org
+ [64.142.3.173]) by dnsbltest.spamassassin.org (Postfix) with SMTP id
+ B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue,
+ 10 Feb 2004 18:18:49 +0000 (GMT)\r
+Received-SPF: fail (dostech.ca: 69.61.78.188 is authorized to use
+ 'spamassassin@dostech.ca' in 'mfrom' identity (mechanism 'mx' matched))
+ receiver=FC5-VPC; identity=mfrom; envelope-from="spamassassin@dostech.ca";
+ helo=smtp.dostech.net; client-ip=69.61.78.188\r
+Received-SPF: softfail (dostech.ca: 69.61.78.188 is authorized to use
+ 'dostech.ca' in 'helo' identity (mechanism 'mx' matched)) receiver=FC5-VPC;
+ identity=helo; helo=dostech.ca; client-ip=69.61.78.188\r
+Received-SPF: neutral (herse.apache.org: domain of spamassassin@dostech.ca
+ designates 69.61.78.188 as permitted sender)\r
+Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org
+ [65.214.43.155]) by dnsbltest.spamassassin.org (Postfix) with SMTP id
+ B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue,
+ 10 Feb 2004 18:18:49 +0000 (GMT)\r
+Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org
+ [65.214.43.156]) by dnsbltest.spamassassin.org (Postfix) with SMTP id
+ B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue,
+ 10 Feb 2004 18:18:49 +0000 (GMT)\r
+Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org
+ [65.214.43.157]) by amgod.boxhost.net (Postfix) with SMTP id B9B2931016D for
+ <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 18:18:49 +0000 (GMT)\r
+Received: by proxy.google.com with SMTP id so1951389 for
+ <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 10:14:01 -0800 (PST)\r
+Received: by abbulk2 with SMTP id mr733125; Tue,
+ 10 Feb 2004 10:14:01 -0800 (PST)\r
+Message-ID: <1076436841.67074.8fa05ccdc458abe5.1446041b@persist.google.com>\r
+Date: Tue, 10 Feb 2004 10:14:01 -0800 (PST)\r
+From: newsalerts-noreply@dnsbltest.spamassassin.org\r
+To: jm-google-news-alerts@jmason.org\r
+Subject: Google News Alert - spamassassin\r
+MIME-Version: 1.0\r
+Content-Type: text/plain; charset="ISO-8859-1";\r
+\r
+SWSOFT Unveils Plesk 7, Deployed by 1&1\r
+Web Host Industry Review - USA\r
+... The software also features a newly designed Windows XP-like user interface,\r
+is equipped SpamAssassin, an open source anti-spam tool, and includes\r
+"Application ...\r
+<http://thewhir.com/marketwatch/sws021004.cfm>\r
+See all stories on this topic:\r
+<http://news.google.com/news?hl=en&lr=&ie=UTF-8&oe=utf8&client=google&num=30&newsc\r
+lusterurl=http://thewhir.com/marketwatch/sws021004.cfm>\r
+\r
+"v=spf1 ip4:64.142.3.173 -ip4:65.214.43.155 ~ip4:65.214.43.156 ?ip4:65.214.43.157 -all"\r
+\r
diff --git a/upstream/t/data/nice/unicode1 b/upstream/t/data/nice/unicode1
new file mode 100644 (file)
index 0000000..7b6735a
--- /dev/null
@@ -0,0 +1,28 @@
+Return-Path: <Marilù.Gioffré@esempio-università.it>
+Received: from mail-ig0-x248.esempio-università.it
+  (mail-ig0-x248.esempio-università.it [IPv6:2001:db8::c05:248])
+  (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits))
+  (No client certificate requested)
+  by Sörensen.example.com (Postfix) with UTF8SMTPS
+  for <Dörte@Sörensen.example.com>; Thu,  8 Oct 2015 07:45:14 +0200 (CEST)
+From: =?ISO-8859-1?Q?Maril=F9?= Gioffré ♥ <Marilù.Gioffré@esempio-università.it>
+To: =?iso-8859-1*sv?Q?D=F6rte_=C5._S=F6rensen=2C_Jr.?=
+  <Dörte@Sörensen.example.com>
+Cc: θσερ@εχαμπλε.ψομ
+Subject: =?iso-8859-2*sl?Q?Doma=e8e?=
+  =?utf-8*sl?Q?_omre=C5?=     =?Utf-8*SL?q?=BEje?=
+X-Note: The above split of UTF-8 char =C5 =BE is invalid, but seen in the wild
+Date: Mon, 05 Oct 2015 12:00:00 +0200
+Message-ID: <b497e6c2@example.срб>
+MIME-Version: 1.0
+Content-Transfer-Encoding: quoted-printable
+Content-Type: application/octet-stream; name=
+       "=?utf-8?B?0LTQvtC60YPQvNC10L3RgtGLINC00LvRjyDQvtGC0LTQ?=
+       =?utf-8?B?tdC70LAg0LrQsNC00YDQvtCyLnBkZg==?="
+Content-Disposition: attachment; filename=
+       "=?utf-8?B?0LTQvtC60YPQvNC10L3RgtGLINC00LvRjyDQvtGC0LTQ?=
+       =?utf-8?B?tdC70LAg0LrQsNC00YDQvtCyLnBkZg==?="
+X-Note: The above split of multibyte char across encoded-words is also invalid
+
+abc
+def
diff --git a/upstream/t/data/nice/unicode2 b/upstream/t/data/nice/unicode2
new file mode 100644 (file)
index 0000000..26dfd83
--- /dev/null
@@ -0,0 +1,10 @@
+From: =?UTF-16?B?//492Enc?= test
+To: test
+Message-ID: <123@test.example.com>
+Date: Thu, 16 Jun 2016 00:41:19 (UTC)
+Subject: =?UTF-8?B?44CQ6YeN6KaB6KiK5oGv44CR5Y+w6Zu7MTA15bm0?=
+        =?UTF-8?B?M+aciOmbu+iyu++8jOWnlOiol+mHkeiejeapn+ani+aJow==?=
+ =?UTF-8?B?57mz5oiQ5Yqf6Zu75a2Q57mz6LK75oaR6K2JKOmbu+iZnw==?=
+ =?UTF-8?B?MDc0ODc2MTY3MzAp?=
+
+test
diff --git a/upstream/t/data/spam/decodeshorturl/base.eml b/upstream/t/data/spam/decodeshorturl/base.eml
new file mode 100644 (file)
index 0000000..7395fbc
--- /dev/null
@@ -0,0 +1,26 @@
+To: Entity <entity@example.com>
+From: Example <example@example.com>
+Subject: This is a test email for a shortened URL
+Message-ID: <ea91fde3-4eb2-c80b-4c21-fa7b50b93825@example.com>
+Date: Tue, 10 Nov 2020 13:33:08 -0500
+
+Greetings,
+
+http://bit.ly/30yH6WK
+
+which should link to:
+
+http://spamassassin.apache.org/
+
+should get 404:
+http://tinyurl.com/qqqxxxyyyzzz
+
+
+This used to have a blocked bit.ly link but bitly expires all blocked links.
+This tests that any shortened link that redirects to the bit.ly blocked page will hit the rule
+If bit.ly ever changes the URL of the blocked link page, this test will still pass but
+the functionality will be broken for actual bit.ly blocked links
+Blocked link: https://sadecodetest.page.link/bitlyblocked
+
+# should link to https spamassassin dot apache dot org slash news dot html
+https://sadecodetest.page.link/news
diff --git a/upstream/t/data/spam/decodeshorturl/base2.eml b/upstream/t/data/spam/decodeshorturl/base2.eml
new file mode 100644 (file)
index 0000000..53f83f2
--- /dev/null
@@ -0,0 +1,13 @@
+To: Entity <entity@example.com>\r
+From: Example <example@example.com>\r
+Subject: This is a test email for a shortened URL\r
+Message-ID: <ea91fde3-4eb2-c80b-4c21-fa7b50b93825@example.com>\r
+Date: Tue, 10 Nov 2020 13:33:08 -0500\r
+\r
+Greetings,\r
+\r
+https://bit.ly/3yhHfzI\r
+\r
+which should link to:\r
+\r
+https://spamassassin.apache.org/\r
diff --git a/upstream/t/data/spam/decodeshorturl/chain.eml b/upstream/t/data/spam/decodeshorturl/chain.eml
new file mode 100644 (file)
index 0000000..49471a2
--- /dev/null
@@ -0,0 +1,17 @@
+To: Entity <entity@example.com>
+From: Example <example@example.com>
+Subject: This is a test email for a shortened URL
+Message-ID: <ea91fde3-4eb2-c80b-4c21-fa7b50b93825@example.com>
+Date: Tue, 10 Nov 2020 13:33:08 -0500
+
+Greetings,
+
+http://bit.ly/3qDCt8z
+
+which should link to:
+
+https://tinyurl.com/jf8wv76t
+
+which should conclude at:
+
+https://spamassassin.apache.org/
diff --git a/upstream/t/data/spam/dmarc/nodmarc.eml b/upstream/t/data/spam/dmarc/nodmarc.eml
new file mode 100644 (file)
index 0000000..0ad2e96
--- /dev/null
@@ -0,0 +1,22 @@
+Return-Path: <newsalerts-noreply@dnsbltest.spamassassin.org>
+Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org [65.214.43.157]) by amgod.boxhost.net (Postfix) with SMTP id B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 18:18:49 +0000 (GMT)
+Received: by proxy.google.com with SMTP id so1951389 for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 10:14:01 -0800 (PST)
+Received: by abbulk2 with SMTP id mr733125; Tue, 10 Feb 2004 10:14:01 -0800 (PST)
+Message-ID: <1076436841.67074.8fa05ccdc458abe5.1446041b@persist.google.com>
+Date: Tue, 10 Feb 2004 10:14:01 -0800 (PST)
+From: newsalerts-noreply@dnsbltest.spamassassin.org
+To: jm-google-news-alerts@jmason.org
+Subject: Google News Alert - spamassassin
+MIME-Version: 1.0
+Content-Type: text/plain; charset="ISO-8859-1";
+
+SWSOFT Unveils Plesk 7, Deployed by 1&1
+Web Host Industry Review - USA
+... The software also features a newly designed Windows XP-like user interface,
+is equipped SpamAssassin, an open source anti-spam tool, and includes
+"Application ...
+<http://thewhir.com/marketwatch/sws021004.cfm>
+See all stories on this topic:
+<http://news.google.com/news?hl=en&lr=&ie=UTF-8&oe=utf8&client=google&num=30&newsc
+lusterurl=http://thewhir.com/marketwatch/sws021004.cfm>
+
diff --git a/upstream/t/data/spam/dmarc/noneko.eml b/upstream/t/data/spam/dmarc/noneko.eml
new file mode 100644 (file)
index 0000000..09edf41
--- /dev/null
@@ -0,0 +1,21 @@
+Return-Path: <test@dmarc1.spamassassin.org>
+Received: from mail-pf1-x432.google.com (mail-pf1-x432.google.com [IPv6:2607:f8b0:4864:20::432])
+       (using TLSv1.3 with cipher AEAD-AES128-GCM-SHA256 (128/128 bits))
+       (No client certificate requested)
+       by dmarc1.spamassassin.org (Postfix) with ESMTPS id EDD4E2073D
+       for <test@dmarc1.spamassassin.org>; Tue, 15 Jun 2021 11:55:45 +0200 (CEST)
+Received: from PC ([2409:4063:231e:c527:1997:664a:34e5:7d88])
+        by smtp.gmail.com with ESMTPSA id n23sm15339981pgv.76.2021.06.15.02.55.37
+        for <test@dmarc1.spamassassin.org>
+        (version=TLS1 cipher=ECDHE-ECDSA-AES128-SHA bits=128/128);
+        Tue, 15 Jun 2021 02:55:43 -0700 (PDT)
+Message-ID: <1076436841.67074.8fa05ccdc458abe5.1446041b@persist.google.com>
+From: SpamAssassin Test <test@dmarc1.spamassassin.org>
+To: undisclosed-recipients:;
+Subject: test message 1
+Message-ID: <4A294538.10002@dmarc1.spamassassin.org>
+Date: Mon, 08 Jun 2009 12:00:00 +0000
+MIME-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+
+testing
diff --git a/upstream/t/data/spam/dmarc/quarko.eml b/upstream/t/data/spam/dmarc/quarko.eml
new file mode 100644 (file)
index 0000000..5fc5351
--- /dev/null
@@ -0,0 +1,21 @@
+Return-Path: <test@dmarc2.spamassassin.org>
+Received: from mail-pf1-x432.google.com (mail-pf1-x432.google.com [IPv6:2607:f8b0:4864:20::432])
+  (using TLSv1.3 with cipher AEAD-AES128-GCM-SHA256 (128/128 bits))
+  (No client certificate requested)
+  by dmarc2.spamassassin.org (Postfix) with ESMTPS id EDD4E2073D
+  for <test@dmarc2.spamassassin.org>; Tue, 15 Jun 2021 11:55:45 +0200 (CEST)
+Received: from PC ([2409:4063:231e:c527:1997:664a:34e5:7d88])
+        by smtp.gmail.com with ESMTPSA id n23sm15339981pgv.76.2021.06.15.02.55.37
+        for <test@dmarc2.spamassassin.org>
+        (version=TLS1 cipher=ECDHE-ECDSA-AES128-SHA bits=128/128);
+        Tue, 15 Jun 2021 02:55:43 -0700 (PDT)
+Message-ID: <1076436841.67074.8fa05ccdc458abe5.1446041b@persist.google.com>
+From: SpamAssassin Test <test@dmarc2.spamassassin.org>
+To: undisclosed-recipients:;
+Subject: test message 1
+Message-ID: <4A294538.10002@dmarc2.spamassassin.org>
+Date: Mon, 08 Jun 2009 12:00:00 +0000
+MIME-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+
+testing
diff --git a/upstream/t/data/spam/dmarc/rejectko.eml b/upstream/t/data/spam/dmarc/rejectko.eml
new file mode 100644 (file)
index 0000000..b3c0009
--- /dev/null
@@ -0,0 +1,21 @@
+Return-Path: <test@dmarc3.spamassassin.org>
+Received: from mail-pf1-x432.google.com (mail-pf1-x432.google.com [IPv6:2607:f8b0:4864:20::432])
+  (using TLSv1.3 with cipher AEAD-AES128-GCM-SHA256 (128/128 bits))
+  (No client certificate requested)
+  by dmarc3.spamassassin.org (Postfix) with ESMTPS id EDD4E2073D
+  for <test@dmarc3.spamassassin.org>; Tue, 15 Jun 2021 11:55:45 +0200 (CEST)
+Received: from PC ([2409:4063:231e:c527:1997:664a:34e5:7d88])
+        by smtp.gmail.com with ESMTPSA id n23sm15339981pgv.76.2021.06.15.02.55.37
+        for <test@dmarc3.spamassassin.org>
+        (version=TLS1 cipher=ECDHE-ECDSA-AES128-SHA bits=128/128);
+        Tue, 15 Jun 2021 02:55:43 -0700 (PDT)
+Message-ID: <1076436841.67074.8fa05ccdc458abe5.1446041b@persist.google.com>
+From: SpamAssassin Test <test@dmarc3.spamassassin.org>
+To: undisclosed-recipients:;
+Subject: test message 1
+Message-ID: <4A294538.10002@dmarc3.spamassassin.org>
+Date: Mon, 08 Jun 2009 12:00:00 +0000
+MIME-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+
+testing
diff --git a/upstream/t/data/spam/dmarc/strictrejectko.eml b/upstream/t/data/spam/dmarc/strictrejectko.eml
new file mode 100644 (file)
index 0000000..edbec9a
--- /dev/null
@@ -0,0 +1,23 @@
+Return-Path: <test@dmarc4.spamassassin.org>\r
+Received: from dmarc4.spamassassin.org (dmarc4.spamassassin.org [1.2.3.4])\r
+       by dmarc4.spamassassin.org (8.14.9/8.14.9) with ESMTP id 13DFe22R006047\r
+       (version=TLSv1/SSLv3 cipher=AES256-GCM-SHA384 bits=256 verify=NO);\r
+       Tue, 13 Apr 2021 11:40:02 -0400\r
+DKIM-Signature: v=1; a=rsa-sha1; c=relaxed/simple; d=\r
+        dmarc4.spamassassin.org; h=from:to:subject:message-id:date\r
+        :mime-version:content-type; s=dkim; bh=vxHXq7bMZ9+UHGuKBsbQKsDHm\r
+        mk=; b=Gm/CB7JaShGbluYAiHOBX0CcOvye9210Tghzmuvya0j0EF7dfJH8I9+wc\r
+        zFxo4JdBQ6xoq6zXwOLU8pVcpDtxrOzIPkidkVsI1iDi7tApONTuG9JW/vVNId/J\r
+        RBsp8Z2gi5vO07L2dtcZEIVOXM1MKN/69gXiGY3TbyhO63iFno04nAQFHooQYdqk\r
+        a6C8s0n3AVfPEE4cbTit67kREHVm+ZMQpd281BUh9zOftfL+R7VEWWSnSz5EmeBU\r
+        MihnFhg5DpEAIFyJ9ZqspI4CG0gAiRzd+Ol2ciJOAhm/hcqn3/J0YPqtN/1Cl7I2\r
+        jrtCRUnSpndamKLJp1aLWibYYkbwQ==\r
+From: SpamAssassin Test <test@dmarc4.spamassassin.org>\r
+To: undisclosed-recipients:;\r
+Subject: test message 1\r
+Message-ID: <4A294538.10002@dmarc4.spamassassin.org>\r
+Date: Sun, 11 Apr 2021 02:00:04 +0000\r
+MIME-Version: 1.0\r
+Content-Type: text/plain; charset=us-ascii\r
+\r
+test message\r
diff --git a/upstream/t/data/spam/extracttext/gtube_b64_oct.eml b/upstream/t/data/spam/extracttext/gtube_b64_oct.eml
new file mode 100644 (file)
index 0000000..f9fd7e0
--- /dev/null
@@ -0,0 +1,29 @@
+To: to@example.com
+From: User <from@example.com>
+Subject: Gtube jpg
+Message-ID: <ed48fad8-753c-458b-4433-76b5659a0f9e@example.com>
+Date: Tue, 9 Feb 2021 12:59:05 +0100
+User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101
+ Thunderbird/78.7.0
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="------------4AF91C32E325771A2459DE43"
+Content-Language: en-US
+
+This is a multi-part message in MIME format.
+--------------4AF91C32E325771A2459DE43
+Content-Type: text/plain; charset=utf-8
+Content-Transfer-Encoding: 7bit
+
+
+
+--------------4AF91C32E325771A2459DE43
+Content-Type: application/octet-stream;
+ name="gtube.txt"; charset=UTF-8
+Content-Transfer-Encoding: base64
+Content-Disposition: inline;
+ filename="gtube.txt"
+
+WEpTKkM0SkRCUUFETjEuTlNCTjMqMklETkVOKkdUVUJFLVNUQU5EQVJELUFOVEktVUJFLVRFU1Qt
+RU1BSUwqQy4zNFgKCkdlbmVyaWMKVGVzdCBmb3IKVW5zb2xpY2l0ZWQKQnVsawpFbWFpbAo=
+--------------4AF91C32E325771A2459DE43--
diff --git a/upstream/t/data/spam/extracttext/gtube_pdf.eml b/upstream/t/data/spam/extracttext/gtube_pdf.eml
new file mode 100644 (file)
index 0000000..9cc3242
--- /dev/null
@@ -0,0 +1,315 @@
+To: to@example.com\r
+From: User <from@example.com>\r
+Subject: Gtube pdf\r
+Message-ID: <58320984-5186-6504-3262-b2ca2e8f298f@example.com>\r
+Date: Tue, 9 Feb 2021 12:44:07 +0100\r
+User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101\r
+ Thunderbird/78.7.0\r
+MIME-Version: 1.0\r
+Content-Type: multipart/mixed;\r
+ boundary="------------8B5F937C37FF6F878A09D1F7"\r
+Content-Language: en-US\r
+\r
+This is a multi-part message in MIME format.\r
+--------------8B5F937C37FF6F878A09D1F7\r
+Content-Type: text/plain; charset=utf-8\r
+Content-Transfer-Encoding: 8bit\r
+\r
+\r
+--------------8B5F937C37FF6F878A09D1F7\r
+Content-Type: application/pdf;\r
+ name="gtube.pdf"\r
+Content-Transfer-Encoding: base64\r
+Content-Disposition: inline;\r
+ filename="gtube.pdf"\r
+\r
+JVBERi0xLjYKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0\r
+ZURlY29kZT4+CnN0cmVhbQp4nIVWy6rbMBDd5yu0LsTVjCxLBmNwYnvR3YVAF6W73nZX6N30\r
+9zsPKVIc1yGQWK85Z47OjGMbMH9Pf4w1trEYje99g8Gb2EITOzAf76evn8xv3UGfj1+ny+3k\r
+uyaaEFzTx97cfpjPKxhAc/v5bbAw4mDROv5pxzPYwfr7CHnQ6Y4wAo+izMqpXh4ne+HBtd75\r
+/fbltNxOb7s8OtvAMw+/+SiOgM76sxCtsx/sehy+7Rr3MvwIgZPoYoLQXIW/Bwtgl2MQh41/\r
+nYMKNGt0AECOL9quDKrpCCiTAHeMCbEJrzEnaAUH/HE02+65YRuNrxY65glBKdMYDyN3Iexc\r
+AESwJG1PKrR0kz5pMklYRWDlMwBWutBdnOMgR7qBspvoQxekG7JJKy0fnHjuatOyu4trZQ0m\r
+uxCpiyK72g4ekucLlewTpXuFnr7n0WsFqTmBBynTY6E6t2cijqDA83gODHMp/ieKlkFTgqAJ\r
+qm83SW/EZHIpzKa+ZTBXJaBR1UVYuVPYKD/Wf9F4krFaRE70WbeZ1HyEyvJWbMrpxAOWQkmh\r
+4HH5WNO23yuSjdnoedVsxhYHtGMfBjGmr9XPJrUlm7t/y4ImbSVpNduzWyT3omy+FzFR1nPO\r
+0kNOVRhWomyvNzUU/QKy4Rkf9lfoS1FPLUWAxzo6v9ce1N1QqhLqS7zX6/1FgpCv0mvhFgsW\r
+obOWmlYldpVRyt3TCtxXsFrL4kuPmDItnnsuaI1emvF8B78mU6tRkoSIeNyZOxt3Oh7qYU+E\r
+kBwMAwaM2NMET0/ahcY44EUmrmnaDyjZ4QJrepLTulcnVgzOQtQgOiedOi0H2vm/97QDvjYK\r
+2fFiCBmYm1iKJxMOdeSAxpmThIPoYBNSYYSBcoWxdwlKt7gSmc63vJPUoAxXUuS4Tfpod0r6\r
+oWj1dpFHWg1up3azh57/2+x09roWc2eTlrpIP6q8lAqq1EFqWq1UtEbjUfW2qt80UooPL8Y5\r
+90jaNOsfB0L1UP3teTP/AEs6I2QKZW5kc3RyZWFtCmVuZG9iagoKMyAwIG9iago3NTkKZW5k\r
+b2JqCgo1IDAgb2JqCjw8L0xlbmd0aCA2IDAgUi9GaWx0ZXIvRmxhdGVEZWNvZGUvTGVuZ3Ro\r
+MSAyMDYyMD4+CnN0cmVhbQp4nN18a2BU1bXwXueceWVe58x7MszMSYY8ICQTMgkQeeQQkhAg\r
+wgCBZlDJhDxIFJKQBPBRS6ygFqRExWetpn7UWkvrIJGitZoq2vZaK95iW6vWtGIftz5SL7Ve\r
+JSffOvvMTCZI2+/e7/v1TThz9l577bXXXnvt9djnDAN9O9uJiQwSlkit21t6Y90t6wghPyME\r
+bK27BsTFa5wLsTxGCPPvHb1bt9/3/cvPEcKNEKIb2brtmo5XbnhmESGmTkIiNZ3tLW2r58aK\r
+Cak6ijTmdSLgOvkaHdbPYn1m5/aBqwdt239CiKTH+pltPa0t72/+djXW38T6tu0tV/dWclcx\r
+hCytx7rY3bK9/ZPDz7dhHekb+3t7+gfayC2ThDQo+GJvX3tvw31bXsD6eULYIYQB/ikfExa1\r
+Sp1hOY1WpzdkGU1mi5UXbHaH0+X2eLN9M/yBoJiTG5qZl19QOGt20ZziknDp3LJIOfn/6aM5\r
+SJykXrOYWEkv/Z72YY8SL7mXkMn3lNrUt9ww+en/Sy706u0e8jAZIQfJ6+SKZEMdiZIushMh\r
+mZ8fkVcRqnyiZBN5lOz/B2SPkpPYruLFySFlJhf9RMnd5Dj58bRRomQ7uQ55eYK8DnPJT1FV\r
+eshHoCc3kBeQ6kcIu/RipBgLfnXQYkcG9A3yNeYAWckoen6v0sKEGZ6cIvfDZqQ8gPM8mJ7x\r
+os8RvZlcj9/rSSfZhWX60Sw+/xtimPxPnNX1ZCX5MllKtmX0eBoeZLNw/RrJgyjTH1FYONWo\r
+q2evZE4wzMQdWLmNbMWrBXDuzEF26T+Q0H/7w24gZpjF5hHDxVqZcmKVP2XKJs+xM0kW2TA5\r
+noJNrpr8T7ZF7uaauRmaxdxL/2wM7W3cduxNJt+Vr5PbNKs1D+NqPYLWYvllm2JNGxrXr1sb\r
+XbP60oZVK1fUL6+rrVlWvVSqWrJ40cJLKhfMn1cxtzRcUjynsCA/b2YoNyfocQi81WI2Zhn0\r
+Oq2GYxkgc8QExGsTbJ4o1LWEakMt9cVzxFpPZ03xnNpQXTwhtogJvHH5ofp6Cgq1JMS4mMjH\r
+W0sGOJ6QELPjAkxJxZTSmMCLi8giZYiQmHi5JiSehE1rm7B8sCYUExPv0/KltMzl04oZKzk5\r
+2INypXAr1ibqdnXur40jj3DMmLUstKw9q3gOOZZlxKIRS4nCUO8xKFwCtMAU1l5yjCF6szIs\r
+zrS2pS0RXdtUW+PLyYkVz1mRsIRqaBNZRkkmtMsSOkpS7FJYJwfEY3NG9996kidb4kWmtlBb\r
+y+VNCbYF++5na/fvvzkhFCVmhWoSs64968GZtyfmhGpqE0UK1VXr0uOsmhoSEpo8PiTu/xvB\r
+6YTef286pCUJ0ebxfyNKMcEsS8C6phzl46tDWe/fXxcS6/bH97ecnBzcEhL50P5jJtP+3loU\r
+N4k2IYmTk08d8CXqbo0l+HgnXBJLTr1u3aqEfe1lTQkmr07sbEEI/qsK5Szw5QhpnOg/aiYo\r
+FhQOSjgnRxHDgZMS2YKVxODaJrUuki2+x4kULoolmLjSMppqcW5QWgZTLenu8RCu7ar1TfsT\r
+XN6KtlAtSvxAS2JwC2rXlcrChPiE5WNfTmi/TRArwzGKKyJXK9q6xIQmH4WEvTI7oN4oXfbz\r
+tGL5WL2978MB8gWbWBlCMgqd2lBtPPlvV6cHCYgo6PoiVREamxJSDRakluSK1R4rDWOPljgu\r
+WFcNXcxEONSbcISq06ursFXbtb6Jdkl2SziWJUi8NdkrEa6l+0qs3R+vUVlQaIXWNj1JIpNj\r
+x8pF3/EIKSexGgXZtQy1LL92f1NbRyIY97XhvusQm3w5CSmGKxwLNbXHFLVDCc0a81HliFFd\r
+aWxatT60au2mpgVJRtQGhRyXV3sBmVCTTyWDCpjQ5+nFJsbHxhCRR4BYh4VQ9SL8Tujy9Hjx\r
+KHAKVRS3epHYBD6SwkY2ErPE2vaaJJ5Sn0ZUo6jTsvoUNa1SRTrL6n05sRz1UzyHwWYxOTD2\r
+0CtCrU81oZnCBj3q57J6ClJk6VGUXmwKtYdioU4xIUWblLkp4qFSTgqDyjy5Vo3TahnCQjGR\r
+HGxOVRRhJuqKfJnCTSyn9XS1/oLmFalmcb8+tGr9foV4KEmQIOcrEkRRYWmB4KO2QNnQIbS9\r
+Io9bmm7o/cckSdnMnZcoREIr2vaH1jctothoT673XauMZSOrYFVjdfEcNG3Vx0Jwy9pjEtyy\r
+flPTkzzGhbc0Nj3OALMsXh07NhPbmp4U0WlQKKNAFaBSEZWKQmkdVvQU3/ekRMggbeUogNZb\r
+TwKhMH0KBqT1JKPCeHWgfDqQRBhs4dQWKYXNIUyvwgYpjH6OEUVkUpZG0ksGycSYGd8xUECP\r
+I+QpjGMNQI6bwAy+Y9hrHQWfhMFjBsmnYgwihqRyeMuGqaE3bGo6bkLv7KPfOFC18kF18XTi\r
+YqNbqRXbFEX5YqxzfzymbDbiwqXBf5CA0BJcptASZERrSmSF2qsTxlC1Aq9S4FUqXKvAdaii\r
+4ALsPohrH02AogGXNeXglhSzf+rbz7+vrFQMjcp+/t1ilFge5g0/whjUAYukt2yMkdGzTpeJ\r
+6MHA6vUGgTWw8ZiBtTGEaY4RW5ULrC4Yc8GzLjjkgj0uaHYBAkUKv2rcBa+4YJi29bpgjQuC\r
+tEGFJ1zwIG3qod0kF5RSBOKCt2nrIIWXUsjCSTqO2u0QbVhD28YpPJEaQ+0g0j7jlNAoHWaQ\r
+tiJr4dQYV6Q/O1KfvuRn8wXwz7UobaSqSCARD/0WIp5w8+YrIoIN3JVCZG5pTsV8IZRrhZCQ\r
+I4QKSqAIBLcTFr4WmbjCt4y7v8YX+Ler575W4ePudrwKC+UXXtUZP7vKV6EEVEAcGLm9izmB\r
+nzwlXUMcDq/ZYjF4DYGgPzsa8xMHVtzeaMzkdtoZRqMR1sU0/HAQxoIwGgQ+CCQIlVgZCkJv\r
+EOJBiAZBCkJpEMQgBGkzNg2mWrHpNO2ZCMJwBjxTOH190wSQnDvOuiqiFCJUDDaoxLmrf3NL\r
+waFbApEylxMnrkijPJSrSxYLKnKgTlf//aprv9gnX3X9w5tv3CO37b4VytiPO0tmLfrqzRN3\r
+eYuLvczmo/4Ju1LSMJ5i3DhFGJ/aNA3EiNnS16UOYjJpBcHtYg3rY4QFnmWdktMWjTmtJsEq\r
+RGNWp8MNnBsqz7lhyA1Mrxviboi6QXLDqBsSbhimVdENvBuIG8YpBFEzMafriCKCZiqF9PKT\r
+bA//c1x8KgAhkpy8NpSbX1E+DwXATk36Oql4jiTNKZayviF7h/dBEfe2Wpc+u0SZJyt6lT3Y\r
+OPke8wv2BVJIYlJ5js6RbUaVmDXbnMO63YFozOfmWWM0pmNdg7OhdzbEZ0N0Noiz4bHZ0Dwb\r
+1syGFL8El4fQNbIRXBt1gZDBNIcFFRG3K1JWUR6GEoay63aGCvJDuVqnw+UOsMwvjn237tul\r
+xXNXXf3cvbH2y8u+PbT1a+HZFX1rN1y6+o5NVSHQ3zrkt/3xxpqHry3359S01n3xUPDl7eFo\r
+TeXq7LKSZRtVfcb5FHM3EBdZLhVkWSw6O8u6PZzJaIrGDDqjFfVdWBsjrgc9kPBAlQfCHmUK\r
+fSkRR6iSKezbKsvKFPlqULhCqKIKIs6IMyQ4cA7znRaA1fHm665vr/rVrxaWXrI+tNfRt5W5\r
+o7jgtdcaJ/YsreaXeoJK2kOik++xdShfJ5lBDkqbvADWbL3T6vQHvATVxhv0MibW6zXZbK5o\r
+zMabNGtjJtdoABIBGA7AUAAGA9AbgHgAogEgAViCNykApQEQA8AHYJziIVJq56SthqI1hO4V\r
+UkktRtHUolC9cToCuGnmKZPB5RFwRUTBCbgcOeX5wC3es3Xe4dLSb25846WfPwtd8t2dPXD7\r
+5fC6bf+9UZtxQbDkPdB8/JHcsQ7uf+TI8XuJqkuam3HPiGRU2uPQZpuIluTkij6/rznm97MG\r
+g9AcM3CsuznG2nflQkcu1OVCfi58nAv/ngswmgsjuXAkFw7nwt5ciOZCTS6U58LMXOByoRKv\r
+c7lwNhdOUbzDGc18LjBjudCbC/FckHJBzFW3UvOUTZ0SyxXNqsZOE09q0VObKg9lIhKBJzll\r
+bmpUQyIj8DZFXMrucuo03MCnQ/JT8mFogQ3n3lnmq/3RVZPk/XMfNp4sPQof3liXXw5xWIbX\r
+FYvksYfnVsivyC/Kr8uvFM6A67PnzctGeRWhvO5H/+chd0hxu8mjNWm92TYNMVvNzTHGyhqc\r
+6P/sXDZUtWVDYzbUZEN5NszMBkc2jGTDkWw4nA17s2EgBeezAbHPZcNYNjCnsyGRDcPZ0JsN\r
+0WzInHqGnd2cVJOkbU0bVtQPOk/BwYXEmRXqpAU+p2xeBXfzCPz0qPz38/Lf5L8fZTxHoXX0\r
+Ze6D7IqK7PNv/3X8r2+x5bT8hnzHiePoXsjCyU+5j3CeemIj+eQt6Q5DLpmhsViczuCMXK6w\r
+II+Px/JsQY1JY4rHrBowshqNx+5xNcc8XDzmYe3O5pjdNlwIQ4UwWAi9hRAvBKkQxgrhQQrB\r
+apRCxEJ4uxBGKaSUVkkhXPIKbeYLYZySILTn6UIYprTUnmnL+zmXrMopqS3TdhOWqCtyZ7qi\r
+8nz0v6gndlpAYzFPo1pk0eV0aHUB3HZcTh77zH3ffeWNh+5tf/qV8f33f+fR856jR5l2Mvne\r
+bTc+8aL8t0kiN7KfXNcrawZl18EbJ36mve1ddOAPZFfcfWTXwzPs373lRz+mp5lkIcZQ16Ns\r
+faRbqjM5HODVWoGb4TfZ47FmU4+JKTYBS0y8iTFoTCbO53PEYz5ojvlsnDEe4zCy4tgxPwz7\r
+odcPUT9IfkgZjwsmjLozNdm5pVWgTAr3hjLNnPkRLOUIOSKaSTZn4dGj7AfVYu+ZN+DKoCQF\r
+5btAD0y0str+2avqNORnX7dMnBuW2x6aeIv/RH6AzmW1/ALsIb8gPJkjeXB6nNEo2Fju6zEL\r
++8hlWstpG8RtyB0JR4oyHWAeirViXkV5fkHSC8KeB29PBPJqaiqkNXO/8uTsSxf0OkR7SJpX\r
+eTkdJ4xK+Tu0yTPQTl1P7HaP0WTSeXT+wAyMdGZY7VhxeaKxLJfThpgsvy7G8kcCcDYApwKA\r
+RpMLQCVWDgdgIABtAWgMQE0AygMwMwA+2ozWm8m03WixTwcgbdbT8EyH3/w/inymxz350wOf\r
+mku/d4ka96zdsAkDnyt37AATG59TmQ57mtY3J+OeZDwwC53WPSgbB1krFQs6HZhMTpdWQFMo\r
+MBaNwDIOnjdHY7xVZ8pCl5rlbKYxLgbUO/r6puIUxZFGUGmElCtVPU6oIFebwaAbuWfuKbqk\r
+7Ctl35Crd+8Gm2HRy4vYF+Run2uiOsXVzrLL1Vh1NfrS1cibi8SlRYLG6NK43B69FaNTPe9y\r
+sI61MdZFPLBE8kCpB0QP8B4Y98BpDwx7YNADCH/MA82Kw0/v7M3p4CpSlGH8qP0XaJCiqDiW\r
+qd9nV889ukme/+fXbx6eX7R+QD73v75z+7bKmbPgr3+ZCMqfPhyWO888kaPwWoVyfFTzEPHB\r
+POnXNpeL9fnc9izOP8Pl8/qiMa+TOOyOKDpCq84SjRl14PMD54dzfviBH/b6YcAPbX4o8ifh\r
+V531wxk/nPLDiB8OUwxsXpXR5zsUfhnt46Dwl1JwpNXoh5oU/JK/UEJH/DCUMVS5H2ZSDOIH\r
+ZtwPaBROU7swSE0D2gXRD7wfErTKU7xpiUtz3wX6m9l6sZapiDGd2qjmha6APVQxf1qU6Acl\r
+/KK687uHHvrmnZdWzy3OLa0q//TTl2TuANs0t6D69Jj95eucvffd33j+45zi4hyMvxSdeRx1\r
+Jgu15inpBtQaoiGoNSh0Pc/gCjAuVBTUmjEPRKneqEozRvVmlEaJqD1DVIF6PRCnaqR2Wfgg\r
+BUUpKK1xZHp/VfXUbvj9OYlkBCZJeaWdTaYz5nNSMb6uPBkzo2thH5frz/z612/+8jcjX7rp\r
+xp27b9g7CG/IgvzXD87//T9//dxTY+/88JS6d5R9zWOeZyAPSb0aY5ZBi/EwIRpWg0roPGOE\r
+U0YYMcIRIxw2wl4jDBihzQgzjeAwAmfEvIZiDBkxtTFC3AhRI0hGGDVCwgjDtMobgRhhnFYR\r
+LxNtmjIk05ppE5/Kb3HKuPumTFiPkr0MQ11dRm62Etf0jzgXO2atg9IaB2ckXi/P8YGgnY/G\r
+7JiTYWRNdDMwb+G9OG/GvRaXGVPR5ZnZKaGJ6CCFqIU4hU/FAVPWQUgatenLoloKrWIoMDJ0\r
+5y9WQiZFQ4EaCwHNBvOLHXfLe35zZluP9gGoGZA/kYODe3dsivXJ5+s2we/+DuDO2XfOU/zp\r
+k95iePmZHxQwfxToevlwjm+xR1Fr75Saic3McQabwe3R2F12nJbLyjE8sy5m5l0mA9o/5zDV\r
+uNGUAlaOZegkoUqc1t1ESidViOiBTNXLdD5TmzOSER+nUm5lhsn5FigZhFeZPVQOf3HbVyGy\r
+W/5Av/ypqvGrIQCmo0Hmj97i8/d5ixsKKsHBdKCnUeZYgC5eeR7pJaekSy06ndZKeN4JWrNe\r
+r3Wy2T7JF/cxwz4UhojlqG/UN+bTLuZ9CR/D+0oREPed9o37tASLvb4hhI8iQKdnfScnR4/H\r
+rqin90s3qPeKSnqXcovm1nsx2fIS3qx32p2Krlh0HGu0O7XAAm4Ha9ol2NyVac8LRfhBl6bk\r
+Ejv6BNtU1Bex0XMYgxoxG4BqL2Q64ia4YQs07JTPQVOHvGejLF/XJu/ZfQDmwgvwoK+42C1/\r
+MPGBGzUc7rxZ/iit6kqOQIjWi/ZrLjwuTQom7YwZOaSwEG2biY2UzS2JxuZaC3NmCKbiouJo\r
+LGgtcnq1WoPBsS5m4AuU4CVPCV52RWBjBOZFYGYEXBHQRuDjCJyNwJkIvBiBIxG4KwJbIgDR\r
+CNREoJTiOSLARaBzPIU4EoGBCEgRKKfN2HYuAm9EYDQCCUpjbwTaIkkSKg6fQjsdgVMR+E4E\r
+hijaVRFYGAExNcYCdYDhCMQj0Jgaw0F7nqU9D0dgEIeXijLafbTvWcoAk6AIvXR4HNUaAf3m\r
+acnePzG6O3ZcBKFvqnsGUoYxSAVmycgsFZtNKcVUdKZEOWVeSB9NWRjdP4zY6lY9ItXu9F/6\r
+Ss34NfKGW4eza2urnMJBufrAhg1NNx6UN2KQZGfjRZeUVxZVy39JRXFH9Vlmbt7SdFAX8094\r
+00EdPYeoR3uyg/0R5gl5ZLtUJejz8jjRZPJybEF+Xm5W7tqYxykIaDKtQlBgTKwgEH2WS8eh\r
+sXES3COEHyyA5gKQCgALV0w5KcVI2CrTGVGGGxeSpz/J+LRAG8oVypdAFVQos7ZCqGIe6CyY\r
+DylZNbx63207Zdned+yvK4bvObh8Zdv63AUPAbnxpuZDNa1l7I++9OWJfd7izX3g2XzdUpa7\r
+o+Xy8M6XQ3KA02zuTgQ9yl5ZiRPdgrmQEYzSDzVmk1Gr1zbH9Kzi5ZpjrG3IDINm6DVDmxka\r
+zVBjBtEMvBk4M4ybYcwMZ8xwygwjqdZyM8w0w9kM+BEzHE4RiafQSimag9JZmMbea4YBOpJK\r
+iKMDnDYDM2qGhBmGKYEo7a1ygW1qE5LP1Mvmi0VPF+puGmPaSfEFztSpgxNHJ/50VMnZo74K\r
+NROryKa22CvXsZ+gv5kB7yh2xu4xeQJexgZmzmb0e+yCyYJO1WlBt4pRNpmBiU9HADZimhOA\r
+j2hqdCYALwbgRAC+QtMi9cCqOgBlNC3CrEkbgM6PA/DLADwXgMcDgBnV7QHYS7E7aBJVR7Fz\r
+A2CjSdS5APyB4mPWNRKAh1P4fQHYEoB1qaQrPwCuFH6aj5Hp9C/AV7lZ8DHFTnNzOEVd+kIm\r
+PzMpP8rZ23gAxmgipzJ0OEW9jc5VpX6OIjBqqpdOAdcEwBpI26QLjNKOvszQ+SKrPN1ETTsi\r
+vxi+aqnKMk91aC4WmQon7bnJ09glMF/1Wj71Bhbmx6sbwjnB+eVNK+bL98Vh5LD88R2weYt8\r
+29L4gFxneynuXrTrHrYXzYtvooe5A+/O85d9c2ilamdy5AY2gf7KTXLIPmltwMrZbG5Pljsr\r
+N+S2OWzRmMNnFqMxs8vv0/nWxjgdzxLF5UohGAwBCUFlaQjGQjBK6/EQSBnlqtDUzlBm3Tct\r
+SFGDs2lximJ/7MlEE82QgwnlFrj8SQet2l4Uwj1XgZ6ZfXDFyAu/emlHh/aILO1m2q7fs3N1\r
+7MrzLAYr82fO+fQ/PpQ/ddXPkj3hsIddPfqDnAm0kMq7ZZPvccpbUxbokSZ1jMHCWKy8xaBj\r
+OAbTOs6q04NZT8A1wEMbD4081PBQzsNMHhw8cDyc4+EsD2d4OMXDCA9HeDjMw14e/gn+2H8H\r
+v/L/coDhi+GX8iDywFP8U5TmII+JAaLHeVCd5r9Q2f9zHccsMbL5ioucT2pCqXiL3pjilfL1\r
+cThxJ9hAeydcvsnBXkt1dDdzQNFR1E2i5IIYbxpp3rDWptP5idvvDgSzMYbOdmltNoeDXRtz\r
+8GoCIdGkQHnANUQfdY2lnlwNpbKFi2cOKvdT6jkVM0wLoGkaR/VvHs0bHLp5KR3NL2DkwZsW\r
+DmQ37tz/xYkDX4Gwtu2e0Zd/99rGV1bD+MkRp2nCzf+aK/EUy4l5Q6v//N6E/F/5QWrHmcn3\r
+NM+gPjrIf0qBLI3VoXE4XQyGB/WM2eywZml0mPAJOqvFaDw5+Yl0IzYYWSCcq9EFNS6Y6QLe\r
+BZwLzrlghD5OPeyCvS4YoK2lFOEIBba5oJE+26xE1LO0IrmAUR+I8rRl0AVxF0RTj1sRfpo+\r
+vR1OPSJtpsCLZodJBZmuDoouKBFXJKUOEU942rGNJmSEUMqgRZI2jfOt/tO7K+Uf9MCz97/9\r
+TuPvX7sPOjodzLaJw0kN2ce0T9zN3EC1RDmvbkJf+Drnw/yknEjwgDQ5Jxx2O7TZS/wLSKHZ\r
+TPJDGp8/22FYWs1WRGPuoqIsjS8/xLFZbJYoiAujMZEXylDKM0aqYbgaDlfDYDUMVENbNTRW\r
+Q001lFfDzGpwVANXDWPVcKYaRqsBkY9Q5L3TkVVMUg3nquEsRT41HbntczQrM1GPpJAyx+Y+\r
+h5AeUqI4YjUwPB12vFrKVbg8TblM0CkN0Sn1VkO8Gkop8vSt3HyRjfwPPNXFETMezlSiyVXD\r
+yQsD7KnHnwW6AERylN1Dn2VVlM+bXwIV85MnJu75bp2Lxf2Wo0SbdMOVzatIHaOwHc+dWFdX\r
+xdbPA9c9d+z8/TdGf1ofr1z9wAPPP5U3EHwrdGDZrLrl8uHZFV8c/NYT8vHtl23u7NoSZ258\r
+6GHrjUJg70DX/Rt2ba/YWmu/vOLxla/f94g1q6doaNX5bZXSzJ7Sjau+yOy8fs++HX17916t\r
+7M+bJ9+Ba8hraIM8kpFotSYza/jaZaydVCWPtOl5dvqBLlxTW15eWxeJ1F0+t75+bqSuDmm4\r
+CdHcobw3QXqkWqLDbMJhNeuz9M0xHbGwnJDFgpbDyJez4SasUvdfNPWiwljG/ktvzan1mHYi\r
+mk5/U6xBMlWZZnnZutcm7nxNnnU3M38EboDuR2HkDoaXJSXChKeYs0qMOXEnc1U61twkNzCd\r
+aIcFMkMyEYOFM3A2u5lofKSqatqZl83ldpYAU1Fuo08pGfDV3bXjxbe7On/7k2uHlsEH8nn5\r
+pTNLG7v+C9Z++AGs+eTKxro35DfpGNU4RjQ1hoFoLMRis2cRcsEY9oolYBN4Jr+gAsM8NL7a\r
+6h131S0buvYnv+3s/K3c0Lj0NagAlCfMfKOu8aq/y8c++EB+7BM11nkI1/IOzKmMuCIJ6RoT\r
+ITat1uN1Wr92mZNXl/WMF055YcQLR7yw1wsDXmjzguSFUi/M9ILDC5wXdytFGkw1N3qhxgun\r
+M3qKXtyMXgzSYdwLw14Y8kKvF+JeiFJiF2y7i+21jBPFqacmaS0TMjXujtqIonFldXVlkbra\r
+8khtUvNqmZOofhGsKPLFtFLzEH3udETapnHO8NstJp1B1xzzaR1mjuUNbLZTzbs8rK3ZD0E/\r
+fOiHx/ywxw9r/FDlB6sfJv3wdupEWaInygsleio9SE+b1SPo0dTTqXj6zDmpqtOe01HvkDqs\r
+ma656STIME1vUwc1sPhheeVh+Il83d3MrEfBBfMfhu8flm+Gl++Z+NWj8jBNlZjNjKQosLwF\r
+aNIkZ000UbhePV8tmnyP+YjuyYR0tcOk1bqcWbZ4zGoJWpgs1mLJsmtQY5pjDKu+u5R+P2mS\r
+vmj0WOr9pD2pt4bezgCGKeaiD6n37KGvFr2S8qR76MZuTr5mlFaAC14rmpYbRtJvA9gqy6Ye\r
+9CqmU0kLlLcCKuC+owx3z7pvbR1cUx2w182+NfmA97mbfrozcO6AO/sZdd5fwZjqnKYB9SEm\r
+lbv1BQEiFAjhkoDeMXs2Lj7Mtjt8qAMObjwMY2E4HYbRMIzT79IwiGE1aFJ9+z96f8Su8oQm\r
+u0RbMfVMRrHeAbzoyyUHlobynmi69euLW7+070uti8dfe+iZpaGOu266e3Hrnn17Whd/MLbt\r
+Nxug64lw/aEv1W9eWlyyYOOeK4ZPFMl/PrJye3zpxsVzwgsvuzH+3Gv5OXRebcxq9gFcTwHy\r
+pHzexHFmVm/UWBhBSww6HUtYm92axRuMFkHPaHRECxaTcrj4iMFUb+LYmXZw2YGxwzk7nLXD\r
+L+3woh2O2OEuO9xkh1126LCDZIcyO+TawWEHzg4f2+H3djhjh1N2OGGHhynaFjtAlKKW2wGJ\r
+8nYgdjQYlOoIJTlohwE7tNmn4akIaivTa4e4HUQ7jNohYYdhWk0qR/MFjveCY6/Pe2hS9bMi\r
+d2XSEduST2tQiezzNWyEzXObQZdnD9k1BXltEiwsuj8MiyV5BBok+fnw/UXyCxLUc7MOPQfL\r
+5SefObil5eDT8g+g/vlDrcn3R7h3UZd8ZImUI7AGL+ud4ddammMkCwxsVpaW83oJcWDdrjym\r
+TqpzOEOfU2Fg6tUN+7RXN5Rn01pWS9/a2AxNr45rqwN1T8blyff+/l7fvy3MW6p90wGtIMEm\r
+aI3Ib3yvKCz/u/y8/Kb8s/klP5FfUB7pkUIMEMc0NSSLWMg6KWzm9KC1AIvpnpXXayysZWeM\r
+sCwYdVrwSDwQmk0N08RI4pNSRxekHJmp1kpl253KCoSQkFMBWHZiIJvDzj468ShzqP+E/IBG\r
+FuFdKJBfh4J97D3n+w6xZRNXqH5o4eR5zU7UVQP6oQYprHEQs8Ps8bqdzTE3F4+5WR5lxuvi\r
+Md6GLqRKog5ljHqSXuo9dqhOP/XAP8MDk5yQkqHYQJVoXoieJHFH5FflP45c/c2P/zzxCfRD\r
+h/wt+dty7tGjR5lHwAu5n12nh1z2BfkJeUROyA9z6kkT5bVRHtEcoe/VzSQbpTCxiyavVjAJ\r
+efnuQzOAzMCFnpEr2nNz2eZYrt1iaI5ZOJKPTOeDmA87dmy+Yiqpinj4U7jyV5SlrIWtktoz\r
+ZbEVpx7KZVg+R+FcoxgQ9UWnEijQhkQCeb/+w7u/eePsH379KnMIqmG1/PWypi8uWufeE17e\r
+3XvjzFL5Wflx5gH5h/IYBKAWloFPfkd+lvme/E35u7Lz9iXdm7nqLF9468McNOO8HOgPT2rq\r
+iZXYoFn6SLBYrZzNzJtMOh3PsXaH2SJY4jGbIACv1XAmHWcFa3MsC2znHHDWAWcccMoBIw44\r
+4oDDDtjrgAEHtDmg0QE1Dih3wEwHOBzAOeC/i1/5TzpkYnMUZ9QBTMIBww4YcsCgA3odEHWA\r
+5IBSB4gO4B0wRpEuQFjjgM9bjOYLs/5/mfNnenB1h5CS9GmyjWoldebUbUdU983msMDmwMvy\r
+8nvgp8/AG49O/HRk38T4zXDgD/CLCuWQ85PP9DQUvVG+nuuc2ElS781o7sE9YydfkErtOq3N\r
+YLBoLU6HhlgFXBg9g/bHYkLbY9eZbMSJCugE0QljThh2QmrHRFKPxzK3cvrcW3WrIcg43udu\r
+VF6dajn/wog8/+hRuIt5XHWrn9m4E5/dlzqSPR9UeRxBW7MPdSqLrJKKdUSjMZqIjteJOtbA\r
+6qQsrRJf9cdYj2QCYoIxEwybIG4CrKbsjMpe9svKmxhq6IfS0yA7eYKmIi/C9INtIgx2+a9w\r
+c7l6IlzXUvFbZewt6NcTuE9nk1uktYW2vDy32xZktRYL0ZKiOYV2m93WHwvbwW4PsSTABxgD\r
+GwiEQtn9sZCONfbHenWDOsaqA/SVXmkOkDkwNgeG50B8DmA1xV8kfMUVqd1MXzLCVQ6rDxJS\r
+wrQlFz+V6bHpYCA/jCneEvVpglZ5/XkJRESBvmPFJj5555lHjdXFBYeWfP2u/bcdOrRj287B\r
+ioE5oerWoeXwvXtvPXEUtj/xsyLIftolDj2y9ys6/TqDdvDLN1/n9xwBJluQTxz4psP5LTVf\r
+wXU4hLKYQeLSfB/PzNA5Gac/oLP5iIW34MwtFpstqz9m0zI+8O2KoeWn72qO0Zd91Nd/pq9I\r
+5nynhTqzlCkxU1NSj4QsoNPqcthD51966vgjK3bdVNFbFKo+sefNt74wcjrWxjx+x7e//tzP\r
+9335Fsp70fe/1fvjF441XEZ5b0AlizNhwpL7pG0s0XBAnozB06UamKkBhwY4DZzTwFkNnNHA\r
+iAaOaGCvBgY0UKOBwxoY1GD4oIG4BqIakDSA3UQNEA1UjmtgVANjGjhNCwkNDFP0Xoq3R5P5\r
+osNFjr1T4agandtxpzQwDrjr3nupP8O9uR73ppMESLe0xMnzPrMPQOsyOwSbYNZyQZHHICEe\r
+8/kMnMEbjyl5h4EV1KzXwdl4EU6L0CuCJCZf7lU36NT7bBc8pqIalpng4mYto+/rKUbGkH6p\r
+Dx55aeK1B48yy86P334DXHUbOombIevOH3zn2PG7mQaZS+3gR5++6fn8iT/5KpgGuP7eGyae\r
+35e0Odrf0fc9b5BsFtxQQMwmltXrzTY222tWzuPmGcz1ZqPWIxiMiutj9FhnjKzbxuq1mN8T\r
+AKfWNpYNp7NhNPV+Z1Xy5U5qkIQpC1qUskqVmQcnyUAjJ9MuGUCxVGqV3RySnUnzBCPwJgwe\r
+PToxNsLd8NkrafvEsefRZm2h9blpe8p1Kr8nhgXSbxid0SjwYLKYcA6swYiM68DC6nQG+pMO\r
+24gARwQ4LMBeAQYE6BBgowB1AuQL4MJVFOCcAH8Q4IwALwpwQoCHBdhF0RpTaL8U4JQAmXTS\r
+CDUClAkAogAOATB+r0RiZykxRGwToDzVwIwLMCbAaQFGBegVQBKgVFD68RnwhADDtDVKET7n\r
+uZoznVvzxX3b9HOt1NbPfFwhVCq6pxNyyubbI2xyVZjdr4D+tbYlS22fvYuR1SKNPvuzTge4\r
+5T3peEr5bTvjvVe/7ofeZuuiv5Gg+rvqn9Sc/vnUr2blOtS6h4jyo2smCcJ+uhy5lnwhjQQX\r
+/NS2QluJK/oOyeMw4mYqSRHrJ41Kdyw72IMkqvkxacSrSLORLES8hdi2GnHDzKNkFravxnsV\r
+1lcj/iy8r8TLh1eBVqF1kNRjuwLz4pWDF1EupMfgvQlp3YxlN+JswqsaaTyE9WLkoYjrJ19B\r
+nDYctxHvhQhfmOTFoZQRNoLXFryUvg0Kfzq/yiOd2XOwGp5jZuNfH/MndpA9xz2k0WsWaf5N\r
+69F268p0J/Qz9F81EMPtWSeMhcYXTf9h3mJZYHnU2sBfy9/PnxdKhLjwW1uH7ff21Q69Y5Pj\r
+iONTZ4driWvI9XN3o/txj8lztecP3oeoRCvIUrS8apTOkzC5HAvPsy8iTGkNQHda7hvTa4A5\r
+A9Yg2UtHOpJlFk3e9mSZQ5xbkmUNMZN7kmUtxp/fTJZ15FqMINSynjigJFk2EAtUJ8tZ0A3R\r
+ZNlIZjDPpP+3hBLmN8mymVSw+mTZQrLZxQr3nPIr76PsF5JlICLHJssMsXChZJkl89BAqGUO\r
+cbYmyxqSzd2cLGtJgPtGsqwj57hnk2U9ru3xZNlAZmjeSJazmDc1f0+WjWSB/hfJsolcbjAm\r
+y2ZypSE1loWUG16t6draNdB1bXub2NYy0CK29vRe09e1tXNALGydJZaVzi0Vl/f0bN3WLi7r\r
+6evt6WsZ6OrpLsladiFambgOSdS3DMwRV3S3ljR0bWlXccX17X1dHevat+7c1tK3tL+1vbut\r
+vU8sFi/EuLC+sb2vX6mUlcwtKZ1qvBC3q19sEQf6Wtrat7f0XSX2dEznQ+xr39rVP9Deh8Cu\r
+bnFDyfoSMdoy0N49ILZ0t4mN6Y5rOjq6WtspsLW9b6AFkXsGOpHTK3f2dfW3dbUqo/WXpCeQ\r
+IY31A+272sVLWwYG2vt7uqtb+nEs5Kyxq7unf464u7OrtVPc3dIvtrX3d23txsYt14jT+4jY\r
+2oJz6e7u2YUkd7XPQb47+tr7O7u6t4r9ypSTvcWBzpYBZdLb2wf6ulpbtm27Bpdsey/22oJr\r
+tLtroBMH3t7eL65u3y2u69ne0v1oicoKyqYDZSp2be/t69lFeSzub+1rb+/GwVraWrZ0besa\r
+QGqdLX0trSgxFFtXaz+VCApC7G3pLq7d2dfT246cfmF5wxQiMqhKs79n2y4cWcHubm9vU0ZE\r
+tne1b8NOOPC2np6rlPl09PQho20DncUZnHf0dA9g1x6xpa0NJ47S6mnduV1ZJxTzQIq5lta+\r
+Hmzr3dYygFS295d0Dgz0XhIO7969u6QluTStuDIlSDn8z9oGrultT65Hn0Jl+7YGXP5uZel2\r
+0vVVJrF+RYO4phflU4fMiUmEOWJKM+eWzE0OgWLs6h3oL+nv2lbS07c1vKaugdSQLrIVrwG8\r
+riXtpI2IeLVgvQVLraSH9JJrSB/F6kSoSAoROgvvZaSUzMVLJMsRqwfbt2F/kSzDch/2Ur5b\r
+KN0e0k1KMAla9i+plWFpXZKLetp7DpZWYP9WpNCA/bZgayZdkaynkC40s0rPrWQn8tGCkKWk\r
+H3u1I04bxRBJMV7/isa/at9IS/3pljLkay5epRft+a/odiElkUp6gLYonG6n3F+FsB7s98/k\r
+ISJeO129fmxpp7U2SlWhvQEx1lOsKO2pSGKAjtZNsRovMuIaHLED+7fSlUxhtlLaikaolHuw\r
+3JmU6ZUo7z7KQRvtl5pbP478+RW4uG6sp9ztomNeSuFKvZ+2VWO9PzkvVWaNlIsehCqy2I2c\r
+KON20nILlWcb7a3oWHey5xbUOvGfjiMm+7Yk16WbjrEryaXSZ05S3h30u5+O241jiJQ/dZWn\r
+jy1SObVQqasrvR1bByhuK8K34d81yV22HaWijrUluY92013ZmZzxdkpXJKvxvptqRQ9dt+6c\r
+XLrGU1JR9aYjqaci7duL5R46i5Qci+naKDNpp5wqpRa687dgj210bJW3TqodLXRt25NrPUBn\r
+kJJXW3KmCte9FFJMaqleKPu9PSnTL6CdaLgoRVWCmbqprMk2ym9/Bu1uym1beo6qtBWsbcmR\r
+1Blvo/boqvT6dFB9UyXaRqkV/wOZd1DZDCRH7aEcteGfuuKqbvVg3510PdT9pGrzwOck10Ll\r
+25Ps10ut0kCSl+10f3RSDewll2BgGUbulL8SqoeZu6Y1uWdKkjyH/8f9FL56qQQz90dfmpft\r
+yGNDcvd3p3fdzoz9m1qJ9WiDGqi96E3qT11ScuIFFJRdc6HNnEtt5vRZqNrYhfUByk8/lWUJ\r
+ncNWbF+DIzSQZCxOJvchSxf5HDNEl26BdsyyO2ErsZMgxMlqaCYbYClZDBLeJWyrxvsyrCv3\r
+ElhMBhFvMcKXYH0Rwhei7QzidxVea/A6hBeHl4pRihhhvIeT9WKsz8Eer+A30EuBViFUua/E\r
+ej3elyfvdQivxXttsr4C63gncdApPzqi388CJx2HsQl4ZQLECdjzGUQ/g8GPhj5i/jo+K/jY\r
++LPjzJoPmz987EO29EOwfgh68j7/fvT9+Pu97w+/r82yvgcm8hcQ3hlbEHx78Vsbfrv4zQ3k\r
+LZzZW6VvRd8afCvxluYtYDe8ybqC/Kg4WjraOzo4enp0bHR8VD/4zNAzzA+fDgetTwefZoLH\r
+1xzfc5yNPwLWR4KPMNGvxb/GDN0P1vuD94fvZ++7tyR47/JA8O67CoJjd43fxSgv6d9lFuqe\r
+hjXQQBajDFcfZyeDjy11wqU4LSt+B/EK47UGrx68DuGFOQ+iB/EKQ4O0gG2+E4y3+24vuv26\r
+2w/crum9afCmoZvYwX1D+5jHdj27i+mPzgr2dBcFu5fPDnojng26CLtBi8MoT+9WbMkrrIs3\r
+S8FmRLpsU2lw0/JZQXvEtkGDE+YQ0coG2Sp2DdvDHmKfZXX6ddFAcC1eY9HxKCNFDaY665rg\r
+mvAa9uTkmNS+KgeprexdObiSXVE3K1i/fEHQujy4PLz8leVvL/9wubZ5OTyI/+oeq3u2jpXq\r
+ZoXrpLpATt2Met8GV8S5QQDrBj5i3cAALnSEbAhbJ62M1dps3WNVfqJAmEEXaOAkDB1rXF9U\r
+tOqkbnLdqoQ+elkCbknkrVe+pbWbEtpbEmTDpsuajgF8Nbbv4EFS7V+VKFvflIj7Y6sSbViQ\r
+lMIgFnj/MRepjvX3DxTRDxQVYXknfpOinUUI3NyvQkm6nRT1Qz+aqH7aCYoUBLUO+F2ktCFA\r
+6QfYe3M/Ub6UxiK1k9K7P0mOdla/aMGz+X8DdGNr8gplbmRzdHJlYW0KZW5kb2JqCgo2IDAg\r
+b2JqCjEyNDM0CmVuZG9iagoKNyAwIG9iago8PC9UeXBlL0ZvbnREZXNjcmlwdG9yL0ZvbnRO\r
+YW1lL0JBQUFBQStMaWJlcmF0aW9uU2VyaWYKL0ZsYWdzIDQKL0ZvbnRCQm94Wy01NDMgLTMw\r
+MyAxMjc3IDk4MV0vSXRhbGljQW5nbGUgMAovQXNjZW50IDg5MQovRGVzY2VudCAtMjE2Ci9D\r
+YXBIZWlnaHQgOTgxCi9TdGVtViA4MAovRm9udEZpbGUyIDUgMCBSCj4+CmVuZG9iagoKOCAw\r
+IG9iago8PC9MZW5ndGggNDQ5L0ZpbHRlci9GbGF0ZURlY29kZT4+CnN0cmVhbQp4nF2Ty27b\r
+MBBF9/oKLtNFIJHUIwEMAY4cA170gTr9AFmiXQE1JdDywn9f3rlsC3Rh43DIGZ0hhnl32B38\r
+tObfwjwc3arOkx+Du833MDh1cpfJZ9qocRrWtJL/4dovWR5zj4/b6q4Hf543myz/Hvdua3io\r
+p+04n9ynLP8aRhcmf1FPP7pjXB/vy/LLXZ1fVZG1rRrdOdb53C9f+qvLJev5MMbtaX08x5R/\r
+Bz4ei1NG1poqwzy629IPLvT+4rJNUbRqs9+3mfPjf3uVYcrpPPzsQzyq49GiqMo2shGuX8CW\r
+/AouhRsLroRNAa4Zl9yG5yvwi3DZgF9ZX+Jbxg34jSzxjjU78I513sHvrC/n94zDQRfkGkz/\r
+eg9O/qijkz/cdPLHtzT96zcw/esdOPlrcPJH75r+jTD9G/Su6d+IA/1reGr6N+hd078Wpr9B\r
+X4b+FWoa+hvcuaG/kTj97RZM/0rO0L+UOP0r3Imhv5F48pc4/S3uwaT7F6Z/hX4N/Uth+pfi\r
+SX8rcfpb1LT0t+jXJn/coaV/JXH6lxKnf9nJQKbJw2ji7fwZeTXcQ4jjLg9M5hwTPnn39w0u\r
+84Is+f0GueDk4wplbmRzdHJlYW0KZW5kb2JqCgo5IDAgb2JqCjw8L1R5cGUvRm9udC9TdWJ0\r
+eXBlL1RydWVUeXBlL0Jhc2VGb250L0JBQUFBQStMaWJlcmF0aW9uU2VyaWYKL0ZpcnN0Q2hh\r
+ciAwCi9MYXN0Q2hhciA1MgovV2lkdGhzWzc3NyA2MTAgNTAwIDI3NyAzODkgMjUwIDI3NyA0\r
+NDMgNzIyIDcyMiA2NjYgNjEwIDI1MCA1MDAgMzMzIDQ0MwozMzMgNTAwIDI3NyA1MDAgNTAw\r
+IDUwMCA3NzcgNDQzIDMzMyA1MDAgNTAwIDUwMCA1MDAgNzIyIDUwMCAyNTAKNzIyIDMzMyAz\r
+MzMgMjc3IDcyMiAzODkgNTU2IDUwMCA2NjYgNTAwIDcyMiA3MjIgNzIyIDcyMiA1MDAgNTAw\r
+CjUwMCAzMzMgNjY2IDg4OSA2MTAgXQovRm9udERlc2NyaXB0b3IgNyAwIFIKL1RvVW5pY29k\r
+ZSA4IDAgUgo+PgplbmRvYmoKCjEwIDAgb2JqCjw8L0YxIDkgMCBSCj4+CmVuZG9iagoKMTEg\r
+MCBvYmoKPDwvRm9udCAxMCAwIFIKL1Byb2NTZXRbL1BERi9UZXh0XQo+PgplbmRvYmoKCjEg\r
+MCBvYmoKPDwvVHlwZS9QYWdlL1BhcmVudCA0IDAgUi9SZXNvdXJjZXMgMTEgMCBSL01lZGlh\r
+Qm94WzAgMCA1OTUuMzAzOTM3MDA3ODc0IDg0MS44ODk3NjM3Nzk1MjhdL0dyb3VwPDwvUy9U\r
+cmFuc3BhcmVuY3kvQ1MvRGV2aWNlUkdCL0kgdHJ1ZT4+L0NvbnRlbnRzIDIgMCBSPj4KZW5k\r
+b2JqCgo0IDAgb2JqCjw8L1R5cGUvUGFnZXMKL1Jlc291cmNlcyAxMSAwIFIKL01lZGlhQm94\r
+WyAwIDAgNTk1IDg0MSBdCi9LaWRzWyAxIDAgUiBdCi9Db3VudCAxPj4KZW5kb2JqCgoxMiAw\r
+IG9iago8PC9UeXBlL0NhdGFsb2cvUGFnZXMgNCAwIFIKL09wZW5BY3Rpb25bMSAwIFIgL1hZ\r
+WiBudWxsIG51bGwgMF0KL0xhbmcoaXQtSVQpCj4+CmVuZG9iagoKMTMgMCBvYmoKPDwvQ3Jl\r
+YXRvcjxGRUZGMDA1NzAwNzIwMDY5MDA3NDAwNjUwMDcyPgovUHJvZHVjZXI8RkVGRjAwNEMw\r
+MDY5MDA2MjAwNzIwMDY1MDA0RjAwNjYwMDY2MDA2OTAwNjMwMDY1MDAyMDAwMzcwMDJFMDAz\r
+MD4KL0NyZWF0aW9uRGF0ZShEOjIwMjEwMjA5MTIzNjM4KzAxJzAwJyk+PgplbmRvYmoKCnhy\r
+ZWYKMCAxNAowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMTQ1NzggMDAwMDAgbiAKMDAwMDAw\r
+MDAxOSAwMDAwMCBuIAowMDAwMDAwODQ5IDAwMDAwIG4gCjAwMDAwMTQ3NDcgMDAwMDAgbiAK\r
+MDAwMDAwMDg2OSAwMDAwMCBuIAowMDAwMDEzMzg4IDAwMDAwIG4gCjAwMDAwMTM0MTAgMDAw\r
+MDAgbiAKMDAwMDAxMzYwNSAwMDAwMCBuIAowMDAwMDE0MTIzIDAwMDAwIG4gCjAwMDAwMTQ0\r
+OTEgMDAwMDAgbiAKMDAwMDAxNDUyMyAwMDAwMCBuIAowMDAwMDE0ODQ2IDAwMDAwIG4gCjAw\r
+MDAwMTQ5NDMgMDAwMDAgbiAKdHJhaWxlcgo8PC9TaXplIDE0L1Jvb3QgMTIgMCBSCi9JbmZv\r
+IDEzIDAgUgovSUQgWyA8OTgzQkQ0NTNFQkYxNDhBQ0Q5Rjg3QkFEMUM0NDgzRUQ+Cjw5ODNC\r
+RDQ1M0VCRjE0OEFDRDlGODdCQUQxQzQ0ODNFRD4gXQovRG9jQ2hlY2tzdW0gL0JBNDQ2NDRC\r
+RDdGQkVBRUJEMzlCNTg5NDk1MDg3MzBFCj4+CnN0YXJ0eHJlZgoxNTExOAolJUVPRgo=\r
+--------------8B5F937C37FF6F878A09D1F7--\r
diff --git a/upstream/t/data/spam/extracttext/gtube_png.eml b/upstream/t/data/spam/extracttext/gtube_png.eml
new file mode 100644 (file)
index 0000000..63769cf
--- /dev/null
@@ -0,0 +1,1086 @@
+To: to@example.com\r
+From: User <from@example.com>\r
+Subject: Gtube jpg\r
+Message-ID: <ed48fad8-753c-458b-4433-76b5659a0f9e@example.com>\r
+Date: Tue, 9 Feb 2021 12:59:05 +0100\r
+User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101\r
+ Thunderbird/78.7.0\r
+MIME-Version: 1.0\r
+Content-Type: multipart/mixed;\r
+ boundary="------------4AF91C32E325771A2459DE43"\r
+Content-Language: en-US\r
+\r
+This is a multi-part message in MIME format.\r
+--------------4AF91C32E325771A2459DE43\r
+Content-Type: text/plain; charset=utf-8\r
+Content-Transfer-Encoding: 7bit\r
+\r
+\r
+\r
+--------------4AF91C32E325771A2459DE43\r
+Content-Type: image/png;\r
+ name="gtube.png"\r
+Content-Transfer-Encoding: base64\r
+Content-Disposition: inline;\r
+ filename="gtube.png"\r
+\r
+iVBORw0KGgoAAAANSUhEUgAAE2MAAA20AQAAAAD+uGylAAAACXBIWXMAAC4jAAAuIwF4pT92\r
+AAAgAElEQVR4nOy9vbK0yJ2vm6gUgwyFaFOGQugS2mxDIXQpugSZbXQIdshoc1/SMCFjzH0J\r
+QnGMYw4ntjFEbDZ5+H9kknxUFUWx8l2r6vdIb9eigCTJfCqB/MJYAGJhvnUEwBsB20A8YBuI\r
+B2wD8YBtIB6wDcQDtoF4wDYQD9gG4gHbQDxgG4gHbAPxgG0gHrANxAO2gXjANhAP2AbiAdtA\r
+PGAbiAdsA/GAbSAesA3EA7aBeMA2EA/YBuIB20A8YBuIB2wD8YBtIB6wDcQDtoF4wDYQD9gG\r
+4gHbQDxgG4gHbAPxgG0gHrANxAO2gXjANhAP2AbiAdtAPGAbiAdsA/GAbSAesA3EA7aBeMA2\r
+EA/YBuIB20A8YBuIB2wD8YBtIB6wDcQDtoF4wDYQD9gG4gHbQDxgG4gHbAPxgG0gHrANxAO2\r
+gXjANhAP2AbiAdtAPGAbiAdsA/GAbSAesA3EA7aBeMA2EA/YBuIB20A8YBuIx0fb1pjig48A\r
+vg43bWsMkwy6VVUEK2cLI32yEcIw7h0EZKucvq0z29Iy/y2rwp26yyNnAL4OZ9n2X9u2kVTl\r
+Vdsyu2HbP2Hby3KWbf9z27ZKnLpiG1m1si2HbS/LWbZlm7YNtHt61TYKdmnbANtel5Ns67dt\r
+68Sxq7aVa9s62Pa6nGRbu22bK8Gu2VasbWtg2+tykm3Ntm2NlmDXbMvXttWw7XX5WNsqLcGu\r
+2ZbBtrfiY20jn6qxBLtmWwrb3op7bQkt5fxwt8Vh27aBLGq5Wk0CCmwbF3uqAqmz+T6w7YX5\r
+YNsu9J/UBzS3zVaw7b34UNt6Ltau29YksO2t+FDbOnrotPXFB7SwrYVt78WH2tZS9S2XYNu2\r
+dbDtvfhg2/i/120zsO2t+FDbpFTrrtrWw7b34mNtY2064wOCbe/Nh9pW89NoD9uAEMW20m7b\r
+NsC292KvbY0zoA338Atim9hljROo4j+GA7a5PfNHzwd8Zvbaxn3RqGnUOJfChdaYtE8Gae7s\r
+jCvlDtjWjEF2l16CaA1KuZdip2099w4aBeuNMyBcENs67rBG7eylBp7LR2kftK2VICpz9xIO\r
+vhI7beu4FBsFa31/jXBBbGtZSXIk18Dl82pbwsZTgtjWiLfS3xK8DDtta7kUq6SnWslrwgWx\r
+reEuRIP0JOLAC/4ofEB7batZ7t74yzZ4CXba1vDwglGw2riSK1wQ22pWkhyR6+sQlkybtq1b\r
+rsS2ioXtJm/BS7DTtpqvmaNgVWhbNbetYiU7Y/QxYXD3bz6g++2kYpsI23pvwWuw07bK2Wb8\r
+1S1cENtEMz8K/rhtvxDNGu8teA122iaDWVQwubqFC2yb9vhujLubu2tbu+5NybZJr3LpRP78\r
+KYJPw17bipptG0w2eNumBbWtrOi235SNaNbfs41qg7dts3ybaMbAnj9F8GnYa9t4O1awYIX2\r
+jpwvUFvCKAh1aKsS3ta6BtIwoGVP8bVt3JYwXkabcd/xow2FBV+dnbZdRnXINhKocbYFC2xb\r
+OnpSkkLa4nTPto5H+G3ZlnGbmMn4qOBl2GlbytUZo2CJ82a+wLZlfO3kauCcvrxpmx+9vGUb\r
+/RNnYdsrsdO2jPNdBJPekfMFti3n5wKeboEVum9b4caTTtVqbBs1islDhuwAXoOdtuV7bCtI\r
+kOER2+wV20rat4dtr8ZO2wrOdxGsD23rA9tKUpJLpCboRhkGtLCN9t20jWtPePflhRZ8ZY7Y\r
+VvCa2UJoG+21z7arZZvYlkjA4GXYaVsZ2KbMFtg2Co4fVHfbVmzblvAhO9j2ajxq27T9bCGw\r
+jRwJbGu0LXXTtuymbRcL216L/bbVYlvh1swWnG1a4MnIgvu2pVdts2pbi04gL8T+UTBqm8/9\r
+2YIbBfOgbZdt22hT2PaCPGjb4HuBzxcm27qdtnFPuOSWbS1sezUet81l/2whsM0VWvdt4zb3\r
+ayP8RtsWHoIvz4O2WePLs9nCMduozR22vREHbJtG+E0Lx2zrYdt78ahtle9OO1tY2sZf37WN\r
+uyzdsQ1dxV+IR22rTTDmalqAbWAHj9rm5LHzhSu2lbJlbrdtowGnsO2NeNS2zgSjYKaFg7ZV\r
+sO2teNS2aWzybGHTtgG2gRmP2kb3aq7vbrBw1LYMtr0TD9vWuefQ2UJg26SHGyt/1DZUfrwa\r
+D9vmJx6aLWzaZte2iVvjF34Rtr0Rj9vWhra1a9uC0e1P2oaL6KvxuG02tM3eti2XLXML24A9\r
+ZJuOKJ4tbNumQ1hgGxAO2NaHtvU3bdOaudxese32UwJsezUO2DaEtg1L28KpxXXW59A2/sbs\r
+sg1Ti78cj9qWhralW7YFIa5tk6F/49PDHtswm9ar8aBtrYhAa2YLC9u6gnd2M4bk9pptt9oS\r
+2La2fOLkwCfjA2wrp9lBtB44t9421/VjR6s8v7GoQaXbC/GgbV1oW7dlGzcgaKGmL+8Iehyx\r
+f3T13WEbe1vDthfiUduS4L4tWFjaJo5I9Ugf2MZvj+ydbbf7t4ltGE/6QjxqmwltMxu2cUmm\r
+JZK8e7kNbeO7Omdbf9M2mQ8Ctr0QD48n9RMuzBZC22j+NrFNBpzWgW08ayUpJ7M83xyXIDMr\r
+wbYX4mHbCn3f6HwhsK26eEcG7iEiL3RxtuUysI8Xm5u2WeNnuQSvwcPjSXP/pBkujDdq3jbD\r
+01RK6KMznQnec8WlndE5LfktbCvbssm2RK/F4EV42LbEmsk2vzA+hDrbalM0zpHKXFynJLGN\r
+hte37s0bhm2TPwrXhD/e8jnbKlPWmAn1lTgwnnTquzstNPR2DuuHxjhH6qn7rbdNepe3rovv\r
+yjZ6F4xl23hVeerpgm/K+ba1gSPNNABZK3z5i8wuZ53ZtK2R0g+8DAdGL0+jYKaFwLYucEQG\r
+KeQ+IO7sy18sZtTatK01ePPQa3Fk9LIf4TcttJNtfeCIvI2o9AFZf51dzBY42dZMtnUYBPNi\r
+nDR6ebzzd7YNoSN+/JWzzV1nFzOhhralzrbeYBqQ1+LI6OWS18wWusk2GzpS+wW1zZV8i1me\r
+J9vaybbB4G24r8WjtvXhrDPTQh/YVpupkqzzz6euJ67q17rHh6Vt3WQb3eQVT58h+Dw83He3\r
+cgXXbMEGtnXhg6T30dnWaNOCe5ZY2jYEtrV4SHgtHq5h6MIasGkh7BkUtqS3y8aA4dqNf5ev\r
+90ZLwmvxeeqzuvwbRwB8OLANxOMT2VZ86xiAjwa2gXjANhCPz2NbW3zrGICPBraBeHwi28pv\r
+HQPw0Xwe25ryW8cAfDSfx7b6W0cAfDifx7bqW0cAfDifxzY0wL8+n8e2v3/rCIAP5/PY9vO3\r
+jgD4cD6Pbf/81hEAH87nsQ28PrANxAO2gXjANhAP2AbiAdtAPGAbiAdsA/GAbSAesA3EA7aB\r
+eMA2EA/YBuIB20A8YBuIB2wD8YBtIB6wDcQDtoF4wDYQD9gG4gHbQDxgG4gHbAPxgG0gHrAN\r
+xAO2gXjANhAP2AbiccS2OnyxGgC7OWBbf+/92wMKTLDJATEaY27vBdvANgfEMMbcfm8obAPb\r
+PC4GvRn+9uveYRvY5nEx+B3dN2/cYBvY5nEx6Lbt9vveYRvY5nExarLt5m6wDWzzuBgs280a\r
+N9gGtnlYjIEeSI0pbm5yPD7glXlYjJ7u2ZrbVSAAbHLAtpT+c7MKBIBNHrato2JtgG3gAA/b\r
+1vItW5WeHxXw8jxsW8N71Lfb5QHY4nHbuGK3gW3gcR62TUq1FraBx3ncNr5ja/GmZPA4D9tW\r
+8dNoB9vA4xy1Dc0F4HEetkZaEXrab9AWBb2s6nNqK1Vx/c3GLfCWPGNby93cjK1YrIGb6tus\r
+TridtDGokgMLHrVtkCKLhaq445EZpCtvy/3H26wyf+Ovb/eBA+/IUduo4DLcFcR00pW3MVSa\r
+tZlh2/o7vZLAO/K4bSV/lCLUWKiZVoqxij/a3xhDBV9nbvdKAu/Io7b1U4lFQo2lmdERf9Kj\r
+t/212NbeG5gF3pDHbfN/tjIaxtSs2SA9ettfiW31vYFZ4A15wrZGRsOYijXjgX8F2ZY42/BQ\r
+CuY8YVst104jmnUyqJlu4si26t4wQPCGPGpb0IhQmVE4si1vqFAzece2Zf8i28ZSD7aBBQ/b\r
+NtWijTa14yV0LN+o3aAxJfXobU3xf0bbhvEyWqPCDcx5xraMNRtLMWrCon5vVcr+sW05+omA\r
+Jcdt41ZSsi3l5ixqJW3INm5o6PlO7tyogi/PM7aVY2mWUxFHH3XGfSypQGPbyvCBAgDiuG39\r
+3LZqZltnMIgZrHjGtvE/Y4lGF9TxYxSOVjrbEt/IBYDjCdvor2ayreCvaMCC2mZhG5hz3LbO\r
+2VZIEVeEtvEoGdgG5hyyjVoRio67GU22lSvb0E4K5jxpW5uGtqlmzjYA5pxgWylFXCmapf4D\r
+gAXHbWtntg2wDdzlCdu488dkm0yQqrY1sA2sgW0gHrANxONZ2y5z20rYBq4D20A8jvbdvWcb\r
+anbBGtgG4nF0FAxsA4/zUbbhvg2sOTpWfrRNhXK2yRJsA9c5OA8IbAMHeMI27eYRtJNa2AZu\r
+cXBGLdgGDvD43JSFfty0DX1AwAYHZ0LdZdvfz4kieBkOzilutTeltWFvSjvZRitpYAwAE0/Y\r
+puNhnG0FL8E2cJ2D74JxtrVlaNuQzWzDVKhgzsH3XJFtPE/9NC4h54YGtQ3jScEGB9/hN5iC\r
+WxVqbxtdYbuZbRgrDxYcfD9p52zLZrYl3jYD28CKg+9ebkzB9byVt43u51pvG+Y4AhsceK88\r
+PZRWZFvOMwY62y58lfW2Yf42sOJhI3pz0XdamZRnDFTb6Ao7Gqe2YW5KsMHDtg1G3r3BEzl3\r
+oW0Fz8TrbMO8u2DF41c7HtVHL0qoTFmTYiXb1pqsG9eobTT3M+YUBwset63ScaTyvgRfu8vv\r
+S/C1u3hfAtjgcdtYMnpUaPTtHCXb1uvbOdQ2vAsGrHnctlZf/sJ/JN62Qd88pLY1Bu+5Akse\r
+t62TKRjkj4u3zcp11dmGd/iBNY/bNhi9JevlYulsq9hBZxveTwrWHKiBrfQiOcins62Wt+Gq\r
+bb2UfwAEHFCiNfoS70re9F2Kba286Vttw3vlwZoDtg2ubqM1rl8R2TbIo4OzrcZDAlhy5HLX\r
+uDuy+XNAM6tf69EDBCzBzRWIB2wD8YBtIB6wDcQDtoF4wDYQD9gG4gHbQDxgG4gHbAPxgG0g\r
+HrANxAO2gXjANhAP2AbiAdtAPGAbiAdsA/GAbSAesA3EA7aBeMA2EA/YBuIB20A8YBuIB2wD\r
+8YBtIB6wDcQDtoF4wDYQD9gG4gHbQDxgG4gHbAPxgG0gHrANxAO2gXjANhAP2AbiAdtAPGAb\r
+iAdsA/GAbSAep9rW4t1W4BaP2lbxm5evvDK+xgtwwS3Ose2fGhhsA7c4x7ac/zvANnCTU2wb\r
+cv7oYRu4ySm2dbl8wDZwk1Nsa3L+aGEbuMkpttU5f4y2ladECrwosA3E40zbGjRMgJscEKTO\r
+Vt/k/NFcno0NeG1OtW27hQEABbaBeJxq22oFACGwDcQDtoF4wDYQD9gG4gHbQDxgG4jHYdtq\r
+aTgwtBDa1ms/ELRigRVHbRukBb41htpH2bnxI6GxCVTHa3qTnxlN8BIctW3ULKe/qUvbzDZD\r
+/lnTXRkoA96Zo7Y10g/EkGihbb3hQs+0sA2sOGpbxQXZwJ2PQts6w4WeaQweGcCSo7YZvmJS\r
+SZbMbGu4tLOmhm1gxUHbqFAbr5jdyrZaulqaCraBFQdto0JtfD5ouYgLbaO+vRe2LT87quDL\r
+c9A2KtRGn9i2MrTNGPdkmp8dVfDlOWhba/Ju9Kmhgm1uGwlHtumIZgAmDto2OjZQ3ceF2w6m\r
+toSBH1FLO15N//3UeIJX4Kht49WySunPYWZbz5fX0TbUtoE1B22jVtImZc0WthX8D0+kYIOj\r
+ttG924U1q/LAtm4s16i0wzMC2OCgbdVV2yjMHLaBLY7allvbJaxZHdrWJha2gWscta2wthfb\r
+/hH2b2up19uoIiY7AhsctI1sUtvsyrZxA9gGNjhqWym2yaNnYFtqYRu4xhO2DcZ1F59sa2Ab\r
+uM6TtsmEbQvbxk9M5AY2OGbboLY10mawtC2FbWCLo7YZ19eotDPbuHketoFNnrOtldEJsA3s\r
+4mnbKADYBnbxlG2ljnlZ2NbCNrDJc7b1Mgphsq2GbeA6z9k2SL9w2AZ28ZxtPOgFtoGdPGlb\r
+w/+FbWAXT9rW80A/2AZ2cdQ2t1DRQylqQMAunrWto9F9sA3s4ol2Ug0AtoG9PG1bC9vAXp62\r
+jYaOog8I2MXzttVr29C/DWzyRG9KpVnZVsM2sMkTo2BG2nJhmx8FU54UP/BKPGHbIMMPWtgG\r
+dnLUtpxmDLxiG40nLc+LIngZnpiZoTM8iHlumxsrX54YR/AqPGNbwhM0zO7b/Dwg5YlxBK/C\r
+0TmOxmfPsRwj6Ra26RxH5amxBK/BM/O3JW5yrY3528pTYwleg8MzobJxJF01t03npixPjSV4\r
+DQ7bVtDcumPxNlCPI5kOJJx3tzw3muAlODyneNaxWHlLtsksu9Qkb4zMKV6eG03wEjzzvoRM\r
+3pdQWH3NkNgm70soT44neAWeeRdMvmGbexdMeXI8wSvwzHuuCiniytA2956r8uR4glfgmXf4\r
+ySAYfvmyt829w688N5rgJXji/aRGirjEuom1Gr2hy2Eb2OSobTVrRtJRq8Jkm3v3cnlmJMGL\r
+8MR75bnjJJdk3WSbFQthG9jgqG2D4Q6VnZFZKifbannffHlaDMHrcMA2oZEJniv2rJpea9VD\r
+NHCNw7YB8DCwDcQDtoF4wDYQD9gG4gHbQDxgG4gHbAPxgG0gHrANxAO2gXjANhAP2AbiAdtA\r
+PGAbiAdsA/GAbSAesA3EA7aBeMA2EA/YBuIB20A8YBuIB2wD8YBtIB6wDcQDtoF4wDYQD9gG\r
+4gHbQDxgG4gHbAPxgG0gHrANxAO2gXjANhAP2AbiAdtAPGAbiAdsA/GAbSAesA3EA7aBeMA2\r
+EA/YBuLxqG2DvKKPXxe5QVVML4tssuXa9Tfgrfhg2/57tha2vTkfbNt/ztbCtjfng237ebYW\r
+tr05H2vbkM7WwrY352Nt62AbCPhY21rYBgJgG4gHbAPxON+2ENgGQmAbiAdsA/GAbSAesA3E\r
+A7aBeMA2EA/YBuIB20A8YBuIxzO2dUa+yacPtU2/E9v8BiVse3OO21aVNWvUmMTyx8X6dtKW\r
+FpqsM/TJC/SRwbY35xnbjKGiqzL8lXyobePCZBsvWFubFLa9OU/Y9pOhUm0whoo4+iicbbLg\r
+bJM140cC296cJ2z70VCh1Y//zfyH2NazgWqbriEDYdub84RtfzV08RyNkpszvq6KbR0LprZ1\r
+soak+wtse2+esO0vbFsrtjXyIbY1LJjaphuQdH+Abe/NE7b9We7ODN++1fIx2XZxtukGLWwD\r
+T9hm5O7M8O1bpR9sm6inttWyhreDbe/NM7Zl7WhbnYw+kW3j/0u1jRa8beMCbVCbsoJtb84z\r
+tpXUSlCn4y1ZafkGrVDb+PrpRsHwmpKsbGHbm/OMbeNHZqt8fNwcvctG6bxttOBty8YNxjVk\r
+JWx7b56wjR5BM/JrkFKO/rFt9EfPraKjbdxKSpuTdLDtvXnCtrHcqtm2cYlKLzvZVmgbPNs2\r
+7jAWgKwjbHtvTrOtdCWdKXnB2dY72wq+qIJ35gnbRnWalG2rCu58NNnGWzjbLFvJraiw7b05\r
+xbafbUe9jurQtsrZRmuajPerYdt784RtubfNim2yZEpecLZ1sA04nrctp+WOOkzObPtZa0Bk\r
+zWibhW1vz/O28dCDdmWbDW1rU766osfRm/OEbQWPO6g4hFacUtu4ZzhsA0uetq3mag2x7bJh\r
+WwvbgOME2ygIHlzlbGtntlHfD5Pqo+mJMQdfj6dta7gf0T7bWtj23pxhWwLbwC6etq3lHrxs\r
+W3fdtgtsAyfYpiNeYBu4z9O29TIGYWabzP4B28CCp22zMgYBtoH7PG8bjXKBbWAPz9tGN24l\r
+7tvADp63jef5uFEDIgUdbAOP2yYztw0ymE+r2YzJYRvYwQm2DbdtkwXYBk6xzTawDeziDNsG\r
+k93pA2It+oCAc2yzVbbu3/YfsA0sedy23FoethfYVmfrvrt1HvSmJD8tbHt7HraNxyH0Mp0R\r
+9wGxZNt6FIy3TbqNc80JbHtznrVNLdoYcxXa1pYYcwWO2EbGdJu2bY4n5XHNTQrbwBHbuI0q\r
+8VdSte3GWPlxgzrFWHlwwLaaa9YC2woqszbmAZnZlvHTBWx7cx63jZ8+A9tysa24OsdRIR5m\r
+mHXm7XnYtoYHvV+8bfTUUOX35m/L6AqMGbXenQO2lTy7qbeNOlTmt+amFOPqC+amfHsetq3l\r
+giv1ttUXnuj0yry79OKhZNQsp5meMe/uu/OwbZ1JZHy8H71cNDLnczineMlzisvThClrfq9C\r
+gTnF350Dtsl7EoLxpIYF23hfAr8ssjb+nTHfwbb35mHberatWNq2+S4Ytk1f3wHbwOO2DUb0\r
+Ckcvm8V7rlp9zxXb1oqHVCZ+D9vem4dtc9fMcPRycuUdfuMfF/eSPyoT/wrb3pvHbdNr5nz0\r
+8vb7SfmRohcPabAM+u6+OY/bpqVYOHo523j3ckm29aNtg75NrTIJbHtzHrdNCq7Jtkpe5F25\r
+y2vwXvnRtMRv0JgLbHtzHrdNvfK2tbLY0PXU2dZQ6Ud9Jyu+peMOlZ3JYNubc8C2ZQOULHLr\r
+u8MvNKQgF4XWyrhn8MYcsA2Ag8A2EA/YBuIB20A8YBuIB2wD8YBtIB6wDcQDtoF4wDYQD9gG\r
+4gHbQDxgG4gHbAPxgG0gHrANxAO2gXjANhAP2AbiAdtAPGAbiAdsA/GAbSAesA3EA7aBeMA2\r
+EA/YBuIB20A8YBuIB2wD8YBtIB6wDcQDtoF4wDYQD9gG4gHbQDxgG4gHbAPxgG0gHrANxAO2\r
+gXjANhAP2AbiAdtAPGAbiMejtvFr5fNpubvwayMB2MEh25JpGbaB/RyyLXjTKGwD+zlmW+6X\r
+YRvYzzHbppcvwzawn2O2TX7BNrCfY7Zd/DJsA/s5Ztv0UArbwH6O2TbtBdvAfh63rbS2mqpA\r
+YBvYzyHbWtgGjnDItt4Ubhm2gf0csm2AbeAIh2yzsA0c4aBtuVuGbWA/x2yrcrcM28B+YBuI\r
+B2wD8ThmW527ZdgG9gPbQDwO29ZkrJraVl/u7QjAWbZVKN/AfQ4/JcxtC3rzAnCNc2wbwkF/\r
+AFzhoG3F3LagmR6AqxxuJ53Z1gVj/gC4xjm2tZjgAezgYI+jcm5bk9zdEYBjtnXjXjPbUN0G\r
+9nCwp/jCNlS3gT0cso1G+M1sQ3Ub2MPh0cuhbahuA7s4PDNDaBuq28AuDs9xFNqG6jawi2O2\r
+FXPbUN0GdnFK2YbqNrCLU+7bUN0GdnF4jqPQNlS3gV0cnuMotA3VbWAXB22btZP+G6rbwC4O\r
+zqg16wPyS1S3gV0cbJWf9d01KNvALg72OMpgG3icgz3FF7bhKQHs4eAIv3RuG2pAwB5gG4jH\r
+Mduay9w2tCWAPRyzrZ3b9h3aScEeTrHtB9gG9nDQtmRm24/ocQT2cIptPXpTgj0cs62b2ZbA\r
+NrCLM2y7DGgoBXs4w7bUoukK7AG2gXic8ZSQUrspAHc5x7YaTVdgB0/Z1sI28AhPtSV42xo0\r
+lIIdPNUq721r0XQFdnDctpQ/YBvYz/H+bfJODjfv7gdEDbwcB3uKy60abAMPcXCW54xtq6YZ\r
+7MvzowZejkO29aNt463aYKa3cxQfEDfwahyyrTa5bU3ZwzbwEAdnZiDb8sbbhoZSsIeDthXj\r
+g4Exv5ze4YeGUnCfw7POkG2/hm3gEQ7PqNWP//3d9DZcNJSC+xyeLXCY2YaGUrCDwzOh2vHj\r
+J9gGHuHwLM+2Mqb3tqGhFOzgmG30V2OSwDY0XYH7HLItp786k062oaEU7OAJS9A2Ch4EZRKI\r
+B2wD8YBtIB6wDcQDtoF4wDYQD9gG4gHbQDxgG4gHbAPxgG0gHrANxAO2gXjANhAP2AbiAdtA\r
+PGAbiAdsA/GAbSAesA3EA7aBeMA2EA/YBuIB20A8YBuIB2wD8YBtIB6wDcQDtoF4wDYQD9gG\r
+4gHbQDxgG4gHbAPxgG0gHrANxAO2gXjANhAP2AbiAdtAPGAbiAdsA/GAbSAesA3EA7aBeBx8\r
+r/zWa+Tb1DbZGXECrwpsA/GAbSAesA3EA7aBeMA2EA/YBuIB20A8YBuIx+O2lTfWwjZwC9gG\r
+4gHbQDxgG4gHbAPxgG0gHrANxAO2gXjANhAP2Abi8YRt9Kcu1hoMbAO3OGxbk7cmt60hv3pD\r
+H2gnBXd4wrbapLbm9vnGmAtsA3d5wrZqVKwyyfh3ZYyBbeAux237vTHJwJpxL6QStoF7HLft\r
+d6NiPWtG/zUFbAP3OG7bb0fFfmLbOrIth23gHs/Z9qMUarAN7OK4bd+Nin0/2ZbBNnCP47aZ\r
+tDN/GP/l459FB9vAfZ6wLR+fRcd/ua2TMZwUtoF7PGFbaekRgWxLLf2DbeAOT9hGtbrjv8zW\r
+o2MNbAN3OTietLDNePmkS+ioWpXDNrCH52y7eNta2Abucty2y2RbAdvAHp6ybbx8kmDjkm0v\r
+sA3c47htqbetK2Eb2MMZthGwDdwHtoF4PGdbBtvAAxy3LZvZ1sA2cJfjbQkz2wYD28BdTrKt\r
+gm3gPufYVhnYBu5zim3UnRK2gbucYlsF28AezrCNn1NhG7jLGbZ1sA3s4gzbWtgGdnGGbc0o\r
+23ewDdzlDNtqYxK0XIH7nGHb+EiKdlKwg3NsK2Eb2MEZttEcbrAN3OcU23LYBvZwgm0DxiWA\r
+fZxiWwnbwB5gG4jHWbah7y64D2wD8TjLthq2gbucZVsF28BdzrINfUDAfU6qb8OYK7CDU2zL\r
+bQfbwH1OabmiLm6wDdzl4Fj5ZGZbYtF3F+zgFNvQUxzs4gzbKtgGdnGGbfX4xQRYrQoAACAA\r
+SURBVG8T2AbucZZtP8A2cJczbKMxVz1sA3c5w7ZuXIZt4D5n2NYbk/YGtoF7nGGbNaYYYBu4\r
+y6O2bdImZ4QCXp5TbANgF7ANxAO2gXjANhAP2AbiAdtAPGAbiAdsA/GAbSAesA3EA7aBeMA2\r
+EA/YBuIB20A8YBuIB2wD8YBtIB6wDcQDtoF4wDYQD9gG4gHbQDxgG4gHbAPxgG0gHrANxAO2\r
+gXjANhAP2AbiAdtAPGAbiAdsA/GAbSAesA3EA7aBeMA2EA/YBuIB20A8YBuIB2wD8YBtIB6w\r
+DcQDtoF4wDYQD9gG4gHbQDxgG4gHbAPxgG0gHrANxAO2gXjANhAP2AbiAdtAPGAbiAdsA/GA\r
+bSAesA3EA7aBeMA2EA/YBuIB20A8YBuIB2wD8YBtIB6wDcQDtoF4wDYQD9gG4gHbQDxgG4gH\r
+bAPxgG0gHrANxAO2gXjANhAP2AbiAdtAPGAbiAdsA/GAbSAesA3EA7aBeMA2EA/YBuIB20A8\r
+YBuIB2wD8YBtIB6wDcQDtoF4wDYQD9gG4gHbQDxgG4gHbAPxgG0gHrANxAO2gXjANhAP2Abi\r
+AdtAPGAbiAdsA/GAbSAesA3EA7aBeMA2EA/YBuIB20A8YBuIB2wD8YBtIB6wDcQDtoF4wDYQ\r
+D9gG4gHbQDxgG4gHbAPxgG0gHrANxAO2gXjANhAP2AbiAdtAPGAbiAdsA/GAbSAesA3EA7aB\r
+eMA2EA/YBuIB20A8YBuIB2wD8YBtIB6wDcQDtoF4wDYQD9gG4gHbQDxgG4gHbAPxgG0gHrAN\r
+xAO2gXjANhAP2AbiAdtAPGAbiAdsA/GAbSAesA3EA7aBeMA2EA/YBuIB20A8YBuIB2wD8YBt\r
+IB6wDcQDtoF4wDYQD9gG4nHftsFkpxypM8Up4TzIYC7f4rBgi9u2NSlZkp5ypMb83tg+OSUs\r
+xpTLI2z9LBpjcnJOyDuOQD+eNi1m/Ldxf96gzq8fYge6+xZ6Gn6L7oV/Hfdta06yrXK2DcfD\r
++O9wgbLp36fFcluFUaT0qm3Gnmzb+tz+Ge6+BWxzkG31SbYZZ1t3PIz/nAVY2qHwS/22bZ04\r
+dc224mTb1ueWh7tvAdscZFt1jm2Dt609HsjP4cKYTX3hl7pt2xoyqbxqW3aybatzG/Jw9y1g\r
+m4NsM+fY1nvb6sNhDLOYjNnUFn6p2batYseu2paebNvq3Lo83H0L2OYYbRtOsq3zth2vdOmW\r
+ttWFX6q2bVOTrtl2Odm21bk1ebj75i7lfIt3t+2UGpDRtj+xbcNx29qlbVURLm2oMGgJds22\r
+5Fzb1uem+8E25sVt67QEu2abgW0xuWtbT9VVJ9CacohvWzv61Jnkum0lbIvIDtvKUw7USjaf\r
+aNvIXdsassjIEbktI7CtoNqdgv/ezcfZ5nlz2845UMuJGN+2gv5T8ha08dy2DrZF5cVtq+lg\r
+7VXbhvE2AbbF475tJ7Vsiimxbaso9r30B9iwzcK2qLy4bVxXOMiDzpZtFWyLyavbxl/dsC2D\r
+bRF5bdu0VKv4v1u21bAtJq9uG29QSQlHf8O2b8lr26aVhTVs+xy8h22835ZtDWyLycO21bPE\r
+6C+zbyUvbatDEKhMqTV/FrbpDq2sHebNY1I35zzoJD+GRMJogggtbdMrZ+dysDPuNOxe2zS+\r
+VUmnMT/1wZRsm8ag8u73rt1rdm78pbetu/arNWXrV1GQo23V0umThoZ8c+7Z1ksHV7nRrqjm\r
+nXKSsn0Up79oxWnPG9VFrTVb3JPnMnBzuBPQGGkndb0bKZislrUNbcVZNR6iKita2eSN3uNT\r
+Wg9JZ37Q9vVco17W/m/qx1Y0WSu9o2qXO4/aNiQS3+pv0uKVq9CmIGtak7FtkiKd8aVSrW1j\r
+4blxEy115uSo1Dm1agy0YjAagYG3HU+j4sTLORnJtl7TzG/e8hcchXP6UX8rHrJt/Kvl5PO2\r
+qRDybV2wGNzxgmzruElcjrCwreWcbLNKcqySXLJqG69s8lpry1jFpDW/Its6399uw7aas2Uw\r
+bpyVlhrtftvk11T9NIbQczBy4iVl9Rgfsq0Tgxp/8EHTyIbnRgoWgW0V97IrtcQeAtv+JomX\r
+c5DFaFurHvvNa+N8fBPb+IpA/Rc5s71ttaR4zRlT/0myueF9ek61VjNiYVsjwWSSroP2r7Vi\r
+20+8sslZM3V3SBqxrTH6y9+yrfLjWuRbZxtHYcdTAjnNBdqP45FaPlJg27hMtrVylpXXvvNd\r
+SIJzs/zlZBvvxPrID0JuKsk2OV06QEVbd5fGy6ubc/K8lW18CSkpQZLAtkqrGPjb+o9iQsWp\r
+QyqOVyDjS7/QNg3mN6JFr/1rrdj2I+dM83uRpuUYDEltfkvf116lDdsksq0va/QG8H/L9vTd\r
+PdtEj+qvowANx5TyeGDb6DdBtmkMjC9BG+PEC85t4C+9bb/jL3nUjdhWsm2UHj9K4uVWxod1\r
+lG6Zj3GfaFDvZttAXdQ4qb1txt2occb8wB86UpNUNHmlKbewTYP5tZjTaY9HK7b9lVc2v5ND\r
+11LwJZXYVk2XrZVtv5FCoDHu+yZ8pNm0LV/YJqV09Zcxe+VIcuKyoTF/yeTHpONqZK/aixec\r
+G22dTLb9VlYVLgZsW8HpIaerOo+3ChdfaurmkjzvZBvdaJMt4pWz7ReycghsK2QpZRVNtm2b\r
+7tD+SjKsXdtWim25ZO5Y8CVGbDM3bPu1XNJqf2VrwszZ0U7KttHl88/jCRg+fs3DsiirKdP/\r
+kFmNQT/ZVhl3dQ/OrVvbVvBlWWo4eAux7S+uDJVduouXVzdvjS/93su2TpI6sK202tl6LAa+\r
+n/LB2eYuMnPbdIfRNs6lhpPT2ybJ3/xWbJPQyDZ6Jh2ML7jWtv1KNKv8le1x2ypnWzZIEU3X\r
+zjHWY1a3YpvGoBOpOOCZbe7c2EZv23dL2wqxbfz5/Dm0bUziXyxta4wr/d7JtjHRW0lqZ5sJ\r
+bBuLgT/wBy9dWEXzb9u2dRqMmWwzk21/5mM23xk/WkoGFnjb9K5mZZuIaY3Prru2LXpTktOU\r
+sVQi649GT3zM6kYO3ctHFxSyvpibn9u44G2TL0kfEaYS27qLG0Vdu126SV7dvDbOx3exje63\r
+2ba8HdPP23bpSstdYDuyzaQd50M2SAWCNb8c/3HGz20bg+k4R7J/0co6oW6P3jaTjevHzL30\r
+PFrqMnDBl/RkW2/y/rptbg89qDYiuFOl85jbxjfqoW38XEM3+uVo23jyFz7Vjm2rqWaMT49i\r
+0NJZFLIT3U3KIaZza6hgC2zjpKmzmW252MZ1cazzeApkm1XbdPOazrR4B9tcW4La1tB9STrZ\r
+lvHEF1SJSmlpcl5Jv0NWcfzm4lsNUjuNguFgKMeK/0PrRik6HiSjthU9Vx5klqXNRMXUlq0M\r
+k9BK/PUomHHTSveYsmvagjZe9hRf2cYNIVVi/0kyVNJ+MTpBttGPjSNN3ebaxLWA9Py7KWV/\r
+f27NhR8DuObIStIsbKOfVEe3hyWFwDrL75aunT764+ZVwma+kW10t0GJnnAPRW9bzitJxYqk\r
+YBOokqtlFTn/my3bEm6l0TySu6fANnaXf/AXvt40Rub1GsMgW+qrtvEePd818Zf3bKuMXdgm\r
+u4/Ouh9Ne3G2VfSbyDjo8Sg8jUAugZWuQTY4tzrjHijeNk4augmUptYqZ9vGMzKiLdlGNwyF\r
+O4a1bnMjI9/eybZEEv3CSe1tK3glLdZcBPHKC28uP/k8qM+fbKNgmtQ3RFaF1K7In5VkF41d\r
+aRIufrjgy8W2i78b27CtJLeDAuumbXp/NLct5ayn0rPlJiM+YCu2ZfILSvgLMoHuq6ZWKGvD\r
+c+P6s8A2/pFQzBut7vW2XVhb0dnKZThocSPbMrl05O9mGyVIE9hW8kpapAROeGWTqm16I7xl\r
+W8Y55pqjSRpTeNsukjOGi8hWat25vV1t0zrbDdt4XZf43L9r2zQzA2/PJahhsdiqTm1jeUzO\r
+6/X0Gt/jIDhecG7kRpV726akCWzjG2B6rpENKxY4GAcmm/MPrXor23ppQaAEaZNrtokJzVTw\r
+ccG0YVt10zYJjXM74W0GWlncty3xoe61LVvalnPWs21sYtkl3rZCipgt2+xkW7Vpm0SbLwL8\r
+DT0UfJdRHxiTuQ3p4HUeFM2yOZ96nb6VbdKCwNezTdtatS1xNVS9XAK3bcu9SQRJU81tu7iy\r
+hTJ9YVt3zTa5hLOMco2/a1u+ZVvGxpBV4znQ+TeZs63KXHXvdAvG0Qlsy91DLN+L5XxsCqyV\r
+qMk3465kG10nc7kNkQPUWTddmGVzTuX3s42SppAMdbbJZmybdb96zQ5Xu9Bv2VZIfYo2LNHi\r
+z1MNiBQOUrbITRrZVk62lRL1a7alGuSRsq3gncgYbavztnEMRAuKKpn2Lzmzye7g3Gizf9jQ\r
+Nv0hSMyu2dYsbasKTuXmrWwbpG+sVFJt2WYlf0inXG2zV23T5ubQtqB/Wy416KnkKruTh7a5\r
+qK9s02vcA7at7ttKFsHZptf3mW20Tm1TfrahbXpuTrPcJw21eCahbd+nNOGWCq4HGG2bbgRk\r
+c05lvTa/mW3lpm1al5r5J7x7tpVz23I5hJ1sS9Q26S0b2DZ1gb1qW2ZD2wZXNb9lm9myLRXb\r
+cuts4ysiR49sK6ZfQZCOhTsFPTcVPbBtPOhk23jDVn+fTtdPZ9u/7Nq2xL6bbdy3LR+u2DbV\r
+N+y3bfz0tnEabttWL2zzHWa3bevVNr0Xv2tbedU2/gnIJZxtkwtapp15y7DDul3aRp1G9BeY\r
++49eJ0ssNTnqH9Lp+ukSd/bQIZt372hb4RJkmGzTdVRzZK27oyGcbclt2zTtpN3H21ZM0qht\r
+WrK0Ur/qor6yLXvctmJhmxyFH1tyiQ4dZss2Mx3bbtmmT8UcD2+bWdrG4TvbeNPgoUM25yh3\r
+72tbubCtk+wMbOuMN23DtmFhm4wjuGVb5jzojY/xNdvqnbYV3KB0x7ZcDzPZJq35ZTt768fg\r
+bXPnphP/B7YNS9t+vDxgW/JWtrEGnCBbtnHhFthW3bFN7pkm2yRcu2Ubhxza5gq3q7a5uo37\r
+tlHbwLwtwU620eYVX+nENtqTbBM5XRd4TajANjm3RgSe2zYYOzlVjbZVgW39yjbZnL94N9vy\r
+67b1xg8KkpuPMakfsU16Ssxs04c+GewU2DYYX7idYJs0QU7nO+jjX2ibCKG2td62zgSFW2dW\r
+trXS1WppW/mYbSVs27KNr0uFu882D9uWBLaVYltur9iWa9RPsK1e2KY35Pts8w8sndm0jR9i\r
+JbKaNHPbfrrQucI2R2AbPT1pUq1sc8M0Cje8Y59tpUs7Hehy3bbG2xYMPrltm7RG3rON+wVN\r
+57uybTy63LotbSt6E1zT17axjfnSNiu1xRSDizXU2e+mbbI5f9G/mW1U2dZv22YlQ12SNo/a\r
+JkPktm3jm/XAtspfSj/YNin7XE3byrZh0r7bsq03fqCUDWwrpnbnPbYVrlbyzWyj+2N7zbaK\r
+LysuSatHbevNbBRMecu22vgO2jtskwN0V21rk6O26dAqTpot21TyhW1VoXNZONtsYJskVGgb\r
+b/6WtqXDddsaTm5/c/KobVI27rOt9TduZ9jW3bdNjr22rfY3btWWbVoIL23LteltPJIZ7ttG\r
+m8O2hW1yk6JJ2j9uG+XcTtum+6UdtjWn2NYnW7ZNY6TNpm2NcUNX7KZtNOfIANsCAtva1DdV\r
+rW2TaQg0SUm9Xz5mWyfXHrvDtmlGhFNsM4dt80/HfM1c2yYP6gvb6lzPeTSNbEvszfs23vwt\r
+bbtoq9NGDYiVuS80SelH/9Md26z7zkXDBL0pyxs1INZNw3HDtnyKfzjR14O2uRoQ/pmFtpW6\r
+ca1Kk1Z5YJsLrOLyXiLjbcsC2+x92zLX7eXNakBu28bTCmmSNlw/8JhtNNnLTtusHyp837Z2\r
+ZZtkKX3hFu/b1l02beu1kOU5jzZs4+9nttnJttE0qQOBbZ7Atu7SXa7bJmP5Cm0hzO0920r3\r
+nTIsbUvUtmXLFUXqnm1Tz7NjtqW3bfPHrJxtqQ1tK32UAttysa3JXL84QzVuP11u28abv6Vt\r
+yU3bePRoLr0fEvuwbaTornZSPv5+2/TGe7dtG+2k3UWGB6xt68Q2Hqe4ZVu7YVvqbftpj23p\r
+m9rWi23lFdt4vHsutqUHbBtMtqsPCMdKIr3Dtu4Z23I7ty3sAyJUMg1AYrdto1bYhW2tt43b\r
+rWa2rfuAyObv2AekT9iXcqt/G9EmrhNXlR2wbdwrtE274A6r/m1Et982zcCrti2fEoyc9GRb\r
+QbMeXrNNHkKa2biE0LZ6ZpsMJHJ3ldVPKbfLL2xry7lt4+bv2L+tN862ftO2brItv2eb3bCt\r
+vmVbNbOtv21bMGJAB4XNbKNv2r22jRv1PCGlyuB6UwpyW6jjdNz+gW3N2raLt+2HlP+FtpVW\r
+7xJ9Al2cbW/Wd3cwNP83Jara1gV9QPJpsFWyy7bCfSeHsd42U1wbBaNhZPdtm3JkbZsUPaTJ\r
+tm2lN0pHwQS26bqcN+2Ka7bpuZUz2yRqNPGkRrneYxtvHo6Cad7GtuaqbfV0CevEttt9dzmY\r
+IXO2aT6GtrlB0Cvb1EGO+i3b/kND5g1u2Jasbavntg2hbbXalgZTsUmUCtnfnxt9tDPbZLoB\r
+F2XuJ/79yrZmYVv1rrb9RW3TIZtP2ZZ7ae0e27Tqd6dtPAdzLiHzx8w2+m/jbFu2yi9sk2m/\r
+KZsD2+T767bJuV2xLZlso1FXWWCbG08a2pY427SYfBfbrPlLbv1YeRkV79tQ1bZUqkZpm9u2\r
+UY51U29KZ1vJuUYraWYGGYvsx8pLGO192+QZMZdv+R6uDW3L5byu2Fa4HpRurLy3zY+Vl+/p\r
+y8m2frJNz42CCGyTpBmjR7WJ/M13Gf9b2FbPbOPN/Vj5zM4npPt6PGDbn3Pr5wGheXwC26So\r
+08kuyLZmh23JZFsh920l59qVeUAkjPZy974tqJGwOhRlsk00rJ1ty/5ttFZt03lAxg3pVotk\r
+yAPb3OQy7uwXtkkJH9y3XTTAxD1E0Eh5Hv0T2JavbdNfoLOtehfbKrYt5VSjDA1tk2JIJ/Kh\r
+dK5v2+amxXC25WJbYd3kafV6jiMJo03u2iZTG8iaSjwLbeNnXGdbvbQt49iwbYnWoLhXY5BG\r
+Ruaj64wvrcS2drJNz01LaGebJA1NtVpqmtG8DHTik220i3YS0a0GsS33mr+PbZzoOn9bwgk4\r
+taHKbaykNSfaHdukbHS2kZ/0IFBwGcHzt2XhbGzdZJvc/EjUr9pWuiubjOrsQ9u4RtboLSjl\r
+3ty2VC7mhXXzt7kTz2lbnlEr4e+b1N1FkUVNYJucG1/3stC2mm8C3NHItKVt9BuY2yZvZhDN\r
+uUB+L9sSPmOe+jQcvcxpTlk7ruRcuW1bI7nibeNktpz+BZnKuSrFnJ+bUsIgldrbttE0hW5I\r
+PeenDv1U27hwVNuG1ZgrOaDYVsolmN+ONMrAE2XyhLtkU5O4uyiejHKyTc+NpKsm2yRpAtva\r
+tW06N+XaNpmbkq/672IbFxM6725HN1LheNKSJ/+kbzPJlVu2lWySmWzT6UsNB0G522tWcDpn\r
+fgroVueynXoc5dOJFJNtMrElH5WyR1vPRQg6jZZzubA8U+vctoRt4gpmuqLObDM67y5/T78w\r
+fQbhTm2F7O/PrdEJTnU6EEkad1G3zrZiZpvMuxvYJpvrvLsNrX4r21qa75vn1W5ntqU9q5JS\r
+GvIU2LdsKygYSjhvG02mPeZRwrlWmazhAu1CRclgksF1I+cnSH41IEe9lMlJS1nKnW20uStC\r
+aKBnp/3QRAgaw85uGO10qWPlOUw6Pk0TQbb1NEP5xZ14zW7InOIy1zidgtqWt942f24NfZu7\r
+h0hNmoVt7dy2hmYXL9e21YZfjtjQx5vZxj1WKYd+MR9PykWQdBi/11Nc3wnga3dlGIn05P9z\r
+4br5N7KNf1+C5UuivmiGo0575N42es+VHM0Y/+a13vW0tZNtcjC1LV/axqMJxDY9Et2Okgx0\r
+7t9l7nt9LYLVNHG2TecmX+rLK/lVErSJt61jMYvQNn1fwoZtRt8NaH71LrZxzwsdzEYp/m/b\r
+tqkmt23T13ls2FarbaV8mVv3LpgHbKuMH3zn+21bZ1urQi1nnZlsS6zOmmWkUFzYpt9PtnVz\r
+29y5zW37Xjep3AD7Ddv0XTChbbx5IzGmEH/7Vrbpe64oxX8zH71Mhdp3/jf4bzJM95ptg2RF\r
+OHrZ8G84Idv+wEv8HqHCuvdcqW3WOXDDttoLKXu7/nAF/de/asV5OLetkreqFbpBbuUpwMnA\r
+lf/GF4kcIr8/Te4gg3PTNxOFtlFEZ7bxs8xkm7yIbW2bvueKQvzhXWzj2gD35rq5bTpYnF8d\r
+Jonzm+u2Wfeeu9LbJq8T4wkayLbvZenXkkE1Z+4gJetFX0fJB6W89LZVk22NF1KvQloWFtad\r
+wcU62+zCtpq3Ztsq1cnZRhv+NXPfD77M5NGw+rwynZu+da1R236QdJtelL5hm3uHn51s4807\r
+4yfRfB/b+I/Kv4L0T+vRy7/zv8Hipm2FDracj17miVrIth/k/aS/MeH7ScW2ZD56uaa3AJUa\r
+hdzZNg059TeavL3uZvzbasOZUNW2Rt7LRps2qlOrttEe1A+mNv6+z8pO4z5a0zGd2+B+QdKa\r
+8Uc9gm96cpNgB7a595PawLZUDyC1J6Z7L9tqd+ds/jYfvUxXsN/7VClv2Fbx7Yx7qa7EgvOY\r
+Hh7Jtj/yEr3b1v2ytW20N4mbzcVKDdtkWz3Z5ieBsf7Sz9sXuqHox45lS9vk6ZJtc+984xYD\r
+kqEyCdnWSAxqf3M46pWrbdO56UhE96b7P2kJu7DNzmyjgDZssz7GydvYJjX4+tL08WNIwqss\r
+pSW9PJ1WUmrLhBebttV8f8xZEdxdFTIUhmwrjPSGcJMImovaRnUbXTjrTDvZ1ky2BbNucdBu\r
++8LFlXcx6uHcNpmNkG1zY6q8bY25tPKj0Ht3vVyPepVTvZqfS4tTSmvI6kKSZurE4V/oFtjW\r
+8A1faJtsXutccObyPrZxfvTuHZGX0LaBU7bOa02VTCcn3rStlbueIrCtFT2qMdfItkq6yWpz\r
+EFVyab+Pii/EbhwJZZe3rZtss2G1VOuvqmrboGZp0bawTSrzpI3CPXM626iWMfPf974ApYeX\r
+JtM/3bmRlKW0VtDuhc6M2rgbymHDNp7jc26b1ujpLz17adtCdEoBvRQEbUaE3kk7I8tbAfHq\r
+5jL7TutJUwmaP2WMuvWDC/gwF+tfjiX75baRgw3+4rmIgK/h8oEEWy4YzGw06nJP10Ah3/sb\r
+fte9yc7PTVrQK9VLk8/btkm1XKubywGG/Zn1SXnAtrs/qyCjnkFFvp0vE2rbKXx8fjb5R27+\r
+2dmfvPcHYMC2+zyYRicl6WfhBWw75aACbPtYHrDtbu7DtvuMT0EfuPlnZ3/y3s992HafxdPV\r
+yZt/dr6+bfUpBxVg28eyP3nvuwTb7nO7cujZzT87X9+2/zjloAJs+1j2J+/9Qv3b2PY/Tjmo\r
+8OG2PXiAr1+fO2f/6VyvgXd8G9su9zfZzYfnbv/YAR7c/NOz+3SG+4X6N7Ft+FK2dcvGsFM3\r
+//TsTd7/3X1S2/oz26k/2rb/1Tykz4Obf372Jm9n7m/5bcq2nRfcfYF9sG2NeagkfnDzz8/X\r
+t+2cg2pgsO1D2W/b/UL92zwllKccVPh42x667j+4+ednv233f2bfxrYz+XjbHjqpBzf//MC2\r
+ENj2sey37X6hDtvu0Uy9fD9g88/Pftvyu9vAtns0j7VEPbj552e/bcXdbWDbPZrHwn9w88/P\r
+3vMZXqye8dvw4JCpLz7Cas2r/XrAZwa2gXjANhAP2AbiAdtAPGAbiAdsA/GAbSAesA3EA7aB\r
+eMA2EA/YBuIB20A8YBuIB2wD8YBtIB6wDcQDtoF4wDYQD9gG4gHbQDxgG4jHmbYNz0wk0L/a\r
+uHCw5izb6KVhy9kbro8F3hjn3JqM3pK9tfXsLTTdjflIqmIxLfJjI6D7O2Nm280Xyt09ymaM\r
+b53GvmC/ImfaNp8A6r9Xtv0f+Rg2bau9beVy1R3b/tv/9ZRt/3WGbcNqp3/Ctokzbatntv3n\r
+yrb/RzfdtK1ytvXlctUd2/5zCuMZ2/7nGbZ1q51y2DZxpm3VzLafV7b9Qz7aTduMs60rl6vu\r
+2Paz/+sp27IzbGuX+wywLeBM20xo25CubNMErrdsG7xt6zdA3rZtmNY+Y1t/im2rF9N0sC3g\r
+RNuGmW3dyrbevd99y7be21aVy3W3bevOsa09xbZVcjawLeBc24L0aVe2aQIP12zLxbb1nGW3\r
+bWvPsa05w7b1M3UN2wJg27QpbPtoTrRtXj/7oG00GyFsuxLsy3BiW0I/y+mHbXPrHp39M7pt\r
+W7sqm7YBz6m2hUuP2pa4dbDtdYFt06aw7aM507ZZZj1q28Wtg22vC2ybNoVtHw1smzaFbR8N\r
+bJs2hW0fDWybNoVtHw1smzaFbR8NbJs2hW0fzT3bpOmz1bdc1cv01pbRmlZv2daIYRWtmtmm\r
+DQ+tO/7CNn2hetC/Yz7mQRrJ6otVBwbufqK2DbJ2MKX3wJ0GL3BnFBMchXZa29brIVv52hSB\r
+bT7e/igclthWX6ZtvG0Sp3Z+CPmyl9StXR+ZQv5pJEpe9xrvILpjm75y2dDbSfvLoK/75kSl\r
+ylxZXRcm7S69MeZ7n/3duMu4meHEGhdyanfXsExZ5/rqzUrbVjv6lttJK8OpTf/pUnmtGAXY\r
+ujeNj1q2mbwttaetOo5bwwdS21p5l2prMmebP42cTqMdD8WNukazeYzLGH462sZfaPlY8yEp\r
+RgUHkQbtpFMHBHcUjfa47yAnzec2xisLYkxh8S8zPA3tYd/zbvRlJiM8xmDrQtKJEv4VRsfd\r
+OYeak4BcoGzqNHsG+pJKAlld/8kkgW3NZFvP+UubpQvbKueL5ODCto5zvZFyawAAIABJREFU\r
+s0trPh7lcq2HlmxqWITWcMYY7shJB1LbKjGzNqmzTeLZ64+mMbmh/JSjUFZeQtsGkWX8mgXS\r
+ri2NuUy2SUg2OIqExba1ctJ8bt42iTGFFdgmX1pJGD4b/vIiL5Rn2ySdKOHfwDbJxYYTnrJJ\r
+ftLetopX1390tv3AttE2Ypvmp6H0nNsmC51+t7St5dzs0srbZlxpwtkkBWBNW7FtdOzU2yaa\r
+UCY62+Q0Wj5af6lH29pxezmK/JbUNiq39Oark0Ma0ZniNdnWGlfUuqO0TvqSUos25KN52yTG\r
+Y1hpYJt82Rv5zfJubVaPu3GxSrb9SQ7UUH+sI9n7ybh9DoOkeMW5RdmkRREXeAmvpnLnB3NR\r
+28idwDbJA1pj5rb9XlK4MXrVWdhWcyJ3qazlW0BfClI2aVFGIXZaMtGBxLZeIk27qG16GnK0\r
+/jI6bOgKJUfhnUtnW+5ta+SQRhylaP/V2+bj7Y8iYfHXlexA20y2ed8vgW3yZSe/DtmNv1Rr\r
+M/odOxOz17dNywzDuUXZpBcQ/p1KkUJjrb6f2VZPtjWcgpScppzZ9jvjSie5QC5sqziRu3/z\r
+tvXGl4JjNv3GuOuWKdm2RlaLbZ1Emnb5SxaeRqVFNF2W6F6tmpQvAtu0d0EtJ2fYGrbqe29b\r
+LZ7Y6SgSlvuP0f1LZ9tvvO/JZJt+2XJgg+zGX/6NI0q2/eBNTF/fNikzOCFSzqbAtpavYpTJ\r
+9R+cbX/0j2Fim/ziKRRTzGz7rS+dJNcWtvEOtvult62b2fZrzqZhsq2WrBLbtKCjXf6QBafB\r
+oY5F9C/ItmoMwPisNLnaRr8UfTSt5JB6DnR233nbfLz9USQsipBES79xtkmMOZ6TbcGXmVwA\r
+SvnyT9627417+ri8h22JJETK2aRZTrc3bSoasW3ptm0Vr2/Wtn0n2WWM3o7NbRvEC7KNY+GP\r
+ZCWbfmX0sWPclm2rQtu0oGvntsl1n7L1F3So8YFRj8I7Z2vbjNompVY3s83H2x1FwzJShjtD\r
+TO5skxjzof5czE+DbZPfE92x/pqTkQIj2/5gXLmXvL5t7Ilkq1w3NcvpgtOmsrrk+9vANu5J\r
+LbbJet4sn9km2aWFpl3a1svqzsxtK3lLuUtz8ckn20q1rTaT49nyNETSUiPkL8hpYJvWC+rZ\r
+GrXAR9XZlk5Ba8nEpWXZGW+IyZxtEmMX2Ow0OPK6lIuCv/W2BaG/vm01iTHmejZI7paV3tPk\r
+XNFR040JZW/xs9j2J6318rYVNa8fb8IXtqUdp2I6bNk2ruul1olHA7PXtglsSwZ+Gh03FNv4\r
+Mc7ZNsaxNhz3ylU+82mMR+v5NC7daNuQ61EGiqizjWpvXEUzScx1dA1d1uhUnG0DJUjqgq44\r
+qhyW4eeNMeRcjjbZxjEe//WhbfwlRZ5tG6/mVO83bvLdhQJj21KpfsvaN7CtSsbTH9NvNIYu\r
+qBdXh862ZbS64ydI+1/0zJBI1YiMEpEHSV5fXXj6o7Atga46dPXIaSXBo5/dKBhqupDV/1eD\r
+ai6uxp2zKaOKdxrKQPmTSuh0ILaNo8kxbtU2OQ0+Gp1GNtA2/65HoYwdIyFtCZNtVP3b8j0/\r
+H7kxtgtsK1zNvx5Fw+LR13QaGZ/b+E2dBzEe+NFkcRrkbZ3Ibil/yaWk2lZYObfhHWyjzCip\r
+uaWlbEqlkUceO8m2lFuJ6kSeUBOpGtE2qdRKgThKQM9sC9tK2plCa/zNf2ibFZ0Kq0HVVAYV\r
+umFLl5mEHlMoWD5Qwf/UtkyOSuFn4Wm4H01uLVdtyVHYJWcbN1uwVD3XpZSWb/hy2dDZRk+t\r
+rk1OjqJhsW2JdedWT7ZxjEXbxWlQ6GPqUiKybQULWaltnE4UevX6tvGFpqBs7SSbtNmOa5j4\r
+B0i3cPQzd7YVM9tEBLrLqxe2cdMfpWK3YRtlGD/yckhsW+7aDjspuVoxo1bbSl7NtnErqeSX\r
+a+nU0+DHYykjuTpiOsoYmtp2mWyTWmyuAcoptsNkm+zJC3IUDWsQ6aiIbMVIZ1spv9fSNTmH\r
+X1LbCe9Wa6ksopJtiaRTIkn25bljW04Zx5cXSZYhsC3n/B/zu06v2ZbxN7pZaFsiOXLxtVtz\r
+2+SAnUZObSNpNZt4NVlBgaQSxmRbydvynXoWngZX/ckPhltIpqN0a9voVzBISUmhUfk8s01/\r
+JXoUDYvPLWPxWvHP2TYdKrBNk5UKUSrCyE6W1HDkmsyZ21zkyy/PzXPgi1fFtvVz26hYyYe1\r
+bVXhquJbuco625q5bVMqbtlWH7GtUtt6Z1thrWtok9Pgo5XeNj0KadE727qLtoWITYFtOV8f\r
+rV3aJkepJ9sqsa2RAn9uW+I7fMwVpKejjG9XWr0Wz21Lw14nX5g7tpWU1E2mtpXuctamcrNc\r
+yqNpYFs+s62QfC9XtqWSI6lvA5/blrGFC9u4Ho+ziTPN3WORbZTzk21WLvOF5Lw/jTpd2CZH\r
+0QJWbUu8bdwtqXRb8t1AaFs/2TYeRcNi23IpK8U/tU1jPLdNvtRClBu2xP3ANkmn+n1sq3NN\r
+yg3brLuFD2zTbJhsc1Ju2MZN9iV9O7ct5820+HC2yfRvC9ukpYFty9U2aauVuGeL0xjURi6V\r
+5Shk2xDaJlbzfRkVzLl3t5nZVkxBZy6sQaqY3dNtv7TtYsOHncm2MdrcGJi4CzA/h0+2cSeT\r
+R7P2E3LzHKSYyIdcbbPBrbo+mi1sq7OZbSVLQJVmC9syn4pbtlHxsLZNoGy6eNtkLYvobOtW\r
+tulpyNEKfxp6FClg1TZaqcf62YY3nSvbrJ5M6Ys+tY27dfhWCbXtsmXb9KXapkuBbdm72fYP\r
+9+fCNi1FdthGbNtG67ZsK1a2+Z67i2ya+m9726TX06AX1Ok05GiBbcVkW762zdqZbWVom081\r
+dxQNy/U9drYVC9vSMAXD05ADXLUtX85E8EW5b5v/M7Qt8bZ1l9C26fbM2aZ5t7atU9ukMnhl\r
+myulJCjXi3qWTf4GkZnZ1qYc22ayTb1f2DZurw6Jbe5S6FLH2can0k62uU3cUTQsZ9v4qb+P\r
+mW3NVdskra7ZRjF/A9umXvRkm96Ly4ohsC2fbEtnttlrtuW7bFPFWm4j04hQNsm1j2pH7S7b\r
+EreBvwz6Ng8/dsHZVi5tK/11ObAtnZLlmm1y3mpb6ovR6XzdaaSSKqW/i9THnyZz6aTX6a/P\r
+Xtu6tW1ypdtpW71lm+a/tQvb+Oc/s22aHH/KJh1s4Gwzalt727bc26ZHuWHbcMU2N/Jgsk3D\r
+4otped22zN6xrXSNGXPbinewrZtsq+a2Gclu6cUQ2CY1EsTcNm71Wts26xMR2CadR2a2ufwN\r
+bePCzR3P28Y7p73vRuxOg6Me2iZHqUPb+OilO+kmsM3ObVPF3VE0LLZNOmzo/ep925rUpQps\r
+Uxozs23gpS3bLtu2mSdta33+TtmknUpcQ9OWbe3jthWTbTzoJ7Ctc7ZZ1/3JuqPAtl3ss60x\r
+z9lmTrBNYzqzjfbl4/XmfNtkvMA127QPOmx7hF226TCW0LbS25aEtnUXN3I8tK25aVvOh1rb\r
+VgS2dW67IJt0ZAzbVi1tu1yxrV7aVsxt0zprTprrtlWuE/PctmKyrdyyrb5jm71iG/2o3se2\r
+Zmmb3MJv2ZZs2Waetk3Hsdgwm2TgIC9JN947tnEhete23GVrd8O22l1KT7Oth20uu6vdtlGT\r
+Tc5bBLb1z9s2mHAUjNpWcZ5z3ty2bRq6s2WbxFdtq/Mh/Ildsa1x0b5hmyTUDtuCOkHYZjUB\r
+Z7ZVxRXb/KN/YFv7vG28qYuTs61hHbib23m2Za7ap7phm45shm0Psse2fss2vZnaY1tzgm2N\r
+u3YFtsnNnHPxbNvMDdv8YGrY9hB77tsoV3+5z7YxTXSLwDYqef7wpG36ADqzTR4NaYk2r065\r
+b2uyoEBPrtkmk0JY3Lc9yB7bKP9+mttW563XZ25bubZtlOqy2XIVtBHNbSv9dxqU1SlH5rY1\r
+VMLoXV3mbRMndtSAyFEWtqVBgZ5ftc1NYOJtK925OSuOPJNeqwF5K9saHuK5sG1qf79rG0+/\r
+8qxtnRvnHNgmsxPxE2tqT7aNj3fVNjeP0mO2ob7t1kpvW243bdM7nMA26qhV8pcz2660ymuO\r
+8KFW7aQL2+yGbeMFTW27TK3y2hFph216KjPbWjdLG4/nu25bu7CtcOemfZFh2xZ7bKuTZR+Q\r
+MSUoGbdsK1a28ViU521rN2zrxTYeWXKSbW6WtlY3uWKbhW0H2NMHRDr077Ot2rKtOMM2HfI0\r
+s40HD6fSr3Vp274+IG4bZ1vnbZPfzmYfEF7P5znrAzKzTR9157aldm1bq7aVN2zL38a2hseS\r
+LGxrr9r2Jw1xsq3fGJeQH7Ct3rBNhpWyGXdsa1b920Lb0sk2Z58bl1Bu29bvsG3INmxbPlpz\r
+qH/3/duu2fYGvSklLfVkZ7al4x/9lm2/37DN3upNKcxs04MsbGs2bJOBfiyYsy04TFiXJadB\r
+fXedba6YZgVKZ5ubz9KZoQPGVraJVe4oGpbGnxMlc93c7tnGkf+T67sL2/IrtunOoW3171XB\r
+Xbb50QYL23L/nQbVlmvb+nzbtkQPUy5sc6NgvG1ylLltfdKEX0+2FaFt6WSbrNawOP6SQlu2\r
+uXFcdm1bQV9fs63O3sa2mm2b992lKXHVtq6Y2/a7bds2++5y/v9dAlzZloa2TZk9r73qJ9tM\r
+aFtbrsZcbduWukGt3jYtcOXrmW1+FIwOk7JqW525sJxtgx9hurJt2LTN3LXt9cdc6TCWLdsu\r
+4yq+dWkvc9t+/YhtsytLYJuM/DxomxSl18eTettkeTaedDynY7ZJWBz/3I946ZOFbcmGbYvx\r
+pJu2pW9jW8W2tRu2ycPm3Lbm12HHDettq7Zsa2a/9ck2HdUe2EaHXdrWzGwbnG38G6jT62Pl\r
+nW26PBsrP25byxmyLZNtlAZ+rHznbfNj5SUsin8lBdF8rPwkVr9lWzBWfsu2d5mZoXC2NXPb\r
+epoG1lVtzGz71bZtm63yYltJ327YFl5JOQpL29Lwvq2f25ZxGeNs49Nw84DMbXND1r1twcND\r
+F9iWhbYlPt56FA3L25bIPCBL24yEOTsNmQekrLIbtl3sG8w6o5MD8e96YZu8JyW3OkeZt63d\r
+tm3YtE0yuqRvZ7ZxTs1tyzZsu4S2dc62QYsb2sVszXHkbHNHMXaa42hcE9jWTraRTpW3zXjb\r
+9CgaFp9bat2EHtMcR/pcQ93xlrYt5jjasu1dZtTiic8oRedjrqxMlUiZWS1sc6NV5vVt22Ou\r
+eJafLdsSvnIEttE9V71oS9CBvlrf1k625RyzSqf08qfh5m/ztulRSIHANq3BkPJksu1igxm1\r
+zDQkWo6iYfG5XTjo+fxtTqwinC1w+jKYv23TNmPfYbbAiid1rGRS07ltvHDhnLxrW8Ffr23r\r
+eA1/O7OtFeNC2y7ushjYZlxpwDNDOttkRtJcJrfMgtNwc1M62/Qos7kpA9sSy5NEqm2NzM3J\r
+oRUqCKFH0bAo/o3Rb8tgbspUa35nM6G6L/Nwbsot295lJlSesJbTbtM2+mXObXMZ7GZq5JYr\r
+6uKW2qAizKdi5q4QgW2FlFMz23TGUDu/4Sl5mlDJWTPZlvBltTZX5t2dbMt1Hl6ed9dNzOVq\r
+p7mr2mQb7e9so6joDK56FA2Lzo0LxIt18+5mM7GSftVNbzHv7rZt6TvM8lzTTNoyJffcNisv\r
+76PXT+RXbEsm2y6DvBGAV/AUyboHrZFcC2zLKXWpvT20jWbcznVDb1tKxU1LcmXdZFtF0eVL\r
+1jSneMFzil94TnFnmx5loKm9ZR7puW30xg5v27gYzimeWOP6xxQyp7iGRT+VjLzko2Va8+Na\r
+P7Q/8Ow0qH2fJ6bOWNst295kBnvu4y29/pe2Xdzq4opt/MZIsU17iksGDfxWNd6D12h3ssk2\r
+fV/CrHa33cgmfUfBeKDOBD3FJbol7/JdFpyGe1+Cs02XdURYo0NQJ9vCnuJUuJtfOdvsFG89\r
+ioY1uPcl6LsgqOjOArGqLdsqE74vYcu24T1sk5eoNNds03dmhLb1UgKFttmrtgVvuZjZpjm1\r
+27Z+Zpu4Fdo2fxeMs02X5V0w3rZaa6e7h2wbvGKljjbU9xzNbaMfwrIPyOJdMFu2UVi/eH3b\r
+Ov+z+zetqHK2cW1AJ/l6zzbKzu8vW7ZJRvORQts0pwLb5MVsuqG/4ZHSQF86U2dqm7jFu3yf\r
+Bafh3nPlbNNlfUXa0jZ+j1uVO9to8bfetmr6lehRJKzBveeqsPoOt7ltDYVZzE+Dfx3Te642\r
+basoAw7k7mfj9jnIoGHKvt9s2aZv1gttG5xt3dy2v9Ldh2RkYJtkNG/ubeO3BXBOBbb1/lKy\r
+uAsq5ED0bjxnm7pFu/w1C07DvcPP2ybLbqCgm6gmsO0y2UaH+MHbVvtfiTuKhuXe4VdaHeHc\r
+zGyjN0aubJu/w2/Ttpoy4NGs/YTcPgd5sSdPiLGwjevVB8nXuW26QUc339bqmKtE7nXlkDKq\r
+rru4jObNA9tyzanAthujl6nIJJsaZ5sbVz9+SN9dO38/qbdNjsK5XfIFmW27BKdee9voYD96\r
+2xr/K3FH0bDc6y6tHI3+mwYxHlMyXdnW+bIwuWbbGNbvX982eY0xJXy5tC3T1ekV2/rJNvpF\r
+y70ur6km21p3fQxsq/myxm/DDfq3VRszMzTyLmV5V3HmbVO3xl0StU3fxqzvXva2yVFspxfe\r
+uW08yCuwbTzEfPRy6Tbjo2hYFV883Zu7+QxD28ao5SvbeuPeZ5pes62jd5buzNHPzJ1zqPUX\r
+m7iuOnPbarnxySfbXPcuqViw0lWaXi2WWO3Kz/URsoebFMuGtjVcBcJvGA1sq4NLbnAJSqXq\r
+rzH8DhGxTV5xSgFdnG21e698GtomR7GDXnh1CKqzrZb5j5xt7Vg+e9umeLujaFg1X5ON3mak\r
+XM0SxJhqZ1a2Te+Vz6/Z5hLzq3PnHFpNw8x1sXa2ST/FzqhQgW2l7lp52/hJbWrqayfbrKsR\r
+C2zr5IEun9vWBYVgWFLwgfhq2nrbWlGhM1k7Vcdw3Gj7yTarBlf84WxLg1NvJtv6URtv27iL\r
+28wdRcJq5VJQSOgFNxCHYrVuWGP4Zc2J0BvXP3htG8/8ezunvgT3zoHTsPcKLamy5Td+U39J\r
+8n+2Uh6EoW302uJrcZssv67S1ZZ6R99clvHQYjA4jnyzDFWXJRLaw8Pb5kLxESiChW4KW/+S\r
+sHrX3O+/WaWQK/wnNEHqjTMMN7q19otw+jl89URpV7+fk1nbtgvYtsGXL/CbD7etPLRbvyrt\r
+vyCwbQFs+0BOt+2rJ0qTf1zYNHLsiG3/YWHbJl8+Uegx8KOQp9Dy4f340fSrJywB2xbAtg/k\r
+bNu6y/1tPjWzio6TCd41/RBUld5+9YQlYNsC2PaBnG1be7OK8gtw8JFxF12yWZ99Fxr/1cC2\r
+Nc0Xt+3IbdVuyLTmwP0Xt5Z88YRlzrat/ujqqg/mQ6vsaYxZfcQ2Y62BbUuG0nxt2/7fD50A\r
+oTfpYA5cEVuTd188YYWTE9csmrK/Gq35yIoG7oF8yDbfV/hrA9tmfKxt3Bv8wBWxM+arJ6xw\r
+um3FuQFGpj1S9OznYBnVG/PVE1Y43bby3AAj037szXh1rIwajPnqCSvAthkfbFt9sIzyI86+\r
+OKfbdm54sWk/9ma8OWhNdejh4vNxtm1fPFHaj7096g5a07zGI+nptn3xOsj2Y+8EhoPWdK/x\r
+kHC2bTpp7Zel/+Cy+eh8ptUXv2YoX/w+C3wpYBuIB2wD8YBtIB6wDcQDtoF4wDYQD9gG4gHb\r
+QDxgG4gHbAPxgG0gHrANxAO2gXjANhAP2AbiAdtAPGAbiAdsA/GAbSAesA3EA7aBeMA2EI+v\r
+b9uRyR4j8eGjQL/aMNMzbNPx36fMMPbQ6wr07Z6HD6avbdx5IOHaYPqNydQHPxnclQPtGJh/\r
+M37Dh8429wGcZ9vwbWzr17b9+/KL/76y/z3b/hkeSNk25J9btnXP2PZ/5eNm/Lp3tE2mu+5P\r
+mariYdu6lW3DKh7/eWX/e7b5uNy1Ld+yrX3Gtv9Pw7gVv4+dSfMDOMO2mhP6+itzHworf2Bj\r
+fd334tu+WG7385X979g2+Ljcs23YtK15xrZ/ycdN25r3ta37NrY1K9vaYrnZtRy7Y1vn43LP\r
+tm7TtvoZ2/6HfNy0rX5H2xp9jfsJQT1uW72ybTXPUnfQtunlkfdsazZtq56wzb1586Zt1Tva\r
+Ju8YWL8K/ggHbFsed/Wmqqs5dse2KS73bKuv2Oa+e9y2fp9tb1gDIrad8yIm2CbAtmvIHds3\r
+sm1dwflpbJsm6vwg277cTKBn2CZPo/Upp/64baupRffbdmfd/rhcsc3vv7sa2bPrrcJf7o0d\r
+59hW2K1sP8Ljtq12+Dy2+Yh8lG3F3U0+F2fYJq0IsG0JbFtySrUF23ZOsf64bcXy289jW+n+\r
++ijbyrubfC7OsS2jnC/OCAq2CbDtKtRQCttW38K2Jec0AKRnNZPCNgW2XYUaSs9puIJtCmy7\r
+CjWUwrbVt7BtySmOUBPpOc2ksE15S9ukCXTM1pI+eu0T3qTyL9yKG67qVZJL/5BB21imAlAD\r
+1g9tiOjNMoebWaIPie/YxB8z23ybIX+jAXOEpoymJxl5mmk0p/w6ObVZfDgu3VSzM7WWSCvd\r
+9GAktlWLPszSoEdn4GxrNhxahiVxENsqiaSuDNoGJZ6vZhu/c67N9LWd3LtnzHJqCA87+lDW\r
+U1L0/DJcSlf/05SXzzW8oirrKSckxIq+6C+DhNaYXGyraeX458C7U7ExijUk3bhUibi11LrQ\r
+mzvFgda95q7m1xtzwBShwr+msc26caeOtx94t6DI0U7XJoiPvKaRe/WMB3KRNCUlAOdza1Lp\r
+zERbdpdegvAvC+e3evszaPm5fVzojfx6xzC3wpKkobct2+pvJht3a3J9R++4peon8Tz22vBv\r
+yW3bes6oNms4BQd+c/CQkFR9+JZleh6lH34rcga2dZIg8oLrqnRqaMD0QRtf9I2IlckC26qc\r
+dk8D20bpxzgkEpXL3LbKJT3bJgFLhCbb6DTkVFoOJrCt5vORaGl82Lae37HNtkkkyZBe3tdd\r
+myS0rZUkqdzv0OjBJ615YZjbtgxL4iC2/WQubJv0c6Et1TaJ56vZ1vK7gttMUrCTX2tCyd6F\r
+L0inhlISpOakDW1rJPsMrxhTz3UPkoApxIRyV4oRUkhsa9i2Qnf3tjVjphqnhZnZRtkjZQrb\r
+JgHT35fANso16X1ZczCBbfJ7aMP48H9aXsG2SSTJkFYOOq4KbWvkJ+Nf5m00kIu3rRGR5Oep\r
+ti3DkqQR235U2+Sc6aejtkk8X822hs+zzeR0G5aGMt3ks7cB030FtVdWnAihbbV8IyqMqed+\r
+980UYjlmpGg6KpSobZTwo2017+BtGy/ErWSmfAS2kYWJO2TuwucITbZRaLLC8P6TbYPk3Cw+\r
+fIq1Xg99JMmQRktWY34MbKs5iN47YNz+3rZaRCrZtjGJtsKSOIhtfzUp2fZ7OedxRaq2STxf\r
+zbaaf9ntb+SXX7M0lOnjFc9fFDmU3HIJIEVhYFtl9A6IVBhTz11/6ynE8U7mUkkJMC4Fto1r\r
+jCuCxLaKLXda5KFtrXEms20SsOzvbaPT+JtqyvtPtrnSN4wP21YZvfq5SJIh8huifX4IbJOo\r
+dMalDMfUnwEdSBbYtsLZtgxLkkZs+4vY9jufWBe1TeL5arZVk22ZLCWc6SZd2JZZvv/l1Axs\r
+G+SbWlQYU8/ZJgHrR38xcn84fvw5p9Ut6SG/cCmCxLZxYylrpiLH2dYYFzbbVqmNM9t+PS79\r
+SXwyvP9km5a+JoxP42y5iG3G21bJXd/4xW8D26aTkEs6HWI6gza1ukD2TbYtw5Koi21/Ftt+\r
+6xMrUdsknq9mm2QKZ1MmS862yqUpU2UDpyGn5tK2ghOQbPuzCe5pXIjjnfgvOGA2ZmZb74sg\r
+Z1tRTYVQGtpWz23TgPnwM9v+aPS2bm1bItH18WmcLWKbRpIMkdOhfX412fYL0awx7pJOYU1n\r
+MB5IF/gxmUrQzbAk6s42fib9bkossU3j+Yq2pbb9lWSx4ZThBLqYmW11SnciraRmYJumbyUq\r
+VLI0BTzIRy8BizG8vhW/JinYNqO2aSF0WdlW6F95GHDubaPT+O1kWxrY1nAM+ll8vG0J26aR\r
+1BKX6nxka6u2yZauHJdzzGZa60IltnWXzbAkacQ2I7YFieXqPfmbF7NtoOROaUj2wHl7GROA\r
+zrqsfjH+C2oq60vPP9CcKuYC2zpK79zyoyDZlrVikwbcU7Jy7pYVlX3Gqm1dorZlnUghtiV9\r
+QYksUZnbVpEdBcdlDEMD7qhgm2wzYyy/u/T8pEmbBLaN0avJqPFXc3HxabhsSq23jSPJhhQ1\r
+3x+U1cw2rocct6lD29wZsG28wLbl3rZFWJI0alsptl16SazO28bxfDnbCqqNp8rF0a2eyi1K\r
+oIQHaYf14s2l4ythyXJOtrWm4NTLrFTMloOzjQOmRK8ody9cOV7RvRKvV9tausu5TLaltqTn\r
+zYQbNeokbEvg7OB9XW5WF266UP05MhnXnHK9BB9tsm08vZbu8HI5UYlPnXO0GyO2yZeSAFSf\r
+XSdWq11lywsHy5uVEqcyOIM2dQtj/OhH16WbYWnSUAKOUfkn25ZTn66ebxA4ZI3ni7Ul9NIi\r
+1bos5kd3Ke3SWcNom9ASzW21tE3u6QqpJDGugUgD5jYICjjlP6XQ4fUJ/5ukENsyrj5oE36m\r
+a83Mttz3HmbbOGC6IFeBbRSFgr6hho9mZhvlZtlKvrv4sEPc3Ma2aSTLgYu/QvYJbMvExszP\r
+iMJXyEBrXahyPs023QxLk4Ztu1jZTX+aXATKmZSS2OUzWf8NuG1bwrdQreStNrlISZXNOn20\r
+hjK2ln+BbVT+1Sm3slKSXtzoBR8wF2P0qyWV+YYq5/VGbEs4CG+8rDWrAAAgAElEQVQb13nQ\r
+Hh2LP7Ot8EMUuOzgzbiOK7BNsqtOuYalDW1jkYuWbwd8fMghrp0Q2zSSpVjP1g6hbbl4mbum\r
+WCvK6xmQNrJQz2xbhqVJw7al1knaSEHdONuslrPHM/6bcNe2jq8xdK5yeeOLoST9tGFnKP8o\r
+dZq5bVSGpLyl/IClTcoHzO0M/OPmgAt3NVTbSN82sK3QivgpKs42FjqwTQLm+vt0ss1KzqUc\r
+i7ltOcVQe05pfFiLy2SbfKl3A3RRzK0NbStkyykiFIQ/g/FAusC2Zc62ZViaNGybNAM2+tNM\r
+rLtGux5eL2qbz2LrbCtmtvXmz6mmThLaVvOvk7esNmxrFraV1t/Xcf077U6ar2xrN22reV9u\r
+8bpqW+Jt6wLbuJNFVchu5cK24ZpthZtMTGwr5VzKmW3+DMg2WRg3br4bbcs2w7pu28V3gNF4\r
+vqBtvbOt7FyVa7FhG1cI5SvbuAzhhYovF9pRyd2XpexJL1daueDmtH6Q+nfaYW0bPT5wVIrQ\r
+NjuzTQJu5VLkbEsm2/KlbSU5Qjr0Pj5sWzrZJl9KAowbD0vb9Fys+0mRDNMZjCHpQpOxbU22\r
+GZYmTWjbxf801TaN5yvaVriSxGex/MiD2QqoViSX25VuZVvHz6v0e1/YVkjju89dKQJzCY+b\r
+JGihD2yTgw4iwWjVZFuYyZUPmG2zoW2t3HhTwF2ysK3O6yy0nwuh1BWiG7aVvstbYBudazPZ\r
+5s9gOp1N23xYmjRsWz6zTRXTEx1e0jbra/Z50j0pgpa2WW+bXr90T87Vi/2H/ElLk21W9dDy\r
+YGablSYJWhikUA1ss9a12gdPCf+yoW31ZJudbLt422hCwdA2kTWX3Qp3n0klYDbZZr1t5ZZt\r
+8pPs1rbJGUynQ7Z9T107NsNy9eKBbemWbRKXW7n3CblbA2KdbYVsf822Qm5aN2wr3Z+BbdqA\r
+ntvQNi0C5TCcPcXaNg5NeyQteooHtknArTR3rG0jVrb9Q3eb2WZv2Gbd6YS2XezMtiK0TRbG\r
+G7b6e76ab4WlSbNhW+b7UGs8X862gj522FY5E9a2uS3mtnFonGqVy10uFoJ7L27pX9nG6xvp\r
+23vbtqpw3dH32abR3Wtbv7bNqm3uQbcMzoAeQQt3N1n/MLetD23jE1rbVk+2aTxfzjZOAL1L\r
+ku0Lf/sU7lobTYphblvhtyDbCutzlz+cbfIUMbMtd7bZwDb64OJqt225j/9k2pZtwY/Dx2dh\r
+m34pT8LjOjaknWy72Cu22dA2O9m2HZar5E2sSy862T5xdwdBPF/MtkH7zE+2DfIgeMu2cts2\r
+E9qmAc9z95ptRWCbjnOoN22rvG0asA42CG7fAtvaa7blC9v6K7YldsO2dsu2IrStINt+vMxt\r
+m8LSpNmwLbdz2/LXs41SILCtuWJbk+gXc9v08mu5xX5uG4fJ3Rq8bTxqK+fNx49aU3NpG2/F\r
+Oy5s68xkmwSs3dm3bTOBbV3w41jZVj9kmwl6c5TBGXBlrluofrxcC0uT5rZtEs8Xs027VYtt\r
+ueVOf9u2aS3oVdt4rMdkmwa8YVuT8/qx8Nq2reLCbcu2waxs66Wr2aZt9V7bWhPVNk2aN7WN\r
+R/hZfbw0B23rzdq27Kht6aZtg9m0zRTbttVmp22kz/m2/XShsaLXbMve07ZKBrE428x92+ym\r
+bdXSNglYpMg3bGsyfxdczWyrje93SV2ZJtvqtW35YPwgMDu3TQbNbNpWz2yTzuZbtnU3bNOu\r
+4mVwBs628Tb1Yg13E98OS5JmbZs8iJZBPF/NttrImCtrdXTnLdv6q7a5LrzFlD3Gd+bdti3l\r
+6rTSLm1ruLha26adp60NbbPGDwKzc9vqTdvkCWRmW/uUbUNom1sg027YJklz0zaN56vZ1nCe\r
+Oduax20rZe3KNgn4um1UF1pu2dYaN25ublu3aVtl3LAcO7fNLGxTPSisuW31x9lmt8OSpHlH\r
+22S8srOtOmpbvbJNAr5h22WeV942GYq3tq3ZtE1KwrVt/W7bzPm20WwMw3XbJGne0Ta58XG2\r
+maO2VSvbJOADtlnOy7Vt9aZtnZTNK9u6vbYNH2Lb+D8zXLNNkuYdbZOpDdQ2Sobk0H0bZUA1\r
+s00Cvm5bd9GmqpVtjZHpFRa2UVB/XtkmD3hr23hA3Z77NioEf3G2bYZtS67YJknzlrbxfBpq\r
+G1djHKkB4cqJhW0csJqyZVsiv327rAHh/J/alLxtVCBMLVe+Eq4xJt2wja6w3Z4aECoE//SQ\r
+bWmQtts1IGzaDds4ae4/k75eDQiXGc42nq/oiG084dDCNg74iG08G9HKNp7zaMM2/n5tG4Wx\r
+y7aW61EeqW+7b9tomtSBbIclSfOO9W38DKi2FTzz2THbLnZlGwXMP9dN2/pEcsOubaNBlepV\r
+aFsStsprwJbc3LatsDtt43FeD7aTurS9ZhvVuP10uWpb+7a20ayA0nJV8BCMI7bRAJGVbfa2\r
+beaqbdS7aWUbj57Zsq3ftK262H22NeZ6H5Djtv102zb7vra1enUYbaMkuWlbud0HhMcCrmwb\r
+A17YFvQBGcS2wm7YNsq7YVu5bRuPC17blt3ocTSz7fJgj6Olbf4MWt8HRNqt5rYFYUnS7Ohx\r
+9Iq29Wqb4RFJO2yzC9v6nDVd2dbfto1yT7KnXNg2bNjGQm/a1m7ZRiXhyrZmo38b9d6+ZVuz\r
+y7Z5/7bRtJTb5bfDkqTZti3sTela9r4Sd20bJtvk47Zt6767fcI5sLJt8Df7mpH9zLYf1bZh\r
+ZRtFZWUb7zvvTSl0m7YVG313m0we+2a2ZQ/a1q1tW/bdtdUPKf+7atuwxzbzcrbJL1tHwdyz\r
+LRwF092xTQJWPZxtkvIaL5qpkXZX23RkvP279eLfsk0Dzh+zzY2C2WUb5/zatiRI23JzFAz3\r
+3J3bFoaVXrFNh39Zd6IvZ1s7XR0Gse1a390btnVimwlt04A51f4+s81dA81fN22TwYJi238s\r
+bavmtv2dP/prtrWrMVdXbLvSd3c55iqwrS3lHG7YRuNgNsNyU9dt2vbSY64O2KZFidom40mv\r
+2yal5aZt36ttOp50YZuUltdtk4B327YYT7rTtmLLtuCaqLYtx5OOttGoq8xuhnXdtsuLjyd1\r
+d74Jp5L8uG7ZlstN+cW6CZBkrLzYGtqmAcsTn7Pt/2fv3ZFkR840bQ+CVqBAY5RYAo2oJVCk\r
+UEZwKbUEihRoBGgtUOwlzFIaNAq9jMHYLGAw0qCt8cN/+Hdxd9wiIk9Gfudk5vuSpzITcHx+\r
+e+Dwu2dr5Zc/Q27oSvOL5w1d4keXm8db2tI4qRjub9HWb2lLa+XXtI3HtGW7KWS0TVmJF2g7\r
+WCvvu++v9O/Q1nhGW7Yzw8dcKy9rJGUfEFppdos22kmAk0U2d6N9QAoeRF3RxoalfyHSVme0\r
+/Xj1cReNi9f9s2SdXdYZk2ibt7Q1sjbumLZuRVsdaNN9QNa0DWe0LUF1x7TF9aRH+4DQSnk6\r
+XeTIliTNAW2UCk1w8zH3AZHdBmSPo5B3w03aSkoI3W/De9njiN/KFW1i+LLKXVf5eGxPR7Rx\r
+KRlcR9oa6r9y8aOeaJsSbWJY9jQ4pq3NaNvscbSmrT+hrV3vqMW0MbeJtsLv9jjifRnIxwNb\r
+2Q4Pa9qyHbU+6B5HXJrQ/m1ccThbc0W06f5tFyo4gnj/Nn4rc9qiYfo4xIy8+inSFn7R3c8o\r
+ncXThmrdYXbrija3WnMlhmW98nF/m1vRttq/bd3fdrLmartboNJWxU/6Esij/duItC1tq90C\r
+p6OxhNVugf4j7t8WhhMdkdLx3n3tTdou9Dmgt45p470paVfRDW1kOAxpZbnblro3pdIWHi+o\r
+QGuVtop3IaqkOhVpW3I20SaG5bS3A9oC1zlt7Wpvyoy20Ot3QttmJ1SmLSSAFtAUJr/bm5L3\r
++lzTlmxJ0uxpy3ZC/Zh7U4ZkCxuKhmm2BX/MbtEm++5S3U5pW3690gaiK9rE8EiTvhNtF913\r
+1/OWzbJRLacze+iuvHtkKR+4OHJVh02k6dG2UsPB44Aw97jmI1e0dXVO22rfXaXtyvu0ntEW\r
+djgX2q6JtosfE21H++4KbbU/tCVJM7k9bYXu8vwx990N0IT5be4Ssnegc39WtCkbnKGDo8P+\r
+xvCDaRvDPtgV7aW9oY0MLwk4uyx3XeNWtNHjupu20Oa4mLsof0pbOcW5u1c1PIarcYfgnDZH\r
+Z/Ml2rqwtXd4bJKZjNKPsgSjP6ONJqfL42WkLcwZoCEq+ktiMMqe4vQH74O6pi3ZiknTbGhb\r
+7WBP4fxgtMmxBAMnxbCfKb6ljRdwhPmunMHxwILtTHE2LGdfaEaGJP9RLAptTo9PvKT1pHKg\r
+mLusaMtmist5CYWXI1/kDMcVbeuZ4pvzEiL91/58prgmS1AXzoLx3C6ITDBtbndegnzna39s\r
+KwZ9Q5vPaXNy5taL8/vr6mHarpxYt2iTs2BCWnzHtPFZMNMZbX6duzltAw+60nN0Pt+attbp\r
+cXhM27ymTQ2f0dZtaVufBZNoG27QFqL7/Y425pYj2WgMxvwsmCPaMluntLXhnWtibK8fjraW\r
+C7VfxjLrwh34x7TJOVchLX4jw4WSV4u6a04bG6YfKXdD1v4sFgeeUOL0cLQyW71c8I8yo408\r
++lltV2JYDk/b0xaQ+LXLaBsT/VVOG12nN2xPW4ju73e0MbccpkZjMObnXBFt1E97aKvVF2VL\r
+WxeC0sTYVh+Oto7Lhu9imVXcok3O8PMZbSHp+Eu4po0Ny1F+mpEha3uxyL3m8u1oE209Y9oL\r
+TSvadIg8NKGdLoKhnD2i7bc5besz/GJZS7SVJ7SFaP1ZP8SRNuHW819HZ/gd0ZbZ6vRF2dLW\r
+h6A0MVnrD0dbz2VDGcusa3eDNtlUIPz4o9DWaS2r7Fe0sWFZ76kZGc5VTrTJ41KgVavVyz4e\r
+Uqu0LX4WkeSrGJbVy9nGWvIvPP63nLb1+aSRtpJW/rTHtIVoxeiUSptw6/mvg/NJuYt6Q9vK\r
+ltQemg1t4TRc4YvD+dFoGx2vNW+VouombR0n9PLjb0Jbr5vEXNe0sWHe0iVmZNgua01bz+XE\r
+8qPOVi/X/Hyd0xY8itl1FcO8HNBPO9qClTmnTU5MHuIeHPxlp5PL6jPaugj4kGhLu994CUER\r
+aev1fPOG43doS5JmT9sUUqER/yhKH4y2SfbR6JWiprveoG3gDozlxyy0jVpkhElCGW2y19W0\r
+yt3Fg1Jp48Wp/Hj40QhtMxcP/COjLXit/WrUd8t7aRHs8462sBQrTgQidXqAeOmzL3vJS8Oq\r
+Y9oGOmKPwxlpkz2/vPzFMRjjufJXz7RNG9qSrUk/EFvaQp+P8DXJ8eDNzdz79nSbNk9djkM5\r
+CEXFEvUbtE2yr+KSlULbLBUVGqzMaPPSl0k9spG2xZtI20Uer730QUl3cuekGC1XtIVatcyb\r
+pfFS7urlnfV9u6WN2605bdKSIPfpyx5qeCXNCDqibY4L+uaMNu1r5L84BuJRjM6etmRLkqbf\r
+09aXkS+O10ejjaoXYZk3O67uWJOpNkJAUM8VuuuhYf1gqrLhiSkN63ufz/zWXZC3KS1eazDF\r
+MM8O6Au/0biLN8ctPyrO81h5vXWZqy+jy42l6KI4++PMFifNPoQ+zt3dhvPd6A5tpKG87+bZ\r
+mm7ly7ej/olJ84Ct+d2VZmt9q7SN74S2XaH9prbmR7LrGxZoe5VA24v0zdL2Ffx8meYAx5No\r
+e8RW70HbW+lr+PkycXPySbQ9YIuapqDtTQTatgJtb6dnVojeRrJOy8yWLOl51wJtXyrQ9nJ9\r
+q7Rl/bnfqmgWRmVmK1um+271rdL2pELjLRXmSt0eaXiqLVrK9j76hU71rdL2rGx8Q/GCHzNb\r
+YX5pC9reQP/1tELjDdU6Xd9pYmt0V12++m71bdI2uXcwINjKVFwjW7TqA7S9gd4FbZ1MjDey\r
+JUt63rW+WdqsvXy5wnTtZ7URH7AlS3retb5V2t5BU39I611MbPGSnnct0PbFGp9Yj3rEVvu8\r
+auLX0rdK2zto6k9P/LI9Yqt73of7a+lbpe09VIefWdY8YGt4903Sb5a2ytrLL1D3xLLmAVvT\r
+u28kPETbV9A3Gqy1nvlOPGKre+8f0veRrdAHEWiD7ATaIDuBNshOoA2yE2iD7ATaIDuBNshO\r
+oA2yE2iD7ATaIDuBNshOoA2yE2iD7ATaIDuBNshOoA2yE2iD7ATaIDuBNshOoA2yE2iD7ATa\r
+IDuBNshOb71WfqhMzhDabgrNf0+PLffVA3WfvRsEG5zd8REu5+e6rO7M7303hlzPoe1/nt7p\r
+669G2//5Atr+3zMD9Rza3sH+Ow/rObRVp3e65qvR9u9fQNt/PjNQT6HNv/vtGDI9hba5Or31\r
+FWm7fgFt/3hmoJ5D20eqWT+FtrE6vdXanP14QNv0BbQ9t5L0HNraZwbpK+sptOlh8If2vxZt\r
+wxfQ9txzKp9DW9c8M0xfV0+h7cYxQZevRlv/BbQ9d6e6J9FWPzNMX1dvTNsM2l5LW18/M0xf\r
+V29M21SAttfSVj0zTF9Xb02bUd/kQQheTJt/9p6vkbZDPUrbUD0xSF9Zb0yb1fHwH5m297Dh\r
+9YN6a9qMkuoD02b1wlrojWmzejFB27vQG9NmVcX9wLRNH2igFLSBNju9MW1WXZMfmLb5Aw3L\r
+gzbQZqc3pi1l5NvqA9P2kSaBvDVtzUsC8+UCbe9C96ISDtNb8oBPYZKj9ahs7/jJIfygvB75\r
+ELpwc4qntsaD6dpdV4i46fhGy/msrYqeOJn50DqxPKZT7gZ9uORgndPWJX5ayfyd07YeJRmI\r
+tsxFxzX08ZL7KrGTe+m0wVGe6wv2p9nQJkHRB5brEncXI8w29AkJyHs/lDTTHdrGcLTcUI58\r
+XmgXDuCeL+OS3JOccd4uKdLT7134D9/s4vGii/ux8GMZj7cNOUBXxkDSfJn5Bh/t3VetPMin\r
+DPd8jGLL0HUpY+mg4qmY+UDw3lVMW3iorQI+XRXOV5wusx4ZHoPQS9jmYDo84JrFecjX/jqS\r
+3SGGfqJgdMvtOvoaLDCTFLYhnUneBg+Wl01vXPNx0hjYUVNiuUi/OjpQTY5VZhuOX62enS7f\r
+h+GD1N3u0NaFpBvKnlJqpqOm58uSkAGEkPRTODOTaJvpAt90MQuUtkGP34y0DZw3E9/oKbX7\r
+Sh4cmWU+InbmU2DndBb2SB5Mxch51Lor0xY+iWvaRj0cOwah1ZDktNHJn0pbG0M/8GtUtxSa\r
+UaLFwRn4/MYunr490RGjFCO+Ua5pk6B06v/sOO4uvHcSYbHh+JshQV1i9Uloo/weSk6ikNwL\r
+Vpd++W/LST+EpCfaJscF33Jzigegz0pbp2drRto6zpuUacuN/neS0czyxMdfj3xS7JQY7smD\r
+qeBCNWCYaAvmEm2D+qtBmGPYKFtrou2P5J3Slo5v7+jvjm+Lr0JE+KvkBJISd6C0ohjxjcua\r
+Ng5sSLhK0makX92wuB/YC7Hh6Ms6y3nMn4a2mRJzKDmJesr9+bJ8d8INSXPHtA1S8C03x3hs\r
+dajEEVutZkqkrWXnfSzFQn7/Vj4zfKj14OTLRrk8JApa8mAqOsrYJfsvQlvFtLlGaev1vGIN\r
+whTxCN7TP9/+RCALbclF8GcJRvcTl2bsK8Ff6705nQffxXeRCvnlrzVtHNj0wOw47i68vB2/\r
+SmLDUa1QA9J9Ftq4dBlKJzWzkJDzZamrhRtUpwnQUar18movN4eYopPSFq9E2iRvupg3i9P+\r
+B6GNS85errLvfTp63dGVqWjp4ZD9TFv4L2VgpK3Tz68GIYWtrakaHmj7A706QtuYAOJXquPb\r
+4quUP/QXJ5CQ0MZ30V34xs8r2tpYXmvFkQPnOvlUNNGGo1bTKE6XWH0O2rh0Gb7jDGgpaeeL\r
+4+JL07wm2vTVXi73O9q+O6RtnTcBr++dtEU5m/lql1jnModul0sGprLvT1W40QV4VrS1CoMG\r
+oY+53dZ04HGg7ff06ghtQ8ZDoq1WX2P5Q/fGRJuL72K4Em78uKLNxVfjItY57q7lmi591dmG\r
+o5dGA/KpaLv44Zeczi7SVg8ZbRXRpq/2crOL+aW0/TJeUdq+E+cpb0Lp8r2T3gNK/JZpaxPr\r
+wrDQ9otU9gltV6HNK20xbzUIXbyyfHaJtotvf8xp66MLKcA7hnHKaCv13sBB1DAXlADOccqt\r
+aJPA5g+0ShtXTKpow1GpqwHpPwttPaXNwIlAaDmuZ9MlecOvRJsmlNAmeVwwWy5eUdp+ucub\r
+kN/C5MSJ3zKDbfJdGOZfJw4PF4GVGJ+53iO0uZi3GoQ2XumYtuWFaNmA0NZFFxKM7kf6Ib5K\r
+cKRwHziIXmjjqj29OJxAPtImQenXDzhuiE1Zcl7YeS2JzzPYPwVt3fJNCrRdZiKpWJJ6+XGZ\r
+lo9nPVJDdMlRoi3cJDfLzXZxt6XNb2jjKzOxGfKmHih9q3/RY+4aujkXB+HmUpKI71oHY+9C\r
+B0JLZZ/PaStXtJELeoaDsPzZcm4TbVem7Tok2hYXnT5Th2B0rgxxFV+DFfreViOVP8v/ydwU\r
+gpli1AWf17RRULpQzW34Ort0bl5MXSdOTrZB3W3dhQPyaWhboBlcE5roS9QnepebkJ9NFz4B\r
+JXV9h5uccd2FMrtprzzEkNFWamd5pC0UIiHFqYe+c2R9yQfaiGO5NIe8DVlch1Zmf1EP+HbD\r
+4Sm4Wyp8/SoxPtH3WVfBOAo/hUSCQM/U4crihF6YwFfwTlfBkAt5hupP1AQv1deZE6MPCXEN\r
+4wYytDDS2xJeN0VlWNMmgb2QS7ruyOZy4z9CkCg5xQY57UoePuk/zlTxO7SFCkozaH434R/V\r
+jMIojKRGJ7SFSoxUm2o/bmiL/EXaruyccyuMAlG7lr2lbLly/0QdOv2Hi3pAty+er3AAyFRF\r
+xku6OCXaSh+Hva4aPsntjoqrEJiWqVLargrQUNBwVyiAloQQX0ONavkRyF0uUk2xZg/on8aI\r
+2pQr2jgobakDUuqSW8rU5xFtsPmKkvzJg7dfVbdpo7oEfeQod2m8hxIrjPD1JaXS8npTMRFQ\r
+5JTkX4NCQhFbkb9IGz3C372KRkkXk70Ob4VerlLGlqiH+KIekNWCrlCtK9BWeRmiHHa0XTnD\r
+vAaB+zwqCYnSdiGqmDaOQa2h7670OVtQiL6SofB+9CX3uZDjkZrWUm+sg71pTZsE9srdgl6+\r
++TVdCRFe7EcbZJ66aJrPRFsVEjOU9yG/L0JbzQnWy/vOtGU3qZe+Cc9H2iIpkbY6lpOBNjbX\r
+y9BUT1lJTzBty6/iAd0uY76Ll1NGW5HTVukzEgQqtTLaSqKtYNaFtkZ5CKHv6bsZQhR9JUPh\r
+deOYq+PCp9eNPv/zmjZmhzop5TrH3VUxRtEGvX40MlJ/HtoYrHqQ0oXGkBNtA+MxFtoj9CLa\r
+mlhOtjHFlbbwnR6YtlZpG9a0TRltjRZXi3fjL4tQFVPa6h1tjU4GCXD9qgyLTLRkvTKuzZa2\r
+glgSX0cq4cnGjrY5o63W2R4r2uacthT3LtHGzqkIrD8bbU1IkIELeFr0rrTVTBunUqCNbtZK\r
+m9/QlkjJaas5xa+cqEWijbKSZqcxbaHezB6ojYlLGc/1OimuqGulEP77q7qgWxwE+p4zbUvY\r
+Am3Lj5arBUqbVx4Ggp5pu6ivUsYGxDjmMtspOCZSamaoTlOcmLYU2L7k6+xEKybhjVIbnC6a\r
+zh9nYcJ92jqlLQLV+IgHv++BtpCCrdxswuZpQf05bT7CuSQ1jfkXkg2c+GNBZWlbEW3RA709\r
+xQzkIrAKNx6g7eIzlg5pu3htBhJtseARX6WEDyHhmPt/xaimegan3BFtXgurGPeagQ1tA7Ux\r
+6Rv9D47VR9FN2vg9r6Q6QVEX2saGPyRXTfN+QxtLabvsaLsk5/113tBGBUdB2ci0iSmhLWxO\r
+SLT5A9p+fQkfR6XN55V4pY29CbT9UIQVr1prJNrGNW3eayTFV37nGpoNcslncV69ftePaUuB\r
+VdqaSFsIQYin2qCGtE4p/lS0/Usrr+y+nhUmpa2WvE201WogY+uItko+LATUlrZGfs0ezOZ0\r
+Z7RdvNK2VNiGhbbhBm00dYi8WZjsF9r6LW2Fj7RxNkuhJr7SRS6Np4vOvhXltPlj2mhqXKJt\r
+8VaLsdklG0N6gz4ZbbECzO53tHkfaesYDRez4CW0LRmR0+b119RhkS8/SLTxx5FuLCaG32xp\r
+Y0IlCENG20LabyNtw4o25kG69w5o4/JbeiqSEm2T0yAf0DbKYLBf0eZ1iGGxQV1BraTGp6FN\r
++u7LNW1yV5pkfk2b9zpI5RNb/PXZXhHnQ6JN55XVMQiBtkIf3NDGX+MD2q75jlpCGwdhSCwp\r
+bVVs8uxpqyR2Gn2pyftEW6o1eCGFntjTxkE5oq3Z00YQy5jbJ6Ztzmjrc9q6jLaWc8k/TNt0\r
+TlvokOIZ1mvaxjPafufu01bIlf6ny462nEd+bTLaRpd/BkO9XqfCcXrdpW3c0DaUcca61/HW\r
+xQYVa7oOY/wgg6T+Dm3jjrY+0UaD9BX9GmijGQ1Km6bPw7Tx5AelLVX8BqKNHLs41cOTHzED\r
+ZQ6Y5+xfaOtv0BaykWkLta6fqJ6/po38EdrIv4y21sXuC57B0bu8cOsSbamtsabNxaksK9pc\r
+nBpCNjTGeS58BL2MtrCSQ2lrT2nrIhXdbdqunOLFKW2TTDyqt7QtubyiracbT6Zt4mlSTFsj\r
+vq5pG1xWuIV1NjltwxfQxjZ0HcQ1y4WPoBfRRlPy42t+i7asm/aYNvpUXGll6jltkxPayg1t\r
+YerXCW1V+PcobX+8hFn/N2gjzmttQLoj2mIKht9fQhvHfU2b2OBZlDIn8LPS5hJtPI15SxsX\r
+Vr1+FV5Gm9vR1hJfnYtzJqUmwxPL97SFSbt1mCZ+m7ZRc7v9mwu3D2iLU4jLRBv7uqKtSQt+\r
++NJjtF3E/w1tdbSh69koIT4pbWNGW3uDtiFmwetok7mzPe1hJa4AACAASURBVKV6Tlt/Sltz\r
+h7Y+o83do80zGEpbf0Db5OKndHgCbWrD6Zfk6j8RbZzoSlufaJMZ9NJ43NCWVi19KW0N+8u0\r
+DU5X2Sht7R3a6pfQ1pzT1lIhrbS1WRiVttnFVTDdE2jrotEmLc/6pLS1ibbxFm1zzK7X0dYx\r
+bVx+ZLTJjP4D2tqFtPZB2nwg7TZtUqrW0gt2QJuu4vZCzCtpUxu/4C4RdvlJaXOJtuEWbbKy\r
+17+WtpZp46XNGW3TDdqW/9Xto7S5MOB5gzaulQlt0yFtvVstankdbdFGyV2MTPKnoW1Vb6Ov\r
+htAWEvn7s3pbaqm9rt4WrgUfekfrYSJtIct+eUxbTbQ1j9HWEG3+nDbed0FoE1/X9TZtt/Ir\r
+8ItX0hZtXKneK1tSfE7aqD9CaFvy/jKsaOPfmLZZX/hHaZP0XNNG20IEH2gHkDSMQED89ZS2\r
+7hZtWQ8IkdbuacsHB3r6htc8xMq+Km3qpJUWUUDjj/dpS6NQK9oav7JRtfQok/w5aaN9eIS2\r
+1tH49AltsmXRK2mj/ZPIh4XtJqOtp2LgiLaFNOoDeYi2tuE+kBu0UT+r0Ca+bmmT3Yloz6P5\r
+5bSVOW3RRt1Gm5+XttIn2poNbQxKHL1+iLZKUlynM21pK2Q6yBhoS3N7emo6HNMWCkHnH6Pt\r
+bwttf7xsaVuNgXeRNvVVaWtiCiptV/8AbYmc3ThpZqPp9HtdfVbahmz6KS1LO6fNd2lO96Oj\r
+8n5L26jzDAnujLawLOWEtuoltP3xPm2BeB65Ul93tMmy0bA68bW0JRu9Noqrz0Pbeg4ILdhW\r
+2qrNjKMNbbII6gtpE0DSlMIFvJy28mTGkYxb3aItzQHhcas7tNGC9YppK7MZRxltnvsW++KR\r
+GUd3aCs2tNG6rE9FW6+09bougSYeRdrm6wFt/hHaZIprf0jbRMsh4trLnLb2ek7bhcblH5nf\r
+tpBW0Lj8TdoGpm1y6is9/G85bfzV68ovoa1f0ZZs6FYM7fUTzW/jNFPaQqoqbU2cuzu7OJsy\r
+o40peZC2eUtbFUgfimhn8bbPaKty2qactt9c6N9t2mSKcPfbgv4dzKZMGhNtVZq729YZbWyu\r
+u+a0zbdmU/qUVPlsytyGbjvdgbZD2jgHKQn/6W/TNhyugvFntP0beauzyP2ONs5fujPcoy1b\r
+BUMzdw9oSzxMlY+LrS5b2mr2sDmjrTmmLaW3OuGp5X5tI9DW+09IW5fTNme09YVO2Vfa/km0\r
+Zdl/mzZZ49AxbVO1om1k2lwlYIoH8fY6AyW7aJ74r19AW1gHc0DbQGEVzDquO4mvss4x0DZf\r
+s9XFnBQr2roT2saar7OTSFuZ2ZhkhUP/iWjT9aSntKX1pH36NOa0lQ/SVlOGKm38Rm9oS9/V\r
+W7SFVVfFHdqkCtj/Nqy6Kre0yQfOn9Am60lDvXTx+eW0NdpKWdHGhVqyMetC589GW5vTNmW0\r
+dXFvjCVvucFa8U1/g7aSMB21rV973Z1gWNM2FLzUq5J8Eg/C7ZDvQ8zAbK28H35Z0L9z2ghN\r
+oe1XJf1b00Y8cMW/F9pSb88Q9yOh6rvTRyXMY6Rtv1ZeA1v7eHyHxL3RVnaTbNB2NE18OT+K\r
+7tBWb2gbM9pa3kohpy27WQUDwyFt3eE+IEORaCuVtslVsjZOPAi3g+1+nYFCW1gpT4drnNGW\r
+7QNCK+VpFcsBbVcNa4hdEWnrnb4nbSrwUkE4ZLRVN2iTTpSKwp1oy224Km5Q81lo0z2OEm1D\r
+BlTY/+6iHSGcCal75JS2Qmhb73F05Q+z0MZ7HNEWVoG24Fc9qmW23cUM5O2EyDvel4Hm257S\r
+VqeCMOzLEAKwom1OpZK+Sxf6VX11vMdRSaUxBTzS1ifawg23py3uMSN9aUIb+eRyG66KG4Ss\r
+xtLet+7Qxvu3lbG/rU9AzbLbYr5/W81dv7WWQse0tbp/m9c9zFou8SJtoZ5fEso0cOhpCgZ5\r
+EG6HnGyzDLxqZ7LnY1Vu0Jbt30akHdFWKScDF2q0f1sZfV3CoPu3cZ2OC8KAUJfRVvr5kLZg\r
+JdIWP5gU1dxGy4uq6fP9WWhreW9KoS10hCptARb+IHayN2UVqy0ubthIxdOKNtrdkfem7J38\r
+WlPx0ZaRNt6bkjZsDLTRPnvigYRqTvPbaN9IAWi6T1vcm5LCsKfNu7h7pL5Li6+uVF91b0pH\r
+7wft6se0FWHINNLWcRz9lrawV5xe57jz4peG2E02Ot7oi6oYn4Y23ne3lHHSkNy1AtU7LqIc\r
+tdxDPuuHpE37j+5pC9zw5442seWNcTvepjLRtvhypV1Ogwe0h6l4EG53vEt0zMCL7rsrtF29\r
+9Mnsacv23RXaqhVtIbsvXra6lJ1LO96WV3yN++7WwVZ/UTRpq9iMNrfZd1cDG2Ii1znuTFtF\r
+0Ug2upKPg6ljh81H0G3aurDtdRqVDxt1C20Xz5vIF7T7cklpR3nheQz9nLaKMXW8TbmrwwYE\r
+fTB9jbSNYRfvagzABdqcbGXudY5msJ8ycPnTZbRNSltxRFsbtvZuxJcrc5XRdpHQ1xKMieao\r
+l4EG8TUEgpC90sbRIeDkd0+nlETaele3R7R1wVnF1znuNGEy+FTkNqhMDVu3ew3XR9Bt2nqa\r
+UhtpSzPFZYIrn5dAewjwnFRObKdzd/e00QxYOW5Annde17sobbzYiue5VnIEjY+TXmUab8zA\r
+EMgfK7ozu2uiLZzO4fe0adjk41+taNMp6U0MxlWXR4iv+XkJVz3oQZMq0RZufH9AWx8fmOXo\r
+EF3wQuxGG/Sx5oB8GtrkLJhSZ1NG2lqZTq0ZcZVMeIi2X0tmlV6Oo+A/60jbHOdNZ7SxB0H9\r
+K2jrIrNntPUx9DlttfqanwVTZbQNj9GWHech5+CEv+aM3c9L20g5nM0Uv+jWZMvvPwgCNdHW\r
+OT0QLB6Zw5NI1rQFI79drvzC8eEnbTxrLFSUdeRdEn1Rd5XD3MSDoJAp3y31vywDf67oDrfz\r
+qhu09ZFZz9t811vahhh6OQ+o+54CJ76SBTm2pebXpIlJpfPoK/rz9we0jZFloq2QdGGfMhuj\r
+i4cUaW/mB9Bt2vQMv0hbkdH2hwv/DBlwjZngvU/nku1pm4W2gpKXThYrJV99oi1ATBcvHVsu\r
+vXjgxf5vEm0hj/rKs/VE23BIWzrD74i2Ub6RaRFMeJd+cPq1/41YqOSFkPXz5EEwnGgLN/58\r
+QFs69I9OlCslXdrIbqm7qDeji/uQfA7a9HxSSTMXDjmOxVdIwJ4pcaVPB7n6tHp53tEWcumv\r
+4UrLOxH08XzSS0ZbpzW1sr/K6uVsBT5tR5JoC9tmCG2xp5doKw5oy04f5a3v6zVtF58tf2+J\r
+qO638WsfJhrRT3khfFq9TGuE2khbCPzB3F2f1trSSaaVpEsX2a10F/VGVy9/Gtp41U+2erkS\r
+2pYkv0zUJRkSZHClZALdnCMX9E1Y07YYmcKVjjNr0MO6g51IW68bzYSz2WURneSy2G8SbaF1\r
+vKKtlm/ZEW3p7GWhrclpm+RctGsMRii/fxfL30aQ8PpCyJY4mlQZba27HNHmszeR407pwm9t\r
+ZoP6fT/Zabh6rjxz09FiFErPic4sk3VR1Hs2y7tORtPZ6Tva+gXTcIWGM+lsdb4aEjbSNur5\r
+t2GikWyLMes3y9PJ7rrhbu3pCPdK7lReN+meDmnLTo3nAZEmp22W5kjNLni/rK5q6eVhX306\r
+V77kBBKvg2GtZ1Qh8MUhbfEBOoFe02XkYizZmLl5VPNTn4Q2YkJpoxbbVYHSrpDaU49nyHOl\r
+rY/bBe5pC11L4Qp1ws5O9jmdtPXFmuUce24Pd+ym01KE28CJttCHV/GdMNIjtPlj2nqtlHm/\r
+p40OIRpi6HnNU1d18rW7Ci9XvldTdHQWb6hOdIm2JX6HtI1K+3zhuPPzzGBmgxu8FKfms9Cm\r
+I9i7PzT+/LPliVl6c44OswnVPr80FtLyjwtp1h3mPClHJmvI8GjyYFqbnY8jsTEZHdeHrjN/\r
+UzR7rqWPRe7rlEZ8vY97Me8MH0SdH0hT3rvokaTjxgZ/0c8MvUPdo+2VWt7LQ426puhtvX+K\r
+Mia+ikDbowJtr9d7SKMH9cZRWdpThwJtD+tdpNGDAm139bVp+yAthKC3pq06vg7aHhZoe1ja\r
+M7EVaHtY08dZlvDmtF2Pr4O2hwXaHtYA2l6r8eNM3X1r2kbQ9lqBtod1llSg7WGdfR7eo944\r
+uyfQ9loNX9f7p+qtaTup4oK2h3XWrH+PeuPsPussAm0Pq6+/qvdP1VtnN2h7tff1V/X+qXrr\r
+7D6xD9oe9775qt4/VW+d3e3xZdD2sE5S8F3qrbP75M0EbQ/rAw2Tgrb7Am1P01tn90mLCrQ9\r
+qvkDDZO+OW0nfZOg7VHNH2jg6iNNQ4a+eYE2yE6gDbITaIPsBNogO4E2yE6gDbITaIPsBNog\r
+O4E2yE6gDbITaIPsBNogO4E2yE6gDbITaIPsBNogO4E2yE6gDbITaIPsBNogO4E2yE6gDbIT\r
+aIPsBNogO4E2yE6gDbITaIPsBNogO4E2yE6gDbITaIPsBNogO4E2yE6gDbITaIPsBNogO4E2\r
+yE6gDbITaIPsBNogO4E2yE6gDbITaIPsBNogO4E2yE6gDbITaIPsBNogO4E2yE6gDbITaIPs\r
+BNogO4E2yE6gDbITaIPsBNogO4E2yE6gDbITaIPsBNogO4E2yE6gDbITaIPsBNogO4E2yE6g\r
+DbITaIPsBNogO4E2yE6gDbITaIPsBNogO4E2yE6gDbITaIPsBNogO4E2yE6gDbITaIPsBNog\r
+O4E2yE6gDbITaIPsBNogO4E2yE6gDbITaIPsBNogO4E2yE6gDbITaIPsBNogO4E2yE6gDbIT\r
+aIPsBNogO4E2yE6gDbITaIPsBNogO4E2yE6gDbITaIPsBNogO4E2yE6gDbITaIPsBNogO4E2\r
+yE6gDbITaIPsBNogO4E2yE6gDbITaIPsBNogO4E2yE6gDbITaIPsBNogO4E2yE6gDbITaIPs\r
+BNogO4E2yE6gDbITaIPsBNogO4E2yE6gDbITaIPsBNogO4E2yE6gDbITaIPsBNogO4E2yE6g\r
+DbITaIPsBNogO4E2yE6gDbITaIPsBNogO72QttFV+Z/t5cHnete8zCPV4Oove/AR9S80von9\r
+t6UvTmLVmyY16yZtI0egrchlCEvrXPbY6DbJPy8PLE5cIXedpMDsXKkuas922NHATpnagt1M\r
+Tm0tHrkLP89eZc41cPRfMh+e2xumZ4P/7AkZdxqqcKlnBxeJZ3dd0t1xPDu+pdHj2Ldq0PdZ\r
+4INdubyJp7uG4KTEkDg2MY5L0o5s0zWDpEEXHvJ9mVIxD0t0ni7OFG+JLGeL3pw5RTz/6MoY\r
+3Fr84p+ScBzTMgW1v6TseKVu0jZxeDjs4fdpCXydHutiACf+qbSRozEl/hCzbGZj0dFAUeEM\r
+myNtjdgiG40k4mXtvKNcZNpGcqW0rQ0n2hwbj8Ef2PgJbVe/pU1in9HGfkS7Ln+/Ujyd+OWy\r
+LOSkFSbv0lb6u7RxEj9CWxGDW4tftdpsUkzlNxcsp0x5pW7SNq+ThKN0TY+lnBg3tJUSfMeF\r
+VqeULoGuwo/oiKMixUOkrRZbREKlidjkzmchnWjryFWkbWU4o63xK9pajs0JbYXf0iaxT7R1\r
+QpvadUrpOp7h1yE3RA9xHCtO2ju0Xfxd2jiJH6CtPaGtd+lL1Wt2zU7fpvEZNfw7tFU+Uh38\r
+7yQXvHw8IkTDhjb+1Dl9XfJc4BJJHQ3kYEtbJbbIXam0VbnzSVK+FXNlRtvKcEZb7YU2+Taw\r
+8RPaNAsTJBL7jDbJB7XrlNJ1PMOvbLKOSTtKHPndvUdbiMsd2tifE9okvkxb+vBLcLpGg6qF\r
+b5eyS9+m4dEq+i3dJpb8nKQgqyXxwh9/D/8ZYrx2tAVHY0zg6SQX/Jq2cU+bsDOnTFPn8hn0\r
+nefbRUbbyvARbRR8LTXOaGu2tEns79B22cezUNqqmLIvpK2+R9vM/nwRbX3jowHJdwUvOONP\r
+zZAqdV+u27S1/EVkl3VWMaHs6gUBr/XljLZak6Tyq2/qmOdCrQ0hpm2ItF3F1pynZUiA5LwX\r
+250XmnPacsMZbZXPadMS7Iy2ekubxD7R1gptatfF9FnH86KepYrbILSVnLT3aKvu0TaxPw/Q\r
+5o5pm1IekRXNLv3C9W9PG7VfpAyNtYIQxr/H2HACdlvaKk2SkGp9ngv0QHTEqc609ZG2UmxN\r
+8iB7XOTOO7Hd+dj6TbTlhjPaYkmdXpblsTPaqg1tGvuctsb7zK6L6bOOp1PaUo4N8kYxAHdp\r
+u96jbVSwDmmTej57lho1NfvVN9GkZoD+SrVffquzJs4X6w5tIXwCwRK2Kcbk7xzumIA72q4a\r
+/tScoritcuG6pq3b0aafkVl4yJy3attLVtYZbddHaGvF+BltmxyOsb9HW7WL5+Js0BiIXkhb\r
+eY82+fh/OW29BDSabziYXupT7fUmKY/pNm3UxE+0hSTsF7/HJbuqEIl6kgRst7SVmiThRrs8\r
+IykwSOVBHXGqM21tpK0QW+Fu5yolJnfOtE1N5/+dUyqnLTec0Sadchx8H3hqF+NntG1yOMY+\r
+0aY9PdrZ55TSTTwjbSm1e3mjLpy092gr7tHW02tQ36dtdqkJXVOe1X3zn8FxGQwk8w0H00t9\r
+yoA2+ohKGbqEZKAsXWir/j5WnkhrpVKX0VaFQBbaTxEs8PeKc+FXnAvqaODOkktmhA2TrZ7K\r
+t0rbp03m3BWhYJiu3RSArENRRrTtDMduyZHsB9oo+HOiV2qMibZC8qbLk1hj7+N119a5XWm2\r
+l5t4UiayydRn1X/PtDlJWm2M7WnTVOzW2c3O48XOxcGA2PLVmwIYh3O+bGkr+7+VEkht87la\r
+hlmoHUQAZC2cL9cd2ghtSdpaytUlBcq/9xWnfC89sWvaKHm4D/Yid/nz6PuUCzN/GiNt8wFt\r
+F263zdzX1uQMXcOXdyq64Ro8CYFR2taGD2ij4M/c985s7GmjknZDm5dBC7k+H9Em36ssnkTB\r
+wN13tRrrXkIbpeId2orYx3ebtmlH26X/S8lN1UnbfE479qkw6YrY6fpK3aaNulqE6lDDkaJu\r
+uPx9QXDkhmft97TROzFyt5dk6ay5wB9NdcRfHHI6Jdqc2KKaXCu0TcS7OJ85OKPrlvCEoj6j\r
+bW04o419keBz0p7T1m9zuE8FfaStyu3K4N5lG89AAZnMxlm777PP2l3aQirepq1N5c9d2tQ3\r
+dji5n38uJCGkzdcXaSxEIv6UoYQ7tE0b2qTRwJ3qgyKQQIm09Urb6PSdk1z4dcqFnqAgV+w0\r
+0dYIbdQOKJmYmWgT5yFkw4VqGCWVvktyR9pWhjPanGfaKPic9V02qLqmbdjRlppMSltX5XY5\r
++7pdPGelbcpo+0Foq/0DtI33aUs1+QPaqAwehLZWfVPauKOODEgPQxnJdZISNrTVsQxdflDm\r
+R9p6LoFCqMYtbYHEUV/5McuF7rfkY3SU0dbvaaPkU9r8mjbKA6aNOisy2laGz2jj7t8+6+lb\r
+0zZuadPYe70+XejnhrbebeMZyt2B4Yr22j/IbIPaP0DbdJc2fqEfoG0spLxd0zZl7x492HKb\r
+mTvKnzRwdYe2merejZdIUPAX0nvKY84oCuWwpS0AJunU8Pf4IBdGwif8LcPsibY6LycLpa1K\r
+znnwisApKNn6jLaV4Zy2hh7i4PNA6nBO2zaHNfZ+T1uTZR9TvKKtFZP6pfIZbZV/iLbmHm3B\r
+BH/mT2n7/pC2kdrj/I2S7ofgouNuHUpE/o48QXeQlZaeRKKTToWR2tj8IjieT7OhbVLauB/D\r
+ex0m736XcoErYjTVg1uuibYqLyeVtrZKzoW2iYopoq1ItK0M57TV3Eqg4PfZ7JMj2nblicbe\r
+J9r6a27XS4dIs4lnoq1NtP0ktF39A7TNj9HWn9FGX/xeaOsqv2oluDRloZf+rGpFG9VanjGU\r
+cI82agzE+HA1hTryL/omUYkrLbBESCgUYzrpxJwQn/Z3Co7UZyJt0xFttT+jreAJCn1ID2qx\r
+ZrStDO9p4+DL5LQsj+/RprHX6+OFsndDG5cSeTyjydh/vTj9ieshT6Ot8Xdp+z1VR4sudkaz\r
+Q0qpMSvpKdn5RfoFh7J+zsDVXdrKWIYqbdNFqrtcS+CK/A8b2vyKNropuVBnuRCg+CW1bEOH\r
+ifuV0nYJeRBpG3Pa1DnT1mT9IxltK8OJtl+EUE/abOGXZXohbVNOWzGUK7vSPt/FMzTjuUWe\r
+aPsb5yO3JWv/FNqGR2gr+w1t9H2X6YCMA9EWbA3fad38KQNX92hbXkZNoUib09lAj9DmtWUp\r
+ueBqzgx1tMSn8jLM/odIW1vmtCkxXeZ8KHSWSM0/xy1t6jLRVgSIY/CVtnD3iLb5mLYsk4eC\r
+K3jRbkbbKp572uaFtoY4KPwDtPkHaSu8P6SNAOuItr4kjnLaQiBXtEk9WGkLtaX2GUMJ92jr\r
+j2mjcorbyBSq9g9b2sIn+C5toT4TaevcX15Am/+Xlm0VcbGmLTd8QBsFv71qiJ9Lm5eKfxbP\r
+XmmLde2vQtufT2gLwdfvD9uuJV/73zhJThvaLrEMzWmjviqhjZLrL1vauusunTgXGiIoZWwZ\r
+4sq0xfltl644oy0698zDRF9Sv6EtN5zRFiCOwX8CbeVYrOxK//sunpG2MdHmmTaq05rSdh02\r
+tFHnu9LWqH1+RNo65XMGru7RtjQklWqljS9fNbBDFcI77mjL0qlNtC0O6H76wpQhq4i2ItHG\r
+I1ZCm/c5bercMw80JD74LW2Z4Yw2Ht7R4HNkXkib/CoscNdrtCu0XbfxTLRpci+uuZeHh8Nq\r
+/xTa5Nd6FUryhT5ARFt3jbVScUhd89zA0JUhXmhr/8i+FM8ZuLpH25I8SrX2gFBQfii11/f/\r
++pu0pSuSC/6UtvKANvE7tRK2tA2/lodeQlsIvsbrdCzhuAck/7W/3qBtG88NbZcX0fZgD8jm\r
+1y1tPFZbbWkbv4sdDHOyktPWF88ZSrhPW3NMW5FyMaTdjrY8ncRCy7UbMpLe+TJkFdFWJdoG\r
+d0qbOvdSTP1aPk4r2nLDGW084MfB17Sm/34Zbd11ciu7bLLbxTPSpn2XIVzh1vKaSkvHgDbH\r
+kw2qODoRadvGND7MLeelPmVD2+T+GOuSdWoGdz+taBuLG7TFWO1zgaDoZbpI1yTaRqr/x2+w\r
+0lZnzj33RvS/E9qGHW3iMqNtcAIRBb9O0Tyk7XDkyseoZLSp3VParqe0cXur9vdo2/FwNHIl\r
+v2rEEm2Usme0DddD2ih4s3TMP2fg6h5ts/tJMyvv4mv/dslrlEe0FX4bK8qFizRi1VHC559+\r
+T5tmcBonzWkjRmuJwZq24iZtIfir1bgPjZNmHZxCWyV9Lxvaym08M9piWMjGo7TdHyfN2oxn\r
+tF2YtpqezPKlr9pm3+bkl4FpG91zBq7u7szgflAXgTb1M5SwGW1DuaNtOKCN61FZLgwBChrz\r
+XZz+Z0bbJL1o2vee0abO+XHtRd3QlhvOaAsQU4D+5r6EtpTifL1V2qJdimi/i2ertEVPB6WN\r
+ZjU8g7ZU8J7QNjNtbkdbVy0fjR1tND5yYdom9/NThhLu0/arnDZJLGq/T+nRpbp8RptMRQsi\r
+2oodbSGrGJ9EG/eiNbKETnKJhlwz58Si9DSsR+VXhjPaAsST4+DfpS38Z01bekJoq4U2sZvT\r
+toqn29NWCm0+tu5v0bYjf09behUOaAu1AqWt2dLW1stXU2vIyXwdYqG0/WhDWxtXbtBIt4S/\r
+Xf6X0RYr+Hva+lTBk1yQ2oM4uk1bL9URzlXn17SFzo+u6aL1R2mT4Dcplke0hZdjlcMx9ok2\r
+z3lySFsWT1cd0BacPUpbvyV/R1s2LfiMtkJok6mp6Uta9/WONprJMpRM25yvTXyN7tEWpwGF\r
+sI2K3j/9vz1IW7embdjTFqeLZLR5x6Of+dwSmkSyck594EtYOAw72sRlRhtPoNoF/5i2dktb\r
+jL1ed0qb2k20reI572nrI23Bm7u0dXdp6xMPB7TNodFcyGjflrah6f2ONpq0q7R557Yf2i/T\r
+C2mTsuZ/L7WsLLva+oy2sBphRZskXwZFnC6S09aWQhunCI+IXnzunGeK/N8lLBzQ65Y2cZnR\r
+ptPJN8E/pI1Y3tAWG35CG0c22c1oy+NJ0+02tF1lpJerA7W/SRut6bhHWyyrj2hziTapkmU9\r
+IKHPdENbx7s/XY1pS+8MfS1khdl/+6ynMiT7IW2y3i2nrX8ZbaPTZd6yzG1N22L3/9MwtC+g\r
+7b+9v02brpJb5XCMvVyfb9C2ime/p617nDZdNXibtsHlxUJuI9JWDlkndD5yRQ9VK+u8ciw0\r
+ujlxN7e/VPdoS5UVaiZmkCfa5iPaLl73FIi0FTvaLqEQcNc9bfIBnmRpZNqZITlPuz5x8Kqc\r
+tsywLLGsg/m2jJTdp632siIzdsPE2G9pE7tC2yaetAhFaaujAaEttHTv01ZtwhJp04uji3va\r
+JNrkZqgdLi7OaNPky8XblbVKW5c7fYXu06bBoHkT2XLvnDZ/Tluzpo0LgRUUbbmnrZcyiUs0\r
+n/Y4Ss6nFBTPdaMtbewyp60rXkCb3+Zwir3Qxr8kuxltGk9dXbyljQvo2VFt9D5tzT3a5nTv\r
+kLZGabtwPO/SFv4OjW75s/HP0D3axhVtnS7D9tl8hhCDc9rS50O6FF5EW+tkjYIUNiva8tSK\r
+zc2d4S+ljWrnqxxOsScWphu0dSvaLjva2hfStgvLljYvTPpz2mg4fjyjLfvda3U9tCjoz1fv\r
+sqq+3Lk/xWBQZmfxTbQtyX2rbJty2uTDmkPRFXvaBul+7fnrlZdtXew3zquuk3Sw7g3ntPWX\r
+15RtKfZM2yX1rV22tFXpbeEK54Y2fjfCVyEMJNf+tWVb9ioc0dYu33KmrYhDa7dp49AJbcM9\r
+TB7UfdqqLDyTS5/SB2mr7tLWX/a0jdJQH122nSzX8MFQjgAAIABJREFU29T5tOoE4nWrR4Zz\r
+2gb3inpbFvstbWL3jLbyBm2T9KO8st6WvQqHtNUvpq1Z0db4Z+hlX9KsvM5oC839U9rKFW3t\r
+C2mbuZBJexytaMsqbsMX0dZLgXBI23WXwzH2xMJ4g7Z2RVu1o829kLbyLm3pVTijjaYaDQ/T\r
+VoV8MP6SrtqknD0SrkTbUiU+p61Y01ZLc0MdDbSwc0fb5GqZQenS/m3Uuxud/5fPG6U66Wdv\r
+OKdtTANu92nb53CMPdMmmZfsZrRpPJ0QuqWNG09UelQP0FbcpY28ShmV3wz9LLXvmTbunrlH\r
+2/8I+U7Lj8TS+vaX6j5tWX/bqhcg0RYaWKe0XVa0BSNb2pas2tG25AHTxn0gQkyzci7roVlt\r
+saeNXea0TTIm7R+hbZ/DMfbEwpBoE7uJthhPMbShbU60hX6a+7Rd7tMW+0AOaauEtv4x2nxY\r
+yzaUibbKP0Mv6t2lv/XCfdqo0ZVmWxBtDWfqw7TxhsPyVarXtI1Z+e7KZ9JWSAfLJodj7Jm2\r
+khPhkDaNpxMGtrR56cC8T5uk4l3agl9Vyii/o60euGvG5/1uMf2y39lamdFm1bvbbbqo59hn\r
+nfrb2vqUtvDFz/vbjmhbsmpH25IH8tjkdPArVCFz51l7mV7FHW3sMh9LWCB+lDaaf7Lpv4+x\r
+39Imdk9oa/wt2pZsvTtOGupNt8cSKEOcDt/mNrgP+bp8TG/SVq1jSms7r0qb1cjVag5IUL+n\r
+LXTmnIyThjHCjLZ5Q1vB70+1p60t9bHOxT2OrivnPpt+S03nzciVuMxpWyB+mLZhn8Mx9nS9\r
+T7SJ3ThylcdzzHzQsEwZbV3xwIyjxcVd2mgLUH+Htu5R2pbcooY1G8ib/6/QfdrUhYQtduFP\r
+D9E2Z7SVPu5E/wLaRhfXym9oy5Jo1NGY59F2sNNLjD3TJi2XQ9pSPHljoy1tTuqjj9E2P0Sb\r
+1LHPaGsGrYle9rS1lV9r+TzktBX+GbpHm/t+Q5vufZpom50/p83HChjPQDugbcmqPW1hUVkj\r
+Hsf1pOXKeU7boLNo9oZXtLXlw7TlOdzF5G5TJncZbWw30raKZ1v6jDYyNWa09ZcNbeRiTVuI\r
+qYald3F3wg1tcc9on9vgEYvSt0Rb+wLa2trrbMo/mdA2uz/ETJew9VvapssLaSs3ULTlnrb+\r
+kuZLbGhT53kSUaB2tJHLFW1d8Ura+i1tTWZ3Q5vEs8vbpDltXC0a3LNok22y79PmHqBt8Vca\r
+zeHLkc2Sf43u0vZT1iHJwcjrHnShuEVbe30tbTqKxWtbVrTFtG41A15AG1+mB45oy3O4y9pG\r
+jX8hbZRTr6RtiekDtMmOevUxbf5ltNE0pbgK5t438DHdsTK5v25pm15GW3fN1iWMh7R1xZ62\r
+wSVKlLaN8zyteZPPLW3kckVbWBm5jsY5be0RbVOirc1oY7uRtlU8KafWtA0ZbWHJbu3v0NY9\r
+RNtwStu4NHy5C+pR2qaMtuFiQ9voZq0cadhSTZd+/nd+FMB+hV+I8pa2widH1Ld92dM28rYs\r
+7NGaNnWepTXXxBNtueEVbYNT2uYvo23e0lZndmWgYRPPMY0lTCvauDtrkg1qnkDbeE5bsaKt\r
+2dPGbpt4YXJcQAdHfZF3bb5Cd2gbeJkGR0IbKLXEgQIw1se0xXQqI23XHW3lbdp4s7RJu+y4\r
+F+2INp47kNFWPoO264q2PPa3advEk7oFlTb/hbSVt2njpONOqRPaLjdpE7epoyOnrSum3O2X\r
+6x5tlx1tfkNbcZ82upKGhC4bKAa3py0s0hfa5g1t6jyjLR2jtze8om1cagZegs/ReCFtPtHG\r
+0a4yu5G2VTzp67CmrRca+WNVPYu26ZS2qZiJtjiwckJbKsJWtJU2tPVFXBe7p41+hm/6AW2d\r
+plNfZvuAbGiTRubo/rKjbXa/29E2XlbOM9p6GVM9MryibeIdSLNoZLSxueVC/PNh2tgum9zG\r
+kz7zgxY+O9qW1/mAtr6UIMRUfB1tl3u0lVmesq1EW2jpVf4JukNbV/ApIzXT1mhs0s9E29ic\r
+0CY10OUHpeH4OG2K8D3aOukWelvaUuy7q5bt7SFt63juaeukw/d5tF28v03bRGsOo4stbf0t\r
+2pYIPGfo6g5tS8EWzjlo64y2VkIktMUvqVvRlqWT1ECd0ubyj1YZIvaHHW3e/SbODtrQps4z\r
+2tr4au8Nr2ib3W8jbRU/Wvn7tAXlsc9ou2Z2hbZtPMPXQWjLAz7I9LG2PBhLOKdNdETbHGlb\r
+JU+Y17SmrTqlrUn2I200KSrV6F6he7RdqRNnRVss2yoK5jFt7Sad/ivRlqBoGYrZ/bCnrf3u\r
+rGxj58FSzABOi2PDK9q8+7XGOJW4L6JtU7Z118wup8wunuFdYJNDTtsotHXFs2ibzmlzOW3t\r
+AW3aZqZANBva6nyfkVfoDm1LyoWuFqWtzmOjnQ23aQv12/Bu17Fvfsod3aEtRH7bSiDnfU6b\r
+nLHyQtquKRaHtLVb2lLsu2vM7vKItnU8w9dhTRtdHt3jtHX3aHPp2jFtY5n3uO9oGzLaZh4G\r
+jLTRiPFThq5u00YLgEOi1RS2DW0Uma48pC08GNMpvNvXA9pcxZuuu1/taet+oZBtaSPnQ06b\r
+LJ5ItOWG17S1v9QYc714RVupzx7R5m/SRnbp3j6efaRNN+Va0dZfjmjTIDxG23SHNn+XNs4B\r
+rbqsaAsEPGfo6jZtk/h1kzYKZuHPaevpS0KJHx6hglkd8R6dbk9b705pC84pV9r4Ztcc2i1t\r
+rdvS1rlz2rg8WWw9RBvnTF9kdiNt63jSIW8r2ujtmGQBnWw/saYt9sk8i7a/EG2DWrtNW62/\r
+SgC9EW0NERcSLdBW+awmydnFOU601Ym2kNKxFzyEtKc9P+O9+Msd2mq/H0vIaKv4AVkSFGlb\r
+GT6jTXL+Bm3VhrYUe6LtIlY2tE27eA5uXXomh4/Tdr1HWwhWGkvw+U2i7c/UjRiDsaVtdRYM\r
+0ZbaHcPFP2mN320jvEdkxdO0YtHUSGR47DqnLeZ32omIep8CPH9OJU+dHPEX8YA2Og9HygC/\r
+GpUn5xva+Lnc9+hyTVufaOOUzWkrvJC6HZX3EpM9bUOR2aXsG1P5Xusns1F6ctpmoW08pE2n\r
+fuxG5UVr2vi9vEFbm9PWH9HWpDTZ0EabfD5l6Oo2bbzb8jXSxk0t8ZeCJt1+W9rSLmuh6hG+\r
++9RLFKvRKReCyf6ANjoZj1Aet7QF55Qb0omhxdTK9+hyTdsQaRNEyYZkAkeoOaMtxb6LPWjD\r
+JbNL0YuL37N4RoDZntYQuL98OqRNP4qP0lb5fFTer28uBn5PtHHx3Rc72niwQNoCwdiQaOuK\r
+WFl5pW7TxmdHUExnbZjEuR8yp6wKv49FbFfLx0Npo3ZGqP3taRsUiuGANtrCiDJ43MxvI+ej\r
+y/oi5QO18j26XNM2ZrQ1MfxZizEeWLebL5vFfk/bGGnrd7RNStt8SNt8RNvofF4f2ZK/pU2O\r
+UmSkaw1wTtuP1R3aQkhbqV1c9Q2WDkGl8bW6Q1vBnnVXbgZzoSM3KbuEeaEtdmjGs5d5N5hQ\r
+NLoidn9es9rzbdqo2FBi2g1tMQWktTDlvt+lTdYL5LTxd63RmeJb2lLsE22j29LWXbbx3J29\r
+rF131S3amqw+Mt+nrdTsOqaty2kbLjva+EWQPtyQnl1G2zV+w16p27SRl0uW0yQaKc1i64Sy\r
+a8hou1LpQAkcZjeODE1oO5Shwh73DdRcCI74FLwD2mgfKsrgLp6Ge03O0ym7fk/byvCatrQP\r
+F23BJysllZw6q9IH66sczmIfrnPGKW1TpC0uo8hoiwtr6hhQH2nzR7QRmLE+siN/R1sr5YI/\r
+o42J50sHtHneAINpC98KtiVDa6lYfp1u00ZedgWf7i2p1SXaSq97LdH8VT6OOSTw7JQ2eoSO\r
+A3XXTS7M7i5tNNreFtrfuKKtSrtTyHuntK0Nn9FGrny+2pNen05p262qy2J/g7ZpT1vc5bl7\r
+CW1XDkJMxdu00Ujx+SoY8vo2beFrJEhR8Rxp42LNgDbyoqcNrujrROWT9irT+iPJPJ4tzWOq\r
+ledVqCPnZhPKtYY2bJYGRlcmR3LE1542z7RdfdxMlV5PdR7eNd1x3GeZuDe8pm1O2znwIbDa\r
+t+KltNPtr3jF8Jq2GPtwnSuLk+yUOCttoQxfx5Nybii8ehYrI61sNNoe0EYetS5LxXu0VQrL\r
+IW2yP6b0V7k9bSEQvby2vWPa+VE5WLb0r9cd2oLvQziJvaFv4xLrtFVDOBRdalxMm/OcREEl\r
+ueaZe2FJKJ2gLu90yAV1RFDMGW2hL4ibYaEC5ZwUijK7Jzpf/s261lD3SSXadoblWd1jLtHW\r
+Lo+0LsvjmaJX+DQbuYvTIPwq9kSbjJY1yS67vubxrMinwmfTsrR3IdLWHdAWCjwq8mMqrsIS\r
+adOLofXNtYKMNrmZ01ZxTJg2TjZ1XMw6KLpQJ+AxbY33zxm6ukmbtKtDIsuyMMlKeTaFlubm\r
+81+tJLmmk/cynSvmQl8kR3Jc8AFtHRc9ziXa6sw5EyTB3NKWG97Q1sYYdyn8kTYhVZ5Y53AW\r
++0TbkjnJrr4TWTwrcl4kkxRR/tnJlt39MW0cuJiKt2mLWwWf0tYk2ibZJCenrReoZzVWcfS0\r
+qy1WoF6jm7RN2l252kYtluhtYm+UEqHU/K40naSn1vFSI45YkRzdpi2t8pW0SM7blFLTjrbq\r
+Edr6RI/kcSRV82GVw1nsA2ntGW11Hs+KnF+SSfLuAdo0cTe7zpzRNqabh7QNTBszdESb7pA4\r
+q7Gao6ddf08ZurpDW0P/rUeXtttMTeEusae0XX2cru/j5PyYYvJOR9pqhaI9oK2nF42d5Ttq\r
+ifMukn5AW254Q1us7Pm4D1/M41ZJVQ7XOZzcE230W6Sti7Q1eTzJUX9JJjPvlLbhiDZN3M2O\r
+Wme0TeK1v0NbLfm6o20UqGc11nD09N14ytDVTRujdoBOglUbg8EBiOyNUuZXmt8afsfVJ04L\r
+yYWwDFsdMRTdAW3U4owvnKRFct4n7ve05YY3tKVtuLIMkjzulVQnWb3O4RT7RNtSWiS7Gsos\r
+nhVHJf/SJe8KXa5Z+x1tmrib3QLPaJvFa39Km0+0zbIlU07bLH/MYuwiV7VUe8r2lDdpY55p\r
+dytOqV6ykjUm9ni2NE9i1zhkZQdHuxrWtFEX1G3aOn3hJMmT8yGlfNqW0B8YPqUtq/pJHg8S\r
+oc1OqOpPin2gTdDZ0VbGL2VGWzMktLUX9jZt+jWLqXibtqxmcUgbHdukRygf0eblBSMn8bps\r
+VOKfNFB6kzYdpL16ieaYB8/Pib1RanWN5PfVazo1Gu8fXBkzNTliKPoD2kbN91Jpq3LnU8q8\r
+HW0rwxvasi9C5/TbJgHT3UQ3uzznVMs38Boz1V2T3RjKFM9KohK3KadQlZK8us1G7Xe0aeLG\r
+VLxDW++0VnOTtoZtV3vaOg77LG9ZKaHQvo+nDF3dpk2qM6VO1Fkdz0FZKzURnlFIydkqY2OK\r
+TEjrv7qL2Btdtq8Z19IPaJu4R1eSQNI+c57tN72lbWV4Q9u4LptrDp+kuUs7JstIQ57DKfY0\r
+bsxX22uyq6HM4llxVHjRbewcE8Yv+RrVDW0hJvELUezCsqMtReaYNupG1BkVR7QNHPZZqhKV\r
+RDk1hyr/at2krYu06fFmncsL1BgonXUTUrHVZM0+tDRKuqWNu7Vv0yYdYrMUGrnzLmbelra1\r
+4XPaErBxxqwMLQg12xyOsb9BWwjlnrZqyIq2x2iTxI2peIe2FJlD2qa7tM16EEqdjbjI5pne\r
+grbcXSW+r7qU21WX33xe2A46xP5STacjJk+oSAzbFJzdzTBuYv/GSnQ+pPG1OOTtgKeMHOz1\r
+jHbtg+q/hDboI8mQthG0fXYZ0jZbfoagb1GGtPl/GPoFfYuypA367AJtkJ1AG2Qn0AbZCbRB\r
+dgJtkJ1AG2Qn0AbZCbRBdgJtkJ1AG2Qn0AbZCbRBdgJtkJ1AG2Qn0AbZCbRBdgJtkJ1AG2Qn\r
+0AbZCbRBdgJtkJ1AG2Qn0AbZCbRBdgJtkJ1AG2Qn0AbZCbRBdgJtkJ1AG2Qn0AbZCbRBdgJt\r
+kJ1AG2Qn0AbZCbRBdgJtkJ1AG2Qn0AbZCbRBdgJtkJ1AG2Qn0AbZCbRBdgJtkJ1AG2Qn0AbZ\r
+CbRBdgJtkJ1AG2Qn0AbZCbRBdgJtkJ1AG2Qn0AbZCbRBdgJtkJ1AG2Qn0AbZCbRBdgJtkJ1A\r
+G2Qn0AbZCbRBdgJtkJ1AG2Qn0AbZCbRBdgJtkJ1AG2Qn0AbZCbRBdgJtkJ1AG2Qn0AbZCbRB\r
+dgJtkJ1AG2Qn0AbZCbRBdgJtkJ1AG2Qn0AbZCbRBdgJtkJ1AG2Qn0AbZCbRBdgJtkJ1AG2Qn\r
+0AbZCbRBdgJtkJ1AG2Qn0AbZCbRBdgJtkJ1AG2Qn0AbZCbRBdgJtkJ1AG2Qn0AbZCbRBdgJt\r
+kJ1AG2Qn0AbZCbRBdgJtkJ1AG2Qn0AbZCbRBdgJtkJ1AG2Qn0AbZCbRBdgJtkJ1AG2Qn0AbZ\r
+CbRBdgJtkJ1AG2Qn0AbZCbRBdgJtkJ1AG2Qn0AbZCbRBdgJtkJ1AG2Qn0AbZCbRBdgJtkJ1A\r
+G2Qn0AbZCbRBdgJtkJ1AG2Qn0AbZCbRBdgJtkJ1AG2Qn0AbZCbRBdgJtkJ1AG2Qn0AbZCbRB\r
+dgJtkJ1AG2Qn0AbZCbRBdgJtkJ1AG2Qn0AbZCbRBdgJtkJ1AG2Qn0AbZ6WPRNrnqawfhQINr\r
+vnYQvhHdpK0r+Ed5+nRzcHG60I/5DsjiLDPTVbefOA+F+jW465eZeK0kBm19dLNzh5e/QMNp\r
+TrwPPUJb+6a0/fcJbf9v+8B8Hgr1q3sz2v7P4dX/SEGgH8e0uZfT9h/Hlz80bT0DcZ6Fz6Dt\r
+f5/Q9p/bB8bzUKhf7ZvR9u9HF+c6BYH9r49cvZy2ZHitD03bwHfPK0PPoO1fJ7T9Yxea81Co\r
+X+7NaDu0O9UpCPTjkLbp5bQlw2t9Atrmt6WtOKZt3iVsdx4K8Wt+M9qmQ7tDnYJAPw5pG19O\r
+WzK8uf6RaRspEefzxHoCbdMJbeMuYU/tRdqmN6NtOLTb1SkI9OOQtuHltCXDG1MfmbaJkmk6\r
+b8A/gbbxhLZdwp7by2mrzhy9Tv0hbQmuO7Q1L/TuuLXxKWgbz92AthiEzYVMoC3qJhJcY3s/\r
+tI1P69ja6jW09S/vQf+UtHFrdLic328OLn4xbSu9gLZ478Z78Urdo+3sAj1bvNi7T0pbSOQb\r
+qfWt0Xb+XrxSr6Lt5Yh8TtpoFAG0gbYn6TYSNEJ6Pkz6zdH28m/Wg3oVbS/vlfmktIXsa89T\r
+C7TduXD+7E19TtpooBS0gbYn6TYS1By90YcF2u5cOH/2pj4pbe7mwBVou3fh/Nmb+py0hYFS\r
+0OZB25N0G4kwRHpjmBS03btw/uxNfVbaau6g73c9WdTx6xoZaOiyfCaM+jLSIfMy5IfA211X\r
+tMXxilHJo4SVyU50MdEWOrC4E2sSoyvaXBaeUC6nstkVeRCCWvaXoye+beJK3Y2RmNXNAEWa\r
+jzVemJJsqjP3jq9p20zg6iu9XMeobmjTJ9rmY9MW4jk6nX4aMjOtAAi/uKaVaSIhkUJaLAwR\r
+Rs6tXS5mwuWu7ikHZndh2pYnL2omjJO2jhN0dAGNgfmgi44Ht+fF+SX8FW50EopZvFicuLAS\r
+RsJzHRfPRqdZxFN/NAjyREWpQLFg31ZxDb4tppYnS0mQ7GYXnu4pXsG7ztWBksnFNB3Cvd6R\r
+iylcnehv6laqCcO+aimGfTWwDzTXfTH8w4UfyZ74m7uGFO7erAB/e9353C3JGYqdkVI7p62j\r
+ZHd/4xllA/3IaFuS/G8rl+FHE7K6paRbDOa0sZmFtpmzRmlrKef4YqQtoMQ4LTcoFCvaAl0S\r
+nmvIp95p9rTsiwRBwlbyg1evvq3iSr5VibbVTaKtpZDQP1cRQ2k6G0U9py0MdvADkTZms686\r
+cjVTtJfnfpvRJk/81RUhhc9XiXz7ukfb1Q8FpWCxps1RBrm/cjZwrmW0Ldnzu5XLkGTV4vCP\r
+jpJ3MfhTRhub6SoGJlxn2pgmvhhpC3NhuVAaJRQr2kIhIeG5hizstKwJbBYpCBK2i9foCbur\r
+uM4XAi3StroZaAsPVUTbEpMyMNSlKZ2OCz/hyNOD4ovS9juljaM3UdiEtpFp0yf+wrR9rWVl\r
+z9Ad2tor1ZA6SoSMtpACIeZ/4aRs6UdG25LEv1m5lB/dT5yqkp5KG5vpqBDR+suSrRPTxBcj\r
+bYvtqqcbvYRiRVv4Tkp4riHYyi/dvqQgsOeU2R3dmBicVVzJtyLRtroZaJsUeApxYIiZTlGP\r
+tDX0oDygtP2Wy/JAXYg4RzWkjmPasif+7MolhW/M2//2dYe2rqRhUkcZlNE2cc65P1Pqz5xr\r
+GW1Len23cjlJwfIHeVGd+3VGG5vpqCDgd5doG5kmvhhpC4UHF14dG13TttR8NDy/ceGDrl+2\r
+ga9qEOQB+kgxQwJOHlfy7ZJoW90MtI0ahODmsjA0O60MeL6X0zYQuMGW0vaD0PZbRr2nHx1/\r
+GUb2Sp/4mWib3mwOn4Hu0VYsrUemqc5pk3LC/UypP3EmZLQt+feLlUstWH7PWb3895cZbWym\r
+o3TOaBucVmNy2hbbJdPWSihWtLXuquH59fLjj5Gtnq9qELzwx4AsN7ikXMeVfEu0rW8G2oZI\r
+W3h8YUjLSzF/UdqoaTxQUUc10ppp+15o+yEVrFelbWDa9Ik/EW2x0f4edYe2pfnfVpyCVU6b\r
+lBPuT/yucyZktLlIm7jUguVHsjRvaPuT0tY6yddRigXH6C4XV7S1Wq07oq3S8ATafnI6fbwT\r
+2n6MV/qMtlpydR3X+ULhVtrWNzv+pHMQyHxbx/JSzW9p6yROtdLGbdLv+TXjqBJtDdOWnvgT\r
+tUmHN5sxaqA7YV8apI7r7qEvYUMb1dwpWzkTEm2UfyuXWrCwpUk4UdrYjNBGyU+0dWyFL0ba\r
+wl8u0ua2tEmBE8Lzq+XHD2vaag1CUM/uhCHO1XVcOSLNkN6B7KbSRkGgUGpa1MnDJqetv7Kz\r
+RJtEl8xIlYXiXQltS5rqE45o2/d8viPdo8156uC9hn6FjLY+vLOBtiW5QwpXYQeORNvkytGt\r
+XHYh2cOPkvvArsOKNjKz0BbKsERbG3LUy0WlbfnR/mL5F5gmJ1va5krD44rJfV9oF3AbQlJr\r
+EIK6wFHF0Qukh87edVxD/1qX05bfDFB0Fw7ikhaXmUvIeoi0NZ2rlbblG8G0Lf9vEm3VvziV\r
+lmCWFKOctppp4ydcE2h7z91t92gb3SzRDpX2jLaCqqvU8xXq9kt+lyvaKu9WLikfa9qAhWir\r
+Z+1YozKKzATaKn15qbFf0PvNF2Nr+EI9qj37QvfjKpiCnvkPDQ/V9q5xMKOkJyQIQW3okAhU\r
+1uEHfbo3cZ2ptGt0PGCTEAsyHdekwv9LMr2wpzstUZwrfVZoozjVibaG9jvpA+0ljY10/J2l\r
+70EdFrLqExf/PwN777i77R5tk/srRZtyJqONmg516NUPLfKQQCvaRnoXc5ctt927cIMrHzlt\r
+YqYitDPawje3louRtvB4Ga7oKO6attBWlfAEf8M/oa2ifxKEoHCjvVJo2iu/BZu4zo5fll4/\r
+vauEWKCgWi0HoeKis/AbvOXZMFISYkjvWqLNi2F6cqIPBdNGxeTiJj5RUKK85+62+7T9tLgI\r
+xc3yL6etoooIVZ4qSuHlX06bpw9M5vJKb3fgoCNUdIhSyiMvtMXFBdq1FGiji5E2+hAHLybH\r
+HQtr2q4+hoe/4DrVnXpeYhCCQnGzvA09XZmpBtes4xp8mzPa1gnREjUahJrxLskmeXilf0rb\r
+lWmrKF5Km9TCAnV9QW/pFA0rbfIE9Z+/6+62e7TN7g8Xzr1hR1tL1azwIyREn9M2XLy+tuqy\r
+pvQOlvqSRrq7jDY2w7RN0roouYuq1YuRtsrzUChxuaet8jE8TrJRei8a9qXQKxqonq7M1Dpt\r
+1nEV4CMx64RgKOhlaiZqm9f8wSw1pNQ9ntNG3WUZbVILC9QNRYyR0hYKxvgEjQ2+6+62u3tT\r
+uu+lKFjKkgPauMSihLjktBVxgyR2OW9oK72+1loeHdA2ndNWK23+kDYJjxbLibZuR9uS5V3J\r
++dgEa+u4bmlbJ8SaNp4yQ3204mFNT2i5SCMIHKfrCW3Dirbux4W2Kj5BtL3r7rb7tFHH4vWI\r
+Nq7UB4bCjx1tY+6SsrrngYnlZsiOIaONzTBtc0ZbeP4qFyNtdaQtZNURbRqey5o2Do0EwfOz\r
+RFvIx4J8c/U6rrN8frWmfz2irVXaqCpbq3nCJKMtFOA1pcoBbQXVFuhBrtWFkP4Y3sD4BNH2\r
+rrvb7tLWyldOczej7V8Rk5DCY05bSNkpd0mPDZG2kL/jGW0VXQ+00Yj5VS5mtM00pTjmzZq2\r
+2sfwcIEhcxCF3Yy2+AqE0DBtbb2O65a2dUIIbf8I3HJBS21JmWdHBvsVbeKsS7RJC1NpK71a\r
+TbTpE1WIybvubrtLW6e0LVmzoc3Hik/4saTtKW3yx1BKMbKjrc5oE4VBmkSbz+YDNEobbSh4\r
+RtvENCba/P/y7Ms1lm1h/8ElqCF6Y0G+MW0prlrZy2jLEkJo8xlt84a2UJBL70kRnQU/6zVt\r
+JSVIv6bt5yW0dXyiCjF5191tD9AmExY2tEmrq2bami1t10QbuaRiKqOtOqOtUY8H7kQl2uji\r
+AW0ShD1tzRFtEpqMtiCljTn4xyauW9rqHW0Vh6Lhz3qtE4tinLe0Ff4WbVcfG6RLdfjnUAbG\r
+J6oQk3fd3XaXtl6qyVvapOJTRxxWtIUcXbmklB8LzWr6Iq1p66/SPyBS2mKj/3Ha5i+gLdK/\r
+juuGtk1CtLX23LmGTLZMG0dtXNNGc9vY59u0ddRiDcMOfw60DSva3nV3213aBv0y+TVtXN5w\r
+NXuWZQPHtJHLSFtFP7a0NZG2iEFOm9SyPP99YUsVAAAexElEQVRg0k5pYxg1Z4dyR1ulhQ9J\r
+cj5eW8dVmxYZbX5NG/t8StuYaLtE2oZEm8BDH4ND2pr8ieG79Dq+R72Atno1Tko5uKKtOaSN\r
+XY6P0ZZt9h6mO0g688Ws+pbRNp/T1ihpGW3tjrbukLb6Fm11RpvsmR9pm7a0XY5oK05o69a0\r
+Bcaa/Inhl++6u+0ubaPOut/SxteaWI1f01ZtXNLkCZdoq09pUw5ocg3PjHCyOoWub2jrD2nz\r
+J7SFQYg1bWEsnHMw6xU5p22dEESb4xvSnKT5JDwoR54k2qRvqPS3aHM8HaWl4XgX/jX5E4P7\r
+2GUb90I1fkObTIq4RZvPXR7QNh/R1sbCLaONLx7SNrqX0EZ7jq9pc6+krefZSwJFd4s2B9q+\r
+nDa9cZ829yLaBI01beUxbaN7CW2z29HmnkAbj2i8BW3j8s9vaPvQrYREW5vTNvIcxYw2f0Ib\r
+uxwlE+7R1rk4zVppK+TiEW3unDa/p63b0daf0Nbeoq3NaZNlOxvanIYmVv3pgZHHkjUFzmij\r
+vt+xnC9jIW0vfSKuj3ineoS2eU/bJFNNG0/jxmTpmDZ2+ShtfZz4mtHGFw9oG49pm45pm92O\r
+Nrejbb5F2zYhWp4Yfj2mbdjSFtqy/cO0FUJbD9pkZdEDtLHLR2kbnH4rMtr44gFt/YtoG3e0\r
+TUJbIx76l9Mma8keoC30nJQvpu2ype1DjyWc0CaL4h6gjV0+SpuUmX5FG188oK19EW39jrbh\r
+9bR5fe0eoK0eXkDbtJA27Wn7/kOPk57RFrKueYg2cvkobWntb0YbXzygzb2Itm5HW/8E2jp5\r
+7Z5P2/K/y7Sl7Q+fkrZJ8vk+beTyYdp6Xfub00YX97TRR/px2kJJ+KcVbSFzf3xdvY1K7eYh\r
+2tpah6oeou1CtBUb2v5yL8O+aX1hD4jnfT0e6AFhl1otv0vb5HRlYJn6yejinjbaSuHxHpBQ\r
+jq1HrpaQFf3rekC8l9dOaUtlz6YHxLdVf73XA1KJU6YtfEs3PSC3dtP79vXFtI20mql5gLbg\r
+8mHaZC+kNW10cU8bB+FR2mj7oDVttJT9tbQN/No9m7aFNOoDWdN2+aS0+YdpCy713Iz7tI0Z\r
+bbH9NZ7QFjphHqftsh2VD8Xoq2mb17SlBN3S1lWh0X2btvjRDXs5lXPo313RVtzal/bb15fT\r
+NjxM2/AS2milrt/QFi7uaRtoDPdR2miV54q2sKDq9bTRpKyHaLu+iDZ3RFt5a4f3b1+P0Vb7\r
+PW1eWnN35oCwyy1t1cmofLggmb+ibZR5imva4uTsLW3NIW3Njrb6bMbRw3NAPEO7ngMiYV7P\r
+AVl+BtruzwHx0rUy8tjVag7I56Gt2dDWbWg7nt/GLl9A23xE23xEGy0hOafNr2mj1SQr2og/\r
+oW3ihsw/Xzq/Lai9RVuRERVGugp/Y37bmrYrjcuv5reVt85K+fb1MG3zjrZ+RdtupvjK5bSm\r
+jVqfJ7T5SFuaqeH9IW2lP6dt2tEmq0ly2jgWzD7fr9ZxPaBt3tHWXYVAnU3J2szdXXxS2vqb\r
+sym90PbnK/3LnxjKW6eOfft6iLaQHZLIutqzyWjLV8GMcRXMnLvUV15pu57Q9m8+p00Q5YuP\r
+05aH58W0reO6oW2TEGEOiFfaeBFNtrJiswpmCUbb+HwVTLenTVAKd1uhLX/ik9IW8nO4TdvK\r
+Je9xXOe0jQe0SZaF60rbkPKR3G9oO5q7e4O2dkdbJ6tgmLb2hbTNmgKrNVdTFc2vaHMP0fbP\r
+RFtYB7Oh7QvO1v129DBtsozylDZZDXlKW+PjYquHaeOy58tok/A8Qlt3sp70mLYsIQ5pqzVq\r
+m/WkS/SVtu56RlvBnjBtYdVVtXpiKG+dhP3t6zHarlTJDvGWFfDaQGo4rStPa9ML+rFeK88u\r
+ufkXaduulW+EtmlFmyw2n27RNh3SJuHZ0+Z2tLW67HCzVp7juqVtnRBtHVMgrZWvNGqbtfKL\r
+p05SpeW1yO2ONj3Fjmj7saJ/+er6T0Gbbn8R1p4LQ1VebwuZILT1cdeZMXepnQ1KW3FCW2wB\r
+J9qucvGYtvGINg3PjrZ5T5vTBfy8D0i9juuGtk1CBNpqqbdN1DwR2lIHS05b2Bs27upR+Wz/\r
+ozVtNZd8gTZX+XznEC3w36seo43hCBFV2q4r2koquGTfjenQZcUfrYqyWnfo8Dvamow23hbh\r
+KhePaRsOaZPw7GibdrSF7eAO9zhyh7StE6Kt494Uje5PQvEV2qpV+TUxbbXXHYv2tI1SQCpt\r
+nRjV8u5T0Kbbljkf98m6xkXy/TXulxaGhuJugRuXV/5oVUyb86sdtcRMpUuTvdBWhSyRi8f9\r
+bUdrrvL95Nb9baPb9reFNVeH+7dxXDe0bRJiAYC+iJXn3QJpR60yre1eEzW5CyM4M23zEW0N\r
+I1t72eiy9vmOb1rgv1c9RltI2YIiqjtOFtKpyZhc6IsUMsHJTqj1xmXYWTFlNW0eekhbpckZ\r
+EKE5lHrxgLaLp70n97RxeLa0hTdgTVvN21w6v9ubUuK6pW2dEC3D5QJtxFUA5OLjAvrV3pRK\r
+G+802ZWE/oa2iZBtuDcu0pZ2s9QC/73qIdpku9l8N13eu1QwkX1uZTddSrXrxmXr8s/YGBgS\r
+2i4ZbXTSkfTTFYRu2LiWL2bbgcRxUj8fzabM9wHOR67CG7AeuapoV52jfXclrpE25meTEEuh\r
+JtuXBtoKKY7I76D1vrue1/zLLrrdhQaad7SVtCc2VXbZmM936v0UtI1hG+7womY7hYeNuxWT\r
+IWwRHncKpx3si41L2ly70qxeLA1Km1vR5rRMpM1SaVNuuSjJvKKtGg5pk/BsaSsnt6GtmHmJ\r
+Tsl7ijfERx7XSFshgV0lROhPCfEi2lone4rTfuZBXdiPvN7QFnxZXHfhx462mTZYl8ouGWvi\r
+E0IbbRlbfmFuf209SJusQwlzZenyoHO0hbYwr5FmN/4ins6xdikLViSrZ3bpyfYlo611ce6u\r
+npfQyMVZd7DPaDueKZ6f35DTtp0pzochlP7ovASJa6TNydd4lRCtnJfQeOLHuT/V6neQBH9L\r
+G80XDc99v6NNzxsJbdGBvvxNfEJpa68fnDbJDjoE74g2WWkXnH13Qpsc0vEy2npJ5xPaxjPa\r
+JDy72ZQb2rzQdnQWjMR1Q9smIY5oE7+DJPhKFG0vrCe7hHu/39PWOjkpM6NNnvg0tEl20Fxr\r
+ydeYyCGh5Fyp4Ow38ZyrYuVy5JTXrGYu+X5OW+fiKhhZ2nfxcnFPGx3YJg1CtiW0ZedurWeK\r
+u5/LnLaQt7/n0ATnOXoS1w1tm4Ro5dgZ8jXw09XqN0eBwVrRxkzTvT/vaeucHODFtA0uPaG0\r
+de94rPQh2iQ7aIsEJ5c1kUNCyZl5PtHWbV3KUX6a1S255Ps5bb1L587yGX6F14uHtBVHtGVn\r
+Cm5o63e0/ZlD4/i9uG7iqrTJycibhOj4SC1u54Qz/Lo6poX3MfhKGzVV5US+8Zi2nsPQ5bRl\r
+p/6FmIQRmY9NW+f0HM9K+yGy1ctezgMNP/4YT8O9rlzqUbQVZ/WSqr9T2lxGW1q9HMo8WV06\r
+KAa1X9FG+3p0B7Rl56VmtIXydljRtsQnzPw/Op9U4ppoi09kCdHxcYHs6/JbsdCmJ+xK1Muc\r
+tivHN0QmnLg07GkbNAzUG8O0ZSeafg7aev6+LT9qeaDNdmbwemL28uNvTNsYudSNZJwer0xZ\r
+PRCXQfOKNl44yB7z4WycMzSqsKWNVnMd0tbFs6Az2pbQl2vawtJzYb9kOJpNXJW2UWhbJ0RP\r
+HT30JlA9ouxqhloTl9BZ0yYnKYc1OQe08TY6ZJhou6QnlLbl2genbeQSJyxGkQc6SkilTXZ6\r
+Wn7MTNucuGSX8kOzeiIuOQQ5bXP8DM1yVmTt9WK3o22x2ciwET0TaePwbGgLF9M0FI0Wj7BT\r
+9Fo9Vz7FVWmbXHoiJUTo6mhl15nw+DXQ1qd9iDqnCcR/0i/sS9is/YA2ieogtPFkEn4i0uY+\r
+Om2zfsUu+gAvjdLEnLhOteSJ0BY6yFYuPfe/xax2l1mn8+a0LX9pwdDSh+XCTmi29I42arce\r
+0cbh2dAWvtnjirbQ8JA8Dk9xU2AVV6VNThbcJMRE/c5UY22ogyzQNmkjQYOfaKs8+VLwj+qA\r
+Nt7DnQxPkTZ+QmkbPzBtIund5PQitasYS/x5slDQUG5dtqsJ9TELNlNo0sk6vOqgShcPRghd\r
+tb2yDs+9i/nhGF77j/dxzZ5d36TyXWMSZ8Mnb9bh44fEl2kV7SiJ5OrBdz18sNKDtEFPUCj5\r
+PrdAm51Syf9ZBdrsBNpAm53a5muH4GsLtNnpPc+DfI5Am51AG2gz04y0RgqYCbSBNjud9Od+\r
+JoE2M42gDbSZKdsg7LMKtFnpf7zvQ7qfItBmpc6915kbzxNosxJoA2126tz1vqMPLtBmpe50\r
+Mt7nEWizEmgDbXbqPswM3C8XaLNSh6QGbWbqMJQA2sw0oEkK2iBDgTbITqANshNog+wE2iA7\r
+gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNo\r
+g+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbI\r
+TqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE\r
+2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqAN\r
+shNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7\r
+gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNo\r
+g+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbI\r
+TqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE\r
+2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqAN\r
+shNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7\r
+gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNo\r
+g+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbI\r
+TqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE\r
+2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqAN\r
+shNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7\r
+gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNo\r
+g+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbI\r
+TqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE\r
+2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqAN\r
+shNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7\r
+gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNo\r
+g+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbI\r
+TqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE\r
+2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqAN\r
+shNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7\r
+gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNo\r
+g+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbI\r
+TqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE\r
+2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqAN\r
+shNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7gTbITqANshNog+wE2iA7\r
+gTbITqANshNog+wE2iA7/f/t0rEAAAAAwCB/61nsKoZs42MbH9v42MbHNj628bGNj218bONj\r
+Gx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2PrbxsY2PbXxs42MbH9v42MbHNj628bGNj218\r
+bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2PrbxsY2PbXxs42MbH9v42MbHNj628bGN\r
+j218bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2PrbxsY2PbXxs42MbH9v42MbHNj62\r
+8bGNj218bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2PrbxsY2PbXxs42MbH9v42MbH\r
+Nj628bGNj218bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2PrbxsY2PbXxs42MbH9v4\r
+2MbHNj628bGNj218bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2PrbxsY2PbXxs42Mb\r
+H9v42MbHNj628bGNj218bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2PrbxsY2PbXxs\r
+42MbH9v42MbHNj628bGNj218bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2PrbxsY2P\r
+bXxs42MbH9v42MbHNj628bGNj218bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2Prbx\r
+sY2PbXxs42MbH9v42MbHNj628bGNj218bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2\r
+PrbxsY2PbXxs42MbH9v42MbHNj628bGNj218bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jY\r
+xsc2PrbxsY2PbXxs42MbH9v42MbHNj628bGNj218bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf\r
+2/jYxsc2PrbxsY2PbXxs42MbH9v42MbHNj628bGNj218bONjGx/b+NjGxzY+tvGxjY9tfGzj\r
+Yxsf2/jYxsc2PrbxsY2PbXxs42MbH9v42MbHNj628bGNj218bONjGx/b+NjGxzY+tvGxjY9t\r
+fGzjYxsf2/jYxsc2PrbxsY2PbXxs42MbH9v42MbHNj628bGNj218bONjGx/b+NjGxzY+tvGx\r
+jY9tfGzjYxsf2/jYxsc2PrbxsY2PbXxs42MbH9v42MbHNj628bGNj218bONjGx/b+NjGxzY+\r
+tvGxjY9tfGzjYxsf2/jYxsc2PrbxsY2PbXxs42MbH9v42MbHNj628bGNj218bONjGx/b+NjG\r
+xzY+tvGxjY9tfGzjYxsf2/jYxsc2PrbxsY2PbXxs42MbH9v42MbHNj628bGNj218bONjGx/b\r
++NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2PrbxsY2PbXxs42MbH9v42MbHNj628bGNj218bONj\r
+Gx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2PrbxsY2PbXxs42MbH9v42MbHNj628bGNj218\r
+bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2PrbxsY2PbXxs42MbH9v42MbHNj628bGN\r
+j218bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2PrbxsY2PbXxs42MbH9v42MbHNj62\r
+8bGNj218bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2PrbxsY2PbXxs42MbH9v42MbH\r
+Nj628bGNj218bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2PrbxsY2PbXxs42MbH9v4\r
+2MbHNj628bGNj218bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2PrbxsY2PbXxs42Mb\r
+H9v42MbHNj628bGNj218bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2PrbxsY2PbXxs\r
+42MbH9v42MbHNj628bGNj218bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2PrbxsY2P\r
+bXxs42MbH9v42MbHNj628bGNj218bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2Prbx\r
+sY2PbXxs42MbH9v42MbHNj628bGNj218bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2\r
+PrbxsY2PbXxs42MbH9v42MbHNj628bGNj218bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jY\r
+xsc2PrbxsY2PbXxs42MbH9v42MbHNj628bGNj218bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf\r
+2/jYxsc2PrbxsY2PbXxs42MbH9v42MbHNj628bGNj218bONjGx/b+NjGxzY+tvGxjY9tfGzj\r
+Yxsf2/jYxsc2PrbxsY2PbXxs42MbH9v42MbHNj628bGNj218bONjGx/b+NjGxzY+tvGxjY9t\r
+fGzjYxsf2/jYxsc2PrbxsY2PbXxs42MbH9v42MbHNj628bGNj218bONjGx/b+NjGxzY+tvGx\r
+jY9tfGzjYxsf2/jYxsc2PrbxsY2PbXxs42MbH9v42MbHNj628bGNj218bONjGx/b+NjGxzY+\r
+tvGxjY9tfGzjYxsf2/jYxsc2PrbxsY2PbXxs42MbH9v42MbHNj628bGNj218bONjGx/b+NjG\r
+xzY+tvGxjY9tfGzjYxsf2/jYxsc2PrbxsY2PbXxs42MbH9v42MbHNj628bGNj218bONjGx/b\r
++NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2PrbxsY2PbXxs42MbH9v42MbHNj628bGNj218bONj\r
+Gx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2PrbxsY2PbXxs42MbH9v42MbHNj628bGNj218\r
+bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2PrbxsY2PbXxs42MbH9v42MbHNj628bGN\r
+j218bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2PrbxsY2PbXxs42MbH9v42MbHNj62\r
+8bGNj218bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2PrbxsY2PbXxs42MbH9v42MbH\r
+Nj628bGNj218bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2PrbxsY2PbXxs42MbH9v4\r
+2MbHNj628bGNj218bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2PrbxsY2PbXxs42Mb\r
+H9v42MbHNj628bGNj218bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2PrbxsY2PbXxs\r
+42MbH9v42MbHNj628bGNj218bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2PrbxsY2P\r
+bXxs42MbH9v42MbHNj628bGNj218bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2Prbx\r
+sY2PbXxs42MbH9v42MbHNj628bGNj218bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jYxsc2\r
+PrbxsY2PbXxs42MbH9v42MbHNj628bGNj218bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf2/jY\r
+xsc2PrbxsY2PbXxs42MbH9v42MbHNj628bGNj218bONjGx/b+NjGxzY+tvGxjY9tfGzjYxsf\r
+2/jYxsc2PrbxsY2PbXxs42MbH9v42MbHNj628bGNj218bONjGx/b+NjGxzY+tvGxjY9tfGzj\r
+Yxsf2/jYxsc2PrbxsY2PbXxs42MbH9v42MbHNj628bGNj218bONjGx/b+NjGxzY+tvGxjY9t\r
+fGzjYxsf2/jYxsc2PrbxsY2PbXxs42MbnwBfxfJ7w78zrwAAAABJRU5ErkJggg==\r
+--------------4AF91C32E325771A2459DE43--\r
diff --git a/upstream/t/data/spam/freemail1 b/upstream/t/data/spam/freemail1
new file mode 100644 (file)
index 0000000..2e2ba75
--- /dev/null
@@ -0,0 +1,15 @@
+Return-Path: <test1@gmail.com>
+Received: from google-public-dns-a.google.com (google-public-dns-a.google.com [8.8.8.8])
+       by in.example.com (Postfix) with ESMTPS
+       for <test@example.com>; Wed, 18 Jul 2018 21:12:22 +0200 (CEST)
+Received: by google-public-dns-a.google.com with SMTP id f21-v6so3811271wmc.5
+        for <test@example.com>; Wed, 18 Jul 2018 12:12:22 -0700 (PDT)
+From: <test1@gmail.com>
+To: test@example.com
+Reply-To: "Spammer" <another1@gmail.com>
+Subject: Freemail test
+Date: Wed, 18 Jul 2018 12:12:00 -0700 (PDT)
+MIME-Version: 1.0
+Message-Id: <20011206235802.4FD6F1143D6@gmail.com>
+
+Freemail test
diff --git a/upstream/t/data/spam/freemail2 b/upstream/t/data/spam/freemail2
new file mode 100644 (file)
index 0000000..f2b0289
--- /dev/null
@@ -0,0 +1,15 @@
+Return-Path: <test@gmail.com>
+Received: from google-public-dns-a.google.com (google-public-dns-a.google.com [8.8.8.8])
+       by in.example.com (Postfix) with ESMTPS
+       for <test@example.com>; Wed, 18 Jul 2018 21:12:22 +0200 (CEST)
+Received: by google-public-dns-a.google.com with SMTP id f21-v6so3811271wmc.5
+        for <test@example.com>; Wed, 18 Jul 2018 12:12:22 -0700 (PDT)
+From: <test@gmail.com>
+To: test@example.com
+Reply-To: innocent@example.com, "Spammer" <another1@gmail.com>
+Subject: Freemail test
+Date: Wed, 18 Jul 2018 12:12:00 -0700 (PDT)
+MIME-Version: 1.0
+Message-Id: <20011206235802.4FD6F1143D6@gmail.com>
+
+Freemail test with multiple Reply-To's
diff --git a/upstream/t/data/spam/freemail3 b/upstream/t/data/spam/freemail3
new file mode 100644 (file)
index 0000000..2314eb9
--- /dev/null
@@ -0,0 +1,15 @@
+Return-Path: <test@gmail.com>
+Received: from google-public-dns-a.google.com (google-public-dns-a.google.com [8.8.8.8])
+       by in.example.com (Postfix) with ESMTPS
+       for <test@example.com>; Wed, 18 Jul 2018 21:12:22 +0200 (CEST)
+Received: by google-public-dns-a.google.com with SMTP id f21-v6so3811271wmc.5
+        for <test@example.com>; Wed, 18 Jul 2018 12:12:22 -0700 (PDT)
+From: <test@gmail.com>
+To: test@example.com
+Subject: Freemail test
+Date: Wed, 18 Jul 2018 12:12:00 -0700 (PDT)
+MIME-Version: 1.0
+Message-Id: <20011206235802.4FD6F1143D6@gmail.com>
+
+Freemail test with body email
+another1@gmail.com
diff --git a/upstream/t/data/spam/fromnamespoof/spoof1 b/upstream/t/data/spam/fromnamespoof/spoof1
new file mode 100644 (file)
index 0000000..5efb24b
--- /dev/null
@@ -0,0 +1,15 @@
+Return-Path: <test1@gmail.com>
+Received: from google-public-dns-a.google.com (google-public-dns-a.google.com [8.8.8.8])
+       by in.example.com (Postfix) with ESMTPS
+       for <test@example.com>; Wed, 18 Jul 2018 21:12:22 +0200 (CEST)
+Received: by google-public-dns-a.google.com with SMTP id f21-v6so3811271wmc.5
+        for <test@example.com>; Wed, 18 Jul 2018 12:12:22 -0700 (PDT)
+From: "test@example.com" <test1@gmail.com>
+To: test@example.com
+Reply-To: test@example.com
+Subject: Freemail test
+Date: Wed, 18 Jul 2018 12:12:00 -0700 (PDT)
+MIME-Version: 1.0
+Message-Id: <20011206235802.4FD6F1143D6@gmail.com>
+
+Test
diff --git a/upstream/t/data/spam/gtubedcc_crlf.eml b/upstream/t/data/spam/gtubedcc_crlf.eml
new file mode 100644 (file)
index 0000000..18075d6
--- /dev/null
@@ -0,0 +1,32 @@
+Received: from [192.168.1.1]\r
+ by mail.example.com with SMTP id gQQvHEt9CmmU\r
+ for <recipient@example.com>; Mon, 07 Oct 2002 09:00:01 +0000\r
+Message-ID: <GTUBE1.1010101@example.com>\r
+Date: Mon, 07 Oct 2002 09:00:00 +0000\r
+From: Sender <sender@example.com>\r
+MIME-Version: 1.0\r
+To: Recipient <recipient@example.com>\r
+Subject: GTUBE\r
+Content-Type: text/plain; charset=us-ascii; format=flowed\r
+Content-Transfer-Encoding: 7bit\r
+\r
+XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X\r
+\r
+Generic\r
+Test for\r
+Unsolicited\r
+Bulk\r
+Email\r
+\r
+Repeated to ensure that DCC fuzzy match sees this as unique\r
+\r
+XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X\r
+\r
+Generic\r
+Test for\r
+Unsolicited\r
+Bulk\r
+Email\r
+\r
+Repeated to ensure that DCC fuzzy match sees this as unique\r
+\r
diff --git a/upstream/t/data/spam/hashbl b/upstream/t/data/spam/hashbl
new file mode 100644 (file)
index 0000000..951022b
--- /dev/null
@@ -0,0 +1,288 @@
+Return-Path: <sb55sb55@yahoo.com>
+Delivered-To: jm@netnoteinc.com
+Received: from webnote.net (mail.webnote.net [193.120.211.219])
+       by mail.netnoteinc.com (Postfix) with ESMTP id 09C18114095
+       for <jm7@netnoteinc.com>; Mon, 19 Feb 2001 13:57:29 +0000 (GMT)
+Received: from netsvr.Internet (USR-157-050.dr.cgocable.ca [24.226.157.50] (may be forged))
+       by webnote.net (8.9.3/8.9.3) with ESMTP id IAA29903
+       for <jm7@netnoteinc.com>; Sun, 18 Feb 2001 08:28:16 GMT
+From: sb55sb123456789@yahoo.com
+Received: from R00UqS18S (max1-45.losangeles.corecomm.net [216.214.106.173]) by netsvr.Internet with SMTP (Microsoft Exchange Internet Mail Service Version 5.5.2653.13)
+       id 1429NTL5; Sun, 18 Feb 2001 03:26:12 -0500
+DATE: 18 Feb 01 12:29:13 AM
+Message-ID: <9PS291LhupY>
+Subject: There yours for FREE!
+X-Original-Sender: hustl.er@gmail.com
+X-Some-ID: 1234567890
+To: undisclosed-recipients:;
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="ETDFsshmzrOmOVdZ"
+Content-Disposition: inline
+
+--ETDFsshmzrOmOVdZ
+Content-Type: text/plain; charset=utf-8
+Content-Disposition: inline
+
+Hello
+
+This is an innocent@email.com
+
+This is a google email: spamm.er@gmail.com
+
+hashbl_email_alias_domain: spamm.er2@aliasdomain.com
+
+Some uris spammer.com https://spammer2.com/
+
+btc 1JaSs2bTZYVbj6jaqZ5Mjfs8gSLY9vYCrK
+
+uridnsbl_skip_domain  https://sub.trusted.com/  email@trusted.com
+
+email host/domain userpart@host.domain.com
+
+telephone: +1(123)123-4567
+
+--ETDFsshmzrOmOVdZ
+Content-Type: application/octet-stream
+Content-Disposition: attachment; filename="macro.xlsm"
+Content-Transfer-Encoding: base64
+
+UEsDBBQABgAIAAAAIQDxGuVmhQEAAE8FAAATANkBW0NvbnRlbnRfVHlwZXNdLnhtbCCi1QEo
+oAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzFTLbsIwELxX6j9EvlbYQKWq
+qhI4tPTYcqAfYJwNuDi25TU0/H034SEhpSmUSy+J8tiZ2ZlJ0nFVmmQDAbWzGRvwPkvAKpdr
+u8jYx+y198gSjNLm0jgLGdsCsvHo9iadbT1gQtMWM7aM0T8JgWoJpUTuPFh6UrhQykiXYSG8
+VCu5ADHs9x+EcjaCjb1YY7BR+gKFXJuYTCq6vVMy15Ylz7v3aqqMSe+NVjKSULGxOS+x54pC
+K+CbuZwG9wkqMjFK32mboHNIpjLEN1nSqKiMiCQNdscBJ3G/oJ+usGfKnVqXJJw3YHc1ys+E
+GLcG8Goq9AFkjkuAWBq+Az0wt/gWwOBlq+2T4TTZmItL7bGDodu7bk++XFjNnVud4QrFC5UC
+WrlenZdSBTexcm6AopfaHhS2xU05USE8CurMGVTdWUNdyhzynidICFHD0Z02bqpavWWjGkVz
+Gl6t4bQER/wuD1p03P8THdd/f3/w49gJ5QJcbsThK6mnW5ogmt/h6BsAAP//AwBQSwMEFAAG
+AAgAAAAhALVVMCP1AAAATAIAAAsAzgFfcmVscy8ucmVscyCiygEooAACAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAIySz07DMAzG70i8Q+T76m5ICKGlu0xIuyFUHsAk7h+1jaMkQPf2hAOCSmPb
+0fbnzz9b3u7maVQfHGIvTsO6KEGxM2J712p4rZ9WD6BiImdpFMcajhxhV93ebF94pJSbYtf7
+qLKLixq6lPwjYjQdTxQL8exypZEwUcphaNGTGahl3JTlPYa/HlAtPNXBaggHeweqPvo8+bK3
+NE1veC/mfWKXToxAnhM7y3blQ2YLqc/bqJpCy0mDFfOc0xHJ+yJjA54m2lxP9P+2OHEiS4nQ
+SODzPN+Kc0Dr64Eun2ip+L3OPOKnhOFNZPhhwcUPVF8AAAD//wMAUEsDBBQABgAIAAAAIQCc
+fziWGAEAAMEDAAAaAAgBeGwvX3JlbHMvd29ya2Jvb2sueG1sLnJlbHMgogQBKKAAAQAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC8U0FqwzAQvBf6B6F7LdtpQymR
+cymF3EpJH6DIa1uNrTVaNa1/X+GAk4DtXEougt1FMyPN7Gr929TsAI4MWsmTKOYMrMbc2FLy
+z+3bwzNn5JXNVY0WJO+A+Dq7v1t9QK18uESVaYkFFEuSV963L0KQrqBRFGELNkwKdI3yoXSl
+aJXeqxJEGsdL4c4xeHaByTa55G6TLzjbdm1gvo6NRWE0vKL+bsD6EQrxg25PFYAPoMqV4CUf
+WiT6ySIKirkYF5PeWEw6Jya5sZhkTsxyQkxjtEPCwkcaG3F0aMyZw069O/wCfWbNqRftjJ3y
+5GmCeSR81wNCvqtDwod0HOu5hz/+J70PewMn9r4U/Tl8vrhYvOwPAAD//wMAUEsDBBQABgAI
+AAAAIQA10IKBkwEAALwCAAAPAAAAeGwvd29ya2Jvb2sueG1sjFLBjpswEL1X6j9Yc2cBA9lV
+FLIqS6Lupeoh3ZxdPARrjY1sp2RV9d87kCZN1R568hvP8/PzG68eT71m39B5ZU0J6V0CDE1j
+pTKHEr7sttEDMB+EkUJbgyW8oYfH9ft3q9G616/WvjISML6ELoRhGce+6bAX/s4OaKjTWteL
+QKU7xH5wKKTvEEOvY54ki7gXysBZYen+R8O2rWqwts2xRxPOIg61CGTfd2rwsF61SuPL+UVM
+DMMn0ZPvkwamhQ8bqQLKEnIq7Yh/bLjjUB2VnrpFsgBGMeD59PfqfrtZZFkePaUfeLQpkjqq
+6qyOim1e52lVPfFN9gPi9TWWz+7m+K5Tfv8rL2ASW3HUYUdBXXxS8jznfDEpTKG+KBz9b7Gp
+ZKe9MtKOJdCI3i44TQpg49zYKxk6Eiqy9Lr3EdWhCyU8JGkyacc34vMg6JJ5ZWZOaWsPWtmU
+Rj5N6ZmSIOyWioB7lukk8Ted39AJX+n83/Tshk74Ss9mgxdXjdDNlCEtsw1e3PNiZlw+3von
+AAAA//8DAFBLAwQUAAYACAAAACEANJKbPYMGAABVGwAAEwAAAHhsL3RoZW1lL3RoZW1lMS54
+bWzsWU1vG0UYviPxH0Z7b2MndhpHdarYsRto00axW9TjeHe8O/XszmpmnNQ31B6RkBAFcUHi
+xgEBlVqJS/k1gSIoUv8C78zsrnfiNUnaCESpD4l39nm/P+ad8dVrD2KGDomQlCdtr3655iGS
++DygSdj27gz7lzY8JBVOAsx4QtrejEjv2tb7713FmyoiMUFAn8hN3PYipdLNlRXpwzKWl3lK
+Eng35iLGCh5FuBIIfAR8Y7ayWqutr8SYJh5KcAxsh0CDAopuj8fUJ95Wzr7HQEaipF7wmRho
+5iSjKWGDSV0j5Ex2mUCHmLU9kBTwoyF5oDzEsFTwou3VzMdb2bq6gjczIqaW0Jbo+uaT0WUE
+wWTVyBThqBBa7zdaV3YK/gbA1CKu1+t1e/WCnwFg3wdLrS5lno3+Rr2T8yyB7NdF3t1as9Zw
+8SX+aws6tzqdTrOV6WKZGpD92ljAb9TWG9urDt6ALL65gG90trvddQdvQBa/voDvX2mtN1y8
+AUWMJpMFtA5ov59xLyBjznYr4RsA36hl8DkKsqHILi1izBO1LNdifJ+LPgA0kGFFE6RmKRlj
+H/K4i+ORoFgLwJsEl97YJV8uLGlZSPqCpqrtfZhiqIk5v1fPv3/1/Cl69fzJ8cNnxw9/On70
+6Pjhj5aXQ7iLk7BM+PLbz/78+mP0x9NvXj7+ohovy/hff/jkl58/rwZCBc01evHlk9+ePXnx
+1ae/f/e4Ar4t8KgMH9KYSHSLHKEDHoNtxjGu5mQkzkcxjDB1KHAEvCtY91TkAG/NMKvCdYjr
+vLsCmkcV8Pr0vqPrIBJTRSsk34hiB7jHOetwUemAG1pWycPDaRJWCxfTMu4A48Mq2V2cOKHt
+TVPomnlSOr7vRsRRc5/hROGQJEQh/Y5PCKmw7h6ljl/3qC+45GOF7lHUwbTSJUM6chJpTrRL
+Y4jLrMpmCLXjm727qMNZldU75NBFQkFgVqH8kDDHjdfxVOG4iuUQx6zs8JtYRVVKDmbCL+N6
+UkGkQ8I46gVEyiqa2wLsLQX9BoZ+VRn2PTaLXaRQdFLF8ybmvIzc4ZNuhOO0CjugSVTGfiAn
+kKIY7XNVBd/jboXoZ4gDTpaG+y4lTrhPbwR3aOioNE8Q/WYqdCyhUTv9N6bJ3zVjRqEb2xx4
+14zb3jZsTVUlsXuiBS/D/Qcb7w6eJvsEcn1x43nXd9/1Xe+t77vLavms3XbeYKH36uHBzsVm
+So6XDsljythAzRi5Kc2cLGGzCPqwqOnMEZEUh6Y0gq9Zc3dwocCGBgmuPqIqGkQ4hRm77mkm
+ocxYhxKlXMLZzixX8tZ4mNOVPRk29ZnB9gOJ1R4P7PKaXs6PBgUbs+WE5vyZC1rTDM4qbO1K
+xhTMfh1hda3UmaXVjWqm1TnSCpMhhoumwWLhTZhCEMwu4OV1OKRr0XA2wYwE2u92A87DYqJw
+kSGSEQ5IFiNt92KM6iZIea6YywDInYoY6XPeKV4rSWtptm8g7SxBKotrLBGXR+9NopRn8DxK
+um5PlCNLysXJEnTU9lrN1aaHfJy2vTEca+FrnELUpR78MAvhdshXwqb9qcVsqnwezVZumFsE
+dbipsH5fMNjpA6mQagfLyKaGeZWlAEu0JKv/ahPcelEG2Ex/DS3WNiAZ/jUtwI9uaMl4THxV
+DnZpRfvOPmatlE8VEYMoOEIjNhUHGMKvUxXsCaiE2wnTEfQDXKVpb5tXbnPOiq58gWVwdh2z
+NMJZu9UlmleyhZs6LnQwTyX1wLZK3Y1x5zfFlPwFmVJO4/+ZKXo/geuCtUBHwIe7XIGRrte2
+x4WKOHShNKJ+X8DgYHoHZAtcx8JrSCq4UTb/BTnU/23NWR6mrOHUpw5oiASF/UhFgpB9aEsm
++05hVs/2LsuSZYxMRpXUlalVe0QOCRvqHriu93YPRZDqpptkbcDgTuaf+5xV0CjUQ0653pwe
+Uuy9tgb+6cnHFjMY5fZhM9Dk/i9UrNhVLb0hz/fesiH6xXzMauRVAcJKW0ErK/vXVOGcW63t
+WAsWrzZz5SCKixbDYjEQpXDpg/Qf2P+o8Jn9cUJvqEN+AL0VwW8NmhmkDWT1JTt4IN0g7eII
+Bie7aJNJs7KuzUYn7bV8s77gSbeQe8LZWrOzxPuczi6GM1ecU4sX6ezMw46v7dpSV0NkT5Yo
+LI3zg4wJjPldq/zDEx/dh0DvwBX/lClpkgl+VhIYRs+BqQMofivRkG79BQAA//8DAFBLAwQU
+AAYACAAAACEA8E99xCUBAADUAQAAGAAAAHhsL3dvcmtzaGVldHMvc2hlZXQyLnhtbIxRTU/D
+MAy9I/EfIt9puqEBmtpOSFMFBxBCwD1rnTZaEleJR+Hfk3bahMSFmz/ee/azi82Xs+ITQzTk
+S1hkOQj0DbXGdyW8v9VXdyAiK98qSx5L+MYIm+ryohgp7GOPyCIp+FhCzzyspYxNj07FjAb0
+qaMpOMUpDZ2MQ0DVziRn5TLPb6RTxsNRYR3+o0Famwa31Bwcej6KBLSK0/6xN0OEqpgnvASR
+bOCzcmnrmjpraAmyKlqTiJNbEVCXcL+YijPjw+AYf8Visrgj2k+Nx7aEfILKP9h6tpjmtajV
+wfIrjQ9oup7TPVdn9a1ileiD6vBJhc74KCzqhMmzWxDhiJ9jpmGurkDsiJncKevT9TBdKc+u
+QWgiPiXTWud/VD8AAAD//wMAUEsDBBQABgAIAAAAIQAj/NISJQEAANQBAAAYAAAAeGwvd29y
+a3NoZWV0cy9zaGVldDMueG1sjFFNT8MwDL0j8R8i32k6pgGa2k5IUwUHEELAPWudNloSV4lH
+4d+TdtqExIWbP9579rOLzZez4hNDNORLWGQ5CPQNtcZ3Jby/1Vd3ICIr3ypLHkv4xgib6vKi
+GCnsY4/IIin4WELPPKyljE2PTsWMBvSpoyk4xSkNnYxDQNXOJGfldZ7fSKeMh6PCOvxHg7Q2
+DW6pOTj0fBQJaBWn/WNvhghVMU94CSLZwGfl0tY1ddbQEmRVtCYRJ7cioC7hfjEVZ8aHwTH+
+isVkcUe0nxqPbQn5BJV/sPVsMc1rUauD5VcaH9B0Pad7rs7qW8Uq0QfV4ZMKnfFRWNQJk2e3
+IMIRP8dMw1xdgdgRM7lT1qfrYbpSni1BaCI+JdNa539UPwAAAP//AwBQSwMEFAAGAAgAAAAh
+AC3pH5ISEgAAAD4AABEAAAB4bC92YmFQcm9qZWN0LmJpbuxbfXBb1ZW/70m2ZfkDxThZJ7GJ
+YgcSQhSenmTZCXFqfTzZDnLs2kkcisCRZCUIy5YrycEhJFECuyQspRQoTaHJLrS7hZa2Bjqd
+6Qy7hEJ3ZneSLdtmZ7vdnWXZpf/tB2zbaafD5u3vvA/ryZVlOWSyhcmVf+/ed865X+fee+59
+916//aNl//rcy6veZfPcdmZil+RqVmmgcwgTFGdjjEeAcEmWZYWGB4LX3MdIA/+Lslah3ZYD
+ZqACoDa3APVANWAFaoBaoA64DlgN2IBlQANwPdAIrACoP/wB/CZgJbAKaAaI3qL5HyMVfaKL
+OsRS+GWZnUlsEn6aHUILle9WoMfoCqL+UsrZf/Ha2q+e/nvOBKFuhyq5h/mYt1SkRXgWWCQ9
+f7JFxZyer+4bZfpR+zE2zZLwnUZGmWEb4zkaP1QnKkeZ0ViSBhrcKH7DbIgNwhdV0pKeyF8Z
+m5RcufnTGP2plgsHn/RG5TeOf6oT2YBi45/amWyADVgGGMc/2ZH545/eyV7o4/8GhNcAdmAt
+0Aq0AeuAG4GbgPXABuBmYCNwC0DxqdtsBm4FBMAJkN5cgBtoBzxAB9AJbAG2ArcBFL8L/nbg
+U0A34AV8gB8IABIQBHqAXqAP2AHcDlD8fvg7gQFgEPg0MAQMA7uA3cAeYATYC9wBfAa4E6D4
+d8G/GxgF9gERIArEgDEgDuwHDgD3AAngXmAcSAITwCSQAqaAzwJpIANkgWngIHAfMAMcAu4H
+DgMPAEeAo8AxIAdwKzhW+T4CbjNjF+HPcmpHmsb78goQ4EBiZ87GZoljIgLc9xDmiDHX7WTZ
+jDBRlu5siEIpU4ZVADSlOI6zaG9UGR4o6dTs+yU9ARJWaXph39HeLXNZ6FldbX+DUhOPlfKl
+4CqLHuK31Wql5LepNL30alMQjeN+A+V3KvWTZf7BRp6oVCuBEtNqlw+pebSAcCNQ09CkyCO4
+D5jTBYXJkbI/vDQ+R6fYJqWBHlLaifK6Uq5X63CC5n+UdJsMabSitxjTonmi23xJ3geZJBik
+YzvqSTJEnwE9XIT+OOihIvQXQKfmmZ/OOdA3FaH/FPRmAz2HvClfcrUGOshz9A/B1tMnOsn2
+wufwMzq9HxDf6Bai6zIWQyIW2H+iE0nnz/fVAXienx2tYtw6GOYwjN9+7bcRBvkuGOD9MGgC
+fiIMmodxNk1MgN8Ge+1m7xhyXXLw0t9w6Iitxy1Ypmr24LiFa8AEYUNaZtB58KliVHiOExB6
+2kyKIKMpy1Pwvwc70x+JpVOshXm7W3kO5jetcGV5EHyKCSX3zzJvNptORKezLG7f4xvdGZlA
+oMve2p/CmiGZcrbWWYfZdNSejWeyG9jNdVZ7f+aAj6Vm7BtakYElnWplvdLkmJ1jgTorUv4d
+l96dA80IVOooLPUxEq0HqikAp/pq1cgUUh8gBfy1ZqSXVSpiyqwgUJBnt6z/8NL6Znn9NvlX
+RNiaRK12hmVzWLZyWK17LOjw2+Rm+cNL6uIdPSCHhlunqE+d49RVP8UmR6t4dZ1AY4VH/+fR
+p3nYCh5xaSGAeUopuSJYxkOtPwnmNFDYBqj15ef6ItGpWc0o33F6UWpP9ou+YXSnMPKPycWK
+Q/Mf9z4inIcuL1JE+JTYT+Cv1yY7GmxnzkZRQ1luw8tJvBeb/ygeJbF0Z0OUxec/qn5Jp2Zf
+zvxHVoWccf3pKpl2caYN+lJTUp/FpQqpq/BK6xtyu7DKSWD1MoLhl0bvicJPKasdlb/YcxXW
+32gXpbdQCRaTJz714ne1pg0itwNYWSXgO8uJPE8G9edoYOs6mMcu+roJ1J8VyV8sKl2aSPqn
+gU9do9z6b4Osnr9e7qvvb0ApeDYcUkvNsWHbXOgpNQT+NjWkr384rL0FrGVpeqEJRsAKyIk1
+vkOjCAUhv0bNc9V46tONyemIojvKg+O6qTw5dT62QZdK+bh8jyIpchDDtKGuo+i9F2Hyu+Gr
+jsyF6oiuO6LoYaNfim7kGcPG+BTmUFMyYTQvv0OCl+todlWLWbjypkwwI84uOCPuugejeCSV
+Ho+mUrZxzIvWR32RDMc/KhwWBEEUBjudWxzMJpgs/irWyDdw6wTB7TlyvO6BnmQqGklyb9uG
+pyIx7olgJGnOxGvH/Ol4JGuJRJPL1wym42OPx2PJCKvsG2MvW3alp+t80sxUCoKNzbviE1NJ
+FsnGA/F04qCJb3jCN53JpiYSlfebA6bFxtQS7H/jw6SZT5j9p4b9/3FXavyTFciPcGPo2vhf
+shlYZPy/uOD4D6YO5JIJZUVsfd+Coc/P0MjPYeh3ioKD2VvMFj8GfrMy8M2eI9rA9yW5v9IG
+vi2S1Aa+PZLVBn53fEwb+K1j7CVt4NumUtrAZ1NJdeDXJg7y69SB3zSRKGfga6pZwvi/8INr
+4/8Kmopr419VJnXEYmrVx67OW0iO+Fdr/l90/Is0839Cx/9jKz6B49/4/XOZ339YI+ed3mtL
++SEw9e+PUeyW0wnQKHbTh7CrvgM78H5lJ71UCnkevv8KRk+es3AoDFZFjcofw5dfemHRRTm0
+3ZSvff5roVTEU2BGeVXC+P0tlIq0AA/ff4r+a8GnciwgVkB+Em8/p49mOGPZr274mv1X9b1Q
+KyjNY2ihheQold8b++/6uNn/85GTtJ0u2zAeqoH3zPmvdw57jTz7rbJ/3VOw6+CGlQp+hK+O
+NuyCb8Z3Sxv2Ltpg77ZiI121fz2wgl6c0Img+GEP+/EbwK7HUexzhBHuA5Xs5LBGUa1nWLOi
+HsgQRcLexmacZ4bwawOFdtimcdaYZHZwI3hLYMvbjjrQrpsdOU7hR7tgMXCz8FM4ZczoXVDx
+mzna7i/UA+2+uD6SHpwopwdlJF3oeqCt+AMoVwSnnVTGBEoWR2nsOBGewY6PB6fCpAsqLclm
+gP3KHYYB5SiC6HFIGN+cikYlnMn6oZ0QcqWwhFyLpSMhH0qD9EUxqa3sSC+Ks9g4OHRfIoT8
+o0op59+ZeK1oj3F9pO/UNq0URk2NoASTOGRJ4bQ3g/oO47SXdlIHAA96WBhvWYVP+qNaZFGj
+KOo8gNJLqIMXvSKL+BOGVjc2unuuzUX0piB0H0At3NChAwc9PlC8CDlBc+LNAQSQbrvWI7zg
+0l6bGz+VLmLHTa2JG/7S29yvlHUCz0mUPt8ziveGYewtR9BCceiAekMQPxo/ktKmFGcY1Pw4
+KdYTjH3ocvpChXJKw8OO0AmJGbN+Jc8xC3wrfJtyCpuftdUdvMJ5uPw52oQc6EyIMzahFj5z
+tgKnSYW3XRrRAr97YCfLjfx8SVmmw3/F0bplNY6cZLlpwc17SnhvwUlgDIMGl0D4hePIclRb
+kjRRFg4TZVGPFjPuz1PCdxRJuAUlLpRELbBdR05YKDkRR2ACLmcYTyzVcq6Zl5yIotDXP7ne
+hZJzKcndWSS5tfOScyE5+pggty+f3AyCHJfDs7ADFH+jchTnXD6VClMqNluEXyruUnnCVciL
+etpSy3U15D/3T+9vTFleD5w6+6xPYv98nvKkQf3srbb0E2++1vNYZvDNX+7dndPpRz/1dMsD
+VW/c/sJI6+D5wz9eqdNPBBIH3/hPq/RM5Lkfn+585IJOdx36vBRevWvHS5X/cRzJngAeBB4C
+/hD4I4AGD1ZGyp2lR+D/MfAo8DngMeDzwOPAF4AnADo3eQr+F4GngS8Bp4EvA88AzwJfAc4A
+Z4E/Af4UeA54Hvgq8DXgz4A/Byi9FzT/G/C/qYW/Bf/bwHeAWeBl4BXgVeC7gHp+QxW95srR
+wA9/uf0GOwlyUB758FiOHqyBu57J7KGb8LFcK83E4sncLTZmqt3j8/7632yswjySmHR63jiq
+BV1i1TGwzbj28OorNmY2Q87zrTYbVtWD6dSBeDabcgZCNlZZm8mOpZLxJ/fZmJUSA/feeCz7
+l68Tb2D//kQsvnzaxqpqccmC7lhcbEAaKJFs4tiodDCSnMaWb9M/IouXNqDQskyXL9zftLHa
+WhxC6WdQ4w/8O6WBbWnalf6gPv8i/o/hxfULvFQyurWRmvnSkI1ZzNoh1vh4E76toRduBrog
+zRS6ep4+/NUJvIE3Ibycp2vSRqnVmOIrWQskrWwNZKxsLW/GUbVRRg3jKx5UCM7j1SqXqjnl
+CiG1iXo1Qg1RixW6K0UpTLXkG3f3Kzn6TqKPKGEjzz9ezaYaK3tN7ATPxt4zm1HnZqa38slY
+tgJrQdbNN95byVu7eGsVn+YaLRWVDdV8g7L4eYrV8ju28dYVjEvn1J6ynV85t5DN0Uq2Duu6
+G/m7mWljuEc7Y3C7BAeztJjNfqyr6rkGHC4yz5E2cbPQhlWmf2sYnRUL5fsy4eFDmRH7wIjH
+HTbFRayIk9G2gZCEpfA0zgwj2URzapLtY9yDErpiIBF7SMIKMMcSF1nshPS13LJnctxxiYmB
+YGdAcGMZ3O4Leh1OIef0OXwBqT0nXfR6c353rkI8KeVOswPpyIQ9mECpM/YNM52em3Nhf2pi
+IjVpqmbh/gSuIGVSbH/WPnxPJI1langgGOzzY30qhvuHBzYHTodCbQ/bHhy0O3NZi30gevwt
+eygRnU1H0ofYTsZdd+IfLBXs+hOmM2dXMr4qZ1WHjODsYfVcJZsdYyem2St0sWFVte0R0WZp
+zmGfqNfH3ev0pVezYzf4eK5i06d9q2Kza32WW3zcSs6X0QYRo5Pc8R7WVNFduetclr4gR7w5
+u3JH4xyXaj/HrfrbGvFnNT9a5zC9xdiFdVFzq6/th+u8Xp8y6CRf8Fe5NQfOeRPnOMmHwx/f
+T2ossZbGh3e3iG+OVi9/Wnxr1N8iDrSILNay68IPdre4BlpcO1pc/d+pd71e//z2x1a8WG/j
+nneU7H7syds3nsQ6GkKq6aL1rx7SY6r3pOiNw8Wv4j/cot1NC+ccpHSYcRMLpAJghB41AePa
+xaLlkFdvKmFVjzC5v1M91nf/s7+++cJs6NUbUzu/XbP1vzhlh8GGZN8DyNGgl2V8I5yMaKnw
+7MUKlUecKTWoSHHsC3Mcxr6vcUxW9dYeSZMj8gcleCtRIXLF4jlL8Po0nplxUEExtxJqKeZo
+V6WY26LVZj4PyfNVIGrmH6nydMFGtyGxLNLjabvTaO31SJq9L3wVC19dSMD03/QpBndOebIg
+EuRvRRimYnBooGfIe1QM+wf6+wd2HnWG+/v8QwPDCKAQBA8ekmdzIBSCVmDXldKhkNaIpiVq
+Cao29YxTGu0DzRdAyzsqy/WWwrJch0S3QoTKgqlTMyG6BdHNBqyGOluGVQ82Q9rrl0Kbpb0S
+VZjUq0zbVLA71By0jlSp5H9co3Faw3UrVP1BBRPmKQkdnF8DAc2e6uZUsaaqrRY3w6BS7pSF
+SkK1j1ERPqtlQ7rRcmZf1mjvav6gnrviUxFCb//G8ds7V9v+4ufsGyzwL0MNSFsCdwHdqMaV
+LG48Y1CUYl5166ob11BIL6iqQKUBqdXg1EKqL6c02lqtw+5TJPQHpUECtApBLepq8W9SuiGg
+y4qsBuniwzuCL2DarIIMT3y9B9XhwvsHWsqU6zBfoyy8KX1hsduLZLVyJAmX00BhylK1SoUL
+CuKpdFqwkDOePziJvURnU+ytmhY9y4n+dQgNaYKFpz73Kf/fUE4aukzjZeT/IiKfKpq/nmr5
+Puqv/BcEmlHRaDkxX4LQdvSbK+Xy2i9P/8Z883Gzi3c2Y0QtrFnpeTtLhcZ5wX0ixjR7PW/z
+RqeLBXQRWaryrgK6q0ixyib1BbpaDwfFDp/L53E6vJKz0+F2uzscnV6/0xHweXweKRDY0iH4
+juBkQ6lsvEurc501kIpNT8Qns13GqejWm3px401xBgmtoqWYYimmq4CJi+jxrtb8lIiy9caT
+U/7UZDY+k6U6CSDtiaczidQkbOIUFrbRZNwldrW6trhEUUTxIODv7+lqFdvFDlFydnR4XJ0d
+XiMgERj0dbW6ve5Ot7PD4+mAkAJwevxdrZ6gJ+Bxtwe8fm9HQPBiSeztFINg1lnv7E1lsnZp
+JhufHIun7X2T+1N31VnnVOPsOuzqdImILTj8wS2Cw+n0Bx2dktvlEASv4N/idApCu/fIbTCT
+t83FEpSU6QMtg7uCcSSoNUWXU3BvsiuPDidC7a4tm+x1VmOzdAmb7HN//jqr1iDFyWJxsms+
+uexu9nsr+H8CAAAA//8DAFBLAwQUAAYACAAAACEAyRFf+qQBAABlAwAADQAAAHhsL3N0eWxl
+cy54bWykU8Fq3DAQvRfyD0L3RrsLDW2xnUNhIZCUQLbQq2yNvQJpZKTxsu7XZ2Q73t1TDr1Y
+T08zb55m5OLx7J04QUw2YCm39xspAJtgLHal/HPYf/0uRSKNRruAUMoRknys7r4UiUYHb0cA
+EiyBqZRHov6nUqk5gtfpPvSAfNKG6DXxNnYq9RG0STnJO7XbbB6U1xZlVbQBKYkmDEjsYiGq
+Iv0TJ+2Y2UpVFU1wIQpieTYyMag9zBG/tLN1tDms1d66caZ3mZgcLXHeYoiZVLnksiROss6t
+BnbZABNV0WsiiLjnjVjwYey5PHI3Zpkp7pPoLupxu/t2laCmglVRh2i4+9dXn6mqcNASG422
+O+aVQs/fOhAFz8BY3QXUjqH6yFgAX6cB597yhP62N9rnVuDg956eTCl51rkJH5AvssBZb95k
+/Wu1Wfu/ZcW5vdVnxSvbN6bX8iLPu5S/85NyIFcNUQ/WkcVbxen+LGrOlx5s8ghI1/x0c3fW
+MtwKA60eHB3Ww1Je8AsYO/gfa9SrPQWaJEp5wc95VNuHacxp/T+qdwAAAP//AwBQSwMEFAAG
+AAgAAAAhAJC+E4cwAQAA5AEAABgAAAB4bC93b3Jrc2hlZXRzL3NoZWV0MS54bWyMkU1PwzAM
+hu9I/IfId9oONEBT2wlpmuAAQnzds9ZtoyVxlXgM/j1uxrjsws0feV77dcrll7PqE0M05CuY
+ZQUo9A21xvcVvL+tL25BRda+1ZY8VvCNEZb1+Vm5p7CNAyIrUfCxgoF5XOR5bAZ0OmY0opdO
+R8FpljT0eRwD6jZBzuaXRXGdO208HBQW4T8a1HWmwRU1O4eeDyIBrWbZPw5mjFCXacJzUGID
+n7STrdfUW0MzyOuyNQJOblXAroK7VEzEh8F9/KWnWLHevKLFhrGV04CaLG+ItlPzQUrFpJef
+sOtkWea32Omd5Rfa36PpBxaR+YQkYqVZSzzqHh916I2PymInb4rsBlQ4vE8x05iqc1AbYiZ3
+zAa5JsrViuwKVEfEx2Ra6+9/6h8AAAD//wMAUEsDBBQABgAIAAAAIQDUAwQvRAEAAGUCAAAR
+AAgBZG9jUHJvcHMvY29yZS54bWwgogQBKKAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAACMkl9LwzAUxd8Fv0PJe5tmc+pC24HKnhwIbii+SEjutmLzhySz27c3
+bbfaMR+EvOSec38595JstpdV9A3WlVrliCQpikBxLUq1ydFqOY/vUeQ8U4JVWkGODuDQrLi+
+yrihXFt4sdqA9SW4KJCUo9zkaOu9oRg7vgXJXBIcKohrbSXz4Wo32DD+xTaAR2l6iyV4Jphn
+uAHGpieiI1LwHml2tmoBgmOoQILyDpOE4F+vByvdnw2tMnDK0h9MmOkYd8gWvBN7996VvbGu
+66QetzFCfoLfF8+v7ahxqZpdcUBFJjjlFpjXtljsKvBef5I0w4Nys8KKOb8I216XIB4OZ85L
+NTDbETowiCiEot0IJ+Vt/Pi0nKNilJJpTEg4SzKhZEwnNx/N42f9TciuII8R/k+8o6PpgHgC
+FBm++BjFDwAAAP//AwBQSwMEFAAGAAgAAAAhAEmgZLSOAQAAPAMAABAACAFkb2NQcm9wcy9h
+cHAueG1sIKIEASigAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+nJPBTuMwEIbvSPsOlu/UaUErVDlGCBZxAG2lFu6DM2ktXDuyh6jdp99JotIUVnsgp5n5J7++
+mUz09W7rRYspuxhKOZ0UUmCwsXJhXcrn1f35lRSZIFTgY8BS7jHLa/PjTC9SbDCRwyzYIuRS
+boiauVLZbnALecJyYKWOaQvEaVqrWNfO4l2071sMpGZF8VPhjjBUWJ03H4ZycJy39F3TKtqO
+L7+s9g0DG33TNN5ZIJ7SPDmbYo41iV87i16rsaiZbon2PTnam0KrcaqXFjzesrGpwWfU6ljQ
+Dwjd0hbgUja6pXmLlmIS2f3htc2keIWMHU4pW0gOAjFW1zYkfeybTMncx7V3onLCQxtT1Iq7
+BqUPxy+MY3dpLvoGDk4bO4OBhoVTzpUjj/l3vYBE/8C+GGP3DAP0CDROx4AfqP0QcfYfbYAd
+D9YvjBE/QT268Jafm1W8A8LD5k+LermBhBV/rIN+LOgHXnryncntBsIaq0PPV6G7k5fhZzDT
+2aTgpz+PQ02r49mbvwAAAP//AwBQSwECLQAUAAYACAAAACEA8RrlZoUBAABPBQAAEwAAAAAA
+AAAAAAAAAAAAAAAAW0NvbnRlbnRfVHlwZXNdLnhtbFBLAQItABQABgAIAAAAIQC1VTAj9QAA
+AEwCAAALAAAAAAAAAAAAAAAAAI8DAABfcmVscy8ucmVsc1BLAQItABQABgAIAAAAIQCcfziW
+GAEAAMEDAAAaAAAAAAAAAAAAAAAAAHsGAAB4bC9fcmVscy93b3JrYm9vay54bWwucmVsc1BL
+AQItABQABgAIAAAAIQA10IKBkwEAALwCAAAPAAAAAAAAAAAAAAAAANMIAAB4bC93b3JrYm9v
+ay54bWxQSwECLQAUAAYACAAAACEANJKbPYMGAABVGwAAEwAAAAAAAAAAAAAAAACTCgAAeGwv
+dGhlbWUvdGhlbWUxLnhtbFBLAQItABQABgAIAAAAIQDwT33EJQEAANQBAAAYAAAAAAAAAAAA
+AAAAAEcRAAB4bC93b3Jrc2hlZXRzL3NoZWV0Mi54bWxQSwECLQAUAAYACAAAACEAI/zSEiUB
+AADUAQAAGAAAAAAAAAAAAAAAAACiEgAAeGwvd29ya3NoZWV0cy9zaGVldDMueG1sUEsBAi0A
+FAAGAAgAAAAhAC3pH5ISEgAAAD4AABEAAAAAAAAAAAAAAAAA/RMAAHhsL3ZiYVByb2plY3Qu
+YmluUEsBAi0AFAAGAAgAAAAhAMkRX/qkAQAAZQMAAA0AAAAAAAAAAAAAAAAAPiYAAHhsL3N0
+eWxlcy54bWxQSwECLQAUAAYACAAAACEAkL4ThzABAADkAQAAGAAAAAAAAAAAAAAAAAANKAAA
+eGwvd29ya3NoZWV0cy9zaGVldDEueG1sUEsBAi0AFAAGAAgAAAAhANQDBC9EAQAAZQIAABEA
+AAAAAAAAAAAAAAAAcykAAGRvY1Byb3BzL2NvcmUueG1sUEsBAi0AFAAGAAgAAAAhAEmgZLSO
+AQAAPAMAABAAAAAAAAAAAAAAAAAA7isAAGRvY1Byb3BzL2FwcC54bWxQSwUGAAAAAAwADAAJ
+AwAAsi4AAAAA
+
+--ETDFsshmzrOmOVdZ--
+
diff --git a/upstream/t/data/spam/olevbmacro/target_uri.eml b/upstream/t/data/spam/olevbmacro/target_uri.eml
new file mode 100644 (file)
index 0000000..337ca96
--- /dev/null
@@ -0,0 +1,123 @@
+Date: Mon, 11 Nov 2019 16:27:11 +0100\r
+From: spammer@example.com\r
+To: victim@example.com\r
+Subject: Macro\r
+Message-ID: <20191111152711.GC97706@example.com>\r
+MIME-Version: 1.0\r
+Content-Type: multipart/mixed; boundary="------------hAgW8a3LP3BWlJ9fOS2eb04F"\r
+\r
+This is a multi-part message in MIME format.\r
+--------------hAgW8a3LP3BWlJ9fOS2eb04F\r
+Content-Type: multipart/alternative;\r
+ boundary="------------iC5uW0YI0OqufC4h0AmvPaCr"\r
+\r
+--------------iC5uW0YI0OqufC4h0AmvPaCr\r
+Content-Type: text/plain; charset=UTF-8\r
+Content-Transfer-Encoding: 7bit\r
+\r
+\r
+--------------iC5uW0YI0OqufC4h0AmvPaCr\r
+Content-Type: text/html; charset=UTF-8\r
+Content-Transfer-Encoding: 7bit\r
+\r
+<html>\r
+  <head>\r
+\r
+    <meta http-equiv="content-type" content="text/html; charset=UTF-8">\r
+  </head>\r
+  <body>\r
+    <br>\r
+  </body>\r
+</html>\r
+--------------iC5uW0YI0OqufC4h0AmvPaCr--\r
+\r
+--------------hAgW8a3LP3BWlJ9fOS2eb04F\r
+Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document;\r
+ name="uridoc.docx"\r
+Content-Disposition: attachment; filename="uridoc.docx"\r
+Content-Transfer-Encoding: base64\r
+\r
+UEsDBBQACAgIAEJ8d1MAAAAAAAAAAAAAAAALAAAAX3JlbHMvLnJlbHOtkk1LA0EMhu/9FUPu\r
+3WwriMjO9iJCbyL1B4SZ7O7Qzgczaa3/3kEKulCKoMe8efPwHNJtzv6gTpyLi0HDqmlBcTDR\r
+ujBqeNs9Lx9g0y+6Vz6Q1EqZXCqq3oSiYRJJj4jFTOypNDFxqJshZk9SxzxiIrOnkXHdtveY\r
+fzKgnzHV1mrIW7sCtftI/Dc2ehayJIQmZl6mXK+zOC4VTnlk0WCjealx+Wo0lQx4XWj9e6E4\r
+DM7wUzRHz0GuefFZOFi2t5UopVtGd/9pNG98y7zHbNFe4ovNosPZG/SfUEsHCOjQASPZAAAA\r
+PQIAAFBLAwQUAAgICABCfHdTAAAAAAAAAAAAAAAAEQAAAGRvY1Byb3BzL2NvcmUueG1sjVJd\r
+T8IwFH33Vyx9H12HELPASNSQmEhi4ozGt9peRnXrmvbC4N/bbWyg8uDbPfecnvvV2WJfFsEO\r
+rFOVnhM2ikgAWlRS6XxOXrJleEMCh1xLXlQa5uQAjizSq5kwiagsPNnKgEUFLvBG2iXCzMkG\r
+0SSUOrGBkruRV2hPritbcvTQ5tRw8cVzoHEUTWkJyCVHThvD0AyO5GgpxWBptrZoDaSgUEAJ\r
+Gh1lI0ZPWgRbuosPWuZMWSo8GLgo7clBvXdqENZ1ParHrdT3z+jb6vG5HTVUulmVAJLOjo0k\r
+wgJHkIE3SLpyPfM6vrvPliSNo5iFjIXxOGPTZBwnk5v3Gf31vjHs4sqmDXsCPpbghFUG/Q07\r
+8kfC44LrfOsXnioMH7JWMqSaUxbc4coffa1A3h68x4Vc31F5zP1/pOskmpyN1Bu0lS3sVPP3\r
+UtYWHWDTtdt+fILAbqQB+BgVFtCl+/DPf0y/AVBLBwjh3s3VYwEAANsCAABQSwMEFAAICAgA\r
+Qnx3UwAAAAAAAAAAAAAAABAAAABkb2NQcm9wcy9hcHAueG1snZFNT8MwDIbv/Ioq2nVNN2BM\r
+U5qJD3GaxCTKxm0KidcGtUmUZNP273FXKBVHkotfv9Zjx2HLU1MnR/BBW5OTSZqRBIy0Spsy\r
+J2/F83hOkhCFUaK2BnJyhkCW/IqtvXXgo4aQIMGEnFQxugWlQVbQiJCibdDZW9+IiNKX1O73\r
+WsKTlYcGTKTTLJtROEUwCtTY9UDSERfH+F+osrKdL2yKs0MeZwU0rhYROKO/YWGjqAvdAJ9g\r
+uhfs3rlaSxFxI3ylPzy8XFrQu3Ta3tFKm8Np9z6f7WY3yaBgh0/4BNnOMHo46FqNp4wOYS15\r
+062aT27TDM+l4CfH1qKE0E7TBWxrvbroLmCPlfBCRizn18geyIG11bF6dULC36KBgZ28KL1w\r
+1Xe7XqHoP4J/AVBLBwi/3MlYKgEAAB4CAABQSwMEFAAICAgAQnx3UwAAAAAAAAAAAAAAABwA\r
+AAB3b3JkL19yZWxzL2RvY3VtZW50LnhtbC5yZWxzrZLPTsMwDMbve4ood5puIIRQ010G0g5c\r
+UHmA0LpNtNSJEoO2t8eDwTZpIA6VcvGf/L5PtqvldvTiHVJ2AbWcF6UUgG3oHA5avjSPV3dy\r
+Wc+qZ/CGuCVbF7PgP5i1tETxXqncWhhNLkIE5Eof0miIwzSoaNqNGUAtyvJWpVOGrM+YYt1p\r
+mdbdXIpmF+E/7ND3roVVaN9GQLogoTLtPGQmmjQAafkVF8yR6rL8Ykp5y6TkHW6ODvbYvOdG\r
+w9jMz2FheEoWPqHfnU+hYxMPW4KE5le311O67QNSY149HN3+pP4a2c2kGwMivrzTnR0yBwuz\r
+Sp2dYv0BUEsHCNLpPe72AAAAwQIAAFBLAwQUAAgICABCfHdTAAAAAAAAAAAAAAAAEQAAAHdv\r
+cmQvZG9jdW1lbnQueG1spZTbjpswEIbv+xTI9wmwu1qlaMneRK0itatISR/AMQbc9Uljk2z6\r
+9B2DgT1IVdS9wdie+Wb+35iHxxclkxMHJ4wuSb7MSMI1M5XQTUl+Hb4tViRxnuqKSqN5SS7c\r
+kcf1l4dzURnWKa59ggTtClOSDnThWMsVdQslGBhnar9gRhWmrgXjcSAxA0rSem+LNI1JS2O5\r
+xr3agKIep9CkQ8om1kpvsuw+BS6px35dK6wbaad/1T8pOcadr6l6NlBZMIw7h0YoOdRVVOgJ\r
+k2dXCA6cKcNeU7kCen5V8m0jm2FzJroPyKmNJbYR3espyMuzd7x9Sy2fac3naN/BdHakKXaN\r
+WkXhubPBMYsnehRS+EsvfG4qv/tcV+89+z9e+H4UK7aNNkCPEi8CgpLQHVnjXTia6hJG2z92\r
+0A97f5E8ORcnKkvyFFRLkvbRohLjejYs/WbjguS1H9YgcNJ5jNz2YjlIoZ8TKERVEthWN30T\r
+MCXh8Kb4VnsOmvsfmBXY6RTn18ELF8ywFL1wNJi2pJaiN/0RhWA/pPTPqfzQU4A4znzE4eZY\r
+VPMXv6MNH8TYZv8Hd/D25fnXcL5Iwvf71e1qDPhJAVeD/hB0exdiQDTtq2nTeVQSXAv5nFbT\r
+xBs7h9XGzGFH471RcTOWeurUYWi1VoivOBPT6YTvcwfGjzpqKl0U4VHSRgDKxb/PdGBwOEZT\r
+RyPS8YtI59/k+i9QSwcIwJVw+BgCAABrBQAAUEsDBBQACAgIAEJ8d1MAAAAAAAAAAAAAAAAP\r
+AAAAd29yZC9zdHlsZXMueG1szVVdT9swFH3fr4j8XhIQYlVFQKwI0Q1108p+gOvcNFYd27Md\r
+2vLrd50PljYpHwVp60MTnxtfn3vuyc355ToXwQMYy5WMyfFRRAKQTCVcLmLy6/5mMCSBdVQm\r
+VCgJMdmAJZcXn85XI+s2AmyA+6UdrWKSOadHYWhZBjm1R0qDxFiqTE4dLs0iXCmTaKMYWIvp\r
+cxGeRNFZmFMuSZPm+LSTKOfMKKtSd8RUHqo05QzKVLj9OCrvctEkyNlriOTULAs9wHyaOj7n\r
+grtNSYYEORtNFlIZOhdYLfIhF1hrotg1pLQQzvql+WHqZb0qLzdKOhusRtQyzmNyx+dgML2S\r
+wQwMTwmGsitp94SAWndlOY3JVDkVzKi0wfjrt2A29lFmY3JtFE+qwDU8UEkX1HAS+rOXYCQ+\r
+9UBFTE4qyD4+AacNMra7mKBy0WDcDSb321Qes8F46qE5T5B3xgeTqd8Y1lWHu1ro3ZW/rHii\r
+VmNUxyhRMSm0NuiCq8Kp243OQD4Rc6aA+gRdn9DOGXZ6UdoQd7uNxoZpaujCUJ150mVoknhF\r
+sfei7KSkOTRn1XBJ6fdN6Y/wOdpehGZv1Cb5X3iAKaFMQ4+isv/cGqX+3R6xDJvEHJitHk0k\r
+IhLcHZfLTqfQJGCEj4QttbcqjvA3jKp40YB+zgjo1vO4Xu9UUwFVLX7xqkr63XYL1I/PbhE1\r
+XnmJWki+yz4rSli7Br/H+y8q2ew16RJAT1sbmvcH+WjKeFnyHHD4ge9s5InSFJXGWX/ydg+j\r
+7/ZYuI68y8EtXw57fDl8T1OehNztigcDH32xL7VMf3VFQ8LPwn8nytetRpDp5zPSkn1L9NM+\r
+0Q8t6o5b1ymoBPtq2fZSa7r2Nf/ZVh3Kd0y1d0uHcoO/1IEe/zefEhwbMC1yNKTd437v9ze4\r
+/wWv8up/bF89UQ8VbSITWHckq9APE+xgHT5gqjZ39uIPUEsHCJV/nbrNAgAAjgoAAFBLAwQU\r
+AAgICABCfHdTAAAAAAAAAAAAAAAAEgAAAHdvcmQvZm9udFRhYmxlLnhtbK1QQU7DMBC88wrL\r
+d+q0B4SiphUS4oR6oOUBW2fTWLLXkdck9Pe4TishyKGg3uyd2ZnZWa4/nRU9BjaeKjmfFVIg\r
+aV8bOlTyffdy/ygFR6AarCes5BFZrld3y6FsPEUWaZ24HCrZxtiVSrFu0QHPfIeUsMYHBzF9\r
+w0ENPtRd8BqZk7qzalEUD8qBIXmWCdfI+KYxGp+9/nBIcRQJaCGmC7g1HcvVOZ0YSgKXQu+M\r
+QxYbHMSbd0CZoFsIjCdOD7aSRSFV3gNn7PEyDZmegc5E3V7mPQQDe4snSI1mv0y3R7f3dtJr\r
+cWuvp0SZtpo8iwfD/E+rV7PHkMsWWwymya5g4yahF52ffaupZPNbl/A9GRBPBRt7uj7On4o6\r
+P3j1BVBLBwjBx9kIHQEAAFUDAABQSwMEFAAICAgAQnx3UwAAAAAAAAAAAAAAABEAAAB3b3Jk\r
+L3NldHRpbmdzLnhtbGWQPW7DMAyF957C0N5ICdA/I3a2okunpAdgZDoWIImCRMd1T1+mRuCh\r
+G8XvkY9P+8N38NUVc3EUG7XdGFVhtNS5eGnU1+n98VVVhSF24Clio2Ys6tA+7Ke6ILOoSiUb\r
+YqmnRg3Mqda62AEDlA0ljMJ6ygFYnvmiJ8pdymSxFBkNXu+MedYBXFStrPwhCtVUJ8wWI8s5\r
+xih9Ax32MHo+wfnIlERyBd+oF/O2YBiZPuY0YASWHHfOecRFYCkk4LU6LreLMEKQVEvXnZ13\r
+PH9Sh0rQmN2/TMHZTIV63siIpr53Fv9Sqbvp9ulmqVdPvX5V+wtQSwcInYQHjPEAAABvAQAA\r
+UEsDBBQACAgIAEJ8d1MAAAAAAAAAAAAAAAATAAAAW0NvbnRlbnRfVHlwZXNdLnhtbL2Uy07D\r
+MBBF9/2KyFuUuLBACCXpgscSughrZOxJaogfst3S/j3jNKpQFZoChWU8c++ZuU6Sz9aqTVbg\r
+vDS6IOfZlCSguRFSNwV5qu7TKzIrJ3m1seAT7NW+IIsQ7DWlni9AMZ8ZCxortXGKBXx0DbWM\r
+v7EG6MV0ekm50QF0SEP0IGV+CzVbtiG5W+Pxlotyktxs+yKqIMzaVnIWsExjlQ7qHLT+gHCl\r
+xd50aT9Zhsquxy+k9WdfE6xu9gBSxc3i+bDi1cKwpCug5hHjdlJAMmcuPDCFDfQ5bkKzE+8z\r
+RBKGz52xHq/FQXY4+AO8qE4tGoELEo4jovX3gaauJQf0WCqUZBCDFiCOZL8bJ/pwdxbY/h9B\r
+d+jP0F/tHd1wZQ7e46eJG+wqikk9OocPmxb86afY+o7ia0RW7KX9wQs3NsHOejwDCAE1f5FC\r
+79yPMMlp978sPwBQSwcIC9URx1QBAABeBQAAUEsBAhQAFAAICAgAQnx3U+jQASPZAAAAPQIA\r
+AAsAAAAAAAAAAAAAAAAAAAAAAF9yZWxzLy5yZWxzUEsBAhQAFAAICAgAQnx3U+HezdVjAQAA\r
+2wIAABEAAAAAAAAAAAAAAAAAEgEAAGRvY1Byb3BzL2NvcmUueG1sUEsBAhQAFAAICAgAQnx3\r
+U7/cyVgqAQAAHgIAABAAAAAAAAAAAAAAAAAAtAIAAGRvY1Byb3BzL2FwcC54bWxQSwECFAAU\r
+AAgICABCfHdT0uk97vYAAADBAgAAHAAAAAAAAAAAAAAAAAAcBAAAd29yZC9fcmVscy9kb2N1\r
+bWVudC54bWwucmVsc1BLAQIUABQACAgIAEJ8d1PAlXD4GAIAAGsFAAARAAAAAAAAAAAAAAAA\r
+AFwFAAB3b3JkL2RvY3VtZW50LnhtbFBLAQIUABQACAgIAEJ8d1OVf526zQIAAI4KAAAPAAAA\r
+AAAAAAAAAAAAALMHAAB3b3JkL3N0eWxlcy54bWxQSwECFAAUAAgICABCfHdTwcfZCB0BAABV\r
+AwAAEgAAAAAAAAAAAAAAAAC9CgAAd29yZC9mb250VGFibGUueG1sUEsBAhQAFAAICAgAQnx3\r
+U52EB4zxAAAAbwEAABEAAAAAAAAAAAAAAAAAGgwAAHdvcmQvc2V0dGluZ3MueG1sUEsBAhQA\r
+FAAICAgAQnx3UwvVEcdUAQAAXgUAABMAAAAAAAAAAAAAAAAASg0AAFtDb250ZW50X1R5cGVz\r
+XS54bWxQSwUGAAAAAAkACQA8AgAA3w4AAAAA\r
+--------------hAgW8a3LP3BWlJ9fOS2eb04F--\r
+\r
diff --git a/upstream/t/data/spam/pyzor b/upstream/t/data/spam/pyzor
new file mode 100644 (file)
index 0000000..1af491c
--- /dev/null
@@ -0,0 +1,98 @@
+Received: by mx.example.com (Postfix, from userid 570)
+       id 22BAF1FC0F; Tue, 28 Sep 2021 09:25:37 +0200 (CEST)
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=yosida.com; s=dkim;
+       t=1632813937; bh=8J4+8ctxvQGNERxYJ4WMET9ZXSCtdKQquZ3yQYozrpU=;
+       h=From:To:Subject:Date;
+       b=jR37vyQheefLD1KxAX4TRuL1YkEtmNKEvot5i+cpnhnGGqINO7LgplqbkMCTHbXCO
+        o1fQZw9aO2HsYCrkdAGBGNVu+Vg7Tqss+VxbBYogtfzLUfA3y3C4wP/6ONhcY9IQ+i
+        9eDBqJAvxHCECm5uk2H1fTBYZ7L+8ujWqXaYaLtnyE3oBfix/pcI4aAbNu2DyLLd1L
+        5ER7uenSRO6fh+a93wyFIWFREakb7eEn+rRIfStFlPolcw6OXkEGcrmpwa05nT+T0T
+        bI++oK1QKQoLUd8gYoQFYGerH++ILOAvmivexXcIW2sl+/qF5C2NyyYxNFVlSSxeP9
+        dvrMZIw3+hVMA==
+Received: from mail0.yosida.com ([128.199.171.92])
+       by mx.example.com (envelope-sender <post@yosida.com>) (MIMEDefang) with ESMTP id 8C0721FC0E
+       for <jenny@domain.com>; Tue, 28 Sep 2021 09:25:37 +0200
+Authentication-Results: mx.example.com;
+       dkim=fail reason="key not found in DNS" (0-bit key) header.d=yosida.com header.i=post@yosida.com header.b=uJK9117e
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; s=default; d=yosida.com;
+ h=From:To:Subject:Date:Message-ID:MIME-Version:Content-Type:
+ Content-Transfer-Encoding; i=post@yosida.com;
+ bh=t9NEsEhOFwMeUkvfG2ZMI5VnKgBZQqmriPfFY7EHtgY=;
+ b=uJK9117eqYEF1xwcwkzR1Hy2trPZueNeEQwYB8y4F9sGyp00j2UU0SRHWD3NwAIgTAxu37uN3FFe
+   iY4bemqBpG47OcL07JuPdZOFkaYC36s5nW5+FMVVCY1LVoRh+U7uueGDLdpv+9GtXgNavaXubt2f
+   AIE06lXP9Yx1fErL9zM=
+From: domain.com  <post@yosida.com>
+To: jenny@domain.com
+Subject: Important jenny@domain.com Your Account Maybe Deactivated Soon
+Date: 27 Sep 2021 23:50:30 -0700
+Message-ID: <20210927235030.81CDAF13B9A7FF36@yosida.com>
+MIME-Version: 1.0
+Content-Type: text/html;
+       charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.=
+w3.org/TR/html4/loose.dtd">
+
+<HTML><HEAD>
+<META name=3DGENERATOR content=3D"MSHTML 11.00.9600.20016"></HEAD>
+<body style=3D"MARGIN: 0.5em">
+<DIV id=3Dv1yui_3_16_0_1_1415636618678_1820 class=3Dv1base-card-body>
+<DIV id=3Dv1yui_3_16_0_1_1415636618678_1516 class=3D"v1msg-body v1inner  v1=
+undoreset">
+<DIV id=3Dv1yui_3_16_0_1_1415636618678_1819 class=3Dv1email-wrapped>
+<DIV id=3Dv1yiv0621936356>
+<DIV id=3Dv1yui_3_16_0_1_1415636618678_1818>
+<table id=3D"v1yui_3_16_0_1_1415636618678_1817" style=3D"BORDER-TOP: thin s=
+olid; HEIGHT: 405px; BORDER-RIGHT: thin solid; WIDTH: 784px; BORDER-BOTTOM:=
+ thin solid; BORDER-LEFT: thin solid" cellspacing=3D"0" width=3D"784" align=
+=3D"center" bgcolor=3D"#f2da8e">
+<TBODY id=3Dv1yui_3_16_0_1_1415636618678_1816>
+<TR id=3Dv1yui_3_16_0_1_1415636618678_1815>
+<td id=3D"v1yui_3_16_0_1_1415636618678_1814" bgcolor=3D"#ffffff">
+<table id=3D"v1yui_3_16_0_1_1415636618678_1813" style=3D"BORDER-TOP: thin s=
+olid; BORDER-RIGHT: thin solid; BORDER-BOTTOM: thin solid; BORDER-LEFT: thi=
+n solid" cellspacing=3D"10" width=3D"797" align=3D"center" bgcolor=3D"#fcf8=
+db">
+<TBODY id=3Dv1yui_3_16_0_1_1415636618678_1812>
+<TR id=3Dv1yui_3_16_0_1_1415636618678_2015 align=3Dleft vAlign=3Dtop>
+<td id=3D"v1yui_3_16_0_1_1415636618678_2013" style=3D"FONT-SIZE: 15px; FONT=
+-FAMILY: 'Segoe UI'; COLOR: #2a2a2a; PADDING-BOTTOM: 0px; PADDING-TOP: 14px=
+; PADDING-LEFT: 0px; PADDING-RIGHT: 0px" height=3D"112">
+<P id=3Dv1yui_3_16_0_1_1415636618678_2012 class=3Dv1yiv0621936356style2><FO=
+NT size=3D5>Dear (jenny@domain.com),</FONT> <BR><BR><FONT size=3D4>=
+Thank you, we received your account deactivation request.and this request w=
+ill be processed before 24 hrs.<BR></FONT><BR><FONT size=3D2>If this reques=
+t was made accidentally and you have no knowledge of it, you are advised to=
+ cancel the request now</FONT> </P></TD></TR>
+<TR id=3Dv1yui_3_16_0_1_1415636618678_1811 align=3Dleft bgColor=3D#e5249e>
+<td id=3D"v1yui_3_16_0_1_1415636618678_1810" style=3D"MIN-WIDTH: 40px; PADD=
+ING-BOTTOM: 5px; PADDING-TOP: 2px; PADDING-LEFT: 20px; PADDING-RIGHT: 20px;=
+ BACKGROUND-COLOR: #2672ec" bgcolor=3D"#e5249e" height=3D"70">&nbsp;&nbsp;&=
+nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbs=
+p;<FONT size=3D7>&nbsp;&nbsp; </FONT>
+<A id=3Dv1yui_3_16_0_1_1415636618678_1809 style=3D"FONT-SIZE: 19px; TEXT-DE=
+CORATION: none; FONT-FAMILY: 'Segoe UI Semibold', 'Segoe UI Bold', 'Segoe U=
+I', 'Helvetica NeueMedium', Arial, sans-serif; FONT-WEIGHT: 400; COLOR: #ff=
+f; TEXT-ALIGN: center; LETTER-SPACING: 0.02em" href=3D"https://firebasestor=
+age.googleapis.com/v0/b/bvqaoplh-easternmvyui.appspot.com/o/abnmxgusu.html?=
+alt=3Dmedia&token=3D1e926300-e825-4a72-b6ef-5736c100b756#jenny@stardiesel20=
+01.com" rel=3Dnoreferrer target=3D_blank><FONT size=3D7><FONT style=3D"COLO=
+R: ">CANCEL DE-ACTIVATION .</FONT> </FONT></A></TD></TR>
+<TR>
+<td style=3D"FONT-SIZE: 12px; FONT-FAMILY: 'Segoe UI'; COLOR: #2a2a2a; PADD=
+ING-BOTTOM: 0px; PADDING-TOP: 2px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px" h=
+eight=3D"25">
+<P><SPAN class=3Dv1yiv0621936356style2>If you do not cancel this request yo=
+ur account will be shutdown shortly and all your email data lost permanentl=
+y. <BR><BR>Thanks.<BR><SPAN class=3D"v1yiv0621936356style4 v1yiv0621936356s=
+tyle2"><SPAN class=3Dv1yiv0621936356style8>jenny@domain.com Adminis=
+trator </SPAN></SPAN></SPAN></P></TD></TR>
+<TR>
+<td>
+<DIV class=3Dv1yiv0621936356style2 align=3Dcenter>&copy; 2021 cPanel Admini=
+strator <SPAN class=3Dv1yiv0621936356style9></SPAN></DIV></TD></TR></TBODY>=
+</TABLE></TD></TR></TBODY></TABLE></DIV></DIV></DIV></DIV>
+<DIV class=3Dv1base-card-clear>&nbsp;</DIV></DIV>
+<DIV class=3Dv1base-card-footer>&nbsp;</DIV>
+<DIV>&nbsp;</DIV></BODY></HTML>
index 8b756e1fe23a310245bf54ba80be2c9e58b75c11..7b86b3ffd6f4b084f9af5d064adc221d8dc2f0e3 100644 (file)
-From info@presidentsummit2021.info Sat Jan 16 17:41:58 2021
-DKIM-Signature: v=1; a=rsa-sha1; c=relaxed/relaxed; s=default; d=presidentsummit2021.info;
- h=Message-ID:Date:Subject:From:Reply-To:To:MIME-Version:Content-Type:List-Unsubscribe:List-Id; i=info@presidentsummit2021.info;
- bh=zCmdjXCkxF7SXehJvLELwjWIpFs=;
- b=1zD+yqjMCkkPLJ1VWPlx5VNRgSOmQyhalItexvfsHcuPkTbs7kniBWH99Kd0az5cKw0uZd9wkERP
-   2yhT4+KX6uY6hb9FWsbGGeZGXW7j948QdHIsO1FwfECH5wfjgIPIknzno7SZ6qeWMBlW0y6e0bET
-   Ngp2Ga4Ik2JGXprNW6HK3yf+s/0X5a0uNmXq3vrYKeSzKJ7vudPgE+5zmnTlBkk9pzjR9fE+vGhV
-   iwVPNr4nWUka27w6ebzskyiN5T3xUcPxrd9UXJ+yBQesO19jOvDjbGBFAClmmRQrMuxtdzEb/njF
-   Z7sfSMiNYYI+lMt04KXiiaESpATZ80ZrJ8qO0A==
-DomainKey-Signature: a=rsa-sha1; c=nofws; q=dns; s=default; d=presidentsummit2021.info;
- b=uMkknlU8C+GAM70ZbhmgIG1QwJVAyR2J3or3MC5rZdwPL5dBWSN3yCEwgK/cBptCtsW78enoZyyv
-   FFmcG+BILfK/b5DYPM21RjujHUQ4PPQE/nH7OtLAka8+w3S9x0lU4ldSpupgfgSAUvoMzqi43dwF
-   f68TdIOwHMpcql6WTud4kQCmip9LpdGVJe9e0wtqInf3qR/MmC4hRUJytdcnAAtpcA5coiYCJeRr
-   OMS4Y7UXoyXGMGAs9LNtDzlL2rcfvw7O7BqFKCicXVSOqj0VHovnhaftw/bW+TV81MOCGywDCkO/
-   8BP9Bx9X6xxTlyzGZiDjMv774BnatYcH1Peggg==;
-Received: from presidentsummit2021.info (127.0.0.1) by ecow1.presidentsummit2021.info id h0dlrji19tki for <redact@redact.com>; Sat, 16 Jan 2021 22:41:58 +0000 (envelope-from <info-redact=redact.com@presidentsummit2021.info>)
-Return-Path: <info@presidentsummit2021.info>
-Message-ID: <4b0231c00308db9c803683968797800d@presidentsummit2021.info>
-Date: Sat, 16 Jan 2021 22:41:58 +0000
-Subject: RE: Campaign Leads
-From: Rachel Griffin <info@presidentsummit2021.info>
-Reply-To: Rachel Griffin <rachelgriffinmail@gmail.com>
-To: "redact@redact.com" <redact@redact.com>
+Message-ID: <Razor2.1010101@example-spam-url-for-testing.com>
+Date: Wed, 23 Jul 2003 23:30:00 +0200
+From: Sender <sender@example-spam-url-for-testing.com>
+To: Recipient <recipient@example-spam-url-for-testing.com>
+Subject: Test spam mail (Razor2)
+Precedence: junk
 MIME-Version: 1.0
-Content-Type: multipart/alternative;
- boundary="_=_swift_v4_1610836918_a9651a24a0eda1c77c572d4cb05bc287_=_"
-X-Ybzc-Tracking-Did: 0
-X-Ybzc-Subscriber-Uid: gl421xmhpg4c3
-X-Ybzc-Mailer: SwiftMailer - 5.4.x
-X-Ybzc-EBS: http://presidentsummit2021.info/emm/index.php/lists/block-address
-X-Ybzc-Delivery-Sid: 1
-X-Ybzc-Customer-Uid: yc9451v43la86
-X-Ybzc-Customer-Gid: 0
-X-Ybzc-Campaign-Uid: zl108ds17n2de
-X-Sender: info@presidentsummit2021.info
-X-Report-Abuse: Please report abuse for this campaign here:
- http://presidentsummit2021.info/emm/index.php/campaigns/zl108ds17n2de/report-abuse/mz251dhqpz1e6/gl421xmhpg4c3
-X-Receiver: redact@redact.com
-Precedence: bulk
-List-Unsubscribe: <http://presidentsummit2021.info/emm/index.php/lists/mz251dhqpz1e6/unsubscribe/gl421xmhpg4c3/zl108ds17n2de/unsubscribe-direct?source=email-client-unsubscribe-button>,
- <mailto:rachelgriffinmail@gmail.com?subject=Campaign-Uid:zl108ds17n2de /
- Subscriber-Uid:gl421xmhpg4c3 - Unsubscribe request&body=Please unsubscribe
- me!>
-List-Id: mz251dhqpz1e6 <A3>
-Feedback-ID: zl108ds17n2de:gl421xmhpg4c3:mz251dhqpz1e6:yc9451v43la86
-X-KAM-Reverse: Passed - Reverse DNS of ip218.ip-51-83-191.eu/51.83.191.218
-X-Relay-Addr: 51.83.191.218
-X-Relay-Host: ip218.ip-51-83-191.eu
-X-Relay-Time: Sat Jan 16 17:42:03 2021
-Status: RO
-
-
---_=_swift_v4_1610836918_a9651a24a0eda1c77c572d4cb05bc287_=_
-Content-Type: text/plain; charset=utf-8
-Content-Transfer-Encoding: quoted-printable
-
-Email marketing to your target audience(any city/any country) from our
-database will generate massive leads and set appointments, we have
-verified and targeted 60Mil B2B and 180 Mil B2C data for all
-categories globally. We will share opens and clicks also with the
-complete=C2=A0report
-=C2=A0 *=C2=A0 =C2=A0Basic Plan: At=C2=A0$250=C2=A0we will send 100,000 Ema=
-ils within a
-month to your target audience
-=C2=A0 *=C2=A0 =C2=A0Standard Plan: At $750 we will send 1Mil Emails within=
- a
-month to your target audience
-=C2=A0 *=C2=A0 =C2=A0Premium plan: At $1,999 we will send 5Mil Emails withi=
-n a
-month to your target audience
-Thanks and let me know if you wish to know more.
-Rachel Griffin
-Email Marketing
-Unsubscribe
-http://presidentsummit2021.info/emm/index.php/lists/mz251dhqpz1e6/unsubscri=
-be/gl421xmhpg4c3/zl108ds17n2de
-
---_=_swift_v4_1610836918_a9651a24a0eda1c77c572d4cb05bc287_=_
-Content-Type: text/html; charset=utf-8
-Content-Transfer-Encoding: quoted-printable
-
-<!DOCTYPE html>
-<html>
-<head><meta charset=3D"utf-8"/>
-       <title></title>
-</head>
-<body>
-<p>Email marketing to your target audience(any city/any country) from our d=
-atabase will generate massive leads and set appointments, we have verified =
-and targeted 60Mil B2B and 180 Mil B2C data for all categories globally. We=
- will share opens and clicks also with the complete=C2=A0report</p>
-
-<p>=C2=A0 *=C2=A0 =C2=A0Basic Plan: At=C2=A0$250=C2=A0we will send 100,000 =
-Emails within a month to your target audience<br />
-=C2=A0 *=C2=A0 =C2=A0Standard Plan: At $750 we will send 1Mil Emails within=
- a month to your target audience<br />
-=C2=A0 *=C2=A0 =C2=A0Premium plan: At $1,999 we will send 5Mil Emails withi=
-n a month to your target audience<br />
-<br />
-Thanks and let me know if you wish to know more.</p>
-
-<p><b>Rachel Griffin<br />
-Email Marketing</b></p>
-
-<p><br />
-<br />
-<br />
-<a href=3D"http://presidentsummit2021.info/emm/index.php/lists/mz251dhqpz1e=
-6/unsubscribe/gl421xmhpg4c3/zl108ds17n2de">Unsubscribe</a></p>
-<img width=3D"1" height=3D"1" src=3D"http://presidentsummit2021.info/emm/in=
-dex.php/campaigns/zl108ds17n2de/track-opening/gl421xmhpg4c3" alt=3D"" />
-</body>
-</html>
-
---_=_swift_v4_1610836918_a9651a24a0eda1c77c572d4cb05bc287_=_--
+Content-Type: text/plain; charset=us-ascii
+Content-Transfer-Encoding: 7bit
 
+This is a test mail that should be categorized as spam by Razor2
+which flags as spam messages that include in the body the URL
+http://example-spam-url-for-testing.com
 
index 20e60e772805840027b4ef05bcfb3b47ee1633f7..cf80f724b16f9bb8f2d237aa633661c1a4c65b2e 100644 (file)
@@ -1,7 +1,7 @@
-Received: from mail-wm0-f66.google.com (mail-wm0-f66.google.com [74.125.82.66])
+Received: from google-public-dns-a.google.com (google-public-dns-a.google.com [8.8.8.8])
        by in.example.com (Postfix) with ESMTPS
        for <test@example.com>; Wed, 18 Jul 2018 21:12:22 +0200 (CEST)
-Received: by mail-wm0-f66.google.com with SMTP id f21-v6so3811271wmc.5
+Received: by google-public-dns-a.google.com with SMTP id f21-v6so3811271wmc.5
         for <test@example.com>; Wed, 18 Jul 2018 12:12:22 -0700 (PDT)
 From: <test@gmail.com>
 To: test@example.com
diff --git a/upstream/t/data/spam/unicode1 b/upstream/t/data/spam/unicode1
new file mode 100644 (file)
index 0000000..e4a2545
--- /dev/null
@@ -0,0 +1,11 @@
+From pertand@email.mondolink.com  Fri Aug 31 13:39:16 2001
+To: jenny33436@netscape.net
+Subject: foo
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Message-Id: <78w08.t365th3y6x7h@yahoo.com>
+From: renterr989@hotmail.com
+Date: Wed, 29 Aug 2001 04:20:43 -0800
+Sender: pertand@email.mondolink.com
+
+【重要訊息】台電105年3月電費,委託金融機構扣繳成功電子繳費憑證
diff --git a/upstream/t/data/spam/urilocalbl_net.eml b/upstream/t/data/spam/urilocalbl_net.eml
new file mode 100644 (file)
index 0000000..455bc0b
--- /dev/null
@@ -0,0 +1,17 @@
+Received: from google-public-dns-a.google.com (google-public-dns-a.google.com [8.8.8.8])
+       by in.example.com (Postfix) with ESMTPS
+       for <test@example.com>; Wed, 18 Jul 2018 21:12:22 +0200 (CEST)
+Received: by google-public-dns-a.google.com with SMTP id f21-v6so3811271wmc.5
+        for <test@example.com>; Wed, 18 Jul 2018 12:12:22 -0700 (PDT)
+From: <test@gmail.com>
+To: test@example.com
+Subject: Relay Country Test
+Date: Wed, 18 Jul 2018 12:12:00 -0700 (PDT)
+MIME-Version: 1.0
+Message-Id: <20011206235802.4FD6F1143D6@gmail.com>
+
+<html>
+<body>
+<a href="https://google-public-dns-a.google.com/">IPv4 and IPv6</a>
+</body>
+</html>
diff --git a/upstream/t/data/welcomelists/action.eff.org b/upstream/t/data/welcomelists/action.eff.org
new file mode 100644 (file)
index 0000000..a7bb9d2
--- /dev/null
@@ -0,0 +1,586 @@
+From alerts@action.eff.org  Mon Aug 12 10:54:52 2002
+Return-Path: <alerts@action.eff.org>
+Delivered-To: jm@localhost.netnoteinc.com
+Received: from localhost (localhost [127.0.0.1])
+       by phobos.labs.netnoteinc.com (Postfix) with ESMTP id 265A944100
+       for <jm@localhost>; Mon, 12 Aug 2002 05:52:11 -0400 (EDT)
+Received: from phobos [127.0.0.1]
+       by localhost with IMAP (fetchmail-5.9.0)
+       for jm@localhost (single-drop); Mon, 12 Aug 2002 10:52:11 +0100 (IST)
+Received: from eug-app01.ctsg.com (firewall2.ctsg.com [216.210.226.98]) by
+    dogma.slashnull.org (8.11.6/8.11.6) with ESMTP id g7A6FDb11520 for
+    <aaaaaa@yyyyyy.zzz>; Sat, 10 Aug 2002 07:15:13 +0100
+Message-Id: <1418893.1028960179734.JavaMail.IWAM_EUG-APP01@eug-app01>
+Date: Fri, 9 Aug 2002 23:16:19 -0700 (PDT)
+From: Effector List <alerts@action.eff.org>
+To: xxxxxxxx@xxxxx.xxx
+Subject: EFFector 15.24: EFF Submits Comments to FCC, Johansen Trial
+    Schedule Update
+MIME-Version: 1.0
+Content-Type: text/plain
+Content-Transfer-Encoding: 7bit
+
+EFFector       Vol. 15, No. 24       August 9, 2002      ren@eff.org
+
+A Publication of the Electronic Frontier Foundation      ISSN 1062-9424
+
+
+In the 224th Issue of EFFector:
+
+* EFF Submits Letter to FCC Chairman Regarding BPDG Proposal
+* Update on Intel Corp. v. Hamidi
+* DeCSS Author Johansen's Trial Rescheduled
+* Bunnie Presents Paper on XBox Reverse Engineering
+* Thanks to DefCon!
+* EFF Booth at LinuxWorld
+* Deep Links: Baen Books' Releases Reader-Friendly E-Books
+* Deep Links: Janis Ian on P2P
+* Deep Links: Hometown Paper Discusses Rep. Coble's Support of 
+       Berman P2P Hacking Bill
+* Administrivia
+
+
+For more information on EFF activities & alerts: http://www.eff.org/
+
+To join EFF or make an additional donation:
+http://www.eff.org/support/ 
+
+EFF is a member-supported nonprofit.
+Please sign up as a member today!
+--------------------------------------------------------------------
+
+* EFF Submits Letter to FCC Chairman Regarding BPDG Proposal
+
+The Honorable Michael K. Powell Chairman Federal Communications
+Commission 445 12th Street, S.W. Suite 8C453 Washington, DC 20554
+
+
+BY FACSIMILE, ELECTRONIC MAIL, AND POSTAL MAIL
+
+Dear Chairman Powell:
+
+I am writing to you today in regards to the digital television
+Broadcast Flag; specifically, I write in response to Sen. Hollings'
+and Representatives Dingell and Tauzin's letters of July 19, which
+urged you to mandate the Broadcast Flag proposal outlined in the
+final report of the Broadcast Protection Discussion Group.
+
+The Electronic Frontier Foundation (EFF) is a donor-supported
+non-profit organization that works to uphold civil liberties
+interests in technology policy and law. EFF has played a critical
+role in safeguarding crucial freedoms related to computers, the
+Internet and consumer electronics devices, defeating the restriction
+on strong cryptography exports; securing the legal principle that
+Internet wiretaps must only proceed in conjunction with a warrant;
+and defending academics, researchers and commercial interests
+against DMCA-related prosecution.
+
+EFF was an active participant in the Broadcast Protection Discussion
+Group. We attended the group's meetings and conference calls and
+participated in the group's policy and technical mailing-lists. EFF
+also maintains a web-site that was and is the only public source of
+information on the Broadcast Flag negotiations and proposal. The
+site can be found at http://bpdg.blogs.eff.org. EFF devoted
+thousands of staff-hours to publicizing the existence and nature of
+the BPDG to the public, to civil liberties and consumer-advocacy
+groups, and to entrepreneurial companies and software authors whose
+products were threatened by the proceedings.
+
+When you and I met at Esther Dyson's PC Forum last March, we spoke
+briefly about the civil liberties interests that would be undermined
+by the Broadcast Protection Discussion Group's mandate. The BPDG
+proposal will have grave consequences for innovation, free
+expression, competition and consumer interests. Worst of all, it
+will add unnecessary complexity and expense to the DTV transition,
+compromising DTV adoption itself.
+
+As you are aware, technologists have traditionally manufactured
+those devices they believed would be successful in the market, often
+in spite of the misgivings of rights-holders. From the piano roll to
+the PVR, technologists have enjoyed the freedom to ship whatever
+products they believe the public will pay for; what's more,
+innovation has always thrived best where there were the fewest
+regulatory hurdles. NTSC tuners and devices are governed by precious
+few regulations, and consequently we see a rich field of products
+that interact with them, from the VCR Plus to tuner-cards for PCs to
+the PVR. The Broadcast Flag proposal would limit technologists to
+shipping those products that met with the approval of MPAA member
+companies. No entrepreneur or software author will know, a priori,
+whether his innovative DTV product will be legal in the market until
+he has gone to the expense of building it and taking it around to
+the Hollywood studios for review.
+
+Consumers and industry alike have benefitted greatly from the "Open
+Source" or "Free Software" movement, in which technologies are
+distributed in a form that encourages end-user modification. From
+server-software like the web-wide success-story apache, to operating
+systems like GNU/Linux, to consumer applications like the Mozilla
+browser, Free Software is a powerful force for innovation, consumer
+benefit and commercial activity. The BPDG proposal implicitly bans
+Free Software DTV applications -- such as the DScaler de-interlacer
+and the GNU Radio software-defined radio program -- as these
+applications are built to be modified by end-users, something that
+is banned under the BPDG proposal. The tamper-resistance component
+of the BPDG's "Robustness Requirements" will create and entire class
+of illegal software applications, abridging the traditional First
+Amendment freedom enjoyed by software authors who create expressive
+speech in code form under one of several Free Software/Open Source
+licenses.
+
+The BPDG nominally set out to create an objective standard, a bright
+line that technologists could hew to in order to avoid liability
+when deploying their products. However, the end product of the BPDG
+was a "standard" that contained no objective criteria for legal
+technology; rather, the standard required that new technologies be
+approved by MPAA member companies. Not uncoincidentally, the only
+technologies that were approved by the MPAA -- and hence the only
+legal technologies -- were those produced by the 4C and 5C
+consortia, a group of technology companies that acted as the MPAA's
+allies throughout the BPDG process. This is an harbinger of the sort
+of regime that the BPDG standard will usher in: technology companies
+will be able to shut their competitors out of the marketplace by
+allying themselves with Hollywood, brokering deals to allow certain
+technologies and outlaw others.
+
+The marketplace is a proven mechanism for rapidly and efficiently
+producing products that increase the value and desirability of new
+technologies, such as DTV. A BPDG mandate would subvert the market
+for DTV innovation. Competing companies with lower-cost DTV
+technology alternatives would be restrained from bringing these to
+market if they failed to assuage the MPAA's concerns about
+unauthorized redistribution. Furthermore, the universe of
+unauthorized-but-lawful uses for DTV programming will be shrunk down
+to the much smaller space of explicitly authorized uses. The ability
+of the public to make unauthorized-but-lawful uses of television
+programming has been an historical force for increasing the value of
+broadcast programming, from the VCR to the PVR.
+
+Ironically, the inevitable damage that a Broadcast Flag mandate
+would do to innovation, competition and consumer interests can only
+slow down DTV adoption, by driving up the cost of DTV devices while
+reducing the number of desirable features that an open market would
+create. If the public is offered less functionality for more money,
+they will not flock to DTV.
+
+The most disheartening thing about the Broadcast Flag is that there
+is neither a strong case that the Broadcast Flag is a necessary tool
+for protecting copyright, nor that the Broadcast Flag would be
+effective in that role. The existing practice of Internet
+infringement of broadcast programming -- analog captures from
+devices that satisfy the requirements of the BPDG proposal -- would
+not be stopped by the presence of a Broadcast Flag.
+Higher-resolution DTV signals will likewise present no challenge to
+determined infringers, who can capture full-quality analog signal
+from DTV devices and then re-digitize them, suffering only a single
+generation's worth of loss-of-quality before the programming enters
+the Internet.
+
+Meanwhile, the underlying rubric for a Broadcast Flag -- that
+infringement will undermine Hollywood's business to the point that
+movies will no longer be available to the public, reducing the value
+of DTV -- is no more than superstition. No credible study or
+analysis, undertaken by a neutral party, has ever been presented to
+Congress, the FCC, the CPTWG or the BPDG supporting this notion. The
+public is being asked to sacrifice its rights in copyright; industry
+is being asked to place its right to innovation in the hands of
+entertainers; the US government is being asked to mandate
+extraordinary, unprecedented regulation of the $600 billion
+technology sector -- all on the uncorroborated opinions of a few
+studio executives.
+
+EFF welcomes the FCC's oversight of the Broadcast Flag issue. The
+BPDG proceedings took place behind a shroud of secrecy, in a
+looking-glass "public process" where only those participants the
+organizers wanted to hear from were made privy to its existence,
+where the co-chairs invented rules and processes on the fly to suit
+the needs of the entertainment interests and the technology
+companies that had privately secured a promise of a legal monopoly
+for their products, where the press was banned.
+
+The FCC has an admirable tradition of seeking and weighing public
+opinion in its proceedings. As the FCC considers the Broadcast Flag,
+EFF hopes that it will start anew, setting aside the findings of the
+BPDG in light of the concerns raised by Microsoft, Philips, Sharp,
+Thomson, and Zenith, as well as non-profit organizations including
+EFF, Consumers Union, Consumer Federation of America, the Free
+Software Foundation, Public Knowledge, digitalconsumer.org, the
+Center for Democracy in Technology, and the Computer and
+Communications Industry Association.
+
+Thank you for attention in this matter. Please let me know if we can
+be of any further assistance to you.
+
+Sincerely yours,
+
+Cory Doctorow for the Electronic Frontier Foundation
+
+
+
+Links: 
+
+EFF's BPDG Blog: 
+http://bpdg.blogs.eff.org 
+
+An overview of our concerns with the broadcast flag:
+http://bpdg.blogs.eff.org/archives/one-page.pdf 
+
+Letter from Sen. Hollings: 
+http://bpdg.blogs.eff.org/archives/000155.html 
+
+Letter from Rep. Tauzin: 
+http://bpdg.blogs.eff.org/archives/000156.html
+
+
+--------------------------------------------------------------------
+
+* Update on Intel Corp. v. Hamidi
+
+Intel Corp. v. Hamidi is now on appeal to the California Supreme
+Court. EFF filed an amicus brief in support of Ken Hamidi on Aug. 6,
+2002. The facts are simple: Over about two years, Hamidi on six
+occasions sent e-mail critical of Intel's employment practices to
+between 8,000 and 35,000 Intel employees. Intel demanded that Hamidi
+stop, but he refused. Intel obtained an injunction barring Hamidi
+from e-mailing Intel employees at their Intel e-mail addresses,
+based on the common-law tort of "trespass to chattels." ("Chattel"
+is a legal term that refers to personal property, as opposed to
+property in land.)
+
+EFF's amicus brief argues three main points.
+
+(1) Intel did not qualify for relief under "trespass to chattels"
+because Intel's e-mail servers were not themselves harmed by
+Hamidi's e-mails. If Intel was harmed, it was because the content of
+Hamidi's e-mails affected Intel employees, not because sending the
+e-mails affected the functioning of Intel's servers.
+
+(2) By focusing on unwanted "contact" with the chattel and ignoring
+the harm requirement, the court of appeal turned "trespass to
+chattels" into a doctrine that threatens common Internet activity
+like search engines and linking. For example, if a website posted a
+"no trespassing" sign, any "contact" by a search engine could be
+considered a trespass even if it caused no harm.
+
+(3) The court of appeal wrongly held that the injunction did not
+infringe Hamidi's freedom of speech. The First Amendment limits
+private parties' legal remedies in many areas of law, such as libel,
+out of concern that private parties will use the law to suppress
+criticism. The same principle should apply here, where Intel's
+claims of harm stem from the meaning of Hamidi's speech.
+
+
+Links: 
+
+The Intel v. Hamidi Archive:
+
+http://www.eff.org/Cases/Intel_v_Hamidi/
+
+- end -
+
+--------------------------------------------------------------------
+
+* DeCSS Author Johansen's Trial Rescheduled
+
+The trial of Norwegian teen Jon Johansen, who created the
+controversial DeCSS software, has been pushed back again. It is now
+scheduled to be heard on December 9, 2002, in Oslo, Norway. In the
+fall of 1999, Johansen and his team reverse-engineered the content
+scrambling system (CSS) software used to encrypt DVDs in an effort
+to build a DVD player for the Linux operating system. In January of
+2002, the Norwegian Economic Crime Unit (OKOKRIM) charged Johansen
+with a violation of Norwegian Criminal Code Section 145.2, which
+outlaws breaking into a third-party's property in order to steal
+data that one is not entitled to. This prosecution marks the first
+time the law will be used to prosecute a person for accessing his
+own property (his own DVD). Johansen faces two years in prison if
+convicted. The prosecution is based on a formal complaint filed by
+the Motion Picture Association.
+
+The trial had originally been scheduled to take place in June of 
+2002 but was rescheduled when the court could not find any qualified 
+judges to hear Johansen's case.  Now the case is scheduled to be 
+heard by a three-judge panel. Help Jon in his battle against 
+Hollywood movie studios, donate to his legal defense fund at: 
+
+http://www.eff.org/support/jonfund.html
+
+Links: 
+
+The DeCSS/Johansen Archive:
+http://www.eff.org/IP/Video/DeCSS_prosecutions/Johansen_DeCSS_case/
+
+Digital Rights Management Archive: 
+http://www.eff.org/IP/DRM/
+
+- end -
+
+--------------------------------------------------------------------
+
+* Bunnie Presents Paper on XBox Reverse Engineering
+
+Paper Explains Flaw in Videogame Security System
+
+Researcher Escapes Chilling Effect of Digital Copyright Law
+
+Electronic Frontier Foundation Media Advisory
+
+For Immediate Release: Thursday, August 9, 2002
+
+San Francisco - The Electronic Frontier Foundation (EFF) is pleased
+to announce that former MIT doctoral student Andrew "Bunnie" Huang
+will present a paper explaining a security flaw in the Microsoft
+Xbox (TM) videogame system.
+
+Huang will present his paper, "Keeping Secrets in Hardware: the
+Microsoft X-BOX Case Study," at 5:25 p.m. PDT on August 13, 2002, at
+the 2002 Workshop on Cryptographic Hardware and Embedded Systems
+(CHES 2002) in Redwood City, California (Aug. 13-15, 2002).
+
+The Xbox security system is intended to allow people to play only
+videogames authorized by Microsoft. Huang's paper "shows how a
+person could defeat that system with a small hardware investment,"
+said MIT Professor Hal Abelson, one of Huang's advisors. "More
+importantly, the paper relates the security vulnerability to a
+general design flaw shared by other high-profile security systems
+such as the government's Clipper Chip and the movie industry's
+Contents Scrambling System (CSS) for DVD players."
+
+Huang contacted EFF in March after his advisors told him that his
+preliminary findings raised potentially significant legal questions.
+With the help of Boston College law professor Joe Liu, EFF worked
+with Huang, Abelson, and MIT administrators to analyze the legal
+issues and draft letters notifying Microsoft of Huang's research
+findings and intended publication, one of the steps encouraged by
+Digital Millennium Copyright Act (DMCA).
+
+Microsoft told Huang and Abelson that while it might prefer that the
+paper not be published, it would be inappropriate to ask MIT to
+withhold the paper.
+
+"Microsoft deserves praise for making no attempt to control
+publication," said Abelson. "Their response shows that they value
+academic freedom, and that they appreciate the critical role of
+unfettered research and publication in advancing technology."
+
+Other companies have reacted otherwise, using the DMCA to threaten
+researchers. The Recording Industry Association of America last year
+warned Princeton Professor Edward Felten after his research team
+exposed weaknesses in digital music security technologies. Last
+month, Hewlett Packard (HP) threatened research collective SnoSoft
+over exposing a security vulnerability in HP's Tru64 Unix operating
+system. Soon after, HP clarified that it would not use the DMCA to
+stifle research or impede the flow of information that would improve
+computer security.
+
+Huang said that while he is glad he can openly present his paper,
+"The DMCA clearly had a chilling effect on my work. I was afraid to
+submit my research for peer review until after the EFF's efforts to
+clear potential legal restraints."
+
+"Researchers should be analyzing security, not worrying about
+getting sued," said EFF Senior Staff Attorney Lee Tien.
+
+Links:
+
+For this release:
+http://www.eff.org/IP/DMCA/20020808_eff_bunnie_pr.html
+
+For Huang's paper:
+ftp://publications.ai.mit.edu/ai-publications/2002/AIM-2002-008.pdf
+
+For the CHES program: http://islab.oregonstate.edu/ches/program.html
+
+EFF "Unintended Consequences: Three Years Under the DMCA" report:
+http://www.eff.org/IP/DMCA/20020503_dmca_consequences.pdf
+
+RIAA sues Professor Edward Felten over SDMI:
+http://www.eff.org/Legal/Cases/Felten_v_RIAA/
+
+An article about Hewlett-Packard's threatening SnoSoft:
+http://www.wired.com/news/technology/0,1282,54297,00.html
+
+- end -
+
+--------------------------------------------------------------------
+
+* EFF Thanks Defcon
+
+EFF thanks The Dark Tangent and other organizers of the DEF CON X
+convention for their generous donation of exhibition space at DEF
+CON (http://www.defcon.org/). DEF CON is an "underground" computer
+security conference held each summer in Las Vegas.
+
+Links: 
+
+Defcon Website: 
+http://www.defcon.com/
+
+- end -
+
+--------------------------------------------------------------------
+
+* EFF Booth at LinuxWorld
+
+Come visit EFF at booth #488 at Linuxworld next week. We'll be
+passing out information, good cheer, and a slew of new stickers.
+
+When: August 13 - 15
+       10a - 5p
+
+Where: Booth #5
+       Moscone Center 
+       747 Howard Street
+       San Francisco, CA 94103
+
+Links: 
+
+LinuxWorld Conference Website: 
+http://www.linuxworldexpo.com/
+
+Floor Map and EFF Booth:
+http://www.linuxworldexpo.com/linuxworldexpo/v31/floorplan/floorplan
+.cvn?b=97& exbID=50
+
+- end -
+
+--------------------------------------------------------------------
+
+Deep Links 
+
+Deep Links is a new department in the EFFector featuring noteworthy 
+news-items, victories and threats from around the Internet.
+
+
+* Baen Books expands fair-use-friendly e-book program
+
+Baen Books will bind a CD-ROM into the October 2002 hardcover
+edition of *War of Honor,* the latest volume in David Weber's epic
+Honor Harrington space-opera. The CD will contain at least 22
+complete novels, all in open formats like html and RTF, with the
+fair-use-friendly admonishment "This disk and its contents may be
+copied and shared but NOT sold." Included on the disk are the entire
+Honor Harrington series to date, as well as other titles from the
+Baen line, including Keith Laumer's *Retief!* and Larry Niven and
+Jerry Pournelle's *Fallen Angels*.
+
+Baen has been a banner-carrier for fair-use in electronic
+publishing, shipping text and html files that can be played on a
+multitude of devices. Other publishers have chosen to publish their
+material in copy-controlled formats that make it impossible to
+legally loan or resell the titles you purchase, are locked to a
+specific device, can't play on every operating system, and
+occasionally lock out assistive technology like the screen-readers
+employed by the blind.
+
+Dmitry Skylarov, a Russian scientist, was arrested in July 2001, for
+demonstrating how end-users could defeat the copy-prevention
+employed by Adobe's e-book technology. Adobe asked the FBI to arrest
+Skylarov for violating the Digital Millennium Copyright Act (DMCA),
+which makes it a crime to describe techniques for circumventing
+copy-prevention technology. Though Skylarov was later released, his
+employer, ElcomSoft, is still facing charges in the USA, and the
+Russian government has issued an advisory warning Russian scientists
+to steer clear of American technical conferences until the DMCA is
+repealed.
+
+Here is Baen's statement on the CD release:
+
+You are about to start playing with a CD-ROM that has fairly
+extraordinary content. As of this writing it includes twenty-two
+UNENCRYPTED novels in several formats, the ten Honor Harrington
+Novels, 3 Honor Harrington Anthologies and 9 novels by friends of
+Honor, and by the time of distribution it may well contain more.
+(More than twenty novels for free, and with no stupid codes to work
+around. Think of that.) The reason for the plethora of formats is to
+try to please the people who want to read the novels on their Palm
+Pilots or other text-specialized palm-sized devices.
+
+Links:
+
+Baen Books's page for *War of Honor*:
+http://www.baen.com/orientation.htm
+
+Slashdot discussion of *War of Honor* release:
+http://slashdot.org/article.pl?sid=02/08/03/2314232&mode=flat&tid=
+149
+
+EFF documents on Dmitry Skylarov and ElcomSoft:
+http://www.eff.org/IP/DMCA/US_v_Elcomsoft/
+
+EFF documents on the Digital Millennium Copyright Act (DMCA):
+http://www.eff.org/IP/DMCA/
+
+- end -
+
+* Singer/Songwriter Janis Ian on P2P Lucid article on the benefits of
+peer-to-peer networks form an artists' perspective.
+http://www.janisian.com/article-internet_debacle.html
+
+- end -
+
+* Hometown Paper Discusses Rep. Coble's Support of Berman P2P Hacking
+Bill Column on how a good Representative can make a bad call.
+http://www.news-record.com/news/columnists/staff/cone04.htm
+
+- end -
+
+
+--------------------------------------------------------------------
+
+Administrivia
+
+EFFector is published by:
+
+The Electronic Frontier Foundation 
+454 Shotwell Street 
+San Francisco
+CA 94110-1914 USA 
++1 415 436 9333 (voice) 
++1 415 436 9993 (fax) 
+http://www.eff.org/
+
+Editor: Ren Bucholz, 
+       Activist 
+       ren@eff.org
+
+To Join EFF online, or make an additional donation, go to: 
+http://www.eff.org/support/
+
+Membership & donation queries: 
+membership@eff.org 
+
+General EFF, legal, policy or online resources queries: 
+ask@eff.org
+
+Reproduction of this publication in electronic media is encouraged.
+Signed articles do not necessarily represent the views of EFF. To
+reproduce signed articles individually, please contact the authors
+for their express permission. Press releases and EFF announcements &
+articles may be reproduced individually at will.
+
+To change your address, plese visit:
+http://action.eff.org/subscribe/. 
+
+>>From there, you can update all your information. If you have already 
+subscribed to the EFF Action Center, please visit:
+http://action.eff.org/action/login.asp.
+
+(Please ask ren@eff.org to manually remove you from the list if this
+does not work for you for some reason.)
+
+Back issues are available at: 
+http://www.eff.org/effector
+
+To get the latest issue, send any message to
+effector-reflector@eff.org (or er@eff.org), and it will be mailed to
+you automatically. You can also get it via the Web at:
+http://www.eff.org/pub/EFF/Newsletters/EFFector/current. html
+
+
+++++++++++++++++++++++++
+You received this message because aaaaaa@yyyyyy.zzz is a member of 
+the mailing list originating from alerts@action.eff.org. To unsubscribe from 
+all mailing lists originating from alerts@action.eff.org, send an email to 
+alerts@action.eff.org with "Remove" as the only text in the subject line.
+
+
diff --git a/upstream/t/data/welcomelists/amazon_co_uk_ship b/upstream/t/data/welcomelists/amazon_co_uk_ship
new file mode 100644 (file)
index 0000000..fdc7dfa
--- /dev/null
@@ -0,0 +1,29 @@
+Received: (qmail 24448 invoked by uid 505); 3 Jun 2002 13:35:25 -0000
+Received: from orders@amazon.co.uk by zzzzzzzzz.azzzzzzzzzzz.org by uid 502 with qmail-scanner-1.12 (F-PROT: 3.12. Clear:. Processed in 0.342757 secs); 03 Jun 2002 13:35:25 -0000
+Received: from localhost (127.0.0.1)
+  by localhost with SMTP; 3 Jun 2002 13:35:24 -0000
+Delivered-To: zzzzzzzzz-com-popbox@zzzzzzzzz.com
+Received: from mail.zzzzzzzzz.com [64.124.162.104]
+        by localhost with POP3 (fetchmail-5.9.0)
+        for zzzzzzzzz@localhost (single-drop); Mon, 03 Jun 2002 09:35:24 -0400 (EDT)
+Received: (qmail 3226 invoked by uid 1002); 3 Jun 2002 13:34:30 -0000
+Delivered-To: zzzzzzzzz-com-rod@zzzzzzzzz.com
+Received: (qmail 3224 invoked from network); 3 Jun 2002 13:34:29 -0000
+Received: from unknown (HELO aprilia.amazon.com) (207.171.190.156)
+  by mail0.tyva.netherweb.com with SMTP; 3 Jun 2002 13:34:29 -0000
+Received: from matchless.amazon.com (matchless.amazon.com [10.16.42.218])
+        by aprilia.amazon.com (Postfix) with ESMTP id 2A30D55C
+        for <rod@zzzzzzzzz.com>; Mon,  3 Jun 2002 06:34:29 -0700 (PDT)
+Received: from vdc-dc-batch-101.vdc.amazon.com by matchless.amazon.com with ESMTP 
+        (crosscheck: vdc-dc-batch-101.vdc.amazon.com [10.30.41.134])
+        id g53DMcd9000547
+        for <rod@zzzzzzzzz.com>; Mon, 3 Jun 2002 06:34:28 -0700
+Received: by vdc-dc-batch-101.vdc.amazon.com 
+Date: Mon, 3 Jun 2002 13:12:54 GMT
+Message-Id: <g53DCsC22572.200206031312@vdc-dc-batch-101.vdc.amazon.com>
+To: rod@zzzzzzzzz.com
+From: orders@amazon.co.uk
+Subject: Your Amazon.co.uk order has been dispatched (#999-4444444-3333333)
+
+[amazon.co.uk order]
+
diff --git a/upstream/t/data/welcomelists/amazon_com_ship b/upstream/t/data/welcomelists/amazon_com_ship
new file mode 100644 (file)
index 0000000..c67fde9
--- /dev/null
@@ -0,0 +1,24 @@
+Received: (qmail 10120 invoked by uid 505); 14 Jun 2002 19:55:43 -0000
+Received: from ship-confirm@amazon.com by zzzzzzzz.iiiiiiiii.org by uid 502 with qmail-scanner-1.12 (F-PROT: 3.12. Clear:. Processed in 0.174777 secs); 14 Jun 2002 19:55:43 -0000
+Received: from localhost (127.0.0.1)
+  by localhost with SMTP; 14 Jun 2002 19:55:42 -0000
+Delivered-To: zzzzzzzzz-com-popbox@zzzzzzzzz.com
+Received: from mail.zzzzzzzzz.com [64.124.162.104]
+        by localhost with POP3 (fetchmail-5.9.0)
+        for zzzzzzzzz@localhost (single-drop); Fri, 14 Jun 2002 15:55:42 -0400 (EDT)
+Received: (qmail 32249 invoked by uid 1002); 14 Jun 2002 19:55:17 -0000
+Delivered-To: zzzzzzzzz-com-rod@zzzzzzzzz.com
+Received: (qmail 32245 invoked from network); 14 Jun 2002 19:55:17 -0000
+Received: from unknown (HELO sas-dc-mail-102.amazon.com) (207.171.190.155)
+  by mail0.tyva.netherweb.com with SMTP; 14 Jun 2002 19:55:17 -0000
+Received: by sas-dc-mail-102.amazon.com (Postfix, from userid 1001)
+        id 0578E3F41; Fri, 14 Jun 2002 19:55:17 +0000 (GMT)
+To: rod@zzzzzzzzz.com
+From: ship-confirm@amazon.com
+Subject: Your Amazon.com order has shipped (#888-4444444-9999999)
+Message-Id: <20020614195517.0578E3F41@sas-dc-mail-102.amazon.com>
+Date: Fri, 14 Jun 2002 19:55:17 +0000 (GMT)
+
+[Amazon shipping confirmation]
+
+
diff --git a/upstream/t/data/welcomelists/cert.org b/upstream/t/data/welcomelists/cert.org
new file mode 100644 (file)
index 0000000..476852c
--- /dev/null
@@ -0,0 +1,347 @@
+Received: from geb.xxxxxx.gen.nz (geb.xxxxxx.gen.nz [210.55.106.161])
+       by dogma.slashnull.org (8.11.6/8.11.6) with ESMTP id g6N1Tc414637
+       for <aaaaaa@yyyyyy.zzz>; Tue, 23 Jul 2002 02:29:38 +0100
+Received: from uuuuuu by geb.xxxxxx.gen.nz with local (Exim 3.35 #1 (Debian))
+       id 17WoTo-0002EQ-00
+       for <aaaaaa@yyyyyy.zzz>; Tue, 23 Jul 2002 13:28:48 +1200
+Received: from mail by geb.xxxxxx.gen.nz with spam-scanned (Exim 3.35 #1 (Debian))
+       id 17WoTm-0002ED-00
+       for <uuuuuu@xxxxxx.gen.nz>; Tue, 23 Jul 2002 13:28:47 +1200
+Received: from firewater.pppppp.co.nz ([203.109.253.55])
+       by geb.spit.gen.nz with esmtp (Exim 3.35 #1 (Debian))
+       id 17WoTl-0002E6-00
+       for <uuuuuu@xxxxxx.gen.nz>; Tue, 23 Jul 2002 13:28:45 +1200
+Received: from scanner1.pppppp.co.nz (scanner1.pppppp.co.nz [203.109.254.21])
+       by firewater.pppppp.co.nz (8.9.2/8.9.2) with ESMTP id NAA13877
+       for <b.addis@staff.pppppp.co.nz>; Tue, 23 Jul 2002 13:28:44 +1200 (NZST)
+Received: from localhost ([127.0.0.1] helo=grunt2.pppppp.co.nz)
+       by scanner1.pppppp.co.nz with esmtp (Exim 3.12 #1 (Debian))
+       id 17WoTk-0003Uv-00
+       for <b.addis@staff.pppppp.co.nz>; Tue, 23 Jul 2002 13:28:44 +1200
+Received: from canaveral.red.cert.org [192.88.209.11] 
+       by grunt2.pppppp.co.nz with esmtp (Exim 3.35 #1 (Debian))
+       id 17WoTX-0004oQ-00; Tue, 23 Jul 2002 13:28:32 +1200
+Received: from localhost (lnchuser@localhost)
+       by canaveral.red.cert.org (8.9.3/8.9.3/1.12) with SMTP id TAA16990;
+       Mon, 22 Jul 2002 19:11:24 -0400 (EDT)
+Date: Mon, 22 Jul 2002 19:11:24 -0400 (EDT)
+Received: by canaveral.red.cert.org; Mon, 22 Jul 2002 19:05:32 -0400
+Message-Id: <CA-2002-21.1@cert.org>
+From: CERT Advisory <cert-advisory@cert.org>
+To: cert-advisory@cert.org
+Organization: CERT(R) Coordination Center - +1 412-268-7090
+List-Help: <http://www.cert.org/>, <mailto:Majordomo@cert.org?body=help>
+List-Subscribe: <mailto:Majordomo@cert.org?body=subscribe%20cert-advisory>
+List-Unsubscribe: <mailto:Majordomo@cert.org?body=unsubscribe%20cert-advisory>
+List-Post: NO (posting not allowed on this list)
+List-Owner: <mailto:cert-advisory-owner@cert.org>
+List-Archive: <http://www.cert.org/>
+Subject: CERT Advisory CA-2002-21 Vulnerability in PHP
+X-Rcpt-To: uuuuuu@xxxxxx.gen.nz
+Sender: Brent Addis <uuuuuu@xxxxxx.gen.nz>
+
+
+
+-----BEGIN PGP SIGNED MESSAGE-----
+
+CERT Advisory CA-2002-21 Vulnerability in PHP
+
+   Original release date: July 22, 2002
+   Last revised: --
+   Source: CERT/CC
+
+   A complete revision history can be found at the end of this file.
+
+Systems Affected
+
+     * Systems running PHP versions 4.2.0 or 4.2.1
+
+Overview
+
+   A  vulnerability  has been discovered in PHP. This vulnerability could
+   be  used  by  a remote attacker to execute arbitrary code or crash PHP
+   and/or the web server.
+
+I. Description
+
+   PHP  is  a  popular  scripting  language  in  widespread use. For more
+   information about PHP, see
+
+          http://www.php.net/manual/en/faq.general.php
+
+   The  vulnerability  occurs  in the portion of PHP code responsible for
+   handling  file uploads, specifically multipart/form-data. By sending a
+   specially  crafted  POST  request  to  the web server, an attacker can
+   corrupt  the  internal  data  structures used by PHP. Specifically, an
+   intruder  can  cause  an improperly initialized memory structure to be
+   freed.  In  most  cases, an intruder can use this flaw to crash PHP or
+   the  web  server. Under some circumstances, an intruder may be able to
+   take  advantage  of  this  flaw  to  execute  arbitrary  code with the
+   privileges of the web server.
+
+   You  may  be  aware that freeing memory at inappropriate times in some
+   implementations  of  malloc  and  free  does not usually result in the
+   execution  of  arbitrary  code.  However, because PHP utilizes its own
+   memory  management  system,  the  implementation of malloc and free is
+   irrelevant to this problem.
+
+   Stefan  Esser  of  e-matters  GmbH has indicated that intruders cannot
+   execute   code   on   x86   systems.   However,  we  encourage  system
+   administrators  to  apply  patches  on  x86  systems  as well to guard
+   against denial-of-service attacks and as-yet-unknown attack techniques
+   that may permit the execution of code on x86 architectures.
+
+   This  vulnerability  was discovered by e-matters GmbH and is described
+   in  detail  in  their  advisory.  The  PHP  Group  has  also issued an
+   advisory.  A list of vendors contacted by the CERT/CC and their status
+   regarding this vulnerability is available in VU#929115.
+
+   Although   this  vulnerability  only  affects  PHP  4.2.0  and  4.2.1,
+   e-matters  GmbH  has  previously  identified  vulnerabilities in older
+   versions  of  PHP.  If  you  are  running  older  versions  of PHP, we
+   encourage you to review
+   http://security.e-matters.de/advisories/012002.html
+
+II. Impact
+
+   A  remote  attacker can execute arbitrary code on a vulnerable system.
+   An  attacker  may not be able to execute code on x86 architectures due
+   to  the way the stack is structured. However, an attacker can leverage
+   this  vulnerability  to  crash PHP and/or the web server running on an
+   x86 architecture.
+
+III. Solution
+
+Apply a patch from your vendor
+
+   Appendix A contains information provided by vendors for this advisory.
+   As  vendors report new information to the CERT/CC, we will update this
+   section  and note the changes in our revision history. If a particular
+   vendor  is  not  listed  below,  we  have not received their comments.
+   Please contact your vendor directly.
+
+Upgrade to the latest version of PHP
+
+   If  a  patch  is  not  available  from your vendor, upgrade to version
+   4.2.2.
+
+Deny POST requests
+
+   Until  patches  or an update can be applied, you may wish to deny POST
+   requests.  The  following  workaround  is  taken from the PHP Security
+   Advisory:
+
+     If  the  PHP  applications on an affected web server do not rely on
+     HTTP POST input from user agents, it is often possible to deny POST
+     requests on the web server.
+
+     In  the  Apache  web server, for example, this is possible with the
+     following  code  included  in  the  main  configuration  file  or a
+     top-level .htaccess file:
+
+     <Limit POST>
+        Order deny,allow
+        Deny from all
+     </Limit>
+
+     Note  that an existing configuration and/or .htaccess file may have
+     parameters contradicting the example given above.
+
+Disable vulnerable service
+
+   Until  you  can upgrade or apply patches, you may wish to disable PHP.
+   As a best practice, the CERT/CC recommends disabling all services that
+   are not explicitly required. Before deciding to disable PHP, carefully
+   consider your service requirements.
+
+Appendix A. - Vendor Information
+
+   This  appendix  contains  information  provided  by  vendors  for this
+   advisory.  As  vendors  report new information to the CERT/CC, we will
+   update this section and note the changes in our revision history. If a
+   particular  vendor  is  not  listed  below, we have not received their
+   comments.
+
+Apple Computer Inc.
+
+          Mac  OS  X  and  Mac  OS X Server are shipping with PHP version
+          4.1.2  which  does  not  contain the vulnerability described in
+          this alert.
+
+Caldera
+
+          Caldera  OpenLinux  does  not provide either vulnerable version
+          (4.2.0,  4.2.1)  of  PHP  in their products. Therefore, Caldera
+          products are not vulnerable to this issue.
+
+Compaq Computer Corporation
+
+          SOURCE:  Compaq Computer Corporation, a wholly-owned subsidiary
+          of  Hewlett-Packard  Company  and  Hewlett-Packard  Company  HP
+          Services Software Security Response Team
+          x-ref: SSRT2300 php post requests
+          At  the  time  of  writing  this  document, Compaq is currently
+          investigating   the   potential  impact  to  Compaq's  released
+          Operating System software products.
+          As  further  information  becomes available Compaq will provide
+          notice  of  the  availability  of any necessary patches through
+          standard  security bulletin announcements and be available from
+          your normal HP Services supportchannel.
+
+Cray Inc.
+
+          Cray, Inc. does not supply PHP on any of its systems.
+
+Debian
+
+          Debian GNU/Linux stable aka 3.0 is not vulnerable.
+          Debian GNU/Linux testing is not vulnerable.
+          Debian GNU/Linux unstable is vulnerable.
+          The  problem  effects PHP versions 4.2.0 and 4.2.1. Woody ships
+          an  older  version  of  PHP  (4.1.2),  that doesn't contain the
+          vulnerable function.
+
+FreeBSD
+
+          FreeBSD  does not include any version of PHP by default, and so
+          is  not  vulnerable; however, the FreeBSD Ports Collection does
+          contain  the  PHP4  package. Updates to the PHP4 package are in
+          progress  and a corrected package will be available in the near
+          future.
+
+Guardian Digital
+
+          Guardian  Digital  has not shipped PHP 4.2.x in any versions of
+          EnGarde, therefore we are not believed to be vulnerable at this
+          time.
+
+Hewlett-Packard Company
+
+          SOURCE:  Hewlett-Packard Company Security Response Team
+          At  the  time  of  writing  this  document,  Hewlett Packard is
+          currently  investigating  the potential impact to HP's released
+          Operating System software products.
+          As further information becomes available HP will provide notice
+          of  the  availability of any necessary patches through standard
+          security  bulletin  announcements  and  be  available from your
+          normal HP Services support channel.
+
+IBM
+
+          IBM  is  not vulnerable to the above vulnerabilities in PHP. We
+          do  supply the PHP packages for AIX through the AIX Toolbox for
+          Linux  Applications.  However,  these packages are at 4.0.6 and
+          also incorporate the security patch from 2/27/2002.
+
+Mandrakesoft
+
+          Mandrake Linux does not ship with PHP version 4.2.x and as such
+          is  not  vulnerable.  The  Mandrake Linux cooker does currently
+          contain  PHP  4.2.1  and  will  be  updated shortly, but cooker
+          should  not be used in a production environment and no advisory
+          will be issued.
+
+Microsoft Corporation
+
+          Microsoft  products  are not affected by the issues detailed in
+          this advisory.
+
+Network Appliance
+
+          No Netapp products are vulnerable to this.
+
+Red Hat Inc.
+
+          None  of  our commercial releases ship with vulnerable versions
+          of PHP (4.2.0, 4.2.1).
+
+SuSE Inc.
+
+          SuSE Linux is not vulnerable to this problem, as we do not ship
+          PHP 4.2.x.
+     _________________________________________________________________
+
+   The  CERT/CC acknowledges e-matters GmbH for discovering and reporting
+   this vulnerability.
+     _________________________________________________________________
+
+   Author: Ian A. Finlay.
+   ______________________________________________________________________
+
+   This document is available from:
+   http://www.cert.org/advisories/CA-2002-21.html
+   ______________________________________________________________________
+
+CERT/CC Contact Information
+
+   Email: cert@cert.org
+          Phone: +1 412-268-7090 (24-hour hotline)
+          Fax: +1 412-268-6989
+          Postal address:
+          CERT Coordination Center
+          Software Engineering Institute
+          Carnegie Mellon University
+          Pittsburgh PA 15213-3890
+          U.S.A.
+
+   CERT/CC   personnel   answer  the  hotline  08:00-17:00  EST(GMT-5)  /
+   EDT(GMT-4)  Monday  through  Friday;  they are on call for emergencies
+   during other hours, on U.S. holidays, and on weekends.
+
+Using encryption
+
+   We  strongly  urge you to encrypt sensitive information sent by email.
+   Our public PGP key is available from
+   http://www.cert.org/CERT_PGP.key
+
+   If  you  prefer  to  use  DES,  please  call the CERT hotline for more
+   information.
+
+Getting security information
+
+   CERT  publications  and  other security information are available from
+   our web site
+   http://www.cert.org/
+
+   To  subscribe  to  the CERT mailing list for advisories and bulletins,
+   send  email  to majordomo@cert.org. Please include in the body of your
+   message
+
+   subscribe cert-advisory
+
+   *  "CERT"  and  "CERT  Coordination Center" are registered in the U.S.
+   Patent and Trademark Office.
+   ______________________________________________________________________
+
+   NO WARRANTY
+   Any  material furnished by Carnegie Mellon University and the Software
+   Engineering  Institute  is  furnished  on  an  "as is" basis. Carnegie
+   Mellon University makes no warranties of any kind, either expressed or
+   implied  as  to  any matter including, but not limited to, warranty of
+   fitness  for  a  particular purpose or merchantability, exclusivity or
+   results  obtained from use of the material. Carnegie Mellon University
+   does  not  make  any warranty of any kind with respect to freedom from
+   patent, trademark, or copyright infringement.
+     _________________________________________________________________
+
+   Conditions for use, disclaimers, and sponsorship information
+
+   Copyright 2002 Carnegie Mellon University.
+
+   Revision History
+July 22, 2002:  Initial release
+
+
+
+
+-----BEGIN PGP SIGNATURE-----
+Version: PGP 6.5.8
+
+iQCVAwUBPTyOVqCVPMXQI2HJAQGK6QQAp1rR7K18PNxpQZvqKPYWxyrtpiT8mmKN
+UuyERmOoX+5MAwH0hbAWCvVcyLH0gKGbTpBkRgToT8IEHZojwHCzqOaMM9kni/FG
+QEVeznLfBX4GIgZGPu0XWlph3ZqaayWln57eGueYZ26zBuriIUu2cUCmyYGQkqlI
+tuZdnDqUmR0=
+=+829
+-----END PGP SIGNATURE-----
+
+
diff --git a/upstream/t/data/welcomelists/debian_bts_reassign b/upstream/t/data/welcomelists/debian_bts_reassign
new file mode 100644 (file)
index 0000000..e492886
--- /dev/null
@@ -0,0 +1,30 @@
+Received: from dogma.slashnull.org (dogma.slashnull.org [212.17.35.15])
+        by zzzzzzzzzzzzzz.zzz (Postfix) with ESMTP id 498AA132505
+        for <yyyyyy@aaaaaaaaa.aaa>; Thu,  1 Aug 2002 14:22:07 -0700 (PDT)
+Received: from intm3.sparklist.com (intm3.sparklist.com [207.250.144.9])
+        by dogma.slashnull.org (8.11.6/8.11.6) with SMTP id g71LN6230402
+        for <zzzzzzzzzzzzz@yyyyyyyy>; Thu, 1 Aug 2002 22:23:06 +0100
+Message-Id: <INTM-6516589-3669406-2002.08.01-16.21.51--zzzzzzzzzzzzz#yyyyyyyy@list3.internet.com>
+To: Colin Watson <cjwatson@debian.org>
+Subject: Processed: reassign 126111 to cdimage.debian.org
+From: owner@bugs.debian.org (Debian Bug Tracking System)
+Date: Thu, 27 Dec 2001 18:48:04 -0600
+Cc: unknown-package@qa.debian.org (pseudo-image-kit-2.0.zip #126111), Debian CD-ROM Team <debian-cd@lists.debian.org>(cdimage.debian.org #126111)
+In-Reply-To: <E16Jl13-0007Q1-00@arborlon.riva.ucam.org>
+References: <E16Jl13-0007Q1-00@arborlon.riva.ucam.org>
+Sender: Debian BTS <debbugs@master.debian.org>
+
+Processing commands for control@bugs.debian.org:
+
+> reassign 126111 cdimage.debian.org
+Bug#126111: 2.2_rev4/i386/binary-i386-1.list not up to date
+Bug reassigned from package `pseudo-image-kit-2.0.zip' to `cdimage.debian.org'.
+
+>
+End of message, stopping processing here.
+
+Please contact me if you need assistance.
+
+Debian bug tracking system administrator
+(administrator, Debian Bugs database)
+
diff --git a/upstream/t/data/welcomelists/ibm_enews_de b/upstream/t/data/welcomelists/ibm_enews_de
new file mode 100644 (file)
index 0000000..0974593
--- /dev/null
@@ -0,0 +1,311 @@
+Return-Path: <info@isource.ibm.com>
+Received: (qmail 21402 invoked by alias); 4 Jul 2002 13:36:52 -0000
+Received: (qmail 21361 invoked by uid 82); 4 Jul 2002 13:36:51 -0000
+Received: from info@isource.ibm.com by mailhost with qmail-scanner-1.00 (uvscan: v4.1.40/v4210. . Clean. Processed in 2.214854 secs); 04 Jul 2002 13:36:51 -0000
+Received: from isource.boulder.ibm.com (HELO isource.ibm.com) (207.25.249.18)
+  by mi-1.rz.ruhr-uni-bochum.de with SMTP; 4 Jul 2002 13:36:48 -0000
+Received: from isource.boulder.ibm.com (loopback [127.0.0.1])
+       by isource.ibm.com (Postfix) with ESMTP id 0585052807
+       for <XXXXXX.YYYYYYYYYY@RUHR-UNI-BOCHUM.DE>; Thu,  4 Jul 2002 13:32:05 +0000 (CUT)
+From: IBM Deutschland <info@isource.ibm.com>
+Reply-To: webmaster@de.ibm.com
+Subject: IBM eNews: Aktuelle Informationen von IBM
+Content-Type: text/plain;
+To: XXXXXX.YYYYYYYYYY@RUHR-UNI-BOCHUM.DE
+Message-Id: <20020704133206.0585052807@isource.ibm.com>
+Date: Thu,  4 Jul 2002 13:32:06 +0000 (CUT)
+
+IBM eNews
+4. Juli 2002
+
+Liebe Leserin, lieber Leser,
+
+zur Zeit findet in Wimbledon das diesjährige Tennisturnier 
+statt - mit Hilfe von IBM auch online unter 
+http://www.wimbledon.org ein packendes Ereignis. 
+
+Lesen Sie mehr über diesem Internetauftritt und zu 
+zahlreichen weiteren Themen aus der IT-Branche in der 
+aktuellen Ausgabe von IBM eNews.
+
+Nutzen Sie die Möglichkeit, auf folgender Webseite aus über 
+40 Interessensgebieten die Themen für Ihre persönliche 
+IBM eNews Ausgabe auszuwählen. Sie erhalten dann Business-
+Informationen nach Maß:
+http://www.ibm.com/de/profile/change_interests.html
+
+Alle Artikel können Sie jederzeit online auf dieser Webseite 
+aufrufen:
+http://www.ibm.com/de/news/enews/online/
+
+Wir halten Sie auf dem Laufenden
+IBM eNews
+
+Wenn Sie in Zukunft IBM eNews nicht mehr erhalten möchten, 
+können Sie sich auf dieser Webseite abmelden:
+http://www.ibm.com/de/profile/unsubscribe.html
+
+
+
+In der heutigen Ausgabe:
+========================
+
+e-business
+
+  o  Wimbledon gewinnt: Mit Hilfe von IBM auch online ein packendes Ereignis 
+     http://isource.ibm.com/cgi-bin/goto?on=de020756
+
+  o  Neues Buch "Deutschland Online" weist Weg in die
+      Informationsgesellschaft 
+     http://isource.ibm.com/cgi-bin/goto?on=de020757
+
+  o  Weitere Artikel aus dem Bereich "e-business" 
+     http://isource.ibm.com/cgi-bin/goto?on=020758
+
+Business Lösungen und Services
+
+  o  Mehr Training für alle: IBM Learning Services Corporate Card 
+     http://isource.ibm.com/cgi-bin/goto?on=020721
+
+  o  Weitere Artikel aus dem Bereich "Business Lösungen und Services"  
+     http://isource.ibm.com/cgi-bin/goto?on=020759
+
+IT Solutions und Services
+
+  o  e-guide aktuell: Produkte und Lösungen für den Mittelstand -
+      zusammengefasst im IBM Kundenmagazin! 
+     http://isource.ibm.com/cgi-bin/goto?on=020710
+
+  o  Sprechen Sie mit uns: Sicherheit ist Trumpf 
+     http://isource.ibm.com/cgi-bin/goto?on=020712
+
+  o  Weitere Artikel aus dem Bereich "IT Solutions und Services" 
+     http://isource.ibm.com/cgi-bin/goto?on=020760
+
+Software
+
+  o  Software für den Mittelstand 
+     http://isource.ibm.com/cgi-bin/goto?on=020711
+
+  o  WebSphere Integration 
+     http://isource.ibm.com/cgi-bin/goto?on=020724
+
+  o  Weitere Artikel aus dem Bereich "Software"  
+     http://isource.ibm.com/cgi-bin/goto?on=020761
+
+Hardware
+
+  o  Neu: IBM ThinkPad A31p - Die erste mobile 3D-Workstation! 
+     http://isource.ibm.com/cgi-bin/goto?on=de020707
+
+  o  IBM eServer* pSeries 630 6E4/6C4 - Ankündigung des neuen Entry Servers 
+     http://isource.ibm.com/cgi-bin/goto?on=020713
+
+  o  Weitere Artikel aus dem Bereich "Hardware" 
+     http://isource.ibm.com/cgi-bin/goto?on=020762
+
+
+
+------------------------------------------------------------
+e-business
+------------------------------------------------------------
+
+Wimbledon gewinnt: Mit Hilfe von IBM auch online ein packendes Ereignis 
+
+   Die neue Turnier-Website bietet Tennisfans in aller Welt 
+  sekundenaktuelle Spielstände, Live-Videos, Kommentare und 
+  Interviews. Mit e-business on demand lässt sich dabei die 
+  erforderliche Kapazität für den Ansturm während des Turniers 
+  einfach einschalten - und anschließend ebenso einfach wieder 
+  abschalten. Ein echter Service-Gewinn, Klick für Klick. 
+  http://isource.ibm.com/cgi-bin/goto?on=de020756
+  
+
+
+Neues Buch "Deutschland Online" weist Weg in die
+      Informationsgesellschaft 
+
+  "Die schnelle Transformation in die Informationsgesellschaft ist 
+  Deutschlands letzte Chance, um im Kreis der großen 
+  Wirtschaftsmächte zu verbleiben. Deutschland muss IT-Weltmacht 
+  werden - und das pronto!" Das verlangte der Vorsitzende der 
+  Geschäftsführung der IBM Deutschland, Erwin Staudt, anlässlich 
+  der Vorstellung des Buches "Deutschland online" - Strategien und 
+  Projekte für die Informationsgesellschaft - in Berlin.
+   http://isource.ibm.com/cgi-bin/goto?on=de020757
+
+
+Weitere Artikel aus dem Bereich "e-business" 
+
+  Weitere Artikel aus dem Bereich "e-business" finden Sie online
+  auf unserer IBM eNews Website:
+  http://isource.ibm.com/cgi-bin/goto?on=020758
+     
+  Unter folgender Adresse können Sie Ihre Interessensgebiete 
+  auswählen und erhalten dann Business-Informationen nach Maß:
+  http://www-5.ibm.com/de/profile/change_interests.html
+
+
+
+------------------------------------------------------------
+Business Lösungen und Services
+------------------------------------------------------------
+
+Mehr Training für alle: IBM Learning Services Corporate Card 
+
+  IBM Learning Services bietet Ihnen preisgünstige Trainings: Mit 
+  der IBM Learning Services Corporate Card sparen Sie bis zu 1.650 
+  Euro. Im Unterschied zur IBM Learning Services Education Card 
+  können Sie damit alle Ihre Mitarbeiter/innen zu den Trainings 
+  senden.
+  http://isource.ibm.com/cgi-bin/goto?on=020721
+
+
+Weitere Artikel aus dem Bereich "Business Lösungen und Services"  
+
+  Weitere Artikel aus dem Bereich "Business Lösungen und Services" 
+  finden Sie online auf unserer IBM eNews Website:
+  http://isource.ibm.com/cgi-bin/goto?on=020759
+     
+  Unter folgender Adresse können Sie Ihre Interessensgebiete 
+  auswählen und erhalten dann Business-Informationen nach Maß:
+  http://www-5.ibm.com/de/profile/change_interests.html
+
+
+
+------------------------------------------------------------
+IT Solutions und Services
+------------------------------------------------------------
+
+e-guide aktuell: Produkte und Lösungen für den Mittelstand -
+      zusammengefasst im IBM Kundenmagazin! 
+
+  Mit dieser Ausgabe übernehmen wir Teile des Heftes auch im Web. 
+  Lesen Sie hier mehr über den neuen Produkt- und Lösungsteil 
+  oder bestellen Sie sich Ihr Exemplar des IBM Kundenmagazins 
+  "e-guide" 2/2002.
+  http://isource.ibm.com/cgi-bin/goto?on=020710
+
+
+Sprechen Sie mit uns: Sicherheit ist Trumpf 
+
+  Sie machen sich sicherlich Gedanken darüber, ob Ihre e-business 
+  Infrastruktur wirkungsvoll geschützt ist - insbesondere vor dem 
+  Hintergrund sich öffnender Strukturen.
+  IBM - als einer der Vorreiter im Bereich e-business Sicherheits-
+  strategien - bietet Ihnen Lösungen, mit denen Sie Ihre 
+  IT-Infrastruktur sichern können. Überzeugen Sie sich selbst:
+  http://isource.ibm.com/cgi-bin/goto?on=020712
+
+
+Weitere Artikel aus dem Bereich "IT Solutions und Services" 
+
+  Weitere Artikel aus dem Bereich "IT Solutions und Services" 
+  finden Sie online auf unserer IBM eNews Website:
+  http://isource.ibm.com/cgi-bin/goto?on=020760
+  
+  Unter folgender Adresse können Sie Ihre Interessensgebiete 
+  auswählen und erhalten dann Business-Informationen nach Maß:
+  http://www-5.ibm.com/de/profile/change_interests.html
+
+
+
+------------------------------------------------------------
+Software
+------------------------------------------------------------
+
+Software für den Mittelstand 
+
+  Hier finden Sie ausgewählte Software-Produkte mit Beispielen 
+  unserer zufriedenen Kunden. Die IBM Produktfamilien WebSphere, 
+  DB2, Lotus und Tivoli sind die Basis für eine Vielfalt von 
+  e-business Lösungen. Sie sind industrie-spezifisch, skalierbar 
+  ausgerichtet und basieren auf offenen Standards, so dass sie 
+  speziell auf die Bedürfnisse des Mittelstandes zugeschnitten 
+  werden können.
+  http://isource.ibm.com/cgi-bin/goto?on=020711
+
+
+WebSphere Integration 
+
+  Mit WebSphere Integration versucht die IBM keine Technologie zu 
+  vermarkten, die die unüberschaubare Vielfalt der Individual-
+  programmierungen um neue Facetten bereichert. Vielmehr agiert 
+  sie wie ein Katalysator und ermöglicht die Erweiterung und 
+  Erneuerung von Systemlandschaften sowie die Migration von 
+  geschäftskritischen Daten. Mehr dazu im Software-Schwerpunkt 
+  des Monats.
+  http://isource.ibm.com/cgi-bin/goto?on=020724
+
+
+Weitere Artikel aus dem Bereich "Software"  
+
+  Weitere Artikel aus dem Bereich "Software" finden Sie online
+  auf unserer IBM eNews Website:
+  http://isource.ibm.com/cgi-bin/goto?on=020761
+  
+  Unter folgender Adresse können Sie Ihre Interessensgebiete 
+  auswählen und erhalten dann Business-Informationen nach Maß:
+  http://www-5.ibm.com/de/profile/change_interests.html
+
+
+
+------------------------------------------------------------
+Hardware
+------------------------------------------------------------
+
+Neu: IBM ThinkPad A31p - Die erste mobile 3D-Workstation! 
+
+  Der ThinkPad A31p (TV2N6GE, TV2L3GE, TV2N5GE) ist ausgerüstet mit 
+  einem Intel Pentium 4 Notebookprozessor-M, schnellen DDR Speicher-
+  modulen und einem extrem leistungsfähigen Grafikchip. Auch die 
+  Highspeed Festplatte lässt keine Wünsche offen. Zwei modulare 
+  Laufwerkschächte sorgen für ein Plus an Flexibilität. Das Notebook 
+  ist mit einer 10/100 Ethernet-Karte, einem 56K V.92 Modem, 
+  integrierten 802.11b Wireless-Antennen/-Chip, Bluetooth sowie 
+  einem IEEE 1394 (Firewire)-Anschluss ausgerüstet. 
+  http://isource.ibm.com/cgi-bin/goto?on=de020707
+
+
+IBM eServer* pSeries 630 6E4/6C4 - Ankündigung des neuen Entry Servers 
+
+  IBM definiert den UNIX Entry Server neu. Zuverlässigkeit und
+  Verfügbarkeit der POWER4-Prozessortechnologie jetzt vom Entry- 
+  bis zum Enterprise-Bereich, mit Selbstverwaltungsfunktionen aus 
+  dem Projekt eLiza, ultraflaches Rack- oder Deskside-Modell, die 
+  richtige Wahl für kleine und mittelständische Unternehmen.
+  http://isource.ibm.com/cgi-bin/goto?on=020713
+
+
+Weitere Artikel aus dem Bereich "Hardware" 
+
+  Weitere Artikel aus dem Bereich "Hardware" finden Sie online
+  auf unserer IBM eNews Website:
+  http://isource.ibm.com/cgi-bin/goto?on=020762
+    
+  Unter folgender Adresse können Sie Ihre Interessensgebiete 
+  auswählen und erhalten dann Business-Informationen nach Maß:
+  http://www-5.ibm.com/de/profile/change_interests.html
+
+
+
+============================================================
+Sie erhalten diese E-Mail, da Sie zu IBM eNews
+  angemeldet sind als XXXXXX.YYYYYYYYYY@RUHR-UNI-BOCHUM.DE
+
+
+*Das IBM eServer Warenzeichen besteht aus dem eingeführten 
+IBM e-business Logo, gefolgt von dem beschreibenden Begriff 
+"Server".
+  
+Nach unseren Kundendaten sind Sie an Informationsmaterial von 
+IBM interessiert. An- und Abmelden sowie Ihre Einstellungen 
+ändern können Sie auf folgender Website: 
+http://www.ibm.com/de/profile/
+
+Kontakt: webmaster@de.ibm.com
+Copyright (c) 2002  IBM Deutschland
+
+
+
diff --git a/upstream/t/data/welcomelists/infoworld b/upstream/t/data/welcomelists/infoworld
new file mode 100644 (file)
index 0000000..76cb286
--- /dev/null
@@ -0,0 +1,188 @@
+From Cringely@bdcimail.com Mon Aug 12 10:58:47 2002
+Return-Path: <bounce-rcringely-0000000@mailcontrol.bellevuedata.com>
+Delivered-To: ffffff@localhost.aaaaaaaaaaaa.net
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by mail.aaaaaaaaaaaa.net (Postfix) with ESMTP id B3DA1BEEB2
+       for <ffffff@localhost>; Mon, 12 Aug 2002 14:28:07 -0700 (PDT)
+Received: from mail.aaaaaaaaaaaa.com
+       by localhost with IMAP (fetchmail-5.9.11)
+       for ffffff@localhost (single-drop); Mon, 12 Aug 2002 14:28:07 -0700 (PDT)
+Received: from mailcontrol.bellevuedata.com (mailcontrol.bellevuedata.com [66.37.227.18])
+       by mail14.megamailservers.com (8.12.5/8.12.0.Beta10) with SMTP id g7CLKt9N008640
+       for <zzzzzz@aaaaaaaaaaaa.com>; Mon, 12 Aug 2002 17:21:09 -0400 (EDT)
+Date: Mon, 12 Aug 2002 12:58:47 -0500
+From: Cringely@bdcimail.com
+Message-Id: <LISTMANAGERSQL-0000000-32368-2002.08.12-12.58.49--zzzzzz#aaaaaaaaaaaa.com@mailcontrol.bellevuedata.com>
+To: zzzzzz@aaaaaaaaaaaa.com
+Subject: ROBERT X. CRINGELY(R): "Notes from the Field" from InfoWorld.com, Monday, August 12, 2002
+Reply-To: CringelyHelp@Bellevue.com
+Content-Type: text/plain;
+       charset="iso-8859-1"
+X-SpamBouncer: 1.5 (7/17/02)
+X-SBNote: FROM_DAEMON/Listserv
+X-SBPass: No Pattern Matching
+X-SBPass: No Freemail Filtering
+X-SBClass: Bulk
+X-Folder: Bulk
+
+========================================================
+ROBERT X. CRINGELY(R): "Notes from the Field" InfoWorld.com
+========================================================
+
+Monday, August 12, 2002
+
+Advertising Sponsor - - - - - - - - - - - - - - - - - - 
+Business Specials from Gateway
+$100 Instant Rebate on select business notebooks,
+Plus FREE Shipping (LIMITED TIME OFFER)
+or call and ask about wireless networking specials
+for business 1.888.851.7359
+http://63.115.136.15/go/infoworld/4524953.html
+
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+
+LOOKING TO INNOVATE
+
+Posted August 9, 2002 01:01 PM  Pacific Time
+
+
+AMBER FOUND A brochure I had for Kauai, Hawaii. "What
+is this, Cringe? Are you planning to surprise me with
+a trip there?" I just hope she doesn't discover that
+single plane ticket I bought.
+
+Gates reveals the awful truth
+
+At an impromptu meeting over lunch at Microsoft's
+Financial Analysts Day in July, Bill Gates said
+companies are not "super" innovative and don't produce
+reliable products. No earth-shattering revelations
+there, but in illustrating his point he asked, "Do you
+really need the next version of Office? I don't think
+so." Gates' implied he acknowledged users need a
+compelling reason to upgrade to the next Office, my
+spy said. The only thing that sells software these
+days is the innovation factor, Gates claimed. "The
+Tablet is going to be the most viral thing ever,"
+Gates added. Of course, Microsoft has been touting the
+Tablet PC for quite some time.
+
+JavaScript pressure
+
+Microsoft is looking to innovate, however, at least
+when it comes to the European Computer Manufacturers
+Association (ECMA). In terms of extending existing
+scripting languages to support XML there appears to be
+both good and bad news from ECMA, my spy said. The
+good news is BEA recently showed ECMA how to better
+extend scripting languages to work directly with XML.
+The bad news is most people wouldn't recognize the
+group today, which seems hell-bent on replacing
+JavaScript (now called ECMAscript) with a derivative
+that looks a lot like a C# scripting language.
+
+A lack of chivalry
+
+Big Blue is cracking the whip against employee
+tailgaters, but not the variety typically associated
+with college football games. Workers are being
+reminded about a no tailgating policy, which means
+they are forbidden from sliding their ID badge through
+the security system, then holding the door for someone
+who doesn't slide their badge. "We have been
+instructed chivalry is dead concerning this matter,"
+my spy said.
+
+Speaking of chivalry's demise, common courtesy may be
+going with it at the newly merged HP. Despite current
+geopolitical situations, HP is relying on parts of its
+support located in India. In so doing, HP bailed out
+on a relationship with The Answer Group (TAG), with
+which Compaq had a long-standing relationship. Adding
+salt to the wound, though, HP's support folks in India
+were telling customers and resellers about the TAG
+termination before HP even told TAG.
+
+"I BOOKED OUR tickets for Kauai," Amber said. I guess
+I'm trapped now. No more tranquil escape for me.
+
+Before vacation, send tips to cringe@infoworld.com.
+
+
+
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+
+MORE NOTES FROM THE FIELD                                
+For a complete archive of his InfoWorld columns visit   
+http://www2.infoworld.com/cgi/component/columnarchive.wbs?column=notefield
+
+INFOWORLD OPINIONS
+Weekly commentary from the most trusted voices in 
+IT at: http://www.infoworld.com/community/t_opinions.html
+
+
+To join, or start, a discussion on this or any IT-related
+topic, please visit our InfoWorld forums at 
+http://forums.infoworld.com. Here you can interact and 
+exchange ideas with InfoWorld staff and other readers.
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+QUOTE OF THE DAY:
+"Of course, there is a lot of legislation that is
+very favorable to folks that subsist on the exclusive
+ownership of their code. We have lobbied against
+those bills. It's easy to get negative about other
+people's ideas. This is an opportunity to say,
+'Here's a better idea: consider open source as an
+alternative.' "
+
+--Jeremy Hogan, community relations manager at Red Hat
+Inc., speaking about a planned march on San Francisco
+city hall to promote the use of open source software
+in government offices.
+
+http://www.infoworld.com/articles/hn/xml/02/08/09/020809hnrally.xml?0812mncr
+
+
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+
+SUBSCRIBE/UNSUBSCRIBE/CHANGE E-MAIL
+To subscribe, unsubscribe or change your e-mail address
+for any of InfoWorld's e-mail newsletters,
+go to:http://www.iwsubscribe.com/newsletters/
+
+To subscribe to InfoWorld.com, or InfoWorld Print,
+or both, or to renew or correct a problem with any InfoWorld
+subscription, go to http://www.iwsubscribe.com
+
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+
+Expectations, Great and Not So Great
+InfoWorld columnist Bob Lewis knows that both are part 
+of the job in IT management. That's what makes his Survival 
+Guide newsletter so fresh, so true, so funny. Do you 
+wonder why you have to manage up as well as down? 
+Or why it matters what Larry Ellison wants and Dubya's
+likely to do? Bob feels your pain. He can help. Subscribe
+to his Survival Guide newsletter free at
+http://www.iwsubscribe.com/newsletters/
+
+
+
+Advertising Sponsor - - - - - - - - - - - - - - - - - - 
+Business Specials from Gateway
+$100 Instant Rebate on select business notebooks,
+Plus FREE Shipping (LIMITED TIME OFFER)
+or call and ask about wireless networking specials
+for business 1.888.851.7359
+http://63.115.136.15/go/infoworld/4524953.html
+
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+
+Copyright 2002 InfoWorld Media Group Inc.
+
+
+
+
+This message was sent to:  zzzzzz@aaaaaaaaaaaa.com
+
+
diff --git a/upstream/t/data/welcomelists/linuxplanet b/upstream/t/data/welcomelists/linuxplanet
new file mode 100644 (file)
index 0000000..002b9dc
--- /dev/null
@@ -0,0 +1,197 @@
+From listsupport@internet.com Mon Aug 12 12:59:02 2002
+Return-Path: <bounce-linuxplanet-text-000000F@list4.internet.com>
+Delivered-To: ffffffff@localhost.zzzzzzzzzz-ffffffff.net
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by mail.zzzzzzzzzz-ffffffff.net (Postfix) with ESMTP id 98624BEE9E
+       for <ffffffff@localhost>; Mon, 12 Aug 2002 14:26:24 -0700 (PDT)
+Received: from mail.zzzzzzzzzz-ffffffff.com
+       by localhost with IMAP (fetchmail-5.9.11)
+       for ffffffff@localhost (single-drop); Mon, 12 Aug 2002 14:26:24 -0700 (PDT)
+Received: from mx3.megamailservers.com (ns3.meganameservers.com [64.29.144.65])
+       by mail1.megamailservers.com (8.12.5/8.12.0.Beta10) with ESMTP id g7CKaOs6025662
+       for <lx@zzzzzzzzzz-ffffffff.com>; Mon, 12 Aug 2002 16:36:24 -0400 (EDT)
+Received: from r00l04.lyris.net (r00l04.lyris.net [216.91.57.134])
+       by mx3.megamailservers.com (8.12.2/8.12.2) with SMTP id g7CKaNLC013752
+       for <lx@zzzzzzzzzz-ffffffff.com>; Mon, 12 Aug 2002 16:36:24 -0400
+X-Mailer: Lyris ListManager Web Interface
+Date: Mon, 12 Aug 2002 12:59:02 -0700
+Subject: LinuxPlanet  Newsletter: August 12, 2002
+To: <lx@zzzzzzzzzz-ffffffff.com>
+From: LinuxPlanet <listsupport@internet.com>
+List-Unsubscribe: <mailto:leave-linuxplanet-text-000000F@list4.internet.com>
+Reply-To: Newsletter Support <listsupport@internet.com>
+Message-Id: <INTM-000000F-1929563-2002.08.12-13.35.16--lx#zzzzzzzzzz-ffffffff.com@list4.internet.com>
+X-SpamBouncer: 1.5 (7/17/02)
+X-SBNote: FROM_DAEMON/Listserv
+X-SBPass: No Pattern Matching
+X-SBPass: No Freemail Filtering
+X-SBClass: Bulk
+X-Folder: Bulk
+
+MyDesktop Proudly Presents:
+
+L  I  N  U  X    P L A N E T
+·¸¸·´¯`·¸¸·´¯`·¸¸·´¯`·¸¸·´¯`·¸¸·´¯`·¸¸·
+Your Weekly Source For Linux Updates!
+LinuxPlanet Newsletter for August 12, 2002
+http://www.linuxplanet.com
+
+___________________________ Sponsors ________________________________
+                  This newsletter sponsored by:  
+                             Thawte
+                          Journyx, Inc.
+_____________________________________________________________________
+
+-----
+IN THIS ISSUE:
+   * NEW AND NOTEWORTHY
+   * COMING UP
+_____
+
+
+/-------------------------------------------------------------------\
+
+FREE Apache SSL Guide from Thawte Certification
+
+Do your online customers demand the best available protection of their
+personal information? Thawte's guide explains how to give this to your
+customers by implementing SSL on your Apache Web Server. Click here to 
+get our FREE Thawte Apache Guide: http://www.gothawte.com/rd348.html
+
+\--------------------------------------------------------------adv.-/
+
+
+NEW AND NOTEWORTHY:
+
+Using the InterMezzo Distributed Filesystem
+<http://www.linuxplanet.com/linuxplanet/reports/4368/1/>
+Getting connected is one of the more vital goals of any IT shop. But what
+happens when users can't get commected to the network right away? Are they
+just cut off altogether from their files? Not necessarily, writes Bill von
+Hagen, especially if you are using the InterMezzo distributed filesystem.
+In this next installment of the Distributed Filesystems series, von Hagen
+examines InterMezzo in detail and shows how to install, configure, and
+implement this DFS.
+
+Building Sounds for your Applications with SoundTracker
+<http://www.linuxplanet.com/linuxplanet/tutorials/4363/1/>
+Beeps, bloops, and buzzes. These are the sounds that enrich our computing
+experience. When done right, these auditory cues provide instant feedback
+to a user from an application. But getting the right sounds for your app
+does not have to involve scrounging around for whatever you can find on
+the Internet. You can professionally edit your own sounds with the Linux
+program SoundTracker, as Dee-Ann LeBlanc and Andrew J.D. Bowman explain in
+this tutorial.
+
+Modern Distributed Filesystems For Linux: An Introduction
+<http://www.linuxplanet.com/linuxplanet/reports/4361/1/>
+Data and information has become the lifeblood of many organizations of
+late, and storing that information safely has led to inventive data
+management. Once known as networked filesystems, distributed filesystems
+are now one of the best ways of storing your data across multiple machines
+on your network. Bill von Hagen begins a series of articles on distributed
+filesystems with an introduction to the technology and what it can do for
+your organization.
+
+
+-----
+
+COMING UP: 
+
+       * An Open-Source Approach to Fighting Cancer
+       * Distributed File Systems: The Series Continues
+       * A Review of Linux Books
+
+-----
+
+/-------------------------------------------------------------------\
+*FREE download of Journyx Timesheet for LINUX*
+Have you been looking for an automated solution to 
+replace your paper timesheets? Do you want something 
+that is easy to use and integrates with your existing 
+business applications for payroll, HR, accounting and 
+project management? You need to try Journyx Timesheet! 
+Download Journyx Timesheet for FREE today! 
+http://www.journyx.com/InetL4aug02ezad
+
+\--------------------------------------------------------------adv.-/
+
+-----
+
+Visit the other sites in the internet.com Linux/Open Source Channel:
+Linux Today <http://www.linuxtoday.com>
+LinuxPlanet <http://www.linuxplanet.com>
+AllLinuxDevices <http://www.alllinuxdevices.com>
+PHPBuilder <http://www.phpbuilder.com>
+BSD Today <http://www.bsdtoday.com>
+Apache Today <http://www.apachetoday.com>
+Enterprise Linux Today <http://www.eltoday.com>
+Linux Central <http://www.linuxcentral.com>
+Linuxnewbie <http://www.linuxnewbie.org>
+The ISP-Linux Moderated Digest
+<http://isp-lists.isp-planet.com/moderated/isp-linux/>.
+
+
+
+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+DEDICATED EMAIL LIST SERVERS!
+Get the speed, control, and responsiveness you need for your
+out-sourced Email Newsletters at an AFFORDABLE price!  
+100% UPTIME GUARANTEED!
+Sign-up by July 15th and the set-up is FREE for your
+DEDICATED solution just for mentioning this ad.  
+Free Quote: mailto:sales@sparklist.com or surf the 
+website: http://SparkLIST.com/ or direct: 920.490.5901, x1
+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Advertising: If you are interested in advertising in our newsletters, call 
+Claudia at 1-203-662-2863 or send email to mailto:nsladsales@internet.com
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+For contact information on sales offices worldwide visit 
+http://www.internet.com/mediakit/salescontacts.html
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
+For details on becoming a Commerce Partner, contact David Arganbright
+on 1-203-662-2858 or mailto:commerce-licensing@internet.com 
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
+To learn about other free newsletters offered by internet.com or 
+to change your subscription visit http://e-newsletters.internet.com 
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
+internet.com's network of more than 160 Web sites is organized into 16 
+channels: 
+Internet Technology          http://internet.com/it
+E-Commerce/Marketing         http://internet.com/marketing
+Web Developer                http://internet.com/webdev
+Windows Internet Technology  http://internet.com/win
+Linux/Open Source            http://internet.com/linux
+Internet Resources           http://internet.com/resources
+ISP Resources                http://internet.com/isp
+Internet Lists               http://internet.com/lists
+Download                     http://internet.com/downloads
+International                http://internet.com/international
+Internet News                http://internet.com/news
+Internet Investing           http://internet.com/stocks 
+ASP Resources                http://internet.com/asp
+Wireless Internet            http://internet.com/wireless 
+Career Resources             http://internet.com/careers
+EarthWeb                    http://www.earthweb.com 
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
+To find an answer - http://search.internet.com 
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
+Looking for a job? Filling an opening? - http://jobs.internet.com
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+This newsletter is published by INT Media Group, Incorporated
+http://internet.com - The Internet & IT Network 
+Copyright (c) 2002 INT Media Group, Incorporated. All rights reserved.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+For information on reprinting or linking to internet.com content: 
+http://internet.com/corporate/permissions.html 
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~   
+---
+You are currently subscribed to linuxplanet-text as: lx@zzzzzzzzzz-ffffffff.com
+To unsubscribe send a blank email to leave-linuxplanet-text-000000F@list4.internet.com
+
+
diff --git a/upstream/t/data/welcomelists/lp.org b/upstream/t/data/welcomelists/lp.org
new file mode 100644 (file)
index 0000000..51f8d20
--- /dev/null
@@ -0,0 +1,132 @@
+Received: from rs6000.resqnet.com (rs6000.resqnet.com [64.209.23.67])
+       by dogma.slashnull.org (8.11.6/8.11.6) with ESMTP id g6PIph423946
+       for <aaaaaa@yyyyyy.zzz>; Thu, 25 Jul 2002 19:51:43 +0100
+Received: from columbia.lp.org (columbia.kia.net [205.252.89.231])
+       by rs6000.resqnet.com (8.11.2/8.11.2) with ESMTP id g6PIoqe17480
+       for <9999999999@kfdjgdkfgjd.com>; Thu, 25 Jul 2002 14:50:52 -0400
+Received: from localhost (daemon@localhost)
+       by columbia.lp.org (8.9.3/8.9.3) with SMTP id OAA51643;
+       Thu, 25 Jul 2002 14:47:50 -0400 (EDT)
+       (envelope-from owner-announce@hq.lp.org)
+Received: by columbia.kia.net (bulk_mailer v1.12); Thu, 25 Jul 2002 12:02:38 -0400
+Received: (from majordom@localhost)
+       by columbia.lp.org (8.9.3/8.9.3) id MAA40103
+       for announce-outgoing; Thu, 25 Jul 2002 12:02:38 -0400 (EDT)
+       (envelope-from owner-announce@hq.lp.org)
+Received: (from lpadmin@localhost)
+       by columbia.lp.org (8.9.3/8.9.3) id MAA40088;
+       Thu, 25 Jul 2002 12:02:37 -0400 (EDT)
+       (envelope-from lpadmin)
+Date: Thu, 25 Jul 2002 12:02:37 -0400 (EDT)
+Message-Id: <200207251602.MAA40088@columbia.lp.org>
+To: announce@hq.lp.org
+Subject: LP RELEASE: Outrageous military spending
+From: Libertarian Party Announcements <owner-announce@lp.org>
+Reply-To: owner-announce@hq.lp.org
+
+-----BEGIN PGP SIGNED MESSAGE-----
+
+===============================
+NEWS FROM THE LIBERTARIAN PARTY
+2600 Virginia Avenue, NW, Suite 100
+Washington DC 20037
+World Wide Web: http://www.LP.org
+===============================
+For release: July 25, 2002
+===============================
+For additional information:
+George Getz, Press Secretary
+Phone: (202) 333-0008 Ext. 222
+E-Mail: pressreleases@hq.LP.org
+===============================
+
+Thousands spent on strippers, golf memberships
+shows Pentagon spending is out of control, Libertarians say
+
+WASHINGTON, DC -- Quiz question: Which of the following items have been 
+charged to the taxpayers recently by military personnel wielding 
+government-issued credit cards?
+
+(a) $38,000 for lap dancing at strip clubs near military bases.
+
+(b) $3,400 for a Sumo wrestling suit and $9,800 for Halloween costumes.
+
+(c) $7,373 for closing costs on a home and $16,000 for a corporate golf 
+membership.
+
+(d) $4,600 for white beach sand and $19,000 worth of decorative "river 
+rock" at a military base in the Arabian desert.
+
+(e) all of the above.
+
+"Incredibly, the answer is 'all of the above,' said Steve Dasbach, 
+Libertarian Party executive director. "Thanks to the federal 
+government's policy of doling out credit cards with no questions asked, 
+the military has launched a raid on your wallet."
+
+The shocking revelations are contained in a General Accounting Office 
+audit released last week that uncovered $101 million in "seemingly 
+unneeded expenditures" made by the Air Force and Army in 2000 and 
+2001. The purchases were made possible by the federal government's lax 
+credit card policy: At least 1.4 million Defense Department employees 
+carry credit cards, and last year they used them to splurge on $6.1 
+billion in goods and services, the audit found.
+
+In one case, a group of 200 soldiers used their military IDs and 
+government-issued travel cards to get cash at adult-entertainment bars, 
+then spent the money there. The clubs charged a 10 percent fee to 
+supply the soldiers with cash -- then billed the full amount to their 
+travel cards as a restaurant charge, the GAO found.
+
+"Are these warriors really fighting terrorism while frolicking in a 
+strip club, or defending our country while wearing a Sumo wrestling 
+suit?" asked Dasbach. "Americans who support a bigger defense budget, 
+take note: The Pentagon frequently behaves like any other bloated, 
+reckless government agency. It promises your money will be spent on the 
+worthiest of causes, then squanders it on things you could never even 
+imagine."
+
+Other spending uncovered by the audit included $45,000 for luxury 
+cruises, $1,800 for executive pillows, and $24,000 for a sofa and 
+armchair at a military installation in the Middle East, Dasbach noted.
+Some military employees actually defended the purchases, the audit 
+noted, by saying that recreational items such as golf memberships can 
+be "a useful tool for building good relations with a host country" 
+such as Saudi Arabia or the United Arab Emirates. 
+
+Not surprisingly, Dasbach said, the audit found "little evidence of 
+documented disciplinary action" against those who misused the cards, 
+so taxpayers may end up paying the tab.
+  
+"It's time to impose a little military discipline on these deadbeat 
+Defense Department workers, and force them to personally reimburse 
+taxpayers for every penny of improper spending," he said.
+"Then cut the Pentagon's massive $379 billion budget to help guard 
+against such wasteful spending in the future. Perhaps that's one way to 
+force the Pentagon to spend its resources defending the country, 
+instead of offending the taxpayer."
+
+
+-----BEGIN PGP SIGNATURE-----
+Version: 2.6.2
+
+iQCVAwUBPUA6FdCSe1KnQG7RAQGAKwP/Zpfw0Uq3BPLnXXmnlWQ2aFFb1FSaj+nJ
+QOMt9q4TBhiYJhIdgdd+uGxoubiPfvyIweSR1PjOdoFe8dYf2h/V4gNS9hSmkSgC
+76RZVuitNf2DbEsaY8TtcUDLDC51m/jgxiGcgPkcyJ+0Wn11RRbktkVEefSNTaBz
+M8ibVFiDPyI=
+=9fYc
+-----END PGP SIGNATURE-----
+
+
+
+-----------------------------------------------------------------------
+The Libertarian Party                                http://www.lp.org/
+2600 Virginia Ave. NW, Suite 100                    voice: 202-333-0008
+Washington DC 20037                                   fax: 202-333-0072
+-----------------------------------------------------------------------
+For subscription changes, please use the WWW form at: 
+http://www.lp.org/action/email.html
+
+
diff --git a/upstream/t/data/welcomelists/media_unspun b/upstream/t/data/welcomelists/media_unspun
new file mode 100644 (file)
index 0000000..310a23d
--- /dev/null
@@ -0,0 +1,1962 @@
+From guterman@mediaunspun.imakenews.net  Wed Aug 14 14:38:59 2002
+Return-Path: <guterman@mediaunspun.imakenews.net>
+Delivered-To: rrrrrrr@localhost.netnoteinc.com
+Received: from localhost (localhost [127.0.0.1])
+       by phobos.labs.netnoteinc.com (Postfix) with ESMTP id 87FA743C34
+       for <rrrrrrr@localhost>; Wed, 14 Aug 2002 09:38:52 -0400 (EDT)
+Received: from phobos [127.0.0.1]
+       by localhost with IMAP (fetchmail-5.9.0)
+       for rrrrrrr@localhost (single-drop); Wed, 14 Aug 2002 14:38:52 +0100 (IST)
+Received: from eng.imakenews.com (mailservice4.imakenews.com
+    [65.214.33.17]) by dogma.slashnull.org (8.11.6/8.11.6) with ESMTP id
+    g7EDZx416820 for <xxxxx@yyyyyy.zzz>; Wed, 14 Aug 2002 14:35:59 +0100
+Received: by eng.imakenews.com (PowerMTA(TM) v1.5); Wed, 14 Aug 2002
+    09:35:04 -0400 (envelope-from <guterman@mediaunspun.imakenews.net>)
+Content-Transfer-Encoding: binary
+Content-Type: multipart/alternative;
+    boundary="----------=_1029331990-31627-4";
+    charset="iso-8859-1"
+Date: Wed, 14 Aug 2002 09:33:10 -0400
+Errors-To: <guterman@mediaunspun.imakenews.net>
+From: "Media Unspun" <guterman@mediaunspun.imakenews.net>
+MIME-Version: 1.0
+Message-Id: <31627$1029331990$mediaunspun$5114587@imakenews.net>
+Precedence: normal
+Reply-To: "Media Unspun" <guterman@vineyard.com>
+Sender: "Media Unspun" <guterman@mediaunspun.imakenews.net>
+Subject: SEC Exposes Big Blue's Pink Slips
+To: xxxxx@yyyyyy.zzz
+X-Imn: mediaunspun,178767,5114587,0
+
+This is a multi-part message in MIME format...
+
+------------=_1029331990-31627-4
+Content-Type: text/plain; charset="iso-8859-1"
+Content-Disposition: inline
+Content-Transfer-Encoding: 7bit
+
+To view this newsletter in full-color, visit:
+http://newsletter.mediaunspun.com/index000018970.cfm
+
+M E D I A  U N S P U N
+What the Press is Reporting and Why (www.mediaunspun.com)
+-----------------------------------------------------------------
+August 14, 2002
+
+-----------------------------------------------------------------
+IN THIS ISSUE
+-----------------------------------------------------------------
+* SEC EXPOSES BIG BLUE'S PINK SLIPS
+* SYNERGY AND BETRAYAL AT VIVENDI
+* OTHER STORIES
+
+Media Unspun serves business news and analysis, authoritatively
+and irreverently, every business day. An annual subscription
+costs $50, less than a dollar a week. If your four-week free
+trial is coming to an end soon, please visit
+http://www.mediaunspun.com/subscribe.html and sign up via credit card 
+or check.
+
+
+-----------------------------------------------------------------
+ADVERTISEMENT
+-----------------------------------------------------------------
+Ken Fisher offers his Quarterly Report for high net worth
+investors FREE of cost & without obligation. Access the same
+investment research he uses to guide his clients at:
+http://pcg.fisherinvestments.com/newrespond/letter.asp?site=UNSP&KC=1229EFCAD0000
+
+
+-----------------------------------------------------------------
+SEC EXPOSES BIG BLUE'S PINK SLIPS
+-----------------------------------------------------------------
+Does the Securities and Exchange Commission have a press pass
+yet? It seems to be bringing us all our news lately. On the day
+of the deadline for companies to certify their financial
+statements with the SEC, the business press squirmed and waited
+for the next Enron or WorldCom. (We might eat these words
+tomorrow, but we doubt it.) In an unrelated confession, IBM gave 
+the commission its latest layoff numbers.
+
+IBM talked about pink slips during its second-quarter earnings
+report, but with a vagueness worthy of your daily horoscope.
+("Capricorn: Career changes may be on their way...") Only after
+"months of surreptitious layoff notices" did the company admit
+that it's cutting more than 15,600 jobs, said the AP. That's
+about 5% of its workforce, and a lot more than pundits expected. 
+An IBM spokesperson told the Wall Street Journal the higher
+number was due to "rebalancing" and more employees than expected 
+taking voluntary layoffs. 
+
+Sorry, we're still back on "rebalancing." Did IBM "rightsize"
+last quarter, too?
+
+IBM's news was still trickling out Wednesday morning, but some
+details were available. About 1,400 workers got cut from IBM's
+microelectronics unit, and most of the rest were from IT
+services and consulting. (That ought to make IBM's new employees 
+from PricewaterhouseCoopers feel all warm and fuzzy inside.)
+Look for news updates from cities that will see the cuts, such
+as Austin and Raleigh. 
+
+OK, none of this is good. Two years into the tech slump, we're
+still tired of seeing people get sacked. But was it really so
+bad that IBM only revealed it because of new accounting
+regulations? Nah, Big Blue was always known for "stealth
+layoffs," as CNN put it, but current corporate scrutiny forced
+it to 'fess up for once. Until now, IBM would acknowledge the
+latest layoffs if reporters called and asked, but wouldn't give
+specifics. Yeesh. - Jen Muehlbauer
+
+IBM Cut 5% of Staff in Period, Double the Expected Number
+http://online.wsj.com/article/0,,SB1029282408667791835,00.html
+(Paid subscription required.) 
+
+IBM to Cut Over 15,000 Employees (AP)
+http://tinyurl.com/10kz
+
+IBM confirms 15,600 job cuts (Reuters)
+http://www.msnbc.com/news/793777.asp 
+
+IBM cutting 15,000 jobs 
+http://news.com.com/2100-1001-949677.html
+
+IBM job cuts exceed 15,600
+http://money.cnn.com/2002/08/13/technology/ibm/index.htm
+
+IBM puts job cuts at 15,600, with fewer than 50 in this state
+http://seattlepi.nwsource.com/business/82508_ibm14.shtml
+
+-----------------------------------------------------------------
+ADVERTISEMENT
+-----------------------------------------------------------------
+You've heard about identity management, but do you know about
+the opportunities and business models that will emerge as a
+result? Download a free executive summary of Esther Dyson's coverage of
+identity management in Release 1.0. Learn more about the
+expanding market for these services and applications.
+http://release1.edventure.com/executivesummary.cfm?MCode=Unspun
+
+-----------------------------------------------------------------
+SYNERGY AND BETRAYAL AT VIVENDI
+-----------------------------------------------------------------
+Synergy always was a fuzzy concept. Now Vivendi Universal's top
+man has slammed the lid on it. The French company announced
+today that it's ready to peddle $9.8 billion in assets to rustle 
+up some cash. First up on the block? Synergy-less U.S. book
+publisher Houghton Mifflin. 
+
+It's unclear whether new chairman Jean-Rene Fourtou has genuine
+turnaround muscle, or whether he and Vivendi's board are simply
+following the winds of post-merger fashion. But when you owe
+$18.7 billion, you get real practical, real fast. The Guardian
+reported that Vivendi's share price sank 5% on Tuesday when
+investors got the willies about the company's impending
+announcement on its financial health. But the company had
+positive news to report: It's making money. Revenue in the first 
+half was up 13%, higher than analysts' estimates of a 7.7%
+boost. 
+
+Details are scant on the breadth of Fourtou's restructuring
+efforts, with more information expected at the next board
+meeting on September 25, according to reporters. Houghton
+Mifflin, acquired a year ago for $1.7 billion, and a vague
+explanation that included the "Curious George" character, were
+the only properties named for sale so far. The Guardian
+speculated that Vivendi will also sell its U.S. video games
+business and possibly its stake in the French mobile phone
+company SFR, a debatable sale because of the cash it generates,
+according to the newspaper. 
+
+Meanwhile, Fourtou's predecessor, Jean-Marie Messier, continues
+to advocate empire-building. The New York Post said its sources
+say Messier hopes his former employer will feel generous enough
+to let him continue to reside in his $17 million Manhattan
+abode. And Bloomberg reported earlier this week that an
+unrepentant Messier is penning a memoir as he vacations in the
+Mediterranean. The working title? "How I Was Betrayed."  -
+Deborah Asbrand 
+
+Vivendi to Sell Publisher Houghton Mifflin (Reuters)
+http://www.washingtonpost.com/wp-dyn/articles/A15954-2002Aug14.html
+
+Vivendi investors expect the worst
+http://www.guardian.co.uk/business/story/0,3604,774190,00.html
+
+Vivendi to Sell $9.8 Billion In Assets, Including Houghton
+http://online.wsj.com/article/0,,SB102931297119161715,00.html
+(Paid subscription required.) 
+
+Ousted Messier Aims To Score $17m Vivendi Pad
+http://www.nypost.com/business/54701.htm
+
+Ex-Chief of Vivendi Plans Tell-All Book (Bloomberg)
+http://www.nytimes.com/2002/08/12/business/media/12VIVE.html
+
+-----------------------------------------------------------------
+OTHER STORIES
+-----------------------------------------------------------------
+A Top AOL Manager Has Left Company
+http://www.nytimes.com/2002/08/14/technology/14AOL.html
+
+Fed Holds Steady on Interest Rates 
+http://www.washingtonpost.com/wp-dyn/articles/A14636-2002Aug13.html
+
+Amtrak halts all high-speed service after finding cracks
+http://www.sunspot.net/bal-te.train14aug14.story
+
+AOL lets resigning exec keep stock options 
+http://www.usatoday.com/money/industries/technology/2002-08-13-aol-pittman_x.htm
+
+Lucent licensing deal with Winstar focus of probe (AP)
+http://www.bayarea.com/mld/mercurynews/business/3861117.htm
+
+Study Says Net Could Benefit Music Firms
+http://www.latimes.com/business/la-fi-music14aug14.story
+
+Eisner Crimping His Own Style
+http://www.latimes.com/business/la-fi-disney14aug14.story
+
+Severance claims by Enron former execs anger ex-workers
+http://www.chron.com/cs/CDA/story.hts/business/1533657
+
+Princeton removes dean after Yale Web site flap (AP)
+http://www.siliconvalley.com/mld/siliconvalley/3857890.htm
+
+Frisbee golf creator dies, may land on someone's roof (SF
+Chronicle)
+http://seattlepi.nwsource.com/national/82560_frisbee14.shtml
+
+Will Kinsley's Slate Get Wiped?
+http://www.ojr.org/ojr/kramer/1029281360.php
+
+Hollywood, Russian Bicker Over Bass
+http://www.cnn.com/2002/SHOWBIZ/News/08/13/bassspace.hollywood.ap/
+
+-----------------------------------------------------------------
+Do you want to reach the Net's savviest audience?
+Advertise in Media Unspun.
+Contact Erik Vanderkolk for details at erikvanderkolk@yahoo.com 
+today.
+
+-----------------------------------------------------------------
+STAFF
+-----------------------------------------------------------------
+Written by Deborah Asbrand (dasbrand@world.std.com), Keith
+Dawson (dawson@world.std.com), Jen Muehlbauer
+(jen@englishmajor.com), and Lori Patel (loripatel@hotmail.com).
+
+Copyedited by Jim Duffy (jimduffy86@yahoo.com). 
+
+Marketing: Cowpoke Productions (cowpokeproductions.com).
+Advertising: Erik Vanderkolk (erikvanderkolk@yahoo.com). 
+
+Editor and publisher: Jimmy Guterman (guterman@vineyard.com).
+
+Media Unspun is produced by The Vineyard Group Inc. 
+Copyright 2002 Media Unspun, Inc., and The Vineyard Group, Inc.
+Subscribe already, willya? http://www.mediaunspun.com 
+
+Redistribution by email is permitted as long as a link to
+http://newsletter.mediaunspun.com is included.
+
+-|________________
+POWERED BY: http://www.imakenews.com
+To be removed from this list, use this link:
+http://www.imakenews.com/eletra/remove.cfm?x=mediaunspun%2Cxxxxx@yyyyyy.zzz
+To receive future messages in HTML format, use this link:
+http://www.imakenews.com/eletra/change.cfm?x=mediaunspun%2Cxxxxx@yyyyyy.zzz%2Chtm
+To change your subscriber information, use this link:
+http://www.imakenews.com/eletra/update.cfm?x=mediaunspun%2Cxxxxx@yyyyyy.zzz
+
+
+------------=_1029331990-31627-4
+Content-Type: text/html; charset="iso-8859-1"
+Content-Disposition: inline
+Content-Transfer-Encoding: 7bit
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<HTML>
+<HEAD>
+<title>M E D I A  U N S P U N</title>
+
+<!--  
+**********************************************************
+If you can read this message but the rest of the email 
+contains strange characters, your email program is not
+capable of displaying HTML email. Use your browser to read the
+complete newsletter online at: 
+   http://newsletter.mediaunspun.com/
+To receive future messages in plain text format, use this link:
+http://www.imakenews.com/eletra/change.cfm?x=mediaunspun%2Cxxxxx@yyyyyy.zzz%2Ctxt
+
+**********************************************************
+CREATED: August 14, 2002   
+-->
+<meta name="description" content="">
+<meta name="keywords" content="">
+<meta name="GENERATOR" content="iMakeNews">
+<meta name="robots" content="ALL">
+
+       
+
+
+<style type="text/css">
+<!--
+ .link {color:#000000; text-decoration:   none; }      .link:hover {color:#FF3300; text-decoration:underline;} 
+
+       .g-article_title, .g-article_url, .g-article_full_story,
+       .g-article_printer_link, .g-contents_article_title,
+       .g-topics_topic_title, .g-issue_issue_title, .g-issue_issue_info,
+       .g-survey_results_link, .g-menu_link, .g-letter_summary_title,
+       .g-letter_summary_author, .g-letter_summary_date,
+       .g-letter_summary_location, .g-letter_post, .g-letter_view_title,
+       .g-letter_view_author, .g-letter_view_post, .g-footer_publisher,
+       .g-footer_tellafriend, .g-footer_archive, .g-footer_pdf
+       {color:#000000;text-decoration:none}
+       .g-article_title:hover, .g-article_url:hover,
+       .g-article_full_story:hover, .g-article_printer_link:hover,
+       .g-contents_article_title:hover, .g-topics_topic_title:hover,
+       .g-issue_issue_title:hover, .g-issue_issue_info:hover,
+       .g-survey_results_link:hover, .g-menu_link:hover,
+       .g-letter_summary_title:hover, .g-letter_summary_author:hover,
+       .g-letter_summary_date:hover, .g-letter_summary_location:hover, 
+       .g-letter_post:hover, .g-letter_view_title:hover,
+       .g-letter_view_author:hover, .g-letter_view_post:hover,
+       .g-footer_publisher:hover, .g-footer_tellafriend:hover,
+       .g-footer_archive:hover, .g-footer_pdf:hover
+       {color:#FF0000;text-decoration:underline}
+       
+
+-->
+</style>
+
+<!-- Footer Styles -->
+<style type='text/css'>
+<!--
+.a226814927149384-section_heading{color:#FFFFFF;background-color:#000000;font-family:arial;font-size:x-small;font-weight:bold;font-style:normal;text-decoration:none;text-align:left}
+.a226814927149384-footer_publisher{color:#000000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:normal;font-style:normal;text-decoration:none}
+.a226814927149384-footer_publisher:hover{color:#FF0000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:normal;font-style:normal;text-decoration:underline}
+.a226814927149384-footer_copyright{color:#000000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:normal;font-style:normal;text-decoration:none}
+.a226814927149384-footer_disclaimer{color:#000000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:normal;font-style:normal;text-decoration:none}
+.a226814927149384-footer_tellafriend{color:#000000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:bold;font-style:normal;text-decoration:underline}
+.a226814927149384-footer_tellafriend:hover{color:#FF0000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:bold;font-style:normal;text-decoration:underline}
+.a226814927149384-footer_archive{color:#000000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:bold;font-style:normal;text-decoration:none}
+.a226814927149384-footer_archive:hover{color:#FF0000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:bold;font-style:normal;text-decoration:underline}
+.a226814927149384-footer_pdf{color:#000000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:bold;font-style:normal;text-decoration:none}
+.a226814927149384-footer_pdf:hover{color:#FF0000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:bold;font-style:normal;text-decoration:underline}
+
+-->
+</style>
+
+
+<!-- Article View Styles -->
+<style type='text/css'>
+<!--
+.a226814927144888-section_heading{color:#FFFFFF;background-color:#000000;font-family:arial;font-size:x-small;font-weight:bold;font-style:normal;text-decoration:none;text-align:left}
+.a226814927144888-contents_article_title{color:#000000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:normal;font-style:normal;text-decoration:underline}
+.a226814927144888-contents_article_title:hover{color:#FF0000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:normal;font-style:normal;text-decoration:underline}
+
+-->
+</style>
+
+
+<!-- Article View Styles -->
+<style type='text/css'>
+<!--
+.a226814927144469-section_heading{color:#FFFFFF;background-color:#000000;font-family:arial;font-size:x-small;font-weight:bold;font-style:normal;text-decoration:none;text-align:left}
+.a226814927144469-contents_article_title{color:#000000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:normal;font-style:normal;text-decoration:underline}
+.a226814927144469-contents_article_title:hover{color:#FF0000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:normal;font-style:normal;text-decoration:underline}
+
+-->
+</style>
+
+
+<!-- Footer Styles -->
+<style type='text/css'>
+<!--
+.a226814927151492-section_heading{color:#FFFFFF;background-color:#000000;font-family:arial;font-size:x-small;font-weight:bold;font-style:normal;text-decoration:none;text-align:left}
+.a226814927151492-footer_publisher{color:#000000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:normal;font-style:normal;text-decoration:none}
+.a226814927151492-footer_publisher:hover{color:#FF0000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:normal;font-style:normal;text-decoration:underline}
+.a226814927151492-footer_copyright{color:#000000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:normal;font-style:normal;text-decoration:none}
+.a226814927151492-footer_disclaimer{color:#000000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:normal;font-style:normal;text-decoration:none}
+.a226814927151492-footer_tellafriend{color:#000000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:bold;font-style:normal;text-decoration:underline}
+.a226814927151492-footer_tellafriend:hover{color:#FF0000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:bold;font-style:normal;text-decoration:underline}
+.a226814927151492-footer_archive{color:#000000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:bold;font-style:normal;text-decoration:none}
+.a226814927151492-footer_archive:hover{color:#FF0000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:bold;font-style:normal;text-decoration:underline}
+.a226814927151492-footer_pdf{color:#000000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:bold;font-style:normal;text-decoration:none}
+.a226814927151492-footer_pdf:hover{color:#FF0000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:bold;font-style:normal;text-decoration:underline}
+
+-->
+</style>
+
+</head> 
+<body bgcolor="#EEEEEE" TEXT="#000000" >
+ <div align="Left"> <!--IMN:TOP--><table bgcolor="#000000" border="0" cellpadding="1" cellspacing="0" width="650" >
+<tr><td> <table bgcolor="#FFFFFF" border="0" cellpadding="0" cellspacing="0" width="100%" cols="1">
+       <tr><td width="644" valign="top" bgcolor="#FFFFFF"><!-- 1,1:footer -->
+                       
+                               
+                               
+                                
+       
+
+       
+       
+
+
+        
+<table width="100%" border="0" cellspacing="0" cellpadding="4" bgcolor="#FFFFFF">
+
+<tr><td bgcolor="#FFFFFF">
+
+
+       
+       <div align="left">
+       <table border="0" cellpadding="2" cellspacing="0" width="100%" bgcolor="#FFFFFF">
+         <tr>
+           <td>
+        
+               <font face="verdana,arial" size="1">
+                
+               </font>
+        
+               </td>
+        
+                               <td align="right" valign="top">
+                
+                       <font face="verdana,arial" size="1">
+                       
+                               
+                       
+                       
+                       <b>
+                       <a href="http://www.imakenews.com/eletra/mod_input_proc.cfm?mod_name=tell_friend_form&XXDESXXuser=mediaunspun&XXDESXXthanks=Thank%20You%2E&XXDESXXsubject=Check%20this%20out%3A%20%5B%5Btitle%5D%5D&XXDESXXheading=&XXDESXXbackto=http://newsletter.mediaunspun.com/index000018970.cfm&XXDESXXissue_id=18970&XXDESXXtitle=M%20E%20D%20I%20A%20%20U%20N%20S%20P%20U%20N"
+                        class="a226814927149384-footer_tellafriend">
+                       <font size=4>Pass it on...</font></a></b>
+                       
+                       </font>
+                
+                       </td>
+               
+               
+               
+         </tr>
+       </table></div>
+        
+       </td></tr></table>
+       
+
+                               
+       
+       
+
+
+
+
+
+
+
+
+
+
+
+
+       
+
+                       
+                       
+                       <!-- 1,2:header -->
+                       
+                               
+                                       
+                                
+
+
+
+       
+       
+               
+        
+               
+        
+        
+<table width="100%" border="0" cellspacing="0" cellpadding="4" bgcolor="#FFFFFF">
+
+<tr><td bgcolor="#FFFFFF">
+
+
+               
+               <table border="0" cellpadding="1" cellspacing="0" width="100%">
+               
+               <tr><td colspan = 3>
+
+                       
+                       
+                       
+                                               
+                        
+                               
+                                       
+                                               
+                                       
+                               
+                        
+                               <a href="http://www.mediaunspun.com">
+                               
+                               <img src="http://a298.g.akamai.net/7/298/5382/081402092715/www.imakenews.com/mediaunspun/mediaunspun_logo.GIF" BORDER="0" alt="M E D I A  U N S P U N" hspace="6" vspace="1" align="top" width="150" ><br>
+                               
+                               </a>
+                        
+                               
+                                       <em><font face="Arial" size="3">
+                                       
+                                       What the Press is Reporting and Why (<a href="http://www.mediaunspun.com">www.mediaunspun.com</a>)
+                                       
+                                       </font></em>
+                               
+                       
+                       
+               </td></tr>
+               
+               
+               <tr><td colspan="3"><hr noshade size="1"></td></tr>
+                
+               
+                <tr>
+                
+                               <td align="left" width="33%">
+                        
+                               <font face="Verdana, Arial" size="1">
+                               
+                               
+                                       
+                                       
+                                               
+                                                       Wednesday, August 14, 2002
+                                               
+                                       
+                                       
+                                       
+                               </font>
+                       
+                       </td>
+                
+                               <td align="center" width="34%">
+                        
+                       </td>
+                
+                               <td align="right" width="33%">
+                        
+                       </td>
+                
+               </tr>
+               </table>
+                
+       </td></tr></table>
+       
+
+                               
+       
+
+
+
+                       
+                                                       
+                       
+                       
+                       <!-- COLUMN: 1 -->
+               
+               </td></tr></table> <table bgcolor="#FFFFFF" border="0" cellpadding="0" cellspacing="0" width="100%" cols="2">
+       <tr><td width="483" valign="top" bgcolor="#FFFFFF"><!-- 2,1:contents -->
+                       
+                               
+                                
+
+
+
+               
+        
+        
+       
+       
+       
+
+        
+<font face="verdana,arial" size="2"><br><FONT face=Arial size=4><STRONG>Top Spins...</STRONG></FONT></font>
+<table width="100%" border="0" cellspacing="0" cellpadding="4" bgcolor="#FFFFFF">
+
+<tr><td bgcolor="#FFFFFF">
+
+
+       
+       
+       <TABLE bgcolor="#FFFFFF" border="0" cellpadding="3" cellspacing="0" width="100%">
+       
+               
+        
+       <tr bgcolor="#FFFFFF"><td>
+          
+                       
+                       
+                       <font face="Verdana,Arial" size="1">
+                       
+                       
+                       
+                       
+                               <A HREF="#a87727"
+                               
+                                       
+                                               class="a226814927144888-contents_article_title"
+                                       
+                               >
+                       
+                                               
+                               
+                       
+                       SEC Exposes Big Blue's Pink Slips
+                       
+                               
+                       
+                       
+                       
+                       </a></font>
+                       
+                       
+                       
+                       
+                       
+                       <br>
+                </td></tr> 
+               
+       
+       </table>
+        
+       </td></tr></table>
+       
+
+                               
+
+
+
+
+
+                       
+                                       
+                       
+                       
+                       <!-- 2,2:contents -->
+                       
+                               
+                                
+
+
+
+               
+        
+        
+       
+       
+       
+       
+
+        
+<table width="100%" border="0" cellspacing="0" cellpadding="4" bgcolor="#FFFFFF">
+
+<tr><td bgcolor="#FFFFFF">
+
+
+       
+       
+       <TABLE bgcolor="#FFFFFF" border="0" cellpadding="3" cellspacing="0" width="100%">
+       
+                
+        
+       <tr bgcolor="#FFFFFF"><td>
+          
+                       
+                       
+                       <font face="Verdana,Arial" size="1">
+                       
+                       
+                       
+                       
+                               <A HREF="#a87728"
+                               
+                                       
+                                               class="a226814927144469-contents_article_title"
+                                       
+                               >
+                       
+                                               
+                               
+                       
+                       Synergy and Betrayal at Vivendi
+                       
+                               
+                       
+                       
+                       
+                       </a></font>
+                       
+                       
+                       
+                       
+                       
+                       <br>
+                </td></tr> <tr><td><img src="http://www.imakenews.com/eletra/empty.gif" height="1" width="1"></td></tr> 
+               
+        
+       <tr bgcolor="#FFFFFF"><td>
+          
+                       
+                       
+                       <font face="Verdana,Arial" size="1">
+                       
+                       
+                       
+                       
+                               <A HREF="#a87730"
+                               
+                                       
+                                               class="a226814927144469-contents_article_title"
+                                       
+                               >
+                       
+                                               
+                               
+                       
+                       Other Stories
+                       
+                               
+                       
+                       
+                       
+                       </a></font>
+                       
+                       
+                       
+                       
+                       
+                       <br>
+                </td></tr> 
+               
+       
+       </table>
+        
+       </td></tr></table>
+       
+
+                               
+
+
+
+
+
+                       
+                                       
+                       
+                       
+                       <!-- 2,3:article_view -->
+                       
+                               
+                               
+                                 
+
+
+
+               
+        
+        
+       
+       
+        
+<font face="verdana,arial" size="2"><br></font>
+<table width="100%" border="0" cellspacing="0" cellpadding="4" bgcolor="#FFFFFF">
+
+<tr><td bgcolor="#FFFFFF">
+
+
+       
+       
+       <TABLE bgcolor="#FFFFFF" border="0" cellpadding="3" cellspacing="0" width="100%">
+       
+               
+        
+       <tr bgcolor="#FFFFFF"><td>
+        <a name="a66461"></a>   
+                       
+                       
+                       <font face="Arial" size="4"><b>
+                       
+                       
+                       
+                       
+                       
+                       
+                       
+                       
+                       </b>
+                       </font>
+                       
+                       
+                       
+                       
+                       
+                       <br>
+                
+                       
+                
+                       <font face="verdana,arial" size="2">
+                        
+                               
+                                       
+                                        
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+                                        
+               
+               
+        
+       
+       
+
+
+
+                                       
+                                       <P>Media Unspun serves business news and analysis, authoritatively and irreverently, every business day. An annual subscription costs $50, less than a dollar a week. If your four-week free trial is coming to an end soon, please visit <A HREF="http://www.mediaunspun.com/subscribe.html">http://www.mediaunspun.com/subscribe.html</A>  and sign up via credit card or check.<br>
+</P>
+                                       
+                                       <br>
+                               
+                       
+                       </font>
+                </td></tr> 
+               
+       
+       </table>
+        
+       </td></tr></table>
+       
+
+                               
+
+
+
+
+
+
+                               
+                                                                       
+
+                       
+                       
+                       <!-- 2,4:article_view -->
+                       
+                               
+                               
+                                 
+
+
+
+               
+        
+        
+       
+       
+        
+<font face="verdana,arial" size="2"><FONT face=Verdana size=1><STRONG>Sponsor</STRONG></FONT></font>
+<table width="100%" border="0" cellspacing="0" cellpadding="1" bgcolor="#000000"> 
+<tr><td>
+<table width="100%" border="0" cellspacing="0" cellpadding="4" bgcolor="#FFFFCC">
+
+<tr><td bgcolor="#FFFFCC">
+
+
+       
+       
+       <TABLE bgcolor="#FFFFCC" border="0" cellpadding="3" cellspacing="0" width="100%">
+       
+               
+        
+       <tr bgcolor="#FFFFCC"><td>
+        <a name="a59384"></a> 
+                       
+                
+                       <font face="verdana,arial" size="2">
+                        
+                               
+                                       
+                                        
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+                                        
+               
+               
+        
+       
+       
+
+
+
+                                       
+                                       <P>Ken Fisher offers his Quarterly Report for high net worth investors FREE of cost & without obligation. Access the same investment research he uses to guide his clients at:<br>
+<A HREF="http://pcg.fisherinvestments.com/newrespond/letter.asp?site=UNSP&KC=1229EFCAD0000">http://pcg.fisherinvestments.com/newrespond/letter.asp?site=UNSP&KC=1229EFCAD0000</A> </P>
+                                       
+                                       <br>
+                               
+                       
+                       </font>
+                </td></tr> 
+               
+       
+       </table>
+        </td></tr></table>
+       </td></tr></table>
+       
+
+                               
+
+
+
+
+
+
+                               
+                                                                       
+
+                       
+                       
+                       <!-- 2,5:article_view -->
+                       
+                               
+                               
+                                 
+
+
+
+               
+        
+        
+       
+        
+<table width="100%" border="0" cellspacing="0" cellpadding="4" bgcolor="#FFFFFF">
+
+<tr><td bgcolor="#FFFFFF">
+
+
+       
+       
+       <TABLE bgcolor="#FFFFFF" border="0" cellpadding="3" cellspacing="0" width="100%">
+       
+               
+        
+       <tr bgcolor="#FFFFFF"><td>
+        <a name="a87727"></a>   
+                       
+                       
+                       <font face="Arial" size="4"><b>
+                       
+                       
+                       
+                       
+                       
+                       SEC Exposes Big Blue's Pink Slips
+                       
+                       
+                       </b>
+                       </font>
+                       
+                       
+                       
+                       
+                       
+                       <br>
+                
+                       
+                
+                       <font face="verdana,arial" size="2">
+                        
+                               
+                                       
+                                        
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+                                        
+               
+               
+        
+       
+       
+
+
+
+                                       
+                                       <P>Does the Securities and Exchange Commission have a press pass yet? It seems to be bringing us all our news lately. On the day of the deadline for companies to certify their financial statements with the SEC, the business press squirmed and waited for the next Enron or WorldCom. (We might eat these words tomorrow, but we doubt it.) In an unrelated confession, IBM gave the commission its latest layoff numbers.</P><P>
+IBM talked about pink slips during its second-quarter earnings report, but with a vagueness worthy of your daily horoscope. ("Capricorn: Career changes may be on their way...") Only after "months of surreptitious layoff notices" did the company admit that it's cutting more than 15,600 jobs, said the AP. That's about 5% of its workforce, and a lot more than pundits expected. An IBM spokesperson told the Wall Street Journal the higher number was due to "rebalancing" and more employees than
+expected taking voluntary layoffs. </P><P>
+Sorry, we're still back on "rebalancing." Did IBM "rightsize" last quarter, too?</P><P>
+IBM's news was still trickling out Wednesday morning, but some details were available. About 1,400 workers got cut from IBM's microelectronics unit, and most of the rest were from IT services and consulting. (That ought to make IBM's new employees from PricewaterhouseCoopers feel all warm and fuzzy inside.) Look for news updates from cities that will see the cuts, such as Austin and Raleigh. </P><P>
+OK, none of this is good. Two years into the tech slump, we're still tired of seeing people get sacked. But was it really so bad that IBM only revealed it because of new accounting regulations? Nah, Big Blue was always known for "stealth layoffs," as CNN put it, but current corporate scrutiny forced it to 'fess up for once. Until now, IBM would acknowledge the latest layoffs if reporters called and asked, but wouldn't give specifics. Yeesh. - Jen Muehlbauer</P><P>
+IBM Cut 5% of Staff in Period, Double the Expected Number<br>
+<A HREF="http://online.wsj.com/article/0,,SB1029282408667791835,00.html">http://online.wsj.com/article/0,,SB1029282408667791835,00.html</A> <br>
+(Paid subscription required.) </P><P>
+IBM to Cut Over 15,000 Employees (AP)<br>
+<A HREF="http://tinyurl.com/10kz">http://tinyurl.com/10kz</A> </P><P>
+IBM confirms 15,600 job cuts (Reuters)<br>
+<A HREF="http://www.msnbc.com/news/793777.asp">http://www.msnbc.com/news/793777.asp</A>  </P><P>
+IBM cutting 15,000 jobs <br>
+<A HREF="http://news.com.com/2100-1001-949677.html">http://news.com.com/2100-1001-949677.html</A> </P><P>
+IBM job cuts exceed 15,600<br>
+<A HREF="http://money.cnn.com/2002/08/13/technology/ibm/index.htm">http://money.cnn.com/2002/08/13/technology/ibm/index.htm</A> </P><P>
+IBM puts job cuts at 15,600, with fewer than 50 in this state<br>
+<A HREF="http://seattlepi.nwsource.com/business/82508_ibm14.shtml">http://seattlepi.nwsource.com/business/82508_ibm14.shtml</A> </P>
+                                       
+                                       <br>
+                               
+                       
+                       </font>
+                </td></tr> 
+               
+       
+       </table>
+        
+       </td></tr></table>
+       
+
+                               
+
+
+
+
+
+
+                               
+                                                                       
+
+                       
+                       
+                       <!-- 2,6:article_view -->
+                       
+                               
+                               
+                                 
+
+
+
+               
+        
+        
+       
+       
+        
+<font face="verdana,arial" size="2"><FONT face=Verdana size=1><STRONG>Sponsor</STRONG></FONT></font>
+<table width="100%" border="0" cellspacing="0" cellpadding="1" bgcolor="#000000"> 
+<tr><td>
+<table width="100%" border="0" cellspacing="0" cellpadding="4" bgcolor="#FFFFCC">
+
+<tr><td bgcolor="#FFFFCC">
+
+
+       
+       
+       <TABLE bgcolor="#FFFFCC" border="0" cellpadding="3" cellspacing="0" width="100%">
+       
+               
+        
+       <tr bgcolor="#FFFFCC"><td>
+        <a name="a75853"></a> 
+                       
+                
+                       <font face="verdana,arial" size="2">
+                        
+                               
+                                       
+                                       You've heard about identity management, but do you know about the opportunities and business models that will emerge as a result? <a href="http://release1.edventure.com/executivesummary.cfm?MCode=Unspun">Download</a> a free executive summary of Esther Dyson's coverage of identity management in Release 1.0. Learn more about the expanding market for these services and applications.
+                                       
+                                       <br>
+                               
+                       
+                       </font>
+                </td></tr> 
+               
+       
+       </table>
+        </td></tr></table>
+       </td></tr></table>
+       
+
+                               
+
+
+
+
+
+
+                               
+                                                                       
+
+                       
+                       
+                       <!-- 2,7:article_view -->
+                       
+                               
+                               
+                                 
+
+
+
+               
+        
+        
+       
+       
+        
+<table width="100%" border="0" cellspacing="0" cellpadding="4" bgcolor="#FFFFFF">
+
+<tr><td bgcolor="#FFFFFF">
+
+
+       
+       
+       <TABLE bgcolor="#FFFFFF" border="0" cellpadding="3" cellspacing="0" width="100%">
+       
+                
+        
+       <tr bgcolor="#FFFFFF"><td>
+        <a name="a87728"></a>   
+                       
+                       
+                       <font face="Arial" size="4"><b>
+                       
+                       
+                       
+                       
+                       
+                       Synergy and Betrayal at Vivendi
+                       
+                       
+                       </b>
+                       </font>
+                       
+                       
+                       
+                       
+                       
+                       <br>
+                
+                       
+                
+                       <font face="verdana,arial" size="2">
+                        
+                               
+                                       
+                                        
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+                                        
+               
+               
+        
+       
+       
+
+
+
+                                       
+                                       <P>Synergy always was a fuzzy concept. Now Vivendi Universal's top man has slammed the lid on it. The French company announced today that it's ready to peddle $9.8 billion in assets to rustle up some cash. First up on the block? Synergy-less U.S. book publisher Houghton Mifflin. </P><P>
+It's unclear whether new chairman Jean-Rene Fourtou has genuine turnaround muscle, or whether he and Vivendi's board are simply following the winds of post-merger fashion. But when you owe $18.7 billion, you get real practical, real fast. The Guardian reported that Vivendi's share price sank 5% on Tuesday when investors got the willies about the company's impending announcement on its financial health. But the company had positive news to report: It's making money. Revenue in the first half was 
+up 13%, higher than analysts' estimates of a 7.7% boost. </P><P>
+Details are scant on the breadth of Fourtou's restructuring efforts, with more information expected at the next board meeting on September 25, according to reporters. Houghton Mifflin, acquired a year ago for $1.7 billion, and a vague explanation that included the "Curious George" character, were the only properties named for sale so far. The Guardian speculated that Vivendi will also sell its U.S. video games business and possibly its stake in the French mobile phone company SFR, a debatable
+sale because of the cash it generates, according to the newspaper. </P><P>
+Meanwhile, Fourtou's predecessor, Jean-Marie Messier, continues to advocate empire-building. The New York Post said its sources say Messier hopes his former employer will feel generous enough to let him continue to reside in his $17 million Manhattan abode. And Bloomberg reported earlier this week that an unrepentant Messier is penning a memoir as he vacations in the Mediterranean. The working title? "How I Was Betrayed."  - Deborah Asbrand </P><P>
+Vivendi to Sell Publisher Houghton Mifflin (Reuters)<br>
+<A HREF="http://www.washingtonpost.com/wp-dyn/articles/A15954-2002Aug14.html">http://www.washingtonpost.com/wp-dyn/articles/A15954-2002Aug14.html</A> </P><P>
+Vivendi investors expect the worst<br>
+<A HREF="http://www.guardian.co.uk/business/story/0,3604,774190,00.html">http://www.guardian.co.uk/business/story/0,3604,774190,00.html</A> </P><P>
+Vivendi to Sell $9.8 Billion In Assets, Including Houghton<br>
+<A HREF="http://online.wsj.com/article/0,,SB102931297119161715,00.html">http://online.wsj.com/article/0,,SB102931297119161715,00.html</A> <br>
+(Paid subscription required.) </P><P>
+Ousted Messier Aims To Score $17m Vivendi Pad<br>
+<A HREF="http://www.nypost.com/business/54701.htm">http://www.nypost.com/business/54701.htm</A> </P><P>
+Ex-Chief of Vivendi Plans Tell-All Book (Bloomberg)<br>
+<A HREF="http://www.nytimes.com/2002/08/12/business/media/12VIVE.html">http://www.nytimes.com/2002/08/12/business/media/12VIVE.html</A> </P>
+                                       
+                                       <br>
+                               
+                       
+                       </font>
+                </td></tr> 
+               
+        
+       <tr bgcolor="#FFFFFF"><td>
+        <a name="a87730"></a>   
+                       
+                       
+                       <font face="Arial" size="4"><b>
+                       
+                       
+                       
+                       
+                       
+                       Other Stories
+                       
+                       
+                       </b>
+                       </font>
+                       
+                       
+                       
+                       
+                       
+                       <br>
+                
+                       
+                
+                       <font face="verdana,arial" size="2">
+                        
+                               
+                                       
+                                        
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+                                        
+               
+               
+        
+       
+       
+
+
+
+                                       
+                                       <P>A Top AOL Manager Has Left Company<br>
+<A HREF="http://www.nytimes.com/2002/08/14/technology/14AOL.html">http://www.nytimes.com/2002/08/14/technology/14AOL.html</A> </P><P>
+Fed Holds Steady on Interest Rates <br>
+<A HREF="http://www.washingtonpost.com/wp-dyn/articles/A14636-2002Aug13.html">http://www.washingtonpost.com/wp-dyn/articles/A14636-2002Aug13.html</A> </P><P>
+Amtrak halts all high-speed service after finding cracks<br>
+<A HREF="http://www.sunspot.net/bal-te.train14aug14.story">http://www.sunspot.net/bal-te.train14aug14.story</A> </P><P>
+AOL lets resigning exec keep stock options <br>
+<A HREF="http://www.usatoday.com/money/industries/technology/2002-08-13-aol-pittman_x.htm">http://www.usatoday.com/money/industries/technology/2002-08-13-aol-pittman_x.htm</A> </P><P>
+Lucent licensing deal with Winstar focus of probe (AP)<br>
+<A HREF="http://www.bayarea.com/mld/mercurynews/business/3861117.htm">http://www.bayarea.com/mld/mercurynews/business/3861117.htm</A> </P><P>
+Study Says Net Could Benefit Music Firms<br>
+<A HREF="http://www.latimes.com/business/la-fi-music14aug14.story">http://www.latimes.com/business/la-fi-music14aug14.story</A> </P><P>
+Eisner Crimping His Own Style<br>
+<A HREF="http://www.latimes.com/business/la-fi-disney14aug14.story">http://www.latimes.com/business/la-fi-disney14aug14.story</A> </P><P></P><P>
+Severance claims by Enron former execs anger ex-workers<br>
+<A HREF="http://www.chron.com/cs/CDA/story.hts/business/1533657">http://www.chron.com/cs/CDA/story.hts/business/1533657</A> </P><P>
+Princeton removes dean after Yale Web site flap (AP)<br>
+<A HREF="http://www.siliconvalley.com/mld/siliconvalley/3857890.htm">http://www.siliconvalley.com/mld/siliconvalley/3857890.htm</A> </P><P>
+Frisbee golf creator dies, may land on someone's roof (SF Chronicle)<br>
+<A HREF="http://seattlepi.nwsource.com/national/82560_frisbee14.shtml">http://seattlepi.nwsource.com/national/82560_frisbee14.shtml</A> </P><P>
+Will Kinsley's Slate Get Wiped?<br>
+<A HREF="http://www.ojr.org/ojr/kramer/1029281360.php">http://www.ojr.org/ojr/kramer/1029281360.php</A> </P><P>
+Hollywood, Russian Bicker Over Bass<br>
+<A HREF="http://www.cnn.com/2002/SHOWBIZ/News/08/13/bassspace.hollywood.ap/">http://www.cnn.com/2002/SHOWBIZ/News/08/13/bassspace.hollywood.ap/</A> </P>
+                                       
+                                       <br>
+                               
+                       
+                       </font>
+                </td></tr> 
+               
+       
+       </table>
+        
+       </td></tr></table>
+       
+
+                               
+
+
+
+
+
+
+                               
+                                                                       
+
+                       
+                       
+                       <!-- 2,8:article_view -->
+                       
+                               
+                               
+                                 
+
+
+
+               
+        
+        
+       
+       
+        
+<font face="verdana,arial" size="2"><FONT face=Verdana size=1><STRONG>Sponsor</STRONG></FONT></font>
+<table width="100%" border="0" cellspacing="0" cellpadding="1" bgcolor="#000000"> 
+<tr><td>
+<table width="100%" border="0" cellspacing="0" cellpadding="4" bgcolor="#FFFFCC">
+
+<tr><td bgcolor="#FFFFCC">
+
+
+       
+       
+       <TABLE bgcolor="#FFFFCC" border="0" cellpadding="3" cellspacing="0" width="100%">
+       
+               
+        
+       <tr bgcolor="#FFFFCC"><td>
+        <a name="a59804"></a> 
+                       
+                
+                       <font face="verdana,arial" size="2">
+                        
+                               
+                                       
+                                       Do you want to reach the Net's savviest audience?<BR>
+Advertise in Media Unspun.<br>
+Contact Erik Vanderkolk for details at <a href="mailto:erikvanderkolk@yahoo.com">erikvanderkolk@yahoo.com</A> today.
+                                       
+                                       <br>
+                               
+                       
+                       </font>
+                </td></tr> 
+               
+       
+       </table>
+        </td></tr></table>
+       </td></tr></table>
+       
+
+                               
+
+
+
+
+
+
+                               
+                                                                       
+
+                       
+                       
+                       <!-- 2,9:article_view -->
+                       
+                               
+                               
+                                 
+
+
+
+               
+        
+        
+       
+       
+        
+<table width="100%" border="0" cellspacing="0" cellpadding="4" bgcolor="#FFFFFF">
+
+<tr><td bgcolor="#FFFFFF">
+
+
+       
+       
+       <TABLE bgcolor="#FFFFFF" border="0" cellpadding="3" cellspacing="0" width="100%">
+       
+               
+        
+       <tr bgcolor="#FFFFFF"><td>
+        <a name="a59810"></a>   
+                       
+                       
+                       <font face="Arial" size="4"><b>
+                       
+                       
+                       
+                       
+                       
+                       Staff
+                       
+                       
+                       </b>
+                       </font>
+                       
+                       
+                       
+                       
+                       
+                       <br>
+                
+                       
+                
+                       <font face="verdana,arial" size="2">
+                        
+                               
+                                       
+                                       Written by Deborah Asbrand (<a href="mailto:dasbrand@world.std.com">dasbrand@world.std.com</a>), Keith Dawson (<a href="mailto:dawson@world.std.com">dawson@world.std.com</a>), Jen Muehlbauer (<a href="mailto:jen@englishmajor.com">jen@englishmajor.com</a>), and Lori Patel (<a href="mailto:loripatel@hotmail.com">loripatel@hotmail.com</a>).
+<P>
+Copyedited by Jim Duffy (<a href="mailto:jimduffy86@yahoo.com">jimduffy86@yahoo.com</a>).
+<P>
+Marketing: Cowpoke Productions (<a href="http://www.cowpokeproductions.com">cowpokeproductions.com</a>).
+<P>
+Advertising: Erik Vanderkolk (<a href="mailto:erikvanderkolk@yahoo.com">erikvanderkolk@yahoo.com)</a>.
+<P>
+Editor and publisher: Jimmy Guterman (<a href="mailto:guterman@vineyard.com">guterman@vineyard.com</a>).
+<P>
+Media Unspun is produced by <a href="http://guterman.com">The Vineyard Group Inc.</a>
+<BR>Copyright 2002 Media Unspun, Inc., and The Vineyard Group, Inc.
+<BR>Subscribe already, willya? <a href="http://www.mediaunspun.com">http://www.mediaunspun.com</a>
+<P>
+Redistribution by email is permitted as long as a link to <a href="http://newsletter.mediaunspun.com">http://newsletter.mediaunspun.com</a> is included.
+                                       
+                                       <br>
+                               
+                       
+                       </font>
+                </td></tr> 
+               
+       
+       </table>
+        
+       </td></tr></table>
+       
+
+                               
+
+
+
+
+
+
+                               
+                                                                       
+
+                       
+                       
+                       <!-- COLUMN: 2 -->
+               
+               </td><td width="5" valign="top" align="center" ><img src="http://www.imakenews.com/eletra/empty.gif" height="1" width="5"></td>
+                       <td width="1" valign="top" align="center" bgcolor="#888888"><img src="http://www.imakenews.com/eletra/empty.gif" height="1" width="1"></td>
+                       <td width="5" valign="top" align="center" ><img src="http://www.imakenews.com/eletra/empty.gif" height="1" width="5"></td><td width="161" valign="top" bgcolor="#FFFFFF"><!-- 3,1:subscription -->
+                       
+                         
+                         
+                               
+                               
+                                
+
+
+
+       
+       
+<table width="100%" border="0" cellspacing="0" cellpadding="1" bgcolor="#000000"> 
+<tr><td>
+<table width="100%" border="0" cellspacing="0" cellpadding="4" bgcolor="#EEEEEE">
+
+<tr><td
+bgcolor="#000000">
+       <font face="arial" size="2" color="#FFFFFF"><b>
+               
+               SUBSCRIBE
+               
+       </b></font>
+</td></tr>
+<tr><td bgcolor="#EEEEEE">
+
+
+        
+               
+                       
+                       
+                                                                       
+        
+               
+        
+               
+        
+               
+        
+               
+        
+               <form method="POST" action="http://www.imakenews.com/eletra/mod_input_proc.cfm">
+                
+<p><font face="verdana,arial" size="1">
+ Enter your email address in the box below to receive a free four-week trial of Media Unspun: 
+</font></p>
+               
+        
+                 <input type="hidden" name="XXDESXXuser" value="mediaunspun">
+                 
+                 
+                 <input type="hidden" name="mod_name" value="subscription">
+                 <input type="hidden" name="XXDESXXfrom_address" value="guterman@vineyard.com">
+          <input type="hidden" name="XXDESXXfrom_name" value="Media Unspun">
+                 
+                 
+                 
+                 
+                 
+                 
+                        <input type="hidden" name="XXDESXXpage" value="http://newsletter.mediaunspun.com/index000018970.cfm">            
+                 
+       
+       
+    
+
+       
+       
+               
+    
+       <p><input type="text" name="XXDESXXemail_address" size="15" maxlength="100">
+               <br><font face="verdana" size="1">
+        
+               
+                       <input type="radio" value="Add" name="XXDESXXsubscribe_op" checked>
+                       
+                       Add
+                       
+               
+        
+               <input type="radio" value="Remove" name="XXDESXXsubscribe_op">
+               
+               Remove<br>
+               
+        
+               <input type="checkbox" name="XXDESXXemail_type" value="htm" checked>
+                Send as HTML<br>
+               
+          
+         <input type="submit" value="Submit" name="add">&nbsp;
+       
+       </font></p>
+    
+
+
+               </form> 
+       
+
+ </td></tr></table>
+       </td></tr></table><font face="verdana,arial" size="2"><FONT face=Verdana size=1><STRONG>
+<P align=center><BR>Newsletter Services <BR>Provided by <BR></STRONG></FONT><A href="http://www.imakenews.com/affiliate.cfm?a_id=unspun"><FONT face=Verdana size=1><STRONG>iMakeNews.com</STRONG></FONT></A></P></font>
+       
+
+                                       
+                                       
+                                       
+                       
+                       
+                       <!-- 3,2:survey_view -->
+                       
+                               
+                                
+
+
+
+       
+       
+       
+
+
+
+
+
+
+
+
+
+                                                                                               
+                       
+                       
+                       <!-- 3,3:menu -->
+                       
+                               
+                                
+       
+       
+
+
+
+
+
+
+
+
+                       
+                       
+                       
+                       <!-- COLUMN: 3 -->
+               
+               </td></tr></table> <table bgcolor="#FFFFFF" border="0" cellpadding="0" cellspacing="0" width="100%" cols="1">
+       <tr><td width="644" valign="top" bgcolor="#FFFFFF"><!-- 4,1:footer -->
+                       
+                               
+                               
+                                
+       
+
+       
+       
+
+
+        
+<table width="100%" border="0" cellspacing="0" cellpadding="4" bgcolor="#FFFFFF">
+
+<tr><td bgcolor="#FFFFFF">
+
+
+       
+       <div align="left">
+       <table border="0" cellpadding="2" cellspacing="0" width="100%" bgcolor="#FFFFFF">
+         <tr>
+           <td>
+        
+               <font face="verdana,arial" size="1">
+                
+               </font>
+        
+               </td>
+        
+                               <td align="right" valign="top">
+                
+                       <font face="verdana,arial" size="1">
+                       
+                               
+                       
+                       
+                       <b>
+                       <a href="http://www.imakenews.com/eletra/mod_input_proc.cfm?mod_name=tell_friend_form&XXDESXXuser=mediaunspun&XXDESXXthanks=Thank%20You%2E&XXDESXXsubject=Check%20this%20out%3A%20%5B%5Btitle%5D%5D&XXDESXXheading=&XXDESXXbackto=http://newsletter.mediaunspun.com/index000018970.cfm&XXDESXXissue_id=18970&XXDESXXtitle=M%20E%20D%20I%20A%20%20U%20N%20S%20P%20U%20N"
+                        class="a226814927151492-footer_tellafriend">
+                       <font size=4>TELL A FRIEND</font></a></b>
+                       
+                       </font>
+                
+                       </td>
+               
+               
+               
+         </tr>
+       </table></div>
+        
+       </td></tr></table>
+       
+
+                               
+       
+       
+
+
+
+
+
+
+
+
+
+
+
+
+       
+
+                       
+                       
+                       <!-- COLUMN: 4 -->
+               
+               </td></tr></table> </td></tr></table>
+<!--IMN:BOTTOM-->
+<table border="0" cellpadding="2" cellspacing="0" width="650">
+  <tr><td><font face="verdana,arial" size="1">Powered by <strong><a href="http://www.imakenews.com" target="_top" class="link">iMakeNews.com</a>&#153;</strong></font><br>
+       <font face="verdana,arial" size="1">This email was sent to: xxxxx@yyyyyy.zzz <br><a href="http://www.imakenews.com/eletra/remove.cfm?x=mediaunspun%2Cxxxxx@yyyyyy.zzz">Click here</a> to be instantly removed from this list.<br><a href="http://www.imakenews.com/eletra/change.cfm?x=mediaunspun%2Cxxxxx@yyyyyy.zzz%2Ctxt">Click here</a> to receive future messages in plain text format.<br></font><font face="verdana,arial" size="1"><a
+href="http://www.imakenews.com/eletra/update.cfm?x=mediaunspun%2Cxxxxx@yyyyyy.zzz">Click here</a> to change your subscriber information and preferences.<br></font></tr></table>
+<!--Ver. 7-->
+
+       
+               
+                       
+                
+                
+                 
+               <p>&nbsp;
+                  
+          
+       <img src="http://machina.imakenews.com/E178767,5114587XXmediaunspunXX18970XXXXindex000018970.cfmXXemailXX5114587XXXX0Y0XX1" alt="" height="0" width="0">                 
+          
+          
+               </p>
+               
+       
+
+   
+</div>
+
+
+</body>
+</html>
+
+
+
+------------=_1029331990-31627-4--
+
+
diff --git a/upstream/t/data/welcomelists/mlist_mailman_message b/upstream/t/data/welcomelists/mlist_mailman_message
new file mode 100644 (file)
index 0000000..3b97783
--- /dev/null
@@ -0,0 +1,95 @@
+Received: from usw-sf-list2.yyyyyyyyyyyy.net (usw-sf-fw2.yyyyyyyyyyyy.net
+     [216.136.171.252]) by dogma.slashnull.org (8.11.6/8.11.6) with ESMTP id
+     g7HFlZ603002 for <zzzzzz-sa@zzzzzz.org>; Sat, 17 Aug 2002 16:47:35 +0100
+Received: from usw-sf-list1-b.yyyyyyyyyyyy.net ([10.3.1.13]
+     helo=usw-sf-list1.yyyyyyyyyyyy.net) by usw-sf-list2.yyyyyyyyyyyy.net with
+     esmtp (Exim 3.31-VA-mm2 #1 (Debian)) id 17g5m8-000654-00; Sat,
+     17 Aug 2002 08:46:04 -0700
+Received: from dogma.slashnull.org ([212.17.35.15]) by
+     usw-sf-list1.yyyyyyyyyyyy.net with esmtp (Exim 3.31-VA-mm2 #1 (Debian)) id
+     17g5lM-0005xL-00 for <SpamAssassin-talk@lists.yyyyyyyyyyyy.net>;
+     Sat, 17 Aug 2002 08:45:16 -0700
+Received: (from apache@localhost) by dogma.slashnull.org (8.11.6/8.11.6)
+     id g7HFj8h02977; Sat, 17 Aug 2002 16:45:08 +0100
+X-Authentication-Warning: dogma.slashnull.org: apache set sender to
+     zzzzzz@zzzzzz.org using -f
+Received: from 194.125.173.146 (SquirrelMail authenticated user zzzzzz) by
+     zzzzzz.org with HTTP; Sat, 17 Aug 2002 16:45:08 +0100 (IST)
+Message-Id: <33025.194.125.173.146.1029599108.squirrel@zzzzzz.org>
+From: "Justin Mason" <zzzzzz@zzzzzz.org>
+To: SpamAssassin-talk@lists.yyyyyyyyyyyy.net
+X-Mailer: SquirrelMail (version 1.0.6)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=iso-8859-1
+Content-Transfer-Encoding: 8bit
+Subject: [SAtalk] spam-phrases existing algo
+Sender: spamassassin-talk-admin@lists.yyyyyyyyyyyy.net
+Errors-To: spamassassin-talk-admin@lists.yyyyyyyyyyyy.net
+X-Beenthere: spamassassin-talk@lists.yyyyyyyyyyyy.net
+X-Mailman-Version: 2.0.9-sf.net
+Precedence: bulk
+List-Help: <mailto:spamassassin-talk-request@lists.yyyyyyyyyyyy.net?subject=help>
+List-Post: <mailto:spamassassin-talk@lists.yyyyyyyyyyyy.net>
+List-Subscribe: <https://lists.yyyyyyyyyyyy.net/lists/listinfo/spamassassin-talk>,
+     <mailto:spamassassin-talk-request@lists.yyyyyyyyyyyy.net?subject=subscribe>
+List-Id: Talk about SpamAssassin <spamassassin-talk.lists.yyyyyyyyyyyy.net>
+List-Unsubscribe: <https://lists.yyyyyyyyyyyy.net/lists/listinfo/spamassassin-talk>,
+     <mailto:spamassassin-talk-request@lists.yyyyyyyyyyyy.net?subject=unsubscribe>
+List-Archive: <http://www.geocrawler.com/redir-sf.php3?list=spamassassin-talk>
+X-Original-Date: Sat, 17 Aug 2002 16:45:08 +0100 (IST)
+Date: Sat, 17 Aug 2002 16:45:08 +0100 (IST)
+
+BTW, I should not that this algorithm Paul Graham uses is
+very close to what we've got in spam-phrases code already.
+
+To turn it into pcode:
+
+  mass-check for spamphrases:
+
+    - get mail body, strip HTML, attachments and mail formatting
+    - strip stopwords ("to", "of", "a" etc.)
+    - find pairs of 3-20 letter words
+    - foreach pair:
+      - skip pair if one word is in stoplist of common terms
+      - ++ the frequency of that word-pair
+
+  settle-phrases -- turn mass-check results into a spamphrases file
+
+    - read all spam word-pairs, let NS = number of word-pairs
+    - read all nonspam word-pairs, let NN = number of word-pairs
+    - let bias = NS / NN (compensates for different corpus size)
+    - foreach nonspam word-pair:
+      - wpfreq = (freq in spam) - (frequency in nonspam * bias)
+    - foreach spam word-pair:
+      - if (wordpair was not found in nonspam):
+        - wpfreq *= 10
+    - note the highest score of all rules
+
+  scoring of an incoming message:
+
+    - get mail body, strip HTML, attachments and mail formatting
+    - strip stopwords ("to", "of", "a" etc.)
+    - find pairs of 3-20 letter words
+    - foreach pair:
+      - score += ((wpfreq*10) / highest_score_of_all_rules)
+    - foreach "!" found in text:
+      - score++
+    - return result as "spam phrase score".
+
+So it's quite close to PG's algo, but he also tracks the non-spam
+word-pairs -- which we don't do for SpamAssassin, because they
+overfit to the mass-checker's nonspam mail corpus (generally
+names of friends, etc.)
+
+--j.
+
+
+
+-------------------------------------------------------
+This sf.net email is sponsored by: OSDN - Tired of that same old
+cell phone?  Get a new here for FREE!
+https://www.inphonic.com/r.asp?r=yyyyyyyyyyyy&refcode1=vs3390
+_______________________________________________
+Spamassassin-talk mailing list
+Spamassassin-talk@lists.yyyyyyyyyyyy.net
+https://lists.yyyyyyyyyyyy.net/lists/listinfo/spamassassin-talk
diff --git a/upstream/t/data/welcomelists/mlist_yahoo_groups_message b/upstream/t/data/welcomelists/mlist_yahoo_groups_message
new file mode 100644 (file)
index 0000000..0e90c9f
--- /dev/null
@@ -0,0 +1,138 @@
+Received: from dogma.slashnull.org (dogma.slashnull.org [212.17.35.15])
+        by sonic.xxxxxxxxx.org (Postfix) with ESMTP id 9424D132505
+        for <aaaaaaaa@bbbbbbbbb>; Thu,  1 Aug 2002 14:21:59 -0700 (PDT)
+Received: from intm3.sparklist.com (intm3.sparklist.com [207.250.144.9])
+        by dogma.slashnull.org (8.11.6/8.11.6) with SMTP id g71LMw230398
+        for <ffffffffff.com@zzzzzzz.org>; Thu, 1 Aug 2002 22:22:58 +0100
+Date: Thu, 2 May 2002 00:02:49 +1200
+Subject: [Sigiii-l] [InfoInternational] REINBERGER FOUNDATION GIFT
+To: <ffffffffff.com@zzzzzzz.org>
+From: "Biju K Abraham" <InfoInternational@yahoogroups.com>
+Message-Id: <INTM-6516584-3669405-2002.08.01-16.21.51--ffffffffff.com#zzzzzzz.org@list3.zzzzzz.com>
+MIME-Version: 1.0
+Content-type: multipart/alternative; boundary="------=_NextPart_000_0146_01C1F16C.B224C240"
+
+------=_NextPart_000_0146_01C1F16C.B224C240
+Content-Type: text/plain;
+        charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+REINBERGER FOUNDATION GIFT TO=20
+KENT STATE UNIVERSITY SLIS=20
+
+Kent State University's School of Library and Information Science
+received a gift of $240,000 from the Reinberger Foundation of Cleveland
+for the construction of a unique national center dedicated to training libr=
+arians who=20
+specialize in services for children, young adults and school
+librarianship. The gift was announced in anticipation of National
+Library Week (April 14-20).=20
+=20
+"The Children's Resource Center will offer an environment similar to
+achildren's or elementary school library complete with books,
+multimedia, puppets and a storytelling area," said Associate Professor
+Dr. Carolyn S.Brodie, who has built the School of Library and
+Information Science's collection of materials for youth, and is a
+co-recipient of the Reinberger gift. Brodie recently served as chair of
+the 2000 John Newbery Award Committee.=20
+=20
+The Children's Resource Center will be unique among the nation's library
+schools and will serve as a model classroom for library science programs
+for children's librarians. The Center is designed to be much more than a
+university classroom and will include a children's
+ resource area that will house more than=20
+5,000 children's books, materials, and resources to
+create a focal point for instruction in children's, young adult, and
+school librarianship.=20
+
+The 1,700-square-foot resource center will also
+include a wireless computer network installed with specialized software
+and other resources used in children's and school libraries. For more
+information contact Megan Harding, (330) 672-0419.=20
+
+ - Moderator -
+
+
+------=_NextPart_000_0146_01C1F16C.B224C240
+Content-Type: text/html; charset=US-ASCII
+Content-Transfer-Encoding: 7bit
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<HTML><HEAD>
+<META content="text/html; charset=iso-8859-1" http-equiv=Content-Type>
+<META content="MSHTML 5.00.2614.3500" name=GENERATOR>
+<STYLE></STYLE>
+</HEAD>
+<BODY bgColor=#ffffff>
+
+
+<DIV align=center><FONT face=Arial size=2><FONT color=#ff0000 
+size=4><U><STRONG>REINBERGER FOUNDATION GIFT TO 
+</STRONG></U></FONT></FONT></DIV>
+<DIV align=center><FONT face=Arial size=2><FONT color=#ff0000 
+size=4><U><STRONG>KENT STATE UNIVERSITY SLIS </STRONG></U></FONT></FONT></DIV>
+<DIV align=center><FONT face=Arial size=2><FONT color=#ff0000 
+size=4><U><STRONG><BR></STRONG></U></FONT>Kent State University's School of 
+Library and Information Science<BR>received a gift of <STRONG><FONT 
+color=#0000ff>$240,000 </FONT></STRONG>from the <FONT 
+color=#0000ff><STRONG>Reinberger Foundation of Cleveland<BR></STRONG></FONT>for 
+the construction of a unique national center dedicated to training librarians 
+who <BR>specialize in services for children, young adults and 
+school<BR>librarianship. The gift was announced in anticipation of 
+National<BR>Library Week (April 14-20). <BR>&nbsp;<BR>"The Children's Resource 
+Center will offer an environment similar to<BR>achildren's or elementary school 
+library complete with books,<BR>multimedia, puppets and a storytelling area," 
+said Associate Professor<BR>Dr. Carolyn S.Brodie, who has built the School of 
+Library and<BR>Information Science's collection of materials for youth, and is 
+a<BR>co-recipient of the Reinberger gift. Brodie recently served as chair 
+of<BR>the 2000 John Newbery Award Committee. <BR>&nbsp;<BR><FONT 
+color=#0000ff>The Children's Resource Center </FONT>will be unique among the 
+nation's library<BR>schools and will serve as a model classroom for library 
+science programs<BR>for children's librarians. The Center is designed to be much 
+more than a<BR>university classroom and will include a children's</FONT></DIV>
+<DIV align=center><FONT face=Arial size=2>&nbsp;resource area that will house 
+more than </FONT></DIV>
+<DIV align=center><FONT face=Arial size=2>5,000 children's books, materials, and 
+resources to<BR>create a focal point for instruction in children's, young adult, 
+and<BR>school librarianship. </FONT></DIV>
+<DIV align=center><FONT face=Arial size=2></FONT>&nbsp;</DIV>
+<DIV align=center><FONT face=Arial size=2>The 1,700-square-foot resource center 
+will also<BR>include a wireless computer network installed with specialized 
+software<BR>and other resources used in children's and school libraries. For 
+more<BR>information contact Megan Harding, (330) 
+672-0419.&nbsp;<BR></FONT></DIV>
+<DIV align=center><FONT face=Arial><STRONG><FONT color=#ff0000 size=4>&nbsp;- 
+Moderator -<BR></FONT></STRONG></DIV></FONT>
+<br>
+
+<!-- |**|begin egp html banner|**| -->
+
+<table border=0 cellspacing=0 cellpadding=2>
+<tr bgcolor=#FFFFCC>
+<td align=center><font size="-1" color=#003399><b>Yahoo! Groups Sponsor</b></font></td>
+</tr>
+<tr bgcolor=#FFFFFF>
+<td align=center width=470><table border=0 cellpadding=0 cellspacing=0><tr><td align=center><font face=arial size=-2>ADVERTISEMENT</font><br><a href="http://rd.yahoo.com/M=213858.2097561.3556641.1829184/D=egroupweb/S=1705082179:HM/A=763352/R=0/*http://www.classmates.com/index.tf?s=5085" target=_top><img src="http://us.a1.yimg.com/us.yimg.com/a/cl/classmates_com2/bll_lrec1.gif" alt="" width="300" height="250" border="0"></a></td></tr></table></td>
+</tr>
+<tr><td><img alt="" width=1 height=1 src="http://us.adserver.yahoo.com/l?M=213858.2097561.3556641.1829184/D=egroupmail/S=1705082179:HM/A=763352/rand=399788106"></td></tr>
+</table>
+
+<!-- |**|end egp html banner|**| -->
+
+
+<br>
+<tt>
+To unsubscribe from this group, send an email to:<BR>
+InfoInternational-unsubscribe@yahoogroups.com<BR>
+<BR>
+</tt>
+<br>
+
+<br>
+<tt>Your use of Yahoo! Groups is subject to the <a href="http://docs.yahoo.com/info/terms/">Yahoo! Terms of Service</a>.</tt>
+</br>
+
+</BODY></HTML>
+
+------=_NextPart_000_0146_01C1F16C.B224C240--
+
diff --git a/upstream/t/data/welcomelists/mypoints b/upstream/t/data/welcomelists/mypoints
new file mode 100644 (file)
index 0000000..5aa1424
--- /dev/null
@@ -0,0 +1,22 @@
+Return-Path: <mpmail@mpmlbx06.mypoints.com>
+Delivered-To: zzz-zzzzzzz@fffffffff.org
+Received: (qmail 6475 invoked by uid 505); 20 Jun 2002 02:01:31 -0000
+Received: from mpmail@mpmlbx06.mypoints.com by zzzzzz.fffffffff.org by uid 502 with qmail-scanner-1.12 (F-PROT: 3.12. Clear:. Processed in 0.243337 secs); 20 Jun 2002 02:01:31 -0000
+Received: from mpmlbx06.mypoints.com (216.33.87.173)
+  by dsl092-072-213.bos1.dsl.speakeasy.net with SMTP; 20 Jun 2002 02:01:30 -0000
+Received: (from mpmail@localhost)
+        by mpmlbx06 (8.11.0/8.11.0) id g5K1onT23615;
+        Wed, 19 Jun 2002 20:50:49
+Date: Wed, 19 Jun 2002 20:50:49
+Message-ID: <2002619205049.g5K1onT23615@mpmlbx06>
+To: zzz-zzzzzzz@fffffffff.org
+From: BonusMail from MyPoints <mpmail@mpmlbx06.mypoints.com>
+Reply-To: BonusMailReply@mypoints.com
+Subject: New Deals Just Added! Massive Sheet Liquidation--Now Save up to 84%! 
+X-Indiv: y6f6f69de10d97c7a932zzc3902bf5331
+X-JobID: 107974
+MIME-Version: 1.0
+Content-Type: text/html;charset=us-ascii
+Content-Transfer-Encoding: 7bit
+
+[MyPoints newsletter]
diff --git a/upstream/t/data/welcomelists/neat_net_tricks b/upstream/t/data/welcomelists/neat_net_tricks
new file mode 100644 (file)
index 0000000..8575c8b
--- /dev/null
@@ -0,0 +1,205 @@
+From bounce-neatnettricks-1234567@silver.lyris.net  Thu Aug 15 10:51:10 2002
+Return-Path: <bounce-neatnettricks-1234567@silver.lyris.net>
+Delivered-To: aaa@localhost.netnoteinc.com
+Received: from localhost (localhost [127.0.0.1])
+       by phobos.labs.netnoteinc.com (Postfix) with ESMTP id 8448D43C4F
+       for <aaa@localhost>; Thu, 15 Aug 2002 05:49:35 -0400 (EDT)
+Received: from phobos [127.0.0.1]
+       by localhost with IMAP (fetchmail-5.9.0)
+       for aaa@localhost (single-drop); Thu, 15 Aug 2002 10:49:35 +0100 (IST)
+Received: from silver.lyris.net (silver.lyris.net [216.91.57.32]) by
+    dogma.slashnull.org (8.11.6/8.11.6) with SMTP id g7ENU3408604 for
+    <aaaaaa@yyyyyy.zzz>; Thu, 15 Aug 2002 00:30:03 +0100
+Message-Id: <LYRIS-1234567-1370323-2002.08.14-16.20.02--aaaaaa@yyyyyy.zzz@silver.lyris.net>
+X-Sender: jteems@rap.midco.net@pop.midco.net
+X-Mailer: QUALCOMM Windows Eudora Version 5.1
+Date: Wed, 14 Aug 2002 16:20:00 -0700
+To: aaaaaa@yyyyyy.zzz
+From: NNT@silver.lyris.net
+Subject: Neat Net Tricks Standard Issue 131 - August 15, 2002
+MIME-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"; format=flowed
+List-Unsubscribe: <mailto:leave-neatnettricks-1234567K@silver.lyris.net>
+Reply-To: NNT@silver.lyris.net
+X-Pyzor: Reported 0 times.
+
+IN THIS ISSUE:
+
+01. Secure IE
+02. Disk cleanup on XP
+03. Thanks but no thanks
+04. Mouseless way home
+05. Kartoo
+06. XP time feature
+07. Leaf Peeper Alert
+08. Saving scraps
+09. Quick access
+10. Don't believe your email
+
+And What's Coming Up Next Week in NNT Premium
+
+01. SECURE IE.  For the past month or so, our Software Review Panel has 
+been giving a grueling test to Secure IE, a piece of software that blocks 
+Flash and pop-ups, prevents malicious file downloads, lets you customize 
+Security Zone settings as you browse dozens of Web sites simultaneously up 
+to five times faster with a tabbed interface, annotate Web page with sticky 
+notes and highlighter, and save complete Web pages, even secure server 
+(HTTPS) pages and archive online transaction receipts.  For what we believe 
+is one of the most thorough reviews ever conducted on a single piece of 
+software, check out what our Panel had to say at 
+http://www.NeatNetTricks.com/SoftwareReviews .  For a free trial download, 
+visit http://www.secureie.com .  And if this one sweeps you off your feet, 
+go to the NNT Store at http://www.NeatNetTricks.com/store and get a super 
+deal, just $17.50 if you order before September 14.  With a 30-day 
+guarantee, what's to lose?
+
+02. DISK CLEANUP ON XP.  Windows XP users can do some fast cleaning chores 
+with Disk Cleanup.  Access this tool from the Start menu by right clicking 
+on your hard drive.  Then select Properties and click on the Disk Cleanup 
+button to determine how many files can be safely deleted.
+
+03. THANKS BUT NO THANKS.  Continuing our periodic feature of 
+less-than-useful sites on the Web:  The International Center for Bathroom 
+Etiquette at http://www.icbe.org/ gets our recognition this issue.  It says 
+it's working hard to bring you the latest in cutting edge research and 
+technology regarding bathroom etiquette.  We'll resist some obvious puns.
+
+. . . . .
+
+West Virginia's Diane Stratton recommended NNT to some friends and is now 
+enjoying QuicKeys 2.0, a great Windows management software package.  Diane 
+is our latest winner and you could be next.  Just go to the NNT Web site at 
+http://www.NeatNetTricks.com and click on "Recommend NNT."  Nothing could 
+be easier.
+
+. . . . .
+
+04. MOUSELESS WAY HOME.  To go home quickly in Internet Explorer, touch F6 
+to highlight the Address Bar and type two periods (..) there.  The Enter 
+key then takes you home.
+
+05. KARTOO.  Search engines are everywhere on the Internet but Kartoo at 
+http://www.kartoo.net is quite unusual.  It's a meta search engine that 
+displays results on a map in the form of a ball.  The larger the ball, the 
+more relevant the result.  As you mouseover each result, site descriptions 
+are revealed.  If all that sounds confusing, the explanation is more 
+complicated than the service itself.  Just try it.
+
+. . . . .
+
+You should make it a habit to visit the NNT Store at 
+http://www.NeatNetTricks.com/store . We try to have several great products 
+there at a limited-time price much less than anywhere else on the Net. 
+Currently, you'll find excellent ebooks, a very effective popup stopper, 
+and the very useful utility described in item 01 above, along with the 
+usual opportunity to subscribe to NNT Premium and ArchivesExpress.  Check 
+us out, you'll be glad you did.
+
+. . . . .
+
+06. XP TIME FEATURE.  Windows XP added a nice feature that heretofore 
+required a separate software application.  It will connect, either at a 
+programmed time or manually, to a time server via the Internet and reset 
+that often erroneous internal clock.  Just click on the time in the systems 
+tray, go to Date and Time Properties and click the Internet Time tab.
+
+07. LEAF PEEPER ALERT.  A bit early, but soon the changing colors of autumn 
+will begin here in the U.S.  For those who like to follow the display, 
+consider http://www.stormfax.com/foliage.htm for a comprehensive collection 
+of links and toll-free numbers to each state to determine peak color times.
+
+08. SAVINGS SCRAPS.  Some oldies are worth repeating.  If you're working 
+with text in, for example, MS Word or WordPad, and would like a handy way 
+to save a portion for later easy retrieval, just select (highlight) it and 
+click/drag it to your desktop.  When the newly created icon is clicked on, 
+it will show the application with which the scrap was created, along with 
+the first few words of the text.  A double click opens the text in the 
+application with which it was created.
+
+09. QUICK ACCESS.  To go to a frequently used program, you may find 
+yourself drilling down to the desktop and searching out the shortcut 
+icon.  Consider instead setting up a key combination that will provide 
+quick access without using the shortcut. Right click on the shortcut and 
+select Properties.  In the Shortcut key window, select any key you can 
+remember and click OK.  Ctrl+Alt and that key will open the application 
+whenever needed.
+
+10.  DON'T BELIEVE YOUR EMAIL.  We've been asked about those emails with 
+virus attachments that appear to be coming from NNT, asking for a 
+confirmation of a subscription.  Don't believe it, and don't open the 
+attachments.  Maybe this exchange between NNT and Lyris (our mail manager) 
+will clear things up:
+
+NNT: Is there anything that can be done about the current strains of virus 
+that implant on address books and randomly send email asking alleged 
+subscribers to verify their subscription (when they haven't subscribed at 
+all)? I've received some of these as well, and I know it's become a 
+widespread problem with other ezine publishers, creating a lot of ill will 
+all around.  Is there some configuration that we could change to keep 
+these from going out except to legitimate subscribers?
+
+Lyris:  Unfortunately, there isn't much we can do about this since it 
+isn't actually ListManager doing the sending.  The real problem is that 
+people sometimes will add the "join" address to their address book and 
+that is what causes the problems.  The best we can do is advise people not 
+to have their address books set to automatically add any email address 
+that they send a message to even once.  The main culprit here seems to be 
+Outlook Express.
+
+WHAT'S COMING NEXT WEEK:  Another batch of Neat Net Tricks in the Premium 
+issue, including:
+
+*  A great collection of information and free downloads to make your system 
+more secure.
+
+*  Free software to measure and display your real-time Internet speed.
+
+*  Can you handle still another popup stopper - that's interference free - 
+and at no cost?
+
+*  An easy-to-use tool to store and arrange all your passwords, user IDs, 
+and other info.
+
+*  A Microsoft Word tip to easily work around that pesky autocompletion 
+feature.
+
+*  A whole arsenal of tools to combat spam.
+
+*  Software that provides more than 200 interesting facts about your 
+computer and displays about your CPU, memory, operating system, and your 
+computer's power source.
+
+*  Our in-depth article discusses how to best manage our important 
+passwords and get out of trouble when we -inevitably - forget those passwords.
+
+And more!  If you haven't subscribed yet, you won't find a better source of 
+useful information for just 42 cents per issue.  That's $10 for a year's 
+worth - 24 issues - at the NNT Store, http://www.NeatNetTricks.com/store .
+
+. . . . . .
+
+NNT makes no endorsement or warranty, expressed or implied, with regard to 
+featured products or services. Results may vary based on operating systems 
+and other variables beyond our control.
+
+For info on how to subscribe, unsubscribe, or change your address, send a 
+blank email to info-neatnettricks@silver.lyris.net .
+
+Sponsor an entire issue of NNT with your exclusive message to our readers 
+at very low rates. Send a blank email to 
+advertise-neatnettricks@silver.lyris.net .
+
+Comments or questions about your computer and the Internet? Visit the NNT 
+Bulletin Board at http://www.escribe.com/computing/neatnettricks/bb/ .
+
+NNT is hosted by Lyris.com, the best in email list management.
+
+Copyright 2002 by Jack Teems. All rights reserved. Neat Net Tricks is 
+registered with the U.S. Library of Congress ISSN: 1533-4619. 
+
+
+---
+You are currently subscribed to neatnettricks as: aaaaaa@yyyyyy.zzz
+To unsubscribe send a blank email to leave-neatnettricks-1234567K@silver.lyris.net
+
+
diff --git a/upstream/t/data/welcomelists/netcenter-direct_de b/upstream/t/data/welcomelists/netcenter-direct_de
new file mode 100644 (file)
index 0000000..3eacb2b
--- /dev/null
@@ -0,0 +1,414 @@
+Return-Path: <dms-errors@dms.netcenter.com>
+Received: (qmail 3387 invoked by alias); 15 Jul 2002 20:26:49 -0000
+Received: (qmail 26987 invoked by uid 82); 15 Jul 2002 20:23:30 -0000
+Received: from dms-errors@dms.netcenter.com by mailhost with qmail-scanner-1.00 (uvscan: v4.1.40/v4212. . Clean. Processed in 5.813084 secs); 15 Jul 2002 20:23:30 -0000
+Received: from dms-mail02.netcenter.com (207.200.87.32)
+  by mi-1.rz.ruhr-uni-bochum.de with SMTP; 15 Jul 2002 20:23:20 -0000
+Received: from dms-www1.netscape.com (dms-mailcaster-s07.netcenter.com) by dms-mail02.netcenter.com (LSMTP for Windows NT v1.1b) with SMTP id <8.00007AB9@dms-mail02.netcenter.com>; Mon, 15 Jul 2002 13:22:22 -0700
+To: xxxxx.yyyyy@ruhr-uni-bochum.de
+Subject: Netscape News - Ausgabe Juli
+From: Netscape <netcenter-direct@dms.netcenter.com>
+Date: Mon, 15 Jul 2002 13:24:23 -0800
+Reply-To: Netscape <netcenter-direct@dms.netcenter.com>
+Content-Type: multipart/alternative;
+ boundary="______BoundaryOfDocument______"
+MIME-Version: 1.0
+Content-Transfer-Encoding: 7bit
+
+This is a multi-part message in MIME format.
+
+--______BoundaryOfDocument______
+Content-Type: text/plain
+Content-Transfer-Encoding: 7bit
+
+Netscape News - Ausgabe Juli
+
+Lieber Netscape-Nutzer,
+
+in dieser Ausgabe:
+
+- Musterverträge, Rechtstipps und mehr
+  Die neuen Netscape Quickfinder liefern Ihnen direkte Links zu
+  Themen und Tools wie Downloads, Rechtstipps, Verträgen und mehr.
+
+- Der neue Women-Channel
+  Nicht nur für Frauen: Die Mode von Morgen, die neusten Trends in
+  puncto Lifestyle, leckere Rezepte und vieles mehr.
+
+- Netscape sucht mit Google
+  Ab sofort bedient sich die Netscape-Suche der Google-Suchengine. So
+  erhalten Sie die besten Suchergebnisse in kürzester Zeit.
+
+- 0190-Dialer gehen um
+  Unseriöse Anbieter von 0190er-Nummern werden immer dreister. Wir zeigen
+  Ihnen, wie Sie sich schützen können.
+
+- Flirten erlaubt
+  Sie fahren als Single in den Urlaub? Sie wollen Spa? Wir zeigen Ihnen
+  die besten Strände für einen heißen Sommer-Flirt!
+
+http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H1b041H2g55Ep6l012000
+WRnwBz
+
+------------------------------------------------------------------
+Netscape respektiert Ihre Online-Arbeitszeit und Ihre
+Privatsphäre. Wenn Sie in Zukunft KEINE E-Mail-Nachrichten
+mehr von Netscape erhalten möchten, klicken Sie auf untenstehenden
+Link.
+HINWEIS:
+KLICKEN SIE NUR AUF DIESEN LINK, WENN SIE IHR ABONNEMENT
+AUCH WIRKLICH BEENDEN MÖCHTEN!
+http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H1d3Qxx41H2g55Ep6l012
+000WRnwBz
+
+Sie sind mit folgender Adresse registriert:[xxxxx.yyyyy@ruhr-uni-bochum.de]
+
+--______BoundaryOfDocument______
+Content-Type: text/html
+Content-Transfer-Encoding: 7bit
+
+<html><head>
+
+<meta name="keywords" content="Netscape, Newsletter">
+<meta name="description" content="Netscape, Newsletter, Juli-Ausgabe">
+<meta name="revisit-after" content="3 days">
+<meta name="channel" content="Newsletter"><title>Netscape News, Ausgabe
+Juli</title>
+</head>
+<body marginheight="0" topmargin="0" bgcolor="#ffffff">
+<table cellpadding=0 cellspacing=0 border=0 width=600 align=center>
+ <tr>
+  <td><img height=1 border=0 width=121
+src="http://ivw.netscape.de/cgi-bin/ivw/CP/newsletter/index.jsp_0"></td>
+  <td><img src="http://www.netscape.de/img/1p.gif" width=10 height=7
+border=0 alt=""></td>
+  <td><img src="http://www.netscape.de/img/1p.gif" width=105 height=7
+border=0 alt=""></td>
+  <td><img src="http://www.netscape.de/img/1p.gif" width=60 height=7
+border=0 alt=""></td>
+  <td><img src="http://www.netscape.de/img/1p.gif" width=304 height=7
+border=0 alt=""></td>
+ </tr>
+    <FORM NAME="searchWidgetForm"
+ACTION="http://cgi.netscape.com/de/cgi-bin/home_search_widget.cgi"
+method="get">
+    <tr>
+        <td valign=top>
+   <A
+href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H1h041H2g55Ep6l
+012000WRnwBz"><img
+src="http://dms-www1.netscape.com/images/nesc_logo_klein.gif" alt="NETSCAPE"
+width=121 height=25 border=0></a></td>
+        <td bgcolor="#000000"><img src="http://www.netscape.de/img/1p.gif"
+width=10 height=1 border=0 alt=""><INPUT TYPE=hidden NAME=engine
+VALUE="0"><INPUT TYPE=hidden NAME=version VALUE=C></td>
+        <td bgcolor="#000000" valign=middle><input type="Text"
+name="searchstring" size="13"></td>
+        <td bgcolor="#000000"><INPUT TYPE=IMAGE NAME=""
+SRC="http://www.netscape.de/img/portal/but_suchen.gif" BORDER=0 WIDTH=50
+HEIGHT=25></td>
+        <td bgcolor="#003366"><img
+src="http://www.netscape.de/img/trenner_header.gif" alt="NETSCAPE" width=15
+height=25 border=0>
+<A
+href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H1g041H2g55Ep6l
+012000WRnwBz"><img src="http://www.netscape.de/img/but_header_mail.gif"
+width=50 height=25 border=0 alt="Mail"></a><img
+src="http://www.netscape.de/img/1p.gif" width=4 height=1>
+<A
+href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H1f041H2g55Ep6l
+012000WRnwBz"><img src="http://www.netscape.de/img/but_header_aim.gif"
+width=115 height=25 border=0 alt="Instant Messenger"></a><img
+src="http://www.netscape.de/img/1p.gif" width=4 height=1><A
+href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H1e041H2g55Ep6l
+012000WRnwBz"><img src="http://www.netscape.de/img/but_header_download.gif"
+width=74 height=25 border=0 alt="Download"></a></td>
+    </tr>
+    </FORM>
+ <tr><td colspan=5><img src="http://www.netscape.de/img/1p.gif" width=600
+height=7></td></tr>
+</table>
+
+
+
+<table border="0" width="600" cellspacing="0" cellpadding="0"
+align="center">
+   <tbody><tr><td bgcolor="#cccccc" colspan="13"><img
+src="http://www.netscape.de/img/1p.gif" alt="" border="0"
+height="1"></td></tr>
+   <tr>
+     <td bgcolor="#cccccc" width="1"><img
+src="http://www.netscape.de/img/1p.gif" alt="" border="0"
+width="1"></td><!--spacer width=4-->
+        <td bgcolor="#ffffff" width="4"><img
+src="http://www.netscape.de/img/1p.gif" border="0" alt="" width="4"></td>
+        <td colspan="9">
+         <table width="590" border="0" cellspacing="0" cellpadding="0">
+         <tbody><tr><td colspan="2"><img
+src="http://www.netscape.de/img/1p.gif" border="0" height="10"
+alt=""></td></tr>
+   <tr><td colspan="2" height="60">
+    <table cellpadding="0" cellspacing="0" border="0" width="590"
+align="center">
+    <tbody><tr><td width="590" align="center">
+    <a href="http://ar.atwola.com/link/93131215/aol">
+    <img src="http://ar.atwola.com/image/93131215/aol" alt="Click here to
+visit our advertiser." width="234" height="60" border="0"></a>
+    <img src="http://www.netscape.de/img/1p.gif" width="30" height="1">
+    <a href="http://ar.atwola.com/link/93131216/aol">
+    <img src="http://ar.atwola.com/image/93131216/aol_002.gif" alt="Click
+here to visit our advertiser." width="234" height="60" border="0"></a>
+    </td></tr>
+    </tbody></table>
+   </td></tr>
+         <tr><td colspan="2"><img src="http://www.netscape.de/img/1p.gif"
+border="0" height="5" alt=""></td></tr>
+         <tr><td align="left"><font face="Arial, Helvetica, sans-serif"
+size="6" color="#990000" nowrap="0">
+         <b>Netscape News</b></font></td><td align="right"><font
+face="Arial, Helvetica, sans-serif" size="2" color="#003399">
+            <b>Juli, 2002  </b></font></td></tr>
+         <tr><td colspan="2"><img src="http://www.netscape.de/img/1p.gif"
+border="0" height="5" alt=""></td></tr>
+         </tbody></table>
+         </td>
+         <td bgcolor="#ffffff" width="4"><img
+src="http://www.netscape.de/img/1p.gif" border="0" width="4" alt=""></td>
+         <td bgcolor="#cccccc" width="1"><img
+src="http://www.netscape.de/img/1p.gif" border="0" width="1" alt=""></td>
+   </tr>
+         <tr><td bgcolor="#cccccc" colspan="13"><img
+src="http://www.netscape.de/img/1p.gif" border="0" width="1"
+alt=""></td></tr>
+         <tr align="left">
+            <td bgcolor="#cccccc" rowspan="3" width="1"><img
+src="http://www.netscape.de/img/1p.gif" border="0" width="1" alt=""></td>
+            <td bgcolor="#ffffff" rowspan="3" width="4"><img
+src="http://www.netscape.de/img/1p.gif" border="0" width="4" alt=""></td>
+            <td width="120" align="left" valign="top" rowspan="3">
+   <table width="120" border="0" cellspacing="0" cellpadding="0">
+   <tbody><tr><td><img src="http://www.netscape.de/img/1p.gif" width="120"
+height="5" border="0" alt=""></td></tr>
+   <tr><td>
+
+          <font face="Arial, Helvetica, sans-serif" size="3"
+color="#990000"><b>Highlights</b></font>
+
+  <font face="Arial, Helvetica, sans-serif" size="1" color="#003399"><BR><A
+href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H16041H2g55Ep6l
+012000WRnwBz">0190-Dialer gehen um</a></font><br>
+        <font face="Arial, Helvetica, sans-serif" size="1"
+color="#000000">Die
+Abzocke nimmt kein Ende: Unseriöse Anbieter von 0190er-Nummern lassen sich
+immer wieder neue Tricks einfallen, um die Verbraucher zu schröpfen. Wir
+geben Tipps zur Vorsorge.</font><br><br>
+
+  <font face="Arial, Helvetica, sans-serif" size="1" color="#003399"><A
+href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H15041H2g55Ep6l
+012000WRnwBz">Ab in den Urlaub</a></font><br>
+        <font face="Arial, Helvetica, sans-serif" size="1"
+color="#000000">Noch
+nichts vor im Sommer? Dann ab in den Urlaub! Unsere Last-Minute-Suche findet
+sicher das passende Schnäppchen für Ihren Geldbeutel.</font><br><br>
+
+  <font face="Arial, Helvetica, sans-serif" size="1" color="#003399"><A
+href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H1A041H2g55Ep6l
+012000WRnwBz">Riester-Rente-Special</a></font><br>
+        <font face="Arial, Helvetica, sans-serif" size="1"
+color="#000000">Jeder
+spricht darüber, doch wissen Sie wirklich Bescheid? Wir klären Sie über die
+Vor- und Nachteile der staatlich geförderten Rentenform auf.</font><br><br>
+
+    </td></tr>
+        <tr><td bgcolor="#cccccc"><img
+src="http://www.netscape.de/img/1p.gif" border="0" width="120" height="1"
+alt=""></td></tr>
+  <tr><td bgcolor="#ffffff"><img src="http://www.netscape.de/img/1p.gif"
+border="0" width="120" height="10" alt=""></td></tr>
+  <tr><td>
+
+
+  <center><A
+href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H13041H2g55Ep6l
+012000WRnwBz"><img height="60" alt=""
+src="http://www.netscape.de/content/NS_Newsletter/266366_1025512239640.jpg"
+width="120" align="middle" vspace="7" border="0"></a></center>
+
+     </td></tr>
+  </tbody></table>
+  </td>
+        <td bgcolor="#ffffff" rowspan="3" width="4"><img
+src="http://www.netscape.de/img/1p.gif" border="0" width="4" alt=""></td>
+        <td bgcolor="#cccccc" rowspan="3" width="1"><img
+src="http://www.netscape.de/img/1p.gif" border="0" width="1" alt=""></td>
+        <td bgcolor="#ffffff" rowspan="3" width="4"><img
+src="http://www.netscape.de/img/1p.gif" border="0" width="4" alt=""></td>
+        <td width="292" align="left" valign="top">
+        <img src="http://www.netscape.de/img/1p.gif" width="292" height="5"
+border="0" alt=""><br>
+
+        <font face="Arial, Helvetica, sans-serif" size="3"
+color="#990000"><b>In dieser Ausgabe</b></font>
+        <img src="http://www.netscape.de/img/1p.gif" width="292" height="3"
+border="0" alt=""><br>
+
+
+        <font face="Arial, Helvetica, sans-serif" size="2"
+color="#003399"><b>Zwei starke Partner:<br>Netscape sucht mit
+Google</b></font>
+        <br><font face="Arial, Helvetica, sans-serif" size="2"
+color="#000000">Die
+Netscape-Suche ist jetzt noch effizienter: Sie bedient sich der
+Google-Technologie,
+der zur Zeit besten Such-Engine im Internet. Egal nach was Sie also suchen:
+Netscape und Google liefern Ihnen in kürzester Zeit die Top-Ergebnisse aus
+über 2 Milliarden Webseiten - und das übersichtlich und mit hoher
+Relevanz.<br></font>
+  <font face="Arial, Helvetica, sans-serif" size="1" color="#000000"><A
+href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H12041H2g55Ep6l
+012000WRnwBz">Mehr... </a></font>
+        <br><br>
+
+        <font face="Arial, Helvetica, sans-serif" size="2"
+color="#003399"><b>Nicht nur für Frauen:<br>Der neue
+Women-Channel</b></font>
+        <br><font face="Arial, Helvetica, sans-serif" size="2"
+color="#000000">
+Netscape.de hat Nachwuchs bekommen: Im neuen <A
+href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H11041H2g55Ep6l
+012000WRnwBz">Women-Channel</a>
+finden Sie alles, was das (Frauen-)Herz höher schlagen lässt. Wir verraten
+Ihnen zum Beispiel, was in Sachen Mode in diesem Sommer angesagt ist, zeigen
+Ihnen die neusten Trends in puncto Lifestyle und stellen leckere Rezepte
+für die leichte Sommerküche vor.      <br></font>
+  <font face="Arial, Helvetica, sans-serif" size="1" color="#000000"><A
+href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H10041H2g55Ep6l
+012000WRnwBz">Mehr... </a></font>
+        <br><br>
+
+        </td>
+        <td bgcolor="#ffffff" width="4"><img
+src="http://www.netscape.de/img/1p.gif" border="0" width="4" alt=""></td>
+        <td bgcolor="#cccccc" width="1"><img
+src="http://www.netscape.de/img/1p.gif" border="0" width="1" alt=""></td>
+        <td bgcolor="#ffffff" width="4"><img
+src="http://www.netscape.de/img/1p.gif" border="0" width="4" alt=""></td>
+        <td align="left" valign="top" width="160">
+        <table border="0" cellspacing="0" cellpadding="0" width="160">
+
+        <tbody><tr><td>
+        <A
+href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H0$041H2g55Ep6l
+012000WRnwBz"><img alt="" hspace="5"
+src="http://www.netscape.de/content/NS_Newsletter/266366_1026282512183.jpg"
+width="60" height="60" align="left" vspace="4" border="0"></a>
+        <font face="Arial, Helvetica, sans-serif" size="2"
+color="#003399"><b>Flirten erlaubt</b></font><br>
+        <font face="Arial, Helvetica, sans-serif" size="2"
+color="#000000">Sie fahren als Single in den Urlaub? Sie wollen Spa? Wir
+zeigen Ihnen die besten Strände für einen heißen Sommer-Flirt!<br>
+        <A
+href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H0_041H2g55Ep6l
+012000WRnwBz">Mehr...</a></font> <br>
+        </td></tr>
+
+        <tr><td>
+       <A
+href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H0z041H2g55Ep6l
+012000WRnwBz"><img alt="" hspace="5"
+src="http://www.netscape.de/content/NS_Newsletter/266366_1026282588133.jpg"
+width="60" height="60" align="left" vspace="4" border="0"></a>
+        <font face="Arial, Helvetica, sans-serif" size="2"
+color="#003399"><b>Grußkarten</b></font><br>
+        <font face="Arial, Helvetica, sans-serif" size="2"
+color="#000000">Verschicken Sie Ihre persönlichen Grüße einfach per eMail.
+Das spart Zeit und kostet Sie keinen Pfennig. Jetzt testen!<br>
+        <A
+href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H0y041H2g55Ep6l
+012000WRnwBz">Mehr...</a></font> <br>
+        </td></tr>
+
+        <tr><td bgcolor="#ffffff"><img
+src="http://www.netscape.de/img/1p.gif" border="0" width="158" height="5"
+alt=""></td></tr>
+        <tr><td bgcolor="#cccccc"><img
+src="http://www.netscape.de/img/1p.gif" border="0" width="158" height="1"
+alt=""></td></tr>
+  <tr><td bgcolor="#ffffff"><img src="http://www.netscape.de/img/1p.gif"
+border="0" width="158" height="5" alt=""></td></tr>
+
+  <tr><td><img src="http://www.netscape.de/img/1p.gif" width="160"
+height="15" border="0" alt=""><br>
+  <A
+href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H1m041H2g55Ep6l
+012000WRnwBz"><img
+src="http://www.netscape.de/content/NS_Newsletter/266366_1025512801645.jpg"
+width="120" height="60" hspace="20" alt="" border="0"
+align="middle"></a><br>
+  <img src="http://www.netscape.de/img/1p.gif" width="160" height="20"
+border="0" alt=""></td></tr>
+
+  </tbody></table>
+        </td>
+        <td bgcolor="#ffffff" width="4" rowspan="3"><img
+src="http://www.netscape.de/img/1p.gif" border="0" width="4" alt=""></td>
+        <td bgcolor="#cccccc" width="1" rowspan="3"><img
+src="http://www.netscape.de/img/1p.gif" border="0" width="1" alt=""></td>
+     </tr>
+     <tr><td colspan="5" valign="top" align="left" bgcolor="#cccccc"><img
+src="http://www.netscape.de/img/1p.gif" border="0" width="457" height="1"
+alt=""></td></tr>
+        <tr><td colspan="5" valign="top" align="left">
+
+        <font face="Arial, Helvetica, sans-serif" size="4"
+color="#990000"><b>Musterverträge, Rechtstipps und mehr...</b></font>
+        <img src="http://www.netscape.de/img/1p.gif" width="457" height="5"
+border="0" alt=""><br>
+
+        <font face="Arial, Helvetica, sans-serif" size="2"
+color="#003399"><b>Die neuen Netscape Quick Finder</b></font>
+        <br>
+        <font face="Arial, Helvetica, sans-serif" size="2"
+color="#000000">Das
+Internet stellt eine fast grenzenlose Menge an Informationen bereit. Wer
+hat da noch den Durchblick, vor allem, wenn es schnell gehen soll? Die <A
+href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H1l041H2g55Ep6l
+012000WRnwBz">Netscape Quick Finder</a>
+schaffen Abhilfe: Hier finden Sie direkte Links zu diversen Themen und Tools
+wie Musterverträge, Rechtstipps, Downloadarchiv, Gebrauchtwagenbewertung
+und Jobbörse - um nur einige zu nennen. Schneller geht's wirklich
+nicht!<br></font>
+  <font face="Arial, Helvetica, sans-serif" size="1" color="#000000"><A
+href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H1j041H2g55Ep6l
+012000WRnwBz">Mehr...</a>
+        <br>
+        <br></font>
+ <!--de.infonie.atps.kernel.exceptions.ATPSException: getContent could not
+find "/Content/Teaser_Mitte_unten/Element:1/Headline"-->
+     </td>
+        </tr>
+<tr><td bgcolor="#cccccc" colspan="13"><img
+src="http://www.netscape.de/img/pixel.gif" border="0" height="1"
+alt=""></td></tr>
+<tr><td colspan=13><center>
+<font color="#000000" face="Arial, Helvetica, sans-serif" size="1">
+Um den Newsletter abzubestellen, klicken Sie bitte <A
+href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H1d3Qxx41H2g55E
+p6l012000WRnwBz">hier</a> - <br>oder antworten
+Sie einfach auf diese email und schreiben "REMOVE" in die Betreffzeile.<br>
+c 2002 Netscape. Alle Rechte vorbehalten. <A
+href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H1n041H2g55Ep6l
+012000WRnwBz">Nutzungsbedingungen und Datenschutz</a></font>
+      </center></td></tr>
+</tbody></table>
+
+
+</body></html>
+--______BoundaryOfDocument______--
+
+
+:
+annmn:[041H2g041H2g55Ep6l012000WRnwBz]
+
+
+
diff --git a/upstream/t/data/welcomelists/netsol_renewal b/upstream/t/data/welcomelists/netsol_renewal
new file mode 100644 (file)
index 0000000..f8e84e9
--- /dev/null
@@ -0,0 +1,58 @@
+From nobody@rs.internic.net  Wed Jan 30 09:50:12 2002
+Delivery-Date: Tue, 13 Jun 2000 12:53:06 +0100
+Received: from zzzzzzzzz.yyyy (mail.zzzzzzzzz.yyyy [193.120.211.219])
+        by zzzzzzzzzz.yyyyyyyyyyy.com (8.9.3/8.9.3) with ESMTP id MAA04894
+        for <foooooooo@yyyyyyyyyyy.com>; Tue, 13 Jun 2000 12:53:04 +0100
+Received: from opsmail.internic.net (opsmail.internic.net [198.41.0.91])
+        by zzzzzzzzz.yyyy (8.9.3/8.9.3) with ESMTP id MAA21530
+        for <foooooooo@yyyyyyyyyyy.com>; Tue, 13 Jun 2000 12:53:03 +0100
+Received: from rs.internic.net (bipwww2.lb.internic.net [192.168.120.8])
+        by opsmail.internic.net (8.9.3/8.9.1) with ESMTP id HAA23653
+        for <foooooooo@yyyyyyyyyyy.com>; Tue, 13 Jun 2000 07:52:32 -0400 (EDT)
+Received: (from nobody@localhost)
+          by rs.internic.net (8.9.3/8.8.4)
+          id HAA02994; Tue, 13 Jun 2000 07:52:32 -0400 (EDT)
+Date: Tue, 13 Jun 2000 07:52:32 -0400 (EDT)
+From: Nobody <nobody@internic.net>
+Message-Id: <200006131152.HAA02994@rs.internic.net>
+Reply-to: billing@netsol.com
+To: foooooooo@yyyyyyyyyyy.com
+Subject: Confirmation of yyyyyyyyyyy.com renewal order
+
+Dear Customer,
+
+Congratulations!  Your Web Address (domain name) has been renewed for 
+an extended period.
+
+We will be processing your order within the next 24-48 hours.  Renewal 
+of your domain name is effective on your current expiration date. *
+
+Here is a summary of your order:
+
+  * Domain Name: yyyyyyyyyyy.com
+  * Total:       $70.00
+  * Rebate:      $10.50
+
+*Subject to receipt of complete and accurate information as requested 
+in your renewal registration order.  If you have any questions, visit 
+the FAQ section of our website:
+http://www.networksolutions.com/help/faq-multiyear-rebate.html 
+
+Be sure to visit your website and learn more about products, services 
+and free resources offered by Network Solutions:
+http://www.networksolutions.com/catalog/
+
+Get VeriSign secure encryption on your new website, and you'll give your
+customers the confidence to place orders online.  Request a FREE Guide, 
+"Securing Your Web Site for Business."
+http://www.verisign.com/cgi-bin/go.cgi?a=w000000000000000   
+
+Thank you for renewing the registration of your domain name with Network 
+Solutions!
+
+Sincerely,
+
+Network Solutions, Inc.
+The dot com people (TM)
+
+
diff --git a/upstream/t/data/welcomelists/networkworld b/upstream/t/data/welcomelists/networkworld
new file mode 100644 (file)
index 0000000..53adc25
--- /dev/null
@@ -0,0 +1,215 @@
+From NSManagement@bdcimail.com Wed Aug 14 15:30:00 2002
+Return-Path: <bounce-nsmanagement-0000000@mailcontrol.bellevuedata.com>
+Delivered-To: ffffffff@localhost.zzzzzzzzzz-ffffffff.net
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by mail.zzzzzzzzzz-ffffffff.net (Postfix) with ESMTP id 707A9BEE4A
+       for <ffffffff@localhost>; Thu, 15 Aug 2002 06:12:03 -0700 (PDT)
+Received: from mail.zzzzzzzzzz-ffffffff.com
+       by localhost with IMAP (fetchmail-5.9.11)
+       for ffffffff@localhost (single-drop); Thu, 15 Aug 2002 06:12:03 -0700 (PDT)
+Received: from mailcontrol.bellevuedata.com (mailcontrol.bellevuedata.com [66.37.227.18])
+       by mail44.megamailservers.com (8.12.5/8.12.0.Beta10) with SMTP id g7F78WpU002632
+       for <aaaaaaa@zzzzzzzzzz-ffffffff.com>; Thu, 15 Aug 2002 03:11:00 -0400 (EDT)
+X-Mailer: ListManager Web Interface
+Date: Wed, 14 Aug 2002 17:30:00 -0500
+Subject: Combining point products and suites
+To: aaaaaaa@zzzzzzzzzz-ffffffff.com
+From: NW on Network/Systems Management <NSManagement@bdcimail.com>
+Reply-To: Network/Systems Management Help <NWReplies@bellevue.com>
+Message-Id: <LISTMANAGERSQL-0000000-32969-2002.08.14-17.30.06--aaaaaaa#zzzzzzzzzz-ffffffff.com@mailcontrol.bellevuedata.com>
+Content-Type: text/plain;
+       charset="iso-8859-1"
+X-SpamBouncer: 1.5 (7/17/02)
+X-SBNote: FROM_DAEMON/Listserv
+X-SBPass: No Pattern Matching
+X-SBPass: No Freemail Filtering
+X-SBClass: Bulk
+X-Folder: Bulk
+
+NETWORK WORLD FUSION FOCUS: AUDREY RASMUSSEN on 
+NETWORK/SYSTEMS MANAGEMENT
+08/14/02
+Today's focus: Combining point products and suites
+
+Dear Robin Frank,
+
+In this issue:
+
+* Readers who advocate using both point products and suites
+* Links related to Network/Systems Management
+* Featured reader resource
+
+_______________________________________________________________
+This newsletter sponsored by 
+Lucent
+
+Do you want to receive calls while online and not need a second 
+phone line?
+
+Do you want shorter connect times?
+
+Could you benefit from faster uploads?
+For Next-Generation Dial Access, you need V.92.
+
+To learn more, click here for the Lucent Technologies V.92 
+InfoCenter. http://www.nww1.com/go2/lucent_rc.html
+_______________________________________________________________
+A NETWORK WORLD SPECIAL REPORT: BUSINESS CONTINUITY & DISASTER 
+RECOVERY PLANNING
+
+Dr. Jim Metzler of Ashton, Metzler & Associates discusses 
+techniques on how to proactively implement Business Continuity 
+and Disaster Recovery Planning. Sponsored by Syncsort, this 
+SPECIAL REPORT emphasizes both the tactical and strategic 
+considerations necessary for data and infrastructure 
+protection. Download your FREE copy today at: 
+http://nww1.com/go/ad306.html (registration required)
+
+_______________________________________________________________
+Today's focus: Combining point products and suites
+
+By Audrey Rasmussen
+
+Today we'll hear from readers who think the best route to take 
+in the debate between point products and suites is to use a 
+little of both.
+
+One reader commented that a company doesn't have to choose one 
+over the other. He says:
+
+"It has been my experience that a well-managed enterprise 
+monitoring system will most likely include some of both: point 
+solutions to address specific network/systems issues, and a 
+centralized, single pane of glass from a 'framework' system 
+providing a common platform for event and problem management."
+
+Another reader said organizational issues are an important 
+factor. An approach must work within the organizational and 
+political structure of a company:
+
+"The approach with best of breed, plus some integration tools 
+above it, is probably the less risky route - and for less 
+integrated organizations, the better way to go. If you go for 
+an integrated framework, you'd better be sure that you can 
+handle it from an organizational viewpoint; otherwise it could 
+be a hard, expensive landing." 
+
+According to yet another reader, there are other factors that 
+affect the decision on the management approach:
+
+"Mostly this question is answered based on:
+
+* How high up in the organization the decision is being made
+
+* How pragmatically (quick and dirty vs. big and beautiful) 
+  does one want to approach the issue
+
+* How specific the requirements are
+
+* Time of decision
+
+"Each supplier has its rise and fall; the winner of today may 
+be a loser tomorrow. If the different user [administrator] 
+groups have a different timing regarding when they need a tool, 
+they will probably come to different decisions."
+
+Another user says:
+
+"My personal favorite solution involves using vendor-supplied 
+software agents such as IBM Director or Compaq Insight Manager 
+and integrating them into a suite solution such as Tivoli or CA 
+Unicenter. This removes not only the cost of the middleware and 
+integration layers, but also removes the cost of the agent 
+technology." 
+
+So, there you have opinions from readers who embrace point 
+products and suites working together.
+
+_______________________________________________________________
+To contact Audrey Rasmussen:
+
+Audrey Rasmussen is a research director with Enterprise 
+Management Associates in Boulder, Colorado, 
+(http://www.enterprisemanagement.com), a leading analyst 
+and market research firm focusing exclusively on all aspects 
+of enterprise management. Audrey has more than 20 years of 
+experience working with distributed systems, applications 
+and networks. Her current focus at EMA is e-business, SMB/SME 
+and MSPs. She can be reached at:
+mailto:rasmussen@enterprisemanagement.com.
+_______________________________________________________________
+2002 SALARY CALCULATOR
+
+How has the turbulent market affected your earning potential? 
+Find out with Network World's 2002 Salary Calculator. We've 
+updated the Salary Calculator and revised it to reflect the 
+results of the Network World 2002 Salary Survey. Give us some 
+details about yourself and we'll tell you if you earn as much 
+as your peers: http://nww1.com/go/ad324.html
+_______________________________________________________________
+RELATED EDITORIAL LINKS
+
+SLAMming service levels into shape
+Network World, 08/12/02
+http://www.nwfusion.com/news/2002/134753_08-12-2002.html
+
+Archive of the Network/Systems Management newsletter:
+http://www.nwfusion.com/newsletters/nsm/index.html 
+_______________________________________________________________
+If you're concerned about the growing turbulence in the telco 
+industry, you are not alone. The massive financial and 
+organizational changes now underway at many of the largest 
+carriers increase the possibility of service outages, 
+performance degradation and poor operations support. Find out 
+what you can do to mitigate your risks. Attend a free web 
+seminar on the best practices for protecting your business from 
+telco turbulence. Leading industry expert, David Willis of the 
+META Group, will analyze the inevitable consequences of the 
+current environment and share pragmatic steps to shield your 
+users and applications from carrier failures. 
+http://nww1.com/go/4531858a.html
+_______________________________________________________________
+FEATURED READER RESOURCE
+
+NW FUSION'S WHITEPAPERS CENTRAL
+
+A free resource to Network World Fusion visitors is the 
+Whitepaper Central area on NW Fusion. Here you can find vendor 
+and Network World produced whitepapers on a variety of network 
+topics. You can search our whitepapers database by company or 
+by title. All are available free of charge. Visit 
+http://www.nwfusion.com/bg/wp/wpbydate.jsp today.
+_______________________________________________________________
+May We Send You a Free Print Subscription? 
+You've got the technology snapshot of your choice delivered 
+at your fingertips each day. Now, extend your knowledge by 
+receiving 51 FREE issues to our print publication. Apply 
+today at http://www.nwwsubscribe.com/nl
+_______________________________________________________________
+SUBSCRIPTION SERVICES 
+
+To subscribe or unsubscribe to any Network World e-mail 
+newsletters, go to: 
+http://www.nwwsubscribe.com/news/scripts/notprinteditnews.asp 
+
+To unsubscribe from promotional e-mail go to: 
+http://www.nwwsubscribe.com/ep
+
+To change your e-mail address, go to: 
+http://www.nwwsubscribe.com/news/scripts/changeemail.asp 
+
+Subscription questions? Contact Customer Service by replying to 
+this message. 
+
+Have editorial comments? Write Jeff Caruso, Newsletter Editor, 
+at: mailto:jcaruso@nww.com 
+
+For advertising information, write Alonna Doucette, V.P. of 
+Online Development, at: mailto:sponsorships@nwfusion.com
+
+Copyright Network World, Inc., 2002
+
+------------------------
+This message was sent to:  aaaaaaa@zzzzzzzzzz-ffffffff.com
+
+
diff --git a/upstream/t/data/welcomelists/oracle_net_techblast b/upstream/t/data/welcomelists/oracle_net_techblast
new file mode 100644 (file)
index 0000000..b238316
--- /dev/null
@@ -0,0 +1,554 @@
+Return-Path: <replies@oracleeblast.com>
+Received: (qmail 19678 invoked by alias); 10 Jul 2002 13:22:47 -0000
+Received: (qmail 19416 invoked by uid 82); 10 Jul 2002 13:22:42 -0000
+Received: from replies@oracleeblast.com by mailhost with qmail-scanner-1.00 (uvscan: v4.1.40/v4210. . Clean. Processed in 8.59332 secs); 10 Jul 2002 13:22:42 -0000
+Received: from inet-mail6.oracle.com (209.246.10.170)
+  by mi-1.rz.ruhr-uni-bochum.de with SMTP; 10 Jul 2002 13:22:30 -0000
+Received: from blaster-smtp.oracle.com (eblast01.oracleeblast.com [148.87.9.11])
+       by inet-mail6.oracle.com (Switch-2.2.2/Switch-2.2.0) with ESMTP id g6ADMHs25188
+       for XXXXXX.YYYYY@RUHR-UNI-BOCHUM.DE; Wed, 10 Jul 2002 06:22:17 -0700 (PDT)
+Date: Wed, 10 Jul 2002 06:22:17 -0700 (PDT)
+Message-Id: <200207101322.g6ADMHs25188@inet-mail6.oracle.com>
+Subject: Oracle Technology Network TechBlast - July 2002
+From: Oracle Technology Network<replies@oracleeblast.com>
+To: XXXXXX.YYYYY@RUHR-UNI-BOCHUM.DE
+Reply-To: replies@oracleeblast.com
+Content-Transfer-Encoding: 8bit
+MIME-Version: 1.0
+Content-Type: multipart/alternative;
+    boundary="next_part_of_message"
+
+--next_part_of_message
+
+
+
+e
+e
+ssage
+Content-type: text/plain; charset=iso-8859-1
+
+
+
+--next_part_of_message
+Content-Type: text/html
+
+
+<body bgcolor="#FFFFFF" link="#000000" vlink="#000000">
+<a href="http://otn.oracle.com/index.html" target="_top"><img src="http://otn.oracle.com/otn300x65.gif" width=300 height=65 border=0 alt="Oracle Technology Network" hspace=5 vspace=5></a> 
+<div align="center"><font face="Arial, Helvetica, sans-serif"><b><font size="+2">OTN 
+  TechBlast </font><font size="+1"><br>
+  </font> <i>July 2002 Issue</i></b><font size="2"><br>
+  <font size="1">The monthly TechBlast is also available through the <a href="http://otn.oracle.com/techblast/index.htm">Oracle 
+  Technology Network</a> website.</font></font></font> <br>
+  <div align="left"> 
+    <hr>
+  </div>
+</div>
+<table width="100%" border="0" cellspacing="10" >
+  <tr> 
+    <td valign="top" width="14%" ><font size="2" face="Arial, Helvetica, sans-serif"><b>In 
+      this issue:</b></font> 
+      <table width="100%" border="0" cellspacing="2" cellpadding="0">
+        <tr> 
+          <td><font size="1"><a href="#topnews"><img src="http://otn.oracle.com/images/bullets_and_symbols/red_arrow_bullet_10.gif" width="12" height="13" border="0"></a></font></td>
+          <td><font face="Arial, Helvetica, sans-serif" size="1"><a href="#feature">This 
+            Month's Feature</a></font></td>
+        </tr>
+        <tr> 
+          <td height="12"><font size="1"><a href="#newdownloads"><img src="http://otn.oracle.com/images/bullets_and_symbols/red_arrow_bullet_10.gif" width="12" height="13" border="0"></a></font></td>
+          <td><font face="Arial, Helvetica, sans-serif" size="1"><a href="#news"> 
+            News</a></font></td>
+        </tr>
+        <tr> 
+          <td height="9"><font size="1"><a href="#newdownloads"><img src="http://otn.oracle.com/images/bullets_and_symbols/red_arrow_bullet_10.gif" width="12" height="13" border="0"></a></font></td>
+          <td><font face="Arial, Helvetica, sans-serif" size="1"><a href="#downloads">Software 
+            Downloads</a></font></td>
+        </tr>
+        <tr> 
+          <td><font size="1"><a href="#ou"><img src="http://otn.oracle.com/images/bullets_and_symbols/red_arrow_bullet_10.gif" width="12" height="13" border="0"></a></font></td>
+          <td><font face="Arial, Helvetica, sans-serif" size="1"><a href="#ou">Oracle 
+            University</a></font></td>
+        </tr>
+        <tr> 
+          <td height="2"><font size="1"><a href="#events"><img src="http://otn.oracle.com/images/bullets_and_symbols/red_arrow_bullet_10.gif" width="12" height="13" border="0"></a></font></td>
+          <td><font face="Arial, Helvetica, sans-serif" size="1"><a href="#books">New 
+            Books</a></font></td>
+        </tr>
+        <tr> 
+          <td valign="top"><font size="1"><a href="#ebn"><img src="http://otn.oracle.com/images/bullets_and_symbols/red_arrow_bullet_10.gif" width="12" height="13" border="0"></a></font></td>
+          <td valign="top"> 
+            <p><font size="1" face="Arial, Helvetica, sans-serif" color="#000000">Worldwide 
+              Events: <a href="#americas"><br>
+              Americas</a> | <a href="#emea">EMEA</a> | <a href="#apac">APAC</a></font></p>
+          </td>
+        </tr>
+      </table>
+      <p align="left"><a href="mailto:?subject=OTN%20newsletter%20&body=Interesting%20reading%20from%20the%20Oracle%20Technology%20Network:%20%20http://otn.oracle.com/techblast"><img src="http://otn.oracle.com/techblast/images/email2friend.gif" width="70" height="80" border="0"></a></p>
+    </td>
+    <td valign="top" width="69%" > 
+      <p><font face="Arial, Helvetica, sans-serif"><a name="feature"></a> <b><font size="4"><i>This 
+        Month's Feature: </i></font><font face="Arial, Helvetica, sans-serif" size="4"> 
+        <i>New Developer Services on OTN</i></font></b></font></p>
+      <p><b><font face="Arial, Helvetica, sans-serif" size="2">OTN Members: Get 
+        Oracle Software on CD </font></b><font face="Arial, Helvetica, sans-serif" size="2"><b>Shipped 
+        to you Today!<br>
+        </b> Order <a href="https://www.oracle.com/jsp/otntt/index.jsp">OTN TechTracks</a> 
+        and receive Oracle9i Database Release 2, Oracle9i Application Server Release 
+        2, and Oracle Developer Suite (including JDeveloper) CDs for the platform 
+        of your choice. TechTracks is a one-year subscription, and it includes 
+        access to Oracle Support's KnowledgeBase and CD updates shipped to you 
+        whenever there are major new releases of Oracle software. <i>Enter promo 
+        code OWC for a $50 savings during the month of July</i>.</font></p>
+      <p><font face="Arial, Helvetica, sans-serif" size="2"><b>Exchange your Knowledge 
+        through OTN Community Code Services<br>
+        </b><a href="http://otncast.otnxchange.oracle.com/">OTN Community Code</a> 
+        is a web-browsable CVS repository that lets you review, customize, extend, 
+        and share Oracle-related code and coding techniques. OTN populated it 
+        with sample application projects, so that you can view sample code source 
+        online, download it, submit bugs and suggestions to the development teams, 
+        and get email notifications when code is updated. Participate in an Oracle-sponsored 
+        project, and then create your own project and share your code with the 
+        OTN community.</font></p>
+      <p><b><font size="2" face="Arial, Helvetica, sans-serif">Web Services Center 
+        Now Available on OTN</font></b><font size="2" face="Arial, Helvetica, sans-serif"><br>
+        The <a href="http://otn.oracle.com/tech/webservices/">OTN Web Services 
+        Center</a> is a new resource for the development and deployment of Web 
+        services. Visitors to this new Center can experience live Web service 
+        examples, access the latest Web services technical information, and build 
+        their own Web services using <a href="http://otn.oracle.com/products/jdev/content.html">Oracle9i 
+        JDeveloper</a>. The Web Services Center offers information of value to 
+        Web services <a href="http://otn.oracle.com/tech/webservices/ws_architect.html">architects</a>, 
+        <a href="http://otn.oracle.com/tech/webservices/ws_appdev.html">developers</a> 
+        and <a href="http://otn.oracle.com/tech/webservices/learner.html">newcomers</a>.</font></p>
+      <p><font size="2" face="Arial, Helvetica, sans-serif"><b>Win Great Prizes 
+        in the OTN Web Services Challenge</b><br>
+        Developers are encouraged to submit their own Web services to the OTN 
+        Web Services Challenge. Entering your Web services makes you eligible 
+        for fantastic prizes, including a fully decked-out Dell mobile workstation. 
+        The Challenge starts August 1, so <a href="http://otn.oracle.com/tech/webservices/challenge.html">get 
+        a head start today</a> by learning more about the rules. You can even 
+        <a href="http://www.oracle.com/go/?&Src=1215798&Act=21">preregister your 
+        interest</a> in the Challenge.</font></p>
+      <p><font size="2" face="Arial, Helvetica, sans-serif"><b>New Internet Seminar: 
+        J2EE and Web Services on Linux with Oracle9iAS Release 2 </b><br>
+        <a href="http://www.oracle.com/go/?&Src=1377459&amp;Act=7">Attend</a> 
+        this on-demand Internet Seminar to learn how to use Oracle9i Application 
+        Server Release 2 to develop high performance J2EE and Web Services applications 
+        on the Linux operating systems.</font></p>
+      <p align="center"><font face="Arial, Helvetica, sans-serif" size="2"><a name="newdownloads"></a><a href="#feature">This 
+        Month's Feature</a> | <a href="#news">News</a> | <a href="#downloads">Software 
+        Downloads</a> | <a href="#ou">Oracle University</a> | <a href="#books">New 
+        Books</a> | <a href="#events">Worldwide Events</a></font></p>
+      <p align="left"><font face="Arial, Helvetica, sans-serif"><b><a name="news"></a>News</b></font></p>
+      <p align="left"><b><font size="2" face="Arial, Helvetica, sans-serif">Special 
+        Discount on Red Hat Linux Advanced Server</font></b> <font size="2" face="Arial, Helvetica, sans-serif"><br>
+        Receive up to 45% discount on the initial purchase of Red Hat Linux Advanced 
+        Server. <a href="http://www.oracle.com/go/?&Src=1376382&amp;Act=11">Find 
+        out how</a>! Offer valid July 1- July 31, 2002. To get more information 
+        on Oracle and Linux, <a href="http://otn.oracle.com/tech/linux">click 
+        here</a>. </font></p>
+      <p><b><font size="2" face="Arial, Helvetica, sans-serif">Helping WebGain 
+        Developers Move to Oracle9i JDeveloper</font></b><font size="2" face="Arial, Helvetica, sans-serif"><br>
+        With all the consolidation taking place in the Java tools space, developers 
+        are seeking tools that provide a complete and integrated environment for 
+        developing J2EE applications and Web services, and also offer security 
+        and stability for the future. Oracle9i JDeveloper delivers on all counts, 
+        and the new <a href="http://otn.oracle.com/products/jdev/htdocs/vcmigration/content.html">WebGain 
+        Developer Center on OTN</a> has been created to give VisualCafe users 
+        the resources to <a href="http://otn.oracle.com/products/jdev/htdocs/vcmigration/move.html">move</a> 
+        rapidly and smoothly to the integrated development environment of Oracle9i 
+        JDeveloper. <a href="http://www.oracle.com/ebusinessnetwork/showiseminar.html?1379826&">Listen</a> 
+        to the interview with Ted Farrell, Oracle's Senior Director of Applications 
+        Tools Technology and former WebGain CTO, on &quot;moving to Oracle9i JDeveloper&quot;. 
+        </font></p>
+      <p><font size="2" face="Arial, Helvetica, sans-serif"><b>Oracle9i Application 
+        Server # 1 in ECperf Benchmarks</b> <br>
+        In its first ECperf submissions, Oracle9i Application Server Release 2 
+        achieved the industry's best ever 'performance' benchmark at 61,863 BBops/min, 
+        beating IBM by 39% and BEA by 63%. The proof is in: Oracle9iAS is still 
+        faster than IBM and BEA. Oracle9iAS also achieved the best results in 
+        the ECperf 'price/performance' category at $5/BBop, 28% better than BEA's 
+        top result, and 54% better than IBM's top result. Get the facts: <a href="http://www.oracle.com/go/?&Src=1380990&amp;Act=7">read</a> 
+        the Oracle9iAS ECperf Benchmark Report now and <a href="http://www.oracle.com/ebusinessnetwork/showiseminar.html?1392270">tune 
+        into</a> a Live Internet Seminar and Q&amp;A on Wednesday, July 17 at 
+        8:00 a.m. PDT for a live presentation and discussion of these record setting 
+        results. </font></p>
+      <p><font size="2" face="Arial, Helvetica, sans-serif"><b>Four Internet Seminars 
+        on the New Security Features in Oracle9i Application Server Release 2</b> 
+        <br>
+        <a href="http://www.oracle.com/ip/deploy/ias/sso/index.html?iseminars.html">Watch</a> 
+        these four Internet Seminars to learn about the new security features 
+        in Oracle9i Application Server Release 2. Oracle9i Application Server 
+        Release 2 is the first application server to offer integrated support 
+        for Single Sign-On, JAAS and an LDAP compliant directory that together 
+        let you cost efficiently secure all your J2EE applications, portals, and 
+        Web services.</font></p>
+      <p><font size="2" face="Arial, Helvetica, sans-serif"><b>OTN Toolbar</b><br>
+        Search OTN from anywhere on the internet with OTN Toolbar. <a href="http://otn.oracle.com/toolbar/content.html">Download</a> 
+        today to easily gain access to many of the key features of OTN (including 
+        downloads, sample code, documentation, and discussion forums).</font></p>
+      <p><b><font size="2" face="Arial, Helvetica, sans-serif">New Internet Seminar 
+        on Oracle9iAS Web Cache and ESI</font></b> <font size="2" face="Arial, Helvetica, sans-serif"><br>
+        <a href="http://www.oracle.com/ebusinessnetwork/showiseminar.html?1293857">Watch</a> 
+        this Internet Seminar and learn how Oracle9iAS Web Cache lets you accelerate 
+        any Web application running on any server by up to 20 times. Speed applications 
+        built in Active Server Pages, Java Server Pages, Servlets, EJBs and more. 
+        Deploy with Web servers like Apache and Microsoft IIS as well as application 
+        servers like BEA WebLogic, IBM WebSphere, Sun/iPlanet and, of course, 
+        Oracle9iAS. Best of all, Oracle9iAS Web Cache uniquely supports caching 
+        of both static and dynamically generated content without changing the 
+        application, enabling dynamic Web sites to more efficiently deliver rich 
+        content and therefore improving the user experience. </font></p>
+      <p><font size="2" face="Arial, Helvetica, sans-serif"><b>Oracle9i Reports 
+        data source SDK available now</b><br>
+        The <a href="http://otn.oracle.com/products/reports/apis/pdstutorial/textPDS/index.html">Oracle9i 
+        Reports data source SDK</a> allows you to plug in your own data sources 
+        and benefit from the sophisticated report creation and distribution environment 
+        of <a href="http://otn.oracle.com/products/reports/content.html">Oracle9i 
+        Reports</a>. Check out the new documentation and samples. </font></p>
+      <p><font size="2" face="Arial, Helvetica, sans-serif"><b>Putting Forms on 
+        the Web </b><br>
+        Looking to move your existing Forms application from client/server to 
+        the Web? Want the easy access and maintainability of a web deployed Forms 
+        application? Then <a href="http://otn.oracle.com/products/forms/pdf/forms9icstowebmigration.pdf">check 
+        out this new paper</a>.</font></p>
+      <p><font size="2" face="Arial, Helvetica, sans-serif"><b>Struts and Oracle9i 
+        JDeveloper</b><br>
+        Here's a <a href="http://otn.oracle.com/products/jdev/howtos/jsp/StrutsHowTo.html">cool 
+        new article</a> with detailed instructions on how to configure and use 
+        the Jakarta Struts open source Model-View-Controller framework with <a href="http://otn.oracle.com/products/jdev/content.html">Oracle9i 
+        JDeveloper</a>.</font></p>
+      <p><font size="2" face="Arial, Helvetica, sans-serif"> <b>Quickstart with 
+        Oracle9i JDeveloper for BEA developers</b><br>
+        Are you using BEA's WebLogic and looking for development tools? Here is 
+        the <a href="http://otn.oracle.com/centers/mov2jdev">easy way to start</a> 
+        using the award winning Oracle9i JDeveloper with WebLogic. And if you 
+        want to use the fastest J2EE container, check out the <a href="http://www.oracle.com/go/?&Src=1260040&Act=8">migration 
+        kit</a> to Oracle9iAS.</font> </p>
+      <p><b><font size="2" face="Arial, Helvetica, sans-serif">Wireless and Voice 
+        Made Easy With Oracle9i Application Server</font></b> <font size="2" face="Arial, Helvetica, sans-serif"><br>
+        New Internet lessons give viewers the low-down on how to use the wireless 
+        and voice services of Oracle9i Application Server (Oracle9iAS Wireless) 
+        to quickly and easily give access to applications and data using any device, 
+        over any network. Learn why Oracle is a leader in wireless and voice infrastructure 
+        for yourselves! <a href="http://www.oracle.com/go/?&Src=1393043&amp;Act=9">Check 
+        out</a> the new Internet lessons in the FREE Mobile eKit!</font></p>
+      <p><font size="2" face="Arial, Helvetica, sans-serif"><b>Snapshot Seminar: 
+        Interwoven &amp; Oracle9iAS Content Management</b><br>
+        Oracle and Interwoven together offer a portal ready, proven, and flexible 
+        Enterprise Content Management solution. . Watch a 15 minute on-demand 
+        <a href="http://www.oracle.com/go/?&Src=1295633&Act=45">snapshot seminar</a> 
+        and learn how you can let your users control their content through a portal 
+        powered by Oracle and Interwoven. </font></p>
+      <p><b><font size="2" face="Arial, Helvetica, sans-serif">Updated Oracle9iAS 
+        Portal Developer Kit (PDK) - July<br>
+        </font></b><font size="2" face="Arial, Helvetica, sans-serif">The <a href="http://portalstudio.oracle.com">updated 
+        Oracle9iAS Portal Developer Kit (PDK)</a> highlights portlet communication. 
+        Using the PDK, you can build smart portlets with such features as inter-portlet 
+        communication, page to portlet communication, and portlet reusability. 
+        This release includes new J2EE-based and Web Services samples. </font></p>
+      <p><b><font size="2" face="Arial, Helvetica, sans-serif">Snapshot Seminar: 
+        Documentum &amp; Oracle9iAS Content Management</font></b><font size="2" face="Arial, Helvetica, sans-serif"><br>
+        Oracle and Documentum now offer a joint solution to create, manage and 
+        deliver content through Web sites and portals. Watch a 15 minute on-demand 
+        <a href="http://www.oracle.com/go/?&Src=1295633&Act=44">snapshot 
+        seminar</a> and learn how you can let your users control their content 
+        through a portal powered by Oracle and Documentum.</font></p>
+      <p></p>
+      <p align="center"><font face="Arial, Helvetica, sans-serif" size="2"><a name="newdownloads"></a><a href="#feature">This 
+        Month's Feature</a> | <a href="#news">News</a> | <a href="#downloads">Software 
+        Downloads</a> | <a href="#ou">Oracle University</a> | <a href="#books">New 
+        Books</a> | <a href="#events">Worldwide Events</a></font></p>
+      <p align="left"><font face="Arial, Helvetica, sans-serif"><b><a name="downloads"></a> 
+        New Software Downloads</b></font></p>
+      <p align="left"><font size="2" face="Arial, Helvetica, sans-serif"><a href="http://otn.oracle.com/software/products/ias/devuse.html">Oracle9i 
+        Application Server Release 2 for Windows NT/2000, AIX, and Compaq Tru64 
+        UNIX</a> </font></p>
+      <p><font size="2" face="Arial, Helvetica, sans-serif"><a href="http://otn.oracle.com/software/products/ias/devuse.html">Oracle9iAS 
+        TopLink 4.6 for Linux, UNIX, and Windows NT/2000</a> </font></p>
+      <p><font size="2" face="Arial, Helvetica, sans-serif"><a href="http://otn.oracle.com/software/products/lite/content.html">Oracle9i 
+        Lite Release 5.0.2.0.0 for Sun SPARC Solaris</a> </font></p>
+      <p><font size="2" face="Arial, Helvetica, sans-serif"><a href="http://otn.oracle.com/software/tech/windows/odpnet/content.html">Oracle 
+        Data Provider for .NET (ODP.NET) Beta</a> </font></p>
+      <p align="center"><font face="Arial, Helvetica, sans-serif" size="2"><a name="newdownloads"></a><a href="#feature">This 
+        Month's Feature</a> | <a href="#news">News</a> | <a href="#downloads">Software 
+        Downloads</a> | <a href="#ou">Oracle University</a> | <a href="#books">New 
+        Books</a> | <a href="#events">Worldwide Events</a></font></p>
+      <p><font face="Arial, Helvetica, sans-serif" size="2"><b><a name="ou"></a></b></font><font face="Arial, Helvetica, sans-serif"><b>Oracle 
+        University</b></font></p>
+      <p><b><font size="2" face="Arial, Helvetica, sans-serif">Special Offer! 
+        Save 45% on Oracle9i DBA Certification Training</font></b> <font size="2" face="Arial, Helvetica, sans-serif"><br>
+        The expanded Oracle Certification Program now offers a true certification 
+        levels that are built to fit the needs of IT professionals as well as 
+        organizations looking to hire them. Each level constitutes reaching a 
+        benchmark of experience and expertise that is industry recognized and 
+        approved. And, with each new credential can come increased opportunities, 
+        higher pay, and more benefits to keep Oracle professionals successful. 
+        </font></p>
+      <p><font size="2" face="Arial, Helvetica, sans-serif">Oracle9i Certification 
+        Savings Plan &#150; Save 45% on 4 Instructor Led inClass courses. 4 for 
+        the price of 2! <a href="http://www.oracle.com/education/index.html?promotions.html">Click 
+        here</a> to learn more! </font></p>
+      </td>
+    <td valign="top" width="14%" > 
+      <div align="center"> 
+        <table width="100%" border="0" cellspacing="0" cellpadding="1" bgcolor="#FF0000">
+          <tr> 
+            <td bgcolor="#000000"> 
+              <table width="100%" border="0" cellpadding="5" bgcolor="#FFFF00" cellspacing="0">
+                <tr> 
+                  <td bgcolor="#FFFFFF" valign="top"> 
+                    <p align="center"><img src="http://otn.oracle.com/techblast/images/LightBulb.gif" width="80" height="109"></p>
+                    <p align="left"><font size="1" face="Arial, Helvetica, sans-serif">Seeking 
+                      a new job? Check out <a href="http://seeker.dice.com/seeker.epl?rel_code=26&op=2&skill=oracle">OTN 
+                      Skills Marketplace</a> for all open Oracle-trained positions.</font></p>
+                    <p align="left"><font face="Arial, Helvetica, sans-serif" size="1">Need 
+                      help implementing technology solutions to business problems? 
+                      <a href="http://otn.oracle.com/products/oracle9i/htdocs/9iober2/index.html">Oracle9i 
+                      by Example Series tutorials</a> can save you time.</font></p>
+                    <p align="left"><font size="1" face="Arial, Helvetica, sans-serif">Taking 
+                      an OCP exam? OTN members, take advantage of the 20% exam 
+                      <a href="http://www.oracle.com/education/certification/faq/index.html?otndisc.html">discount</a>.</font></p>
+                    </td>
+                </tr>
+              </table>
+            </td>
+          </tr>
+        </table>
+      </div>
+    </td>
+  </tr>
+</table>
+</body>
+</html>
+
+
+
+
+
+
+
+
+
+
+
+<html>
+<head>
+<title>Untitled Document</title>
+<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
+</head>
+<body bgcolor="#FFFFFF" text="#000000">
+<table width="100%" border="0" cellspacing="10" >
+  <tr> 
+    <td valign="top" width="17%" > 
+      <h5>&nbsp;</h5>
+    </td>
+    <td valign="top" width="67%" > 
+      <div align="center"> 
+        <p align="center"><font face="Arial, Helvetica, sans-serif" size="2"><a name="newdownloads"></a><a href="#feature">This 
+          Month's Feature</a> | <a href="#news">News</a> | <a href="#downloads">Software 
+          Downloads</a> | <a href="#ou">Oracle University</a> | <a href="#books">New 
+          Books</a> | <a href="#events">Worldwide Events</a></font></p>
+        <div align="center"> 
+          <div align="center"> 
+            <div align="center"> 
+              <p align="left"><font face="Arial, Helvetica, sans-serif"><b><a name="books"></a>New 
+                Books </b></font></p>
+              <p align="left"><b><font size="2" face="Arial, Helvetica, sans-serif">Oracle9i 
+                DBA 101</font></b><font size="2" face="Arial, Helvetica, sans-serif"><br>
+                <a href="http://shop.osborne.com/cgi-bin/oraclepress/0072224746.html">Oracle9i 
+                DBA 101</a> by Marlene Theriault, Rachel Carmichael, &amp; James 
+                Viscusi (ISBN 0-07-222474-6) explains, step-by-step, how to effectively 
+                administer an Oracle database. Readers will find coverage of the 
+                key Oracle9i new features as well as details on the daily responsibilities 
+                of a DBA and tips on how to successfully accomplish those tasks. 
+                From the exclusive publishers of Oracle Press books, this is the 
+                ideal resource for the aspiring Oracle database administrator. 
+                </font></p>
+              <p align="left"><font size="2" face="Arial, Helvetica, sans-serif"><b>Oracle9i 
+                Mobile</b><br>
+                <a href="http://shop.osborne.com/cgi-bin/oraclepress/007222455X.html">Oracle9i 
+                Mobile</a> by Alan Yeung, Philip Stephenson, &amp; Nicholas Pang 
+                (ISBN 0-07-222455-X) helps readers design, deploy, and manage 
+                flexible mobile applications on the Oracle platform. From the 
+                exclusive publishers of Oracle Press books, this resource explains 
+                how to use and extend the mobile services available in Oracle9iAS 
+                Wireless and integrate with other Oracle technologies. Mobilize 
+                any e-business, reach new customers, and deliver critical information 
+                to mobile users with the most scalable and reliable mobile infrastructure 
+                available. </font></p>
+              <p align="left"><font size="2" face="Arial, Helvetica, sans-serif"> 
+                <b>Oracle Press User Group Program</b><br>
+                Oracle Press has a new User Group Program! Oracle Press supports 
+                the service that User Groups provide to the technical community. 
+                We value our relationship with community-based groups and welcome 
+                the opportunity to form partnerships with User Groups to disseminate 
+                the latest technological information available in Osborne publications. 
+                Osborne encourages participation by technical User Groups that 
+                meet regularly, discuss, teach, and troubleshoot technical topics, 
+                write book reviews, and publish print and/or online newsletters. 
+                </font></p>
+            </div>
+          </div>
+        </div>
+      </div>
+      <div align="left"> 
+        <p><font size="2" face="Arial, Helvetica, sans-serif">Oracle Press can 
+          provide User Groups: </font></p>
+      </div>
+      <ul>
+        <li> 
+          <div align="left"><font size="2" face="Arial, Helvetica, sans-serif">Review 
+            copies of Oracle Press books for newsletter reviews </font></div>
+        </li>
+        <li> 
+          <div align="left"><font size="2" face="Arial, Helvetica, sans-serif">Book 
+            donations and promotional items for User Group events </font></div>
+        </li>
+        <li> 
+          <div align="left"><font size="2" face="Arial, Helvetica, sans-serif">30% 
+            discount on bulk purchases of 10 or more books </font></div>
+        </li>
+        <li>
+          <div align="left"><font size="2" face="Arial, Helvetica, sans-serif">And 
+            more...</font></div>
+        </li>
+      </ul>
+      <div align="center"> 
+        <div align="center"> 
+          <div align="center"> 
+            <div align="center"> 
+              <p align="left"><font size="2" face="Arial, Helvetica, sans-serif"> 
+                <a href="http://www.osborne.com/usergroups/index.shtml">Click 
+                here</a> for complete details about Oracle Press' User Group Program.</font><font size="2"> 
+                </font> </p>
+              <p align="center"><font face="Arial, Helvetica, sans-serif" size="2"><a name="newdownloads"></a><a href="#feature">This 
+                Month's Feature</a> | <a href="#news">News</a> | <a href="#downloads">Software 
+                Downloads</a> | <a href="#ou">Oracle University</a> | <a href="#books">New 
+                Books</a> | <a href="#events">Worldwide Events</a></font></p>
+              <p align="left"><font face="Arial, Helvetica, sans-serif" size="2"><b><a name="events"></a></b></font><font face="Arial, Helvetica, sans-serif"><b>Worldwide 
+                Events </b></font></p>
+            </div>
+            <p align="left"><b><font face="Arial, Helvetica, sans-serif" size="2"><a name="americas"></a>Americas</font></b></p>
+            <p align="left"><b><font face="Arial, Helvetica, sans-serif" size="2">Oracle 
+              User Group Events</font></b><font face="Arial, Helvetica, sans-serif" size="2"><br>
+              <a href="http://otn.oracle.com/collaboration/user_group/events.html">Find 
+              out</a> where new user group events are happening in your area. 
+              </font> </p>
+            <p align="left"><font face="Arial, Helvetica, sans-serif" size="2"><b><a name="apac"></a>APAC</b></font></p>
+          </div>
+        </div>
+      </div>
+      <div align="left"> 
+        <table width="100%" border="0" cellpadding="5">
+          <tr> 
+            <td width="16%" height="47"><b><font face="Arial, Helvetica, sans-serif" size="2"><a href="http://www.oracle.com/oracleworld"><img src="http://otn.oracle.com/events/nsmailH020.gif" align=absmiddle 
+                       width="104" height="104" border="0"></a></font></b></td>
+            <td width="84%" valign="top"><b><font face="Arial, Helvetica, sans-serif" size="2">OracleWorld 
+              Online - Beijing <br>
+              </font></b><font size="2" face="Arial, Helvetica, sans-serif">Over 
+              5,000 industry professionals from all over China and the world gathered 
+              to learn how Oracle can help your business reduce costs, improve 
+              efficiencies, and improve the way you run your business. If you 
+              missed OracleWorld in Copenhagen, you can get all the highlights 
+              including keynotes, conference presentations and whitepapers <a href="http://www.oracle.com/oracleworld/online/beijing/">online</a>.</font></td>
+          </tr>
+        </table>
+        <br>
+      </div>
+      <p align="left"><font face="Arial, Helvetica, sans-serif" size="2"><b>Oracle 
+        iSeminars: Free &amp; Live @ Your Desktop<br>
+        </b> Attend a FREE Oracle APAC iSeminar to learn more about how Oracle9i 
+        - Application Server, Database &amp; Tools could provide you with a complete 
+        and cost-effective e-business infrastructure. </font></p>
+      <div align="left"></div>
+      <div align="center"> 
+        <div align="center"> 
+          <div align="center"> 
+            <p align="left"><font face="Arial, Helvetica, sans-serif" size="2">Please 
+              <a href="http://isdapac.oracle.com/iccdocs/seminarList.shtml">click 
+              here</a> for further information and online registration for all 
+              iseminars. (Please select correct time zone &amp; click &quot;reset&quot;). 
+              </font><font face="Arial, Helvetica, sans-serif" size="2">For any 
+              questions, please <a href="mailto:oracleisd_au@oracle.com">email</a> 
+              us.</font> </p>
+            <p align="left"><font face="Arial, Helvetica, sans-serif" size="2"><b><a name="emea"></a>EMEA</b></font></p>
+            <table width="100%" border="0" cellpadding="5">
+              <tr> 
+                <td width="16%" height="56"><b><font face="Arial, Helvetica, sans-serif" size="2"><a href="http://www.oracle.com/oracleworld"><img src="http://otn.oracle.com/events/nsmailH020.gif" align=absmiddle 
+                       width="104" height="104" border="0"></a></font></b></td>
+                <td width="84%" valign="top"><b><font size="2" face="Arial, Helvetica, sans-serif">OracleWorld 
+                  Online - Copenhagen<br>
+                  </font></b><font size="2" face="Arial, Helvetica, sans-serif">Thousands 
+                  of professionals from all over the world gathered to learn how 
+                  Oracle can help your business reduce costs, improve efficiencies, 
+                  and improve the way you run your business. If you missed OracleWorld 
+                  in Copenhagen, you can get all the highlights including keynotes, 
+                  conference presentations and whitepapers <a href="http://www.oracle.com/oracleworld/online/copenhagen/">online</a>.</font></td>
+              </tr>
+            </table>
+          </div>
+        </div>
+        <div align="left"><br>
+        </div>
+        <div align="left"><b><font size="2" face="Arial, Helvetica, sans-serif">Oracle 
+          Technology Days Belgian &amp; Luxembourg<br>
+          </font></b><font size="2" face="Arial, Helvetica, sans-serif">Join us 
+          for the Oracle Technology Days - Featuring Oracle9i Release 2 &#150; 
+          Live, Local, Free! </font></div>
+      </div>
+      <p align="left"><font size="2" face="Arial, Helvetica, sans-serif">Join 
+        us for this executive full-day event :<br>
+        29/8/2002 - Brussels (sessions in English)<br>
+        3/9/2002 - Gent (sessies in het Nederlands)<br>
+        12/9/2002 - Li&egrave;ge (session en fran&ccedil;ais)</font></p>
+      <p align="left"><font size="2" face="Arial, Helvetica, sans-serif"><a href="http://www.oracle.com/go/?&Src=1336077&Act=17">Click 
+        here</a> for more information and registration.</font></p>
+      <p align="left"><font size="2" face="Arial, Helvetica, sans-serif">Regards,</font></p>
+      <p align="left"><font size="2" face="Arial, Helvetica, sans-serif">Oracle 
+        Technology Network Team</font></p>
+      <p align="left"><font size="2" face="Arial, Helvetica, sans-serif"><b><font size="1">UNSUBSCRIBE<br>
+        </font></b><font size="1"> When you registered at OTN, you indicated you 
+        would like to receive e-mail updates from us. If you do not want to receive 
+        future e-mails, please visit our <a href="http://otn.oracle.com/admin/account/membership.html">update 
+        section</a> , log in with your username and password, and UNCHECK the 
+        I wish to receive informational e-mails box. </font></font></p>
+      <p align="left"><font size="1" face="Arial, Helvetica, sans-serif"><b>USERNAME 
+        AND PASSWORD QUESTIONS?<br>
+        </b> Forget your OTN login information? Use our <a href="http://otn.oracle.com/admin/account/membership.html">password 
+        lookup</a>.</font></p>
+      <p align="left"><b><font size="1" face="Arial, Helvetica, sans-serif">DUPLICATE 
+        MESSAGES?<br>
+        </font></b><font face="Arial, Helvetica, sans-serif" size="1"> You may 
+        have multiple accounts on OTN. Please send a message to <a href="mailto:otn_us@oracle.com">OTN</a> 
+        with the username you're using to access http://otn.oracle.com. We'll 
+        then contact you and delete the unused account. </font> 
+          </td>
+    <td valign="top" width="16%" > 
+      <div align="center"> </div>
+    </td>
+  </tr>
+</table>
+</body>
+</html>
+
+<p><font face="Arial, helvetica" size="1">
+<br>To be removed from Oracle's mailing lists, send an email to: 
+<br><a href="mailto:unsubscribe@oracleeblast.com?subject=REMOVE OF ORACLE MAILING LIST 1400444&body=REMOVE XXXXXX.YYYYY@RUHR-UNI-BOCHUM.DE ">unsubscribe@oracleeblast.com</a> 
+<br>with the following in the message body: 
+<br>REMOVE XXXXXX.YYYYY@RUHR-UNI-BOCHUM.DE
+<br>STOP 
+<p>
+[250000/116/137209217] 
+</font>
+<img src="http://www.oracle.com/elog/trackurl?di=1400444&si1=137209217" border=0> 
+
+
+
+
+
+
+
+
+
+
diff --git a/upstream/t/data/welcomelists/orbitz.com b/upstream/t/data/welcomelists/orbitz.com
new file mode 100644 (file)
index 0000000..50dcb30
--- /dev/null
@@ -0,0 +1,27 @@
+Received: (qmail 9304 invoked by uid 505); 12 Aug 2002 16:57:57 -0000
+Delivered-To: zzzzzz@xyz.com
+Received: (qmail 19051 invoked by uid 74); 12 Aug 2002 16:58:16 -0000
+Received: from travelercare@orbitz.com by agogo0 by uid 71 with qmail-scanner-1.13 
+ (clamscan: 0.22.  Clear:SA:1(0/0):. 
+ Processed in 0.774434 secs); 12 Aug 2002 16:58:16 -0000
+Received: from unknown (HELO mailhost.wm.orbitz.com) (65.216.67.72)
+  by mail0.tyva.xyz.com with SMTP; 12 Aug 2002 16:58:15 -0000
+Received: from wl14 (sim-snat-01.wm.orbitz.com [10.50.100.11])
+       by mailhost.wm.orbitz.com (8.12.1/8.12.1) with ESMTP id g7CGwEsF005188
+       for <zzzzz@xyz.com>; Mon, 12 Aug 2002 11:58:14 -0500
+Message-ID: <17728173.1029171478187.JavaMail.weblogic@wl14>
+Date: Mon, 12 Aug 2002 11:57:58 -0500 (CDT)
+From: Orbitz Traveler Care <travelercare@orbitz.com>
+To: Rod <zzzzz@xyz.com>
+Subject: Orbitz Travel Document
+Mime-Version: 1.0
+Content-Type: text/html
+Content-Transfer-Encoding: 7bit
+
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html>
+<head>
+<title>Orbitz Travel Document</title>
+</head>
+
diff --git a/upstream/t/data/welcomelists/paypal.com b/upstream/t/data/welcomelists/paypal.com
new file mode 100644 (file)
index 0000000..de3fc95
--- /dev/null
@@ -0,0 +1,46 @@
+Received: (qmail 18217 invoked from network); 4 Apr 2002 21:41:45 -0000
+Received: from localhost (127.0.0.1)
+  by localhost with SMTP; 4 Apr 2002 21:41:45 -0000
+Delivered-To: zzzzzz@xyz.com
+Received: from mail.xyz.com [64.123.162.104]
+       by localhost with POP3 (fetchmail-5.9.0)
+       for xyz@localhost (single-drop); Thu, 04 Apr 2002 16:41:45 -0500 (EST)
+Received: (qmail 857 invoked from network); 4 Apr 2002 21:41:11 -0000
+Received: from unknown (HELO web18.nix.paypal.com) (65.206.229.164)
+  by mail0.tyva.xyz.com with SMTP; 4 Apr 2002 21:41:11 -0000
+Received: (qmail 13807 invoked by uid 99); 4 Apr 2002 21:41:10 -0000
+Date: Thu, 04 Apr 2002 13:41:10 -0800
+Message-Id: <1017000000.00000@paypal.com>
+From: service@paypal.com
+To: rod@xyz.com
+Subject: Receipt for your Payment
+
+This email confirms that you have paid xyz.com Ltd. $12.00 
+using PayPal.
+
+---------------------------------------------------------------
+This payment was sent using your bank account. 
+
+By using your bank account to send money, you just:
+
+- Paid instantly and securely 
+- Sent money faster than writing and mailing paper checks.
+- Received an additional entry in our $1,000 Sweepstakes!
+  
+Thanks for using your bank account!
+
+---------------------------------------------------------------
+
+
+Thank you,
+The PayPal Team
+
+Note: When you log in to your PayPal account, be sure that the website's URL always begins with "https://www.paypal.com/".  The "s" in "https" at the beginning of the URL means you are logging into a secure page.  If the URL does not begin with https, you are not on a PayPal page.  
+
+
+
+Please do not reply to this e-mail. Mail sent to this address 
+cannot be answered. For assistance, log in to your PayPal 
+account and choose the "Help" link in the footer of any page.
+
+
diff --git a/upstream/t/data/welcomelists/register.com_password b/upstream/t/data/welcomelists/register.com_password
new file mode 100644 (file)
index 0000000..6973a66
--- /dev/null
@@ -0,0 +1,53 @@
+From customersupport@register.com  Wed Jan 30 09:50:12 2002
+Delivery-Date: Mon, 18 Sep 2000 16:41:35 +0100
+Return-Path: <customersupport@register.com>
+Delivered-To: jm@ooooooooooo.com
+Received: from wwwn.register.com (outgoing2.jrcy.register.com [209.67.50.16])
+        by mail (Postfix) with ESMTP id 9A73FD894B
+        for <ppppp@ooooooooooo.com>; Mon, 18 Sep 2000 15:41:33 +0000 (Eire)
+Received: (from nobody@localhost)
+        by wwwn.register.com (8.9.3/8.9.3) id LAA18712
+        for ppppp@ooooooooooo.com; Mon, 18 Sep 2000 11:41:22 -0400
+Date: Mon, 18 Sep 2000 11:41:22 -0400
+Message-Id: <200009181541.LAA18712@wwwn.register.com>
+X-Authentication-Warning: wwwn.register.com: nobody set sender to customersupport@register.com using -f
+From: Domain.Management.System@www.register.com
+Reply-To: customersupport@register.com
+To: ppppp@ooooooooooo.com
+Subject: Domain Manager Password
+Sender: customersupport@register.com
+
+User Name : xxxxxxxxxxxxxxx
+
+Thank you for using register.com's Domain Manager.
+
+To change or re-enter your password, please copy and paste the URL below into the "Location" or "Address" field of your web browser and hit the 'Enter' key on your keyboard.  Note: If your email program supports HTML, you may be able to click on the link below.
+
+==========================================================================================
+http://mydomain.register.com/change_password.cgi?00000000000
+==========================================================================================
+Note:  Above link will be expire within three days
+
+The page displayed will allow you to change or re-enter your Domain Manager password.
+
+In the event that the email program you are using does not display the URL as a hyperlink or the URL is broken into two lines, do not click on it.  Instead, please follow the copy and pasting instructions below to complete the confirmation process.
+
+- Copy and Pasting Instructions -
+
+Highlight the URL with your cursor. Once you have highlighted the URL, hit CTRL + C to copy the highlighted area.
+
+Open an Internet browser window and click in the Address or Location field.  Hit CTRL + V to paste the URL into the address field.  If necessary, repeat this process with the second line of the URL. Please be sure to delete spaces if there are any embedded in the URL - otherwise you will not be able to connect to the proper confirmation page.
+
+Once you have entered and looked over the URL, hit the Enter key on your keyboard. The web page displayed will allow you to complete the final step in the confirmation process.
+
+If you have further questions, please do not hesitate to contact us at:
+http://www.register.com/create_ticket.cgi
+
+Thank you for using register.com, the first step on the web.
+
+Customer Service
+register.com, inc
+http://www.register.com
+
+
+
diff --git a/upstream/t/data/welcomelists/ryanairmail.com b/upstream/t/data/welcomelists/ryanairmail.com
new file mode 100644 (file)
index 0000000..a99c301
--- /dev/null
@@ -0,0 +1,165 @@
+From webster@ryanairmail.com  Fri Aug 16 12:59:01 2002
+Return-Path: <webster@ryanairmail.com>
+Delivered-To: zzzz@localhost.foofoofoofoo.com
+Received: from localhost (localhost [127.0.0.1])
+       by phobos.labs.foofoofoofoo.com (Postfix) with ESMTP id E163743C32
+       for <zzzz@localhost>; Fri, 16 Aug 2002 07:58:59 -0400 (EDT)
+Received: from phobos [127.0.0.1]
+       by localhost with IMAP (fetchmail-5.9.0)
+       for zzzz@localhost (single-drop); Fri, 16 Aug 2002 12:58:59 +0100 (IST)
+Received: from mail.ryanair2.ie ([193.120.152.8]) by dogma.slashnull.org
+    (8.11.6/8.11.6) with SMTP id g7GBwca16137 for <xxxxx@yyyyyy.zzz>;
+    Fri, 16 Aug 2002 12:58:38 +0100
+From: webster@ryanairmail.com
+To: "Customers" <customers@mail.ryanairmail.com>
+Subject: Incredible Autumn Fares
+Date: Fri, 16 Aug 2002 08:41:00 +0100
+X-Assembled-BY: XWall v3.21
+X-Mailer: MailBeamer v3.28
+Message-Id: <LISTMANAGER-123546-16680-2002.08.16-08.51.02--xxxxx@yyyyyy.zzz@mail.ryanairmail.com>
+MIME-Version: 1.0
+Content-Type: text/plain; charset="iso-8859-1"
+List-Unsubscribe: <mailto:leave-customers-123546K@mail.ryanairmail.com>
+Reply-To: webster@ryanairmail.com
+Content-Transfer-Encoding: 8bit
+X-MIME-Autoconverted: from quoted-printable to 8bit by dogma.slashnull.org
+    id g7GBwca16137
+
+Massive seat sale this weekend on Ryanair.com
+Fares from £ 6.25 one way including taxes
+Travel between 9 September and 17 December
+Sale until midnight Monday 19 August
+Travel between 1200 hrs Monday and 1300 hrs Thursday or
+Saturdays after 1200hrs to get these fares
+Limited availability during school break periods and bank
+holiday weekends.  All fares quoted are one way including taxes.
+Book now at http://www.ryanair.com
+
+
+*********************** Domestic UK *************************
+London Stansted to Glasgow Prestwick           from £  6.25
+Glasgow Prestwick to London Stansted           from £  6.25
+London Stansted to City of Derry               from £12.99
+City of Derry to London Stansted                       from £12.99
+
+*********************** UK to Scandinavia *************************
+London Stansted to Gothenburg                  from £  9.99
+London Stansted to Stockholm NYO               from £12.99
+London Stansted to Stockholm VST               from £12.99
+London Stansted to Aarhus                      from £14.99
+London Stansted to Esbjerg                     from £14.99
+London Stansted to Oslo Torp                   from £14.99
+Glasgow Prestwick to Oslo Torp                 from £19.99
+
+****************** UK to Belgium/Netherlands ********************
+London Stansted to Brussels Charleroi          from £  9.99
+London Stansted to Eindhoven                   from £12.99
+Liverpool to Brussels Charleroi                        from £12.99
+Glasgow Prestwick to Brussels Charleroi        from £12.99
+
+****************** UK to France/Italy ********************
+London Stansted to Dinard                      from £14.99
+London Stansted to St Etienne                  from £14.99
+London Stansted to Milan Bergamo               from £14.99
+Glasgow Prestwick to Paris Beauvais            from £14.99
+
+****************** UK to Germany/Austria ********************
+Bournemouth to Frankfurt Hahn                  from £12.99
+London Stansted to Frankfurt Hahn              from £12.99
+London Stansted to Hamburg Lubeck              from £14.99
+London Stansted to Klagenfurt                  from £14.99
+
+*********************** UK to Ireland *************************
+Manchester to Dublin                           from £  9.99
+Leeds Bradford to Dublin                               from £  9.99
+Bristol to Dublin                                      from £  9.99
+Edinburgh to Dublin                            from £  9.99
+Teesside to Dublin                             from £  9.99
+Glasgow Prestwick to Dublin                    from £  9.99
+Bournemouth to Dublin                          from £  9.99
+Liverpool to Dublin                            from £  9.99
+London Stansted to Knock                       from £12.99
+London Stansted to Shannon                     from £14.99
+London Stansted to Cork                        from £14.99
+London Luton to Dublin                         from £14.99
+London Gatwick to Dublin                       from £14.99
+London Stansted to Dublin                      from £14.99
+
+*********************** Ireland to UK *************************
+Dublin to Liverpool                            from Eur  9.99
+Dublin to Manchester                           from Eur  9.99
+Dublin to Bournemouth                          from Eur12.99
+Dublin to Bristol                                      from Eur12.99
+Dublin to Leeds Bradford                               from Eur12.99
+Dublin to Edinburgh                            from Eur12.99
+Dublin to Teesside                             from Eur12.99
+Dublin to Glasgow Prestwick                    from Eur12.99
+Knock to London Stansted                       from Eur12.99
+Cork to London Stansted                        from Eur14.99
+Shannon to London Stansted                     from Eur14.99
+Dublin to London Stansted                      from Eur14.99
+****************************************************************
+
+====================================================================
+
+E-MAIL DISCLAIMER
+
+This e-mail and any files and attachments transmitted with it
+are confidential and may be legally privileged. They are intended
+solely for the use of the intended recipient.  Any views and
+opinions expressed are those of the individual author/sender
+and are not necessarily shared or endorsed by Ryanair Holdings plc
+or any associated or related company. In particular e-mail
+transmissions are not binding for the purposes of forming
+a contract to sell airline seats, directly or via promotions,
+and do not form a contractual obligation of any type.
+Such contracts can only be formed in writing by post or fax,
+duly signed by a senior company executive, subject to approval
+by the Board of Directors.
+
+The content of this e-mail or any file or attachment transmitted
+with it may have been changed or altered without the consent
+of the author.  If you are not the intended recipient of this e-mail,
+you are hereby notified that any review, dissemination, disclosure,
+alteration, printing, circulation or transmission of, or any
+action taken or omitted in reliance on this e-mail or any file
+or attachment transmitted with it is prohibited and may be unlawful.
+
+If you have received this e-mail in error
+please notify Ryanair Holdings plc by emailing postmaster@ryanair.ie
+or contact Ryanair Holdings plc, Dublin Airport, Co Dublin, Ireland.
+
+=====================================================================
+
+E-MAIL DISCLAIMER
+
+This e-mail and any files and attachments transmitted with it 
+are confidential and may be legally privileged. They are intended 
+solely for the use of the intended recipient.  Any views and 
+opinions expressed are those of the individual author/sender 
+and are not necessarily shared or endorsed by Ryanair Holdings plc 
+or any associated or related company. In particular e-mail 
+transmissions are not binding for the purposes of forming 
+a contract to sell airline seats, directly or via promotions, 
+and do not form a contractual obligation of any type.   
+Such contracts can only be formed in writing by post or fax, 
+duly signed by a senior company executive, subject to approval 
+by the Board of Directors.
+
+The content of this e-mail or any file or attachment transmitted 
+with it may have been changed or altered without the consent 
+of the author.  If you are not the intended recipient of this e-mail, 
+you are hereby notified that any review, dissemination, disclosure, 
+alteration, printing, circulation or transmission of, or any 
+action taken or omitted in reliance on this e-mail or any file 
+or attachment transmitted with it is prohibited and may be unlawful.
+
+If you have received this e-mail in error 
+please notify Ryanair Holdings plc by emailing postmaster@ryanair.ie
+or contact Ryanair Holdings plc, Dublin Airport, Co Dublin, Ireland.  
+
+
+---
+You are currently subscribed to customers as: xxxxx@yyyyyy.zzz
+To unsubscribe send a blank email to leave-customers-123546K@mail.ryanairmail.com
+
diff --git a/upstream/t/data/welcomelists/sf.net b/upstream/t/data/welcomelists/sf.net
new file mode 100644 (file)
index 0000000..7d9a1ab
--- /dev/null
@@ -0,0 +1,263 @@
+From noreply@sourceforge.net  Wed Aug 14 17:36:08 2002
+Return-Path: <noreply@sourceforge.net>
+Delivered-To: aaaa@localhost.xxxxxxxxxxxx.com
+Received: from localhost (localhost [127.0.0.1])
+       by phobos.labs.xxxxxxxxxxxx.com (Postfix) with ESMTP id EEAC943C32
+       for <aaaa@localhost>; Wed, 14 Aug 2002 12:36:06 -0400 (EDT)
+Received: from phobos [127.0.0.1]
+       by localhost with IMAP (fetchmail-5.9.0)
+       for aaaa@localhost (single-drop); Wed, 14 Aug 2002 17:36:07 +0100 (IST)
+Received: from usw-sf-list2.sourceforge.net (usw-sf-fw2.sourceforge.net
+    [216.136.171.252]) by dogma.slashnull.org (8.11.6/8.11.6) with ESMTP id
+    g7EGW3424685 for <xxxxx@yyyyyy.zzz>; Wed, 14 Aug 2002 17:32:04 +0100
+Received: from usw-sf-db2-b.sourceforge.net ([10.3.1.4]
+    helo=sourceforge.net ident=tperdue) by usw-sf-list2.sourceforge.net with
+    smtp (Exim 3.31-VA-mm2 #1 (Debian)) id 17f141-00043a-00 for
+    <xxxxx@yyyyyy.zzz>; Wed, 14 Aug 2002 09:32:05 -0700
+From: Mailer <noreply@sourceforge.net>
+To: "" <xxxxx@yyyyyy.zzz>
+Subject: SOURCEFORGE.NET UPDATE: August 14, 2002
+Message-Id: <E17f141-00043a-00@usw-sf-list2.sourceforge.net>
+Date: Wed, 14 Aug 2002 09:32:05 -0700
+
+
+(You are receiving this email because you subscribed to it (honest).
+For information on how to unsubscribe please read the bottom of this
+email).
+
+0. INTRO.  [IBM DB2]
+1. INCREASED DOWNLOAD CAPACITY.
+2. AUDIO OF KERNEL SUMMIT AVAILABLE.
+3. BE A SF.NET FOUNDRY GUIDE.
+4. WORK FOR SOURCEFORGE.NET
+5. SITE STATISTICS
+
+
+0. INTRO
+
+Hello SourceForge.net Users,
+
+This week we've made a big announcement.  As you likely know, any
+large dynamic website is powered by a database that funnels data to
+the web servers serving data which ultimately gets sent to you.
+These databases manage everything from user authentication, session
+management, site searching, etc.  SourceForge.net is a database-
+dependent website.
+
+Today we have announced that we are moving SourceForge.net to DB2,
+a powerful relational database by IBM.  We are doing this because
+the site continues to grow at a rapid rate, with 700 new users and
+70 new projects a day, and we need a database that can handle this
+growth.  We feel that DB2 can do this for us, and IBM is giving us
+the resources to make this transition successful.  You can read the
+press release here:
+
+http://www.vasoftware.com/news/press.php/2002/1070.html
+
+How will this effect you?  In the first phase, you won't see much
+difference other then the site will continue to grow and the
+SourceForge.net team will be able to handle the growth.  In later
+phases you will see new features on the site that take advantage of
+the databases advanced capabilities.
+
+Today our mail archives have been converted over.  The rest of the
+site will make the migration to DB2 in the coming months.
+
+If you have questions about this or any other aspect of the site,
+please feel free to email me, pat@sourceforge.net.  I always
+appreciate the feedback.
+
+Thank you for your continued support of SourceForge.net and the
+Open Source Community.
+
+Pat-
+
+Patrick McGovern
+Director, SourceForge.net
+
+
+
+1. INCREASED DOWNLOAD CAPACITY
+
+SourceForge.net continues to grow, and it's appetite for bandwidth
+is never-ending.  Every day SF.NET serves over 300,000 files to
+ensure that developers and end-users within the Open Source
+community can always obtain the software released by hosted
+projects, SourceForge.net maintains a network of high-capacity
+download servers.  These servers are located throughout the world,
+as to provide better download times regardless of which network
+provider you are using, and regardless of your geographic location.
+
+Three new download servers have recently been added to our network,
+further strengthening our file serving capabilities.  These latest
+additions include servers hosted by:
+
+Time Warner Telecom (Wisconsin,USA);
+http://www.twtelecom.com/
+
+University of Minnesota (Minnesota, USA)
+http://www.umn.edu/
+
+CESNET (Czech Republic)
+http://www.cesnet.cz/
+
+We thank these sponsors for their commitment to SourceForge.net and
+the needs of the Open Source community.
+
+On a related note, we are looking for a mirror in Japan.  If you are
+an ISP or University in Japan and are willing to spare 20Mbps for a
+SourceForge.net mirror (we'll supply the hardware), please let us
+know at bandwidth@sourceforge.net
+
+
+
+2. AUDIO OF KERNEL SUMMIT AVAILABLE
+
+SourceForge.net now has the audio from the entire 2002 OSDN/USENIX
+Kernel Summit, held in June.  Listen to the Linux kernel master
+discuss such hot topics as kernel modules, virtual memory,
+block I/O, database scaling, security modules, and async I/O.
+You may find this audio repository at:
+
+http://linuxkernel.foundries.sourceforge.net/article.pl?sid=02/06/26/0116225
+
+
+
+3. CONTRIBUTE TO SOURCEFORGE.NET! BE A FOUNDRY GUIDE!
+
+Want to contribute to SourceForge.net, but you don't know how to
+code? Be a foundry guide!  Foundry guides get to hype the cool
+projects that they think are worth downloading and testing.
+A guide finds all the stuff on the web about their subject of
+choice, and gives it prominent placement.  How do you become a
+foundry guide? Go to http://foundries.sourceforge.net/; find a
+topic that interests you; and send email to
+foundries@sourceforge.net stating your desired topic and why you
+are qualified to be a foundry guide.
+
+
+
+4. WORK FOR SOURCEFORGE.NET
+
+We have a new position for a senior web developer available at
+SourceForge.net.  We are looking for someone to help us maintain,
+upgrade, and add new features to SourceForge.net.  Ideal person has
+5+ years of development experience on high end, high volume
+websites (3+ million page views a day).  Has a vast level of
+knowledge of Internet technologies:  PHP, PostgreSQL, MySQL, DB2,
+Linux, PERL, Apache, LDAP, Mailman.  A flare for design / UI is a
+bonus.  SourceForge is a unique site with unique challenges.  We are
+looking for someone at the top of their game.
+
+Location of Job is in Fremont, California.  Please send resume and
+URL's of sites you have worked on to jobs@sourceforge.net.  Text
+resumes only.  (No MS WORD files!)
+
+
+
+5. SITE STATISTICS
+
+Stats: (Monday 12th, 2000)
+Hosted Projects: 45,194
+Registered Users: 465,530
+Page Views: 3,344,708 in a single day (Monday)
+Files transfered in a single day: 340,838 (Monday)
+Emails sent in a single day from Mailing lists: 851,143 (Monday)
+
+
+Top Ten Projects
+
+1 phpMyAdmin
+http://sourceforge.net/projects/phpmyadmin/
+phpMyAdmin is a tool written in PHP intended to handle the
+administration of MySQL over the WWW.  Currently it can create and
+drop databases, create/drop/alter tables, delete/edit/add fields,
+execute any SQL statement, manage keys on fields.
+
+2 Compiere ERP + CRM Business Solution
+http://sourceforge.net/projects/compiere/
+Smart ERP+CRM solution for small-medium enterprises (SME) in the
+global marketplace covering all areas from customer management,
+supply chain and accounting.  For $2-200M revenue companies looking
+for "brick and click" first tier functionality.
+
+3 SquirrelMail
+http://sourceforge.net/projects/squirrelmail/
+SquirrelMail is a PHP4-based Web email client.  It includes built-in
+pure PHP support for IMAP and SMTP, and renders all pages in pure
+HTML 4.0 for maximum compatibility across browsers.  It also has
+MIME support, folder manipulation, etc
+
+4 TUTOS
+http://sourceforge.net/projects/tutos/
+TUTOS is the ultimate team organization software, a web-based
+groupware or ERP/CRM system to manage events/calendars, addresses,
+teams, projects,tasks,bugs,mailboxes,documents and your time spent
+with these things
+
+5 JBoss.org
+http://sourceforge.net/projects/jboss/
+The JBoss/Server is the leading Open Source, standards-compliant,
+J2EE based application server implemented in 100% Pure Java
+
+6 Firewall Builder
+http://sourceforge.net/projects/fwbuilder/
+Object-oriented GUI and set of compilers for various firewall
+platforms.  Currently implemented compilers for iptables, ipfilter
+and OpenBSD pf
+
+7 openMosix
+http://sourceforge.net/projects/openmosix/
+openMosix is a Linux kernel extension for single-system image
+clustering.  Taking n PC boxes, openMosix gives users and
+applications the illusion of one single computer with n CPUs.
+openMosix is perfectly scalable and adaptive.
+
+8 CDex
+http://sourceforge.net/projects/cdexos/
+CDex a CD-Ripper, thus extracting digital audio data from an Audio
+CD. The application supports many Audio encoders, like MPEG
+(MP2,MP3), VQF, AAC encoders.
+
+9 phpChrystal - An Open Intranet System
+http://sourceforge.net/projects/phpchrystal/
+phpChrystal ist ein OpenSource-Intranetsystem welches vorrangig auf
+Lan-Partys eingesetzt werden kann.  Vorteile von phpChrystal sind
+seine Portierbarkeit, Flexibilität und Performance, da es vollends
+auf PHP, MySQL und XML basiert
+
+10 Dev-C++
+http://sourceforge.net/projects/dev-cpp/
+Dev-C++ is an full-featured Integrated Development Environment
+(IDE) for Win32 and Linux.  It uses GCC, Mingw or Cygwin as
+compiler and libraries set.
+
+More Top Projects:
+http://sourceforge.net/top/mostactive.php?type=week
+
+
+
+
+
+EMAIL LIST REMOVAL:
+
+When the SF.NET team sends out a site-wide email, we sometimes see
+replies that look like this:  "Hey!! I didn't subscribe to this list!!!
+You spammer. I hate you!  I hate your dog!  (insert other colorful
+phrases here)".  The truth is, when you registered on SourceForge.net
+there was a check box that said "Receive Site-wide updates, low
+volume".  You left it checked when you submitted the registration form,
+hence you are receiving this email.  We send these updates every 4 to 6
+weeks, so it truly is low volume.  However if you want off, this is not
+a problem.  Simply click on the link below.
+
+
+==================================================================
+You receive this message because you subscribed to SourceForge
+site mailing(s). You may opt out from some of them selectively
+by logging in to SourceForge and visiting your Account Maintenance
+page (http://sourceforge.net/account/), or disable them altogether
+by visiting following link:
+<http://sourceforge.net/account/unsubscribe.php?ch=_ac9123456755a6f7>
+
+
diff --git a/upstream/t/data/welcomelists/winxpnews.com b/upstream/t/data/welcomelists/winxpnews.com
new file mode 100644 (file)
index 0000000..6df65ee
--- /dev/null
@@ -0,0 +1,528 @@
+Received: from ooooooooo.net (ns1.ooooooooo.net [216.27.147.130])
+       by dogma.slashnull.org (8.11.6/8.11.6) with ESMTP id g6J6AZJ25232
+       for <aaaaaa@yyyyyy.zzz>; Fri, 19 Jul 2002 07:10:35 +0100
+Received: from bounce.winxpnews.com (dal21037lyr001.datareturn.com [216.46.238.20])
+       by ooooooooo.net (8.11.3/8.11.1) with SMTP id g6J6ABS16827
+       for <zzzz@zzzzzzzz.com>; Fri, 19 Jul 2002 02:10:12 -0400 (EDT)
+       (envelope-from do_not_reply@bounce.winxpnews.com)
+Importance: Normal
+To: zzzz@zzzzzzzz.com
+Reply-To: "WinXPnews"<do_not_reply@bounce.winxpnews.com>
+Content-Type: text/html;
+        charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Date: Fri, 19 Jul 2002 01:10:07 -0600
+From: "WinXPnews"<do_not_reply@bounce.winxpnews.com>
+Subject: WinXPnews: Time To Patch Your Windows Media Player
+Message-Id: <5ksc2.105x1y34m@bounce.winxpnews.com>
+X-Priority: 3 (Normal)
+X-MSMail-Priority: Normal
+
+<html><head><!--
+***************************** WinXPnews HTML ****************************
+     If you can see this text, then you are not using an HTML enabled
+     email client or your email client could not interpret this HTML.
+                 Please read the following instructions!
+  This is a posting from WinXPnews for zzzz@zzzzzzzz.com
+  To manage your profile, click on the following customized link:
+         http://www.winxpnews.com/login.cfm?id=9665862091709486
+  You can modify or delete your profile there. You may also forward this
+  email to listmanager@winxpnews.com stating that you wish to be removed
+  from WinXPnews. Please include this complete text section in your email.
+ --- Read this newsletter online by visiting http://www.winxpnews.com ---
+ --- Please disregard all the text below as it is HTML formatted text ---
+***************************** WinXPnews HTML *****************************
+-->
+<title>WinXPnews&#153;</title>
+<style type="text/css">
+a:link {color: #b04040; font-weight: bold;}
+a:visited {color: #804040; font-weight: bold;}
+a:active {color: #ff0000; font-weight: bold;}
+a:hover {color: #ff0000; font-weight: bold;} </style>
+</head>
+<body bgcolor="#ffffff" topmargin="0" leftmargin="0" marginheight="0" marginwidth="0">
+<table width = '100%' border = '0'>
+<tr>
+<td bgcolor = '#0055e7' align='right'>
+<img src='http://www.winxpnews.com/graphics/winxpnews_logo_200.jpg' align='left' border='0'>
+<font face='verdana, sans-serif' size='5' color='#ffffff'>
+<b>WinXPnews&#153; E-Zine</b><br>
+</font>
+<font face='verdana, sans-serif' size='1' color='#ffffff'>
+Tue, Jul 9, 2002 (Vol. 2, 27 - Issue 33)
+</font>
+</td>
+</tr>
+<tr><td align='center'><font face='verdana, sans-serif' size='2'><b>
+Feel free to forward this newsletter to other WinXP enthusiasts.</b><br>
+<b>Read this newsletter online here:
+<a href="http://www.winxpnews.com/?id=33">
+http://www.winxpnews.com/?id=33</a><br>
+For a quick unsubscribe (gasp!) click here:<br>
+<a href="http://www.winxpnews.com/unsubscribe.cfm?email=zzzz@zzzzzzzz.com">
+http://www.winxpnews.com/unsubscribe.cfm?email=zzzz@zzzzzzzz.com</a></b>
+</font>
+<img src='http://www.winxpnews.com/tr/tr.cfm?mid=9665862091709486&xid=33'
+width='0' height='0' border='0'>
+</td></tr>
+<tr>
+<td>
+<font face='verdana, sans-serif' size='5'>
+<b>Time To Patch Your Windows Media Player</b>
+</font>
+</td>
+</tr>
+<tr>
+<td>&nbsp;</td>
+</tr>
+<tr>
+<td background='http://www.winxpnews.com/graphics/h_border.gif' align=left>
+<img src='http://www.winxpnews.com/graphics/winxpnews_logo_50.jpg' width='53' height='24' align='right'>
+<font face='verdana, sans-serif' size='4' color='#ffffff'>
+&nbsp;&nbsp;This issue of WinXPnews&#153 contains:<br>
+</font>
+</td>
+</tr>
+<tr>
+<td><font size='1'>&nbsp;</td>
+</tr>
+<tr>
+<td>
+<font face='verdana, sans-serif' size='2'>
+<ol>
+<li>EDITOR'S CORNER
+<code1=zzzz@zzzzzzzz.c
+<ul type='square'>
+<li>How to Publish Your Windows XP FTP Server to the Internet
+</ul>
+<li>HINTS, TIPS, TRICKS & TWEAKS
+<ul type='square'>
+<li>Allow Dial-up Connections to Synchronize Time with Internet Time Servers
+</ul>
+<li>HOW TO'S: ALL THE NEW XP FEATURES
+<ul type='square'>
+<li>How to Secure an FTP Server on Windows XP Professional
+</ul>
+<li>WINXP SECURITY: UPDATES & PATCHES
+<ul type='square'>
+<li>Cumulative Patch for Windows Media Player<li>Cumulative Patches for Excel and Word for Windows
+</ul>
+<li>UPGRADING & COMPATIBILITY ISSUES
+<ul type='square'>
+<li>A Computer May Hang During a Heavy Load with an Ericsson HIS Modem<li>Knowledge Base Search Center - If it is Not Broke, Do Not Break it!
+</ul>
+<li>WINXP CONFIGURING & TROUBLESHOOTING
+<ul type='square'>
+<li>A Description of the Repair Option on a Local Area Network or High-Speed Internet Connection<li>Keyboard and Mouse Do Not Work When You Start Windows<li>How to Deploy Windows XP Images from Windows 2000 RIS Servers
+</ul>
+<li>FAVE LINKS
+<ul type='square'>
+<li>This Week's Links We Like. Tips, Hints And Fun Stuff
+</ul>
+<li>BOOK OF THE WEEK
+<ul type='square'>
+<li>Windows XP Power Tools
+</ul>
+</ol>
+</font>
+</td>
+</tr>
+<tr>
+<td>&nbsp;</td>
+</tr>
+<tr>
+<td background='http://www.winxpnews.com/graphics/h_border.gif' align=left>
+<img src='http://www.winxpnews.com/graphics/winxpnews_logo_50.jpg' width='53' height='24' align='right'>
+<font face='verdana, sans-serif' size='4' color='#ffffff'>
+&nbsp;&nbsp;SPONSOR: iHateSpam - Eliminate Irritating Junk Email<br>
+</font>
+</td>
+</tr>
+<tr>
+<td><font size='1'>&nbsp;</td>
+</tr>
+<tr>
+<td>
+<a href="http://www.winxpnews.com/rd/rd.cfm?id=020709S1-iHateSpam&mid=9665862091709486" target="_top">
+<img src='http://www.winxpnews.com/ads/ihs.gif' align='right' border='0'>
+</a>
+<font face='verdana, sans-serif' size=2>
+Irritated with porn, bogus business offers and viagra ads in your mailbox?<br>
+Angry about losing your valuable time deleting all that junk? Need a spam-<br>
+blocker that eliminates this annoying spam? Stop the spam in your inbox<br>
+with iHateSpam. It gives you control over the ever increasing flood of <br>
+junk email. Runs under Windows 95/98/ME/NT/2000/XP. Best of all, the limited<br>
+time Intro Offer is just $19.95 with online delivery of full product and a <br>
+30-day money back guarantee. This is a real no-brainer. <b>Get Your Copy Now!</b><br>
+<b>Visit <a href="http://www.winxpnews.com/rd/rd.cfm?id=020709S1-iHateSpam&mid=9665862091709486" target="_top">iHateSpam - Eliminate Irritating Junk Email</a> for more information.</b>
+</font>
+</td>
+</tr>
+<tr>
+<td>&nbsp;</td>
+</tr>
+<tr>
+<td background='http://www.winxpnews.com/graphics/h_border.gif' align=left>
+<img src='http://www.winxpnews.com/graphics/winxpnews_logo_50.jpg' width='53' height='24' align='right'>
+<font face='verdana, sans-serif' size='4' color='#ffffff'>
+&nbsp;&nbsp;EDITOR'S CORNER<br>
+</font>
+</td>
+</tr>
+<tr>
+<td><font size='1'>&nbsp;</td>
+</tr>
+<tr>
+<td>
+<font face='verdana, sans-serif' size='2'>
+<p><font size=3><b>How to Publish Your Windows XP FTP Server to the Internet</b></font><p>
+Several of you wrote in about last week's article on installing an FTP Server. You said "that was great, but you only told half the story". You wanted to know two more things:
+<ol>
+<li>How to make the FTP Server available to Internet users
+<li>How to secure the FTP Server
+</ol>
+There are several ways to make an FTP server on the internal network available to users on the Internet. These methods are referred to as "Server Publishing". You can use a Windows XP computer running Internet Connection Services (ICS) to publish a server on your internal network.
+<p>
+Let's take a look at a common scenario. You have a Windows XP computer connected to the Internet with an always-on cable or DSL connection. You have another computer on your private network also running Windows XP. You've installed the FTP Server on this internal network computer and put files into the FTP folder. Now you want Internet users to connect to the FTP Server through the ICS computer directly connected to the Internet.
+<p>
+You can do this with the Windows XP ICS! Here's how:
+<ol>
+<li>Go into the Network Connections window. You can get there from the Network applet in the Control Panel.
+<li>Right click the network interface directly connected to the Internet and click Properties.
+<li>Click on the Advanced tab in the connection's Properties dialog box. Put a checkmark in the Internet Connection Firewall checkbox. Always make sure the Internet Connection Firewall (ICF) is enabled when you connect a computer directly to the Internet.
+<li>Click the Settings button, then click on the Services tab in the Advanced Settings dialog box.
+<li>Now click the Add button. This brings up the Service Settings dialog box. Type in My FTP Server in the Description of service text box. In the Name or IP address text box, type in the IP address of the computer on your private network that's running the FTP server. Since you're using ICS, it'll have an IP address like 192.168.0.x, where x is different for each machine on your network. You might want to manually assign the IP address the FTP Server already has, so that it doesn't change in the future. You can find out what IP address your FTP server is using by opening a command prompt at the FTP server and typing in the command ipconfig. That will give you the IP address the FTP Server is using. Back to the Service Settings dialog box, select the TCP option button. For the External Port and the Internet port, put in the port number you assigned to the FTP server on your internal network. Read this week's How To section to see how to change the listening port number. Clic!
+k OK
+<li>Click OK, and then click OK one more time! You might need to disable and enable the adapter after making the change. You can do that by right clicking the always-on interface.
+</ol>
+The procedure is very similar for dial-up connections. However, there are problems with dial-up connections (and many always-on connections) because the IP address on the external interface of the ICS computer changes over time. Next week I'll share with you a cool way you can get around this problem by using something called a "dynamic DNS service". I've used one for years, and it works great. Make sure to tune in next week for the details.
+<p>
+There you have it. Is server publishing in your future? Have any questions on the method I described above? If so, let me know! There are lots of ways you can publish services. Tell me how you do it, and tricks you've learned along the way. If you're having problems with server publishing, let me know about those too! I'll be sure to include what I learn from you in upcoming newsletters.
+<p>
+Until next week,<br>
+Tom Shinder, Editor<br>
+(email us with feedback: <a href="mailto:feedback@winxpnews.com?subject=WinXPnews Issue #33">feedback@winxpnews.com</a>)
+</font>
+</td>
+</tr>
+<tr>
+<td>&nbsp;</td>
+</tr>
+<tr>
+<td background='http://www.winxpnews.com/graphics/h_border.gif' align=left>
+<img src='http://www.winxpnews.com/graphics/winxpnews_logo_50.jpg' width='53' height='24' align='right'>
+<font face='verdana, sans-serif' size='4' color='#ffffff'>
+&nbsp;&nbsp;SPONSOR: Is Your PC Spying On You?<br>
+</font>
+</td>
+</tr>
+<tr>
+<td><font size='1'>&nbsp;</td>
+</tr>
+<tr>
+<td>
+<a href="http://www.winxpnews.com/rd/rd.cfm?id=020709S2-PestPatrol&mid=9665862091709486" target="_top">
+<img src='http://www.winxpnews.com/ads/pestpatrol.gif' align='right' border='0'>
+</a>
+<font face='verdana, sans-serif' size=2>
+You are surfing the Web. Check out sites, download some music or<br>
+software that might be cool. Guess what? Your PC might have picked up<br>
+a cyber transmitted disease (CTD). These pests might now be monitoring <br>
+what you are doing and report this back to their "black hat" owners <br>
+and reveal your personal information. PestPatrol kills 'em all off. <br>
+Get your copy on the online shop for just 30 bucks with immediate online<br> delivery. Protect your PC and your confidential data!<br>
+<b>Visit <a href="http://www.winxpnews.com/rd/rd.cfm?id=020709S2-PestPatrol&mid=9665862091709486" target="_top">Is Your PC Spying On You?</a> for more information.</b>
+</font>
+</td>
+</tr>
+<tr>
+<td>&nbsp;</td>
+</tr>
+<tr>
+<td background='http://www.winxpnews.com/graphics/h_border.gif' align=left>
+<img src='http://www.winxpnews.com/graphics/winxpnews_logo_50.jpg' width='53' height='24' align='right'>
+<font face='verdana, sans-serif' size='4' color='#ffffff'>
+&nbsp;&nbsp;HINTS, TIPS, TRICKS & TWEAKS<br>
+</font>
+</td>
+</tr>
+<tr>
+<td><font size='1'>&nbsp;</td>
+</tr>
+<tr>
+<td>
+<font face='verdana, sans-serif' size='2'>
+<p><font size=3><b>Allow Dial-up Connections to Synchronize Time with Internet Time Servers</b></font><p>
+Do you use a dial-up connection but can't get your machine to synchronize its clock with an Internet time server when the Internet Connection Firewall (ICF) is enabled? If so, here's a tip Richard Surry sent in on how to fix the problem:
+<ol>
+<li>Open your Network Connections window from the start menu.
+<li>Right click on your modem (or other dial-up connection) and click Properties.
+<li>Click on the Advanced tab. You already have a checkmark in the box that enables the ICF. Click on the Settings button.
+<li>Click on the Services tab, then click on the Add button in the Services tab.
+<li>That should open the Service Settings dialog box. In the Description box, put in Internet Time Service. For the Name or IP address of the computer hosting this service on your network, type in 127.0.0.1. Select the TCP  protocol option button. For both the external and internal port numbers, type 123.
+<li>If you're online, disconnect and reconnect. Now synchronize the time by double click on the clock in the system tray and going to the Internet Time tab.
+</ol>
+This is an interesting tip, and it represents an even more interesting problem. For you network geeks out there, I'll ask you this question: Why should we allow unsolicited inbound connections for the Internet Time Service? The ICF should not block responses to solicited outbound connections, so why should we have to enable reverse NAT to make this work?
+</font>
+</td>
+</tr>
+<tr>
+<td>&nbsp;</td>
+</tr>
+<tr>
+<td background='http://www.winxpnews.com/graphics/h_border.gif' align=left>
+<img src='http://www.winxpnews.com/graphics/winxpnews_logo_50.jpg' width='53' height='24' align='right'>
+<font face='verdana, sans-serif' size='4' color='#ffffff'>
+&nbsp;&nbsp;HOW TO'S: ALL THE NEW XP FEATURES<br>
+</font>
+</td>
+</tr>
+<tr>
+<td><font size='1'>&nbsp;</td>
+</tr>
+<tr>
+<td>
+<font face='verdana, sans-serif' size='2'>
+<p><font size=3><b>How to Secure an FTP Server on Windows XP Professional</b></font><p>
+Last week we went over how to install the Windows XP FTP Server. It will work fine after going through the steps outlined last week, but several of you asked for more information on how to secure the FTP Server because you wanted to connect it to the Internet. It's a very good idea to understand how FTP security works before putting the server on the Internet. Here are some suggestions:
+<ol>
+<li>Open the Internet Information Services console from the Administrative Tools menu. In the left pane of the console, expand your server name and then expand the FTP Sites node.
+<li>Right click on the Default FTP Site and click the Properties command.
+<li>Click on the FTP Site tab. Notice that the default TCP Port is set to 21. This is the well-known port for FTP. You can increase security a bit by changing this port to another value that's in the 1026-65534 range. This secures it from poorly motivated click-kiddies and also allows you to get around your ISP blocking incoming connections to TCP port 21. Friends who connect to your FTP server will need to change the port number on their FTP client software as well.
+<li>The Windows XP FTP server has a hard coded limit of 10 simultaneous connections. You might want to change this to a lower number to reduce the chance of a LAN party on the external interface of the FTP server.
+<li>Put a checkmark in the Enable Logging checkbox. Click the Properties button to the right of the log format drop-down list box. Click the Daily option button on the General Properties tab. On the Extended Properties tab, select all of the Extended Properties. Click OK.
+<li>Click on the Security Accounts tab. Place a checkmark in the Allow only anonymous connections checkbox. This prevents users from sending username and password credentials to the FTP server. You don't want users to send credentials because those credentials are sent in "clear text", which can be read by anyone who's listening on the wire.
+<li>Click the Messages tab. Enter a Welcome message, an Exit message, and a message users will see if there are no available connections.
+<li>Click on the Home Directory tab. Make sure there is a checkmark in the Read and Log Visits checkboxes. REMOVE the checkmark in the Write checkbox. Note the location in the Local Path text box. Navigate to that path in the Windows Explorer.
+<li>Right click on the FTPROOT folder and click Properties.
+<li>Click on the Security tab. Make sure that SYSTEM has Full Control. Assign the IUSR_<computername> account READ access only. Remove all other permissions for the IUSR account. Make sure you give Adminstrators Full Control tool. This allows you, the administrator on the FTP Server computer, to add, remove and change files in the FTPROOT folder.
+</ol>
+Stop and restart the FTP Server. Now your FTP server is secure and Internet bad guys won't be able to use it to distribute porno and bootlegged software.
+</font>
+</td>
+</tr>
+<tr>
+<td>&nbsp;</td>
+</tr>
+<tr>
+<td background='http://www.winxpnews.com/graphics/h_border.gif' align=left>
+<img src='http://www.winxpnews.com/graphics/winxpnews_logo_50.jpg' width='53' height='24' align='right'>
+<font face='verdana, sans-serif' size='4' color='#ffffff'>
+&nbsp;&nbsp;WINXP SECURITY: UPDATES & PATCHES<br>
+</font>
+</td>
+</tr>
+<tr>
+<td><font size='1'>&nbsp;</td>
+</tr>
+<tr>
+<td>
+<font face='verdana, sans-serif' size='2'>
+<p><font size=3><b>Cumulative Patch for Windows Media Player</b></font><p>
+I think it was a couple months ago when I wrote about some serious problems with the Windows Media Player (WMP). At that time you could download a "cumulative" patch that would update the Media Player with the latest security fixes. Well, it's time to download another "cumulative" patch! A couple other problems were found in WMP that could cause some problems. To read more about the problem head on over to:<bR>
+<a href="http://www.winxpnews.com/rd/rd.cfm?id=020709SE-WMP_Patch&mid=9665862091709486" target="_top">http://www.winxpnews.com/rd/rd.cfm?id=020709SE-WMP_Patch</a>
+<p>
+You'll also find the download locations for Windows Media Player versions 6.4, 7.1 and 8.0 (XP) on that page.
+<p><font size=3><b>Cumulative Patches for Excel and Word for Windows</b></font><p>
+If you run Microsoft Word or Excel, versions 2000 or 2002 (XP), then you need to head on over to the Microsoft site to download some security fixes. These fixes handle security glitches that could get you in trouble if you don't take care of them! Head on over to Microsoft's site where you can find individual fixes for each program. You only need download the fix that applies to your computer:<br>
+<a href="http://www.winxpnews.com/rd/rd.cfm?id=020709SE-Word_Excel_Patch&mid=9665862091709486" target="_top">http://www.winxpnews.com/rd/rd.cfm?id=020709SE-Word_Excel_Patch</a>
+</font>
+</td>
+</tr>
+<tr>
+<td>&nbsp;</td>
+</tr>
+<tr>
+<td background='http://www.winxpnews.com/graphics/h_border.gif' align=left>
+<img src='http://www.winxpnews.com/graphics/winxpnews_logo_50.jpg' width='53' height='24' align='right'>
+<font face='verdana, sans-serif' size='4' color='#ffffff'>
+&nbsp;&nbsp;UPGRADING & COMPATIBILITY ISSUES<br>
+</font>
+</td>
+</tr>
+<tr>
+<td><font size='1'>&nbsp;</td>
+</tr>
+<tr>
+<td>
+<font face='verdana, sans-serif' size='2'>
+<p><font size=3><b>A Computer May Hang During a Heavy Load with an Ericsson HIS Modem</b></font><p>
+If your computer has a Ericsson HIS modem, you might experience a dreaded blue screen and see the message IRQL_NOT_LESS_OR_EQUAL or DRIVER_CORRUPTED_EXPOOL. The problem is that you're downloading too much and your poor modem can't keep up! Microsoft recognizes that this isn't a problem with the modem, but with the modem driver. To download a fix visit Microsoft's site. After getting the fix, you can download as much as you like without worrying about blue screens!<br>
+<a href="http://www.winxpnews.com/rd/rd.cfm?id=020709UP-HIS_Modem&mid=9665862091709486" target="_top">http://www.winxpnews.com/rd/rd.cfm?id=020709UP-HIS_Modem</a>
+<p><font size=3><b>Knowledge Base Search Center - If it is Not Broke, Do Not Break it!</b></font><p>
+It wasn't so long ago when you could search the Microsoft Knowledge Base for articles that came up in the last 3 days, 7 days, 14 days, 30 days, 90 days and 6 months. It was great! But Microsoft decided to "fix" the Knowledge Base search page, and now it really sucks! It's hard to find things that used to come up easily, the site is often down, and searching based on age of articles just doesn't work anymore.
+<p>
+Try this: go to:<br>
+<a href="http://support.microsoft.com/default.aspx?ln=EN-US&pr=kbinfo&" target="_top">http://support.microsoft.com/default.aspx?ln=EN-US&pr=kbinfo&</a><br>
+and on the left side of the page select Windows XP in the top drop down list box. Don't put anything in the For solutions containing...(optional) text box. Leave the Any of the words entered option selected in the Using drop down list box. For Maximum Age select 3 days. For Results Limit select 150 articles. Click Search Now. Whoa! Nothing. OK, it's reasonable to see no articles related to Windows XP in the last 3 days. Try again, this time using 7 days. Whaat? Still no articles. OK, it was a holiday week in the USA last week. Let's try 14 days. Nothing again! That seems sort of strange, doesn't it? Let's give it another try with 30 days. Still no articles! What's going on here? Keep trying for 6 months and one year. You still won't find anything. It's pretty sad, because this used to work.
+</font>
+</td>
+</tr>
+<tr>
+<td>&nbsp;</td>
+</tr>
+<tr>
+<td background='http://www.winxpnews.com/graphics/h_border.gif' align=left>
+<img src='http://www.winxpnews.com/graphics/winxpnews_logo_50.jpg' width='53' height='24' align='right'>
+<font face='verdana, sans-serif' size='4' color='#ffffff'>
+&nbsp;&nbsp;WINXP CONFIGURING & TROUBLESHOOTING<br>
+</font>
+</td>
+</tr>
+<tr>
+<td><font size='1'>&nbsp;</td>
+</tr>
+<tr>
+<td>
+<font face='verdana, sans-serif' size='2'>
+<p><font size=3><b>A Description of the Repair Option on a Local Area Network or High-Speed Internet Connection</b></font><p>
+Here's the answer to a question I've had for a long time. What the heck does that "Repair" option for a network connection actually do? It's not in the help file, but it's on the Microsoft Web site. Here's what it does:
+<ul>
+<li>Sends an ipconfig /renew
+<li>Flushes the ARP cache with a arp -d
+<li>Reloads the NetBIOS name cache with a nbtstat -R
+<li>Updates its WINS server with an nbtstat -RR
+<li>Clear out the DNS client cache with an ipconfig /flushdns
+<li>Reregisters the client with a DDNS server with a ipconfig /registerdns
+</ul>
+Check out the original article over at:<br>
+<a href="http://www.winxpnews.com/rd/rd.cfm?id=020709CO-Repair_Option&mid=9665862091709486" target="_top">http://www.winxpnews.com/rd/rd.cfm?id=020709CO-Repair_Option</a>
+<p><font size=3><b>Keyboard and Mouse Do Not Work When You Start Windows</b></font><p>
+Have you been hit with this one? You're working in Windows XP and shut down for the day. The next morning you start up your Windows XP computer and the mouse pointer is stuck! The only way to get it going again is to restart the computer, and for some reason the pointer starts moving again. What's up with that? I still haven't figured that one out, but Microsoft has a KB article that claims it's from a corrupt registry. I doubt that's the case in my situation because the problem is intermittent. But if you find that your mouse is always stuck, you might want to check out:<br>
+<a href="http://www.winxpnews.com/rd/rd.cfm?id=020709CO-Frozen_Mouse&mid=9665862091709486" target="_top">http://www.winxpnews.com/rd/rd.cfm?id=020709CO-Frozen_Mouse</a>
+<p><font size=3><b>How to Deploy Windows XP Images from Windows 2000 RIS Servers</b></font><p>
+Are you planning to roll out lots of Windows XP computers on your network in the near future? If so, you're probably looking for a good way to automate the process. You can use the Windows 2000 Remote Installation Services (RIS) if you're running Windows 2000 Servers on your network. For the basic procedure and some tips, tricks, and gotcha's, check out:<br>
+<a href="http://www.winxpnews.com/rd/rd.cfm?id=020709CO-Deploy_XP_Images&mid=9665862091709486" target="_top">http://www.winxpnews.com/rd/rd.cfm?id=020709CO-Deploy_XP_Images</a>
+</font>
+</td>
+</tr>
+<tr>
+<td>&nbsp;</td>
+</tr>
+<tr>
+<td background='http://www.winxpnews.com/graphics/h_border.gif' align=left>
+<img src='http://www.winxpnews.com/graphics/winxpnews_logo_50.jpg' width='53' height='24' align='right'>
+<font face='verdana, sans-serif' size='4' color='#ffffff'>
+&nbsp;&nbsp;FAVE LINKS<br>
+</font>
+</td>
+</tr>
+<tr>
+<td><font size='1'>&nbsp;</td>
+</tr>
+<tr>
+<td>
+<font face='verdana, sans-serif' size='2'>
+<p><font size=3><b>This Week's Links We Like. Tips, Hints And Fun Stuff</b></font><p><li>Be Afraid, be very afraid - the future of Big Brother in computing</li><br>
+<a href="http://www.winxpnews.com/rd/rd.cfm?id=020709FA-Palladium_FAQ&mid=9665862091709486" target="_top">http://www.winxpnews.com/rd/rd.cfm?id=020709FA-Palladium_FAQ</a>
+<li>Get Revenge on your computer!</li><br>
+<a href="http://www.winxpnews.com/rd/rd.cfm?id=020709FA-PC_Revenge&mid=9665862091709486" target="_top">http://www.winxpnews.com/rd/rd.cfm?id=020709FA-PC_Revenge</a>
+<li>Pringles Super Spud Boxing</li><br>
+<a href="http://www.winxpnews.com/rd/rd.cfm?id=020709FA-Spud_Boxing&mid=9665862091709486" target="_top">http://www.winxpnews.com/rd/rd.cfm?id=020709FA-Spud_Boxing</a>
+</font>
+</td>
+</tr>
+<tr>
+<td>&nbsp;</td>
+</tr>
+<tr>
+<td background='http://www.winxpnews.com/graphics/h_border.gif' align=left>
+<img src='http://www.winxpnews.com/graphics/winxpnews_logo_50.jpg' width='53' height='24' align='right'>
+<font face='verdana, sans-serif' size='4' color='#ffffff'>
+&nbsp;&nbsp;BOOK OF THE WEEK<br>
+</font>
+</td>
+</tr>
+<tr>
+<td><font size='1'>&nbsp;</td>
+</tr>
+<tr>
+<td>
+<font face='verdana, sans-serif' size='2'>
+<p><font size=3><b>Windows XP Power Tools</b></font><p>
+A book full of personal experiences and anecdotes that will equip you with the tips and tricks you need to become an XP afficionado. Coverage includes automating tasks using scripting, the Command Console Survivor Guide, networking, registry, maximizing security/firewalls, hardware, installation/configuration, and database hosting/accessing. The CD contains the best third party utilities around.
+<p>
+Step-by-Step Instruction Helps You Harness the Full Power of Windows XP. Whether you're running Windows XP Home Edition or Professional, Windows XP Power Tools arms you with the advanced skills you need to become the ultimate power user. Full of undocumented tips and tricks and written by a Windows expert, this book provides you with step-by-step instructions for customization, optimization, troubleshooting and shortcuts for working more efficiently. A must-have for power users and network administrators, Windows XP Power Tools includes a CD filled with power tools including security, e-mail, diagnostic and data recovery utilities.
+<p>
+<a href="http://www.winxpnews.com/rd/rd.cfm?id=020709BW-XP_Power_Tools&mid=9665862091709486" target="_top">http://www.winxpnews.com/rd/rd.cfm?id=020709BW-XP_Power_Tools</a>
+</font>
+</td>
+</tr>
+</table>
+<table width="100%" border="0">
+<tr>
+<td>&nbsp;</td>
+</tr>
+<tr>
+<td background='http://www.winxpnews.com/graphics/h_border.gif' align=left>
+<img src='http://www.winxpnews.com/graphics/winxpnews_logo_50.jpg' width='53' height='24' align='right'>
+<font face='verdana, sans-serif' size='4' color='#ffffff'>
+&nbsp;&nbsp;ABOUT WINXPNEWS&#153;<br>
+</font>
+</td>
+</tr>
+<tr>
+<td><font size='1'>&nbsp;</td>
+</tr>
+<tr>
+<td><font size='1'>&nbsp;</font><br>
+<font face='verdana, sans-serif' size=3><b>
+What Our Lawyers Make Us Say</b></font>
+</td>
+</tr>
+<tr>
+<td>
+<font size="1" face="arial">
+These documents are provided for informational purposes only. The information
+contained in this document represents the current view of Sunbelt Software
+Distribution on the issues discussed as of the date of publication. Because
+Sunbelt must respond to changes in market conditions, it should not be
+interpreted to be a commitment on the part of Sunbelt and Sunbelt cannot
+guarantee the accuracy of any information presented after the date of
+publication.
+<p>
+INFORMATION PROVIDED IN THIS DOCUMENT IS PROVIDED "AS IS" WITHOUT WARRANTY OF
+ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED
+WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND FREEDOM
+FROM INFRINGEMENT.
+<p>
+The user assumes the entire risk as to the accuracy and the use of this
+<code2=fsg.com>document. This document may be copied and distributed subject to the
+following conditions: 1) All text must be copied without modification and all pages
+must be included; 2) All copies must contain Sunbelt's copyright notice and any
+other notices provided therein; and 3) This document may not be distributed
+for profit. All trademarks acknowledged. Copyright Sunbelt Software
+Distribution, Inc. 1996-2002.
+</font>
+</td>
+</tr>
+<tr>
+<td><font size='1'>&nbsp;</font><br>
+<font face='verdana, sans-serif' size=3><b>
+About Your Subscription to WinXPnews&#153;</b></font>
+</td>
+</tr>
+<tr>
+<td>
+<font size="2" face="arial, verdana, sans-serif">
+This is a posting from WinXPnews. You are subscribed as zzzz@zzzzzzzz.com
+<p>
+To manage your profile, please visit our site by clicking on the following link:<br>
+<a href="http://www.winxpnews.com/login.cfm?id=9665862091709486">
+http://www.winxpnews.com/login.cfm?id=9665862091709486</a><br>
+For a quick unsubscribe (gasp!), click here:<br>
+<a href="http://www.winxpnews.com/unsubscribe.cfm?email=zzzz@zzzzzzzz.com">
+http://www.winxpnews.com/unsubscribe.cfm?email=zzzz@zzzzzzzz.com</a>
+</font>
+</td>
+</tr>
+</table>
+</body>
+</html>
+
diff --git a/upstream/t/data/welcomelists/yahoo-inc.com b/upstream/t/data/welcomelists/yahoo-inc.com
new file mode 100644 (file)
index 0000000..7241b6d
--- /dev/null
@@ -0,0 +1,20 @@
+Return-Path: <yahoo-dev-null@yahoo-inc.com>
+Delivered-To: zzzzz@xyz.org
+Received: (qmail 8790 invoked by uid 505); 29 Jul 2002 03:28:42 -0000
+Received: from yahoo-dev-null@yahoo-inc.com by blazing.xyz.org by uid 502 with qmail-scanner-1.12 (F-PROT: 3.12. Clear:. Processed in 0.195404 secs); 29 Jul 2002 03:28:42 -0000
+Received: from e5.member.yahoo.com (216.136.131.107)
+  by dsl092-072-xyz.bos1.dsl.speakeasy.net with SMTP; 29 Jul 2002 03:28:42 -0000
+Received: (from yahoo@localhost)
+       by e5.member.yahoo.com (8.11.3/8.11.3) id g6T3PIh88736;
+       Sun, 28 Jul 2002 20:25:18 -0700 (PDT)
+       (envelope-from yahoo-dev-null@yahoo-inc.com)
+Date: Sun, 28 Jul 2002 20:25:18 -0700 (PDT)
+Message-Id: <200207290325.g6T3PIh88736@e5.member.yahoo.com>
+X-Authentication-Warning: e5.member.yahoo.com: yahoo set sender to <yahoo-dev-null@yahoo-inc.com> using -f
+From: Yahoo! Member Services <my-login-request@yahoo-inc.com>
+Errors-To: yahoo-dev-null@yahoo-inc.com
+To: zzzzz@xyz.org
+Subject: Yahoo! Email Verification
+
+[email from Yahoo!]
+
diff --git a/upstream/t/data/whitelists/action.eff.org b/upstream/t/data/whitelists/action.eff.org
deleted file mode 100644 (file)
index a7bb9d2..0000000
+++ /dev/null
@@ -1,586 +0,0 @@
-From alerts@action.eff.org  Mon Aug 12 10:54:52 2002
-Return-Path: <alerts@action.eff.org>
-Delivered-To: jm@localhost.netnoteinc.com
-Received: from localhost (localhost [127.0.0.1])
-       by phobos.labs.netnoteinc.com (Postfix) with ESMTP id 265A944100
-       for <jm@localhost>; Mon, 12 Aug 2002 05:52:11 -0400 (EDT)
-Received: from phobos [127.0.0.1]
-       by localhost with IMAP (fetchmail-5.9.0)
-       for jm@localhost (single-drop); Mon, 12 Aug 2002 10:52:11 +0100 (IST)
-Received: from eug-app01.ctsg.com (firewall2.ctsg.com [216.210.226.98]) by
-    dogma.slashnull.org (8.11.6/8.11.6) with ESMTP id g7A6FDb11520 for
-    <aaaaaa@yyyyyy.zzz>; Sat, 10 Aug 2002 07:15:13 +0100
-Message-Id: <1418893.1028960179734.JavaMail.IWAM_EUG-APP01@eug-app01>
-Date: Fri, 9 Aug 2002 23:16:19 -0700 (PDT)
-From: Effector List <alerts@action.eff.org>
-To: xxxxxxxx@xxxxx.xxx
-Subject: EFFector 15.24: EFF Submits Comments to FCC, Johansen Trial
-    Schedule Update
-MIME-Version: 1.0
-Content-Type: text/plain
-Content-Transfer-Encoding: 7bit
-
-EFFector       Vol. 15, No. 24       August 9, 2002      ren@eff.org
-
-A Publication of the Electronic Frontier Foundation      ISSN 1062-9424
-
-
-In the 224th Issue of EFFector:
-
-* EFF Submits Letter to FCC Chairman Regarding BPDG Proposal
-* Update on Intel Corp. v. Hamidi
-* DeCSS Author Johansen's Trial Rescheduled
-* Bunnie Presents Paper on XBox Reverse Engineering
-* Thanks to DefCon!
-* EFF Booth at LinuxWorld
-* Deep Links: Baen Books' Releases Reader-Friendly E-Books
-* Deep Links: Janis Ian on P2P
-* Deep Links: Hometown Paper Discusses Rep. Coble's Support of 
-       Berman P2P Hacking Bill
-* Administrivia
-
-
-For more information on EFF activities & alerts: http://www.eff.org/
-
-To join EFF or make an additional donation:
-http://www.eff.org/support/ 
-
-EFF is a member-supported nonprofit.
-Please sign up as a member today!
---------------------------------------------------------------------
-
-* EFF Submits Letter to FCC Chairman Regarding BPDG Proposal
-
-The Honorable Michael K. Powell Chairman Federal Communications
-Commission 445 12th Street, S.W. Suite 8C453 Washington, DC 20554
-
-
-BY FACSIMILE, ELECTRONIC MAIL, AND POSTAL MAIL
-
-Dear Chairman Powell:
-
-I am writing to you today in regards to the digital television
-Broadcast Flag; specifically, I write in response to Sen. Hollings'
-and Representatives Dingell and Tauzin's letters of July 19, which
-urged you to mandate the Broadcast Flag proposal outlined in the
-final report of the Broadcast Protection Discussion Group.
-
-The Electronic Frontier Foundation (EFF) is a donor-supported
-non-profit organization that works to uphold civil liberties
-interests in technology policy and law. EFF has played a critical
-role in safeguarding crucial freedoms related to computers, the
-Internet and consumer electronics devices, defeating the restriction
-on strong cryptography exports; securing the legal principle that
-Internet wiretaps must only proceed in conjunction with a warrant;
-and defending academics, researchers and commercial interests
-against DMCA-related prosecution.
-
-EFF was an active participant in the Broadcast Protection Discussion
-Group. We attended the group's meetings and conference calls and
-participated in the group's policy and technical mailing-lists. EFF
-also maintains a web-site that was and is the only public source of
-information on the Broadcast Flag negotiations and proposal. The
-site can be found at http://bpdg.blogs.eff.org. EFF devoted
-thousands of staff-hours to publicizing the existence and nature of
-the BPDG to the public, to civil liberties and consumer-advocacy
-groups, and to entrepreneurial companies and software authors whose
-products were threatened by the proceedings.
-
-When you and I met at Esther Dyson's PC Forum last March, we spoke
-briefly about the civil liberties interests that would be undermined
-by the Broadcast Protection Discussion Group's mandate. The BPDG
-proposal will have grave consequences for innovation, free
-expression, competition and consumer interests. Worst of all, it
-will add unnecessary complexity and expense to the DTV transition,
-compromising DTV adoption itself.
-
-As you are aware, technologists have traditionally manufactured
-those devices they believed would be successful in the market, often
-in spite of the misgivings of rights-holders. From the piano roll to
-the PVR, technologists have enjoyed the freedom to ship whatever
-products they believe the public will pay for; what's more,
-innovation has always thrived best where there were the fewest
-regulatory hurdles. NTSC tuners and devices are governed by precious
-few regulations, and consequently we see a rich field of products
-that interact with them, from the VCR Plus to tuner-cards for PCs to
-the PVR. The Broadcast Flag proposal would limit technologists to
-shipping those products that met with the approval of MPAA member
-companies. No entrepreneur or software author will know, a priori,
-whether his innovative DTV product will be legal in the market until
-he has gone to the expense of building it and taking it around to
-the Hollywood studios for review.
-
-Consumers and industry alike have benefitted greatly from the "Open
-Source" or "Free Software" movement, in which technologies are
-distributed in a form that encourages end-user modification. From
-server-software like the web-wide success-story apache, to operating
-systems like GNU/Linux, to consumer applications like the Mozilla
-browser, Free Software is a powerful force for innovation, consumer
-benefit and commercial activity. The BPDG proposal implicitly bans
-Free Software DTV applications -- such as the DScaler de-interlacer
-and the GNU Radio software-defined radio program -- as these
-applications are built to be modified by end-users, something that
-is banned under the BPDG proposal. The tamper-resistance component
-of the BPDG's "Robustness Requirements" will create and entire class
-of illegal software applications, abridging the traditional First
-Amendment freedom enjoyed by software authors who create expressive
-speech in code form under one of several Free Software/Open Source
-licenses.
-
-The BPDG nominally set out to create an objective standard, a bright
-line that technologists could hew to in order to avoid liability
-when deploying their products. However, the end product of the BPDG
-was a "standard" that contained no objective criteria for legal
-technology; rather, the standard required that new technologies be
-approved by MPAA member companies. Not uncoincidentally, the only
-technologies that were approved by the MPAA -- and hence the only
-legal technologies -- were those produced by the 4C and 5C
-consortia, a group of technology companies that acted as the MPAA's
-allies throughout the BPDG process. This is an harbinger of the sort
-of regime that the BPDG standard will usher in: technology companies
-will be able to shut their competitors out of the marketplace by
-allying themselves with Hollywood, brokering deals to allow certain
-technologies and outlaw others.
-
-The marketplace is a proven mechanism for rapidly and efficiently
-producing products that increase the value and desirability of new
-technologies, such as DTV. A BPDG mandate would subvert the market
-for DTV innovation. Competing companies with lower-cost DTV
-technology alternatives would be restrained from bringing these to
-market if they failed to assuage the MPAA's concerns about
-unauthorized redistribution. Furthermore, the universe of
-unauthorized-but-lawful uses for DTV programming will be shrunk down
-to the much smaller space of explicitly authorized uses. The ability
-of the public to make unauthorized-but-lawful uses of television
-programming has been an historical force for increasing the value of
-broadcast programming, from the VCR to the PVR.
-
-Ironically, the inevitable damage that a Broadcast Flag mandate
-would do to innovation, competition and consumer interests can only
-slow down DTV adoption, by driving up the cost of DTV devices while
-reducing the number of desirable features that an open market would
-create. If the public is offered less functionality for more money,
-they will not flock to DTV.
-
-The most disheartening thing about the Broadcast Flag is that there
-is neither a strong case that the Broadcast Flag is a necessary tool
-for protecting copyright, nor that the Broadcast Flag would be
-effective in that role. The existing practice of Internet
-infringement of broadcast programming -- analog captures from
-devices that satisfy the requirements of the BPDG proposal -- would
-not be stopped by the presence of a Broadcast Flag.
-Higher-resolution DTV signals will likewise present no challenge to
-determined infringers, who can capture full-quality analog signal
-from DTV devices and then re-digitize them, suffering only a single
-generation's worth of loss-of-quality before the programming enters
-the Internet.
-
-Meanwhile, the underlying rubric for a Broadcast Flag -- that
-infringement will undermine Hollywood's business to the point that
-movies will no longer be available to the public, reducing the value
-of DTV -- is no more than superstition. No credible study or
-analysis, undertaken by a neutral party, has ever been presented to
-Congress, the FCC, the CPTWG or the BPDG supporting this notion. The
-public is being asked to sacrifice its rights in copyright; industry
-is being asked to place its right to innovation in the hands of
-entertainers; the US government is being asked to mandate
-extraordinary, unprecedented regulation of the $600 billion
-technology sector -- all on the uncorroborated opinions of a few
-studio executives.
-
-EFF welcomes the FCC's oversight of the Broadcast Flag issue. The
-BPDG proceedings took place behind a shroud of secrecy, in a
-looking-glass "public process" where only those participants the
-organizers wanted to hear from were made privy to its existence,
-where the co-chairs invented rules and processes on the fly to suit
-the needs of the entertainment interests and the technology
-companies that had privately secured a promise of a legal monopoly
-for their products, where the press was banned.
-
-The FCC has an admirable tradition of seeking and weighing public
-opinion in its proceedings. As the FCC considers the Broadcast Flag,
-EFF hopes that it will start anew, setting aside the findings of the
-BPDG in light of the concerns raised by Microsoft, Philips, Sharp,
-Thomson, and Zenith, as well as non-profit organizations including
-EFF, Consumers Union, Consumer Federation of America, the Free
-Software Foundation, Public Knowledge, digitalconsumer.org, the
-Center for Democracy in Technology, and the Computer and
-Communications Industry Association.
-
-Thank you for attention in this matter. Please let me know if we can
-be of any further assistance to you.
-
-Sincerely yours,
-
-Cory Doctorow for the Electronic Frontier Foundation
-
-
-
-Links: 
-
-EFF's BPDG Blog: 
-http://bpdg.blogs.eff.org 
-
-An overview of our concerns with the broadcast flag:
-http://bpdg.blogs.eff.org/archives/one-page.pdf 
-
-Letter from Sen. Hollings: 
-http://bpdg.blogs.eff.org/archives/000155.html 
-
-Letter from Rep. Tauzin: 
-http://bpdg.blogs.eff.org/archives/000156.html
-
-
---------------------------------------------------------------------
-
-* Update on Intel Corp. v. Hamidi
-
-Intel Corp. v. Hamidi is now on appeal to the California Supreme
-Court. EFF filed an amicus brief in support of Ken Hamidi on Aug. 6,
-2002. The facts are simple: Over about two years, Hamidi on six
-occasions sent e-mail critical of Intel's employment practices to
-between 8,000 and 35,000 Intel employees. Intel demanded that Hamidi
-stop, but he refused. Intel obtained an injunction barring Hamidi
-from e-mailing Intel employees at their Intel e-mail addresses,
-based on the common-law tort of "trespass to chattels." ("Chattel"
-is a legal term that refers to personal property, as opposed to
-property in land.)
-
-EFF's amicus brief argues three main points.
-
-(1) Intel did not qualify for relief under "trespass to chattels"
-because Intel's e-mail servers were not themselves harmed by
-Hamidi's e-mails. If Intel was harmed, it was because the content of
-Hamidi's e-mails affected Intel employees, not because sending the
-e-mails affected the functioning of Intel's servers.
-
-(2) By focusing on unwanted "contact" with the chattel and ignoring
-the harm requirement, the court of appeal turned "trespass to
-chattels" into a doctrine that threatens common Internet activity
-like search engines and linking. For example, if a website posted a
-"no trespassing" sign, any "contact" by a search engine could be
-considered a trespass even if it caused no harm.
-
-(3) The court of appeal wrongly held that the injunction did not
-infringe Hamidi's freedom of speech. The First Amendment limits
-private parties' legal remedies in many areas of law, such as libel,
-out of concern that private parties will use the law to suppress
-criticism. The same principle should apply here, where Intel's
-claims of harm stem from the meaning of Hamidi's speech.
-
-
-Links: 
-
-The Intel v. Hamidi Archive:
-
-http://www.eff.org/Cases/Intel_v_Hamidi/
-
-- end -
-
---------------------------------------------------------------------
-
-* DeCSS Author Johansen's Trial Rescheduled
-
-The trial of Norwegian teen Jon Johansen, who created the
-controversial DeCSS software, has been pushed back again. It is now
-scheduled to be heard on December 9, 2002, in Oslo, Norway. In the
-fall of 1999, Johansen and his team reverse-engineered the content
-scrambling system (CSS) software used to encrypt DVDs in an effort
-to build a DVD player for the Linux operating system. In January of
-2002, the Norwegian Economic Crime Unit (OKOKRIM) charged Johansen
-with a violation of Norwegian Criminal Code Section 145.2, which
-outlaws breaking into a third-party's property in order to steal
-data that one is not entitled to. This prosecution marks the first
-time the law will be used to prosecute a person for accessing his
-own property (his own DVD). Johansen faces two years in prison if
-convicted. The prosecution is based on a formal complaint filed by
-the Motion Picture Association.
-
-The trial had originally been scheduled to take place in June of 
-2002 but was rescheduled when the court could not find any qualified 
-judges to hear Johansen's case.  Now the case is scheduled to be 
-heard by a three-judge panel. Help Jon in his battle against 
-Hollywood movie studios, donate to his legal defense fund at: 
-
-http://www.eff.org/support/jonfund.html
-
-Links: 
-
-The DeCSS/Johansen Archive:
-http://www.eff.org/IP/Video/DeCSS_prosecutions/Johansen_DeCSS_case/
-
-Digital Rights Management Archive: 
-http://www.eff.org/IP/DRM/
-
-- end -
-
---------------------------------------------------------------------
-
-* Bunnie Presents Paper on XBox Reverse Engineering
-
-Paper Explains Flaw in Videogame Security System
-
-Researcher Escapes Chilling Effect of Digital Copyright Law
-
-Electronic Frontier Foundation Media Advisory
-
-For Immediate Release: Thursday, August 9, 2002
-
-San Francisco - The Electronic Frontier Foundation (EFF) is pleased
-to announce that former MIT doctoral student Andrew "Bunnie" Huang
-will present a paper explaining a security flaw in the Microsoft
-Xbox (TM) videogame system.
-
-Huang will present his paper, "Keeping Secrets in Hardware: the
-Microsoft X-BOX Case Study," at 5:25 p.m. PDT on August 13, 2002, at
-the 2002 Workshop on Cryptographic Hardware and Embedded Systems
-(CHES 2002) in Redwood City, California (Aug. 13-15, 2002).
-
-The Xbox security system is intended to allow people to play only
-videogames authorized by Microsoft. Huang's paper "shows how a
-person could defeat that system with a small hardware investment,"
-said MIT Professor Hal Abelson, one of Huang's advisors. "More
-importantly, the paper relates the security vulnerability to a
-general design flaw shared by other high-profile security systems
-such as the government's Clipper Chip and the movie industry's
-Contents Scrambling System (CSS) for DVD players."
-
-Huang contacted EFF in March after his advisors told him that his
-preliminary findings raised potentially significant legal questions.
-With the help of Boston College law professor Joe Liu, EFF worked
-with Huang, Abelson, and MIT administrators to analyze the legal
-issues and draft letters notifying Microsoft of Huang's research
-findings and intended publication, one of the steps encouraged by
-Digital Millennium Copyright Act (DMCA).
-
-Microsoft told Huang and Abelson that while it might prefer that the
-paper not be published, it would be inappropriate to ask MIT to
-withhold the paper.
-
-"Microsoft deserves praise for making no attempt to control
-publication," said Abelson. "Their response shows that they value
-academic freedom, and that they appreciate the critical role of
-unfettered research and publication in advancing technology."
-
-Other companies have reacted otherwise, using the DMCA to threaten
-researchers. The Recording Industry Association of America last year
-warned Princeton Professor Edward Felten after his research team
-exposed weaknesses in digital music security technologies. Last
-month, Hewlett Packard (HP) threatened research collective SnoSoft
-over exposing a security vulnerability in HP's Tru64 Unix operating
-system. Soon after, HP clarified that it would not use the DMCA to
-stifle research or impede the flow of information that would improve
-computer security.
-
-Huang said that while he is glad he can openly present his paper,
-"The DMCA clearly had a chilling effect on my work. I was afraid to
-submit my research for peer review until after the EFF's efforts to
-clear potential legal restraints."
-
-"Researchers should be analyzing security, not worrying about
-getting sued," said EFF Senior Staff Attorney Lee Tien.
-
-Links:
-
-For this release:
-http://www.eff.org/IP/DMCA/20020808_eff_bunnie_pr.html
-
-For Huang's paper:
-ftp://publications.ai.mit.edu/ai-publications/2002/AIM-2002-008.pdf
-
-For the CHES program: http://islab.oregonstate.edu/ches/program.html
-
-EFF "Unintended Consequences: Three Years Under the DMCA" report:
-http://www.eff.org/IP/DMCA/20020503_dmca_consequences.pdf
-
-RIAA sues Professor Edward Felten over SDMI:
-http://www.eff.org/Legal/Cases/Felten_v_RIAA/
-
-An article about Hewlett-Packard's threatening SnoSoft:
-http://www.wired.com/news/technology/0,1282,54297,00.html
-
-- end -
-
---------------------------------------------------------------------
-
-* EFF Thanks Defcon
-
-EFF thanks The Dark Tangent and other organizers of the DEF CON X
-convention for their generous donation of exhibition space at DEF
-CON (http://www.defcon.org/). DEF CON is an "underground" computer
-security conference held each summer in Las Vegas.
-
-Links: 
-
-Defcon Website: 
-http://www.defcon.com/
-
-- end -
-
---------------------------------------------------------------------
-
-* EFF Booth at LinuxWorld
-
-Come visit EFF at booth #488 at Linuxworld next week. We'll be
-passing out information, good cheer, and a slew of new stickers.
-
-When: August 13 - 15
-       10a - 5p
-
-Where: Booth #5
-       Moscone Center 
-       747 Howard Street
-       San Francisco, CA 94103
-
-Links: 
-
-LinuxWorld Conference Website: 
-http://www.linuxworldexpo.com/
-
-Floor Map and EFF Booth:
-http://www.linuxworldexpo.com/linuxworldexpo/v31/floorplan/floorplan
-.cvn?b=97& exbID=50
-
-- end -
-
---------------------------------------------------------------------
-
-Deep Links 
-
-Deep Links is a new department in the EFFector featuring noteworthy 
-news-items, victories and threats from around the Internet.
-
-
-* Baen Books expands fair-use-friendly e-book program
-
-Baen Books will bind a CD-ROM into the October 2002 hardcover
-edition of *War of Honor,* the latest volume in David Weber's epic
-Honor Harrington space-opera. The CD will contain at least 22
-complete novels, all in open formats like html and RTF, with the
-fair-use-friendly admonishment "This disk and its contents may be
-copied and shared but NOT sold." Included on the disk are the entire
-Honor Harrington series to date, as well as other titles from the
-Baen line, including Keith Laumer's *Retief!* and Larry Niven and
-Jerry Pournelle's *Fallen Angels*.
-
-Baen has been a banner-carrier for fair-use in electronic
-publishing, shipping text and html files that can be played on a
-multitude of devices. Other publishers have chosen to publish their
-material in copy-controlled formats that make it impossible to
-legally loan or resell the titles you purchase, are locked to a
-specific device, can't play on every operating system, and
-occasionally lock out assistive technology like the screen-readers
-employed by the blind.
-
-Dmitry Skylarov, a Russian scientist, was arrested in July 2001, for
-demonstrating how end-users could defeat the copy-prevention
-employed by Adobe's e-book technology. Adobe asked the FBI to arrest
-Skylarov for violating the Digital Millennium Copyright Act (DMCA),
-which makes it a crime to describe techniques for circumventing
-copy-prevention technology. Though Skylarov was later released, his
-employer, ElcomSoft, is still facing charges in the USA, and the
-Russian government has issued an advisory warning Russian scientists
-to steer clear of American technical conferences until the DMCA is
-repealed.
-
-Here is Baen's statement on the CD release:
-
-You are about to start playing with a CD-ROM that has fairly
-extraordinary content. As of this writing it includes twenty-two
-UNENCRYPTED novels in several formats, the ten Honor Harrington
-Novels, 3 Honor Harrington Anthologies and 9 novels by friends of
-Honor, and by the time of distribution it may well contain more.
-(More than twenty novels for free, and with no stupid codes to work
-around. Think of that.) The reason for the plethora of formats is to
-try to please the people who want to read the novels on their Palm
-Pilots or other text-specialized palm-sized devices.
-
-Links:
-
-Baen Books's page for *War of Honor*:
-http://www.baen.com/orientation.htm
-
-Slashdot discussion of *War of Honor* release:
-http://slashdot.org/article.pl?sid=02/08/03/2314232&mode=flat&tid=
-149
-
-EFF documents on Dmitry Skylarov and ElcomSoft:
-http://www.eff.org/IP/DMCA/US_v_Elcomsoft/
-
-EFF documents on the Digital Millennium Copyright Act (DMCA):
-http://www.eff.org/IP/DMCA/
-
-- end -
-
-* Singer/Songwriter Janis Ian on P2P Lucid article on the benefits of
-peer-to-peer networks form an artists' perspective.
-http://www.janisian.com/article-internet_debacle.html
-
-- end -
-
-* Hometown Paper Discusses Rep. Coble's Support of Berman P2P Hacking
-Bill Column on how a good Representative can make a bad call.
-http://www.news-record.com/news/columnists/staff/cone04.htm
-
-- end -
-
-
---------------------------------------------------------------------
-
-Administrivia
-
-EFFector is published by:
-
-The Electronic Frontier Foundation 
-454 Shotwell Street 
-San Francisco
-CA 94110-1914 USA 
-+1 415 436 9333 (voice) 
-+1 415 436 9993 (fax) 
-http://www.eff.org/
-
-Editor: Ren Bucholz, 
-       Activist 
-       ren@eff.org
-
-To Join EFF online, or make an additional donation, go to: 
-http://www.eff.org/support/
-
-Membership & donation queries: 
-membership@eff.org 
-
-General EFF, legal, policy or online resources queries: 
-ask@eff.org
-
-Reproduction of this publication in electronic media is encouraged.
-Signed articles do not necessarily represent the views of EFF. To
-reproduce signed articles individually, please contact the authors
-for their express permission. Press releases and EFF announcements &
-articles may be reproduced individually at will.
-
-To change your address, plese visit:
-http://action.eff.org/subscribe/. 
-
->>From there, you can update all your information. If you have already 
-subscribed to the EFF Action Center, please visit:
-http://action.eff.org/action/login.asp.
-
-(Please ask ren@eff.org to manually remove you from the list if this
-does not work for you for some reason.)
-
-Back issues are available at: 
-http://www.eff.org/effector
-
-To get the latest issue, send any message to
-effector-reflector@eff.org (or er@eff.org), and it will be mailed to
-you automatically. You can also get it via the Web at:
-http://www.eff.org/pub/EFF/Newsletters/EFFector/current. html
-
-
-++++++++++++++++++++++++
-You received this message because aaaaaa@yyyyyy.zzz is a member of 
-the mailing list originating from alerts@action.eff.org. To unsubscribe from 
-all mailing lists originating from alerts@action.eff.org, send an email to 
-alerts@action.eff.org with "Remove" as the only text in the subject line.
-
-
diff --git a/upstream/t/data/whitelists/amazon_co_uk_ship b/upstream/t/data/whitelists/amazon_co_uk_ship
deleted file mode 100644 (file)
index fdc7dfa..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-Received: (qmail 24448 invoked by uid 505); 3 Jun 2002 13:35:25 -0000
-Received: from orders@amazon.co.uk by zzzzzzzzz.azzzzzzzzzzz.org by uid 502 with qmail-scanner-1.12 (F-PROT: 3.12. Clear:. Processed in 0.342757 secs); 03 Jun 2002 13:35:25 -0000
-Received: from localhost (127.0.0.1)
-  by localhost with SMTP; 3 Jun 2002 13:35:24 -0000
-Delivered-To: zzzzzzzzz-com-popbox@zzzzzzzzz.com
-Received: from mail.zzzzzzzzz.com [64.124.162.104]
-        by localhost with POP3 (fetchmail-5.9.0)
-        for zzzzzzzzz@localhost (single-drop); Mon, 03 Jun 2002 09:35:24 -0400 (EDT)
-Received: (qmail 3226 invoked by uid 1002); 3 Jun 2002 13:34:30 -0000
-Delivered-To: zzzzzzzzz-com-rod@zzzzzzzzz.com
-Received: (qmail 3224 invoked from network); 3 Jun 2002 13:34:29 -0000
-Received: from unknown (HELO aprilia.amazon.com) (207.171.190.156)
-  by mail0.tyva.netherweb.com with SMTP; 3 Jun 2002 13:34:29 -0000
-Received: from matchless.amazon.com (matchless.amazon.com [10.16.42.218])
-        by aprilia.amazon.com (Postfix) with ESMTP id 2A30D55C
-        for <rod@zzzzzzzzz.com>; Mon,  3 Jun 2002 06:34:29 -0700 (PDT)
-Received: from vdc-dc-batch-101.vdc.amazon.com by matchless.amazon.com with ESMTP 
-        (crosscheck: vdc-dc-batch-101.vdc.amazon.com [10.30.41.134])
-        id g53DMcd9000547
-        for <rod@zzzzzzzzz.com>; Mon, 3 Jun 2002 06:34:28 -0700
-Received: by vdc-dc-batch-101.vdc.amazon.com 
-Date: Mon, 3 Jun 2002 13:12:54 GMT
-Message-Id: <g53DCsC22572.200206031312@vdc-dc-batch-101.vdc.amazon.com>
-To: rod@zzzzzzzzz.com
-From: orders@amazon.co.uk
-Subject: Your Amazon.co.uk order has been dispatched (#999-4444444-3333333)
-
-[amazon.co.uk order]
-
diff --git a/upstream/t/data/whitelists/amazon_com_ship b/upstream/t/data/whitelists/amazon_com_ship
deleted file mode 100644 (file)
index c67fde9..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-Received: (qmail 10120 invoked by uid 505); 14 Jun 2002 19:55:43 -0000
-Received: from ship-confirm@amazon.com by zzzzzzzz.iiiiiiiii.org by uid 502 with qmail-scanner-1.12 (F-PROT: 3.12. Clear:. Processed in 0.174777 secs); 14 Jun 2002 19:55:43 -0000
-Received: from localhost (127.0.0.1)
-  by localhost with SMTP; 14 Jun 2002 19:55:42 -0000
-Delivered-To: zzzzzzzzz-com-popbox@zzzzzzzzz.com
-Received: from mail.zzzzzzzzz.com [64.124.162.104]
-        by localhost with POP3 (fetchmail-5.9.0)
-        for zzzzzzzzz@localhost (single-drop); Fri, 14 Jun 2002 15:55:42 -0400 (EDT)
-Received: (qmail 32249 invoked by uid 1002); 14 Jun 2002 19:55:17 -0000
-Delivered-To: zzzzzzzzz-com-rod@zzzzzzzzz.com
-Received: (qmail 32245 invoked from network); 14 Jun 2002 19:55:17 -0000
-Received: from unknown (HELO sas-dc-mail-102.amazon.com) (207.171.190.155)
-  by mail0.tyva.netherweb.com with SMTP; 14 Jun 2002 19:55:17 -0000
-Received: by sas-dc-mail-102.amazon.com (Postfix, from userid 1001)
-        id 0578E3F41; Fri, 14 Jun 2002 19:55:17 +0000 (GMT)
-To: rod@zzzzzzzzz.com
-From: ship-confirm@amazon.com
-Subject: Your Amazon.com order has shipped (#888-4444444-9999999)
-Message-Id: <20020614195517.0578E3F41@sas-dc-mail-102.amazon.com>
-Date: Fri, 14 Jun 2002 19:55:17 +0000 (GMT)
-
-[Amazon shipping confirmation]
-
-
diff --git a/upstream/t/data/whitelists/cert.org b/upstream/t/data/whitelists/cert.org
deleted file mode 100644 (file)
index 476852c..0000000
+++ /dev/null
@@ -1,347 +0,0 @@
-Received: from geb.xxxxxx.gen.nz (geb.xxxxxx.gen.nz [210.55.106.161])
-       by dogma.slashnull.org (8.11.6/8.11.6) with ESMTP id g6N1Tc414637
-       for <aaaaaa@yyyyyy.zzz>; Tue, 23 Jul 2002 02:29:38 +0100
-Received: from uuuuuu by geb.xxxxxx.gen.nz with local (Exim 3.35 #1 (Debian))
-       id 17WoTo-0002EQ-00
-       for <aaaaaa@yyyyyy.zzz>; Tue, 23 Jul 2002 13:28:48 +1200
-Received: from mail by geb.xxxxxx.gen.nz with spam-scanned (Exim 3.35 #1 (Debian))
-       id 17WoTm-0002ED-00
-       for <uuuuuu@xxxxxx.gen.nz>; Tue, 23 Jul 2002 13:28:47 +1200
-Received: from firewater.pppppp.co.nz ([203.109.253.55])
-       by geb.spit.gen.nz with esmtp (Exim 3.35 #1 (Debian))
-       id 17WoTl-0002E6-00
-       for <uuuuuu@xxxxxx.gen.nz>; Tue, 23 Jul 2002 13:28:45 +1200
-Received: from scanner1.pppppp.co.nz (scanner1.pppppp.co.nz [203.109.254.21])
-       by firewater.pppppp.co.nz (8.9.2/8.9.2) with ESMTP id NAA13877
-       for <b.addis@staff.pppppp.co.nz>; Tue, 23 Jul 2002 13:28:44 +1200 (NZST)
-Received: from localhost ([127.0.0.1] helo=grunt2.pppppp.co.nz)
-       by scanner1.pppppp.co.nz with esmtp (Exim 3.12 #1 (Debian))
-       id 17WoTk-0003Uv-00
-       for <b.addis@staff.pppppp.co.nz>; Tue, 23 Jul 2002 13:28:44 +1200
-Received: from canaveral.red.cert.org [192.88.209.11] 
-       by grunt2.pppppp.co.nz with esmtp (Exim 3.35 #1 (Debian))
-       id 17WoTX-0004oQ-00; Tue, 23 Jul 2002 13:28:32 +1200
-Received: from localhost (lnchuser@localhost)
-       by canaveral.red.cert.org (8.9.3/8.9.3/1.12) with SMTP id TAA16990;
-       Mon, 22 Jul 2002 19:11:24 -0400 (EDT)
-Date: Mon, 22 Jul 2002 19:11:24 -0400 (EDT)
-Received: by canaveral.red.cert.org; Mon, 22 Jul 2002 19:05:32 -0400
-Message-Id: <CA-2002-21.1@cert.org>
-From: CERT Advisory <cert-advisory@cert.org>
-To: cert-advisory@cert.org
-Organization: CERT(R) Coordination Center - +1 412-268-7090
-List-Help: <http://www.cert.org/>, <mailto:Majordomo@cert.org?body=help>
-List-Subscribe: <mailto:Majordomo@cert.org?body=subscribe%20cert-advisory>
-List-Unsubscribe: <mailto:Majordomo@cert.org?body=unsubscribe%20cert-advisory>
-List-Post: NO (posting not allowed on this list)
-List-Owner: <mailto:cert-advisory-owner@cert.org>
-List-Archive: <http://www.cert.org/>
-Subject: CERT Advisory CA-2002-21 Vulnerability in PHP
-X-Rcpt-To: uuuuuu@xxxxxx.gen.nz
-Sender: Brent Addis <uuuuuu@xxxxxx.gen.nz>
-
-
-
------BEGIN PGP SIGNED MESSAGE-----
-
-CERT Advisory CA-2002-21 Vulnerability in PHP
-
-   Original release date: July 22, 2002
-   Last revised: --
-   Source: CERT/CC
-
-   A complete revision history can be found at the end of this file.
-
-Systems Affected
-
-     * Systems running PHP versions 4.2.0 or 4.2.1
-
-Overview
-
-   A  vulnerability  has been discovered in PHP. This vulnerability could
-   be  used  by  a remote attacker to execute arbitrary code or crash PHP
-   and/or the web server.
-
-I. Description
-
-   PHP  is  a  popular  scripting  language  in  widespread use. For more
-   information about PHP, see
-
-          http://www.php.net/manual/en/faq.general.php
-
-   The  vulnerability  occurs  in the portion of PHP code responsible for
-   handling  file uploads, specifically multipart/form-data. By sending a
-   specially  crafted  POST  request  to  the web server, an attacker can
-   corrupt  the  internal  data  structures used by PHP. Specifically, an
-   intruder  can  cause  an improperly initialized memory structure to be
-   freed.  In  most  cases, an intruder can use this flaw to crash PHP or
-   the  web  server. Under some circumstances, an intruder may be able to
-   take  advantage  of  this  flaw  to  execute  arbitrary  code with the
-   privileges of the web server.
-
-   You  may  be  aware that freeing memory at inappropriate times in some
-   implementations  of  malloc  and  free  does not usually result in the
-   execution  of  arbitrary  code.  However, because PHP utilizes its own
-   memory  management  system,  the  implementation of malloc and free is
-   irrelevant to this problem.
-
-   Stefan  Esser  of  e-matters  GmbH has indicated that intruders cannot
-   execute   code   on   x86   systems.   However,  we  encourage  system
-   administrators  to  apply  patches  on  x86  systems  as well to guard
-   against denial-of-service attacks and as-yet-unknown attack techniques
-   that may permit the execution of code on x86 architectures.
-
-   This  vulnerability  was discovered by e-matters GmbH and is described
-   in  detail  in  their  advisory.  The  PHP  Group  has  also issued an
-   advisory.  A list of vendors contacted by the CERT/CC and their status
-   regarding this vulnerability is available in VU#929115.
-
-   Although   this  vulnerability  only  affects  PHP  4.2.0  and  4.2.1,
-   e-matters  GmbH  has  previously  identified  vulnerabilities in older
-   versions  of  PHP.  If  you  are  running  older  versions  of PHP, we
-   encourage you to review
-   http://security.e-matters.de/advisories/012002.html
-
-II. Impact
-
-   A  remote  attacker can execute arbitrary code on a vulnerable system.
-   An  attacker  may not be able to execute code on x86 architectures due
-   to  the way the stack is structured. However, an attacker can leverage
-   this  vulnerability  to  crash PHP and/or the web server running on an
-   x86 architecture.
-
-III. Solution
-
-Apply a patch from your vendor
-
-   Appendix A contains information provided by vendors for this advisory.
-   As  vendors report new information to the CERT/CC, we will update this
-   section  and note the changes in our revision history. If a particular
-   vendor  is  not  listed  below,  we  have not received their comments.
-   Please contact your vendor directly.
-
-Upgrade to the latest version of PHP
-
-   If  a  patch  is  not  available  from your vendor, upgrade to version
-   4.2.2.
-
-Deny POST requests
-
-   Until  patches  or an update can be applied, you may wish to deny POST
-   requests.  The  following  workaround  is  taken from the PHP Security
-   Advisory:
-
-     If  the  PHP  applications on an affected web server do not rely on
-     HTTP POST input from user agents, it is often possible to deny POST
-     requests on the web server.
-
-     In  the  Apache  web server, for example, this is possible with the
-     following  code  included  in  the  main  configuration  file  or a
-     top-level .htaccess file:
-
-     <Limit POST>
-        Order deny,allow
-        Deny from all
-     </Limit>
-
-     Note  that an existing configuration and/or .htaccess file may have
-     parameters contradicting the example given above.
-
-Disable vulnerable service
-
-   Until  you  can upgrade or apply patches, you may wish to disable PHP.
-   As a best practice, the CERT/CC recommends disabling all services that
-   are not explicitly required. Before deciding to disable PHP, carefully
-   consider your service requirements.
-
-Appendix A. - Vendor Information
-
-   This  appendix  contains  information  provided  by  vendors  for this
-   advisory.  As  vendors  report new information to the CERT/CC, we will
-   update this section and note the changes in our revision history. If a
-   particular  vendor  is  not  listed  below, we have not received their
-   comments.
-
-Apple Computer Inc.
-
-          Mac  OS  X  and  Mac  OS X Server are shipping with PHP version
-          4.1.2  which  does  not  contain the vulnerability described in
-          this alert.
-
-Caldera
-
-          Caldera  OpenLinux  does  not provide either vulnerable version
-          (4.2.0,  4.2.1)  of  PHP  in their products. Therefore, Caldera
-          products are not vulnerable to this issue.
-
-Compaq Computer Corporation
-
-          SOURCE:  Compaq Computer Corporation, a wholly-owned subsidiary
-          of  Hewlett-Packard  Company  and  Hewlett-Packard  Company  HP
-          Services Software Security Response Team
-          x-ref: SSRT2300 php post requests
-          At  the  time  of  writing  this  document, Compaq is currently
-          investigating   the   potential  impact  to  Compaq's  released
-          Operating System software products.
-          As  further  information  becomes available Compaq will provide
-          notice  of  the  availability  of any necessary patches through
-          standard  security bulletin announcements and be available from
-          your normal HP Services supportchannel.
-
-Cray Inc.
-
-          Cray, Inc. does not supply PHP on any of its systems.
-
-Debian
-
-          Debian GNU/Linux stable aka 3.0 is not vulnerable.
-          Debian GNU/Linux testing is not vulnerable.
-          Debian GNU/Linux unstable is vulnerable.
-          The  problem  effects PHP versions 4.2.0 and 4.2.1. Woody ships
-          an  older  version  of  PHP  (4.1.2),  that doesn't contain the
-          vulnerable function.
-
-FreeBSD
-
-          FreeBSD  does not include any version of PHP by default, and so
-          is  not  vulnerable; however, the FreeBSD Ports Collection does
-          contain  the  PHP4  package. Updates to the PHP4 package are in
-          progress  and a corrected package will be available in the near
-          future.
-
-Guardian Digital
-
-          Guardian  Digital  has not shipped PHP 4.2.x in any versions of
-          EnGarde, therefore we are not believed to be vulnerable at this
-          time.
-
-Hewlett-Packard Company
-
-          SOURCE:  Hewlett-Packard Company Security Response Team
-          At  the  time  of  writing  this  document,  Hewlett Packard is
-          currently  investigating  the potential impact to HP's released
-          Operating System software products.
-          As further information becomes available HP will provide notice
-          of  the  availability of any necessary patches through standard
-          security  bulletin  announcements  and  be  available from your
-          normal HP Services support channel.
-
-IBM
-
-          IBM  is  not vulnerable to the above vulnerabilities in PHP. We
-          do  supply the PHP packages for AIX through the AIX Toolbox for
-          Linux  Applications.  However,  these packages are at 4.0.6 and
-          also incorporate the security patch from 2/27/2002.
-
-Mandrakesoft
-
-          Mandrake Linux does not ship with PHP version 4.2.x and as such
-          is  not  vulnerable.  The  Mandrake Linux cooker does currently
-          contain  PHP  4.2.1  and  will  be  updated shortly, but cooker
-          should  not be used in a production environment and no advisory
-          will be issued.
-
-Microsoft Corporation
-
-          Microsoft  products  are not affected by the issues detailed in
-          this advisory.
-
-Network Appliance
-
-          No Netapp products are vulnerable to this.
-
-Red Hat Inc.
-
-          None  of  our commercial releases ship with vulnerable versions
-          of PHP (4.2.0, 4.2.1).
-
-SuSE Inc.
-
-          SuSE Linux is not vulnerable to this problem, as we do not ship
-          PHP 4.2.x.
-     _________________________________________________________________
-
-   The  CERT/CC acknowledges e-matters GmbH for discovering and reporting
-   this vulnerability.
-     _________________________________________________________________
-
-   Author: Ian A. Finlay.
-   ______________________________________________________________________
-
-   This document is available from:
-   http://www.cert.org/advisories/CA-2002-21.html
-   ______________________________________________________________________
-
-CERT/CC Contact Information
-
-   Email: cert@cert.org
-          Phone: +1 412-268-7090 (24-hour hotline)
-          Fax: +1 412-268-6989
-          Postal address:
-          CERT Coordination Center
-          Software Engineering Institute
-          Carnegie Mellon University
-          Pittsburgh PA 15213-3890
-          U.S.A.
-
-   CERT/CC   personnel   answer  the  hotline  08:00-17:00  EST(GMT-5)  /
-   EDT(GMT-4)  Monday  through  Friday;  they are on call for emergencies
-   during other hours, on U.S. holidays, and on weekends.
-
-Using encryption
-
-   We  strongly  urge you to encrypt sensitive information sent by email.
-   Our public PGP key is available from
-   http://www.cert.org/CERT_PGP.key
-
-   If  you  prefer  to  use  DES,  please  call the CERT hotline for more
-   information.
-
-Getting security information
-
-   CERT  publications  and  other security information are available from
-   our web site
-   http://www.cert.org/
-
-   To  subscribe  to  the CERT mailing list for advisories and bulletins,
-   send  email  to majordomo@cert.org. Please include in the body of your
-   message
-
-   subscribe cert-advisory
-
-   *  "CERT"  and  "CERT  Coordination Center" are registered in the U.S.
-   Patent and Trademark Office.
-   ______________________________________________________________________
-
-   NO WARRANTY
-   Any  material furnished by Carnegie Mellon University and the Software
-   Engineering  Institute  is  furnished  on  an  "as is" basis. Carnegie
-   Mellon University makes no warranties of any kind, either expressed or
-   implied  as  to  any matter including, but not limited to, warranty of
-   fitness  for  a  particular purpose or merchantability, exclusivity or
-   results  obtained from use of the material. Carnegie Mellon University
-   does  not  make  any warranty of any kind with respect to freedom from
-   patent, trademark, or copyright infringement.
-     _________________________________________________________________
-
-   Conditions for use, disclaimers, and sponsorship information
-
-   Copyright 2002 Carnegie Mellon University.
-
-   Revision History
-July 22, 2002:  Initial release
-
-
-
-
------BEGIN PGP SIGNATURE-----
-Version: PGP 6.5.8
-
-iQCVAwUBPTyOVqCVPMXQI2HJAQGK6QQAp1rR7K18PNxpQZvqKPYWxyrtpiT8mmKN
-UuyERmOoX+5MAwH0hbAWCvVcyLH0gKGbTpBkRgToT8IEHZojwHCzqOaMM9kni/FG
-QEVeznLfBX4GIgZGPu0XWlph3ZqaayWln57eGueYZ26zBuriIUu2cUCmyYGQkqlI
-tuZdnDqUmR0=
-=+829
------END PGP SIGNATURE-----
-
-
diff --git a/upstream/t/data/whitelists/debian_bts_reassign b/upstream/t/data/whitelists/debian_bts_reassign
deleted file mode 100644 (file)
index e492886..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-Received: from dogma.slashnull.org (dogma.slashnull.org [212.17.35.15])
-        by zzzzzzzzzzzzzz.zzz (Postfix) with ESMTP id 498AA132505
-        for <yyyyyy@aaaaaaaaa.aaa>; Thu,  1 Aug 2002 14:22:07 -0700 (PDT)
-Received: from intm3.sparklist.com (intm3.sparklist.com [207.250.144.9])
-        by dogma.slashnull.org (8.11.6/8.11.6) with SMTP id g71LN6230402
-        for <zzzzzzzzzzzzz@yyyyyyyy>; Thu, 1 Aug 2002 22:23:06 +0100
-Message-Id: <INTM-6516589-3669406-2002.08.01-16.21.51--zzzzzzzzzzzzz#yyyyyyyy@list3.internet.com>
-To: Colin Watson <cjwatson@debian.org>
-Subject: Processed: reassign 126111 to cdimage.debian.org
-From: owner@bugs.debian.org (Debian Bug Tracking System)
-Date: Thu, 27 Dec 2001 18:48:04 -0600
-Cc: unknown-package@qa.debian.org (pseudo-image-kit-2.0.zip #126111), Debian CD-ROM Team <debian-cd@lists.debian.org>(cdimage.debian.org #126111)
-In-Reply-To: <E16Jl13-0007Q1-00@arborlon.riva.ucam.org>
-References: <E16Jl13-0007Q1-00@arborlon.riva.ucam.org>
-Sender: Debian BTS <debbugs@master.debian.org>
-
-Processing commands for control@bugs.debian.org:
-
-> reassign 126111 cdimage.debian.org
-Bug#126111: 2.2_rev4/i386/binary-i386-1.list not up to date
-Bug reassigned from package `pseudo-image-kit-2.0.zip' to `cdimage.debian.org'.
-
->
-End of message, stopping processing here.
-
-Please contact me if you need assistance.
-
-Debian bug tracking system administrator
-(administrator, Debian Bugs database)
-
diff --git a/upstream/t/data/whitelists/ibm_enews_de b/upstream/t/data/whitelists/ibm_enews_de
deleted file mode 100644 (file)
index 0974593..0000000
+++ /dev/null
@@ -1,311 +0,0 @@
-Return-Path: <info@isource.ibm.com>
-Received: (qmail 21402 invoked by alias); 4 Jul 2002 13:36:52 -0000
-Received: (qmail 21361 invoked by uid 82); 4 Jul 2002 13:36:51 -0000
-Received: from info@isource.ibm.com by mailhost with qmail-scanner-1.00 (uvscan: v4.1.40/v4210. . Clean. Processed in 2.214854 secs); 04 Jul 2002 13:36:51 -0000
-Received: from isource.boulder.ibm.com (HELO isource.ibm.com) (207.25.249.18)
-  by mi-1.rz.ruhr-uni-bochum.de with SMTP; 4 Jul 2002 13:36:48 -0000
-Received: from isource.boulder.ibm.com (loopback [127.0.0.1])
-       by isource.ibm.com (Postfix) with ESMTP id 0585052807
-       for <XXXXXX.YYYYYYYYYY@RUHR-UNI-BOCHUM.DE>; Thu,  4 Jul 2002 13:32:05 +0000 (CUT)
-From: IBM Deutschland <info@isource.ibm.com>
-Reply-To: webmaster@de.ibm.com
-Subject: IBM eNews: Aktuelle Informationen von IBM
-Content-Type: text/plain;
-To: XXXXXX.YYYYYYYYYY@RUHR-UNI-BOCHUM.DE
-Message-Id: <20020704133206.0585052807@isource.ibm.com>
-Date: Thu,  4 Jul 2002 13:32:06 +0000 (CUT)
-
-IBM eNews
-4. Juli 2002
-
-Liebe Leserin, lieber Leser,
-
-zur Zeit findet in Wimbledon das diesjährige Tennisturnier 
-statt - mit Hilfe von IBM auch online unter 
-http://www.wimbledon.org ein packendes Ereignis. 
-
-Lesen Sie mehr über diesem Internetauftritt und zu 
-zahlreichen weiteren Themen aus der IT-Branche in der 
-aktuellen Ausgabe von IBM eNews.
-
-Nutzen Sie die Möglichkeit, auf folgender Webseite aus über 
-40 Interessensgebieten die Themen für Ihre persönliche 
-IBM eNews Ausgabe auszuwählen. Sie erhalten dann Business-
-Informationen nach Maß:
-http://www.ibm.com/de/profile/change_interests.html
-
-Alle Artikel können Sie jederzeit online auf dieser Webseite 
-aufrufen:
-http://www.ibm.com/de/news/enews/online/
-
-Wir halten Sie auf dem Laufenden
-IBM eNews
-
-Wenn Sie in Zukunft IBM eNews nicht mehr erhalten möchten, 
-können Sie sich auf dieser Webseite abmelden:
-http://www.ibm.com/de/profile/unsubscribe.html
-
-
-
-In der heutigen Ausgabe:
-========================
-
-e-business
-
-  o  Wimbledon gewinnt: Mit Hilfe von IBM auch online ein packendes Ereignis 
-     http://isource.ibm.com/cgi-bin/goto?on=de020756
-
-  o  Neues Buch "Deutschland Online" weist Weg in die
-      Informationsgesellschaft 
-     http://isource.ibm.com/cgi-bin/goto?on=de020757
-
-  o  Weitere Artikel aus dem Bereich "e-business" 
-     http://isource.ibm.com/cgi-bin/goto?on=020758
-
-Business Lösungen und Services
-
-  o  Mehr Training für alle: IBM Learning Services Corporate Card 
-     http://isource.ibm.com/cgi-bin/goto?on=020721
-
-  o  Weitere Artikel aus dem Bereich "Business Lösungen und Services"  
-     http://isource.ibm.com/cgi-bin/goto?on=020759
-
-IT Solutions und Services
-
-  o  e-guide aktuell: Produkte und Lösungen für den Mittelstand -
-      zusammengefasst im IBM Kundenmagazin! 
-     http://isource.ibm.com/cgi-bin/goto?on=020710
-
-  o  Sprechen Sie mit uns: Sicherheit ist Trumpf 
-     http://isource.ibm.com/cgi-bin/goto?on=020712
-
-  o  Weitere Artikel aus dem Bereich "IT Solutions und Services" 
-     http://isource.ibm.com/cgi-bin/goto?on=020760
-
-Software
-
-  o  Software für den Mittelstand 
-     http://isource.ibm.com/cgi-bin/goto?on=020711
-
-  o  WebSphere Integration 
-     http://isource.ibm.com/cgi-bin/goto?on=020724
-
-  o  Weitere Artikel aus dem Bereich "Software"  
-     http://isource.ibm.com/cgi-bin/goto?on=020761
-
-Hardware
-
-  o  Neu: IBM ThinkPad A31p - Die erste mobile 3D-Workstation! 
-     http://isource.ibm.com/cgi-bin/goto?on=de020707
-
-  o  IBM eServer* pSeries 630 6E4/6C4 - Ankündigung des neuen Entry Servers 
-     http://isource.ibm.com/cgi-bin/goto?on=020713
-
-  o  Weitere Artikel aus dem Bereich "Hardware" 
-     http://isource.ibm.com/cgi-bin/goto?on=020762
-
-
-
-------------------------------------------------------------
-e-business
-------------------------------------------------------------
-
-Wimbledon gewinnt: Mit Hilfe von IBM auch online ein packendes Ereignis 
-
-   Die neue Turnier-Website bietet Tennisfans in aller Welt 
-  sekundenaktuelle Spielstände, Live-Videos, Kommentare und 
-  Interviews. Mit e-business on demand lässt sich dabei die 
-  erforderliche Kapazität für den Ansturm während des Turniers 
-  einfach einschalten - und anschließend ebenso einfach wieder 
-  abschalten. Ein echter Service-Gewinn, Klick für Klick. 
-  http://isource.ibm.com/cgi-bin/goto?on=de020756
-  
-
-
-Neues Buch "Deutschland Online" weist Weg in die
-      Informationsgesellschaft 
-
-  "Die schnelle Transformation in die Informationsgesellschaft ist 
-  Deutschlands letzte Chance, um im Kreis der großen 
-  Wirtschaftsmächte zu verbleiben. Deutschland muss IT-Weltmacht 
-  werden - und das pronto!" Das verlangte der Vorsitzende der 
-  Geschäftsführung der IBM Deutschland, Erwin Staudt, anlässlich 
-  der Vorstellung des Buches "Deutschland online" - Strategien und 
-  Projekte für die Informationsgesellschaft - in Berlin.
-   http://isource.ibm.com/cgi-bin/goto?on=de020757
-
-
-Weitere Artikel aus dem Bereich "e-business" 
-
-  Weitere Artikel aus dem Bereich "e-business" finden Sie online
-  auf unserer IBM eNews Website:
-  http://isource.ibm.com/cgi-bin/goto?on=020758
-     
-  Unter folgender Adresse können Sie Ihre Interessensgebiete 
-  auswählen und erhalten dann Business-Informationen nach Maß:
-  http://www-5.ibm.com/de/profile/change_interests.html
-
-
-
-------------------------------------------------------------
-Business Lösungen und Services
-------------------------------------------------------------
-
-Mehr Training für alle: IBM Learning Services Corporate Card 
-
-  IBM Learning Services bietet Ihnen preisgünstige Trainings: Mit 
-  der IBM Learning Services Corporate Card sparen Sie bis zu 1.650 
-  Euro. Im Unterschied zur IBM Learning Services Education Card 
-  können Sie damit alle Ihre Mitarbeiter/innen zu den Trainings 
-  senden.
-  http://isource.ibm.com/cgi-bin/goto?on=020721
-
-
-Weitere Artikel aus dem Bereich "Business Lösungen und Services"  
-
-  Weitere Artikel aus dem Bereich "Business Lösungen und Services" 
-  finden Sie online auf unserer IBM eNews Website:
-  http://isource.ibm.com/cgi-bin/goto?on=020759
-     
-  Unter folgender Adresse können Sie Ihre Interessensgebiete 
-  auswählen und erhalten dann Business-Informationen nach Maß:
-  http://www-5.ibm.com/de/profile/change_interests.html
-
-
-
-------------------------------------------------------------
-IT Solutions und Services
-------------------------------------------------------------
-
-e-guide aktuell: Produkte und Lösungen für den Mittelstand -
-      zusammengefasst im IBM Kundenmagazin! 
-
-  Mit dieser Ausgabe übernehmen wir Teile des Heftes auch im Web. 
-  Lesen Sie hier mehr über den neuen Produkt- und Lösungsteil 
-  oder bestellen Sie sich Ihr Exemplar des IBM Kundenmagazins 
-  "e-guide" 2/2002.
-  http://isource.ibm.com/cgi-bin/goto?on=020710
-
-
-Sprechen Sie mit uns: Sicherheit ist Trumpf 
-
-  Sie machen sich sicherlich Gedanken darüber, ob Ihre e-business 
-  Infrastruktur wirkungsvoll geschützt ist - insbesondere vor dem 
-  Hintergrund sich öffnender Strukturen.
-  IBM - als einer der Vorreiter im Bereich e-business Sicherheits-
-  strategien - bietet Ihnen Lösungen, mit denen Sie Ihre 
-  IT-Infrastruktur sichern können. Überzeugen Sie sich selbst:
-  http://isource.ibm.com/cgi-bin/goto?on=020712
-
-
-Weitere Artikel aus dem Bereich "IT Solutions und Services" 
-
-  Weitere Artikel aus dem Bereich "IT Solutions und Services" 
-  finden Sie online auf unserer IBM eNews Website:
-  http://isource.ibm.com/cgi-bin/goto?on=020760
-  
-  Unter folgender Adresse können Sie Ihre Interessensgebiete 
-  auswählen und erhalten dann Business-Informationen nach Maß:
-  http://www-5.ibm.com/de/profile/change_interests.html
-
-
-
-------------------------------------------------------------
-Software
-------------------------------------------------------------
-
-Software für den Mittelstand 
-
-  Hier finden Sie ausgewählte Software-Produkte mit Beispielen 
-  unserer zufriedenen Kunden. Die IBM Produktfamilien WebSphere, 
-  DB2, Lotus und Tivoli sind die Basis für eine Vielfalt von 
-  e-business Lösungen. Sie sind industrie-spezifisch, skalierbar 
-  ausgerichtet und basieren auf offenen Standards, so dass sie 
-  speziell auf die Bedürfnisse des Mittelstandes zugeschnitten 
-  werden können.
-  http://isource.ibm.com/cgi-bin/goto?on=020711
-
-
-WebSphere Integration 
-
-  Mit WebSphere Integration versucht die IBM keine Technologie zu 
-  vermarkten, die die unüberschaubare Vielfalt der Individual-
-  programmierungen um neue Facetten bereichert. Vielmehr agiert 
-  sie wie ein Katalysator und ermöglicht die Erweiterung und 
-  Erneuerung von Systemlandschaften sowie die Migration von 
-  geschäftskritischen Daten. Mehr dazu im Software-Schwerpunkt 
-  des Monats.
-  http://isource.ibm.com/cgi-bin/goto?on=020724
-
-
-Weitere Artikel aus dem Bereich "Software"  
-
-  Weitere Artikel aus dem Bereich "Software" finden Sie online
-  auf unserer IBM eNews Website:
-  http://isource.ibm.com/cgi-bin/goto?on=020761
-  
-  Unter folgender Adresse können Sie Ihre Interessensgebiete 
-  auswählen und erhalten dann Business-Informationen nach Maß:
-  http://www-5.ibm.com/de/profile/change_interests.html
-
-
-
-------------------------------------------------------------
-Hardware
-------------------------------------------------------------
-
-Neu: IBM ThinkPad A31p - Die erste mobile 3D-Workstation! 
-
-  Der ThinkPad A31p (TV2N6GE, TV2L3GE, TV2N5GE) ist ausgerüstet mit 
-  einem Intel Pentium 4 Notebookprozessor-M, schnellen DDR Speicher-
-  modulen und einem extrem leistungsfähigen Grafikchip. Auch die 
-  Highspeed Festplatte lässt keine Wünsche offen. Zwei modulare 
-  Laufwerkschächte sorgen für ein Plus an Flexibilität. Das Notebook 
-  ist mit einer 10/100 Ethernet-Karte, einem 56K V.92 Modem, 
-  integrierten 802.11b Wireless-Antennen/-Chip, Bluetooth sowie 
-  einem IEEE 1394 (Firewire)-Anschluss ausgerüstet. 
-  http://isource.ibm.com/cgi-bin/goto?on=de020707
-
-
-IBM eServer* pSeries 630 6E4/6C4 - Ankündigung des neuen Entry Servers 
-
-  IBM definiert den UNIX Entry Server neu. Zuverlässigkeit und
-  Verfügbarkeit der POWER4-Prozessortechnologie jetzt vom Entry- 
-  bis zum Enterprise-Bereich, mit Selbstverwaltungsfunktionen aus 
-  dem Projekt eLiza, ultraflaches Rack- oder Deskside-Modell, die 
-  richtige Wahl für kleine und mittelständische Unternehmen.
-  http://isource.ibm.com/cgi-bin/goto?on=020713
-
-
-Weitere Artikel aus dem Bereich "Hardware" 
-
-  Weitere Artikel aus dem Bereich "Hardware" finden Sie online
-  auf unserer IBM eNews Website:
-  http://isource.ibm.com/cgi-bin/goto?on=020762
-    
-  Unter folgender Adresse können Sie Ihre Interessensgebiete 
-  auswählen und erhalten dann Business-Informationen nach Maß:
-  http://www-5.ibm.com/de/profile/change_interests.html
-
-
-
-============================================================
-Sie erhalten diese E-Mail, da Sie zu IBM eNews
-  angemeldet sind als XXXXXX.YYYYYYYYYY@RUHR-UNI-BOCHUM.DE
-
-
-*Das IBM eServer Warenzeichen besteht aus dem eingeführten 
-IBM e-business Logo, gefolgt von dem beschreibenden Begriff 
-"Server".
-  
-Nach unseren Kundendaten sind Sie an Informationsmaterial von 
-IBM interessiert. An- und Abmelden sowie Ihre Einstellungen 
-ändern können Sie auf folgender Website: 
-http://www.ibm.com/de/profile/
-
-Kontakt: webmaster@de.ibm.com
-Copyright (c) 2002  IBM Deutschland
-
-
-
diff --git a/upstream/t/data/whitelists/infoworld b/upstream/t/data/whitelists/infoworld
deleted file mode 100644 (file)
index 76cb286..0000000
+++ /dev/null
@@ -1,188 +0,0 @@
-From Cringely@bdcimail.com Mon Aug 12 10:58:47 2002
-Return-Path: <bounce-rcringely-0000000@mailcontrol.bellevuedata.com>
-Delivered-To: ffffff@localhost.aaaaaaaaaaaa.net
-Received: from localhost (localhost.localdomain [127.0.0.1])
-       by mail.aaaaaaaaaaaa.net (Postfix) with ESMTP id B3DA1BEEB2
-       for <ffffff@localhost>; Mon, 12 Aug 2002 14:28:07 -0700 (PDT)
-Received: from mail.aaaaaaaaaaaa.com
-       by localhost with IMAP (fetchmail-5.9.11)
-       for ffffff@localhost (single-drop); Mon, 12 Aug 2002 14:28:07 -0700 (PDT)
-Received: from mailcontrol.bellevuedata.com (mailcontrol.bellevuedata.com [66.37.227.18])
-       by mail14.megamailservers.com (8.12.5/8.12.0.Beta10) with SMTP id g7CLKt9N008640
-       for <zzzzzz@aaaaaaaaaaaa.com>; Mon, 12 Aug 2002 17:21:09 -0400 (EDT)
-Date: Mon, 12 Aug 2002 12:58:47 -0500
-From: Cringely@bdcimail.com
-Message-Id: <LISTMANAGERSQL-0000000-32368-2002.08.12-12.58.49--zzzzzz#aaaaaaaaaaaa.com@mailcontrol.bellevuedata.com>
-To: zzzzzz@aaaaaaaaaaaa.com
-Subject: ROBERT X. CRINGELY(R): "Notes from the Field" from InfoWorld.com, Monday, August 12, 2002
-Reply-To: CringelyHelp@Bellevue.com
-Content-Type: text/plain;
-       charset="iso-8859-1"
-X-SpamBouncer: 1.5 (7/17/02)
-X-SBNote: FROM_DAEMON/Listserv
-X-SBPass: No Pattern Matching
-X-SBPass: No Freemail Filtering
-X-SBClass: Bulk
-X-Folder: Bulk
-
-========================================================
-ROBERT X. CRINGELY(R): "Notes from the Field" InfoWorld.com
-========================================================
-
-Monday, August 12, 2002
-
-Advertising Sponsor - - - - - - - - - - - - - - - - - - 
-Business Specials from Gateway
-$100 Instant Rebate on select business notebooks,
-Plus FREE Shipping (LIMITED TIME OFFER)
-or call and ask about wireless networking specials
-for business 1.888.851.7359
-http://63.115.136.15/go/infoworld/4524953.html
-
-- - - - - - - - - - - - - - - - - - - - - - - - - - - - 
-
-LOOKING TO INNOVATE
-
-Posted August 9, 2002 01:01 PM  Pacific Time
-
-
-AMBER FOUND A brochure I had for Kauai, Hawaii. "What
-is this, Cringe? Are you planning to surprise me with
-a trip there?" I just hope she doesn't discover that
-single plane ticket I bought.
-
-Gates reveals the awful truth
-
-At an impromptu meeting over lunch at Microsoft's
-Financial Analysts Day in July, Bill Gates said
-companies are not "super" innovative and don't produce
-reliable products. No earth-shattering revelations
-there, but in illustrating his point he asked, "Do you
-really need the next version of Office? I don't think
-so." Gates' implied he acknowledged users need a
-compelling reason to upgrade to the next Office, my
-spy said. The only thing that sells software these
-days is the innovation factor, Gates claimed. "The
-Tablet is going to be the most viral thing ever,"
-Gates added. Of course, Microsoft has been touting the
-Tablet PC for quite some time.
-
-JavaScript pressure
-
-Microsoft is looking to innovate, however, at least
-when it comes to the European Computer Manufacturers
-Association (ECMA). In terms of extending existing
-scripting languages to support XML there appears to be
-both good and bad news from ECMA, my spy said. The
-good news is BEA recently showed ECMA how to better
-extend scripting languages to work directly with XML.
-The bad news is most people wouldn't recognize the
-group today, which seems hell-bent on replacing
-JavaScript (now called ECMAscript) with a derivative
-that looks a lot like a C# scripting language.
-
-A lack of chivalry
-
-Big Blue is cracking the whip against employee
-tailgaters, but not the variety typically associated
-with college football games. Workers are being
-reminded about a no tailgating policy, which means
-they are forbidden from sliding their ID badge through
-the security system, then holding the door for someone
-who doesn't slide their badge. "We have been
-instructed chivalry is dead concerning this matter,"
-my spy said.
-
-Speaking of chivalry's demise, common courtesy may be
-going with it at the newly merged HP. Despite current
-geopolitical situations, HP is relying on parts of its
-support located in India. In so doing, HP bailed out
-on a relationship with The Answer Group (TAG), with
-which Compaq had a long-standing relationship. Adding
-salt to the wound, though, HP's support folks in India
-were telling customers and resellers about the TAG
-termination before HP even told TAG.
-
-"I BOOKED OUR tickets for Kauai," Amber said. I guess
-I'm trapped now. No more tranquil escape for me.
-
-Before vacation, send tips to cringe@infoworld.com.
-
-
-
-- - - - - - - - - - - - - - - - - - - - - - - - - - - - 
-
-MORE NOTES FROM THE FIELD                                
-For a complete archive of his InfoWorld columns visit   
-http://www2.infoworld.com/cgi/component/columnarchive.wbs?column=notefield
-
-INFOWORLD OPINIONS
-Weekly commentary from the most trusted voices in 
-IT at: http://www.infoworld.com/community/t_opinions.html
-
-
-To join, or start, a discussion on this or any IT-related
-topic, please visit our InfoWorld forums at 
-http://forums.infoworld.com. Here you can interact and 
-exchange ideas with InfoWorld staff and other readers.
-- - - - - - - - - - - - - - - - - - - - - - - - - - - - 
-QUOTE OF THE DAY:
-"Of course, there is a lot of legislation that is
-very favorable to folks that subsist on the exclusive
-ownership of their code. We have lobbied against
-those bills. It's easy to get negative about other
-people's ideas. This is an opportunity to say,
-'Here's a better idea: consider open source as an
-alternative.' "
-
---Jeremy Hogan, community relations manager at Red Hat
-Inc., speaking about a planned march on San Francisco
-city hall to promote the use of open source software
-in government offices.
-
-http://www.infoworld.com/articles/hn/xml/02/08/09/020809hnrally.xml?0812mncr
-
-
-- - - - - - - - - - - - - - - - - - - - - - - - - - - - 
-
-SUBSCRIBE/UNSUBSCRIBE/CHANGE E-MAIL
-To subscribe, unsubscribe or change your e-mail address
-for any of InfoWorld's e-mail newsletters,
-go to:http://www.iwsubscribe.com/newsletters/
-
-To subscribe to InfoWorld.com, or InfoWorld Print,
-or both, or to renew or correct a problem with any InfoWorld
-subscription, go to http://www.iwsubscribe.com
-
-- - - - - - - - - - - - - - - - - - - - - - - - - - - - 
-
-Expectations, Great and Not So Great
-InfoWorld columnist Bob Lewis knows that both are part 
-of the job in IT management. That's what makes his Survival 
-Guide newsletter so fresh, so true, so funny. Do you 
-wonder why you have to manage up as well as down? 
-Or why it matters what Larry Ellison wants and Dubya's
-likely to do? Bob feels your pain. He can help. Subscribe
-to his Survival Guide newsletter free at
-http://www.iwsubscribe.com/newsletters/
-
-
-
-Advertising Sponsor - - - - - - - - - - - - - - - - - - 
-Business Specials from Gateway
-$100 Instant Rebate on select business notebooks,
-Plus FREE Shipping (LIMITED TIME OFFER)
-or call and ask about wireless networking specials
-for business 1.888.851.7359
-http://63.115.136.15/go/infoworld/4524953.html
-
-- - - - - - - - - - - - - - - - - - - - - - - - - - - - 
-
-Copyright 2002 InfoWorld Media Group Inc.
-
-
-
-
-This message was sent to:  zzzzzz@aaaaaaaaaaaa.com
-
-
diff --git a/upstream/t/data/whitelists/linuxplanet b/upstream/t/data/whitelists/linuxplanet
deleted file mode 100644 (file)
index 002b9dc..0000000
+++ /dev/null
@@ -1,197 +0,0 @@
-From listsupport@internet.com Mon Aug 12 12:59:02 2002
-Return-Path: <bounce-linuxplanet-text-000000F@list4.internet.com>
-Delivered-To: ffffffff@localhost.zzzzzzzzzz-ffffffff.net
-Received: from localhost (localhost.localdomain [127.0.0.1])
-       by mail.zzzzzzzzzz-ffffffff.net (Postfix) with ESMTP id 98624BEE9E
-       for <ffffffff@localhost>; Mon, 12 Aug 2002 14:26:24 -0700 (PDT)
-Received: from mail.zzzzzzzzzz-ffffffff.com
-       by localhost with IMAP (fetchmail-5.9.11)
-       for ffffffff@localhost (single-drop); Mon, 12 Aug 2002 14:26:24 -0700 (PDT)
-Received: from mx3.megamailservers.com (ns3.meganameservers.com [64.29.144.65])
-       by mail1.megamailservers.com (8.12.5/8.12.0.Beta10) with ESMTP id g7CKaOs6025662
-       for <lx@zzzzzzzzzz-ffffffff.com>; Mon, 12 Aug 2002 16:36:24 -0400 (EDT)
-Received: from r00l04.lyris.net (r00l04.lyris.net [216.91.57.134])
-       by mx3.megamailservers.com (8.12.2/8.12.2) with SMTP id g7CKaNLC013752
-       for <lx@zzzzzzzzzz-ffffffff.com>; Mon, 12 Aug 2002 16:36:24 -0400
-X-Mailer: Lyris ListManager Web Interface
-Date: Mon, 12 Aug 2002 12:59:02 -0700
-Subject: LinuxPlanet  Newsletter: August 12, 2002
-To: <lx@zzzzzzzzzz-ffffffff.com>
-From: LinuxPlanet <listsupport@internet.com>
-List-Unsubscribe: <mailto:leave-linuxplanet-text-000000F@list4.internet.com>
-Reply-To: Newsletter Support <listsupport@internet.com>
-Message-Id: <INTM-000000F-1929563-2002.08.12-13.35.16--lx#zzzzzzzzzz-ffffffff.com@list4.internet.com>
-X-SpamBouncer: 1.5 (7/17/02)
-X-SBNote: FROM_DAEMON/Listserv
-X-SBPass: No Pattern Matching
-X-SBPass: No Freemail Filtering
-X-SBClass: Bulk
-X-Folder: Bulk
-
-MyDesktop Proudly Presents:
-
-L  I  N  U  X    P L A N E T
-·¸¸·´¯`·¸¸·´¯`·¸¸·´¯`·¸¸·´¯`·¸¸·´¯`·¸¸·
-Your Weekly Source For Linux Updates!
-LinuxPlanet Newsletter for August 12, 2002
-http://www.linuxplanet.com
-
-___________________________ Sponsors ________________________________
-                  This newsletter sponsored by:  
-                             Thawte
-                          Journyx, Inc.
-_____________________________________________________________________
-
------
-IN THIS ISSUE:
-   * NEW AND NOTEWORTHY
-   * COMING UP
-_____
-
-
-/-------------------------------------------------------------------\
-
-FREE Apache SSL Guide from Thawte Certification
-
-Do your online customers demand the best available protection of their
-personal information? Thawte's guide explains how to give this to your
-customers by implementing SSL on your Apache Web Server. Click here to 
-get our FREE Thawte Apache Guide: http://www.gothawte.com/rd348.html
-
-\--------------------------------------------------------------adv.-/
-
-
-NEW AND NOTEWORTHY:
-
-Using the InterMezzo Distributed Filesystem
-<http://www.linuxplanet.com/linuxplanet/reports/4368/1/>
-Getting connected is one of the more vital goals of any IT shop. But what
-happens when users can't get commected to the network right away? Are they
-just cut off altogether from their files? Not necessarily, writes Bill von
-Hagen, especially if you are using the InterMezzo distributed filesystem.
-In this next installment of the Distributed Filesystems series, von Hagen
-examines InterMezzo in detail and shows how to install, configure, and
-implement this DFS.
-
-Building Sounds for your Applications with SoundTracker
-<http://www.linuxplanet.com/linuxplanet/tutorials/4363/1/>
-Beeps, bloops, and buzzes. These are the sounds that enrich our computing
-experience. When done right, these auditory cues provide instant feedback
-to a user from an application. But getting the right sounds for your app
-does not have to involve scrounging around for whatever you can find on
-the Internet. You can professionally edit your own sounds with the Linux
-program SoundTracker, as Dee-Ann LeBlanc and Andrew J.D. Bowman explain in
-this tutorial.
-
-Modern Distributed Filesystems For Linux: An Introduction
-<http://www.linuxplanet.com/linuxplanet/reports/4361/1/>
-Data and information has become the lifeblood of many organizations of
-late, and storing that information safely has led to inventive data
-management. Once known as networked filesystems, distributed filesystems
-are now one of the best ways of storing your data across multiple machines
-on your network. Bill von Hagen begins a series of articles on distributed
-filesystems with an introduction to the technology and what it can do for
-your organization.
-
-
------
-
-COMING UP: 
-
-       * An Open-Source Approach to Fighting Cancer
-       * Distributed File Systems: The Series Continues
-       * A Review of Linux Books
-
------
-
-/-------------------------------------------------------------------\
-*FREE download of Journyx Timesheet for LINUX*
-Have you been looking for an automated solution to 
-replace your paper timesheets? Do you want something 
-that is easy to use and integrates with your existing 
-business applications for payroll, HR, accounting and 
-project management? You need to try Journyx Timesheet! 
-Download Journyx Timesheet for FREE today! 
-http://www.journyx.com/InetL4aug02ezad
-
-\--------------------------------------------------------------adv.-/
-
------
-
-Visit the other sites in the internet.com Linux/Open Source Channel:
-Linux Today <http://www.linuxtoday.com>
-LinuxPlanet <http://www.linuxplanet.com>
-AllLinuxDevices <http://www.alllinuxdevices.com>
-PHPBuilder <http://www.phpbuilder.com>
-BSD Today <http://www.bsdtoday.com>
-Apache Today <http://www.apachetoday.com>
-Enterprise Linux Today <http://www.eltoday.com>
-Linux Central <http://www.linuxcentral.com>
-Linuxnewbie <http://www.linuxnewbie.org>
-The ISP-Linux Moderated Digest
-<http://isp-lists.isp-planet.com/moderated/isp-linux/>.
-
-
-
-
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-DEDICATED EMAIL LIST SERVERS!
-Get the speed, control, and responsiveness you need for your
-out-sourced Email Newsletters at an AFFORDABLE price!  
-100% UPTIME GUARANTEED!
-Sign-up by July 15th and the set-up is FREE for your
-DEDICATED solution just for mentioning this ad.  
-Free Quote: mailto:sales@sparklist.com or surf the 
-website: http://SparkLIST.com/ or direct: 920.490.5901, x1
-
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Advertising: If you are interested in advertising in our newsletters, call 
-Claudia at 1-203-662-2863 or send email to mailto:nsladsales@internet.com
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-For contact information on sales offices worldwide visit 
-http://www.internet.com/mediakit/salescontacts.html
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
-For details on becoming a Commerce Partner, contact David Arganbright
-on 1-203-662-2858 or mailto:commerce-licensing@internet.com 
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
-To learn about other free newsletters offered by internet.com or 
-to change your subscription visit http://e-newsletters.internet.com 
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
-internet.com's network of more than 160 Web sites is organized into 16 
-channels: 
-Internet Technology          http://internet.com/it
-E-Commerce/Marketing         http://internet.com/marketing
-Web Developer                http://internet.com/webdev
-Windows Internet Technology  http://internet.com/win
-Linux/Open Source            http://internet.com/linux
-Internet Resources           http://internet.com/resources
-ISP Resources                http://internet.com/isp
-Internet Lists               http://internet.com/lists
-Download                     http://internet.com/downloads
-International                http://internet.com/international
-Internet News                http://internet.com/news
-Internet Investing           http://internet.com/stocks 
-ASP Resources                http://internet.com/asp
-Wireless Internet            http://internet.com/wireless 
-Career Resources             http://internet.com/careers
-EarthWeb                    http://www.earthweb.com 
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
-To find an answer - http://search.internet.com 
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
-Looking for a job? Filling an opening? - http://jobs.internet.com
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-This newsletter is published by INT Media Group, Incorporated
-http://internet.com - The Internet & IT Network 
-Copyright (c) 2002 INT Media Group, Incorporated. All rights reserved.
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-For information on reprinting or linking to internet.com content: 
-http://internet.com/corporate/permissions.html 
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~   
----
-You are currently subscribed to linuxplanet-text as: lx@zzzzzzzzzz-ffffffff.com
-To unsubscribe send a blank email to leave-linuxplanet-text-000000F@list4.internet.com
-
-
diff --git a/upstream/t/data/whitelists/lp.org b/upstream/t/data/whitelists/lp.org
deleted file mode 100644 (file)
index 51f8d20..0000000
+++ /dev/null
@@ -1,132 +0,0 @@
-Received: from rs6000.resqnet.com (rs6000.resqnet.com [64.209.23.67])
-       by dogma.slashnull.org (8.11.6/8.11.6) with ESMTP id g6PIph423946
-       for <aaaaaa@yyyyyy.zzz>; Thu, 25 Jul 2002 19:51:43 +0100
-Received: from columbia.lp.org (columbia.kia.net [205.252.89.231])
-       by rs6000.resqnet.com (8.11.2/8.11.2) with ESMTP id g6PIoqe17480
-       for <9999999999@kfdjgdkfgjd.com>; Thu, 25 Jul 2002 14:50:52 -0400
-Received: from localhost (daemon@localhost)
-       by columbia.lp.org (8.9.3/8.9.3) with SMTP id OAA51643;
-       Thu, 25 Jul 2002 14:47:50 -0400 (EDT)
-       (envelope-from owner-announce@hq.lp.org)
-Received: by columbia.kia.net (bulk_mailer v1.12); Thu, 25 Jul 2002 12:02:38 -0400
-Received: (from majordom@localhost)
-       by columbia.lp.org (8.9.3/8.9.3) id MAA40103
-       for announce-outgoing; Thu, 25 Jul 2002 12:02:38 -0400 (EDT)
-       (envelope-from owner-announce@hq.lp.org)
-Received: (from lpadmin@localhost)
-       by columbia.lp.org (8.9.3/8.9.3) id MAA40088;
-       Thu, 25 Jul 2002 12:02:37 -0400 (EDT)
-       (envelope-from lpadmin)
-Date: Thu, 25 Jul 2002 12:02:37 -0400 (EDT)
-Message-Id: <200207251602.MAA40088@columbia.lp.org>
-To: announce@hq.lp.org
-Subject: LP RELEASE: Outrageous military spending
-From: Libertarian Party Announcements <owner-announce@lp.org>
-Reply-To: owner-announce@hq.lp.org
-
------BEGIN PGP SIGNED MESSAGE-----
-
-===============================
-NEWS FROM THE LIBERTARIAN PARTY
-2600 Virginia Avenue, NW, Suite 100
-Washington DC 20037
-World Wide Web: http://www.LP.org
-===============================
-For release: July 25, 2002
-===============================
-For additional information:
-George Getz, Press Secretary
-Phone: (202) 333-0008 Ext. 222
-E-Mail: pressreleases@hq.LP.org
-===============================
-
-Thousands spent on strippers, golf memberships
-shows Pentagon spending is out of control, Libertarians say
-
-WASHINGTON, DC -- Quiz question: Which of the following items have been 
-charged to the taxpayers recently by military personnel wielding 
-government-issued credit cards?
-
-(a) $38,000 for lap dancing at strip clubs near military bases.
-
-(b) $3,400 for a Sumo wrestling suit and $9,800 for Halloween costumes.
-
-(c) $7,373 for closing costs on a home and $16,000 for a corporate golf 
-membership.
-
-(d) $4,600 for white beach sand and $19,000 worth of decorative "river 
-rock" at a military base in the Arabian desert.
-
-(e) all of the above.
-
-"Incredibly, the answer is 'all of the above,' said Steve Dasbach, 
-Libertarian Party executive director. "Thanks to the federal 
-government's policy of doling out credit cards with no questions asked, 
-the military has launched a raid on your wallet."
-
-The shocking revelations are contained in a General Accounting Office 
-audit released last week that uncovered $101 million in "seemingly 
-unneeded expenditures" made by the Air Force and Army in 2000 and 
-2001. The purchases were made possible by the federal government's lax 
-credit card policy: At least 1.4 million Defense Department employees 
-carry credit cards, and last year they used them to splurge on $6.1 
-billion in goods and services, the audit found.
-
-In one case, a group of 200 soldiers used their military IDs and 
-government-issued travel cards to get cash at adult-entertainment bars, 
-then spent the money there. The clubs charged a 10 percent fee to 
-supply the soldiers with cash -- then billed the full amount to their 
-travel cards as a restaurant charge, the GAO found.
-
-"Are these warriors really fighting terrorism while frolicking in a 
-strip club, or defending our country while wearing a Sumo wrestling 
-suit?" asked Dasbach. "Americans who support a bigger defense budget, 
-take note: The Pentagon frequently behaves like any other bloated, 
-reckless government agency. It promises your money will be spent on the 
-worthiest of causes, then squanders it on things you could never even 
-imagine."
-
-Other spending uncovered by the audit included $45,000 for luxury 
-cruises, $1,800 for executive pillows, and $24,000 for a sofa and 
-armchair at a military installation in the Middle East, Dasbach noted.
-Some military employees actually defended the purchases, the audit 
-noted, by saying that recreational items such as golf memberships can 
-be "a useful tool for building good relations with a host country" 
-such as Saudi Arabia or the United Arab Emirates. 
-
-Not surprisingly, Dasbach said, the audit found "little evidence of 
-documented disciplinary action" against those who misused the cards, 
-so taxpayers may end up paying the tab.
-  
-"It's time to impose a little military discipline on these deadbeat 
-Defense Department workers, and force them to personally reimburse 
-taxpayers for every penny of improper spending," he said.
-"Then cut the Pentagon's massive $379 billion budget to help guard 
-against such wasteful spending in the future. Perhaps that's one way to 
-force the Pentagon to spend its resources defending the country, 
-instead of offending the taxpayer."
-
-
------BEGIN PGP SIGNATURE-----
-Version: 2.6.2
-
-iQCVAwUBPUA6FdCSe1KnQG7RAQGAKwP/Zpfw0Uq3BPLnXXmnlWQ2aFFb1FSaj+nJ
-QOMt9q4TBhiYJhIdgdd+uGxoubiPfvyIweSR1PjOdoFe8dYf2h/V4gNS9hSmkSgC
-76RZVuitNf2DbEsaY8TtcUDLDC51m/jgxiGcgPkcyJ+0Wn11RRbktkVEefSNTaBz
-M8ibVFiDPyI=
-=9fYc
------END PGP SIGNATURE-----
-
-
-
------------------------------------------------------------------------
-The Libertarian Party                                http://www.lp.org/
-2600 Virginia Ave. NW, Suite 100                    voice: 202-333-0008
-Washington DC 20037                                   fax: 202-333-0072
------------------------------------------------------------------------
-For subscription changes, please use the WWW form at: 
-http://www.lp.org/action/email.html
-
-
diff --git a/upstream/t/data/whitelists/media_unspun b/upstream/t/data/whitelists/media_unspun
deleted file mode 100644 (file)
index 310a23d..0000000
+++ /dev/null
@@ -1,1962 +0,0 @@
-From guterman@mediaunspun.imakenews.net  Wed Aug 14 14:38:59 2002
-Return-Path: <guterman@mediaunspun.imakenews.net>
-Delivered-To: rrrrrrr@localhost.netnoteinc.com
-Received: from localhost (localhost [127.0.0.1])
-       by phobos.labs.netnoteinc.com (Postfix) with ESMTP id 87FA743C34
-       for <rrrrrrr@localhost>; Wed, 14 Aug 2002 09:38:52 -0400 (EDT)
-Received: from phobos [127.0.0.1]
-       by localhost with IMAP (fetchmail-5.9.0)
-       for rrrrrrr@localhost (single-drop); Wed, 14 Aug 2002 14:38:52 +0100 (IST)
-Received: from eng.imakenews.com (mailservice4.imakenews.com
-    [65.214.33.17]) by dogma.slashnull.org (8.11.6/8.11.6) with ESMTP id
-    g7EDZx416820 for <xxxxx@yyyyyy.zzz>; Wed, 14 Aug 2002 14:35:59 +0100
-Received: by eng.imakenews.com (PowerMTA(TM) v1.5); Wed, 14 Aug 2002
-    09:35:04 -0400 (envelope-from <guterman@mediaunspun.imakenews.net>)
-Content-Transfer-Encoding: binary
-Content-Type: multipart/alternative;
-    boundary="----------=_1029331990-31627-4";
-    charset="iso-8859-1"
-Date: Wed, 14 Aug 2002 09:33:10 -0400
-Errors-To: <guterman@mediaunspun.imakenews.net>
-From: "Media Unspun" <guterman@mediaunspun.imakenews.net>
-MIME-Version: 1.0
-Message-Id: <31627$1029331990$mediaunspun$5114587@imakenews.net>
-Precedence: normal
-Reply-To: "Media Unspun" <guterman@vineyard.com>
-Sender: "Media Unspun" <guterman@mediaunspun.imakenews.net>
-Subject: SEC Exposes Big Blue's Pink Slips
-To: xxxxx@yyyyyy.zzz
-X-Imn: mediaunspun,178767,5114587,0
-
-This is a multi-part message in MIME format...
-
-------------=_1029331990-31627-4
-Content-Type: text/plain; charset="iso-8859-1"
-Content-Disposition: inline
-Content-Transfer-Encoding: 7bit
-
-To view this newsletter in full-color, visit:
-http://newsletter.mediaunspun.com/index000018970.cfm
-
-M E D I A  U N S P U N
-What the Press is Reporting and Why (www.mediaunspun.com)
------------------------------------------------------------------
-August 14, 2002
-
------------------------------------------------------------------
-IN THIS ISSUE
------------------------------------------------------------------
-* SEC EXPOSES BIG BLUE'S PINK SLIPS
-* SYNERGY AND BETRAYAL AT VIVENDI
-* OTHER STORIES
-
-Media Unspun serves business news and analysis, authoritatively
-and irreverently, every business day. An annual subscription
-costs $50, less than a dollar a week. If your four-week free
-trial is coming to an end soon, please visit
-http://www.mediaunspun.com/subscribe.html and sign up via credit card 
-or check.
-
-
------------------------------------------------------------------
-ADVERTISEMENT
------------------------------------------------------------------
-Ken Fisher offers his Quarterly Report for high net worth
-investors FREE of cost & without obligation. Access the same
-investment research he uses to guide his clients at:
-http://pcg.fisherinvestments.com/newrespond/letter.asp?site=UNSP&KC=1229EFCAD0000
-
-
------------------------------------------------------------------
-SEC EXPOSES BIG BLUE'S PINK SLIPS
------------------------------------------------------------------
-Does the Securities and Exchange Commission have a press pass
-yet? It seems to be bringing us all our news lately. On the day
-of the deadline for companies to certify their financial
-statements with the SEC, the business press squirmed and waited
-for the next Enron or WorldCom. (We might eat these words
-tomorrow, but we doubt it.) In an unrelated confession, IBM gave 
-the commission its latest layoff numbers.
-
-IBM talked about pink slips during its second-quarter earnings
-report, but with a vagueness worthy of your daily horoscope.
-("Capricorn: Career changes may be on their way...") Only after
-"months of surreptitious layoff notices" did the company admit
-that it's cutting more than 15,600 jobs, said the AP. That's
-about 5% of its workforce, and a lot more than pundits expected. 
-An IBM spokesperson told the Wall Street Journal the higher
-number was due to "rebalancing" and more employees than expected 
-taking voluntary layoffs. 
-
-Sorry, we're still back on "rebalancing." Did IBM "rightsize"
-last quarter, too?
-
-IBM's news was still trickling out Wednesday morning, but some
-details were available. About 1,400 workers got cut from IBM's
-microelectronics unit, and most of the rest were from IT
-services and consulting. (That ought to make IBM's new employees 
-from PricewaterhouseCoopers feel all warm and fuzzy inside.)
-Look for news updates from cities that will see the cuts, such
-as Austin and Raleigh. 
-
-OK, none of this is good. Two years into the tech slump, we're
-still tired of seeing people get sacked. But was it really so
-bad that IBM only revealed it because of new accounting
-regulations? Nah, Big Blue was always known for "stealth
-layoffs," as CNN put it, but current corporate scrutiny forced
-it to 'fess up for once. Until now, IBM would acknowledge the
-latest layoffs if reporters called and asked, but wouldn't give
-specifics. Yeesh. - Jen Muehlbauer
-
-IBM Cut 5% of Staff in Period, Double the Expected Number
-http://online.wsj.com/article/0,,SB1029282408667791835,00.html
-(Paid subscription required.) 
-
-IBM to Cut Over 15,000 Employees (AP)
-http://tinyurl.com/10kz
-
-IBM confirms 15,600 job cuts (Reuters)
-http://www.msnbc.com/news/793777.asp 
-
-IBM cutting 15,000 jobs 
-http://news.com.com/2100-1001-949677.html
-
-IBM job cuts exceed 15,600
-http://money.cnn.com/2002/08/13/technology/ibm/index.htm
-
-IBM puts job cuts at 15,600, with fewer than 50 in this state
-http://seattlepi.nwsource.com/business/82508_ibm14.shtml
-
------------------------------------------------------------------
-ADVERTISEMENT
------------------------------------------------------------------
-You've heard about identity management, but do you know about
-the opportunities and business models that will emerge as a
-result? Download a free executive summary of Esther Dyson's coverage of
-identity management in Release 1.0. Learn more about the
-expanding market for these services and applications.
-http://release1.edventure.com/executivesummary.cfm?MCode=Unspun
-
------------------------------------------------------------------
-SYNERGY AND BETRAYAL AT VIVENDI
------------------------------------------------------------------
-Synergy always was a fuzzy concept. Now Vivendi Universal's top
-man has slammed the lid on it. The French company announced
-today that it's ready to peddle $9.8 billion in assets to rustle 
-up some cash. First up on the block? Synergy-less U.S. book
-publisher Houghton Mifflin. 
-
-It's unclear whether new chairman Jean-Rene Fourtou has genuine
-turnaround muscle, or whether he and Vivendi's board are simply
-following the winds of post-merger fashion. But when you owe
-$18.7 billion, you get real practical, real fast. The Guardian
-reported that Vivendi's share price sank 5% on Tuesday when
-investors got the willies about the company's impending
-announcement on its financial health. But the company had
-positive news to report: It's making money. Revenue in the first 
-half was up 13%, higher than analysts' estimates of a 7.7%
-boost. 
-
-Details are scant on the breadth of Fourtou's restructuring
-efforts, with more information expected at the next board
-meeting on September 25, according to reporters. Houghton
-Mifflin, acquired a year ago for $1.7 billion, and a vague
-explanation that included the "Curious George" character, were
-the only properties named for sale so far. The Guardian
-speculated that Vivendi will also sell its U.S. video games
-business and possibly its stake in the French mobile phone
-company SFR, a debatable sale because of the cash it generates,
-according to the newspaper. 
-
-Meanwhile, Fourtou's predecessor, Jean-Marie Messier, continues
-to advocate empire-building. The New York Post said its sources
-say Messier hopes his former employer will feel generous enough
-to let him continue to reside in his $17 million Manhattan
-abode. And Bloomberg reported earlier this week that an
-unrepentant Messier is penning a memoir as he vacations in the
-Mediterranean. The working title? "How I Was Betrayed."  -
-Deborah Asbrand 
-
-Vivendi to Sell Publisher Houghton Mifflin (Reuters)
-http://www.washingtonpost.com/wp-dyn/articles/A15954-2002Aug14.html
-
-Vivendi investors expect the worst
-http://www.guardian.co.uk/business/story/0,3604,774190,00.html
-
-Vivendi to Sell $9.8 Billion In Assets, Including Houghton
-http://online.wsj.com/article/0,,SB102931297119161715,00.html
-(Paid subscription required.) 
-
-Ousted Messier Aims To Score $17m Vivendi Pad
-http://www.nypost.com/business/54701.htm
-
-Ex-Chief of Vivendi Plans Tell-All Book (Bloomberg)
-http://www.nytimes.com/2002/08/12/business/media/12VIVE.html
-
------------------------------------------------------------------
-OTHER STORIES
------------------------------------------------------------------
-A Top AOL Manager Has Left Company
-http://www.nytimes.com/2002/08/14/technology/14AOL.html
-
-Fed Holds Steady on Interest Rates 
-http://www.washingtonpost.com/wp-dyn/articles/A14636-2002Aug13.html
-
-Amtrak halts all high-speed service after finding cracks
-http://www.sunspot.net/bal-te.train14aug14.story
-
-AOL lets resigning exec keep stock options 
-http://www.usatoday.com/money/industries/technology/2002-08-13-aol-pittman_x.htm
-
-Lucent licensing deal with Winstar focus of probe (AP)
-http://www.bayarea.com/mld/mercurynews/business/3861117.htm
-
-Study Says Net Could Benefit Music Firms
-http://www.latimes.com/business/la-fi-music14aug14.story
-
-Eisner Crimping His Own Style
-http://www.latimes.com/business/la-fi-disney14aug14.story
-
-Severance claims by Enron former execs anger ex-workers
-http://www.chron.com/cs/CDA/story.hts/business/1533657
-
-Princeton removes dean after Yale Web site flap (AP)
-http://www.siliconvalley.com/mld/siliconvalley/3857890.htm
-
-Frisbee golf creator dies, may land on someone's roof (SF
-Chronicle)
-http://seattlepi.nwsource.com/national/82560_frisbee14.shtml
-
-Will Kinsley's Slate Get Wiped?
-http://www.ojr.org/ojr/kramer/1029281360.php
-
-Hollywood, Russian Bicker Over Bass
-http://www.cnn.com/2002/SHOWBIZ/News/08/13/bassspace.hollywood.ap/
-
------------------------------------------------------------------
-Do you want to reach the Net's savviest audience?
-Advertise in Media Unspun.
-Contact Erik Vanderkolk for details at erikvanderkolk@yahoo.com 
-today.
-
------------------------------------------------------------------
-STAFF
------------------------------------------------------------------
-Written by Deborah Asbrand (dasbrand@world.std.com), Keith
-Dawson (dawson@world.std.com), Jen Muehlbauer
-(jen@englishmajor.com), and Lori Patel (loripatel@hotmail.com).
-
-Copyedited by Jim Duffy (jimduffy86@yahoo.com). 
-
-Marketing: Cowpoke Productions (cowpokeproductions.com).
-Advertising: Erik Vanderkolk (erikvanderkolk@yahoo.com). 
-
-Editor and publisher: Jimmy Guterman (guterman@vineyard.com).
-
-Media Unspun is produced by The Vineyard Group Inc. 
-Copyright 2002 Media Unspun, Inc., and The Vineyard Group, Inc.
-Subscribe already, willya? http://www.mediaunspun.com 
-
-Redistribution by email is permitted as long as a link to
-http://newsletter.mediaunspun.com is included.
-
--|________________
-POWERED BY: http://www.imakenews.com
-To be removed from this list, use this link:
-http://www.imakenews.com/eletra/remove.cfm?x=mediaunspun%2Cxxxxx@yyyyyy.zzz
-To receive future messages in HTML format, use this link:
-http://www.imakenews.com/eletra/change.cfm?x=mediaunspun%2Cxxxxx@yyyyyy.zzz%2Chtm
-To change your subscriber information, use this link:
-http://www.imakenews.com/eletra/update.cfm?x=mediaunspun%2Cxxxxx@yyyyyy.zzz
-
-
-------------=_1029331990-31627-4
-Content-Type: text/html; charset="iso-8859-1"
-Content-Disposition: inline
-Content-Transfer-Encoding: 7bit
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-<HTML>
-<HEAD>
-<title>M E D I A  U N S P U N</title>
-
-<!--  
-**********************************************************
-If you can read this message but the rest of the email 
-contains strange characters, your email program is not
-capable of displaying HTML email. Use your browser to read the
-complete newsletter online at: 
-   http://newsletter.mediaunspun.com/
-To receive future messages in plain text format, use this link:
-http://www.imakenews.com/eletra/change.cfm?x=mediaunspun%2Cxxxxx@yyyyyy.zzz%2Ctxt
-
-**********************************************************
-CREATED: August 14, 2002   
--->
-<meta name="description" content="">
-<meta name="keywords" content="">
-<meta name="GENERATOR" content="iMakeNews">
-<meta name="robots" content="ALL">
-
-       
-
-
-<style type="text/css">
-<!--
- .link {color:#000000; text-decoration:   none; }      .link:hover {color:#FF3300; text-decoration:underline;} 
-
-       .g-article_title, .g-article_url, .g-article_full_story,
-       .g-article_printer_link, .g-contents_article_title,
-       .g-topics_topic_title, .g-issue_issue_title, .g-issue_issue_info,
-       .g-survey_results_link, .g-menu_link, .g-letter_summary_title,
-       .g-letter_summary_author, .g-letter_summary_date,
-       .g-letter_summary_location, .g-letter_post, .g-letter_view_title,
-       .g-letter_view_author, .g-letter_view_post, .g-footer_publisher,
-       .g-footer_tellafriend, .g-footer_archive, .g-footer_pdf
-       {color:#000000;text-decoration:none}
-       .g-article_title:hover, .g-article_url:hover,
-       .g-article_full_story:hover, .g-article_printer_link:hover,
-       .g-contents_article_title:hover, .g-topics_topic_title:hover,
-       .g-issue_issue_title:hover, .g-issue_issue_info:hover,
-       .g-survey_results_link:hover, .g-menu_link:hover,
-       .g-letter_summary_title:hover, .g-letter_summary_author:hover,
-       .g-letter_summary_date:hover, .g-letter_summary_location:hover, 
-       .g-letter_post:hover, .g-letter_view_title:hover,
-       .g-letter_view_author:hover, .g-letter_view_post:hover,
-       .g-footer_publisher:hover, .g-footer_tellafriend:hover,
-       .g-footer_archive:hover, .g-footer_pdf:hover
-       {color:#FF0000;text-decoration:underline}
-       
-
--->
-</style>
-
-<!-- Footer Styles -->
-<style type='text/css'>
-<!--
-.a226814927149384-section_heading{color:#FFFFFF;background-color:#000000;font-family:arial;font-size:x-small;font-weight:bold;font-style:normal;text-decoration:none;text-align:left}
-.a226814927149384-footer_publisher{color:#000000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:normal;font-style:normal;text-decoration:none}
-.a226814927149384-footer_publisher:hover{color:#FF0000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:normal;font-style:normal;text-decoration:underline}
-.a226814927149384-footer_copyright{color:#000000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:normal;font-style:normal;text-decoration:none}
-.a226814927149384-footer_disclaimer{color:#000000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:normal;font-style:normal;text-decoration:none}
-.a226814927149384-footer_tellafriend{color:#000000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:bold;font-style:normal;text-decoration:underline}
-.a226814927149384-footer_tellafriend:hover{color:#FF0000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:bold;font-style:normal;text-decoration:underline}
-.a226814927149384-footer_archive{color:#000000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:bold;font-style:normal;text-decoration:none}
-.a226814927149384-footer_archive:hover{color:#FF0000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:bold;font-style:normal;text-decoration:underline}
-.a226814927149384-footer_pdf{color:#000000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:bold;font-style:normal;text-decoration:none}
-.a226814927149384-footer_pdf:hover{color:#FF0000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:bold;font-style:normal;text-decoration:underline}
-
--->
-</style>
-
-
-<!-- Article View Styles -->
-<style type='text/css'>
-<!--
-.a226814927144888-section_heading{color:#FFFFFF;background-color:#000000;font-family:arial;font-size:x-small;font-weight:bold;font-style:normal;text-decoration:none;text-align:left}
-.a226814927144888-contents_article_title{color:#000000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:normal;font-style:normal;text-decoration:underline}
-.a226814927144888-contents_article_title:hover{color:#FF0000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:normal;font-style:normal;text-decoration:underline}
-
--->
-</style>
-
-
-<!-- Article View Styles -->
-<style type='text/css'>
-<!--
-.a226814927144469-section_heading{color:#FFFFFF;background-color:#000000;font-family:arial;font-size:x-small;font-weight:bold;font-style:normal;text-decoration:none;text-align:left}
-.a226814927144469-contents_article_title{color:#000000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:normal;font-style:normal;text-decoration:underline}
-.a226814927144469-contents_article_title:hover{color:#FF0000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:normal;font-style:normal;text-decoration:underline}
-
--->
-</style>
-
-
-<!-- Footer Styles -->
-<style type='text/css'>
-<!--
-.a226814927151492-section_heading{color:#FFFFFF;background-color:#000000;font-family:arial;font-size:x-small;font-weight:bold;font-style:normal;text-decoration:none;text-align:left}
-.a226814927151492-footer_publisher{color:#000000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:normal;font-style:normal;text-decoration:none}
-.a226814927151492-footer_publisher:hover{color:#FF0000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:normal;font-style:normal;text-decoration:underline}
-.a226814927151492-footer_copyright{color:#000000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:normal;font-style:normal;text-decoration:none}
-.a226814927151492-footer_disclaimer{color:#000000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:normal;font-style:normal;text-decoration:none}
-.a226814927151492-footer_tellafriend{color:#000000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:bold;font-style:normal;text-decoration:underline}
-.a226814927151492-footer_tellafriend:hover{color:#FF0000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:bold;font-style:normal;text-decoration:underline}
-.a226814927151492-footer_archive{color:#000000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:bold;font-style:normal;text-decoration:none}
-.a226814927151492-footer_archive:hover{color:#FF0000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:bold;font-style:normal;text-decoration:underline}
-.a226814927151492-footer_pdf{color:#000000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:bold;font-style:normal;text-decoration:none}
-.a226814927151492-footer_pdf:hover{color:#FF0000;background-color:transparent;font-family:verdana;font-size:xx-small;font-weight:bold;font-style:normal;text-decoration:underline}
-
--->
-</style>
-
-</head> 
-<body bgcolor="#EEEEEE" TEXT="#000000" >
- <div align="Left"> <!--IMN:TOP--><table bgcolor="#000000" border="0" cellpadding="1" cellspacing="0" width="650" >
-<tr><td> <table bgcolor="#FFFFFF" border="0" cellpadding="0" cellspacing="0" width="100%" cols="1">
-       <tr><td width="644" valign="top" bgcolor="#FFFFFF"><!-- 1,1:footer -->
-                       
-                               
-                               
-                                
-       
-
-       
-       
-
-
-        
-<table width="100%" border="0" cellspacing="0" cellpadding="4" bgcolor="#FFFFFF">
-
-<tr><td bgcolor="#FFFFFF">
-
-
-       
-       <div align="left">
-       <table border="0" cellpadding="2" cellspacing="0" width="100%" bgcolor="#FFFFFF">
-         <tr>
-           <td>
-        
-               <font face="verdana,arial" size="1">
-                
-               </font>
-        
-               </td>
-        
-                               <td align="right" valign="top">
-                
-                       <font face="verdana,arial" size="1">
-                       
-                               
-                       
-                       
-                       <b>
-                       <a href="http://www.imakenews.com/eletra/mod_input_proc.cfm?mod_name=tell_friend_form&XXDESXXuser=mediaunspun&XXDESXXthanks=Thank%20You%2E&XXDESXXsubject=Check%20this%20out%3A%20%5B%5Btitle%5D%5D&XXDESXXheading=&XXDESXXbackto=http://newsletter.mediaunspun.com/index000018970.cfm&XXDESXXissue_id=18970&XXDESXXtitle=M%20E%20D%20I%20A%20%20U%20N%20S%20P%20U%20N"
-                        class="a226814927149384-footer_tellafriend">
-                       <font size=4>Pass it on...</font></a></b>
-                       
-                       </font>
-                
-                       </td>
-               
-               
-               
-         </tr>
-       </table></div>
-        
-       </td></tr></table>
-       
-
-                               
-       
-       
-
-
-
-
-
-
-
-
-
-
-
-
-       
-
-                       
-                       
-                       <!-- 1,2:header -->
-                       
-                               
-                                       
-                                
-
-
-
-       
-       
-               
-        
-               
-        
-        
-<table width="100%" border="0" cellspacing="0" cellpadding="4" bgcolor="#FFFFFF">
-
-<tr><td bgcolor="#FFFFFF">
-
-
-               
-               <table border="0" cellpadding="1" cellspacing="0" width="100%">
-               
-               <tr><td colspan = 3>
-
-                       
-                       
-                       
-                                               
-                        
-                               
-                                       
-                                               
-                                       
-                               
-                        
-                               <a href="http://www.mediaunspun.com">
-                               
-                               <img src="http://a298.g.akamai.net/7/298/5382/081402092715/www.imakenews.com/mediaunspun/mediaunspun_logo.GIF" BORDER="0" alt="M E D I A  U N S P U N" hspace="6" vspace="1" align="top" width="150" ><br>
-                               
-                               </a>
-                        
-                               
-                                       <em><font face="Arial" size="3">
-                                       
-                                       What the Press is Reporting and Why (<a href="http://www.mediaunspun.com">www.mediaunspun.com</a>)
-                                       
-                                       </font></em>
-                               
-                       
-                       
-               </td></tr>
-               
-               
-               <tr><td colspan="3"><hr noshade size="1"></td></tr>
-                
-               
-                <tr>
-                
-                               <td align="left" width="33%">
-                        
-                               <font face="Verdana, Arial" size="1">
-                               
-                               
-                                       
-                                       
-                                               
-                                                       Wednesday, August 14, 2002
-                                               
-                                       
-                                       
-                                       
-                               </font>
-                       
-                       </td>
-                
-                               <td align="center" width="34%">
-                        
-                       </td>
-                
-                               <td align="right" width="33%">
-                        
-                       </td>
-                
-               </tr>
-               </table>
-                
-       </td></tr></table>
-       
-
-                               
-       
-
-
-
-                       
-                                                       
-                       
-                       
-                       <!-- COLUMN: 1 -->
-               
-               </td></tr></table> <table bgcolor="#FFFFFF" border="0" cellpadding="0" cellspacing="0" width="100%" cols="2">
-       <tr><td width="483" valign="top" bgcolor="#FFFFFF"><!-- 2,1:contents -->
-                       
-                               
-                                
-
-
-
-               
-        
-        
-       
-       
-       
-
-        
-<font face="verdana,arial" size="2"><br><FONT face=Arial size=4><STRONG>Top Spins...</STRONG></FONT></font>
-<table width="100%" border="0" cellspacing="0" cellpadding="4" bgcolor="#FFFFFF">
-
-<tr><td bgcolor="#FFFFFF">
-
-
-       
-       
-       <TABLE bgcolor="#FFFFFF" border="0" cellpadding="3" cellspacing="0" width="100%">
-       
-               
-        
-       <tr bgcolor="#FFFFFF"><td>
-          
-                       
-                       
-                       <font face="Verdana,Arial" size="1">
-                       
-                       
-                       
-                       
-                               <A HREF="#a87727"
-                               
-                                       
-                                               class="a226814927144888-contents_article_title"
-                                       
-                               >
-                       
-                                               
-                               
-                       
-                       SEC Exposes Big Blue's Pink Slips
-                       
-                               
-                       
-                       
-                       
-                       </a></font>
-                       
-                       
-                       
-                       
-                       
-                       <br>
-                </td></tr> 
-               
-       
-       </table>
-        
-       </td></tr></table>
-       
-
-                               
-
-
-
-
-
-                       
-                                       
-                       
-                       
-                       <!-- 2,2:contents -->
-                       
-                               
-                                
-
-
-
-               
-        
-        
-       
-       
-       
-       
-
-        
-<table width="100%" border="0" cellspacing="0" cellpadding="4" bgcolor="#FFFFFF">
-
-<tr><td bgcolor="#FFFFFF">
-
-
-       
-       
-       <TABLE bgcolor="#FFFFFF" border="0" cellpadding="3" cellspacing="0" width="100%">
-       
-                
-        
-       <tr bgcolor="#FFFFFF"><td>
-          
-                       
-                       
-                       <font face="Verdana,Arial" size="1">
-                       
-                       
-                       
-                       
-                               <A HREF="#a87728"
-                               
-                                       
-                                               class="a226814927144469-contents_article_title"
-                                       
-                               >
-                       
-                                               
-                               
-                       
-                       Synergy and Betrayal at Vivendi
-                       
-                               
-                       
-                       
-                       
-                       </a></font>
-                       
-                       
-                       
-                       
-                       
-                       <br>
-                </td></tr> <tr><td><img src="http://www.imakenews.com/eletra/empty.gif" height="1" width="1"></td></tr> 
-               
-        
-       <tr bgcolor="#FFFFFF"><td>
-          
-                       
-                       
-                       <font face="Verdana,Arial" size="1">
-                       
-                       
-                       
-                       
-                               <A HREF="#a87730"
-                               
-                                       
-                                               class="a226814927144469-contents_article_title"
-                                       
-                               >
-                       
-                                               
-                               
-                       
-                       Other Stories
-                       
-                               
-                       
-                       
-                       
-                       </a></font>
-                       
-                       
-                       
-                       
-                       
-                       <br>
-                </td></tr> 
-               
-       
-       </table>
-        
-       </td></tr></table>
-       
-
-                               
-
-
-
-
-
-                       
-                                       
-                       
-                       
-                       <!-- 2,3:article_view -->
-                       
-                               
-                               
-                                 
-
-
-
-               
-        
-        
-       
-       
-        
-<font face="verdana,arial" size="2"><br></font>
-<table width="100%" border="0" cellspacing="0" cellpadding="4" bgcolor="#FFFFFF">
-
-<tr><td bgcolor="#FFFFFF">
-
-
-       
-       
-       <TABLE bgcolor="#FFFFFF" border="0" cellpadding="3" cellspacing="0" width="100%">
-       
-               
-        
-       <tr bgcolor="#FFFFFF"><td>
-        <a name="a66461"></a>   
-                       
-                       
-                       <font face="Arial" size="4"><b>
-                       
-                       
-                       
-                       
-                       
-                       
-                       
-                       
-                       </b>
-                       </font>
-                       
-                       
-                       
-                       
-                       
-                       <br>
-                
-                       
-                
-                       <font face="verdana,arial" size="2">
-                        
-                               
-                                       
-                                        
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-                                        
-               
-               
-        
-       
-       
-
-
-
-                                       
-                                       <P>Media Unspun serves business news and analysis, authoritatively and irreverently, every business day. An annual subscription costs $50, less than a dollar a week. If your four-week free trial is coming to an end soon, please visit <A HREF="http://www.mediaunspun.com/subscribe.html">http://www.mediaunspun.com/subscribe.html</A>  and sign up via credit card or check.<br>
-</P>
-                                       
-                                       <br>
-                               
-                       
-                       </font>
-                </td></tr> 
-               
-       
-       </table>
-        
-       </td></tr></table>
-       
-
-                               
-
-
-
-
-
-
-                               
-                                                                       
-
-                       
-                       
-                       <!-- 2,4:article_view -->
-                       
-                               
-                               
-                                 
-
-
-
-               
-        
-        
-       
-       
-        
-<font face="verdana,arial" size="2"><FONT face=Verdana size=1><STRONG>Sponsor</STRONG></FONT></font>
-<table width="100%" border="0" cellspacing="0" cellpadding="1" bgcolor="#000000"> 
-<tr><td>
-<table width="100%" border="0" cellspacing="0" cellpadding="4" bgcolor="#FFFFCC">
-
-<tr><td bgcolor="#FFFFCC">
-
-
-       
-       
-       <TABLE bgcolor="#FFFFCC" border="0" cellpadding="3" cellspacing="0" width="100%">
-       
-               
-        
-       <tr bgcolor="#FFFFCC"><td>
-        <a name="a59384"></a> 
-                       
-                
-                       <font face="verdana,arial" size="2">
-                        
-                               
-                                       
-                                        
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-                                        
-               
-               
-        
-       
-       
-
-
-
-                                       
-                                       <P>Ken Fisher offers his Quarterly Report for high net worth investors FREE of cost & without obligation. Access the same investment research he uses to guide his clients at:<br>
-<A HREF="http://pcg.fisherinvestments.com/newrespond/letter.asp?site=UNSP&KC=1229EFCAD0000">http://pcg.fisherinvestments.com/newrespond/letter.asp?site=UNSP&KC=1229EFCAD0000</A> </P>
-                                       
-                                       <br>
-                               
-                       
-                       </font>
-                </td></tr> 
-               
-       
-       </table>
-        </td></tr></table>
-       </td></tr></table>
-       
-
-                               
-
-
-
-
-
-
-                               
-                                                                       
-
-                       
-                       
-                       <!-- 2,5:article_view -->
-                       
-                               
-                               
-                                 
-
-
-
-               
-        
-        
-       
-        
-<table width="100%" border="0" cellspacing="0" cellpadding="4" bgcolor="#FFFFFF">
-
-<tr><td bgcolor="#FFFFFF">
-
-
-       
-       
-       <TABLE bgcolor="#FFFFFF" border="0" cellpadding="3" cellspacing="0" width="100%">
-       
-               
-        
-       <tr bgcolor="#FFFFFF"><td>
-        <a name="a87727"></a>   
-                       
-                       
-                       <font face="Arial" size="4"><b>
-                       
-                       
-                       
-                       
-                       
-                       SEC Exposes Big Blue's Pink Slips
-                       
-                       
-                       </b>
-                       </font>
-                       
-                       
-                       
-                       
-                       
-                       <br>
-                
-                       
-                
-                       <font face="verdana,arial" size="2">
-                        
-                               
-                                       
-                                        
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-                                        
-               
-               
-        
-       
-       
-
-
-
-                                       
-                                       <P>Does the Securities and Exchange Commission have a press pass yet? It seems to be bringing us all our news lately. On the day of the deadline for companies to certify their financial statements with the SEC, the business press squirmed and waited for the next Enron or WorldCom. (We might eat these words tomorrow, but we doubt it.) In an unrelated confession, IBM gave the commission its latest layoff numbers.</P><P>
-IBM talked about pink slips during its second-quarter earnings report, but with a vagueness worthy of your daily horoscope. ("Capricorn: Career changes may be on their way...") Only after "months of surreptitious layoff notices" did the company admit that it's cutting more than 15,600 jobs, said the AP. That's about 5% of its workforce, and a lot more than pundits expected. An IBM spokesperson told the Wall Street Journal the higher number was due to "rebalancing" and more employees than
-expected taking voluntary layoffs. </P><P>
-Sorry, we're still back on "rebalancing." Did IBM "rightsize" last quarter, too?</P><P>
-IBM's news was still trickling out Wednesday morning, but some details were available. About 1,400 workers got cut from IBM's microelectronics unit, and most of the rest were from IT services and consulting. (That ought to make IBM's new employees from PricewaterhouseCoopers feel all warm and fuzzy inside.) Look for news updates from cities that will see the cuts, such as Austin and Raleigh. </P><P>
-OK, none of this is good. Two years into the tech slump, we're still tired of seeing people get sacked. But was it really so bad that IBM only revealed it because of new accounting regulations? Nah, Big Blue was always known for "stealth layoffs," as CNN put it, but current corporate scrutiny forced it to 'fess up for once. Until now, IBM would acknowledge the latest layoffs if reporters called and asked, but wouldn't give specifics. Yeesh. - Jen Muehlbauer</P><P>
-IBM Cut 5% of Staff in Period, Double the Expected Number<br>
-<A HREF="http://online.wsj.com/article/0,,SB1029282408667791835,00.html">http://online.wsj.com/article/0,,SB1029282408667791835,00.html</A> <br>
-(Paid subscription required.) </P><P>
-IBM to Cut Over 15,000 Employees (AP)<br>
-<A HREF="http://tinyurl.com/10kz">http://tinyurl.com/10kz</A> </P><P>
-IBM confirms 15,600 job cuts (Reuters)<br>
-<A HREF="http://www.msnbc.com/news/793777.asp">http://www.msnbc.com/news/793777.asp</A>  </P><P>
-IBM cutting 15,000 jobs <br>
-<A HREF="http://news.com.com/2100-1001-949677.html">http://news.com.com/2100-1001-949677.html</A> </P><P>
-IBM job cuts exceed 15,600<br>
-<A HREF="http://money.cnn.com/2002/08/13/technology/ibm/index.htm">http://money.cnn.com/2002/08/13/technology/ibm/index.htm</A> </P><P>
-IBM puts job cuts at 15,600, with fewer than 50 in this state<br>
-<A HREF="http://seattlepi.nwsource.com/business/82508_ibm14.shtml">http://seattlepi.nwsource.com/business/82508_ibm14.shtml</A> </P>
-                                       
-                                       <br>
-                               
-                       
-                       </font>
-                </td></tr> 
-               
-       
-       </table>
-        
-       </td></tr></table>
-       
-
-                               
-
-
-
-
-
-
-                               
-                                                                       
-
-                       
-                       
-                       <!-- 2,6:article_view -->
-                       
-                               
-                               
-                                 
-
-
-
-               
-        
-        
-       
-       
-        
-<font face="verdana,arial" size="2"><FONT face=Verdana size=1><STRONG>Sponsor</STRONG></FONT></font>
-<table width="100%" border="0" cellspacing="0" cellpadding="1" bgcolor="#000000"> 
-<tr><td>
-<table width="100%" border="0" cellspacing="0" cellpadding="4" bgcolor="#FFFFCC">
-
-<tr><td bgcolor="#FFFFCC">
-
-
-       
-       
-       <TABLE bgcolor="#FFFFCC" border="0" cellpadding="3" cellspacing="0" width="100%">
-       
-               
-        
-       <tr bgcolor="#FFFFCC"><td>
-        <a name="a75853"></a> 
-                       
-                
-                       <font face="verdana,arial" size="2">
-                        
-                               
-                                       
-                                       You've heard about identity management, but do you know about the opportunities and business models that will emerge as a result? <a href="http://release1.edventure.com/executivesummary.cfm?MCode=Unspun">Download</a> a free executive summary of Esther Dyson's coverage of identity management in Release 1.0. Learn more about the expanding market for these services and applications.
-                                       
-                                       <br>
-                               
-                       
-                       </font>
-                </td></tr> 
-               
-       
-       </table>
-        </td></tr></table>
-       </td></tr></table>
-       
-
-                               
-
-
-
-
-
-
-                               
-                                                                       
-
-                       
-                       
-                       <!-- 2,7:article_view -->
-                       
-                               
-                               
-                                 
-
-
-
-               
-        
-        
-       
-       
-        
-<table width="100%" border="0" cellspacing="0" cellpadding="4" bgcolor="#FFFFFF">
-
-<tr><td bgcolor="#FFFFFF">
-
-
-       
-       
-       <TABLE bgcolor="#FFFFFF" border="0" cellpadding="3" cellspacing="0" width="100%">
-       
-                
-        
-       <tr bgcolor="#FFFFFF"><td>
-        <a name="a87728"></a>   
-                       
-                       
-                       <font face="Arial" size="4"><b>
-                       
-                       
-                       
-                       
-                       
-                       Synergy and Betrayal at Vivendi
-                       
-                       
-                       </b>
-                       </font>
-                       
-                       
-                       
-                       
-                       
-                       <br>
-                
-                       
-                
-                       <font face="verdana,arial" size="2">
-                        
-                               
-                                       
-                                        
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-                                        
-               
-               
-        
-       
-       
-
-
-
-                                       
-                                       <P>Synergy always was a fuzzy concept. Now Vivendi Universal's top man has slammed the lid on it. The French company announced today that it's ready to peddle $9.8 billion in assets to rustle up some cash. First up on the block? Synergy-less U.S. book publisher Houghton Mifflin. </P><P>
-It's unclear whether new chairman Jean-Rene Fourtou has genuine turnaround muscle, or whether he and Vivendi's board are simply following the winds of post-merger fashion. But when you owe $18.7 billion, you get real practical, real fast. The Guardian reported that Vivendi's share price sank 5% on Tuesday when investors got the willies about the company's impending announcement on its financial health. But the company had positive news to report: It's making money. Revenue in the first half was 
-up 13%, higher than analysts' estimates of a 7.7% boost. </P><P>
-Details are scant on the breadth of Fourtou's restructuring efforts, with more information expected at the next board meeting on September 25, according to reporters. Houghton Mifflin, acquired a year ago for $1.7 billion, and a vague explanation that included the "Curious George" character, were the only properties named for sale so far. The Guardian speculated that Vivendi will also sell its U.S. video games business and possibly its stake in the French mobile phone company SFR, a debatable
-sale because of the cash it generates, according to the newspaper. </P><P>
-Meanwhile, Fourtou's predecessor, Jean-Marie Messier, continues to advocate empire-building. The New York Post said its sources say Messier hopes his former employer will feel generous enough to let him continue to reside in his $17 million Manhattan abode. And Bloomberg reported earlier this week that an unrepentant Messier is penning a memoir as he vacations in the Mediterranean. The working title? "How I Was Betrayed."  - Deborah Asbrand </P><P>
-Vivendi to Sell Publisher Houghton Mifflin (Reuters)<br>
-<A HREF="http://www.washingtonpost.com/wp-dyn/articles/A15954-2002Aug14.html">http://www.washingtonpost.com/wp-dyn/articles/A15954-2002Aug14.html</A> </P><P>
-Vivendi investors expect the worst<br>
-<A HREF="http://www.guardian.co.uk/business/story/0,3604,774190,00.html">http://www.guardian.co.uk/business/story/0,3604,774190,00.html</A> </P><P>
-Vivendi to Sell $9.8 Billion In Assets, Including Houghton<br>
-<A HREF="http://online.wsj.com/article/0,,SB102931297119161715,00.html">http://online.wsj.com/article/0,,SB102931297119161715,00.html</A> <br>
-(Paid subscription required.) </P><P>
-Ousted Messier Aims To Score $17m Vivendi Pad<br>
-<A HREF="http://www.nypost.com/business/54701.htm">http://www.nypost.com/business/54701.htm</A> </P><P>
-Ex-Chief of Vivendi Plans Tell-All Book (Bloomberg)<br>
-<A HREF="http://www.nytimes.com/2002/08/12/business/media/12VIVE.html">http://www.nytimes.com/2002/08/12/business/media/12VIVE.html</A> </P>
-                                       
-                                       <br>
-                               
-                       
-                       </font>
-                </td></tr> 
-               
-        
-       <tr bgcolor="#FFFFFF"><td>
-        <a name="a87730"></a>   
-                       
-                       
-                       <font face="Arial" size="4"><b>
-                       
-                       
-                       
-                       
-                       
-                       Other Stories
-                       
-                       
-                       </b>
-                       </font>
-                       
-                       
-                       
-                       
-                       
-                       <br>
-                
-                       
-                
-                       <font face="verdana,arial" size="2">
-                        
-                               
-                                       
-                                        
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-                                        
-               
-               
-        
-       
-       
-
-
-
-                                       
-                                       <P>A Top AOL Manager Has Left Company<br>
-<A HREF="http://www.nytimes.com/2002/08/14/technology/14AOL.html">http://www.nytimes.com/2002/08/14/technology/14AOL.html</A> </P><P>
-Fed Holds Steady on Interest Rates <br>
-<A HREF="http://www.washingtonpost.com/wp-dyn/articles/A14636-2002Aug13.html">http://www.washingtonpost.com/wp-dyn/articles/A14636-2002Aug13.html</A> </P><P>
-Amtrak halts all high-speed service after finding cracks<br>
-<A HREF="http://www.sunspot.net/bal-te.train14aug14.story">http://www.sunspot.net/bal-te.train14aug14.story</A> </P><P>
-AOL lets resigning exec keep stock options <br>
-<A HREF="http://www.usatoday.com/money/industries/technology/2002-08-13-aol-pittman_x.htm">http://www.usatoday.com/money/industries/technology/2002-08-13-aol-pittman_x.htm</A> </P><P>
-Lucent licensing deal with Winstar focus of probe (AP)<br>
-<A HREF="http://www.bayarea.com/mld/mercurynews/business/3861117.htm">http://www.bayarea.com/mld/mercurynews/business/3861117.htm</A> </P><P>
-Study Says Net Could Benefit Music Firms<br>
-<A HREF="http://www.latimes.com/business/la-fi-music14aug14.story">http://www.latimes.com/business/la-fi-music14aug14.story</A> </P><P>
-Eisner Crimping His Own Style<br>
-<A HREF="http://www.latimes.com/business/la-fi-disney14aug14.story">http://www.latimes.com/business/la-fi-disney14aug14.story</A> </P><P></P><P>
-Severance claims by Enron former execs anger ex-workers<br>
-<A HREF="http://www.chron.com/cs/CDA/story.hts/business/1533657">http://www.chron.com/cs/CDA/story.hts/business/1533657</A> </P><P>
-Princeton removes dean after Yale Web site flap (AP)<br>
-<A HREF="http://www.siliconvalley.com/mld/siliconvalley/3857890.htm">http://www.siliconvalley.com/mld/siliconvalley/3857890.htm</A> </P><P>
-Frisbee golf creator dies, may land on someone's roof (SF Chronicle)<br>
-<A HREF="http://seattlepi.nwsource.com/national/82560_frisbee14.shtml">http://seattlepi.nwsource.com/national/82560_frisbee14.shtml</A> </P><P>
-Will Kinsley's Slate Get Wiped?<br>
-<A HREF="http://www.ojr.org/ojr/kramer/1029281360.php">http://www.ojr.org/ojr/kramer/1029281360.php</A> </P><P>
-Hollywood, Russian Bicker Over Bass<br>
-<A HREF="http://www.cnn.com/2002/SHOWBIZ/News/08/13/bassspace.hollywood.ap/">http://www.cnn.com/2002/SHOWBIZ/News/08/13/bassspace.hollywood.ap/</A> </P>
-                                       
-                                       <br>
-                               
-                       
-                       </font>
-                </td></tr> 
-               
-       
-       </table>
-        
-       </td></tr></table>
-       
-
-                               
-
-
-
-
-
-
-                               
-                                                                       
-
-                       
-                       
-                       <!-- 2,8:article_view -->
-                       
-                               
-                               
-                                 
-
-
-
-               
-        
-        
-       
-       
-        
-<font face="verdana,arial" size="2"><FONT face=Verdana size=1><STRONG>Sponsor</STRONG></FONT></font>
-<table width="100%" border="0" cellspacing="0" cellpadding="1" bgcolor="#000000"> 
-<tr><td>
-<table width="100%" border="0" cellspacing="0" cellpadding="4" bgcolor="#FFFFCC">
-
-<tr><td bgcolor="#FFFFCC">
-
-
-       
-       
-       <TABLE bgcolor="#FFFFCC" border="0" cellpadding="3" cellspacing="0" width="100%">
-       
-               
-        
-       <tr bgcolor="#FFFFCC"><td>
-        <a name="a59804"></a> 
-                       
-                
-                       <font face="verdana,arial" size="2">
-                        
-                               
-                                       
-                                       Do you want to reach the Net's savviest audience?<BR>
-Advertise in Media Unspun.<br>
-Contact Erik Vanderkolk for details at <a href="mailto:erikvanderkolk@yahoo.com">erikvanderkolk@yahoo.com</A> today.
-                                       
-                                       <br>
-                               
-                       
-                       </font>
-                </td></tr> 
-               
-       
-       </table>
-        </td></tr></table>
-       </td></tr></table>
-       
-
-                               
-
-
-
-
-
-
-                               
-                                                                       
-
-                       
-                       
-                       <!-- 2,9:article_view -->
-                       
-                               
-                               
-                                 
-
-
-
-               
-        
-        
-       
-       
-        
-<table width="100%" border="0" cellspacing="0" cellpadding="4" bgcolor="#FFFFFF">
-
-<tr><td bgcolor="#FFFFFF">
-
-
-       
-       
-       <TABLE bgcolor="#FFFFFF" border="0" cellpadding="3" cellspacing="0" width="100%">
-       
-               
-        
-       <tr bgcolor="#FFFFFF"><td>
-        <a name="a59810"></a>   
-                       
-                       
-                       <font face="Arial" size="4"><b>
-                       
-                       
-                       
-                       
-                       
-                       Staff
-                       
-                       
-                       </b>
-                       </font>
-                       
-                       
-                       
-                       
-                       
-                       <br>
-                
-                       
-                
-                       <font face="verdana,arial" size="2">
-                        
-                               
-                                       
-                                       Written by Deborah Asbrand (<a href="mailto:dasbrand@world.std.com">dasbrand@world.std.com</a>), Keith Dawson (<a href="mailto:dawson@world.std.com">dawson@world.std.com</a>), Jen Muehlbauer (<a href="mailto:jen@englishmajor.com">jen@englishmajor.com</a>), and Lori Patel (<a href="mailto:loripatel@hotmail.com">loripatel@hotmail.com</a>).
-<P>
-Copyedited by Jim Duffy (<a href="mailto:jimduffy86@yahoo.com">jimduffy86@yahoo.com</a>).
-<P>
-Marketing: Cowpoke Productions (<a href="http://www.cowpokeproductions.com">cowpokeproductions.com</a>).
-<P>
-Advertising: Erik Vanderkolk (<a href="mailto:erikvanderkolk@yahoo.com">erikvanderkolk@yahoo.com)</a>.
-<P>
-Editor and publisher: Jimmy Guterman (<a href="mailto:guterman@vineyard.com">guterman@vineyard.com</a>).
-<P>
-Media Unspun is produced by <a href="http://guterman.com">The Vineyard Group Inc.</a>
-<BR>Copyright 2002 Media Unspun, Inc., and The Vineyard Group, Inc.
-<BR>Subscribe already, willya? <a href="http://www.mediaunspun.com">http://www.mediaunspun.com</a>
-<P>
-Redistribution by email is permitted as long as a link to <a href="http://newsletter.mediaunspun.com">http://newsletter.mediaunspun.com</a> is included.
-                                       
-                                       <br>
-                               
-                       
-                       </font>
-                </td></tr> 
-               
-       
-       </table>
-        
-       </td></tr></table>
-       
-
-                               
-
-
-
-
-
-
-                               
-                                                                       
-
-                       
-                       
-                       <!-- COLUMN: 2 -->
-               
-               </td><td width="5" valign="top" align="center" ><img src="http://www.imakenews.com/eletra/empty.gif" height="1" width="5"></td>
-                       <td width="1" valign="top" align="center" bgcolor="#888888"><img src="http://www.imakenews.com/eletra/empty.gif" height="1" width="1"></td>
-                       <td width="5" valign="top" align="center" ><img src="http://www.imakenews.com/eletra/empty.gif" height="1" width="5"></td><td width="161" valign="top" bgcolor="#FFFFFF"><!-- 3,1:subscription -->
-                       
-                         
-                         
-                               
-                               
-                                
-
-
-
-       
-       
-<table width="100%" border="0" cellspacing="0" cellpadding="1" bgcolor="#000000"> 
-<tr><td>
-<table width="100%" border="0" cellspacing="0" cellpadding="4" bgcolor="#EEEEEE">
-
-<tr><td
-bgcolor="#000000">
-       <font face="arial" size="2" color="#FFFFFF"><b>
-               
-               SUBSCRIBE
-               
-       </b></font>
-</td></tr>
-<tr><td bgcolor="#EEEEEE">
-
-
-        
-               
-                       
-                       
-                                                                       
-        
-               
-        
-               
-        
-               
-        
-               
-        
-               <form method="POST" action="http://www.imakenews.com/eletra/mod_input_proc.cfm">
-                
-<p><font face="verdana,arial" size="1">
- Enter your email address in the box below to receive a free four-week trial of Media Unspun: 
-</font></p>
-               
-        
-                 <input type="hidden" name="XXDESXXuser" value="mediaunspun">
-                 
-                 
-                 <input type="hidden" name="mod_name" value="subscription">
-                 <input type="hidden" name="XXDESXXfrom_address" value="guterman@vineyard.com">
-          <input type="hidden" name="XXDESXXfrom_name" value="Media Unspun">
-                 
-                 
-                 
-                 
-                 
-                 
-                        <input type="hidden" name="XXDESXXpage" value="http://newsletter.mediaunspun.com/index000018970.cfm">            
-                 
-       
-       
-    
-
-       
-       
-               
-    
-       <p><input type="text" name="XXDESXXemail_address" size="15" maxlength="100">
-               <br><font face="verdana" size="1">
-        
-               
-                       <input type="radio" value="Add" name="XXDESXXsubscribe_op" checked>
-                       
-                       Add
-                       
-               
-        
-               <input type="radio" value="Remove" name="XXDESXXsubscribe_op">
-               
-               Remove<br>
-               
-        
-               <input type="checkbox" name="XXDESXXemail_type" value="htm" checked>
-                Send as HTML<br>
-               
-          
-         <input type="submit" value="Submit" name="add">&nbsp;
-       
-       </font></p>
-    
-
-
-               </form> 
-       
-
- </td></tr></table>
-       </td></tr></table><font face="verdana,arial" size="2"><FONT face=Verdana size=1><STRONG>
-<P align=center><BR>Newsletter Services <BR>Provided by <BR></STRONG></FONT><A href="http://www.imakenews.com/affiliate.cfm?a_id=unspun"><FONT face=Verdana size=1><STRONG>iMakeNews.com</STRONG></FONT></A></P></font>
-       
-
-                                       
-                                       
-                                       
-                       
-                       
-                       <!-- 3,2:survey_view -->
-                       
-                               
-                                
-
-
-
-       
-       
-       
-
-
-
-
-
-
-
-
-
-                                                                                               
-                       
-                       
-                       <!-- 3,3:menu -->
-                       
-                               
-                                
-       
-       
-
-
-
-
-
-
-
-
-                       
-                       
-                       
-                       <!-- COLUMN: 3 -->
-               
-               </td></tr></table> <table bgcolor="#FFFFFF" border="0" cellpadding="0" cellspacing="0" width="100%" cols="1">
-       <tr><td width="644" valign="top" bgcolor="#FFFFFF"><!-- 4,1:footer -->
-                       
-                               
-                               
-                                
-       
-
-       
-       
-
-
-        
-<table width="100%" border="0" cellspacing="0" cellpadding="4" bgcolor="#FFFFFF">
-
-<tr><td bgcolor="#FFFFFF">
-
-
-       
-       <div align="left">
-       <table border="0" cellpadding="2" cellspacing="0" width="100%" bgcolor="#FFFFFF">
-         <tr>
-           <td>
-        
-               <font face="verdana,arial" size="1">
-                
-               </font>
-        
-               </td>
-        
-                               <td align="right" valign="top">
-                
-                       <font face="verdana,arial" size="1">
-                       
-                               
-                       
-                       
-                       <b>
-                       <a href="http://www.imakenews.com/eletra/mod_input_proc.cfm?mod_name=tell_friend_form&XXDESXXuser=mediaunspun&XXDESXXthanks=Thank%20You%2E&XXDESXXsubject=Check%20this%20out%3A%20%5B%5Btitle%5D%5D&XXDESXXheading=&XXDESXXbackto=http://newsletter.mediaunspun.com/index000018970.cfm&XXDESXXissue_id=18970&XXDESXXtitle=M%20E%20D%20I%20A%20%20U%20N%20S%20P%20U%20N"
-                        class="a226814927151492-footer_tellafriend">
-                       <font size=4>TELL A FRIEND</font></a></b>
-                       
-                       </font>
-                
-                       </td>
-               
-               
-               
-         </tr>
-       </table></div>
-        
-       </td></tr></table>
-       
-
-                               
-       
-       
-
-
-
-
-
-
-
-
-
-
-
-
-       
-
-                       
-                       
-                       <!-- COLUMN: 4 -->
-               
-               </td></tr></table> </td></tr></table>
-<!--IMN:BOTTOM-->
-<table border="0" cellpadding="2" cellspacing="0" width="650">
-  <tr><td><font face="verdana,arial" size="1">Powered by <strong><a href="http://www.imakenews.com" target="_top" class="link">iMakeNews.com</a>&#153;</strong></font><br>
-       <font face="verdana,arial" size="1">This email was sent to: xxxxx@yyyyyy.zzz <br><a href="http://www.imakenews.com/eletra/remove.cfm?x=mediaunspun%2Cxxxxx@yyyyyy.zzz">Click here</a> to be instantly removed from this list.<br><a href="http://www.imakenews.com/eletra/change.cfm?x=mediaunspun%2Cxxxxx@yyyyyy.zzz%2Ctxt">Click here</a> to receive future messages in plain text format.<br></font><font face="verdana,arial" size="1"><a
-href="http://www.imakenews.com/eletra/update.cfm?x=mediaunspun%2Cxxxxx@yyyyyy.zzz">Click here</a> to change your subscriber information and preferences.<br></font></tr></table>
-<!--Ver. 7-->
-
-       
-               
-                       
-                
-                
-                 
-               <p>&nbsp;
-                  
-          
-       <img src="http://machina.imakenews.com/E178767,5114587XXmediaunspunXX18970XXXXindex000018970.cfmXXemailXX5114587XXXX0Y0XX1" alt="" height="0" width="0">                 
-          
-          
-               </p>
-               
-       
-
-   
-</div>
-
-
-</body>
-</html>
-
-
-
-------------=_1029331990-31627-4--
-
-
diff --git a/upstream/t/data/whitelists/mlist_mailman_message b/upstream/t/data/whitelists/mlist_mailman_message
deleted file mode 100644 (file)
index 3b97783..0000000
+++ /dev/null
@@ -1,95 +0,0 @@
-Received: from usw-sf-list2.yyyyyyyyyyyy.net (usw-sf-fw2.yyyyyyyyyyyy.net
-     [216.136.171.252]) by dogma.slashnull.org (8.11.6/8.11.6) with ESMTP id
-     g7HFlZ603002 for <zzzzzz-sa@zzzzzz.org>; Sat, 17 Aug 2002 16:47:35 +0100
-Received: from usw-sf-list1-b.yyyyyyyyyyyy.net ([10.3.1.13]
-     helo=usw-sf-list1.yyyyyyyyyyyy.net) by usw-sf-list2.yyyyyyyyyyyy.net with
-     esmtp (Exim 3.31-VA-mm2 #1 (Debian)) id 17g5m8-000654-00; Sat,
-     17 Aug 2002 08:46:04 -0700
-Received: from dogma.slashnull.org ([212.17.35.15]) by
-     usw-sf-list1.yyyyyyyyyyyy.net with esmtp (Exim 3.31-VA-mm2 #1 (Debian)) id
-     17g5lM-0005xL-00 for <SpamAssassin-talk@lists.yyyyyyyyyyyy.net>;
-     Sat, 17 Aug 2002 08:45:16 -0700
-Received: (from apache@localhost) by dogma.slashnull.org (8.11.6/8.11.6)
-     id g7HFj8h02977; Sat, 17 Aug 2002 16:45:08 +0100
-X-Authentication-Warning: dogma.slashnull.org: apache set sender to
-     zzzzzz@zzzzzz.org using -f
-Received: from 194.125.173.146 (SquirrelMail authenticated user zzzzzz) by
-     zzzzzz.org with HTTP; Sat, 17 Aug 2002 16:45:08 +0100 (IST)
-Message-Id: <33025.194.125.173.146.1029599108.squirrel@zzzzzz.org>
-From: "Justin Mason" <zzzzzz@zzzzzz.org>
-To: SpamAssassin-talk@lists.yyyyyyyyyyyy.net
-X-Mailer: SquirrelMail (version 1.0.6)
-MIME-Version: 1.0
-Content-Type: text/plain; charset=iso-8859-1
-Content-Transfer-Encoding: 8bit
-Subject: [SAtalk] spam-phrases existing algo
-Sender: spamassassin-talk-admin@lists.yyyyyyyyyyyy.net
-Errors-To: spamassassin-talk-admin@lists.yyyyyyyyyyyy.net
-X-Beenthere: spamassassin-talk@lists.yyyyyyyyyyyy.net
-X-Mailman-Version: 2.0.9-sf.net
-Precedence: bulk
-List-Help: <mailto:spamassassin-talk-request@lists.yyyyyyyyyyyy.net?subject=help>
-List-Post: <mailto:spamassassin-talk@lists.yyyyyyyyyyyy.net>
-List-Subscribe: <https://lists.yyyyyyyyyyyy.net/lists/listinfo/spamassassin-talk>,
-     <mailto:spamassassin-talk-request@lists.yyyyyyyyyyyy.net?subject=subscribe>
-List-Id: Talk about SpamAssassin <spamassassin-talk.lists.yyyyyyyyyyyy.net>
-List-Unsubscribe: <https://lists.yyyyyyyyyyyy.net/lists/listinfo/spamassassin-talk>,
-     <mailto:spamassassin-talk-request@lists.yyyyyyyyyyyy.net?subject=unsubscribe>
-List-Archive: <http://www.geocrawler.com/redir-sf.php3?list=spamassassin-talk>
-X-Original-Date: Sat, 17 Aug 2002 16:45:08 +0100 (IST)
-Date: Sat, 17 Aug 2002 16:45:08 +0100 (IST)
-
-BTW, I should not that this algorithm Paul Graham uses is
-very close to what we've got in spam-phrases code already.
-
-To turn it into pcode:
-
-  mass-check for spamphrases:
-
-    - get mail body, strip HTML, attachments and mail formatting
-    - strip stopwords ("to", "of", "a" etc.)
-    - find pairs of 3-20 letter words
-    - foreach pair:
-      - skip pair if one word is in stoplist of common terms
-      - ++ the frequency of that word-pair
-
-  settle-phrases -- turn mass-check results into a spamphrases file
-
-    - read all spam word-pairs, let NS = number of word-pairs
-    - read all nonspam word-pairs, let NN = number of word-pairs
-    - let bias = NS / NN (compensates for different corpus size)
-    - foreach nonspam word-pair:
-      - wpfreq = (freq in spam) - (frequency in nonspam * bias)
-    - foreach spam word-pair:
-      - if (wordpair was not found in nonspam):
-        - wpfreq *= 10
-    - note the highest score of all rules
-
-  scoring of an incoming message:
-
-    - get mail body, strip HTML, attachments and mail formatting
-    - strip stopwords ("to", "of", "a" etc.)
-    - find pairs of 3-20 letter words
-    - foreach pair:
-      - score += ((wpfreq*10) / highest_score_of_all_rules)
-    - foreach "!" found in text:
-      - score++
-    - return result as "spam phrase score".
-
-So it's quite close to PG's algo, but he also tracks the non-spam
-word-pairs -- which we don't do for SpamAssassin, because they
-overfit to the mass-checker's nonspam mail corpus (generally
-names of friends, etc.)
-
---j.
-
-
-
--------------------------------------------------------
-This sf.net email is sponsored by: OSDN - Tired of that same old
-cell phone?  Get a new here for FREE!
-https://www.inphonic.com/r.asp?r=yyyyyyyyyyyy&refcode1=vs3390
-_______________________________________________
-Spamassassin-talk mailing list
-Spamassassin-talk@lists.yyyyyyyyyyyy.net
-https://lists.yyyyyyyyyyyy.net/lists/listinfo/spamassassin-talk
diff --git a/upstream/t/data/whitelists/mlist_yahoo_groups_message b/upstream/t/data/whitelists/mlist_yahoo_groups_message
deleted file mode 100644 (file)
index 0e90c9f..0000000
+++ /dev/null
@@ -1,138 +0,0 @@
-Received: from dogma.slashnull.org (dogma.slashnull.org [212.17.35.15])
-        by sonic.xxxxxxxxx.org (Postfix) with ESMTP id 9424D132505
-        for <aaaaaaaa@bbbbbbbbb>; Thu,  1 Aug 2002 14:21:59 -0700 (PDT)
-Received: from intm3.sparklist.com (intm3.sparklist.com [207.250.144.9])
-        by dogma.slashnull.org (8.11.6/8.11.6) with SMTP id g71LMw230398
-        for <ffffffffff.com@zzzzzzz.org>; Thu, 1 Aug 2002 22:22:58 +0100
-Date: Thu, 2 May 2002 00:02:49 +1200
-Subject: [Sigiii-l] [InfoInternational] REINBERGER FOUNDATION GIFT
-To: <ffffffffff.com@zzzzzzz.org>
-From: "Biju K Abraham" <InfoInternational@yahoogroups.com>
-Message-Id: <INTM-6516584-3669405-2002.08.01-16.21.51--ffffffffff.com#zzzzzzz.org@list3.zzzzzz.com>
-MIME-Version: 1.0
-Content-type: multipart/alternative; boundary="------=_NextPart_000_0146_01C1F16C.B224C240"
-
-------=_NextPart_000_0146_01C1F16C.B224C240
-Content-Type: text/plain;
-        charset="iso-8859-1"
-Content-Transfer-Encoding: quoted-printable
-
-REINBERGER FOUNDATION GIFT TO=20
-KENT STATE UNIVERSITY SLIS=20
-
-Kent State University's School of Library and Information Science
-received a gift of $240,000 from the Reinberger Foundation of Cleveland
-for the construction of a unique national center dedicated to training libr=
-arians who=20
-specialize in services for children, young adults and school
-librarianship. The gift was announced in anticipation of National
-Library Week (April 14-20).=20
-=20
-"The Children's Resource Center will offer an environment similar to
-achildren's or elementary school library complete with books,
-multimedia, puppets and a storytelling area," said Associate Professor
-Dr. Carolyn S.Brodie, who has built the School of Library and
-Information Science's collection of materials for youth, and is a
-co-recipient of the Reinberger gift. Brodie recently served as chair of
-the 2000 John Newbery Award Committee.=20
-=20
-The Children's Resource Center will be unique among the nation's library
-schools and will serve as a model classroom for library science programs
-for children's librarians. The Center is designed to be much more than a
-university classroom and will include a children's
- resource area that will house more than=20
-5,000 children's books, materials, and resources to
-create a focal point for instruction in children's, young adult, and
-school librarianship.=20
-
-The 1,700-square-foot resource center will also
-include a wireless computer network installed with specialized software
-and other resources used in children's and school libraries. For more
-information contact Megan Harding, (330) 672-0419.=20
-
- - Moderator -
-
-
-------=_NextPart_000_0146_01C1F16C.B224C240
-Content-Type: text/html; charset=US-ASCII
-Content-Transfer-Encoding: 7bit
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
-<HTML><HEAD>
-<META content="text/html; charset=iso-8859-1" http-equiv=Content-Type>
-<META content="MSHTML 5.00.2614.3500" name=GENERATOR>
-<STYLE></STYLE>
-</HEAD>
-<BODY bgColor=#ffffff>
-
-
-<DIV align=center><FONT face=Arial size=2><FONT color=#ff0000 
-size=4><U><STRONG>REINBERGER FOUNDATION GIFT TO 
-</STRONG></U></FONT></FONT></DIV>
-<DIV align=center><FONT face=Arial size=2><FONT color=#ff0000 
-size=4><U><STRONG>KENT STATE UNIVERSITY SLIS </STRONG></U></FONT></FONT></DIV>
-<DIV align=center><FONT face=Arial size=2><FONT color=#ff0000 
-size=4><U><STRONG><BR></STRONG></U></FONT>Kent State University's School of 
-Library and Information Science<BR>received a gift of <STRONG><FONT 
-color=#0000ff>$240,000 </FONT></STRONG>from the <FONT 
-color=#0000ff><STRONG>Reinberger Foundation of Cleveland<BR></STRONG></FONT>for 
-the construction of a unique national center dedicated to training librarians 
-who <BR>specialize in services for children, young adults and 
-school<BR>librarianship. The gift was announced in anticipation of 
-National<BR>Library Week (April 14-20). <BR>&nbsp;<BR>"The Children's Resource 
-Center will offer an environment similar to<BR>achildren's or elementary school 
-library complete with books,<BR>multimedia, puppets and a storytelling area," 
-said Associate Professor<BR>Dr. Carolyn S.Brodie, who has built the School of 
-Library and<BR>Information Science's collection of materials for youth, and is 
-a<BR>co-recipient of the Reinberger gift. Brodie recently served as chair 
-of<BR>the 2000 John Newbery Award Committee. <BR>&nbsp;<BR><FONT 
-color=#0000ff>The Children's Resource Center </FONT>will be unique among the 
-nation's library<BR>schools and will serve as a model classroom for library 
-science programs<BR>for children's librarians. The Center is designed to be much 
-more than a<BR>university classroom and will include a children's</FONT></DIV>
-<DIV align=center><FONT face=Arial size=2>&nbsp;resource area that will house 
-more than </FONT></DIV>
-<DIV align=center><FONT face=Arial size=2>5,000 children's books, materials, and 
-resources to<BR>create a focal point for instruction in children's, young adult, 
-and<BR>school librarianship. </FONT></DIV>
-<DIV align=center><FONT face=Arial size=2></FONT>&nbsp;</DIV>
-<DIV align=center><FONT face=Arial size=2>The 1,700-square-foot resource center 
-will also<BR>include a wireless computer network installed with specialized 
-software<BR>and other resources used in children's and school libraries. For 
-more<BR>information contact Megan Harding, (330) 
-672-0419.&nbsp;<BR></FONT></DIV>
-<DIV align=center><FONT face=Arial><STRONG><FONT color=#ff0000 size=4>&nbsp;- 
-Moderator -<BR></FONT></STRONG></DIV></FONT>
-<br>
-
-<!-- |**|begin egp html banner|**| -->
-
-<table border=0 cellspacing=0 cellpadding=2>
-<tr bgcolor=#FFFFCC>
-<td align=center><font size="-1" color=#003399><b>Yahoo! Groups Sponsor</b></font></td>
-</tr>
-<tr bgcolor=#FFFFFF>
-<td align=center width=470><table border=0 cellpadding=0 cellspacing=0><tr><td align=center><font face=arial size=-2>ADVERTISEMENT</font><br><a href="http://rd.yahoo.com/M=213858.2097561.3556641.1829184/D=egroupweb/S=1705082179:HM/A=763352/R=0/*http://www.classmates.com/index.tf?s=5085" target=_top><img src="http://us.a1.yimg.com/us.yimg.com/a/cl/classmates_com2/bll_lrec1.gif" alt="" width="300" height="250" border="0"></a></td></tr></table></td>
-</tr>
-<tr><td><img alt="" width=1 height=1 src="http://us.adserver.yahoo.com/l?M=213858.2097561.3556641.1829184/D=egroupmail/S=1705082179:HM/A=763352/rand=399788106"></td></tr>
-</table>
-
-<!-- |**|end egp html banner|**| -->
-
-
-<br>
-<tt>
-To unsubscribe from this group, send an email to:<BR>
-InfoInternational-unsubscribe@yahoogroups.com<BR>
-<BR>
-</tt>
-<br>
-
-<br>
-<tt>Your use of Yahoo! Groups is subject to the <a href="http://docs.yahoo.com/info/terms/">Yahoo! Terms of Service</a>.</tt>
-</br>
-
-</BODY></HTML>
-
-------=_NextPart_000_0146_01C1F16C.B224C240--
-
diff --git a/upstream/t/data/whitelists/mypoints b/upstream/t/data/whitelists/mypoints
deleted file mode 100644 (file)
index 5aa1424..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-Return-Path: <mpmail@mpmlbx06.mypoints.com>
-Delivered-To: zzz-zzzzzzz@fffffffff.org
-Received: (qmail 6475 invoked by uid 505); 20 Jun 2002 02:01:31 -0000
-Received: from mpmail@mpmlbx06.mypoints.com by zzzzzz.fffffffff.org by uid 502 with qmail-scanner-1.12 (F-PROT: 3.12. Clear:. Processed in 0.243337 secs); 20 Jun 2002 02:01:31 -0000
-Received: from mpmlbx06.mypoints.com (216.33.87.173)
-  by dsl092-072-213.bos1.dsl.speakeasy.net with SMTP; 20 Jun 2002 02:01:30 -0000
-Received: (from mpmail@localhost)
-        by mpmlbx06 (8.11.0/8.11.0) id g5K1onT23615;
-        Wed, 19 Jun 2002 20:50:49
-Date: Wed, 19 Jun 2002 20:50:49
-Message-ID: <2002619205049.g5K1onT23615@mpmlbx06>
-To: zzz-zzzzzzz@fffffffff.org
-From: BonusMail from MyPoints <mpmail@mpmlbx06.mypoints.com>
-Reply-To: BonusMailReply@mypoints.com
-Subject: New Deals Just Added! Massive Sheet Liquidation--Now Save up to 84%! 
-X-Indiv: y6f6f69de10d97c7a932zzc3902bf5331
-X-JobID: 107974
-MIME-Version: 1.0
-Content-Type: text/html;charset=us-ascii
-Content-Transfer-Encoding: 7bit
-
-[MyPoints newsletter]
diff --git a/upstream/t/data/whitelists/neat_net_tricks b/upstream/t/data/whitelists/neat_net_tricks
deleted file mode 100644 (file)
index 8575c8b..0000000
+++ /dev/null
@@ -1,205 +0,0 @@
-From bounce-neatnettricks-1234567@silver.lyris.net  Thu Aug 15 10:51:10 2002
-Return-Path: <bounce-neatnettricks-1234567@silver.lyris.net>
-Delivered-To: aaa@localhost.netnoteinc.com
-Received: from localhost (localhost [127.0.0.1])
-       by phobos.labs.netnoteinc.com (Postfix) with ESMTP id 8448D43C4F
-       for <aaa@localhost>; Thu, 15 Aug 2002 05:49:35 -0400 (EDT)
-Received: from phobos [127.0.0.1]
-       by localhost with IMAP (fetchmail-5.9.0)
-       for aaa@localhost (single-drop); Thu, 15 Aug 2002 10:49:35 +0100 (IST)
-Received: from silver.lyris.net (silver.lyris.net [216.91.57.32]) by
-    dogma.slashnull.org (8.11.6/8.11.6) with SMTP id g7ENU3408604 for
-    <aaaaaa@yyyyyy.zzz>; Thu, 15 Aug 2002 00:30:03 +0100
-Message-Id: <LYRIS-1234567-1370323-2002.08.14-16.20.02--aaaaaa@yyyyyy.zzz@silver.lyris.net>
-X-Sender: jteems@rap.midco.net@pop.midco.net
-X-Mailer: QUALCOMM Windows Eudora Version 5.1
-Date: Wed, 14 Aug 2002 16:20:00 -0700
-To: aaaaaa@yyyyyy.zzz
-From: NNT@silver.lyris.net
-Subject: Neat Net Tricks Standard Issue 131 - August 15, 2002
-MIME-Version: 1.0
-Content-Type: text/plain; charset="us-ascii"; format=flowed
-List-Unsubscribe: <mailto:leave-neatnettricks-1234567K@silver.lyris.net>
-Reply-To: NNT@silver.lyris.net
-X-Pyzor: Reported 0 times.
-
-IN THIS ISSUE:
-
-01. Secure IE
-02. Disk cleanup on XP
-03. Thanks but no thanks
-04. Mouseless way home
-05. Kartoo
-06. XP time feature
-07. Leaf Peeper Alert
-08. Saving scraps
-09. Quick access
-10. Don't believe your email
-
-And What's Coming Up Next Week in NNT Premium
-
-01. SECURE IE.  For the past month or so, our Software Review Panel has 
-been giving a grueling test to Secure IE, a piece of software that blocks 
-Flash and pop-ups, prevents malicious file downloads, lets you customize 
-Security Zone settings as you browse dozens of Web sites simultaneously up 
-to five times faster with a tabbed interface, annotate Web page with sticky 
-notes and highlighter, and save complete Web pages, even secure server 
-(HTTPS) pages and archive online transaction receipts.  For what we believe 
-is one of the most thorough reviews ever conducted on a single piece of 
-software, check out what our Panel had to say at 
-http://www.NeatNetTricks.com/SoftwareReviews .  For a free trial download, 
-visit http://www.secureie.com .  And if this one sweeps you off your feet, 
-go to the NNT Store at http://www.NeatNetTricks.com/store and get a super 
-deal, just $17.50 if you order before September 14.  With a 30-day 
-guarantee, what's to lose?
-
-02. DISK CLEANUP ON XP.  Windows XP users can do some fast cleaning chores 
-with Disk Cleanup.  Access this tool from the Start menu by right clicking 
-on your hard drive.  Then select Properties and click on the Disk Cleanup 
-button to determine how many files can be safely deleted.
-
-03. THANKS BUT NO THANKS.  Continuing our periodic feature of 
-less-than-useful sites on the Web:  The International Center for Bathroom 
-Etiquette at http://www.icbe.org/ gets our recognition this issue.  It says 
-it's working hard to bring you the latest in cutting edge research and 
-technology regarding bathroom etiquette.  We'll resist some obvious puns.
-
-. . . . .
-
-West Virginia's Diane Stratton recommended NNT to some friends and is now 
-enjoying QuicKeys 2.0, a great Windows management software package.  Diane 
-is our latest winner and you could be next.  Just go to the NNT Web site at 
-http://www.NeatNetTricks.com and click on "Recommend NNT."  Nothing could 
-be easier.
-
-. . . . .
-
-04. MOUSELESS WAY HOME.  To go home quickly in Internet Explorer, touch F6 
-to highlight the Address Bar and type two periods (..) there.  The Enter 
-key then takes you home.
-
-05. KARTOO.  Search engines are everywhere on the Internet but Kartoo at 
-http://www.kartoo.net is quite unusual.  It's a meta search engine that 
-displays results on a map in the form of a ball.  The larger the ball, the 
-more relevant the result.  As you mouseover each result, site descriptions 
-are revealed.  If all that sounds confusing, the explanation is more 
-complicated than the service itself.  Just try it.
-
-. . . . .
-
-You should make it a habit to visit the NNT Store at 
-http://www.NeatNetTricks.com/store . We try to have several great products 
-there at a limited-time price much less than anywhere else on the Net. 
-Currently, you'll find excellent ebooks, a very effective popup stopper, 
-and the very useful utility described in item 01 above, along with the 
-usual opportunity to subscribe to NNT Premium and ArchivesExpress.  Check 
-us out, you'll be glad you did.
-
-. . . . .
-
-06. XP TIME FEATURE.  Windows XP added a nice feature that heretofore 
-required a separate software application.  It will connect, either at a 
-programmed time or manually, to a time server via the Internet and reset 
-that often erroneous internal clock.  Just click on the time in the systems 
-tray, go to Date and Time Properties and click the Internet Time tab.
-
-07. LEAF PEEPER ALERT.  A bit early, but soon the changing colors of autumn 
-will begin here in the U.S.  For those who like to follow the display, 
-consider http://www.stormfax.com/foliage.htm for a comprehensive collection 
-of links and toll-free numbers to each state to determine peak color times.
-
-08. SAVINGS SCRAPS.  Some oldies are worth repeating.  If you're working 
-with text in, for example, MS Word or WordPad, and would like a handy way 
-to save a portion for later easy retrieval, just select (highlight) it and 
-click/drag it to your desktop.  When the newly created icon is clicked on, 
-it will show the application with which the scrap was created, along with 
-the first few words of the text.  A double click opens the text in the 
-application with which it was created.
-
-09. QUICK ACCESS.  To go to a frequently used program, you may find 
-yourself drilling down to the desktop and searching out the shortcut 
-icon.  Consider instead setting up a key combination that will provide 
-quick access without using the shortcut. Right click on the shortcut and 
-select Properties.  In the Shortcut key window, select any key you can 
-remember and click OK.  Ctrl+Alt and that key will open the application 
-whenever needed.
-
-10.  DON'T BELIEVE YOUR EMAIL.  We've been asked about those emails with 
-virus attachments that appear to be coming from NNT, asking for a 
-confirmation of a subscription.  Don't believe it, and don't open the 
-attachments.  Maybe this exchange between NNT and Lyris (our mail manager) 
-will clear things up:
-
-NNT: Is there anything that can be done about the current strains of virus 
-that implant on address books and randomly send email asking alleged 
-subscribers to verify their subscription (when they haven't subscribed at 
-all)? I've received some of these as well, and I know it's become a 
-widespread problem with other ezine publishers, creating a lot of ill will 
-all around.  Is there some configuration that we could change to keep 
-these from going out except to legitimate subscribers?
-
-Lyris:  Unfortunately, there isn't much we can do about this since it 
-isn't actually ListManager doing the sending.  The real problem is that 
-people sometimes will add the "join" address to their address book and 
-that is what causes the problems.  The best we can do is advise people not 
-to have their address books set to automatically add any email address 
-that they send a message to even once.  The main culprit here seems to be 
-Outlook Express.
-
-WHAT'S COMING NEXT WEEK:  Another batch of Neat Net Tricks in the Premium 
-issue, including:
-
-*  A great collection of information and free downloads to make your system 
-more secure.
-
-*  Free software to measure and display your real-time Internet speed.
-
-*  Can you handle still another popup stopper - that's interference free - 
-and at no cost?
-
-*  An easy-to-use tool to store and arrange all your passwords, user IDs, 
-and other info.
-
-*  A Microsoft Word tip to easily work around that pesky autocompletion 
-feature.
-
-*  A whole arsenal of tools to combat spam.
-
-*  Software that provides more than 200 interesting facts about your 
-computer and displays about your CPU, memory, operating system, and your 
-computer's power source.
-
-*  Our in-depth article discusses how to best manage our important 
-passwords and get out of trouble when we -inevitably - forget those passwords.
-
-And more!  If you haven't subscribed yet, you won't find a better source of 
-useful information for just 42 cents per issue.  That's $10 for a year's 
-worth - 24 issues - at the NNT Store, http://www.NeatNetTricks.com/store .
-
-. . . . . .
-
-NNT makes no endorsement or warranty, expressed or implied, with regard to 
-featured products or services. Results may vary based on operating systems 
-and other variables beyond our control.
-
-For info on how to subscribe, unsubscribe, or change your address, send a 
-blank email to info-neatnettricks@silver.lyris.net .
-
-Sponsor an entire issue of NNT with your exclusive message to our readers 
-at very low rates. Send a blank email to 
-advertise-neatnettricks@silver.lyris.net .
-
-Comments or questions about your computer and the Internet? Visit the NNT 
-Bulletin Board at http://www.escribe.com/computing/neatnettricks/bb/ .
-
-NNT is hosted by Lyris.com, the best in email list management.
-
-Copyright 2002 by Jack Teems. All rights reserved. Neat Net Tricks is 
-registered with the U.S. Library of Congress ISSN: 1533-4619. 
-
-
----
-You are currently subscribed to neatnettricks as: aaaaaa@yyyyyy.zzz
-To unsubscribe send a blank email to leave-neatnettricks-1234567K@silver.lyris.net
-
-
diff --git a/upstream/t/data/whitelists/netcenter-direct_de b/upstream/t/data/whitelists/netcenter-direct_de
deleted file mode 100644 (file)
index 3eacb2b..0000000
+++ /dev/null
@@ -1,414 +0,0 @@
-Return-Path: <dms-errors@dms.netcenter.com>
-Received: (qmail 3387 invoked by alias); 15 Jul 2002 20:26:49 -0000
-Received: (qmail 26987 invoked by uid 82); 15 Jul 2002 20:23:30 -0000
-Received: from dms-errors@dms.netcenter.com by mailhost with qmail-scanner-1.00 (uvscan: v4.1.40/v4212. . Clean. Processed in 5.813084 secs); 15 Jul 2002 20:23:30 -0000
-Received: from dms-mail02.netcenter.com (207.200.87.32)
-  by mi-1.rz.ruhr-uni-bochum.de with SMTP; 15 Jul 2002 20:23:20 -0000
-Received: from dms-www1.netscape.com (dms-mailcaster-s07.netcenter.com) by dms-mail02.netcenter.com (LSMTP for Windows NT v1.1b) with SMTP id <8.00007AB9@dms-mail02.netcenter.com>; Mon, 15 Jul 2002 13:22:22 -0700
-To: xxxxx.yyyyy@ruhr-uni-bochum.de
-Subject: Netscape News - Ausgabe Juli
-From: Netscape <netcenter-direct@dms.netcenter.com>
-Date: Mon, 15 Jul 2002 13:24:23 -0800
-Reply-To: Netscape <netcenter-direct@dms.netcenter.com>
-Content-Type: multipart/alternative;
- boundary="______BoundaryOfDocument______"
-MIME-Version: 1.0
-Content-Transfer-Encoding: 7bit
-
-This is a multi-part message in MIME format.
-
---______BoundaryOfDocument______
-Content-Type: text/plain
-Content-Transfer-Encoding: 7bit
-
-Netscape News - Ausgabe Juli
-
-Lieber Netscape-Nutzer,
-
-in dieser Ausgabe:
-
-- Musterverträge, Rechtstipps und mehr
-  Die neuen Netscape Quickfinder liefern Ihnen direkte Links zu
-  Themen und Tools wie Downloads, Rechtstipps, Verträgen und mehr.
-
-- Der neue Women-Channel
-  Nicht nur für Frauen: Die Mode von Morgen, die neusten Trends in
-  puncto Lifestyle, leckere Rezepte und vieles mehr.
-
-- Netscape sucht mit Google
-  Ab sofort bedient sich die Netscape-Suche der Google-Suchengine. So
-  erhalten Sie die besten Suchergebnisse in kürzester Zeit.
-
-- 0190-Dialer gehen um
-  Unseriöse Anbieter von 0190er-Nummern werden immer dreister. Wir zeigen
-  Ihnen, wie Sie sich schützen können.
-
-- Flirten erlaubt
-  Sie fahren als Single in den Urlaub? Sie wollen Spa? Wir zeigen Ihnen
-  die besten Strände für einen heißen Sommer-Flirt!
-
-http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H1b041H2g55Ep6l012000
-WRnwBz
-
-------------------------------------------------------------------
-Netscape respektiert Ihre Online-Arbeitszeit und Ihre
-Privatsphäre. Wenn Sie in Zukunft KEINE E-Mail-Nachrichten
-mehr von Netscape erhalten möchten, klicken Sie auf untenstehenden
-Link.
-HINWEIS:
-KLICKEN SIE NUR AUF DIESEN LINK, WENN SIE IHR ABONNEMENT
-AUCH WIRKLICH BEENDEN MÖCHTEN!
-http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H1d3Qxx41H2g55Ep6l012
-000WRnwBz
-
-Sie sind mit folgender Adresse registriert:[xxxxx.yyyyy@ruhr-uni-bochum.de]
-
---______BoundaryOfDocument______
-Content-Type: text/html
-Content-Transfer-Encoding: 7bit
-
-<html><head>
-
-<meta name="keywords" content="Netscape, Newsletter">
-<meta name="description" content="Netscape, Newsletter, Juli-Ausgabe">
-<meta name="revisit-after" content="3 days">
-<meta name="channel" content="Newsletter"><title>Netscape News, Ausgabe
-Juli</title>
-</head>
-<body marginheight="0" topmargin="0" bgcolor="#ffffff">
-<table cellpadding=0 cellspacing=0 border=0 width=600 align=center>
- <tr>
-  <td><img height=1 border=0 width=121
-src="http://ivw.netscape.de/cgi-bin/ivw/CP/newsletter/index.jsp_0"></td>
-  <td><img src="http://www.netscape.de/img/1p.gif" width=10 height=7
-border=0 alt=""></td>
-  <td><img src="http://www.netscape.de/img/1p.gif" width=105 height=7
-border=0 alt=""></td>
-  <td><img src="http://www.netscape.de/img/1p.gif" width=60 height=7
-border=0 alt=""></td>
-  <td><img src="http://www.netscape.de/img/1p.gif" width=304 height=7
-border=0 alt=""></td>
- </tr>
-    <FORM NAME="searchWidgetForm"
-ACTION="http://cgi.netscape.com/de/cgi-bin/home_search_widget.cgi"
-method="get">
-    <tr>
-        <td valign=top>
-   <A
-href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H1h041H2g55Ep6l
-012000WRnwBz"><img
-src="http://dms-www1.netscape.com/images/nesc_logo_klein.gif" alt="NETSCAPE"
-width=121 height=25 border=0></a></td>
-        <td bgcolor="#000000"><img src="http://www.netscape.de/img/1p.gif"
-width=10 height=1 border=0 alt=""><INPUT TYPE=hidden NAME=engine
-VALUE="0"><INPUT TYPE=hidden NAME=version VALUE=C></td>
-        <td bgcolor="#000000" valign=middle><input type="Text"
-name="searchstring" size="13"></td>
-        <td bgcolor="#000000"><INPUT TYPE=IMAGE NAME=""
-SRC="http://www.netscape.de/img/portal/but_suchen.gif" BORDER=0 WIDTH=50
-HEIGHT=25></td>
-        <td bgcolor="#003366"><img
-src="http://www.netscape.de/img/trenner_header.gif" alt="NETSCAPE" width=15
-height=25 border=0>
-<A
-href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H1g041H2g55Ep6l
-012000WRnwBz"><img src="http://www.netscape.de/img/but_header_mail.gif"
-width=50 height=25 border=0 alt="Mail"></a><img
-src="http://www.netscape.de/img/1p.gif" width=4 height=1>
-<A
-href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H1f041H2g55Ep6l
-012000WRnwBz"><img src="http://www.netscape.de/img/but_header_aim.gif"
-width=115 height=25 border=0 alt="Instant Messenger"></a><img
-src="http://www.netscape.de/img/1p.gif" width=4 height=1><A
-href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H1e041H2g55Ep6l
-012000WRnwBz"><img src="http://www.netscape.de/img/but_header_download.gif"
-width=74 height=25 border=0 alt="Download"></a></td>
-    </tr>
-    </FORM>
- <tr><td colspan=5><img src="http://www.netscape.de/img/1p.gif" width=600
-height=7></td></tr>
-</table>
-
-
-
-<table border="0" width="600" cellspacing="0" cellpadding="0"
-align="center">
-   <tbody><tr><td bgcolor="#cccccc" colspan="13"><img
-src="http://www.netscape.de/img/1p.gif" alt="" border="0"
-height="1"></td></tr>
-   <tr>
-     <td bgcolor="#cccccc" width="1"><img
-src="http://www.netscape.de/img/1p.gif" alt="" border="0"
-width="1"></td><!--spacer width=4-->
-        <td bgcolor="#ffffff" width="4"><img
-src="http://www.netscape.de/img/1p.gif" border="0" alt="" width="4"></td>
-        <td colspan="9">
-         <table width="590" border="0" cellspacing="0" cellpadding="0">
-         <tbody><tr><td colspan="2"><img
-src="http://www.netscape.de/img/1p.gif" border="0" height="10"
-alt=""></td></tr>
-   <tr><td colspan="2" height="60">
-    <table cellpadding="0" cellspacing="0" border="0" width="590"
-align="center">
-    <tbody><tr><td width="590" align="center">
-    <a href="http://ar.atwola.com/link/93131215/aol">
-    <img src="http://ar.atwola.com/image/93131215/aol" alt="Click here to
-visit our advertiser." width="234" height="60" border="0"></a>
-    <img src="http://www.netscape.de/img/1p.gif" width="30" height="1">
-    <a href="http://ar.atwola.com/link/93131216/aol">
-    <img src="http://ar.atwola.com/image/93131216/aol_002.gif" alt="Click
-here to visit our advertiser." width="234" height="60" border="0"></a>
-    </td></tr>
-    </tbody></table>
-   </td></tr>
-         <tr><td colspan="2"><img src="http://www.netscape.de/img/1p.gif"
-border="0" height="5" alt=""></td></tr>
-         <tr><td align="left"><font face="Arial, Helvetica, sans-serif"
-size="6" color="#990000" nowrap="0">
-         <b>Netscape News</b></font></td><td align="right"><font
-face="Arial, Helvetica, sans-serif" size="2" color="#003399">
-            <b>Juli, 2002  </b></font></td></tr>
-         <tr><td colspan="2"><img src="http://www.netscape.de/img/1p.gif"
-border="0" height="5" alt=""></td></tr>
-         </tbody></table>
-         </td>
-         <td bgcolor="#ffffff" width="4"><img
-src="http://www.netscape.de/img/1p.gif" border="0" width="4" alt=""></td>
-         <td bgcolor="#cccccc" width="1"><img
-src="http://www.netscape.de/img/1p.gif" border="0" width="1" alt=""></td>
-   </tr>
-         <tr><td bgcolor="#cccccc" colspan="13"><img
-src="http://www.netscape.de/img/1p.gif" border="0" width="1"
-alt=""></td></tr>
-         <tr align="left">
-            <td bgcolor="#cccccc" rowspan="3" width="1"><img
-src="http://www.netscape.de/img/1p.gif" border="0" width="1" alt=""></td>
-            <td bgcolor="#ffffff" rowspan="3" width="4"><img
-src="http://www.netscape.de/img/1p.gif" border="0" width="4" alt=""></td>
-            <td width="120" align="left" valign="top" rowspan="3">
-   <table width="120" border="0" cellspacing="0" cellpadding="0">
-   <tbody><tr><td><img src="http://www.netscape.de/img/1p.gif" width="120"
-height="5" border="0" alt=""></td></tr>
-   <tr><td>
-
-          <font face="Arial, Helvetica, sans-serif" size="3"
-color="#990000"><b>Highlights</b></font>
-
-  <font face="Arial, Helvetica, sans-serif" size="1" color="#003399"><BR><A
-href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H16041H2g55Ep6l
-012000WRnwBz">0190-Dialer gehen um</a></font><br>
-        <font face="Arial, Helvetica, sans-serif" size="1"
-color="#000000">Die
-Abzocke nimmt kein Ende: Unseriöse Anbieter von 0190er-Nummern lassen sich
-immer wieder neue Tricks einfallen, um die Verbraucher zu schröpfen. Wir
-geben Tipps zur Vorsorge.</font><br><br>
-
-  <font face="Arial, Helvetica, sans-serif" size="1" color="#003399"><A
-href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H15041H2g55Ep6l
-012000WRnwBz">Ab in den Urlaub</a></font><br>
-        <font face="Arial, Helvetica, sans-serif" size="1"
-color="#000000">Noch
-nichts vor im Sommer? Dann ab in den Urlaub! Unsere Last-Minute-Suche findet
-sicher das passende Schnäppchen für Ihren Geldbeutel.</font><br><br>
-
-  <font face="Arial, Helvetica, sans-serif" size="1" color="#003399"><A
-href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H1A041H2g55Ep6l
-012000WRnwBz">Riester-Rente-Special</a></font><br>
-        <font face="Arial, Helvetica, sans-serif" size="1"
-color="#000000">Jeder
-spricht darüber, doch wissen Sie wirklich Bescheid? Wir klären Sie über die
-Vor- und Nachteile der staatlich geförderten Rentenform auf.</font><br><br>
-
-    </td></tr>
-        <tr><td bgcolor="#cccccc"><img
-src="http://www.netscape.de/img/1p.gif" border="0" width="120" height="1"
-alt=""></td></tr>
-  <tr><td bgcolor="#ffffff"><img src="http://www.netscape.de/img/1p.gif"
-border="0" width="120" height="10" alt=""></td></tr>
-  <tr><td>
-
-
-  <center><A
-href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H13041H2g55Ep6l
-012000WRnwBz"><img height="60" alt=""
-src="http://www.netscape.de/content/NS_Newsletter/266366_1025512239640.jpg"
-width="120" align="middle" vspace="7" border="0"></a></center>
-
-     </td></tr>
-  </tbody></table>
-  </td>
-        <td bgcolor="#ffffff" rowspan="3" width="4"><img
-src="http://www.netscape.de/img/1p.gif" border="0" width="4" alt=""></td>
-        <td bgcolor="#cccccc" rowspan="3" width="1"><img
-src="http://www.netscape.de/img/1p.gif" border="0" width="1" alt=""></td>
-        <td bgcolor="#ffffff" rowspan="3" width="4"><img
-src="http://www.netscape.de/img/1p.gif" border="0" width="4" alt=""></td>
-        <td width="292" align="left" valign="top">
-        <img src="http://www.netscape.de/img/1p.gif" width="292" height="5"
-border="0" alt=""><br>
-
-        <font face="Arial, Helvetica, sans-serif" size="3"
-color="#990000"><b>In dieser Ausgabe</b></font>
-        <img src="http://www.netscape.de/img/1p.gif" width="292" height="3"
-border="0" alt=""><br>
-
-
-        <font face="Arial, Helvetica, sans-serif" size="2"
-color="#003399"><b>Zwei starke Partner:<br>Netscape sucht mit
-Google</b></font>
-        <br><font face="Arial, Helvetica, sans-serif" size="2"
-color="#000000">Die
-Netscape-Suche ist jetzt noch effizienter: Sie bedient sich der
-Google-Technologie,
-der zur Zeit besten Such-Engine im Internet. Egal nach was Sie also suchen:
-Netscape und Google liefern Ihnen in kürzester Zeit die Top-Ergebnisse aus
-über 2 Milliarden Webseiten - und das übersichtlich und mit hoher
-Relevanz.<br></font>
-  <font face="Arial, Helvetica, sans-serif" size="1" color="#000000"><A
-href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H12041H2g55Ep6l
-012000WRnwBz">Mehr... </a></font>
-        <br><br>
-
-        <font face="Arial, Helvetica, sans-serif" size="2"
-color="#003399"><b>Nicht nur für Frauen:<br>Der neue
-Women-Channel</b></font>
-        <br><font face="Arial, Helvetica, sans-serif" size="2"
-color="#000000">
-Netscape.de hat Nachwuchs bekommen: Im neuen <A
-href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H11041H2g55Ep6l
-012000WRnwBz">Women-Channel</a>
-finden Sie alles, was das (Frauen-)Herz höher schlagen lässt. Wir verraten
-Ihnen zum Beispiel, was in Sachen Mode in diesem Sommer angesagt ist, zeigen
-Ihnen die neusten Trends in puncto Lifestyle und stellen leckere Rezepte
-für die leichte Sommerküche vor.      <br></font>
-  <font face="Arial, Helvetica, sans-serif" size="1" color="#000000"><A
-href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H10041H2g55Ep6l
-012000WRnwBz">Mehr... </a></font>
-        <br><br>
-
-        </td>
-        <td bgcolor="#ffffff" width="4"><img
-src="http://www.netscape.de/img/1p.gif" border="0" width="4" alt=""></td>
-        <td bgcolor="#cccccc" width="1"><img
-src="http://www.netscape.de/img/1p.gif" border="0" width="1" alt=""></td>
-        <td bgcolor="#ffffff" width="4"><img
-src="http://www.netscape.de/img/1p.gif" border="0" width="4" alt=""></td>
-        <td align="left" valign="top" width="160">
-        <table border="0" cellspacing="0" cellpadding="0" width="160">
-
-        <tbody><tr><td>
-        <A
-href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H0$041H2g55Ep6l
-012000WRnwBz"><img alt="" hspace="5"
-src="http://www.netscape.de/content/NS_Newsletter/266366_1026282512183.jpg"
-width="60" height="60" align="left" vspace="4" border="0"></a>
-        <font face="Arial, Helvetica, sans-serif" size="2"
-color="#003399"><b>Flirten erlaubt</b></font><br>
-        <font face="Arial, Helvetica, sans-serif" size="2"
-color="#000000">Sie fahren als Single in den Urlaub? Sie wollen Spa? Wir
-zeigen Ihnen die besten Strände für einen heißen Sommer-Flirt!<br>
-        <A
-href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H0_041H2g55Ep6l
-012000WRnwBz">Mehr...</a></font> <br>
-        </td></tr>
-
-        <tr><td>
-       <A
-href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H0z041H2g55Ep6l
-012000WRnwBz"><img alt="" hspace="5"
-src="http://www.netscape.de/content/NS_Newsletter/266366_1026282588133.jpg"
-width="60" height="60" align="left" vspace="4" border="0"></a>
-        <font face="Arial, Helvetica, sans-serif" size="2"
-color="#003399"><b>Grußkarten</b></font><br>
-        <font face="Arial, Helvetica, sans-serif" size="2"
-color="#000000">Verschicken Sie Ihre persönlichen Grüße einfach per eMail.
-Das spart Zeit und kostet Sie keinen Pfennig. Jetzt testen!<br>
-        <A
-href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H0y041H2g55Ep6l
-012000WRnwBz">Mehr...</a></font> <br>
-        </td></tr>
-
-        <tr><td bgcolor="#ffffff"><img
-src="http://www.netscape.de/img/1p.gif" border="0" width="158" height="5"
-alt=""></td></tr>
-        <tr><td bgcolor="#cccccc"><img
-src="http://www.netscape.de/img/1p.gif" border="0" width="158" height="1"
-alt=""></td></tr>
-  <tr><td bgcolor="#ffffff"><img src="http://www.netscape.de/img/1p.gif"
-border="0" width="158" height="5" alt=""></td></tr>
-
-  <tr><td><img src="http://www.netscape.de/img/1p.gif" width="160"
-height="15" border="0" alt=""><br>
-  <A
-href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H1m041H2g55Ep6l
-012000WRnwBz"><img
-src="http://www.netscape.de/content/NS_Newsletter/266366_1025512801645.jpg"
-width="120" height="60" hspace="20" alt="" border="0"
-align="middle"></a><br>
-  <img src="http://www.netscape.de/img/1p.gif" width="160" height="20"
-border="0" alt=""></td></tr>
-
-  </tbody></table>
-        </td>
-        <td bgcolor="#ffffff" width="4" rowspan="3"><img
-src="http://www.netscape.de/img/1p.gif" border="0" width="4" alt=""></td>
-        <td bgcolor="#cccccc" width="1" rowspan="3"><img
-src="http://www.netscape.de/img/1p.gif" border="0" width="1" alt=""></td>
-     </tr>
-     <tr><td colspan="5" valign="top" align="left" bgcolor="#cccccc"><img
-src="http://www.netscape.de/img/1p.gif" border="0" width="457" height="1"
-alt=""></td></tr>
-        <tr><td colspan="5" valign="top" align="left">
-
-        <font face="Arial, Helvetica, sans-serif" size="4"
-color="#990000"><b>Musterverträge, Rechtstipps und mehr...</b></font>
-        <img src="http://www.netscape.de/img/1p.gif" width="457" height="5"
-border="0" alt=""><br>
-
-        <font face="Arial, Helvetica, sans-serif" size="2"
-color="#003399"><b>Die neuen Netscape Quick Finder</b></font>
-        <br>
-        <font face="Arial, Helvetica, sans-serif" size="2"
-color="#000000">Das
-Internet stellt eine fast grenzenlose Menge an Informationen bereit. Wer
-hat da noch den Durchblick, vor allem, wenn es schnell gehen soll? Die <A
-href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H1l041H2g55Ep6l
-012000WRnwBz">Netscape Quick Finder</a>
-schaffen Abhilfe: Hier finden Sie direkte Links zu diversen Themen und Tools
-wie Musterverträge, Rechtstipps, Downloadarchiv, Gebrauchtwagenbewertung
-und Jobbörse - um nur einige zu nennen. Schneller geht's wirklich
-nicht!<br></font>
-  <font face="Arial, Helvetica, sans-serif" size="1" color="#000000"><A
-href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H1j041H2g55Ep6l
-012000WRnwBz">Mehr...</a>
-        <br>
-        <br></font>
- <!--de.infonie.atps.kernel.exceptions.ATPSException: getContent could not
-find "/Content/Teaser_Mitte_unten/Element:1/Headline"-->
-     </td>
-        </tr>
-<tr><td bgcolor="#cccccc" colspan="13"><img
-src="http://www.netscape.de/img/pixel.gif" border="0" height="1"
-alt=""></td></tr>
-<tr><td colspan=13><center>
-<font color="#000000" face="Arial, Helvetica, sans-serif" size="1">
-Um den Newsletter abzubestellen, klicken Sie bitte <A
-href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H1d3Qxx41H2g55E
-p6l012000WRnwBz">hier</a> - <br>oder antworten
-Sie einfach auf diese email und schreiben "REMOVE" in die Betreffzeile.<br>
-c 2002 Netscape. Alle Rechte vorbehalten. <A
-href="http://dms-www01.netcenter.com/cgi-bin/gx.cgi/mcp?p=041H1n041H2g55Ep6l
-012000WRnwBz">Nutzungsbedingungen und Datenschutz</a></font>
-      </center></td></tr>
-</tbody></table>
-
-
-</body></html>
---______BoundaryOfDocument______--
-
-
-:
-annmn:[041H2g041H2g55Ep6l012000WRnwBz]
-
-
-
diff --git a/upstream/t/data/whitelists/netsol_renewal b/upstream/t/data/whitelists/netsol_renewal
deleted file mode 100644 (file)
index f8e84e9..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-From nobody@rs.internic.net  Wed Jan 30 09:50:12 2002
-Delivery-Date: Tue, 13 Jun 2000 12:53:06 +0100
-Received: from zzzzzzzzz.yyyy (mail.zzzzzzzzz.yyyy [193.120.211.219])
-        by zzzzzzzzzz.yyyyyyyyyyy.com (8.9.3/8.9.3) with ESMTP id MAA04894
-        for <foooooooo@yyyyyyyyyyy.com>; Tue, 13 Jun 2000 12:53:04 +0100
-Received: from opsmail.internic.net (opsmail.internic.net [198.41.0.91])
-        by zzzzzzzzz.yyyy (8.9.3/8.9.3) with ESMTP id MAA21530
-        for <foooooooo@yyyyyyyyyyy.com>; Tue, 13 Jun 2000 12:53:03 +0100
-Received: from rs.internic.net (bipwww2.lb.internic.net [192.168.120.8])
-        by opsmail.internic.net (8.9.3/8.9.1) with ESMTP id HAA23653
-        for <foooooooo@yyyyyyyyyyy.com>; Tue, 13 Jun 2000 07:52:32 -0400 (EDT)
-Received: (from nobody@localhost)
-          by rs.internic.net (8.9.3/8.8.4)
-          id HAA02994; Tue, 13 Jun 2000 07:52:32 -0400 (EDT)
-Date: Tue, 13 Jun 2000 07:52:32 -0400 (EDT)
-From: Nobody <nobody@internic.net>
-Message-Id: <200006131152.HAA02994@rs.internic.net>
-Reply-to: billing@netsol.com
-To: foooooooo@yyyyyyyyyyy.com
-Subject: Confirmation of yyyyyyyyyyy.com renewal order
-
-Dear Customer,
-
-Congratulations!  Your Web Address (domain name) has been renewed for 
-an extended period.
-
-We will be processing your order within the next 24-48 hours.  Renewal 
-of your domain name is effective on your current expiration date. *
-
-Here is a summary of your order:
-
-  * Domain Name: yyyyyyyyyyy.com
-  * Total:       $70.00
-  * Rebate:      $10.50
-
-*Subject to receipt of complete and accurate information as requested 
-in your renewal registration order.  If you have any questions, visit 
-the FAQ section of our website:
-http://www.networksolutions.com/help/faq-multiyear-rebate.html 
-
-Be sure to visit your website and learn more about products, services 
-and free resources offered by Network Solutions:
-http://www.networksolutions.com/catalog/
-
-Get VeriSign secure encryption on your new website, and you'll give your
-customers the confidence to place orders online.  Request a FREE Guide, 
-"Securing Your Web Site for Business."
-http://www.verisign.com/cgi-bin/go.cgi?a=w000000000000000   
-
-Thank you for renewing the registration of your domain name with Network 
-Solutions!
-
-Sincerely,
-
-Network Solutions, Inc.
-The dot com people (TM)
-
-
diff --git a/upstream/t/data/whitelists/networkworld b/upstream/t/data/whitelists/networkworld
deleted file mode 100644 (file)
index 53adc25..0000000
+++ /dev/null
@@ -1,215 +0,0 @@
-From NSManagement@bdcimail.com Wed Aug 14 15:30:00 2002
-Return-Path: <bounce-nsmanagement-0000000@mailcontrol.bellevuedata.com>
-Delivered-To: ffffffff@localhost.zzzzzzzzzz-ffffffff.net
-Received: from localhost (localhost.localdomain [127.0.0.1])
-       by mail.zzzzzzzzzz-ffffffff.net (Postfix) with ESMTP id 707A9BEE4A
-       for <ffffffff@localhost>; Thu, 15 Aug 2002 06:12:03 -0700 (PDT)
-Received: from mail.zzzzzzzzzz-ffffffff.com
-       by localhost with IMAP (fetchmail-5.9.11)
-       for ffffffff@localhost (single-drop); Thu, 15 Aug 2002 06:12:03 -0700 (PDT)
-Received: from mailcontrol.bellevuedata.com (mailcontrol.bellevuedata.com [66.37.227.18])
-       by mail44.megamailservers.com (8.12.5/8.12.0.Beta10) with SMTP id g7F78WpU002632
-       for <aaaaaaa@zzzzzzzzzz-ffffffff.com>; Thu, 15 Aug 2002 03:11:00 -0400 (EDT)
-X-Mailer: ListManager Web Interface
-Date: Wed, 14 Aug 2002 17:30:00 -0500
-Subject: Combining point products and suites
-To: aaaaaaa@zzzzzzzzzz-ffffffff.com
-From: NW on Network/Systems Management <NSManagement@bdcimail.com>
-Reply-To: Network/Systems Management Help <NWReplies@bellevue.com>
-Message-Id: <LISTMANAGERSQL-0000000-32969-2002.08.14-17.30.06--aaaaaaa#zzzzzzzzzz-ffffffff.com@mailcontrol.bellevuedata.com>
-Content-Type: text/plain;
-       charset="iso-8859-1"
-X-SpamBouncer: 1.5 (7/17/02)
-X-SBNote: FROM_DAEMON/Listserv
-X-SBPass: No Pattern Matching
-X-SBPass: No Freemail Filtering
-X-SBClass: Bulk
-X-Folder: Bulk
-
-NETWORK WORLD FUSION FOCUS: AUDREY RASMUSSEN on 
-NETWORK/SYSTEMS MANAGEMENT
-08/14/02
-Today's focus: Combining point products and suites
-
-Dear Robin Frank,
-
-In this issue:
-
-* Readers who advocate using both point products and suites
-* Links related to Network/Systems Management
-* Featured reader resource
-
-_______________________________________________________________
-This newsletter sponsored by 
-Lucent
-
-Do you want to receive calls while online and not need a second 
-phone line?
-
-Do you want shorter connect times?
-
-Could you benefit from faster uploads?
-For Next-Generation Dial Access, you need V.92.
-
-To learn more, click here for the Lucent Technologies V.92 
-InfoCenter. http://www.nww1.com/go2/lucent_rc.html
-_______________________________________________________________
-A NETWORK WORLD SPECIAL REPORT: BUSINESS CONTINUITY & DISASTER 
-RECOVERY PLANNING
-
-Dr. Jim Metzler of Ashton, Metzler & Associates discusses 
-techniques on how to proactively implement Business Continuity 
-and Disaster Recovery Planning. Sponsored by Syncsort, this 
-SPECIAL REPORT emphasizes both the tactical and strategic 
-considerations necessary for data and infrastructure 
-protection. Download your FREE copy today at: 
-http://nww1.com/go/ad306.html (registration required)
-
-_______________________________________________________________
-Today's focus: Combining point products and suites
-
-By Audrey Rasmussen
-
-Today we'll hear from readers who think the best route to take 
-in the debate between point products and suites is to use a 
-little of both.
-
-One reader commented that a company doesn't have to choose one 
-over the other. He says:
-
-"It has been my experience that a well-managed enterprise 
-monitoring system will most likely include some of both: point 
-solutions to address specific network/systems issues, and a 
-centralized, single pane of glass from a 'framework' system 
-providing a common platform for event and problem management."
-
-Another reader said organizational issues are an important 
-factor. An approach must work within the organizational and 
-political structure of a company:
-
-"The approach with best of breed, plus some integration tools 
-above it, is probably the less risky route - and for less 
-integrated organizations, the better way to go. If you go for 
-an integrated framework, you'd better be sure that you can 
-handle it from an organizational viewpoint; otherwise it could 
-be a hard, expensive landing." 
-
-According to yet another reader, there are other factors that 
-affect the decision on the management approach:
-
-"Mostly this question is answered based on:
-
-* How high up in the organization the decision is being made
-
-* How pragmatically (quick and dirty vs. big and beautiful) 
-  does one want to approach the issue
-
-* How specific the requirements are
-
-* Time of decision
-
-"Each supplier has its rise and fall; the winner of today may 
-be a loser tomorrow. If the different user [administrator] 
-groups have a different timing regarding when they need a tool, 
-they will probably come to different decisions."
-
-Another user says:
-
-"My personal favorite solution involves using vendor-supplied 
-software agents such as IBM Director or Compaq Insight Manager 
-and integrating them into a suite solution such as Tivoli or CA 
-Unicenter. This removes not only the cost of the middleware and 
-integration layers, but also removes the cost of the agent 
-technology." 
-
-So, there you have opinions from readers who embrace point 
-products and suites working together.
-
-_______________________________________________________________
-To contact Audrey Rasmussen:
-
-Audrey Rasmussen is a research director with Enterprise 
-Management Associates in Boulder, Colorado, 
-(http://www.enterprisemanagement.com), a leading analyst 
-and market research firm focusing exclusively on all aspects 
-of enterprise management. Audrey has more than 20 years of 
-experience working with distributed systems, applications 
-and networks. Her current focus at EMA is e-business, SMB/SME 
-and MSPs. She can be reached at:
-mailto:rasmussen@enterprisemanagement.com.
-_______________________________________________________________
-2002 SALARY CALCULATOR
-
-How has the turbulent market affected your earning potential? 
-Find out with Network World's 2002 Salary Calculator. We've 
-updated the Salary Calculator and revised it to reflect the 
-results of the Network World 2002 Salary Survey. Give us some 
-details about yourself and we'll tell you if you earn as much 
-as your peers: http://nww1.com/go/ad324.html
-_______________________________________________________________
-RELATED EDITORIAL LINKS
-
-SLAMming service levels into shape
-Network World, 08/12/02
-http://www.nwfusion.com/news/2002/134753_08-12-2002.html
-
-Archive of the Network/Systems Management newsletter:
-http://www.nwfusion.com/newsletters/nsm/index.html 
-_______________________________________________________________
-If you're concerned about the growing turbulence in the telco 
-industry, you are not alone. The massive financial and 
-organizational changes now underway at many of the largest 
-carriers increase the possibility of service outages, 
-performance degradation and poor operations support. Find out 
-what you can do to mitigate your risks. Attend a free web 
-seminar on the best practices for protecting your business from 
-telco turbulence. Leading industry expert, David Willis of the 
-META Group, will analyze the inevitable consequences of the 
-current environment and share pragmatic steps to shield your 
-users and applications from carrier failures. 
-http://nww1.com/go/4531858a.html
-_______________________________________________________________
-FEATURED READER RESOURCE
-
-NW FUSION'S WHITEPAPERS CENTRAL
-
-A free resource to Network World Fusion visitors is the 
-Whitepaper Central area on NW Fusion. Here you can find vendor 
-and Network World produced whitepapers on a variety of network 
-topics. You can search our whitepapers database by company or 
-by title. All are available free of charge. Visit 
-http://www.nwfusion.com/bg/wp/wpbydate.jsp today.
-_______________________________________________________________
-May We Send You a Free Print Subscription? 
-You've got the technology snapshot of your choice delivered 
-at your fingertips each day. Now, extend your knowledge by 
-receiving 51 FREE issues to our print publication. Apply 
-today at http://www.nwwsubscribe.com/nl
-_______________________________________________________________
-SUBSCRIPTION SERVICES 
-
-To subscribe or unsubscribe to any Network World e-mail 
-newsletters, go to: 
-http://www.nwwsubscribe.com/news/scripts/notprinteditnews.asp 
-
-To unsubscribe from promotional e-mail go to: 
-http://www.nwwsubscribe.com/ep
-
-To change your e-mail address, go to: 
-http://www.nwwsubscribe.com/news/scripts/changeemail.asp 
-
-Subscription questions? Contact Customer Service by replying to 
-this message. 
-
-Have editorial comments? Write Jeff Caruso, Newsletter Editor, 
-at: mailto:jcaruso@nww.com 
-
-For advertising information, write Alonna Doucette, V.P. of 
-Online Development, at: mailto:sponsorships@nwfusion.com
-
-Copyright Network World, Inc., 2002
-
-------------------------
-This message was sent to:  aaaaaaa@zzzzzzzzzz-ffffffff.com
-
-
diff --git a/upstream/t/data/whitelists/oracle_net_techblast b/upstream/t/data/whitelists/oracle_net_techblast
deleted file mode 100644 (file)
index b238316..0000000
+++ /dev/null
@@ -1,554 +0,0 @@
-Return-Path: <replies@oracleeblast.com>
-Received: (qmail 19678 invoked by alias); 10 Jul 2002 13:22:47 -0000
-Received: (qmail 19416 invoked by uid 82); 10 Jul 2002 13:22:42 -0000
-Received: from replies@oracleeblast.com by mailhost with qmail-scanner-1.00 (uvscan: v4.1.40/v4210. . Clean. Processed in 8.59332 secs); 10 Jul 2002 13:22:42 -0000
-Received: from inet-mail6.oracle.com (209.246.10.170)
-  by mi-1.rz.ruhr-uni-bochum.de with SMTP; 10 Jul 2002 13:22:30 -0000
-Received: from blaster-smtp.oracle.com (eblast01.oracleeblast.com [148.87.9.11])
-       by inet-mail6.oracle.com (Switch-2.2.2/Switch-2.2.0) with ESMTP id g6ADMHs25188
-       for XXXXXX.YYYYY@RUHR-UNI-BOCHUM.DE; Wed, 10 Jul 2002 06:22:17 -0700 (PDT)
-Date: Wed, 10 Jul 2002 06:22:17 -0700 (PDT)
-Message-Id: <200207101322.g6ADMHs25188@inet-mail6.oracle.com>
-Subject: Oracle Technology Network TechBlast - July 2002
-From: Oracle Technology Network<replies@oracleeblast.com>
-To: XXXXXX.YYYYY@RUHR-UNI-BOCHUM.DE
-Reply-To: replies@oracleeblast.com
-Content-Transfer-Encoding: 8bit
-MIME-Version: 1.0
-Content-Type: multipart/alternative;
-    boundary="next_part_of_message"
-
---next_part_of_message
-
-
-
-e
-e
-ssage
-Content-type: text/plain; charset=iso-8859-1
-
-
-
---next_part_of_message
-Content-Type: text/html
-
-
-<body bgcolor="#FFFFFF" link="#000000" vlink="#000000">
-<a href="http://otn.oracle.com/index.html" target="_top"><img src="http://otn.oracle.com/otn300x65.gif" width=300 height=65 border=0 alt="Oracle Technology Network" hspace=5 vspace=5></a> 
-<div align="center"><font face="Arial, Helvetica, sans-serif"><b><font size="+2">OTN 
-  TechBlast </font><font size="+1"><br>
-  </font> <i>July 2002 Issue</i></b><font size="2"><br>
-  <font size="1">The monthly TechBlast is also available through the <a href="http://otn.oracle.com/techblast/index.htm">Oracle 
-  Technology Network</a> website.</font></font></font> <br>
-  <div align="left"> 
-    <hr>
-  </div>
-</div>
-<table width="100%" border="0" cellspacing="10" >
-  <tr> 
-    <td valign="top" width="14%" ><font size="2" face="Arial, Helvetica, sans-serif"><b>In 
-      this issue:</b></font> 
-      <table width="100%" border="0" cellspacing="2" cellpadding="0">
-        <tr> 
-          <td><font size="1"><a href="#topnews"><img src="http://otn.oracle.com/images/bullets_and_symbols/red_arrow_bullet_10.gif" width="12" height="13" border="0"></a></font></td>
-          <td><font face="Arial, Helvetica, sans-serif" size="1"><a href="#feature">This 
-            Month's Feature</a></font></td>
-        </tr>
-        <tr> 
-          <td height="12"><font size="1"><a href="#newdownloads"><img src="http://otn.oracle.com/images/bullets_and_symbols/red_arrow_bullet_10.gif" width="12" height="13" border="0"></a></font></td>
-          <td><font face="Arial, Helvetica, sans-serif" size="1"><a href="#news"> 
-            News</a></font></td>
-        </tr>
-        <tr> 
-          <td height="9"><font size="1"><a href="#newdownloads"><img src="http://otn.oracle.com/images/bullets_and_symbols/red_arrow_bullet_10.gif" width="12" height="13" border="0"></a></font></td>
-          <td><font face="Arial, Helvetica, sans-serif" size="1"><a href="#downloads">Software 
-            Downloads</a></font></td>
-        </tr>
-        <tr> 
-          <td><font size="1"><a href="#ou"><img src="http://otn.oracle.com/images/bullets_and_symbols/red_arrow_bullet_10.gif" width="12" height="13" border="0"></a></font></td>
-          <td><font face="Arial, Helvetica, sans-serif" size="1"><a href="#ou">Oracle 
-            University</a></font></td>
-        </tr>
-        <tr> 
-          <td height="2"><font size="1"><a href="#events"><img src="http://otn.oracle.com/images/bullets_and_symbols/red_arrow_bullet_10.gif" width="12" height="13" border="0"></a></font></td>
-          <td><font face="Arial, Helvetica, sans-serif" size="1"><a href="#books">New 
-            Books</a></font></td>
-        </tr>
-        <tr> 
-          <td valign="top"><font size="1"><a href="#ebn"><img src="http://otn.oracle.com/images/bullets_and_symbols/red_arrow_bullet_10.gif" width="12" height="13" border="0"></a></font></td>
-          <td valign="top"> 
-            <p><font size="1" face="Arial, Helvetica, sans-serif" color="#000000">Worldwide 
-              Events: <a href="#americas"><br>
-              Americas</a> | <a href="#emea">EMEA</a> | <a href="#apac">APAC</a></font></p>
-          </td>
-        </tr>
-      </table>
-      <p align="left"><a href="mailto:?subject=OTN%20newsletter%20&body=Interesting%20reading%20from%20the%20Oracle%20Technology%20Network:%20%20http://otn.oracle.com/techblast"><img src="http://otn.oracle.com/techblast/images/email2friend.gif" width="70" height="80" border="0"></a></p>
-    </td>
-    <td valign="top" width="69%" > 
-      <p><font face="Arial, Helvetica, sans-serif"><a name="feature"></a> <b><font size="4"><i>This 
-        Month's Feature: </i></font><font face="Arial, Helvetica, sans-serif" size="4"> 
-        <i>New Developer Services on OTN</i></font></b></font></p>
-      <p><b><font face="Arial, Helvetica, sans-serif" size="2">OTN Members: Get 
-        Oracle Software on CD </font></b><font face="Arial, Helvetica, sans-serif" size="2"><b>Shipped 
-        to you Today!<br>
-        </b> Order <a href="https://www.oracle.com/jsp/otntt/index.jsp">OTN TechTracks</a> 
-        and receive Oracle9i Database Release 2, Oracle9i Application Server Release 
-        2, and Oracle Developer Suite (including JDeveloper) CDs for the platform 
-        of your choice. TechTracks is a one-year subscription, and it includes 
-        access to Oracle Support's KnowledgeBase and CD updates shipped to you 
-        whenever there are major new releases of Oracle software. <i>Enter promo 
-        code OWC for a $50 savings during the month of July</i>.</font></p>
-      <p><font face="Arial, Helvetica, sans-serif" size="2"><b>Exchange your Knowledge 
-        through OTN Community Code Services<br>
-        </b><a href="http://otncast.otnxchange.oracle.com/">OTN Community Code</a> 
-        is a web-browsable CVS repository that lets you review, customize, extend, 
-        and share Oracle-related code and coding techniques. OTN populated it 
-        with sample application projects, so that you can view sample code source 
-        online, download it, submit bugs and suggestions to the development teams, 
-        and get email notifications when code is updated. Participate in an Oracle-sponsored 
-        project, and then create your own project and share your code with the 
-        OTN community.</font></p>
-      <p><b><font size="2" face="Arial, Helvetica, sans-serif">Web Services Center 
-        Now Available on OTN</font></b><font size="2" face="Arial, Helvetica, sans-serif"><br>
-        The <a href="http://otn.oracle.com/tech/webservices/">OTN Web Services 
-        Center</a> is a new resource for the development and deployment of Web 
-        services. Visitors to this new Center can experience live Web service 
-        examples, access the latest Web services technical information, and build 
-        their own Web services using <a href="http://otn.oracle.com/products/jdev/content.html">Oracle9i 
-        JDeveloper</a>. The Web Services Center offers information of value to 
-        Web services <a href="http://otn.oracle.com/tech/webservices/ws_architect.html">architects</a>, 
-        <a href="http://otn.oracle.com/tech/webservices/ws_appdev.html">developers</a> 
-        and <a href="http://otn.oracle.com/tech/webservices/learner.html">newcomers</a>.</font></p>
-      <p><font size="2" face="Arial, Helvetica, sans-serif"><b>Win Great Prizes 
-        in the OTN Web Services Challenge</b><br>
-        Developers are encouraged to submit their own Web services to the OTN 
-        Web Services Challenge. Entering your Web services makes you eligible 
-        for fantastic prizes, including a fully decked-out Dell mobile workstation. 
-        The Challenge starts August 1, so <a href="http://otn.oracle.com/tech/webservices/challenge.html">get 
-        a head start today</a> by learning more about the rules. You can even 
-        <a href="http://www.oracle.com/go/?&Src=1215798&Act=21">preregister your 
-        interest</a> in the Challenge.</font></p>
-      <p><font size="2" face="Arial, Helvetica, sans-serif"><b>New Internet Seminar: 
-        J2EE and Web Services on Linux with Oracle9iAS Release 2 </b><br>
-        <a href="http://www.oracle.com/go/?&Src=1377459&amp;Act=7">Attend</a> 
-        this on-demand Internet Seminar to learn how to use Oracle9i Application 
-        Server Release 2 to develop high performance J2EE and Web Services applications 
-        on the Linux operating systems.</font></p>
-      <p align="center"><font face="Arial, Helvetica, sans-serif" size="2"><a name="newdownloads"></a><a href="#feature">This 
-        Month's Feature</a> | <a href="#news">News</a> | <a href="#downloads">Software 
-        Downloads</a> | <a href="#ou">Oracle University</a> | <a href="#books">New 
-        Books</a> | <a href="#events">Worldwide Events</a></font></p>
-      <p align="left"><font face="Arial, Helvetica, sans-serif"><b><a name="news"></a>News</b></font></p>
-      <p align="left"><b><font size="2" face="Arial, Helvetica, sans-serif">Special 
-        Discount on Red Hat Linux Advanced Server</font></b> <font size="2" face="Arial, Helvetica, sans-serif"><br>
-        Receive up to 45% discount on the initial purchase of Red Hat Linux Advanced 
-        Server. <a href="http://www.oracle.com/go/?&Src=1376382&amp;Act=11">Find 
-        out how</a>! Offer valid July 1- July 31, 2002. To get more information 
-        on Oracle and Linux, <a href="http://otn.oracle.com/tech/linux">click 
-        here</a>. </font></p>
-      <p><b><font size="2" face="Arial, Helvetica, sans-serif">Helping WebGain 
-        Developers Move to Oracle9i JDeveloper</font></b><font size="2" face="Arial, Helvetica, sans-serif"><br>
-        With all the consolidation taking place in the Java tools space, developers 
-        are seeking tools that provide a complete and integrated environment for 
-        developing J2EE applications and Web services, and also offer security 
-        and stability for the future. Oracle9i JDeveloper delivers on all counts, 
-        and the new <a href="http://otn.oracle.com/products/jdev/htdocs/vcmigration/content.html">WebGain 
-        Developer Center on OTN</a> has been created to give VisualCafe users 
-        the resources to <a href="http://otn.oracle.com/products/jdev/htdocs/vcmigration/move.html">move</a> 
-        rapidly and smoothly to the integrated development environment of Oracle9i 
-        JDeveloper. <a href="http://www.oracle.com/ebusinessnetwork/showiseminar.html?1379826&">Listen</a> 
-        to the interview with Ted Farrell, Oracle's Senior Director of Applications 
-        Tools Technology and former WebGain CTO, on &quot;moving to Oracle9i JDeveloper&quot;. 
-        </font></p>
-      <p><font size="2" face="Arial, Helvetica, sans-serif"><b>Oracle9i Application 
-        Server # 1 in ECperf Benchmarks</b> <br>
-        In its first ECperf submissions, Oracle9i Application Server Release 2 
-        achieved the industry's best ever 'performance' benchmark at 61,863 BBops/min, 
-        beating IBM by 39% and BEA by 63%. The proof is in: Oracle9iAS is still 
-        faster than IBM and BEA. Oracle9iAS also achieved the best results in 
-        the ECperf 'price/performance' category at $5/BBop, 28% better than BEA's 
-        top result, and 54% better than IBM's top result. Get the facts: <a href="http://www.oracle.com/go/?&Src=1380990&amp;Act=7">read</a> 
-        the Oracle9iAS ECperf Benchmark Report now and <a href="http://www.oracle.com/ebusinessnetwork/showiseminar.html?1392270">tune 
-        into</a> a Live Internet Seminar and Q&amp;A on Wednesday, July 17 at 
-        8:00 a.m. PDT for a live presentation and discussion of these record setting 
-        results. </font></p>
-      <p><font size="2" face="Arial, Helvetica, sans-serif"><b>Four Internet Seminars 
-        on the New Security Features in Oracle9i Application Server Release 2</b> 
-        <br>
-        <a href="http://www.oracle.com/ip/deploy/ias/sso/index.html?iseminars.html">Watch</a> 
-        these four Internet Seminars to learn about the new security features 
-        in Oracle9i Application Server Release 2. Oracle9i Application Server 
-        Release 2 is the first application server to offer integrated support 
-        for Single Sign-On, JAAS and an LDAP compliant directory that together 
-        let you cost efficiently secure all your J2EE applications, portals, and 
-        Web services.</font></p>
-      <p><font size="2" face="Arial, Helvetica, sans-serif"><b>OTN Toolbar</b><br>
-        Search OTN from anywhere on the internet with OTN Toolbar. <a href="http://otn.oracle.com/toolbar/content.html">Download</a> 
-        today to easily gain access to many of the key features of OTN (including 
-        downloads, sample code, documentation, and discussion forums).</font></p>
-      <p><b><font size="2" face="Arial, Helvetica, sans-serif">New Internet Seminar 
-        on Oracle9iAS Web Cache and ESI</font></b> <font size="2" face="Arial, Helvetica, sans-serif"><br>
-        <a href="http://www.oracle.com/ebusinessnetwork/showiseminar.html?1293857">Watch</a> 
-        this Internet Seminar and learn how Oracle9iAS Web Cache lets you accelerate 
-        any Web application running on any server by up to 20 times. Speed applications 
-        built in Active Server Pages, Java Server Pages, Servlets, EJBs and more. 
-        Deploy with Web servers like Apache and Microsoft IIS as well as application 
-        servers like BEA WebLogic, IBM WebSphere, Sun/iPlanet and, of course, 
-        Oracle9iAS. Best of all, Oracle9iAS Web Cache uniquely supports caching 
-        of both static and dynamically generated content without changing the 
-        application, enabling dynamic Web sites to more efficiently deliver rich 
-        content and therefore improving the user experience. </font></p>
-      <p><font size="2" face="Arial, Helvetica, sans-serif"><b>Oracle9i Reports 
-        data source SDK available now</b><br>
-        The <a href="http://otn.oracle.com/products/reports/apis/pdstutorial/textPDS/index.html">Oracle9i 
-        Reports data source SDK</a> allows you to plug in your own data sources 
-        and benefit from the sophisticated report creation and distribution environment 
-        of <a href="http://otn.oracle.com/products/reports/content.html">Oracle9i 
-        Reports</a>. Check out the new documentation and samples. </font></p>
-      <p><font size="2" face="Arial, Helvetica, sans-serif"><b>Putting Forms on 
-        the Web </b><br>
-        Looking to move your existing Forms application from client/server to 
-        the Web? Want the easy access and maintainability of a web deployed Forms 
-        application? Then <a href="http://otn.oracle.com/products/forms/pdf/forms9icstowebmigration.pdf">check 
-        out this new paper</a>.</font></p>
-      <p><font size="2" face="Arial, Helvetica, sans-serif"><b>Struts and Oracle9i 
-        JDeveloper</b><br>
-        Here's a <a href="http://otn.oracle.com/products/jdev/howtos/jsp/StrutsHowTo.html">cool 
-        new article</a> with detailed instructions on how to configure and use 
-        the Jakarta Struts open source Model-View-Controller framework with <a href="http://otn.oracle.com/products/jdev/content.html">Oracle9i 
-        JDeveloper</a>.</font></p>
-      <p><font size="2" face="Arial, Helvetica, sans-serif"> <b>Quickstart with 
-        Oracle9i JDeveloper for BEA developers</b><br>
-        Are you using BEA's WebLogic and looking for development tools? Here is 
-        the <a href="http://otn.oracle.com/centers/mov2jdev">easy way to start</a> 
-        using the award winning Oracle9i JDeveloper with WebLogic. And if you 
-        want to use the fastest J2EE container, check out the <a href="http://www.oracle.com/go/?&Src=1260040&Act=8">migration 
-        kit</a> to Oracle9iAS.</font> </p>
-      <p><b><font size="2" face="Arial, Helvetica, sans-serif">Wireless and Voice 
-        Made Easy With Oracle9i Application Server</font></b> <font size="2" face="Arial, Helvetica, sans-serif"><br>
-        New Internet lessons give viewers the low-down on how to use the wireless 
-        and voice services of Oracle9i Application Server (Oracle9iAS Wireless) 
-        to quickly and easily give access to applications and data using any device, 
-        over any network. Learn why Oracle is a leader in wireless and voice infrastructure 
-        for yourselves! <a href="http://www.oracle.com/go/?&Src=1393043&amp;Act=9">Check 
-        out</a> the new Internet lessons in the FREE Mobile eKit!</font></p>
-      <p><font size="2" face="Arial, Helvetica, sans-serif"><b>Snapshot Seminar: 
-        Interwoven &amp; Oracle9iAS Content Management</b><br>
-        Oracle and Interwoven together offer a portal ready, proven, and flexible 
-        Enterprise Content Management solution. . Watch a 15 minute on-demand 
-        <a href="http://www.oracle.com/go/?&Src=1295633&Act=45">snapshot seminar</a> 
-        and learn how you can let your users control their content through a portal 
-        powered by Oracle and Interwoven. </font></p>
-      <p><b><font size="2" face="Arial, Helvetica, sans-serif">Updated Oracle9iAS 
-        Portal Developer Kit (PDK) - July<br>
-        </font></b><font size="2" face="Arial, Helvetica, sans-serif">The <a href="http://portalstudio.oracle.com">updated 
-        Oracle9iAS Portal Developer Kit (PDK)</a> highlights portlet communication. 
-        Using the PDK, you can build smart portlets with such features as inter-portlet 
-        communication, page to portlet communication, and portlet reusability. 
-        This release includes new J2EE-based and Web Services samples. </font></p>
-      <p><b><font size="2" face="Arial, Helvetica, sans-serif">Snapshot Seminar: 
-        Documentum &amp; Oracle9iAS Content Management</font></b><font size="2" face="Arial, Helvetica, sans-serif"><br>
-        Oracle and Documentum now offer a joint solution to create, manage and 
-        deliver content through Web sites and portals. Watch a 15 minute on-demand 
-        <a href="http://www.oracle.com/go/?&Src=1295633&Act=44">snapshot 
-        seminar</a> and learn how you can let your users control their content 
-        through a portal powered by Oracle and Documentum.</font></p>
-      <p></p>
-      <p align="center"><font face="Arial, Helvetica, sans-serif" size="2"><a name="newdownloads"></a><a href="#feature">This 
-        Month's Feature</a> | <a href="#news">News</a> | <a href="#downloads">Software 
-        Downloads</a> | <a href="#ou">Oracle University</a> | <a href="#books">New 
-        Books</a> | <a href="#events">Worldwide Events</a></font></p>
-      <p align="left"><font face="Arial, Helvetica, sans-serif"><b><a name="downloads"></a> 
-        New Software Downloads</b></font></p>
-      <p align="left"><font size="2" face="Arial, Helvetica, sans-serif"><a href="http://otn.oracle.com/software/products/ias/devuse.html">Oracle9i 
-        Application Server Release 2 for Windows NT/2000, AIX, and Compaq Tru64 
-        UNIX</a> </font></p>
-      <p><font size="2" face="Arial, Helvetica, sans-serif"><a href="http://otn.oracle.com/software/products/ias/devuse.html">Oracle9iAS 
-        TopLink 4.6 for Linux, UNIX, and Windows NT/2000</a> </font></p>
-      <p><font size="2" face="Arial, Helvetica, sans-serif"><a href="http://otn.oracle.com/software/products/lite/content.html">Oracle9i 
-        Lite Release 5.0.2.0.0 for Sun SPARC Solaris</a> </font></p>
-      <p><font size="2" face="Arial, Helvetica, sans-serif"><a href="http://otn.oracle.com/software/tech/windows/odpnet/content.html">Oracle 
-        Data Provider for .NET (ODP.NET) Beta</a> </font></p>
-      <p align="center"><font face="Arial, Helvetica, sans-serif" size="2"><a name="newdownloads"></a><a href="#feature">This 
-        Month's Feature</a> | <a href="#news">News</a> | <a href="#downloads">Software 
-        Downloads</a> | <a href="#ou">Oracle University</a> | <a href="#books">New 
-        Books</a> | <a href="#events">Worldwide Events</a></font></p>
-      <p><font face="Arial, Helvetica, sans-serif" size="2"><b><a name="ou"></a></b></font><font face="Arial, Helvetica, sans-serif"><b>Oracle 
-        University</b></font></p>
-      <p><b><font size="2" face="Arial, Helvetica, sans-serif">Special Offer! 
-        Save 45% on Oracle9i DBA Certification Training</font></b> <font size="2" face="Arial, Helvetica, sans-serif"><br>
-        The expanded Oracle Certification Program now offers a true certification 
-        levels that are built to fit the needs of IT professionals as well as 
-        organizations looking to hire them. Each level constitutes reaching a 
-        benchmark of experience and expertise that is industry recognized and 
-        approved. And, with each new credential can come increased opportunities, 
-        higher pay, and more benefits to keep Oracle professionals successful. 
-        </font></p>
-      <p><font size="2" face="Arial, Helvetica, sans-serif">Oracle9i Certification 
-        Savings Plan &#150; Save 45% on 4 Instructor Led inClass courses. 4 for 
-        the price of 2! <a href="http://www.oracle.com/education/index.html?promotions.html">Click 
-        here</a> to learn more! </font></p>
-      </td>
-    <td valign="top" width="14%" > 
-      <div align="center"> 
-        <table width="100%" border="0" cellspacing="0" cellpadding="1" bgcolor="#FF0000">
-          <tr> 
-            <td bgcolor="#000000"> 
-              <table width="100%" border="0" cellpadding="5" bgcolor="#FFFF00" cellspacing="0">
-                <tr> 
-                  <td bgcolor="#FFFFFF" valign="top"> 
-                    <p align="center"><img src="http://otn.oracle.com/techblast/images/LightBulb.gif" width="80" height="109"></p>
-                    <p align="left"><font size="1" face="Arial, Helvetica, sans-serif">Seeking 
-                      a new job? Check out <a href="http://seeker.dice.com/seeker.epl?rel_code=26&op=2&skill=oracle">OTN 
-                      Skills Marketplace</a> for all open Oracle-trained positions.</font></p>
-                    <p align="left"><font face="Arial, Helvetica, sans-serif" size="1">Need 
-                      help implementing technology solutions to business problems? 
-                      <a href="http://otn.oracle.com/products/oracle9i/htdocs/9iober2/index.html">Oracle9i 
-                      by Example Series tutorials</a> can save you time.</font></p>
-                    <p align="left"><font size="1" face="Arial, Helvetica, sans-serif">Taking 
-                      an OCP exam? OTN members, take advantage of the 20% exam 
-                      <a href="http://www.oracle.com/education/certification/faq/index.html?otndisc.html">discount</a>.</font></p>
-                    </td>
-                </tr>
-              </table>
-            </td>
-          </tr>
-        </table>
-      </div>
-    </td>
-  </tr>
-</table>
-</body>
-</html>
-
-
-
-
-
-
-
-
-
-
-
-<html>
-<head>
-<title>Untitled Document</title>
-<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
-</head>
-<body bgcolor="#FFFFFF" text="#000000">
-<table width="100%" border="0" cellspacing="10" >
-  <tr> 
-    <td valign="top" width="17%" > 
-      <h5>&nbsp;</h5>
-    </td>
-    <td valign="top" width="67%" > 
-      <div align="center"> 
-        <p align="center"><font face="Arial, Helvetica, sans-serif" size="2"><a name="newdownloads"></a><a href="#feature">This 
-          Month's Feature</a> | <a href="#news">News</a> | <a href="#downloads">Software 
-          Downloads</a> | <a href="#ou">Oracle University</a> | <a href="#books">New 
-          Books</a> | <a href="#events">Worldwide Events</a></font></p>
-        <div align="center"> 
-          <div align="center"> 
-            <div align="center"> 
-              <p align="left"><font face="Arial, Helvetica, sans-serif"><b><a name="books"></a>New 
-                Books </b></font></p>
-              <p align="left"><b><font size="2" face="Arial, Helvetica, sans-serif">Oracle9i 
-                DBA 101</font></b><font size="2" face="Arial, Helvetica, sans-serif"><br>
-                <a href="http://shop.osborne.com/cgi-bin/oraclepress/0072224746.html">Oracle9i 
-                DBA 101</a> by Marlene Theriault, Rachel Carmichael, &amp; James 
-                Viscusi (ISBN 0-07-222474-6) explains, step-by-step, how to effectively 
-                administer an Oracle database. Readers will find coverage of the 
-                key Oracle9i new features as well as details on the daily responsibilities 
-                of a DBA and tips on how to successfully accomplish those tasks. 
-                From the exclusive publishers of Oracle Press books, this is the 
-                ideal resource for the aspiring Oracle database administrator. 
-                </font></p>
-              <p align="left"><font size="2" face="Arial, Helvetica, sans-serif"><b>Oracle9i 
-                Mobile</b><br>
-                <a href="http://shop.osborne.com/cgi-bin/oraclepress/007222455X.html">Oracle9i 
-                Mobile</a> by Alan Yeung, Philip Stephenson, &amp; Nicholas Pang 
-                (ISBN 0-07-222455-X) helps readers design, deploy, and manage 
-                flexible mobile applications on the Oracle platform. From the 
-                exclusive publishers of Oracle Press books, this resource explains 
-                how to use and extend the mobile services available in Oracle9iAS 
-                Wireless and integrate with other Oracle technologies. Mobilize 
-                any e-business, reach new customers, and deliver critical information 
-                to mobile users with the most scalable and reliable mobile infrastructure 
-                available. </font></p>
-              <p align="left"><font size="2" face="Arial, Helvetica, sans-serif"> 
-                <b>Oracle Press User Group Program</b><br>
-                Oracle Press has a new User Group Program! Oracle Press supports 
-                the service that User Groups provide to the technical community. 
-                We value our relationship with community-based groups and welcome 
-                the opportunity to form partnerships with User Groups to disseminate 
-                the latest technological information available in Osborne publications. 
-                Osborne encourages participation by technical User Groups that 
-                meet regularly, discuss, teach, and troubleshoot technical topics, 
-                write book reviews, and publish print and/or online newsletters. 
-                </font></p>
-            </div>
-          </div>
-        </div>
-      </div>
-      <div align="left"> 
-        <p><font size="2" face="Arial, Helvetica, sans-serif">Oracle Press can 
-          provide User Groups: </font></p>
-      </div>
-      <ul>
-        <li> 
-          <div align="left"><font size="2" face="Arial, Helvetica, sans-serif">Review 
-            copies of Oracle Press books for newsletter reviews </font></div>
-        </li>
-        <li> 
-          <div align="left"><font size="2" face="Arial, Helvetica, sans-serif">Book 
-            donations and promotional items for User Group events </font></div>
-        </li>
-        <li> 
-          <div align="left"><font size="2" face="Arial, Helvetica, sans-serif">30% 
-            discount on bulk purchases of 10 or more books </font></div>
-        </li>
-        <li>
-          <div align="left"><font size="2" face="Arial, Helvetica, sans-serif">And 
-            more...</font></div>
-        </li>
-      </ul>
-      <div align="center"> 
-        <div align="center"> 
-          <div align="center"> 
-            <div align="center"> 
-              <p align="left"><font size="2" face="Arial, Helvetica, sans-serif"> 
-                <a href="http://www.osborne.com/usergroups/index.shtml">Click 
-                here</a> for complete details about Oracle Press' User Group Program.</font><font size="2"> 
-                </font> </p>
-              <p align="center"><font face="Arial, Helvetica, sans-serif" size="2"><a name="newdownloads"></a><a href="#feature">This 
-                Month's Feature</a> | <a href="#news">News</a> | <a href="#downloads">Software 
-                Downloads</a> | <a href="#ou">Oracle University</a> | <a href="#books">New 
-                Books</a> | <a href="#events">Worldwide Events</a></font></p>
-              <p align="left"><font face="Arial, Helvetica, sans-serif" size="2"><b><a name="events"></a></b></font><font face="Arial, Helvetica, sans-serif"><b>Worldwide 
-                Events </b></font></p>
-            </div>
-            <p align="left"><b><font face="Arial, Helvetica, sans-serif" size="2"><a name="americas"></a>Americas</font></b></p>
-            <p align="left"><b><font face="Arial, Helvetica, sans-serif" size="2">Oracle 
-              User Group Events</font></b><font face="Arial, Helvetica, sans-serif" size="2"><br>
-              <a href="http://otn.oracle.com/collaboration/user_group/events.html">Find 
-              out</a> where new user group events are happening in your area. 
-              </font> </p>
-            <p align="left"><font face="Arial, Helvetica, sans-serif" size="2"><b><a name="apac"></a>APAC</b></font></p>
-          </div>
-        </div>
-      </div>
-      <div align="left"> 
-        <table width="100%" border="0" cellpadding="5">
-          <tr> 
-            <td width="16%" height="47"><b><font face="Arial, Helvetica, sans-serif" size="2"><a href="http://www.oracle.com/oracleworld"><img src="http://otn.oracle.com/events/nsmailH020.gif" align=absmiddle 
-                       width="104" height="104" border="0"></a></font></b></td>
-            <td width="84%" valign="top"><b><font face="Arial, Helvetica, sans-serif" size="2">OracleWorld 
-              Online - Beijing <br>
-              </font></b><font size="2" face="Arial, Helvetica, sans-serif">Over 
-              5,000 industry professionals from all over China and the world gathered 
-              to learn how Oracle can help your business reduce costs, improve 
-              efficiencies, and improve the way you run your business. If you 
-              missed OracleWorld in Copenhagen, you can get all the highlights 
-              including keynotes, conference presentations and whitepapers <a href="http://www.oracle.com/oracleworld/online/beijing/">online</a>.</font></td>
-          </tr>
-        </table>
-        <br>
-      </div>
-      <p align="left"><font face="Arial, Helvetica, sans-serif" size="2"><b>Oracle 
-        iSeminars: Free &amp; Live @ Your Desktop<br>
-        </b> Attend a FREE Oracle APAC iSeminar to learn more about how Oracle9i 
-        - Application Server, Database &amp; Tools could provide you with a complete 
-        and cost-effective e-business infrastructure. </font></p>
-      <div align="left"></div>
-      <div align="center"> 
-        <div align="center"> 
-          <div align="center"> 
-            <p align="left"><font face="Arial, Helvetica, sans-serif" size="2">Please 
-              <a href="http://isdapac.oracle.com/iccdocs/seminarList.shtml">click 
-              here</a> for further information and online registration for all 
-              iseminars. (Please select correct time zone &amp; click &quot;reset&quot;). 
-              </font><font face="Arial, Helvetica, sans-serif" size="2">For any 
-              questions, please <a href="mailto:oracleisd_au@oracle.com">email</a> 
-              us.</font> </p>
-            <p align="left"><font face="Arial, Helvetica, sans-serif" size="2"><b><a name="emea"></a>EMEA</b></font></p>
-            <table width="100%" border="0" cellpadding="5">
-              <tr> 
-                <td width="16%" height="56"><b><font face="Arial, Helvetica, sans-serif" size="2"><a href="http://www.oracle.com/oracleworld"><img src="http://otn.oracle.com/events/nsmailH020.gif" align=absmiddle 
-                       width="104" height="104" border="0"></a></font></b></td>
-                <td width="84%" valign="top"><b><font size="2" face="Arial, Helvetica, sans-serif">OracleWorld 
-                  Online - Copenhagen<br>
-                  </font></b><font size="2" face="Arial, Helvetica, sans-serif">Thousands 
-                  of professionals from all over the world gathered to learn how 
-                  Oracle can help your business reduce costs, improve efficiencies, 
-                  and improve the way you run your business. If you missed OracleWorld 
-                  in Copenhagen, you can get all the highlights including keynotes, 
-                  conference presentations and whitepapers <a href="http://www.oracle.com/oracleworld/online/copenhagen/">online</a>.</font></td>
-              </tr>
-            </table>
-          </div>
-        </div>
-        <div align="left"><br>
-        </div>
-        <div align="left"><b><font size="2" face="Arial, Helvetica, sans-serif">Oracle 
-          Technology Days Belgian &amp; Luxembourg<br>
-          </font></b><font size="2" face="Arial, Helvetica, sans-serif">Join us 
-          for the Oracle Technology Days - Featuring Oracle9i Release 2 &#150; 
-          Live, Local, Free! </font></div>
-      </div>
-      <p align="left"><font size="2" face="Arial, Helvetica, sans-serif">Join 
-        us for this executive full-day event :<br>
-        29/8/2002 - Brussels (sessions in English)<br>
-        3/9/2002 - Gent (sessies in het Nederlands)<br>
-        12/9/2002 - Li&egrave;ge (session en fran&ccedil;ais)</font></p>
-      <p align="left"><font size="2" face="Arial, Helvetica, sans-serif"><a href="http://www.oracle.com/go/?&Src=1336077&Act=17">Click 
-        here</a> for more information and registration.</font></p>
-      <p align="left"><font size="2" face="Arial, Helvetica, sans-serif">Regards,</font></p>
-      <p align="left"><font size="2" face="Arial, Helvetica, sans-serif">Oracle 
-        Technology Network Team</font></p>
-      <p align="left"><font size="2" face="Arial, Helvetica, sans-serif"><b><font size="1">UNSUBSCRIBE<br>
-        </font></b><font size="1"> When you registered at OTN, you indicated you 
-        would like to receive e-mail updates from us. If you do not want to receive 
-        future e-mails, please visit our <a href="http://otn.oracle.com/admin/account/membership.html">update 
-        section</a> , log in with your username and password, and UNCHECK the 
-        I wish to receive informational e-mails box. </font></font></p>
-      <p align="left"><font size="1" face="Arial, Helvetica, sans-serif"><b>USERNAME 
-        AND PASSWORD QUESTIONS?<br>
-        </b> Forget your OTN login information? Use our <a href="http://otn.oracle.com/admin/account/membership.html">password 
-        lookup</a>.</font></p>
-      <p align="left"><b><font size="1" face="Arial, Helvetica, sans-serif">DUPLICATE 
-        MESSAGES?<br>
-        </font></b><font face="Arial, Helvetica, sans-serif" size="1"> You may 
-        have multiple accounts on OTN. Please send a message to <a href="mailto:otn_us@oracle.com">OTN</a> 
-        with the username you're using to access http://otn.oracle.com. We'll 
-        then contact you and delete the unused account. </font> 
-          </td>
-    <td valign="top" width="16%" > 
-      <div align="center"> </div>
-    </td>
-  </tr>
-</table>
-</body>
-</html>
-
-<p><font face="Arial, helvetica" size="1">
-<br>To be removed from Oracle's mailing lists, send an email to: 
-<br><a href="mailto:unsubscribe@oracleeblast.com?subject=REMOVE OF ORACLE MAILING LIST 1400444&body=REMOVE XXXXXX.YYYYY@RUHR-UNI-BOCHUM.DE ">unsubscribe@oracleeblast.com</a> 
-<br>with the following in the message body: 
-<br>REMOVE XXXXXX.YYYYY@RUHR-UNI-BOCHUM.DE
-<br>STOP 
-<p>
-[250000/116/137209217] 
-</font>
-<img src="http://www.oracle.com/elog/trackurl?di=1400444&si1=137209217" border=0> 
-
-
-
-
-
-
-
-
-
-
diff --git a/upstream/t/data/whitelists/orbitz.com b/upstream/t/data/whitelists/orbitz.com
deleted file mode 100644 (file)
index 50dcb30..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-Received: (qmail 9304 invoked by uid 505); 12 Aug 2002 16:57:57 -0000
-Delivered-To: zzzzzz@xyz.com
-Received: (qmail 19051 invoked by uid 74); 12 Aug 2002 16:58:16 -0000
-Received: from travelercare@orbitz.com by agogo0 by uid 71 with qmail-scanner-1.13 
- (clamscan: 0.22.  Clear:SA:1(0/0):. 
- Processed in 0.774434 secs); 12 Aug 2002 16:58:16 -0000
-Received: from unknown (HELO mailhost.wm.orbitz.com) (65.216.67.72)
-  by mail0.tyva.xyz.com with SMTP; 12 Aug 2002 16:58:15 -0000
-Received: from wl14 (sim-snat-01.wm.orbitz.com [10.50.100.11])
-       by mailhost.wm.orbitz.com (8.12.1/8.12.1) with ESMTP id g7CGwEsF005188
-       for <zzzzz@xyz.com>; Mon, 12 Aug 2002 11:58:14 -0500
-Message-ID: <17728173.1029171478187.JavaMail.weblogic@wl14>
-Date: Mon, 12 Aug 2002 11:57:58 -0500 (CDT)
-From: Orbitz Traveler Care <travelercare@orbitz.com>
-To: Rod <zzzzz@xyz.com>
-Subject: Orbitz Travel Document
-Mime-Version: 1.0
-Content-Type: text/html
-Content-Transfer-Encoding: 7bit
-
-
-<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
-<html>
-<head>
-<title>Orbitz Travel Document</title>
-</head>
-
diff --git a/upstream/t/data/whitelists/paypal.com b/upstream/t/data/whitelists/paypal.com
deleted file mode 100644 (file)
index de3fc95..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-Received: (qmail 18217 invoked from network); 4 Apr 2002 21:41:45 -0000
-Received: from localhost (127.0.0.1)
-  by localhost with SMTP; 4 Apr 2002 21:41:45 -0000
-Delivered-To: zzzzzz@xyz.com
-Received: from mail.xyz.com [64.123.162.104]
-       by localhost with POP3 (fetchmail-5.9.0)
-       for xyz@localhost (single-drop); Thu, 04 Apr 2002 16:41:45 -0500 (EST)
-Received: (qmail 857 invoked from network); 4 Apr 2002 21:41:11 -0000
-Received: from unknown (HELO web18.nix.paypal.com) (65.206.229.164)
-  by mail0.tyva.xyz.com with SMTP; 4 Apr 2002 21:41:11 -0000
-Received: (qmail 13807 invoked by uid 99); 4 Apr 2002 21:41:10 -0000
-Date: Thu, 04 Apr 2002 13:41:10 -0800
-Message-Id: <1017000000.00000@paypal.com>
-From: service@paypal.com
-To: rod@xyz.com
-Subject: Receipt for your Payment
-
-This email confirms that you have paid xyz.com Ltd. $12.00 
-using PayPal.
-
----------------------------------------------------------------
-This payment was sent using your bank account. 
-
-By using your bank account to send money, you just:
-
-- Paid instantly and securely 
-- Sent money faster than writing and mailing paper checks.
-- Received an additional entry in our $1,000 Sweepstakes!
-  
-Thanks for using your bank account!
-
----------------------------------------------------------------
-
-
-Thank you,
-The PayPal Team
-
-Note: When you log in to your PayPal account, be sure that the website's URL always begins with "https://www.paypal.com/".  The "s" in "https" at the beginning of the URL means you are logging into a secure page.  If the URL does not begin with https, you are not on a PayPal page.  
-
-
-
-Please do not reply to this e-mail. Mail sent to this address 
-cannot be answered. For assistance, log in to your PayPal 
-account and choose the "Help" link in the footer of any page.
-
-
diff --git a/upstream/t/data/whitelists/register.com_password b/upstream/t/data/whitelists/register.com_password
deleted file mode 100644 (file)
index 6973a66..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-From customersupport@register.com  Wed Jan 30 09:50:12 2002
-Delivery-Date: Mon, 18 Sep 2000 16:41:35 +0100
-Return-Path: <customersupport@register.com>
-Delivered-To: jm@ooooooooooo.com
-Received: from wwwn.register.com (outgoing2.jrcy.register.com [209.67.50.16])
-        by mail (Postfix) with ESMTP id 9A73FD894B
-        for <ppppp@ooooooooooo.com>; Mon, 18 Sep 2000 15:41:33 +0000 (Eire)
-Received: (from nobody@localhost)
-        by wwwn.register.com (8.9.3/8.9.3) id LAA18712
-        for ppppp@ooooooooooo.com; Mon, 18 Sep 2000 11:41:22 -0400
-Date: Mon, 18 Sep 2000 11:41:22 -0400
-Message-Id: <200009181541.LAA18712@wwwn.register.com>
-X-Authentication-Warning: wwwn.register.com: nobody set sender to customersupport@register.com using -f
-From: Domain.Management.System@www.register.com
-Reply-To: customersupport@register.com
-To: ppppp@ooooooooooo.com
-Subject: Domain Manager Password
-Sender: customersupport@register.com
-
-User Name : xxxxxxxxxxxxxxx
-
-Thank you for using register.com's Domain Manager.
-
-To change or re-enter your password, please copy and paste the URL below into the "Location" or "Address" field of your web browser and hit the 'Enter' key on your keyboard.  Note: If your email program supports HTML, you may be able to click on the link below.
-
-==========================================================================================
-http://mydomain.register.com/change_password.cgi?00000000000
-==========================================================================================
-Note:  Above link will be expire within three days
-
-The page displayed will allow you to change or re-enter your Domain Manager password.
-
-In the event that the email program you are using does not display the URL as a hyperlink or the URL is broken into two lines, do not click on it.  Instead, please follow the copy and pasting instructions below to complete the confirmation process.
-
-- Copy and Pasting Instructions -
-
-Highlight the URL with your cursor. Once you have highlighted the URL, hit CTRL + C to copy the highlighted area.
-
-Open an Internet browser window and click in the Address or Location field.  Hit CTRL + V to paste the URL into the address field.  If necessary, repeat this process with the second line of the URL. Please be sure to delete spaces if there are any embedded in the URL - otherwise you will not be able to connect to the proper confirmation page.
-
-Once you have entered and looked over the URL, hit the Enter key on your keyboard. The web page displayed will allow you to complete the final step in the confirmation process.
-
-If you have further questions, please do not hesitate to contact us at:
-http://www.register.com/create_ticket.cgi
-
-Thank you for using register.com, the first step on the web.
-
-Customer Service
-register.com, inc
-http://www.register.com
-
-
-
diff --git a/upstream/t/data/whitelists/ryanairmail.com b/upstream/t/data/whitelists/ryanairmail.com
deleted file mode 100644 (file)
index a99c301..0000000
+++ /dev/null
@@ -1,165 +0,0 @@
-From webster@ryanairmail.com  Fri Aug 16 12:59:01 2002
-Return-Path: <webster@ryanairmail.com>
-Delivered-To: zzzz@localhost.foofoofoofoo.com
-Received: from localhost (localhost [127.0.0.1])
-       by phobos.labs.foofoofoofoo.com (Postfix) with ESMTP id E163743C32
-       for <zzzz@localhost>; Fri, 16 Aug 2002 07:58:59 -0400 (EDT)
-Received: from phobos [127.0.0.1]
-       by localhost with IMAP (fetchmail-5.9.0)
-       for zzzz@localhost (single-drop); Fri, 16 Aug 2002 12:58:59 +0100 (IST)
-Received: from mail.ryanair2.ie ([193.120.152.8]) by dogma.slashnull.org
-    (8.11.6/8.11.6) with SMTP id g7GBwca16137 for <xxxxx@yyyyyy.zzz>;
-    Fri, 16 Aug 2002 12:58:38 +0100
-From: webster@ryanairmail.com
-To: "Customers" <customers@mail.ryanairmail.com>
-Subject: Incredible Autumn Fares
-Date: Fri, 16 Aug 2002 08:41:00 +0100
-X-Assembled-BY: XWall v3.21
-X-Mailer: MailBeamer v3.28
-Message-Id: <LISTMANAGER-123546-16680-2002.08.16-08.51.02--xxxxx@yyyyyy.zzz@mail.ryanairmail.com>
-MIME-Version: 1.0
-Content-Type: text/plain; charset="iso-8859-1"
-List-Unsubscribe: <mailto:leave-customers-123546K@mail.ryanairmail.com>
-Reply-To: webster@ryanairmail.com
-Content-Transfer-Encoding: 8bit
-X-MIME-Autoconverted: from quoted-printable to 8bit by dogma.slashnull.org
-    id g7GBwca16137
-
-Massive seat sale this weekend on Ryanair.com
-Fares from £ 6.25 one way including taxes
-Travel between 9 September and 17 December
-Sale until midnight Monday 19 August
-Travel between 1200 hrs Monday and 1300 hrs Thursday or
-Saturdays after 1200hrs to get these fares
-Limited availability during school break periods and bank
-holiday weekends.  All fares quoted are one way including taxes.
-Book now at http://www.ryanair.com
-
-
-*********************** Domestic UK *************************
-London Stansted to Glasgow Prestwick           from £  6.25
-Glasgow Prestwick to London Stansted           from £  6.25
-London Stansted to City of Derry               from £12.99
-City of Derry to London Stansted                       from £12.99
-
-*********************** UK to Scandinavia *************************
-London Stansted to Gothenburg                  from £  9.99
-London Stansted to Stockholm NYO               from £12.99
-London Stansted to Stockholm VST               from £12.99
-London Stansted to Aarhus                      from £14.99
-London Stansted to Esbjerg                     from £14.99
-London Stansted to Oslo Torp                   from £14.99
-Glasgow Prestwick to Oslo Torp                 from £19.99
-
-****************** UK to Belgium/Netherlands ********************
-London Stansted to Brussels Charleroi          from £  9.99
-London Stansted to Eindhoven                   from £12.99
-Liverpool to Brussels Charleroi                        from £12.99
-Glasgow Prestwick to Brussels Charleroi        from £12.99
-
-****************** UK to France/Italy ********************
-London Stansted to Dinard                      from £14.99
-London Stansted to St Etienne                  from £14.99
-London Stansted to Milan Bergamo               from £14.99
-Glasgow Prestwick to Paris Beauvais            from £14.99
-
-****************** UK to Germany/Austria ********************
-Bournemouth to Frankfurt Hahn                  from £12.99
-London Stansted to Frankfurt Hahn              from £12.99
-London Stansted to Hamburg Lubeck              from £14.99
-London Stansted to Klagenfurt                  from £14.99
-
-*********************** UK to Ireland *************************
-Manchester to Dublin                           from £  9.99
-Leeds Bradford to Dublin                               from £  9.99
-Bristol to Dublin                                      from £  9.99
-Edinburgh to Dublin                            from £  9.99
-Teesside to Dublin                             from £  9.99
-Glasgow Prestwick to Dublin                    from £  9.99
-Bournemouth to Dublin                          from £  9.99
-Liverpool to Dublin                            from £  9.99
-London Stansted to Knock                       from £12.99
-London Stansted to Shannon                     from £14.99
-London Stansted to Cork                        from £14.99
-London Luton to Dublin                         from £14.99
-London Gatwick to Dublin                       from £14.99
-London Stansted to Dublin                      from £14.99
-
-*********************** Ireland to UK *************************
-Dublin to Liverpool                            from Eur  9.99
-Dublin to Manchester                           from Eur  9.99
-Dublin to Bournemouth                          from Eur12.99
-Dublin to Bristol                                      from Eur12.99
-Dublin to Leeds Bradford                               from Eur12.99
-Dublin to Edinburgh                            from Eur12.99
-Dublin to Teesside                             from Eur12.99
-Dublin to Glasgow Prestwick                    from Eur12.99
-Knock to London Stansted                       from Eur12.99
-Cork to London Stansted                        from Eur14.99
-Shannon to London Stansted                     from Eur14.99
-Dublin to London Stansted                      from Eur14.99
-****************************************************************
-
-====================================================================
-
-E-MAIL DISCLAIMER
-
-This e-mail and any files and attachments transmitted with it
-are confidential and may be legally privileged. They are intended
-solely for the use of the intended recipient.  Any views and
-opinions expressed are those of the individual author/sender
-and are not necessarily shared or endorsed by Ryanair Holdings plc
-or any associated or related company. In particular e-mail
-transmissions are not binding for the purposes of forming
-a contract to sell airline seats, directly or via promotions,
-and do not form a contractual obligation of any type.
-Such contracts can only be formed in writing by post or fax,
-duly signed by a senior company executive, subject to approval
-by the Board of Directors.
-
-The content of this e-mail or any file or attachment transmitted
-with it may have been changed or altered without the consent
-of the author.  If you are not the intended recipient of this e-mail,
-you are hereby notified that any review, dissemination, disclosure,
-alteration, printing, circulation or transmission of, or any
-action taken or omitted in reliance on this e-mail or any file
-or attachment transmitted with it is prohibited and may be unlawful.
-
-If you have received this e-mail in error
-please notify Ryanair Holdings plc by emailing postmaster@ryanair.ie
-or contact Ryanair Holdings plc, Dublin Airport, Co Dublin, Ireland.
-
-=====================================================================
-
-E-MAIL DISCLAIMER
-
-This e-mail and any files and attachments transmitted with it 
-are confidential and may be legally privileged. They are intended 
-solely for the use of the intended recipient.  Any views and 
-opinions expressed are those of the individual author/sender 
-and are not necessarily shared or endorsed by Ryanair Holdings plc 
-or any associated or related company. In particular e-mail 
-transmissions are not binding for the purposes of forming 
-a contract to sell airline seats, directly or via promotions, 
-and do not form a contractual obligation of any type.   
-Such contracts can only be formed in writing by post or fax, 
-duly signed by a senior company executive, subject to approval 
-by the Board of Directors.
-
-The content of this e-mail or any file or attachment transmitted 
-with it may have been changed or altered without the consent 
-of the author.  If you are not the intended recipient of this e-mail, 
-you are hereby notified that any review, dissemination, disclosure, 
-alteration, printing, circulation or transmission of, or any 
-action taken or omitted in reliance on this e-mail or any file 
-or attachment transmitted with it is prohibited and may be unlawful.
-
-If you have received this e-mail in error 
-please notify Ryanair Holdings plc by emailing postmaster@ryanair.ie
-or contact Ryanair Holdings plc, Dublin Airport, Co Dublin, Ireland.  
-
-
----
-You are currently subscribed to customers as: xxxxx@yyyyyy.zzz
-To unsubscribe send a blank email to leave-customers-123546K@mail.ryanairmail.com
-
diff --git a/upstream/t/data/whitelists/sf.net b/upstream/t/data/whitelists/sf.net
deleted file mode 100644 (file)
index 7d9a1ab..0000000
+++ /dev/null
@@ -1,263 +0,0 @@
-From noreply@sourceforge.net  Wed Aug 14 17:36:08 2002
-Return-Path: <noreply@sourceforge.net>
-Delivered-To: aaaa@localhost.xxxxxxxxxxxx.com
-Received: from localhost (localhost [127.0.0.1])
-       by phobos.labs.xxxxxxxxxxxx.com (Postfix) with ESMTP id EEAC943C32
-       for <aaaa@localhost>; Wed, 14 Aug 2002 12:36:06 -0400 (EDT)
-Received: from phobos [127.0.0.1]
-       by localhost with IMAP (fetchmail-5.9.0)
-       for aaaa@localhost (single-drop); Wed, 14 Aug 2002 17:36:07 +0100 (IST)
-Received: from usw-sf-list2.sourceforge.net (usw-sf-fw2.sourceforge.net
-    [216.136.171.252]) by dogma.slashnull.org (8.11.6/8.11.6) with ESMTP id
-    g7EGW3424685 for <xxxxx@yyyyyy.zzz>; Wed, 14 Aug 2002 17:32:04 +0100
-Received: from usw-sf-db2-b.sourceforge.net ([10.3.1.4]
-    helo=sourceforge.net ident=tperdue) by usw-sf-list2.sourceforge.net with
-    smtp (Exim 3.31-VA-mm2 #1 (Debian)) id 17f141-00043a-00 for
-    <xxxxx@yyyyyy.zzz>; Wed, 14 Aug 2002 09:32:05 -0700
-From: Mailer <noreply@sourceforge.net>
-To: "" <xxxxx@yyyyyy.zzz>
-Subject: SOURCEFORGE.NET UPDATE: August 14, 2002
-Message-Id: <E17f141-00043a-00@usw-sf-list2.sourceforge.net>
-Date: Wed, 14 Aug 2002 09:32:05 -0700
-
-
-(You are receiving this email because you subscribed to it (honest).
-For information on how to unsubscribe please read the bottom of this
-email).
-
-0. INTRO.  [IBM DB2]
-1. INCREASED DOWNLOAD CAPACITY.
-2. AUDIO OF KERNEL SUMMIT AVAILABLE.
-3. BE A SF.NET FOUNDRY GUIDE.
-4. WORK FOR SOURCEFORGE.NET
-5. SITE STATISTICS
-
-
-0. INTRO
-
-Hello SourceForge.net Users,
-
-This week we've made a big announcement.  As you likely know, any
-large dynamic website is powered by a database that funnels data to
-the web servers serving data which ultimately gets sent to you.
-These databases manage everything from user authentication, session
-management, site searching, etc.  SourceForge.net is a database-
-dependent website.
-
-Today we have announced that we are moving SourceForge.net to DB2,
-a powerful relational database by IBM.  We are doing this because
-the site continues to grow at a rapid rate, with 700 new users and
-70 new projects a day, and we need a database that can handle this
-growth.  We feel that DB2 can do this for us, and IBM is giving us
-the resources to make this transition successful.  You can read the
-press release here:
-
-http://www.vasoftware.com/news/press.php/2002/1070.html
-
-How will this effect you?  In the first phase, you won't see much
-difference other then the site will continue to grow and the
-SourceForge.net team will be able to handle the growth.  In later
-phases you will see new features on the site that take advantage of
-the databases advanced capabilities.
-
-Today our mail archives have been converted over.  The rest of the
-site will make the migration to DB2 in the coming months.
-
-If you have questions about this or any other aspect of the site,
-please feel free to email me, pat@sourceforge.net.  I always
-appreciate the feedback.
-
-Thank you for your continued support of SourceForge.net and the
-Open Source Community.
-
-Pat-
-
-Patrick McGovern
-Director, SourceForge.net
-
-
-
-1. INCREASED DOWNLOAD CAPACITY
-
-SourceForge.net continues to grow, and it's appetite for bandwidth
-is never-ending.  Every day SF.NET serves over 300,000 files to
-ensure that developers and end-users within the Open Source
-community can always obtain the software released by hosted
-projects, SourceForge.net maintains a network of high-capacity
-download servers.  These servers are located throughout the world,
-as to provide better download times regardless of which network
-provider you are using, and regardless of your geographic location.
-
-Three new download servers have recently been added to our network,
-further strengthening our file serving capabilities.  These latest
-additions include servers hosted by:
-
-Time Warner Telecom (Wisconsin,USA);
-http://www.twtelecom.com/
-
-University of Minnesota (Minnesota, USA)
-http://www.umn.edu/
-
-CESNET (Czech Republic)
-http://www.cesnet.cz/
-
-We thank these sponsors for their commitment to SourceForge.net and
-the needs of the Open Source community.
-
-On a related note, we are looking for a mirror in Japan.  If you are
-an ISP or University in Japan and are willing to spare 20Mbps for a
-SourceForge.net mirror (we'll supply the hardware), please let us
-know at bandwidth@sourceforge.net
-
-
-
-2. AUDIO OF KERNEL SUMMIT AVAILABLE
-
-SourceForge.net now has the audio from the entire 2002 OSDN/USENIX
-Kernel Summit, held in June.  Listen to the Linux kernel master
-discuss such hot topics as kernel modules, virtual memory,
-block I/O, database scaling, security modules, and async I/O.
-You may find this audio repository at:
-
-http://linuxkernel.foundries.sourceforge.net/article.pl?sid=02/06/26/0116225
-
-
-
-3. CONTRIBUTE TO SOURCEFORGE.NET! BE A FOUNDRY GUIDE!
-
-Want to contribute to SourceForge.net, but you don't know how to
-code? Be a foundry guide!  Foundry guides get to hype the cool
-projects that they think are worth downloading and testing.
-A guide finds all the stuff on the web about their subject of
-choice, and gives it prominent placement.  How do you become a
-foundry guide? Go to http://foundries.sourceforge.net/; find a
-topic that interests you; and send email to
-foundries@sourceforge.net stating your desired topic and why you
-are qualified to be a foundry guide.
-
-
-
-4. WORK FOR SOURCEFORGE.NET
-
-We have a new position for a senior web developer available at
-SourceForge.net.  We are looking for someone to help us maintain,
-upgrade, and add new features to SourceForge.net.  Ideal person has
-5+ years of development experience on high end, high volume
-websites (3+ million page views a day).  Has a vast level of
-knowledge of Internet technologies:  PHP, PostgreSQL, MySQL, DB2,
-Linux, PERL, Apache, LDAP, Mailman.  A flare for design / UI is a
-bonus.  SourceForge is a unique site with unique challenges.  We are
-looking for someone at the top of their game.
-
-Location of Job is in Fremont, California.  Please send resume and
-URL's of sites you have worked on to jobs@sourceforge.net.  Text
-resumes only.  (No MS WORD files!)
-
-
-
-5. SITE STATISTICS
-
-Stats: (Monday 12th, 2000)
-Hosted Projects: 45,194
-Registered Users: 465,530
-Page Views: 3,344,708 in a single day (Monday)
-Files transfered in a single day: 340,838 (Monday)
-Emails sent in a single day from Mailing lists: 851,143 (Monday)
-
-
-Top Ten Projects
-
-1 phpMyAdmin
-http://sourceforge.net/projects/phpmyadmin/
-phpMyAdmin is a tool written in PHP intended to handle the
-administration of MySQL over the WWW.  Currently it can create and
-drop databases, create/drop/alter tables, delete/edit/add fields,
-execute any SQL statement, manage keys on fields.
-
-2 Compiere ERP + CRM Business Solution
-http://sourceforge.net/projects/compiere/
-Smart ERP+CRM solution for small-medium enterprises (SME) in the
-global marketplace covering all areas from customer management,
-supply chain and accounting.  For $2-200M revenue companies looking
-for "brick and click" first tier functionality.
-
-3 SquirrelMail
-http://sourceforge.net/projects/squirrelmail/
-SquirrelMail is a PHP4-based Web email client.  It includes built-in
-pure PHP support for IMAP and SMTP, and renders all pages in pure
-HTML 4.0 for maximum compatibility across browsers.  It also has
-MIME support, folder manipulation, etc
-
-4 TUTOS
-http://sourceforge.net/projects/tutos/
-TUTOS is the ultimate team organization software, a web-based
-groupware or ERP/CRM system to manage events/calendars, addresses,
-teams, projects,tasks,bugs,mailboxes,documents and your time spent
-with these things
-
-5 JBoss.org
-http://sourceforge.net/projects/jboss/
-The JBoss/Server is the leading Open Source, standards-compliant,
-J2EE based application server implemented in 100% Pure Java
-
-6 Firewall Builder
-http://sourceforge.net/projects/fwbuilder/
-Object-oriented GUI and set of compilers for various firewall
-platforms.  Currently implemented compilers for iptables, ipfilter
-and OpenBSD pf
-
-7 openMosix
-http://sourceforge.net/projects/openmosix/
-openMosix is a Linux kernel extension for single-system image
-clustering.  Taking n PC boxes, openMosix gives users and
-applications the illusion of one single computer with n CPUs.
-openMosix is perfectly scalable and adaptive.
-
-8 CDex
-http://sourceforge.net/projects/cdexos/
-CDex a CD-Ripper, thus extracting digital audio data from an Audio
-CD. The application supports many Audio encoders, like MPEG
-(MP2,MP3), VQF, AAC encoders.
-
-9 phpChrystal - An Open Intranet System
-http://sourceforge.net/projects/phpchrystal/
-phpChrystal ist ein OpenSource-Intranetsystem welches vorrangig auf
-Lan-Partys eingesetzt werden kann.  Vorteile von phpChrystal sind
-seine Portierbarkeit, Flexibilität und Performance, da es vollends
-auf PHP, MySQL und XML basiert
-
-10 Dev-C++
-http://sourceforge.net/projects/dev-cpp/
-Dev-C++ is an full-featured Integrated Development Environment
-(IDE) for Win32 and Linux.  It uses GCC, Mingw or Cygwin as
-compiler and libraries set.
-
-More Top Projects:
-http://sourceforge.net/top/mostactive.php?type=week
-
-
-
-
-
-EMAIL LIST REMOVAL:
-
-When the SF.NET team sends out a site-wide email, we sometimes see
-replies that look like this:  "Hey!! I didn't subscribe to this list!!!
-You spammer. I hate you!  I hate your dog!  (insert other colorful
-phrases here)".  The truth is, when you registered on SourceForge.net
-there was a check box that said "Receive Site-wide updates, low
-volume".  You left it checked when you submitted the registration form,
-hence you are receiving this email.  We send these updates every 4 to 6
-weeks, so it truly is low volume.  However if you want off, this is not
-a problem.  Simply click on the link below.
-
-
-==================================================================
-You receive this message because you subscribed to SourceForge
-site mailing(s). You may opt out from some of them selectively
-by logging in to SourceForge and visiting your Account Maintenance
-page (http://sourceforge.net/account/), or disable them altogether
-by visiting following link:
-<http://sourceforge.net/account/unsubscribe.php?ch=_ac9123456755a6f7>
-
-
diff --git a/upstream/t/data/whitelists/winxpnews.com b/upstream/t/data/whitelists/winxpnews.com
deleted file mode 100644 (file)
index 6df65ee..0000000
+++ /dev/null
@@ -1,528 +0,0 @@
-Received: from ooooooooo.net (ns1.ooooooooo.net [216.27.147.130])
-       by dogma.slashnull.org (8.11.6/8.11.6) with ESMTP id g6J6AZJ25232
-       for <aaaaaa@yyyyyy.zzz>; Fri, 19 Jul 2002 07:10:35 +0100
-Received: from bounce.winxpnews.com (dal21037lyr001.datareturn.com [216.46.238.20])
-       by ooooooooo.net (8.11.3/8.11.1) with SMTP id g6J6ABS16827
-       for <zzzz@zzzzzzzz.com>; Fri, 19 Jul 2002 02:10:12 -0400 (EDT)
-       (envelope-from do_not_reply@bounce.winxpnews.com)
-Importance: Normal
-To: zzzz@zzzzzzzz.com
-Reply-To: "WinXPnews"<do_not_reply@bounce.winxpnews.com>
-Content-Type: text/html;
-        charset="us-ascii"
-Content-Transfer-Encoding: 7bit
-Date: Fri, 19 Jul 2002 01:10:07 -0600
-From: "WinXPnews"<do_not_reply@bounce.winxpnews.com>
-Subject: WinXPnews: Time To Patch Your Windows Media Player
-Message-Id: <5ksc2.105x1y34m@bounce.winxpnews.com>
-X-Priority: 3 (Normal)
-X-MSMail-Priority: Normal
-
-<html><head><!--
-***************************** WinXPnews HTML ****************************
-     If you can see this text, then you are not using an HTML enabled
-     email client or your email client could not interpret this HTML.
-                 Please read the following instructions!
-  This is a posting from WinXPnews for zzzz@zzzzzzzz.com
-  To manage your profile, click on the following customized link:
-         http://www.winxpnews.com/login.cfm?id=9665862091709486
-  You can modify or delete your profile there. You may also forward this
-  email to listmanager@winxpnews.com stating that you wish to be removed
-  from WinXPnews. Please include this complete text section in your email.
- --- Read this newsletter online by visiting http://www.winxpnews.com ---
- --- Please disregard all the text below as it is HTML formatted text ---
-***************************** WinXPnews HTML *****************************
--->
-<title>WinXPnews&#153;</title>
-<style type="text/css">
-a:link {color: #b04040; font-weight: bold;}
-a:visited {color: #804040; font-weight: bold;}
-a:active {color: #ff0000; font-weight: bold;}
-a:hover {color: #ff0000; font-weight: bold;} </style>
-</head>
-<body bgcolor="#ffffff" topmargin="0" leftmargin="0" marginheight="0" marginwidth="0">
-<table width = '100%' border = '0'>
-<tr>
-<td bgcolor = '#0055e7' align='right'>
-<img src='http://www.winxpnews.com/graphics/winxpnews_logo_200.jpg' align='left' border='0'>
-<font face='verdana, sans-serif' size='5' color='#ffffff'>
-<b>WinXPnews&#153; E-Zine</b><br>
-</font>
-<font face='verdana, sans-serif' size='1' color='#ffffff'>
-Tue, Jul 9, 2002 (Vol. 2, 27 - Issue 33)
-</font>
-</td>
-</tr>
-<tr><td align='center'><font face='verdana, sans-serif' size='2'><b>
-Feel free to forward this newsletter to other WinXP enthusiasts.</b><br>
-<b>Read this newsletter online here:
-<a href="http://www.winxpnews.com/?id=33">
-http://www.winxpnews.com/?id=33</a><br>
-For a quick unsubscribe (gasp!) click here:<br>
-<a href="http://www.winxpnews.com/unsubscribe.cfm?email=zzzz@zzzzzzzz.com">
-http://www.winxpnews.com/unsubscribe.cfm?email=zzzz@zzzzzzzz.com</a></b>
-</font>
-<img src='http://www.winxpnews.com/tr/tr.cfm?mid=9665862091709486&xid=33'
-width='0' height='0' border='0'>
-</td></tr>
-<tr>
-<td>
-<font face='verdana, sans-serif' size='5'>
-<b>Time To Patch Your Windows Media Player</b>
-</font>
-</td>
-</tr>
-<tr>
-<td>&nbsp;</td>
-</tr>
-<tr>
-<td background='http://www.winxpnews.com/graphics/h_border.gif' align=left>
-<img src='http://www.winxpnews.com/graphics/winxpnews_logo_50.jpg' width='53' height='24' align='right'>
-<font face='verdana, sans-serif' size='4' color='#ffffff'>
-&nbsp;&nbsp;This issue of WinXPnews&#153 contains:<br>
-</font>
-</td>
-</tr>
-<tr>
-<td><font size='1'>&nbsp;</td>
-</tr>
-<tr>
-<td>
-<font face='verdana, sans-serif' size='2'>
-<ol>
-<li>EDITOR'S CORNER
-<code1=zzzz@zzzzzzzz.c
-<ul type='square'>
-<li>How to Publish Your Windows XP FTP Server to the Internet
-</ul>
-<li>HINTS, TIPS, TRICKS & TWEAKS
-<ul type='square'>
-<li>Allow Dial-up Connections to Synchronize Time with Internet Time Servers
-</ul>
-<li>HOW TO'S: ALL THE NEW XP FEATURES
-<ul type='square'>
-<li>How to Secure an FTP Server on Windows XP Professional
-</ul>
-<li>WINXP SECURITY: UPDATES & PATCHES
-<ul type='square'>
-<li>Cumulative Patch for Windows Media Player<li>Cumulative Patches for Excel and Word for Windows
-</ul>
-<li>UPGRADING & COMPATIBILITY ISSUES
-<ul type='square'>
-<li>A Computer May Hang During a Heavy Load with an Ericsson HIS Modem<li>Knowledge Base Search Center - If it is Not Broke, Do Not Break it!
-</ul>
-<li>WINXP CONFIGURING & TROUBLESHOOTING
-<ul type='square'>
-<li>A Description of the Repair Option on a Local Area Network or High-Speed Internet Connection<li>Keyboard and Mouse Do Not Work When You Start Windows<li>How to Deploy Windows XP Images from Windows 2000 RIS Servers
-</ul>
-<li>FAVE LINKS
-<ul type='square'>
-<li>This Week's Links We Like. Tips, Hints And Fun Stuff
-</ul>
-<li>BOOK OF THE WEEK
-<ul type='square'>
-<li>Windows XP Power Tools
-</ul>
-</ol>
-</font>
-</td>
-</tr>
-<tr>
-<td>&nbsp;</td>
-</tr>
-<tr>
-<td background='http://www.winxpnews.com/graphics/h_border.gif' align=left>
-<img src='http://www.winxpnews.com/graphics/winxpnews_logo_50.jpg' width='53' height='24' align='right'>
-<font face='verdana, sans-serif' size='4' color='#ffffff'>
-&nbsp;&nbsp;SPONSOR: iHateSpam - Eliminate Irritating Junk Email<br>
-</font>
-</td>
-</tr>
-<tr>
-<td><font size='1'>&nbsp;</td>
-</tr>
-<tr>
-<td>
-<a href="http://www.winxpnews.com/rd/rd.cfm?id=020709S1-iHateSpam&mid=9665862091709486" target="_top">
-<img src='http://www.winxpnews.com/ads/ihs.gif' align='right' border='0'>
-</a>
-<font face='verdana, sans-serif' size=2>
-Irritated with porn, bogus business offers and viagra ads in your mailbox?<br>
-Angry about losing your valuable time deleting all that junk? Need a spam-<br>
-blocker that eliminates this annoying spam? Stop the spam in your inbox<br>
-with iHateSpam. It gives you control over the ever increasing flood of <br>
-junk email. Runs under Windows 95/98/ME/NT/2000/XP. Best of all, the limited<br>
-time Intro Offer is just $19.95 with online delivery of full product and a <br>
-30-day money back guarantee. This is a real no-brainer. <b>Get Your Copy Now!</b><br>
-<b>Visit <a href="http://www.winxpnews.com/rd/rd.cfm?id=020709S1-iHateSpam&mid=9665862091709486" target="_top">iHateSpam - Eliminate Irritating Junk Email</a> for more information.</b>
-</font>
-</td>
-</tr>
-<tr>
-<td>&nbsp;</td>
-</tr>
-<tr>
-<td background='http://www.winxpnews.com/graphics/h_border.gif' align=left>
-<img src='http://www.winxpnews.com/graphics/winxpnews_logo_50.jpg' width='53' height='24' align='right'>
-<font face='verdana, sans-serif' size='4' color='#ffffff'>
-&nbsp;&nbsp;EDITOR'S CORNER<br>
-</font>
-</td>
-</tr>
-<tr>
-<td><font size='1'>&nbsp;</td>
-</tr>
-<tr>
-<td>
-<font face='verdana, sans-serif' size='2'>
-<p><font size=3><b>How to Publish Your Windows XP FTP Server to the Internet</b></font><p>
-Several of you wrote in about last week's article on installing an FTP Server. You said "that was great, but you only told half the story". You wanted to know two more things:
-<ol>
-<li>How to make the FTP Server available to Internet users
-<li>How to secure the FTP Server
-</ol>
-There are several ways to make an FTP server on the internal network available to users on the Internet. These methods are referred to as "Server Publishing". You can use a Windows XP computer running Internet Connection Services (ICS) to publish a server on your internal network.
-<p>
-Let's take a look at a common scenario. You have a Windows XP computer connected to the Internet with an always-on cable or DSL connection. You have another computer on your private network also running Windows XP. You've installed the FTP Server on this internal network computer and put files into the FTP folder. Now you want Internet users to connect to the FTP Server through the ICS computer directly connected to the Internet.
-<p>
-You can do this with the Windows XP ICS! Here's how:
-<ol>
-<li>Go into the Network Connections window. You can get there from the Network applet in the Control Panel.
-<li>Right click the network interface directly connected to the Internet and click Properties.
-<li>Click on the Advanced tab in the connection's Properties dialog box. Put a checkmark in the Internet Connection Firewall checkbox. Always make sure the Internet Connection Firewall (ICF) is enabled when you connect a computer directly to the Internet.
-<li>Click the Settings button, then click on the Services tab in the Advanced Settings dialog box.
-<li>Now click the Add button. This brings up the Service Settings dialog box. Type in My FTP Server in the Description of service text box. In the Name or IP address text box, type in the IP address of the computer on your private network that's running the FTP server. Since you're using ICS, it'll have an IP address like 192.168.0.x, where x is different for each machine on your network. You might want to manually assign the IP address the FTP Server already has, so that it doesn't change in the future. You can find out what IP address your FTP server is using by opening a command prompt at the FTP server and typing in the command ipconfig. That will give you the IP address the FTP Server is using. Back to the Service Settings dialog box, select the TCP option button. For the External Port and the Internet port, put in the port number you assigned to the FTP server on your internal network. Read this week's How To section to see how to change the listening port number. Clic!
-k OK
-<li>Click OK, and then click OK one more time! You might need to disable and enable the adapter after making the change. You can do that by right clicking the always-on interface.
-</ol>
-The procedure is very similar for dial-up connections. However, there are problems with dial-up connections (and many always-on connections) because the IP address on the external interface of the ICS computer changes over time. Next week I'll share with you a cool way you can get around this problem by using something called a "dynamic DNS service". I've used one for years, and it works great. Make sure to tune in next week for the details.
-<p>
-There you have it. Is server publishing in your future? Have any questions on the method I described above? If so, let me know! There are lots of ways you can publish services. Tell me how you do it, and tricks you've learned along the way. If you're having problems with server publishing, let me know about those too! I'll be sure to include what I learn from you in upcoming newsletters.
-<p>
-Until next week,<br>
-Tom Shinder, Editor<br>
-(email us with feedback: <a href="mailto:feedback@winxpnews.com?subject=WinXPnews Issue #33">feedback@winxpnews.com</a>)
-</font>
-</td>
-</tr>
-<tr>
-<td>&nbsp;</td>
-</tr>
-<tr>
-<td background='http://www.winxpnews.com/graphics/h_border.gif' align=left>
-<img src='http://www.winxpnews.com/graphics/winxpnews_logo_50.jpg' width='53' height='24' align='right'>
-<font face='verdana, sans-serif' size='4' color='#ffffff'>
-&nbsp;&nbsp;SPONSOR: Is Your PC Spying On You?<br>
-</font>
-</td>
-</tr>
-<tr>
-<td><font size='1'>&nbsp;</td>
-</tr>
-<tr>
-<td>
-<a href="http://www.winxpnews.com/rd/rd.cfm?id=020709S2-PestPatrol&mid=9665862091709486" target="_top">
-<img src='http://www.winxpnews.com/ads/pestpatrol.gif' align='right' border='0'>
-</a>
-<font face='verdana, sans-serif' size=2>
-You are surfing the Web. Check out sites, download some music or<br>
-software that might be cool. Guess what? Your PC might have picked up<br>
-a cyber transmitted disease (CTD). These pests might now be monitoring <br>
-what you are doing and report this back to their "black hat" owners <br>
-and reveal your personal information. PestPatrol kills 'em all off. <br>
-Get your copy on the online shop for just 30 bucks with immediate online<br> delivery. Protect your PC and your confidential data!<br>
-<b>Visit <a href="http://www.winxpnews.com/rd/rd.cfm?id=020709S2-PestPatrol&mid=9665862091709486" target="_top">Is Your PC Spying On You?</a> for more information.</b>
-</font>
-</td>
-</tr>
-<tr>
-<td>&nbsp;</td>
-</tr>
-<tr>
-<td background='http://www.winxpnews.com/graphics/h_border.gif' align=left>
-<img src='http://www.winxpnews.com/graphics/winxpnews_logo_50.jpg' width='53' height='24' align='right'>
-<font face='verdana, sans-serif' size='4' color='#ffffff'>
-&nbsp;&nbsp;HINTS, TIPS, TRICKS & TWEAKS<br>
-</font>
-</td>
-</tr>
-<tr>
-<td><font size='1'>&nbsp;</td>
-</tr>
-<tr>
-<td>
-<font face='verdana, sans-serif' size='2'>
-<p><font size=3><b>Allow Dial-up Connections to Synchronize Time with Internet Time Servers</b></font><p>
-Do you use a dial-up connection but can't get your machine to synchronize its clock with an Internet time server when the Internet Connection Firewall (ICF) is enabled? If so, here's a tip Richard Surry sent in on how to fix the problem:
-<ol>
-<li>Open your Network Connections window from the start menu.
-<li>Right click on your modem (or other dial-up connection) and click Properties.
-<li>Click on the Advanced tab. You already have a checkmark in the box that enables the ICF. Click on the Settings button.
-<li>Click on the Services tab, then click on the Add button in the Services tab.
-<li>That should open the Service Settings dialog box. In the Description box, put in Internet Time Service. For the Name or IP address of the computer hosting this service on your network, type in 127.0.0.1. Select the TCP  protocol option button. For both the external and internal port numbers, type 123.
-<li>If you're online, disconnect and reconnect. Now synchronize the time by double click on the clock in the system tray and going to the Internet Time tab.
-</ol>
-This is an interesting tip, and it represents an even more interesting problem. For you network geeks out there, I'll ask you this question: Why should we allow unsolicited inbound connections for the Internet Time Service? The ICF should not block responses to solicited outbound connections, so why should we have to enable reverse NAT to make this work?
-</font>
-</td>
-</tr>
-<tr>
-<td>&nbsp;</td>
-</tr>
-<tr>
-<td background='http://www.winxpnews.com/graphics/h_border.gif' align=left>
-<img src='http://www.winxpnews.com/graphics/winxpnews_logo_50.jpg' width='53' height='24' align='right'>
-<font face='verdana, sans-serif' size='4' color='#ffffff'>
-&nbsp;&nbsp;HOW TO'S: ALL THE NEW XP FEATURES<br>
-</font>
-</td>
-</tr>
-<tr>
-<td><font size='1'>&nbsp;</td>
-</tr>
-<tr>
-<td>
-<font face='verdana, sans-serif' size='2'>
-<p><font size=3><b>How to Secure an FTP Server on Windows XP Professional</b></font><p>
-Last week we went over how to install the Windows XP FTP Server. It will work fine after going through the steps outlined last week, but several of you asked for more information on how to secure the FTP Server because you wanted to connect it to the Internet. It's a very good idea to understand how FTP security works before putting the server on the Internet. Here are some suggestions:
-<ol>
-<li>Open the Internet Information Services console from the Administrative Tools menu. In the left pane of the console, expand your server name and then expand the FTP Sites node.
-<li>Right click on the Default FTP Site and click the Properties command.
-<li>Click on the FTP Site tab. Notice that the default TCP Port is set to 21. This is the well-known port for FTP. You can increase security a bit by changing this port to another value that's in the 1026-65534 range. This secures it from poorly motivated click-kiddies and also allows you to get around your ISP blocking incoming connections to TCP port 21. Friends who connect to your FTP server will need to change the port number on their FTP client software as well.
-<li>The Windows XP FTP server has a hard coded limit of 10 simultaneous connections. You might want to change this to a lower number to reduce the chance of a LAN party on the external interface of the FTP server.
-<li>Put a checkmark in the Enable Logging checkbox. Click the Properties button to the right of the log format drop-down list box. Click the Daily option button on the General Properties tab. On the Extended Properties tab, select all of the Extended Properties. Click OK.
-<li>Click on the Security Accounts tab. Place a checkmark in the Allow only anonymous connections checkbox. This prevents users from sending username and password credentials to the FTP server. You don't want users to send credentials because those credentials are sent in "clear text", which can be read by anyone who's listening on the wire.
-<li>Click the Messages tab. Enter a Welcome message, an Exit message, and a message users will see if there are no available connections.
-<li>Click on the Home Directory tab. Make sure there is a checkmark in the Read and Log Visits checkboxes. REMOVE the checkmark in the Write checkbox. Note the location in the Local Path text box. Navigate to that path in the Windows Explorer.
-<li>Right click on the FTPROOT folder and click Properties.
-<li>Click on the Security tab. Make sure that SYSTEM has Full Control. Assign the IUSR_<computername> account READ access only. Remove all other permissions for the IUSR account. Make sure you give Adminstrators Full Control tool. This allows you, the administrator on the FTP Server computer, to add, remove and change files in the FTPROOT folder.
-</ol>
-Stop and restart the FTP Server. Now your FTP server is secure and Internet bad guys won't be able to use it to distribute porno and bootlegged software.
-</font>
-</td>
-</tr>
-<tr>
-<td>&nbsp;</td>
-</tr>
-<tr>
-<td background='http://www.winxpnews.com/graphics/h_border.gif' align=left>
-<img src='http://www.winxpnews.com/graphics/winxpnews_logo_50.jpg' width='53' height='24' align='right'>
-<font face='verdana, sans-serif' size='4' color='#ffffff'>
-&nbsp;&nbsp;WINXP SECURITY: UPDATES & PATCHES<br>
-</font>
-</td>
-</tr>
-<tr>
-<td><font size='1'>&nbsp;</td>
-</tr>
-<tr>
-<td>
-<font face='verdana, sans-serif' size='2'>
-<p><font size=3><b>Cumulative Patch for Windows Media Player</b></font><p>
-I think it was a couple months ago when I wrote about some serious problems with the Windows Media Player (WMP). At that time you could download a "cumulative" patch that would update the Media Player with the latest security fixes. Well, it's time to download another "cumulative" patch! A couple other problems were found in WMP that could cause some problems. To read more about the problem head on over to:<bR>
-<a href="http://www.winxpnews.com/rd/rd.cfm?id=020709SE-WMP_Patch&mid=9665862091709486" target="_top">http://www.winxpnews.com/rd/rd.cfm?id=020709SE-WMP_Patch</a>
-<p>
-You'll also find the download locations for Windows Media Player versions 6.4, 7.1 and 8.0 (XP) on that page.
-<p><font size=3><b>Cumulative Patches for Excel and Word for Windows</b></font><p>
-If you run Microsoft Word or Excel, versions 2000 or 2002 (XP), then you need to head on over to the Microsoft site to download some security fixes. These fixes handle security glitches that could get you in trouble if you don't take care of them! Head on over to Microsoft's site where you can find individual fixes for each program. You only need download the fix that applies to your computer:<br>
-<a href="http://www.winxpnews.com/rd/rd.cfm?id=020709SE-Word_Excel_Patch&mid=9665862091709486" target="_top">http://www.winxpnews.com/rd/rd.cfm?id=020709SE-Word_Excel_Patch</a>
-</font>
-</td>
-</tr>
-<tr>
-<td>&nbsp;</td>
-</tr>
-<tr>
-<td background='http://www.winxpnews.com/graphics/h_border.gif' align=left>
-<img src='http://www.winxpnews.com/graphics/winxpnews_logo_50.jpg' width='53' height='24' align='right'>
-<font face='verdana, sans-serif' size='4' color='#ffffff'>
-&nbsp;&nbsp;UPGRADING & COMPATIBILITY ISSUES<br>
-</font>
-</td>
-</tr>
-<tr>
-<td><font size='1'>&nbsp;</td>
-</tr>
-<tr>
-<td>
-<font face='verdana, sans-serif' size='2'>
-<p><font size=3><b>A Computer May Hang During a Heavy Load with an Ericsson HIS Modem</b></font><p>
-If your computer has a Ericsson HIS modem, you might experience a dreaded blue screen and see the message IRQL_NOT_LESS_OR_EQUAL or DRIVER_CORRUPTED_EXPOOL. The problem is that you're downloading too much and your poor modem can't keep up! Microsoft recognizes that this isn't a problem with the modem, but with the modem driver. To download a fix visit Microsoft's site. After getting the fix, you can download as much as you like without worrying about blue screens!<br>
-<a href="http://www.winxpnews.com/rd/rd.cfm?id=020709UP-HIS_Modem&mid=9665862091709486" target="_top">http://www.winxpnews.com/rd/rd.cfm?id=020709UP-HIS_Modem</a>
-<p><font size=3><b>Knowledge Base Search Center - If it is Not Broke, Do Not Break it!</b></font><p>
-It wasn't so long ago when you could search the Microsoft Knowledge Base for articles that came up in the last 3 days, 7 days, 14 days, 30 days, 90 days and 6 months. It was great! But Microsoft decided to "fix" the Knowledge Base search page, and now it really sucks! It's hard to find things that used to come up easily, the site is often down, and searching based on age of articles just doesn't work anymore.
-<p>
-Try this: go to:<br>
-<a href="http://support.microsoft.com/default.aspx?ln=EN-US&pr=kbinfo&" target="_top">http://support.microsoft.com/default.aspx?ln=EN-US&pr=kbinfo&</a><br>
-and on the left side of the page select Windows XP in the top drop down list box. Don't put anything in the For solutions containing...(optional) text box. Leave the Any of the words entered option selected in the Using drop down list box. For Maximum Age select 3 days. For Results Limit select 150 articles. Click Search Now. Whoa! Nothing. OK, it's reasonable to see no articles related to Windows XP in the last 3 days. Try again, this time using 7 days. Whaat? Still no articles. OK, it was a holiday week in the USA last week. Let's try 14 days. Nothing again! That seems sort of strange, doesn't it? Let's give it another try with 30 days. Still no articles! What's going on here? Keep trying for 6 months and one year. You still won't find anything. It's pretty sad, because this used to work.
-</font>
-</td>
-</tr>
-<tr>
-<td>&nbsp;</td>
-</tr>
-<tr>
-<td background='http://www.winxpnews.com/graphics/h_border.gif' align=left>
-<img src='http://www.winxpnews.com/graphics/winxpnews_logo_50.jpg' width='53' height='24' align='right'>
-<font face='verdana, sans-serif' size='4' color='#ffffff'>
-&nbsp;&nbsp;WINXP CONFIGURING & TROUBLESHOOTING<br>
-</font>
-</td>
-</tr>
-<tr>
-<td><font size='1'>&nbsp;</td>
-</tr>
-<tr>
-<td>
-<font face='verdana, sans-serif' size='2'>
-<p><font size=3><b>A Description of the Repair Option on a Local Area Network or High-Speed Internet Connection</b></font><p>
-Here's the answer to a question I've had for a long time. What the heck does that "Repair" option for a network connection actually do? It's not in the help file, but it's on the Microsoft Web site. Here's what it does:
-<ul>
-<li>Sends an ipconfig /renew
-<li>Flushes the ARP cache with a arp -d
-<li>Reloads the NetBIOS name cache with a nbtstat -R
-<li>Updates its WINS server with an nbtstat -RR
-<li>Clear out the DNS client cache with an ipconfig /flushdns
-<li>Reregisters the client with a DDNS server with a ipconfig /registerdns
-</ul>
-Check out the original article over at:<br>
-<a href="http://www.winxpnews.com/rd/rd.cfm?id=020709CO-Repair_Option&mid=9665862091709486" target="_top">http://www.winxpnews.com/rd/rd.cfm?id=020709CO-Repair_Option</a>
-<p><font size=3><b>Keyboard and Mouse Do Not Work When You Start Windows</b></font><p>
-Have you been hit with this one? You're working in Windows XP and shut down for the day. The next morning you start up your Windows XP computer and the mouse pointer is stuck! The only way to get it going again is to restart the computer, and for some reason the pointer starts moving again. What's up with that? I still haven't figured that one out, but Microsoft has a KB article that claims it's from a corrupt registry. I doubt that's the case in my situation because the problem is intermittent. But if you find that your mouse is always stuck, you might want to check out:<br>
-<a href="http://www.winxpnews.com/rd/rd.cfm?id=020709CO-Frozen_Mouse&mid=9665862091709486" target="_top">http://www.winxpnews.com/rd/rd.cfm?id=020709CO-Frozen_Mouse</a>
-<p><font size=3><b>How to Deploy Windows XP Images from Windows 2000 RIS Servers</b></font><p>
-Are you planning to roll out lots of Windows XP computers on your network in the near future? If so, you're probably looking for a good way to automate the process. You can use the Windows 2000 Remote Installation Services (RIS) if you're running Windows 2000 Servers on your network. For the basic procedure and some tips, tricks, and gotcha's, check out:<br>
-<a href="http://www.winxpnews.com/rd/rd.cfm?id=020709CO-Deploy_XP_Images&mid=9665862091709486" target="_top">http://www.winxpnews.com/rd/rd.cfm?id=020709CO-Deploy_XP_Images</a>
-</font>
-</td>
-</tr>
-<tr>
-<td>&nbsp;</td>
-</tr>
-<tr>
-<td background='http://www.winxpnews.com/graphics/h_border.gif' align=left>
-<img src='http://www.winxpnews.com/graphics/winxpnews_logo_50.jpg' width='53' height='24' align='right'>
-<font face='verdana, sans-serif' size='4' color='#ffffff'>
-&nbsp;&nbsp;FAVE LINKS<br>
-</font>
-</td>
-</tr>
-<tr>
-<td><font size='1'>&nbsp;</td>
-</tr>
-<tr>
-<td>
-<font face='verdana, sans-serif' size='2'>
-<p><font size=3><b>This Week's Links We Like. Tips, Hints And Fun Stuff</b></font><p><li>Be Afraid, be very afraid - the future of Big Brother in computing</li><br>
-<a href="http://www.winxpnews.com/rd/rd.cfm?id=020709FA-Palladium_FAQ&mid=9665862091709486" target="_top">http://www.winxpnews.com/rd/rd.cfm?id=020709FA-Palladium_FAQ</a>
-<li>Get Revenge on your computer!</li><br>
-<a href="http://www.winxpnews.com/rd/rd.cfm?id=020709FA-PC_Revenge&mid=9665862091709486" target="_top">http://www.winxpnews.com/rd/rd.cfm?id=020709FA-PC_Revenge</a>
-<li>Pringles Super Spud Boxing</li><br>
-<a href="http://www.winxpnews.com/rd/rd.cfm?id=020709FA-Spud_Boxing&mid=9665862091709486" target="_top">http://www.winxpnews.com/rd/rd.cfm?id=020709FA-Spud_Boxing</a>
-</font>
-</td>
-</tr>
-<tr>
-<td>&nbsp;</td>
-</tr>
-<tr>
-<td background='http://www.winxpnews.com/graphics/h_border.gif' align=left>
-<img src='http://www.winxpnews.com/graphics/winxpnews_logo_50.jpg' width='53' height='24' align='right'>
-<font face='verdana, sans-serif' size='4' color='#ffffff'>
-&nbsp;&nbsp;BOOK OF THE WEEK<br>
-</font>
-</td>
-</tr>
-<tr>
-<td><font size='1'>&nbsp;</td>
-</tr>
-<tr>
-<td>
-<font face='verdana, sans-serif' size='2'>
-<p><font size=3><b>Windows XP Power Tools</b></font><p>
-A book full of personal experiences and anecdotes that will equip you with the tips and tricks you need to become an XP afficionado. Coverage includes automating tasks using scripting, the Command Console Survivor Guide, networking, registry, maximizing security/firewalls, hardware, installation/configuration, and database hosting/accessing. The CD contains the best third party utilities around.
-<p>
-Step-by-Step Instruction Helps You Harness the Full Power of Windows XP. Whether you're running Windows XP Home Edition or Professional, Windows XP Power Tools arms you with the advanced skills you need to become the ultimate power user. Full of undocumented tips and tricks and written by a Windows expert, this book provides you with step-by-step instructions for customization, optimization, troubleshooting and shortcuts for working more efficiently. A must-have for power users and network administrators, Windows XP Power Tools includes a CD filled with power tools including security, e-mail, diagnostic and data recovery utilities.
-<p>
-<a href="http://www.winxpnews.com/rd/rd.cfm?id=020709BW-XP_Power_Tools&mid=9665862091709486" target="_top">http://www.winxpnews.com/rd/rd.cfm?id=020709BW-XP_Power_Tools</a>
-</font>
-</td>
-</tr>
-</table>
-<table width="100%" border="0">
-<tr>
-<td>&nbsp;</td>
-</tr>
-<tr>
-<td background='http://www.winxpnews.com/graphics/h_border.gif' align=left>
-<img src='http://www.winxpnews.com/graphics/winxpnews_logo_50.jpg' width='53' height='24' align='right'>
-<font face='verdana, sans-serif' size='4' color='#ffffff'>
-&nbsp;&nbsp;ABOUT WINXPNEWS&#153;<br>
-</font>
-</td>
-</tr>
-<tr>
-<td><font size='1'>&nbsp;</td>
-</tr>
-<tr>
-<td><font size='1'>&nbsp;</font><br>
-<font face='verdana, sans-serif' size=3><b>
-What Our Lawyers Make Us Say</b></font>
-</td>
-</tr>
-<tr>
-<td>
-<font size="1" face="arial">
-These documents are provided for informational purposes only. The information
-contained in this document represents the current view of Sunbelt Software
-Distribution on the issues discussed as of the date of publication. Because
-Sunbelt must respond to changes in market conditions, it should not be
-interpreted to be a commitment on the part of Sunbelt and Sunbelt cannot
-guarantee the accuracy of any information presented after the date of
-publication.
-<p>
-INFORMATION PROVIDED IN THIS DOCUMENT IS PROVIDED "AS IS" WITHOUT WARRANTY OF
-ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED
-WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND FREEDOM
-FROM INFRINGEMENT.
-<p>
-The user assumes the entire risk as to the accuracy and the use of this
-<code2=fsg.com>document. This document may be copied and distributed subject to the
-following conditions: 1) All text must be copied without modification and all pages
-must be included; 2) All copies must contain Sunbelt's copyright notice and any
-other notices provided therein; and 3) This document may not be distributed
-for profit. All trademarks acknowledged. Copyright Sunbelt Software
-Distribution, Inc. 1996-2002.
-</font>
-</td>
-</tr>
-<tr>
-<td><font size='1'>&nbsp;</font><br>
-<font face='verdana, sans-serif' size=3><b>
-About Your Subscription to WinXPnews&#153;</b></font>
-</td>
-</tr>
-<tr>
-<td>
-<font size="2" face="arial, verdana, sans-serif">
-This is a posting from WinXPnews. You are subscribed as zzzz@zzzzzzzz.com
-<p>
-To manage your profile, please visit our site by clicking on the following link:<br>
-<a href="http://www.winxpnews.com/login.cfm?id=9665862091709486">
-http://www.winxpnews.com/login.cfm?id=9665862091709486</a><br>
-For a quick unsubscribe (gasp!), click here:<br>
-<a href="http://www.winxpnews.com/unsubscribe.cfm?email=zzzz@zzzzzzzz.com">
-http://www.winxpnews.com/unsubscribe.cfm?email=zzzz@zzzzzzzz.com</a>
-</font>
-</td>
-</tr>
-</table>
-</body>
-</html>
-
diff --git a/upstream/t/data/whitelists/yahoo-inc.com b/upstream/t/data/whitelists/yahoo-inc.com
deleted file mode 100644 (file)
index 7241b6d..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-Return-Path: <yahoo-dev-null@yahoo-inc.com>
-Delivered-To: zzzzz@xyz.org
-Received: (qmail 8790 invoked by uid 505); 29 Jul 2002 03:28:42 -0000
-Received: from yahoo-dev-null@yahoo-inc.com by blazing.xyz.org by uid 502 with qmail-scanner-1.12 (F-PROT: 3.12. Clear:. Processed in 0.195404 secs); 29 Jul 2002 03:28:42 -0000
-Received: from e5.member.yahoo.com (216.136.131.107)
-  by dsl092-072-xyz.bos1.dsl.speakeasy.net with SMTP; 29 Jul 2002 03:28:42 -0000
-Received: (from yahoo@localhost)
-       by e5.member.yahoo.com (8.11.3/8.11.3) id g6T3PIh88736;
-       Sun, 28 Jul 2002 20:25:18 -0700 (PDT)
-       (envelope-from yahoo-dev-null@yahoo-inc.com)
-Date: Sun, 28 Jul 2002 20:25:18 -0700 (PDT)
-Message-Id: <200207290325.g6T3PIh88736@e5.member.yahoo.com>
-X-Authentication-Warning: e5.member.yahoo.com: yahoo set sender to <yahoo-dev-null@yahoo-inc.com> using -f
-From: Yahoo! Member Services <my-login-request@yahoo-inc.com>
-Errors-To: yahoo-dev-null@yahoo-inc.com
-To: zzzzz@xyz.org
-Subject: Yahoo! Email Verification
-
-[email from Yahoo!]
-
index 4d326c25dd8260fc0484328de84b7919cda7e24d..774d2791fac9d11819248f05071d17d53028d083 100755 (executable)
@@ -1,22 +1,10 @@
 #!/usr/bin/perl -w -T
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_tests.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
-
 use strict;
 
+use lib '.'; use lib 't';
+use SATest; sa_t_init("date");
+
 use Mail::SpamAssassin;
 use Mail::SpamAssassin::Util;
 
index 1e8fa6840e51245f3a26597c6c052431c5dad930..36d5f8a41198d0245413cea9706f916e2052611c 100755 (executable)
@@ -2,10 +2,12 @@
 
 use lib '.'; use lib 't';
 use SATest; sa_t_init("db_awl_path");
-use Test::More tests => 4;
+use Test::More;
+plan tests => 4;
 use IO::File;
 
 # ---------------------------------------------------------------------------
+diag "Note: This test when successful displays lockfile warning messages";
 
 %is_spam_patterns = (
 q{ X-Spam-Status: Yes}, 'isspam',
@@ -15,14 +17,13 @@ q{ X-Spam-Status: Yes}, 'isspam',
 # is tell SpamAssassin to use an inaccessible one, then verify that
 # the address in question was *not* whitelisted successfully.   '
 
-open (OUT, ">log/awl");
+open (OUT, ">$workdir/awl");
 print OUT "file created to block AWL from working; AWL expects a dir";
 close OUT;
 
 tstprefs ("
-        $default_cf_lines
-        auto_whitelist_path ./log/awl/shouldbeinaccessible
-        auto_whitelist_file_mode 0755
+  auto_whitelist_path ./$workdir/awl/this_lock_warning_is_ok
+  auto_whitelist_file_mode 0755
 ");
 
 my $fh = IO::File->new_tmpfile();
@@ -44,4 +45,4 @@ like($error, qr/(cannot create tmp lockfile)|(unlink of lock file.*failed)/, "Ch
 sarun ("-L -t < data/spam/004", \&patterns_run_cb);
 ok_all_patterns();
 
-ok(unlink 'log/awl'); # need a little cleanup
+ok(unlink "$workdir/awl"); # need a little cleanup
diff --git a/upstream/t/db_awl_path_welcome_block.t b/upstream/t/db_awl_path_welcome_block.t
new file mode 100755 (executable)
index 0000000..96e5f0d
--- /dev/null
@@ -0,0 +1,47 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("db_awl_path_welcome_block");
+use Test::More;
+plan tests => 4;
+use IO::File;
+
+# ---------------------------------------------------------------------------
+
+%is_spam_patterns = (
+q{ X-Spam-Status: Yes}, 'isspam',
+);
+
+# We can't easily test to see if a given AWL was used.  so what we do
+# is tell SpamAssassin to use an inaccessible one, then verify that
+# the address in question was *not* welcomelisted successfully.   '
+
+open (OUT, ">$workdir/awl");
+print OUT "file created to block AWL from working; AWL expects a dir";
+close OUT;
+
+tstprefs ("
+  auto_welcomelist_path ./$workdir/awl/this_lock_warning_is_ok
+  auto_welcomelist_file_mode 0755
+");
+
+my $fh = IO::File->new_tmpfile();
+ok($fh);
+open(STDERR, ">&=".fileno($fh)) || die "Cannot reopen STDERR";
+sarun("--add-addr-to-welcomelist whitelist_test\@whitelist.spamassassin.taint.org",
+      \&patterns_run_cb);
+seek($fh, 0, 0);
+my $error = do {
+  local $/;
+  <$fh>;
+};
+
+diag $error;
+like($error, qr/(cannot create tmp lockfile)|(unlink of lock file.*failed)/, "Check we get the right error back");
+
+# and this mail should *not* be welcomelisted as a result.
+%patterns = %is_spam_patterns;
+sarun ("-L -t < data/spam/004", \&patterns_run_cb);
+ok_all_patterns();
+
+ok(unlink "$workdir/awl"); # need a little cleanup
index 8d9acea06b58c3d558ee90a751dd53d8acab7700..95415a1e348640a62fd4fd0d87872e57e1b5e2d8 100755 (executable)
@@ -2,27 +2,29 @@
 
 use lib '.'; use lib 't';
 use SATest; sa_t_init("db_awl_perms");
-use Test::More tests => 5;
 use IO::File;
+use Test::More;
+plan skip_all => "Tests don't work on windows" if $RUNNING_ON_WINDOWS;
+plan tests => 5;
 
 # ---------------------------------------------------------------------------
 # bug 6173
 
 tstprefs ("
-        $default_cf_lines
-        use_auto_whitelist 1
-        auto_whitelist_path ./log/user_state/awl
-        auto_whitelist_file_mode 0755
-        lock_method flock
+  use_auto_whitelist 1
+  auto_whitelist_path ./$userstate/awl
+  auto_whitelist_file_mode 0755
+  lock_method flock
 ");
 
-unlink "log/user_state/awl";
-unlink "log/user_state/awl.mutex";
+unlink "$userstate/awl";
+unlink "$userstate/awl.mutex";
 umask 022;
 sarun("--add-addr-to-whitelist whitelist_test\@example.org",
       \&patterns_run_cb);
 
-untaint_system "ls -l log/user_state";          # for the logs
+# in case this test is ever made to work on Windows
+untaint_system($RUNNING_ON_WINDOWS?("dir " . File::Spec->canonpath($userstate)):"ls -l $userstate");  # for the logs
 
 sub checkmode {
   my $fname = shift;
@@ -31,12 +33,12 @@ sub checkmode {
   return (($mode & 0777) == 0644);
 }
 
-ok checkmode "log/user_state/awl";              # DB_File
-ok checkmode "log/user_state/awl.dir";          # SDBM
-ok checkmode "log/user_state/awl.pag";          # SDBM
-ok checkmode "log/user_state/awl.mutex";
+ok checkmode "$userstate/awl";              # DB_File
+ok checkmode "$userstate/awl.dir";          # SDBM
+ok checkmode "$userstate/awl.pag";          # SDBM
+ok checkmode "$userstate/awl.mutex";
 
-unlink 'log/user_state/awl',
-    'log/user_state/awl.dir',
-    'log/user_state/awl.pag';
-ok unlink 'log/user_state/awl.mutex';
+unlink "$userstate/awl",
+    "$userstate/awl.dir",
+    "$userstate/awl.pag";
+ok unlink "$userstate/awl.mutex";
diff --git a/upstream/t/db_awl_perms_welcome_block.t b/upstream/t/db_awl_perms_welcome_block.t
new file mode 100755 (executable)
index 0000000..e167d58
--- /dev/null
@@ -0,0 +1,44 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("db_awl_perms_welcome_block");
+use IO::File;
+use Test::More;
+plan skip_all => "Tests don't work on windows" if $RUNNING_ON_WINDOWS;
+plan tests => 5;
+
+# ---------------------------------------------------------------------------
+# bug 6173
+
+tstprefs ("
+  use_auto_welcomelist 1
+  auto_welcomelist_path ./$userstate/awl
+  auto_welcomelist_file_mode 0755
+  lock_method flock
+");
+
+unlink "$userstate/awl";
+unlink "$userstate/awl.mutex";
+umask 022;
+sarun("--add-addr-to-welcomelist whitelist_test\@example.org",
+      \&patterns_run_cb);
+
+# in case this test is ever made to work on Windows
+untaint_system($RUNNING_ON_WINDOWS?("dir " . File::Spec->canonpath($userstate)):"ls -l $userstate");  # for the logs
+
+sub checkmode {
+  my $fname = shift;
+  if (!-f $fname) { return 1; }
+  my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size) = stat $fname;
+  return (($mode & 0777) == 0644);
+}
+
+ok checkmode "$userstate/awl";              # DB_File
+ok checkmode "$userstate/awl.dir";          # SDBM
+ok checkmode "$userstate/awl.pag";          # SDBM
+ok checkmode "$userstate/awl.mutex";
+
+unlink "$userstate/awl",
+    "$userstate/awl.dir",
+    "$userstate/awl.pag";
+ok unlink "$userstate/awl.mutex";
diff --git a/upstream/t/db_based_welcomelist.t b/upstream/t/db_based_welcomelist.t
new file mode 100755 (executable)
index 0000000..d464c11
--- /dev/null
@@ -0,0 +1,36 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+
+use SATest; sa_t_init("db_based_welcomelist");
+
+use Test::More;
+plan skip_all => "Long running tests disabled" unless conf_bool('run_long_tests');
+plan tests => 8;
+
+# ---------------------------------------------------------------------------
+
+%is_nonspam_patterns = (
+  q{ Subject: Re: [SAtalk] auto-whitelisting}, 'subj',
+);
+%is_spam_patterns = (
+  q{Subject: 4000           Your Vacation Winning !}, 'subj',
+);
+
+%patterns = %is_nonspam_patterns;
+
+ok (sarun ("--remove-addr-from-welcomelist whitelist_test\@whitelist.spamassassin.taint.org", \&patterns_run_cb));
+
+# 3 times, to get into the welcomelist:
+ok (sarun ("-L -t < data/nice/002", \&patterns_run_cb));
+ok (sarun ("-L -t < data/nice/002", \&patterns_run_cb));
+ok (sarun ("-L -t < data/nice/002", \&patterns_run_cb));
+
+# Now check
+ok (sarun ("-L -t < data/nice/002", \&patterns_run_cb));
+ok_all_patterns();
+
+%patterns = %is_spam_patterns;
+ok (sarun ("-L -t < data/spam/004", \&patterns_run_cb));
+ok_all_patterns();
+
diff --git a/upstream/t/db_based_welcomelist_ips.t b/upstream/t/db_based_welcomelist_ips.t
new file mode 100755 (executable)
index 0000000..7b89123
--- /dev/null
@@ -0,0 +1,36 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+
+use SATest; sa_t_init("db_based_welcomelist_ips");
+
+use Test::More;
+plan skip_all => "Long running tests disabled" unless conf_bool('run_long_tests');
+plan tests => 8;
+
+# ---------------------------------------------------------------------------
+
+%is_nonspam_patterns = (
+  q{ Subject: Re: [SAtalk] auto-whitelisting}, 'subj',
+);
+%is_spam_patterns = (
+  q{ X-Spam-Status: Yes}, 'status',
+);
+
+%patterns = %is_nonspam_patterns;
+
+ok (sarun ("--remove-addr-from-welcomelist whitelist_test\@whitelist.spamassassin.taint.org", \&patterns_run_cb));
+
+# 3 times, to get into the welcomelist:
+ok (sarun ("-L -t < data/nice/002", \&patterns_run_cb));
+ok (sarun ("-L -t < data/nice/002", \&patterns_run_cb));
+ok (sarun ("-L -t < data/nice/002", \&patterns_run_cb));
+
+# Now check
+ok (sarun ("-L -t < data/nice/002", \&patterns_run_cb));
+ok_all_patterns();
+
+%patterns = %is_spam_patterns;
+ok (sarun ("-L -t < data/spam/007", \&patterns_run_cb));
+ok_all_patterns();
+
index 2aa076bd783399ae61a2fc452a0df317111e4b9b..55dc4bb73bce57ed137050659f47780e8d4081a1 100755 (executable)
@@ -11,10 +11,10 @@ plan tests => 8;
 # ---------------------------------------------------------------------------
 
 %is_nonspam_patterns = (
-q{ Subject: Re: [SAtalk] auto-whitelisting}, 'subj',
+  q{ Subject: Re: [SAtalk] auto-whitelisting}, 'subj',
 );
 %is_spam_patterns = (
-q{Subject: 4000           Your Vacation Winning !}, 'subj',
+  q{Subject: 4000           Your Vacation Winning !}, 'subj',
 );
 
 %patterns = %is_nonspam_patterns;
@@ -33,3 +33,4 @@ ok_all_patterns();
 %patterns = %is_spam_patterns;
 ok (sarun ("-L -t < data/spam/004", \&patterns_run_cb));
 ok_all_patterns();
+
index d100397bf44d335c7cf4c48211c5d1d3065d16f3..6064ee3a4823868af2bf1dcacb650471e4fd8cfa 100755 (executable)
@@ -11,10 +11,10 @@ plan tests => 8;
 # ---------------------------------------------------------------------------
 
 %is_nonspam_patterns = (
-q{ Subject: Re: [SAtalk] auto-whitelisting}, 'subj',
+  q{ Subject: Re: [SAtalk] auto-whitelisting}, 'subj',
 );
 %is_spam_patterns = (
-q{ X-Spam-Status: Yes}, 'status',
+  q{ X-Spam-Status: Yes}, 'status',
 );
 
 %patterns = %is_nonspam_patterns;
index d0dd144b503a5564b0ad6f28d31fd668a22a948c..f3d52a9a2b8aef64a0c34f624d656f838dceaad7 100755 (executable)
@@ -3,13 +3,14 @@
 use lib '.'; use lib 't';
 use SATest; sa_t_init("dcc");
 
-use constant HAS_DCC => eval { $_ = untaint_cmd("which cdcc"); chomp; -x };
+use constant HAS_DCC => !$RUNNING_ON_WINDOWS && eval { $_ = untaint_cmd("which cdcc"); chomp; -x };
 
 use Test::More;
+plan skip_all => "Tests don't work on windows" if $RUNNING_ON_WINDOWS;
 plan skip_all => "Net tests disabled" unless conf_bool('run_net_tests');
 plan skip_all => "DCC tests disabled" unless conf_bool('run_dcc_tests');
 plan skip_all => "DCC executable not found in path" unless HAS_DCC;
-plan tests => 4;
+plan tests => 12;
 
 diag('Note: Failure may not be an SpamAssassin bug, as DCC tests can fail due to problems with the DCC servers.');
 
@@ -17,26 +18,37 @@ diag('Note: Failure may not be an SpamAssassin bug, as DCC tests can fail due to
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
   q{ spam reported to DCC }, 'dcc report',
-
 );
 
-tstpre ("
-
-  loadplugin Mail::SpamAssassin::Plugin::DCC
-  dcc_timeout 30
-
+tstprefs ("
+  full     DCC_CHECK  eval:check_dcc()
+  tflags   DCC_CHECK  net autolearn_body
+  priority DCC_CHECK  10
+  dns_available no
+  use_dcc 1
+  meta X_META_POS DCC_CHECK
+  meta X_META_NEG !DCC_CHECK
+  score DCC_CHECK 3.3
+  score X_META_POS 3.3
+  score X_META_NEG 3.3
 ");
 
 ok sarun ("-t -D info -r < data/spam/gtubedcc.eml 2>&1", \&patterns_run_cb);
 ok_all_patterns();
+ok sarun ("-t -D info -r < data/spam/gtubedcc_crlf.eml 2>&1", \&patterns_run_cb);
+ok_all_patterns();
 
 %patterns = (
-
-  q{ Detected as bulk mail by DCC }, 'dcc',
-
+  q{ 3.3 DCC_CHECK }, 'dcc',
+  q{ 3.3 X_META_POS }, 'pos',
+);
+%anti_patterns = (
+  q{ X_META_NEG }, 'neg',
 );
 
-ok sarun ("-t < data/spam/gtubedcc.eml", \&patterns_run_cb);
+ok sarun ("-t < data/spam/gtubedcc.eml 2>&1", \&patterns_run_cb);
 ok_all_patterns();
+ok sarun ("-t < data/spam/gtubedcc_crlf.eml 2>&1", \&patterns_run_cb);
+ok_all_patterns();
+
index 6dd56950d70d111ad15c9740c94606b099ff17c9..fb5bdb7b322c83baf93016fad77db51e499875dd 100755 (executable)
@@ -1,20 +1,5 @@
 #!/usr/bin/perl -w -T
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_names.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
-
 use strict;
 use lib '.'; use lib 't';
 use SATest; sa_t_init("debug");
@@ -22,19 +7,20 @@ use SATest; sa_t_init("debug");
 use Mail::SpamAssassin;
 
 use Test::More;
-plan skip_all => "Long running tests disabled" unless conf_bool('run_long_tests');
 plan tests => 3;
 
 # list of known debug facilities
 my %facility = map( ($_, 1),
-  qw( accessdb archive-iterator async auto-whitelist bayes check config daemon
-      dcc dkim askdns dns eval generic https_http_mismatch facility FreeMail
-      hashcash ident ignore info ldap learn locker log logger markup HashBL
+  qw( accessdb archive-iterator async auto-welcomelist bayes check config daemon
+      dcc dkim askdns dns dnseval eval generic https_http_mismatch facility FreeMail
+      ident ignore info ldap learn locker log logger markup HashBL
       message metadata mimeheader netset plugin prefork progress pyzor razor2
       received-header replacetags reporter rules rules-all spamd spf textcat
-      timing TxRep uri uridnsbl util pdfinfo asn ));
+      timing TxRep uri uridnsbl util pdfinfo asn geodb FromNameSpoof
+      PHISHTAG resourcelimits https_http_mismatch DMARC ));
 
 my $fh = IO::File->new_tmpfile();
+open(OLDERR, ">&STDERR");
 open(STDERR, ">&=".fileno($fh)) || die "Cannot reopen STDERR";
 
 ok(sarun("-t -D < data/spam/dnsbl.eml"));
@@ -56,7 +42,7 @@ for (split(/^/m, $error)) {
     if (/^(?: \[ \d+ \] \s+)? (dbg|info): \s* ([^:\s]+) : \s* (.*)/x) {
        if (!exists $facility{$2}) {
            $unlisted++;
-           print "unlisted debug facility: $2\n";
+           print OLDERR "unlisted debug facility: $2\n";
        }
     }
     elsif (/^(?: \[ \d+ \] \s+)? (warn|error):/x) {
diff --git a/upstream/t/decodeshorturl.t b/upstream/t/decodeshorturl.t
new file mode 100755 (executable)
index 0000000..95f2087
--- /dev/null
@@ -0,0 +1,111 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("decodeshorturl");
+
+use Test::More;
+
+use constant HAS_DBI => eval { require DBI; };
+use constant HAS_DBD_SQLITE => eval { require DBD::SQLite; DBD::SQLite->VERSION(1.59_01); };
+
+use constant SQLITE => (HAS_DBI && HAS_DBD_SQLITE);
+
+plan skip_all => "Net tests disabled"                unless conf_bool('run_net_tests');
+my $tests = 8;
+$tests += 4 if (SQLITE);
+plan tests => $tests;
+
+tstpre ("
+loadplugin Mail::SpamAssassin::Plugin::DecodeShortURLs
+");
+
+tstprefs(q{
+dns_query_restriction allow bit.ly
+dns_query_restriction allow tinyurl.com
+
+clear_url_shortener
+url_shortener tinyurl.com
+url_shortener .page.link
+url_shortener_get bit.ly
+
+body HAS_SHORT_URL              eval:short_url()
+body HAS_SHORT_REDIR            eval:short_url_redir()
+body SHORT_URL_CHAINED          eval:short_url_chained()
+body SHORT_URL_404             eval:short_url_404()
+body SHORT_URL_C404            eval:short_url_code('404')
+uri URI_BITLY_BLOCKED          m,^https://bitly\.com/a/blocked,
+uri URI_PAGE_LINK              m,^https://spamassassin\.apache\.org/news\.html,
+});
+
+###
+### Basic functions, no caching
+###
+
+%patterns = (
+   q{ 1.0 HAS_SHORT_URL } => '',
+   q{ 1.0 HAS_SHORT_REDIR } => '',
+   q{ 1.0 SHORT_URL_404 } => '',
+   q{ 1.0 SHORT_URL_C404 } => '',
+   q{ 1.0 URI_BITLY_BLOCKED } => '',
+   q{ 1.0 URI_PAGE_LINK } => '',
+);
+sarun ("-t < data/spam/decodeshorturl/base.eml", \&patterns_run_cb);
+ok_all_patterns();
+
+%patterns = (
+   q{ 1.0 SHORT_URL_CHAINED } => '',
+);
+sarun ("-t < data/spam/decodeshorturl/chain.eml", \&patterns_run_cb);
+ok_all_patterns();
+
+
+###
+### short_url() should hit even without network enabled
+###
+
+%patterns = (
+   q{ 1.0 HAS_SHORT_URL } => '',
+);
+sarun ("-t -L < data/spam/decodeshorturl/base.eml", \&patterns_run_cb);
+ok_all_patterns();
+
+###
+### With SQLITE caching
+###
+
+if (SQLITE) {
+
+tstprefs("
+dns_query_restriction allow bit.ly
+dns_query_restriction allow tinyurl.com
+
+url_shortener bit.ly
+url_shortener tinyurl.com
+
+url_shortener_cache_type dbi
+url_shortener_cache_dsn dbi:SQLite:dbname=$workdir/DecodeShortURLs.db
+
+body HAS_SHORT_URL              eval:short_url()
+describe HAS_SHORT_URL          Message contains one or more shortened URLs
+");
+
+%patterns = (
+   q{ 1.0 HAS_SHORT_URL } => '',
+);
+sarun ("-t < data/spam/decodeshorturl/base.eml", \&patterns_run_cb);
+ok_all_patterns();
+
+my $dbh = DBI->connect("dbi:SQLite:dbname=$workdir/DecodeShortURLs.db","","");
+my @row = $dbh->selectrow_array("SELECT decoded_url FROM short_url_cache WHERE short_url = 'http://bit.ly/30yH6WK'");
+is($row[0], 'http://spamassassin.apache.org/');
+
+# Check another email to cleanup old entries from database
+sarun ("-t < data/spam/decodeshorturl/base2.eml", \&patterns_run_cb);
+ok_all_patterns();
+
+$dbh = DBI->connect("dbi:SQLite:dbname=$workdir/DecodeShortURLs.db","","");
+@row = $dbh->selectrow_array("SELECT decoded_url FROM short_url_cache WHERE short_url = 'http://bit.ly/30yH6WK'");
+isnt($row[0], 'https://spamassassin.apache.org/');
+
+}
+
index 2eb291a7a2e8e4b9915651d1830945e77ad78466..20b880c5f3f522a6eb5913b75e8ebe555d1f4ee5 100755 (executable)
@@ -16,13 +16,9 @@ q{ 1.0 THIS_IS_A_VERY_LONG_RULE_NAME_WHICH_NEEDS_WRAP A very very long },
 );
 
 tstprefs ("
-        $default_cf_lines
-
-        report_safe 1
-        header THIS_IS_A_VERY_LONG_RULE_NAME_WHICH_NEEDS_WRAP Subject =~ /FREE/
-
-        describe THIS_IS_A_VERY_LONG_RULE_NAME_WHICH_NEEDS_WRAP A very very long rule name and this is a very very long description lorem ipsum etc. blah blah blah blah This mailing is done by an independent marketing co. We apologize if this message has reached you in error. Save the Planet, Save the Trees! Advertise via E mail. No wasted paper! Delete with one simple keystroke!
-
+  report_safe 1
+  header THIS_IS_A_VERY_LONG_RULE_NAME_WHICH_NEEDS_WRAP Subject =~ /FREE/
+  describe THIS_IS_A_VERY_LONG_RULE_NAME_WHICH_NEEDS_WRAP A very very long rule name and this is a very very long description lorem ipsum etc. blah blah blah blah This mailing is done by an independent marketing co. We apologize if this message has reached you in error. Save the Planet, Save the Trees! Advertise via E mail. No wasted paper! Delete with one simple keystroke!
 ");
 
 ok (sarun ("-L -t < data/spam/001", \&patterns_run_cb));
@@ -39,13 +35,9 @@ ok ($matched_output =~ /^                            .{0,60}very very/m);
 ok ($matched_output =~ /^                            .{0,60}keystroke!/m);
 
 tstprefs ("
-        $default_cf_lines
-
-        report_safe 0
-        header THIS_IS_A_VERY_LONG_RULE_NAME_WHICH_NEEDS_WRAP Subject =~ /FREE/
-
-        describe THIS_IS_A_VERY_LONG_RULE_NAME_WHICH_NEEDS_WRAP A very very long rule name and this is a very very long description lorem ipsum etc. blah blah blah blah This mailing is done by an independent marketing co. We apologize if this message has reached you in error. Save the Planet, Save the Trees! Advertise via E mail. No wasted paper! Delete with one simple keystroke!
-
+  report_safe 0
+  header THIS_IS_A_VERY_LONG_RULE_NAME_WHICH_NEEDS_WRAP Subject =~ /FREE/
+  describe THIS_IS_A_VERY_LONG_RULE_NAME_WHICH_NEEDS_WRAP A very very long rule name and this is a very very long description lorem ipsum etc. blah blah blah blah This mailing is done by an independent marketing co. We apologize if this message has reached you in error. Save the Planet, Save the Trees! Advertise via E mail. No wasted paper! Delete with one simple keystroke!
 ");
 
 ok (sarun ("-L -t < data/spam/001", \&patterns_run_cb));
@@ -56,4 +48,3 @@ $matched_output =~ s/\t/        /gs; # expand tabs
 ok ($matched_output =~ /^\s+\*      .{0,60}very very/m);
 ok ($matched_output =~ /^\s+\*      .{0,60}keystroke!/m);
 
-
index 3c7fe6559357984b5bd82146863173c4c8cc8675..ab3270009daca65f6557d1d69053f2903c57fc1c 100755 (executable)
@@ -12,7 +12,7 @@ use vars qw(%patterns %anti_patterns);
 
 use constant HAS_DKIM_VERIFIER => eval {
   require Mail::DKIM::Verifier;
-  version->parse(Mail::DKIM::Verifier->VERSION) >= version->parse->(0.31);
+  version->parse(Mail::DKIM::Verifier->VERSION) >= version->parse(0.31);
 };
 
 use Test::More;
@@ -20,21 +20,6 @@ plan skip_all => "Net tests disabled" unless conf_bool('run_net_tests');
 plan skip_all => "Needs Mail::DKIM::Verifier >= 0.31" unless HAS_DKIM_VERIFIER ;
 plan tests => 258;
 
-BEGIN {
-  if (-e 't/test_dir') {
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {
-    unshift(@INC, '../blib/lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
-
 use IO::File;
 use Mail::SpamAssassin;
 
@@ -85,6 +70,22 @@ sub test_samples($$) {
 
 # ensure rules will fire, and disable some expensive ones
 tstlocalrules("
+  full   DKIM_SIGNED           eval:check_dkim_signed()
+  full   DKIM_VALID            eval:check_dkim_valid()
+  full   DKIM_VALID_AU         eval:check_dkim_valid_author_sig()
+  meta   DKIM_INVALID          DKIM_SIGNED && !DKIM_VALID
+  header DKIM_ADSP_NXDOMAIN    eval:check_dkim_adsp('N')
+  header DKIM_ADSP_DISCARD     eval:check_dkim_adsp('D')
+  header DKIM_ADSP_ALL         eval:check_dkim_adsp('A')
+  header DKIM_ADSP_CUSTOM_LOW  eval:check_dkim_adsp('1')
+  header DKIM_ADSP_CUSTOM_MED  eval:check_dkim_adsp('2')
+  header DKIM_ADSP_CUSTOM_HIGH eval:check_dkim_adsp('3')
+  adsp_override sa-test-nxd.spamassassin.org  nxdomain
+  adsp_override sa-test-unk.spamassassin.org  unknown
+  adsp_override sa-test-all.spamassassin.org  all
+  adsp_override sa-test-dis.spamassassin.org  discardable
+  adsp_override sa-test-di2.spamassassin.org
+
   dkim_minimum_key_bits 512
   score DKIM_SIGNED          -0.1
   score DKIM_VALID           -0.1
@@ -99,18 +100,14 @@ tstlocalrules("
   header DKIM_ADSP_SEL_TEST   eval:check_dkim_adsp('*', .spamassassin.org)
   priority DKIM_ADSP_SEL_TEST -100
   score  DKIM_ADSP_SEL_TEST   0.1
-  score RAZOR2_CHECK 0
-  score RAZOR2_CF_RANGE_51_100 0
-  score RAZOR2_CF_RANGE_E4_51_100 0
-  score RAZOR2_CF_RANGE_E8_51_100 0
 ");
 
 my $dirname = "data/dkim";
 
 $spamassassin_obj = Mail::SpamAssassin->new({
-  rules_filename      => "$prefix/t/log/test_rules_copy",
-  site_rules_filename => "$prefix/t/log/localrules.tmp",
-  userprefs_filename  => "$prefix/masses/spamassassin/user_prefs",
+  rules_filename      => $localrules,
+  site_rules_filename => $siterules,
+  userprefs_filename  => $userrules,
   dont_copy_prefs     => 1,
   require_rules       => 1,
 # debug               => 'dkim',
diff --git a/upstream/t/dmarc.t b/upstream/t/dmarc.t
new file mode 100755 (executable)
index 0000000..9987b62
--- /dev/null
@@ -0,0 +1,136 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("dmarc");
+
+use Test::More;
+
+use vars qw(%patterns %anti_patterns);
+
+use constant HAS_MAILSPF => eval { require Mail::SPF; };
+use constant HAS_DKIM_VERIFIER => eval {
+  require Mail::DKIM::Verifier;
+  version->parse(Mail::DKIM::Verifier->VERSION) >= version->parse(0.31);
+};
+use constant HAS_MAILDMARC => eval { require Mail::DMARC::PurePerl; };
+
+plan skip_all => "Net tests disabled" unless conf_bool('run_net_tests');
+plan skip_all => "Needs Mail::SPF" unless HAS_MAILSPF;
+plan skip_all => "Needs Mail::DMARC::PurePerl" unless HAS_MAILDMARC;
+plan skip_all => "Needs Mail::DKIM::Verifier >= 0.31" unless HAS_DKIM_VERIFIER ;
+plan tests => 18;
+
+tstprefs("
+
+header SPF_PASS     eval:check_for_spf_pass()
+tflags SPF_PASS     nice userconf net
+full   DKIM_SIGNED  eval:check_dkim_signed()
+tflags DKIM_SIGNED  net
+
+# Check that rename backwards compatibility works with if's
+ifplugin Mail::SpamAssassin::Plugin::Dmarc
+if plugin ( Mail::SpamAssassin::Plugin::Dmarc)
+ifplugin Mail::SpamAssassin::Plugin::DMARC
+
+header DMARC_PASS eval:check_dmarc_pass()
+tflags DMARC_PASS net
+
+header DMARC_NONE eval:check_dmarc_none()
+tflags DMARC_NONE net
+
+header DMARC_QUAR eval:check_dmarc_quarantine()
+tflags DMARC_QUAR net
+
+header DMARC_REJECT eval:check_dmarc_reject()
+tflags DMARC_REJECT net
+
+header DMARC_MISSING eval:check_dmarc_missing()
+tflags DMARC_MISSING net
+
+endif
+endif
+endif
+");
+
+##
+## PASS
+##
+
+%patterns = (
+    q{ DMARC_PASS } => '',
+);
+%anti_patterns = (
+    qr/DMARC_(?!PASS)/ => '',
+);
+
+sarun ("-t < data/nice/dmarc/noneok.eml", \&patterns_run_cb);
+ok_all_patterns();
+
+sarun ("-t < data/nice/dmarc/quarok.eml", \&patterns_run_cb);
+ok_all_patterns();
+
+sarun ("-t < data/nice/dmarc/rejectok.eml", \&patterns_run_cb);
+ok_all_patterns();
+
+sarun ("-t < data/nice/dmarc/strictrejectok.eml", \&patterns_run_cb);
+ok_all_patterns();
+
+##
+## REJECT
+##
+
+%patterns = (
+    q{ DMARC_REJECT } => '',
+);
+%anti_patterns = (
+    qr/DMARC_(?!REJECT)/ => '',
+);
+
+sarun ("-t < data/spam/dmarc/rejectko.eml", \&patterns_run_cb);
+ok_all_patterns();
+
+sarun ("-t < data/spam/dmarc/strictrejectko.eml", \&patterns_run_cb);
+ok_all_patterns();
+
+##
+## QUAR
+##
+
+%patterns = (
+    q{ DMARC_QUAR } => '',
+);
+%anti_patterns = (
+    qr/DMARC_(?!QUAR)/ => '',
+);
+
+sarun ("-t < data/spam/dmarc/quarko.eml", \&patterns_run_cb);
+ok_all_patterns();
+
+##
+## NONE
+##
+
+%patterns = (
+    q{ DMARC_NONE } => '',
+);
+%anti_patterns = (
+    qr/DMARC_(?!NONE)/ => '',
+);
+
+sarun ("-t < data/spam/dmarc/noneko.eml", \&patterns_run_cb);
+ok_all_patterns();
+
+##
+## MISSING
+##
+
+%patterns = (
+    q{ DMARC_MISSING } => '',
+);
+%anti_patterns = (
+    qr/DMARC_(?!MISSING)/ => '',
+);
+
+sarun ("-t < data/spam/dmarc/nodmarc.eml", \&patterns_run_cb);
+ok_all_patterns();
+
index 792a8ba4db4c9678f9f1f7072fde878c90af53b5..b154e7e608f279f4f5fc19a492bba9073db3a5f6 100755 (executable)
@@ -1,13 +1,15 @@
 #!/usr/bin/perl -T
 
 use lib '.'; use lib 't';
-use SATest; sa_t_init("dns");
+use SATest; sa_t_init("dnsbl");
 
 use Test::More;
-plan skip_all => "Long running tests disabled" unless conf_bool('run_long_tests');
 plan skip_all => "Net tests disabled" unless conf_bool('run_net_tests');
 plan skip_all => "Can't use Net::DNS Safely" unless can_use_net_dns_safely();
-plan tests => 18;
+
+# run many times to catch some random natured failures
+my $iterations = 5;
+plan tests => 21 * $iterations;
 
 # ---------------------------------------------------------------------------
 # bind configuration currently used to support this test
@@ -48,27 +50,30 @@ EOF
 # hits we expect and some hits we don't expect
 
 %patterns = (
- q{ <dns:98.3.137.144.dnsbltest.spamassassin.org> [127.0.0.2] } => 'P_1',
- q{ <dns:134.88.73.210.dnsbltest.spamassassin.org> [127.0.0.4] } => 'P_2',
- q{ <dns:18.13.119.61.dnsbltest.spamassassin.org> [127.0.0.12] } => 'P_3',
- q{ <dns:14.35.17.212.dnsbltest.spamassassin.org> [127.0.0.1] } => 'P_4',
- q{ <dns:226.149.120.193.dnsbltest.spamassassin.org> [127.0.0.1] } => 'P_5',
- q{ <dns:example.com.dnsbltest.spamassassin.org> [127.0.0.2] } => 'P_6',
- q{,DNSBL_TEST_TOP,} => 'P_8',
- q{,DNSBL_TEST_WHITELIST,} => 'P_9',
- q{,DNSBL_TEST_DYNAMIC,} => 'P_10',
- q{,DNSBL_TEST_SPAM,} => 'P_11',
- q{,DNSBL_TEST_RELAY,} => 'P_12',
- q{,DNSBL_TXT_TOP,} => 'P_13',
- q{,DNSBL_TXT_RE,} => 'P_14',
- q{,DNSBL_RHS,} => 'P_15',
+ '<dns:98.3.137.144.dnsbltest.spamassassin.org> [127.0.0.2]'   => '',
+ '<dns:134.88.73.210.dnsbltest.spamassassin.org> [127.0.0.4]'  => '',
+ '<dns:18.13.119.61.dnsbltest.spamassassin.org> [127.0.0.12]'  => '',
+ '<dns:14.35.17.212.dnsbltest.spamassassin.org> [127.0.0.1]'   => '',
+ '<dns:226.149.120.193.dnsbltest.spamassassin.org> [127.0.0.1]' => '',
+ '<dns:example.com.dnsbltest.spamassassin.org> [127.0.0.2]'    => '',
+ ' 1.0 DNSBL_TEST_TOP '                => '',
+ ' -1.0 DNSBL_TEST_WHITELIST ' => '',
+ ' 1.0 DNSBL_TEST_DYNAMIC '    => '',
+ ' 1.0 DNSBL_TEST_SPAM '       => '',
+ ' 1.0 DNSBL_TEST_RELAY '      => '',
+ ' 1.0 DNSBL_TXT_TOP '         => '',
+ ' 1.0 DNSBL_TXT_RE '          => '',
+ ' 1.0 DNSBL_RHS '             => '',
+ ' 1.0 META_DNSBL_A '          => '',
+ ' 1.0 META_DNSBL_B '          => '',
+ ' 1.0 META_DNSBL_C '          => '',
 );
 
 %anti_patterns = (
q{,DNSBL_TEST_MISS,} => 'P_19',
q{,DNSBL_TXT_MISS,} => 'P_20',
q{,DNSBL_TEST_WHITELIST_MISS,} => 'P_21',
q{ launching DNS A query for 14.35.17.212.untrusted.dnsbltest.spamassassin.org. } => 'untrusted',
' 1.0 DNSBL_TEST_MISS '               => '',
' 1.0 DNSBL_TXT_MISS '                        => '',
' 1.0 DNSBL_TEST_WHITELIST_MISS '     => '',
'14.35.17.212.untrusted.dnsbltest.spamassassin.org'           => '',
 );
 
 tstprefs("
@@ -83,13 +88,9 @@ add_header all Trusted _RELAYSTRUSTED_
 add_header all Untrusted _RELAYSUNTRUSTED_
 
 clear_trusted_networks
-trusted_networks 127.
 trusted_networks 10.
 trusted_networks 150.51.53.1
 
-# make ,DNSBL, pattern matches work (never allow it first in the tests= list)
-meta AAA 1
-
 header DNSBL_TEST_TOP  eval:check_rbl('test', 'dnsbltest.spamassassin.org.')
 describe DNSBL_TEST_TOP        DNSBL A record match
 tflags DNSBL_TEST_TOP  net
@@ -138,8 +139,21 @@ header DNSBL_RHS   eval:check_rbl_from_host('r', 'dnsbltest.spamassassin.org.')
 describe DNSBL_RHS     DNSBL RHS match
 tflags DNSBL_RHS       net
 
+# Bug 7897 - test that meta rules depending on net rules hit
+meta META_DNSBL_A DNSBL_TEST_DYNAMIC
+# It also needs to hit even if priority is lower than dnsbl (-100)
+meta META_DNSBL_B DNSBL_TEST_SPAM
+priority META_DNSBL_B -500
+# Or super high
+meta META_DNSBL_C DNSBL_TEST_RELAY
+priority META_DNSBL_C 2000
+priority DNSBL_TEST_RELAY 2000
+
 ");
 
-# The -D clobbers test performance but some patterns & antipatterns depend on debug output
-sarun ("-D -t < data/spam/dnsbl.eml 2>&1", \&patterns_run_cb);
-ok_all_patterns();
+for (1 .. $iterations) {
+  clear_localrules() if $_ == 3; # do some tests without any other rules to check meta bugs
+  sarun ("-t < data/spam/dnsbl.eml 2>&1", \&patterns_run_cb);
+  ok_all_patterns();
+}
+
index 0f9df0cf9a20275fc8504b31b7d65b8fa5faf824..3b8dbf44c1c34db62ad37f0b316cef09aaf3bd77 100755 (executable)
@@ -11,8 +11,8 @@ plan tests => 2;
 # ---------------------------------------------------------------------------
 
 %patterns = (
- q{ DNSBL_TEST_TOP } => 'DNSBL_TEST_TOP',
- q{ SC_DNSBL } => 'SC_DNSBL',
+ q{ 1.0 DNSBL_TEST_TOP } => 'DNSBL_TEST_TOP',
+ q{ 1.0 SC_DNSBL } => 'SC_DNSBL',
 );
 
 %anti_patterns = (
index 3a411a4eaacf26c99a98ab87d980032a8a7e69e7..449fa1ebedffd5d9c0623549de4d3d6ec0046a44 100755 (executable)
@@ -10,25 +10,14 @@ use lib '.'; use lib 't';
 use SATest; sa_t_init("dnsbl_subtests");
 
 use vars qw(%patterns %anti_patterns);
-use Test::More tests => 46;
+use Test::More;
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_tests.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-  }
-}
+use Errno qw(EADDRINUSE EACCES);
 
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
+use constant HAS_NET_DNS_NAMESERVER => eval { require Net::DNS::Nameserver; };
+plan skip_all => "Net::DNS::Nameserver in unavailable on this system" unless (HAS_NET_DNS_NAMESERVER);
+plan  tests => 46;
 
-use Errno qw(EADDRINUSE EACCES);
-use Net::DNS::Nameserver;
 use Mail::SpamAssassin;
 
 # Bug 5761 (no 127.0.0.1 in jail, use SPAMD_LOCALHOST if specified)
@@ -332,10 +321,10 @@ if (!$pid) {  # child
 sleep 1;
 
 $spamassassin_obj = Mail::SpamAssassin->new({
-  rules_filename      => "$prefix/t/log/test_rules_copy",
+  rules_filename      => $localrules,
   require_rules       => 1,
-  site_rules_filename => "$prefix/t/log/localrules.tmp",
-  userprefs_filename  => "$prefix/masses/spamassassin/user_prefs",
+  site_rules_filename => $siterules,
+  userprefs_filename  => $userrules,
   post_config_text    => $local_conf,
   dont_copy_prefs     => 1,
 # debug               => 'dns,async,uridnsbl',
diff --git a/upstream/t/duplicates.t b/upstream/t/duplicates.t
deleted file mode 100755 (executable)
index ee29c4a..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-#!/usr/bin/perl -T
-
-use lib '.'; use lib 't';
-use SATest; sa_t_init("duplicates");
-use Test::More tests => 21;
-
-$ENV{'LANGUAGE'} = $ENV{'LC_ALL'} = 'C';             # a cheat, but we need the patterns to work
-
-# ---------------------------------------------------------------------------
-
-%patterns = (
-
-  q{ FOO1 }     => '',  # use default names
-  q{ FOO2 }     => '',
-  q{ HDR1 }     => '',
-  q{ HDR2 }     => '',
-  q{ META1 }     => '',
-  q{ META2 }     => '',
-  q{ META3 }     => '',
-  q{ HDREVAL1 }     => '',
-  q{ HDREVAL2 }     => '',
-  q{ ran body rule FOO1 ======> got hit } => '',
-  q{ ran header rule HDR1 ======> got hit } => '',
-  q{ rules: FOO1 merged duplicates: FOO2 } => '',
-  q{ rules: HDR1 merged duplicates: HDR2 } => '',
-  q{ rules: META3 merged duplicates: META1 } => '',
-  q{ ran eval rule HDREVAL1 ======> got hit } => '',
-  q{ ran eval rule HDREVAL2 ======> got hit } => '',
-);
-
-%anti_patterns = (
-
-  q{ FOO3 }     => '',
-  q{ RAWFOO }   => '',
-  q{ ran body rule FOO2 ======> got hit } => '',
-  q{ ran header rule HDR2 ======> got hit } => '',
-
-);
-
-tstprefs (qq{
-
-   $default_cf_lines
-
-   loadplugin Mail::SpamAssassin::Plugin::Test
-
-   body FOO1 /click here and e= nter your/i
-   describe FOO1 Test rule
-   body FOO2 /click here and e= nter your/i
-   describe FOO2 Test rule
-
-   # should not be found, not a dup (/i)
-   body FOO3 /click here and e= nter your/
-   describe FOO3 Test rule
-
-   # should not be found, not dup since different type
-   rawbody RAWFOO /click here and e= nter your/i
-   describe RAWFOO Test rule
-
-   header HDR1 Subject =~ /stained/
-   describe HDR1 Test rule
-   header HDR2 Subject =~ /stained/
-   describe HDR2 Test rule
-
-   # should not be merged -- eval rules (bug 5959)
-   header HDREVAL1 eval:check_test_plugin() 
-   describe HDREVAL1 Test rule
-   header HDREVAL2 eval:check_test_plugin()
-   describe HDREVAL2 Test rule
-
-   meta META1 (1)
-   describe META1 Test rule
-   meta META2 (META1 && META3)
-   describe META2 Test rule
-   meta META3 (1)
-   priority META3 -500
-   describe META3 Test rule
-
-});
-
-sarun ("-L -t -D < data/spam/006 2>&1", \&patterns_run_cb);
-ok ok_all_patterns();
diff --git a/upstream/t/enable_compat.t b/upstream/t/enable_compat.t
new file mode 100755 (executable)
index 0000000..353342e
--- /dev/null
@@ -0,0 +1,44 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("enable_compat");
+use Test::More tests => 6;
+
+# ---------------------------------------------------------------------------
+
+%patterns = (
+  q{ 1.0 ANY_RULE }, '',
+  q{ 1.0 COMPAT_RULE }, '',
+);
+%anti_patterns = ();
+
+tstprefs("
+  enable_compat foo_testing
+  body ANY_RULE /./
+  if can(Mail::SpamAssassin::Conf::compat_foo_testing)
+    body COMPAT_RULE /EVOLUTION/
+  endif
+");
+
+ok (sarun ("-t -L < data/nice/001", \&patterns_run_cb));
+ok_all_patterns();
+
+# ---------------------------------------------------------------------------
+
+%patterns = (
+  q{ 1.0 ANY_RULE }, '',
+);
+%anti_patterns = (
+  q{ COMPAT_RULE }, '',
+);
+
+tstprefs("
+  body ANY_RULE /./
+  if can(Mail::SpamAssassin::Conf::compat_foo_testing)
+    body COMPAT_RULE /EVOLUTION/
+  endif
+");
+
+ok (sarun ("-t -L < data/nice/001", \&patterns_run_cb));
+ok_all_patterns();
+
diff --git a/upstream/t/extracttext.t b/upstream/t/extracttext.t
new file mode 100755 (executable)
index 0000000..70b4f8d
--- /dev/null
@@ -0,0 +1,75 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("extracttext");
+use Mail::SpamAssassin::Util;
+use Test::More;
+
+use constant PDFTOTEXT => eval { my $f = Mail::SpamAssassin::Util::find_executable_in_env_path('pdftotext'); ($f !~ /\s/)?$f:undef};
+use constant TESSERACT => eval { my $f = Mail::SpamAssassin::Util::find_executable_in_env_path('tesseract'); ($f !~ /\s/)?$f:undef};
+use constant CAT => eval { my $f = Mail::SpamAssassin::Util::find_executable_in_env_path('cat'); ($f !~ /\s/)?$f:undef};
+
+my $tests = 0;
+$tests += 2 if (PDFTOTEXT);
+$tests += 1 if (TESSERACT);
+$tests += 1 if (CAT);
+if ($tests && $tests < 4) { diag("some binaries missing, not running all tests\n"); }
+
+plan skip_all => "no needed binaries found, pdftotext, tesseract, or cat" unless $tests;
+plan tests => $tests;
+
+%patterns_gtube = (
+  q{ 1000 GTUBE }, 'gtube',
+);
+
+if (PDFTOTEXT) {
+   tstprefs("
+     extracttext_external  pdftotext  ".PDFTOTEXT." -nopgbrk -layout -enc UTF-8 {} -
+     extracttext_use       pdftotext  .pdf
+     extracttext_timeout 30 40
+   ");
+   %anti_patterns = ();
+   %patterns = %patterns_gtube;
+   sarun ("-L -t < data/spam/extracttext/gtube_pdf.eml", \&patterns_run_cb);
+   ok_all_patterns();
+   clear_pattern_counters();
+
+   # Should fail
+   tstprefs("
+     extracttext_external  pdftotext  ".PDFTOTEXT." -nopgbrk -layout -enc UTF-8 {} -
+     extracttext_use       pdftotext  .FOO
+     extracttext_timeout 30 40
+   ");
+   %anti_patterns = %patterns_gtube;
+   %patterns = ();
+   sarun ("-L -t < data/spam/extracttext/gtube_pdf.eml", \&patterns_run_cb);
+   ok_all_patterns();
+   clear_pattern_counters();
+}
+
+if (TESSERACT) {
+   tstprefs("
+     extracttext_external  tesseract  {OMP_THREAD_LIMIT=1} ".TESSERACT." -c page_separator= {} -
+     extracttext_use       tesseract  .jpg .png .bmp .tif .tiff image/(?:jpeg|png|x-ms-bmp|tiff)
+     extracttext_timeout 30 1
+   ");
+   %anti_patterns = ();
+   %patterns = %patterns_gtube;
+   sarun ("-L -t < data/spam/extracttext/gtube_png.eml", \&patterns_run_cb);
+   ok_all_patterns();
+   clear_pattern_counters();
+}
+
+if (CAT) {
+   tstprefs("
+     extracttext_external  cat  ".CAT." {}
+     extracttext_use       cat  .txt .html .shtml .xhtml octet/stream
+     extracttext_timeout 30 1
+   ");
+   %anti_patterns = ();
+   %patterns = %patterns_gtube;
+   sarun ("-L -t < data/spam/extracttext/gtube_b64_oct.eml", \&patterns_run_cb);
+   ok_all_patterns();
+   clear_pattern_counters();
+}
+
index 80b53fdde427bdbc0a368b29a20054111330c628..624d7832d56145ebd86ad669ac383de643026e60 100755 (executable)
@@ -1,39 +1,51 @@
 #!/usr/bin/perl -T
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_tests.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-    unshift(@INC, '../lib');
-  }
-}
-
 use lib '.'; use lib 't';
 use SATest; sa_t_init("freemail");
 
 use Test::More;
 
-plan tests => 4;
+plan tests => 23;
 
 # ---------------------------------------------------------------------------
 
-tstpre ("
-loadplugin Mail::SpamAssassin::Plugin::FreeMail
-");
-
+# Global
 tstprefs ("
-        header FREEMAIL_FROM eval:check_freemail_from()
-        freemail_domains gmail.com
-        freemail_import_whitelist_auth 0
-        whitelist_auth test\@gmail.com
+  freemail_domains gmail.com
 ");
 
+## Standard + whitelist should not hit
+
+tstlocalrules (q{
+  freemail_import_whitelist_auth 0
+  whitelist_auth test@gmail.com
+  header FREEMAIL_FROM eval:check_freemail_from()
+  score FREEMAIL_FROM 3.3
+  header FREEMAIL_REPLYXX eval:check_freemail_replyto('reply')
+  score FREEMAIL_REPLYXX 3.3
+  header FREEMAIL_REPLYTO eval:check_freemail_replyto('replyto')
+  score FREEMAIL_REPLYTO 3.3
+  header FREEMAIL_REPLYXX eval:check_freemail_replyto('reply')
+  score FREEMAIL_REPLYXX 3.3
+  header FREEMAIL_ENVFROM_END_DIGIT  eval:check_freemail_header('EnvelopeFrom', '\d@')
+  score FREEMAIL_ENVFROM_END_DIGIT 3.3
+  header FREEMAIL_REPLYTO_END_DIGIT  eval:check_freemail_header('Reply-To', '\d@')
+  score FREEMAIL_REPLYTO_END_DIGIT 3.3
+  header FREEMAIL_HDR_REPLYTO eval:check_freemail_header('Reply-To')
+  score FREEMAIL_HDR_REPLYTO 3.3
+});
+
 %patterns = (
-        q{ FREEMAIL_FROM }, 'FREEMAIL_FROM',
-            );
+  q{ 3.3 FREEMAIL_FROM }, '',
+);
+%anti_patterns = (
+  # No Reply-To or body
+  q{ FREEMAIL_REPLYTO }, '',
+  q{ FREEMAIL_REPLYXX }, '',
+  q{ FREEMAIL_ENVFROM_END_DIGIT }, '',
+  q{ FREEMAIL_REPLYTO_END_DIGIT }, '',
+  q{ FREEMAIL_HDR_REPLYTO }, '',
+);
 
 ok sarun ("-L -t < data/spam/relayUS.eml", \&patterns_run_cb);
 ok_all_patterns();
@@ -43,15 +55,85 @@ clear_pattern_counters();
 
 %patterns = ();
 %anti_patterns = (
-        q{ FREEMAIL_FROM }, 'FREEMAIL_FROM',
-            );
+  q{ FREEMAIL_FROM }, '',
+);
 
-tstprefs ("
-        header FREEMAIL_FROM eval:check_freemail_from()
-        freemail_domains gmail.com
-        freemail_import_whitelist_auth 1
-        whitelist_auth test\@gmail.com
-");
+tstlocalrules (q{
+  freemail_import_whitelist_auth 1
+  whitelist_auth test@gmail.com
+  header FREEMAIL_FROM eval:check_freemail_from()
+  score FREEMAIL_FROM 3.3
+});
 
 ok sarun ("-L -t < data/spam/relayUS.eml", \&patterns_run_cb);
 ok_all_patterns();
+
+## From and Reply-To different
+
+%patterns = (
+  q{ 3.3 FREEMAIL_FROM }, '',
+  q{ 3.3 FREEMAIL_REPLYTO }, '',
+  q{ 3.3 FREEMAIL_REPLYXX }, '',
+  q{ 3.3 FREEMAIL_ENVFROM_END_DIGIT }, '',
+  q{ 3.3 FREEMAIL_REPLYTO_END_DIGIT }, '',
+  q{ 3.3 FREEMAIL_HDR_REPLYTO }, '',
+);
+%anti_patterns = ();
+
+tstlocalrules (q{
+  header FREEMAIL_FROM eval:check_freemail_from()
+  score FREEMAIL_FROM 3.3
+  header FREEMAIL_REPLYTO eval:check_freemail_replyto('replyto')
+  score FREEMAIL_REPLYTO 3.3
+  header FREEMAIL_REPLYXX eval:check_freemail_replyto('reply')
+  score FREEMAIL_REPLYXX 3.3
+  header FREEMAIL_ENVFROM_END_DIGIT  eval:check_freemail_header('EnvelopeFrom', '\d@')
+  score FREEMAIL_ENVFROM_END_DIGIT 3.3
+  header FREEMAIL_REPLYTO_END_DIGIT  eval:check_freemail_header('Reply-To', '\d@')
+  score FREEMAIL_REPLYTO_END_DIGIT 3.3
+  header FREEMAIL_HDR_REPLYTO eval:check_freemail_header('Reply-To')
+  score FREEMAIL_HDR_REPLYTO 3.3
+});
+
+ok sarun ("-L -t < data/spam/freemail1", \&patterns_run_cb);
+ok_all_patterns();
+
+## Multiple Reply-To values, no email on body
+
+%patterns = (
+  q{ 3.3 FREEMAIL_REPLYTO }, '',
+  q{ 3.3 FREEMAIL_REPLYXX }, '',
+  q{ 3.3 FREEMAIL_REPLYTO_END_DIGIT }, '',
+  q{ 3.3 FREEMAIL_HDR_REPLYTO }, '',
+);
+%anti_patterns = ();
+
+tstlocalrules (q{
+  header FREEMAIL_REPLYTO eval:check_freemail_replyto('replyto')
+  score FREEMAIL_REPLYTO 3.3
+  header FREEMAIL_REPLYXX eval:check_freemail_replyto('reply')
+  score FREEMAIL_REPLYXX 3.3
+  header FREEMAIL_REPLYTO_END_DIGIT  eval:check_freemail_header('Reply-To', '\d@')
+  score FREEMAIL_REPLYTO_END_DIGIT 3.3
+  header FREEMAIL_HDR_REPLYTO eval:check_freemail_header('Reply-To')
+  score FREEMAIL_HDR_REPLYTO 3.3
+});
+
+ok sarun ("-L -t < data/spam/freemail2", \&patterns_run_cb);
+ok_all_patterns();
+
+## No Reply-To, another freemail in body
+
+%patterns = (
+  q{ 3.3 FREEMAIL_REPLYXX }, '',
+);
+%anti_patterns = ();
+
+tstlocalrules (q{
+  header FREEMAIL_REPLYXX eval:check_freemail_replyto('reply')
+  score FREEMAIL_REPLYXX 3.3
+});
+
+ok sarun ("-L -t < data/spam/freemail3", \&patterns_run_cb);
+ok_all_patterns();
+
diff --git a/upstream/t/freemail_welcome_block.t b/upstream/t/freemail_welcome_block.t
new file mode 100755 (executable)
index 0000000..2860ddf
--- /dev/null
@@ -0,0 +1,139 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("freemail");
+
+use Test::More;
+
+plan tests => 23;
+
+# ---------------------------------------------------------------------------
+
+# Global
+tstprefs ("
+  freemail_domains gmail.com
+");
+
+## Standard + welcomelist should not hit
+
+tstlocalrules (q{
+  freemail_import_welcomelist_auth 0
+  welcomelist_auth test@gmail.com
+  header FREEMAIL_FROM eval:check_freemail_from()
+  score FREEMAIL_FROM 3.3
+  header FREEMAIL_REPLYXX eval:check_freemail_replyto('reply')
+  score FREEMAIL_REPLYXX 3.3
+  header FREEMAIL_REPLYTO eval:check_freemail_replyto('replyto')
+  score FREEMAIL_REPLYTO 3.3
+  header FREEMAIL_REPLYXX eval:check_freemail_replyto('reply')
+  score FREEMAIL_REPLYXX 3.3
+  header FREEMAIL_ENVFROM_END_DIGIT  eval:check_freemail_header('EnvelopeFrom', '\d@')
+  score FREEMAIL_ENVFROM_END_DIGIT 3.3
+  header FREEMAIL_REPLYTO_END_DIGIT  eval:check_freemail_header('Reply-To', '\d@')
+  score FREEMAIL_REPLYTO_END_DIGIT 3.3
+  header FREEMAIL_HDR_REPLYTO eval:check_freemail_header('Reply-To')
+  score FREEMAIL_HDR_REPLYTO 3.3
+});
+
+%patterns = (
+  q{ 3.3 FREEMAIL_FROM }, '',
+);
+%anti_patterns = (
+  # No Reply-To or body
+  q{ FREEMAIL_REPLYTO }, '',
+  q{ FREEMAIL_REPLYXX }, '',
+  q{ FREEMAIL_ENVFROM_END_DIGIT }, '',
+  q{ FREEMAIL_REPLYTO_END_DIGIT }, '',
+  q{ FREEMAIL_HDR_REPLYTO }, '',
+);
+
+ok sarun ("-L -t < data/spam/relayUS.eml", \&patterns_run_cb);
+ok_all_patterns();
+clear_pattern_counters();
+
+## Now test with freemail_import_welcomelist_auth, should not hit
+
+%patterns = ();
+%anti_patterns = (
+  q{ FREEMAIL_FROM }, '',
+);
+
+tstlocalrules (q{
+  freemail_import_welcomelist_auth 1
+  welcomelist_auth test@gmail.com
+  header FREEMAIL_FROM eval:check_freemail_from()
+  score FREEMAIL_FROM 3.3
+});
+
+ok sarun ("-L -t < data/spam/relayUS.eml", \&patterns_run_cb);
+ok_all_patterns();
+
+## From and Reply-To different
+
+%patterns = (
+  q{ 3.3 FREEMAIL_FROM }, '',
+  q{ 3.3 FREEMAIL_REPLYTO }, '',
+  q{ 3.3 FREEMAIL_REPLYXX }, '',
+  q{ 3.3 FREEMAIL_ENVFROM_END_DIGIT }, '',
+  q{ 3.3 FREEMAIL_REPLYTO_END_DIGIT }, '',
+  q{ 3.3 FREEMAIL_HDR_REPLYTO }, '',
+);
+%anti_patterns = ();
+
+tstlocalrules (q{
+  header FREEMAIL_FROM eval:check_freemail_from()
+  score FREEMAIL_FROM 3.3
+  header FREEMAIL_REPLYTO eval:check_freemail_replyto('replyto')
+  score FREEMAIL_REPLYTO 3.3
+  header FREEMAIL_REPLYXX eval:check_freemail_replyto('reply')
+  score FREEMAIL_REPLYXX 3.3
+  header FREEMAIL_ENVFROM_END_DIGIT  eval:check_freemail_header('EnvelopeFrom', '\d@')
+  score FREEMAIL_ENVFROM_END_DIGIT 3.3
+  header FREEMAIL_REPLYTO_END_DIGIT  eval:check_freemail_header('Reply-To', '\d@')
+  score FREEMAIL_REPLYTO_END_DIGIT 3.3
+  header FREEMAIL_HDR_REPLYTO eval:check_freemail_header('Reply-To')
+  score FREEMAIL_HDR_REPLYTO 3.3
+});
+
+ok sarun ("-L -t < data/spam/freemail1", \&patterns_run_cb);
+ok_all_patterns();
+
+## Multiple Reply-To values, no email on body
+
+%patterns = (
+  q{ 3.3 FREEMAIL_REPLYTO }, '',
+  q{ 3.3 FREEMAIL_REPLYXX }, '',
+  q{ 3.3 FREEMAIL_REPLYTO_END_DIGIT }, '',
+  q{ 3.3 FREEMAIL_HDR_REPLYTO }, '',
+);
+%anti_patterns = ();
+
+tstlocalrules (q{
+  header FREEMAIL_REPLYTO eval:check_freemail_replyto('replyto')
+  score FREEMAIL_REPLYTO 3.3
+  header FREEMAIL_REPLYXX eval:check_freemail_replyto('reply')
+  score FREEMAIL_REPLYXX 3.3
+  header FREEMAIL_REPLYTO_END_DIGIT  eval:check_freemail_header('Reply-To', '\d@')
+  score FREEMAIL_REPLYTO_END_DIGIT 3.3
+  header FREEMAIL_HDR_REPLYTO eval:check_freemail_header('Reply-To')
+  score FREEMAIL_HDR_REPLYTO 3.3
+});
+
+ok sarun ("-L -t < data/spam/freemail2", \&patterns_run_cb);
+ok_all_patterns();
+
+## No Reply-To, another freemail in body
+
+%patterns = (
+  q{ 3.3 FREEMAIL_REPLYXX }, '',
+);
+%anti_patterns = ();
+
+tstlocalrules (q{
+  header FREEMAIL_REPLYXX eval:check_freemail_replyto('reply')
+  score FREEMAIL_REPLYXX 3.3
+});
+
+ok sarun ("-L -t < data/spam/freemail3", \&patterns_run_cb);
+ok_all_patterns();
+
diff --git a/upstream/t/fromnamespoof.t b/upstream/t/fromnamespoof.t
new file mode 100755 (executable)
index 0000000..d046c8c
--- /dev/null
@@ -0,0 +1,30 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("fromnamespoof");
+
+use Test::More;
+
+plan tests => 3;
+
+# ---------------------------------------------------------------------------
+
+tstpre ("
+loadplugin Mail::SpamAssassin::Plugin::FromNameSpoof
+");
+
+tstlocalrules ("
+  header FROMNAME_EQUALS_TO  eval:check_fromname_equals_to()
+  score FROMNAME_EQUALS_TO 3.3
+
+  header FROMNAME_EQUALS_REPLYTO  eval:check_fromname_equals_replyto()
+  score FROMNAME_EQUALS_REPLYTO 3.3
+");
+
+%patterns = (
+  q{ 3.3 FROMNAME_EQUALS_TO }, 'FROMNAME_EQUALS_TO',
+  q{ 3.3 FROMNAME_EQUALS_REPLYTO }, 'FROMNAME_EQUALS_REPLYTO',
+);
+
+ok sarun ("-L -t < data/spam/fromnamespoof/spoof1", \&patterns_run_cb);
+ok_all_patterns();
index b6c47d79bf5fe9acab65d104dd64bd8d4ab700c2..4e3c7930859f234dc9204ad9d245fe58d37f42af 100755 (executable)
@@ -2,30 +2,53 @@
 
 use lib '.'; use lib 't';
 use SATest; sa_t_init("get_all_headers");
-use Test::More tests => 5;
+use Test::More;
 
-# ---------------------------------------------------------------------------
-
-%patterns = (
+use constant HAS_EMAIL_ADDRESS_XS => eval { require Email::Address::XS; };
 
-q{ MIME-Version: 1.0 } => 'no-extra-space',
+$tests = 19;
+$tests += 19 if (HAS_EMAIL_ADDRESS_XS);
+plan tests => $tests;
 
-q{/text-all-raw: Received: from yahoo\.com\[\\\\n\]    \(PPPa33-ResaleLosAngelesMetroB2-2R7452\.dialinx\.net \[4\.48\.136\.190\]\) by\[\\\\n\]    www\.goabroad\.com\.cn \(8\.9\.3/8\.9\.3\) with SMTP id TAA96146; Thu,\[\\\\n\]    30 Aug 2001 19:06:45 \+0800 \(CST\) \(envelope-from\[\\\\n\]    pertand\@email\.mondolink\.com\)\[\\\\n\]From  :<tst1\@example\.com>\[\\\\n\]X-Mailer: Mozilla 4\.04 \[en\]C-bls40  \(Win95; U\)\[\\\\n\]To: jenny33436\@netscape\.net\[\\\\n\]Subject: via\.gra\[\\\\n\]From:\[\\\\t\]  <tst2\@example\.com>\[\\\\n\]DATE: Fri, 7 Dec 2001 07:01:03\[\\\\n\]MIME-Version: 1\.0\[\\\\n\]Message-Id: <20011206235802\.4FD6F1143D6\@mail\.netnoteinc\.com>\[\\\\n\]Sender: travelincentives\@aol\.com\[\\\\n\]Content-Type: text/plain; charset="us-ascii"\[\\\\n\]/} => 'full-headers-raw',
-
-q{/text-all-noraw: Received: from yahoo\\.com \\(PPPa33-ResaleLosAngelesMetroB2-2R7452\\.dialinx\\.net \\[4\\.48\\.136\\.190\\]\\) by www\\.goabroad\\.com\\.cn \\(8\\.9\\.3/8\\.9\\.3\\) with SMTP id TAA96146; Thu, 30 Aug 2001 19:06:45 \\+0800 \\(CST\\) \\(envelope-from pertand\\@email\\.mondolink\\.com\\)\[\\\\n\]From: <tst1\\@example\\.com>\[\\\\n\]X-Mailer: Mozilla 4\\.04 \\[en\\]C-bls40  \\(Win95; U\\)\[\\\\n\]To: jenny33436\\@netscape\\.net\[\\\\n\]Subject: via\\.gra\[\\\\n\]From: <tst2\\@example\\.com>\[\\\\n\]DATE: Fri, 7 Dec 2001 07:01:03\[\\\\n\]MIME-Version: 1\\.0\[\\\\n\]Message-Id: <20011206235802\\.4FD6F1143D6\\@mail\\.netnoteinc\\.com>\[\\\\n\]Sender: travelincentives\\@aol\\.com\[\\\\n\]Content-Type: text/plain; charset="us-ascii"\[\\\\n\]/} => 'full-headers-noraw',
+# ---------------------------------------------------------------------------
 
+%patterns = (
+  'MIME-Version: 1.0' => 'no-extra-space',
+  'scalar-text-all-raw: Received: from yahoo.com[\n]    (PPPa33-ResaleLosAngelesMetroB2-2R7452.dialinx.net [4.48.136.190]) by[\n]    www.goabroad.com.cn (8.9.3/8.9.3) with SMTP id TAA96146; Thu,[\n]    30 Aug 2001 19:06:45 +0800 (CST) (envelope-from[\n]    pertand@email.mondolink.com)[\n]From  :<tst1@example.com>[\n]X-Mailer: Mozilla 4.04 [en]C-bls40  (Win95; U)[\n]To: jenny33436@netscape.net[\n]Subject: via.gra[\n]From:[\t]  <tst2@example.com>[\n]DATE: Fri, 7 Dec 2001 07:01:03[\n]MIME-Version: 1.0[\n]Message-Id: <20011206235802.4FD6F1143D6@mail.netnoteinc.com>[\n]Sender: travelincentives@aol.com[\n]Content-Type: text/plain; charset="us-ascii"[\n][END]' => 'scalar-text-all-raw',
+  'scalar-text-all-noraw: Received: from yahoo.com (PPPa33-ResaleLosAngelesMetroB2-2R7452.dialinx.net [4.48.136.190]) by www.goabroad.com.cn (8.9.3/8.9.3) with SMTP id TAA96146; Thu, 30 Aug 2001 19:06:45 +0800 (CST) (envelope-from pertand@email.mondolink.com)[\n]From: <tst1@example.com>[\n]X-Mailer: Mozilla 4.04 [en]C-bls40  (Win95; U)[\n]To: jenny33436@netscape.net[\n]Subject: via.gra[\n]From: <tst2@example.com>[\n]DATE: Fri, 7 Dec 2001 07:01:03[\n]MIME-Version: 1.0[\n]Message-Id: <20011206235802.4FD6F1143D6@mail.netnoteinc.com>[\n]Sender: travelincentives@aol.com[\n]Content-Type: text/plain; charset="us-ascii"[\n][END]' => 'scalar-text-all-noraw',
+  'scalar-text-from-raw: <tst1@example.com>[\n][\t]  <tst2@example.com>[\n][END]' => 'scalar-text-from-raw',
+  'scalar-text-from-noraw: <tst1@example.com>[\n]<tst2@example.com>[\n][END]' => 'scalar-text-from-noraw',
+  'scalar-text-from-addr: tst1@example.com[END]' => 'scalar-text-from-addr',
+  'list-text-all-raw: Received: from yahoo.com[\n]    (PPPa33-ResaleLosAngelesMetroB2-2R7452.dialinx.net [4.48.136.190]) by[\n]    www.goabroad.com.cn (8.9.3/8.9.3) with SMTP id TAA96146; Thu,[\n]    30 Aug 2001 19:06:45 +0800 (CST) (envelope-from[\n]    pertand@email.mondolink.com)[\n][LIST]From  :<tst1@example.com>[\n][LIST]X-Mailer: Mozilla 4.04 [en]C-bls40  (Win95; U)[\n][LIST]To: jenny33436@netscape.net[\n][LIST]Subject: via.gra[\n][LIST]From:[\t]  <tst2@example.com>[\n][LIST]DATE: Fri, 7 Dec 2001 07:01:03[\n][LIST]MIME-Version: 1.0[\n][LIST]Message-Id: <20011206235802.4FD6F1143D6@mail.netnoteinc.com>[\n][LIST]Sender: travelincentives@aol.com[\n][LIST]Content-Type: text/plain; charset="us-ascii"[\n][END]' => 'list-text-all-raw',
+  'list-text-all-noraw: Received: from yahoo.com (PPPa33-ResaleLosAngelesMetroB2-2R7452.dialinx.net [4.48.136.190]) by www.goabroad.com.cn (8.9.3/8.9.3) with SMTP id TAA96146; Thu, 30 Aug 2001 19:06:45 +0800 (CST) (envelope-from pertand@email.mondolink.com)[\n][LIST]From: <tst1@example.com>[\n][LIST]X-Mailer: Mozilla 4.04 [en]C-bls40  (Win95; U)[\n][LIST]To: jenny33436@netscape.net[\n][LIST]Subject: via.gra[\n][LIST]From: <tst2@example.com>[\n][LIST]DATE: Fri, 7 Dec 2001 07:01:03[\n][LIST]MIME-Version: 1.0[\n][LIST]Message-Id: <20011206235802.4FD6F1143D6@mail.netnoteinc.com>[\n][LIST]Sender: travelincentives@aol.com[\n][LIST]Content-Type: text/plain; charset="us-ascii"[\n][END]' => 'list-text-all-noraw',
+  'list-text-from-raw: <tst1@example.com>[\n][LIST][\t]  <tst2@example.com>[\n][END]' => 'list-text-from-raw',
+  'list-text-from-noraw: <tst1@example.com>[\n][LIST]<tst2@example.com>[\n][END]' => 'list-text-from-noraw',
+  'list-text-from-addr: tst1@example.com[LIST]tst2@example.com[END]' => 'list-text-from-addr',
+  'list-text-from-first-addr: tst1@example.com[END]' => 'list-text-from-first-addr',
+  'list-text-from-last-addr: tst2@example.com[END]' => 'list-text-from-last-addr',
+  'list-text-msgid-host: mail.netnoteinc.com[END]' => 'list-text-msgid-host',
+  'list-text-msgid-domain: netnoteinc.com[END]' => 'list-text-msgid-domain',
+  'list-text-received-ip: 4.48.136.190[END]' => 'list-text-received-ip',
+  'list-text-received-revip: 190.136.48.4[END]' => 'list-text-received-revip',
 );
 
 %anti_patterns = (
-
-q{/MIME-Version:  1\.0/} => 'extra-space'
-
+  qr/MIME-Version:  1\.0/ => 'extra-space'
 );
 
-tstlocalrules ("
-  loadplugin Dumpheaders ../../data/Dumpheaders.pm
+tstprefs ("
+  loadplugin Dumpheaders ../../../data/Dumpheaders.pm
 ");
 
+# Internal parser
+$ENV{'SA_HEADER_ADDRESS_PARSER'} = 1;
 ok (sarun ("-L -t < data/spam/008", \&patterns_run_cb));
 ok_all_patterns();
 
+if (HAS_EMAIL_ADDRESS_XS) {
+  # Email::Address::XS
+  $ENV{'SA_HEADER_ADDRESS_PARSER'} = 2;
+  ok (sarun ("-L -t < data/spam/008", \&patterns_run_cb));
+  ok_all_patterns();
+} else { warn "Not running Email::Address::XS tests, module missing\n"; }
+
index adaf409019fe8f9542bab16fca42adcff8822827..54ee5abb9c5b8fa626191890a3562c8a52bbdf93 100755 (executable)
@@ -1,46 +1,79 @@
 #!/usr/bin/perl -w -T
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_tests.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
+###
+### UTF-8 CONTENT, edit with UTF-8 locale/editor
+###
 
 use strict;
 
 use lib '.'; use lib 't';
 use SATest; sa_t_init("get_headers");
+use Test::More;
+
 use Mail::SpamAssassin;
 
-use Test::More tests => 22;
+use constant HAS_EMAIL_ADDRESS_XS => eval { require Email::Address::XS; };
+
+my $tests = 52;
+$tests *= 2 if (HAS_EMAIL_ADDRESS_XS);
+plan tests => $tests;
 
 ##############################################
 
 # initialize SpamAssassin
-my $sa = create_saobj({'dont_copy_prefs' => 1});
-$sa->init(0);
-my $mail = $sa->parse( get_raw_headers()."\n\nBlah\n" );
-my $msg = Mail::SpamAssassin::PerMsgStatus->new($sa, $mail);
+my ($sa,$mail,$pms);
+sub new_saobj {
+  $pms->finish() if $pms;
+  $mail->finish() if $mail;
+  $sa->finish() if $sa;
+  undef $sa; undef $mail; undef $pms;
+  $sa = create_saobj({'dont_copy_prefs' => 1});
+  $sa->init(0);
+  $mail = $sa->parse( get_raw_headers()."\n\nBlah\n" );
+  $pms = Mail::SpamAssassin::PerMsgStatus->new($sa, $mail);
+}
 
 sub try {
   my ($try, $expect) = @_;
-  my $result = $msg->get($try);
 
-  # undef might be valid in some situations, so deal with it...
+  my $result;
+  my @results = $pms->get($try);
+  if (!@results) {
+    $result = undef;
+  } else {
+    $result = join("\\n", @results);
+  }
+
+  my $parser = $Mail::SpamAssassin::Util::header_address_parser == 1 ?
+    'internal' : 'Email::Address::XS';
+
+  # Whitelist some differences
+  if ($parser eq 'Email::Address::XS') {
+    # try: Email::Address::XS: 'From5:addr' failed! expect: 'noreply@foobar.com\ninfo=foobar.com@mlsend.com' got: 'noreply@foobar.com'
+    return 1 if $try eq 'From5:addr' && $result eq 'noreply@foobar.com';
+    # try: Email::Address::XS: 'From5:name' failed! expect: undef got: '=?UTF-8?Q? Foobar _'
+    return 1 if $try eq 'From5:name' && $result eq '=?UTF-8?Q? Foobar _';
+    # try: Email::Address::XS: 'From9:name' failed! expect: 'Mr\nSpam' got: 'Mr, Spam <spam@blah.com>\nSpam'
+    return 1 if $try eq 'From9:name' && $result eq 'Mr, Spam <spam@blah.com>\nSpam';
+  }
+
   if (!defined $expect) {
-    return !defined $result;
+    if (defined $result) {
+      my $lr=$result;$lr=~s/\t/\\t/gs;$lr =~s/\n/\\n/gs;
+      warn "try: $parser: '$try' failed! expect: undef got: '$lr'\n";
+      return 0;
+    } else {
+      return 1;
+    }
   }
   elsif (!defined $result) {
-    return 0;
+    if (defined $expect) {
+      my $le=$expect;$le=~s/\t/\\t/gs;$le =~s/\n/\\n/gs;
+      warn "try: $parser: '$try' failed! expect: '$le' got: undef\n";
+      return 0;
+    } else {
+      return 1;
+    }
   }
 
   if ($expect eq $result) {
@@ -48,7 +81,7 @@ sub try {
   } else {
     my $le=$expect;$le=~s/\t/\\t/gs;$le =~s/\n/\\n/gs;
     my $lr=$result;$lr=~s/\t/\\t/gs;$lr =~s/\n/\\n/gs;
-    warn "try: '$try' failed! expect: '$le' got: '$lr'\n";
+    warn "try: $parser: '$try' failed! expect: '$le' got: '$lr'\n";
     return 0;
   }
 }
@@ -75,25 +108,55 @@ Hdr1:    foo
   
 To_bug5201_a: =?ISO-2022-JP?B?GyRCQjw+ZRsoQiAbJEI1V0JlGyhC?= <jm@foo>
 To_bug5201_b: =?ISO-2022-JP?B?GyRCNiVHTyM3JSQlcyU1JSQlQCE8PnBKcxsoQg==?= <jm@foo>
-To_bug5201_c: "joe+<blah>@example.com"
+To_bug5201_c: "joe+foobar@example.com"
+From1: Foo Blah
+From2: <jm@foo>, "'Foo Blah'" <jm@bar>, =?utf-8?Q?'Baz Bl=C3=A4h'?= <baz@blaeh>
+From3: =?utf-8?Q?"B=C3=A4z=C3=A4=C3=A4_=28baz=40blah.?= =?utf-8?Q?com=29"?= <jm@foo>
+From4: "Mr., Spam"<spam@(comment)blah.com(comment)>(comment)
+From5: =?UTF-8?Q?"Foobar"_<noreply@foobar.com>?=, =?utf-8?Q?"Foobar"?=<info=foobar.com@mlsend.com>
+X-Note: From6 is really \\\" - escaped perl backslashes..
+From6: "Mr. <Spam> (foo@bar)\\\\\\"" <spam@blah.com> (comment)
+From7: "Mr. <Spam> \(foo\@bar)\\\\\\\\\\"" <spam@blah.com> (comment)
+From8: "Foo Blah \(via Foobar\)" <no-reply@foobar.com>, "Foo Blah (via Foobar)" <no-reply@foobar.com>
+From9: Mr, Spam <spam@blah.com>
 };
 }
 
 ##############################################
 
+
+for (1 .. 2) { ## parser loop
+
+if ($_ == 2 && !HAS_EMAIL_ADDRESS_XS) {
+  warn "Not running Email::Address::XS tests, module missing\n";
+  next;
+}
+
+$Mail::SpamAssassin::Util::header_address_parser = $_;
+new_saobj();
+
 ok(try('To1:addr', 'jm@foo'));
+ok(try('To1:name', undef));
 ok(try('To2:addr', 'jm@foo'));
+ok(try('To2:name', undef));
 ok(try('To3:addr', 'jm@foo'));
-ok(try('To4:addr', 'jm@foo'));
-ok(try('To5:addr', 'jm@foo'));
+ok(try('To3:name', 'Foo Blah'));
+ok(try('To4:addr', 'jm@foo\njm@bar'));
+ok(try('To4:name', undef));
+ok(try('To5:addr', 'jm@foo\njm@bar'));
+ok(try('To5:name', 'Foo Blah'));
 ok(try('To6:addr', 'jm@foo'));
+ok(try('To6:name', 'Foo Blah'));
 ok(try('To7:addr', 'jm@foo'));
+ok(try('To7:name', 'Foo Blah'));
 ok(try('To8:addr', 'jm@foo'));
+ok(try('To8:name', 'Foo Blah'));
 ok(try('To9:addr', 'jm@foo'));
+ok(try('To9:name', '_$B!z8=6b$=$N>l$GEv$?$j!*!zEv_(B_$B$?$k!*!)$/$8!z7|>^%%s%P!<!z_(B'));
 ok(try('To10:addr', '"Another User"@foo'));
 ok(try('To10:name', 'Some User'));
 ok(try('To11:addr', '"Some User"@foo'));
-ok(try('To11:name', ''));
+ok(try('To11:name', undef));
 ok(try('To12:addr', 'jm@foo'));
 ok(try('To12:name', 'Some User <jm@bar>'));
 ok(try('To13:addr', 'jm@foo'));
@@ -101,6 +164,33 @@ ok(try('To13:name', 'Some User <"Some User"@bar>'));
 ok(try('Hdr1', "foo   bar baz\n"));
 ok(try('Hdr1:raw', "    foo  \n  bar\n\tbaz \n  \n"));
 ok(try('To_bug5201_a:addr', 'jm@foo'));
+ok(try('To_bug5201_a:name', '村上 久代'));
 ok(try('To_bug5201_b:addr', 'jm@foo'));
-ok(try('To_bug5201_c:addr', '"joe+<blah>@example.com"'));
+ok(try('To_bug5201_b:name', '競馬7インサイダー情報'));
+ok(try('To_bug5201_c:addr', 'joe+foobar@example.com'));
+ok(try('To_bug5201_c:name', undef));
+ok(try('From1:addr', undef));
+ok(try('From1:name', 'Foo Blah'));
+ok(try('From2:addr', 'jm@foo\njm@bar\nbaz@blaeh'));
+ok(try('From2:name', 'Foo Blah\nBaz Bläh'));
+ok(try('From3:addr', 'jm@foo'));
+ok(try('From3:name', 'Bäzää (baz@blah.com)'));
+ok(try('From4:addr', 'spam@blah.com'));
+ok(try('From4:name', 'Mr., Spam'));
+ok(try('From5:addr', 'noreply@foobar.com\ninfo=foobar.com@mlsend.com'));
+ok(try('From5:name', undef));
+ok(try('From6:addr', 'spam@blah.com'));
+ok(try('From6:name', 'Mr. <Spam> (foo@bar)"'));
+ok(try('From7:addr', 'spam@blah.com'));
+ok(try('From7:name', 'Mr. <Spam> (foo@bar)"'));
+ok(try('From8:addr', 'no-reply@foobar.com\nno-reply@foobar.com'));
+ok(try('From8:name', 'Foo Blah (via Foobar)\nFoo Blah (via Foobar)'));
+ok(try('From9:addr', 'spam@blah.com'));
+ok(try('From9:name', 'Mr\nSpam'));
+
+} ## end parser loop
+
+$pms->finish() if $pms;
+$mail->finish() if $mail;
+$sa->finish() if $sa;
 
index ed689793914930d9fa94b34df4671ea44e1661c0..9fec17cb912bf83169c374caf18eb40b2543490c 100755 (executable)
@@ -1,36 +1,21 @@
 #!/usr/bin/perl -T
 
 use lib '.'; use lib 't';
-use SATest; sa_t_init("spam");
+use SATest; sa_t_init("gtube");
+
 use Test::More tests => 4;
 
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ BODY: Generic Test for Unsolicited Bulk Email }, 'gtube',
-
+  q{ 1000 GTUBE }, 'gtube',
 );
 
-tstprefs ("
-        $default_cf_lines
-
-        ifplugin Mail::SpamAssassin::Plugin::AWL
-         use_auto_whitelist 1
-         auto_whitelist_path ./log/awl
-         auto_whitelist_file_mode 0755
-        endif
-");
-
-$ENV{'LANGUAGE'} = $ENV{'LC_ALL'} = 'C';             # a cheat, but we match the description
-
 ok (sarun ("-L -t < data/spam/gtube.eml", \&patterns_run_cb));
 ok_all_patterns();
 
 %patterns = (
-
-q{ X-Spam-Status: No }, 'not_marked_as_spam_from_awl_bonus',
-
+  qr/^X-Spam-Status: No/m, 'not_marked_as_spam_from_awl_bonus',
 );
 
 ok (sarun ("-L -t < data/nice/not_gtube.eml", \&patterns_run_cb));
diff --git a/upstream/t/hashbl.t b/upstream/t/hashbl.t
new file mode 100755 (executable)
index 0000000..4e84a9c
--- /dev/null
@@ -0,0 +1,150 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("hashbl");
+
+use Test::More;
+plan skip_all => "Net tests disabled"          unless conf_bool('run_net_tests');
+plan skip_all => "Can't use Net::DNS Safely"   unless can_use_net_dns_safely();
+
+# run many times to catch some random natured failures
+my $iterations = 5;
+plan tests => 13 * $iterations;
+
+# ---------------------------------------------------------------------------
+
+%patterns = (
+ q{ 1.0 X_HASHBL_EMAIL } => '',
+ q{ 1.0 X_HASHBL_OSENDR } => '',
+ q{ 1.0 X_HASHBL_BTC } => '',
+ q{ 1.0 X_HASHBL_NUM } => '',
+ q{ 1.0 X_HASHBL_URI } => '',
+ q{ 1.0 X_HASHBL_TAG } => '',
+ q{ 1.0 META_HASHBL_EMAIL } => '',
+ q{ 1.0 META_HASHBL_BTC } => '',
+ q{ 1.0 META_HASHBL_URI } => '',
+);
+%anti_patterns = (
+ q{ 1.0 X_HASHBL_SHA256 } => '',
+ q{ warn: } => '',
+);
+
+# Check from debug output log that nothing else than these were queried
+@valid_queries = qw(
+cb565607a98fbdf1be52cdb86466ab34244bd6fc.hashbltest1.spamassassin.org
+bc9f1b35acd338b92b0659cc2111e6b661a8b2bc.hashbltest1.spamassassin.org
+62e12fbe4b32adc2e87147d74590372b461f35f6.hashbltest1.spamassassin.org
+96b802967118135ef048c2bc860e7b0deb7d2333.hashbltest1.spamassassin.org
+1675677ba3d539bdfb0ae8940bf7e6c836f3ad17.hashbltest1.spamassassin.org
+2ead26370ef9d238584aa3c86a02e254708370a0.hashbltest1.spamassassin.org
+170d83ef2dc9c2de0e65ce4461a3a375.hashbltest2.spamassassin.org
+cc205dd956d568ff8524d7fc42868500e4d7d162.hashbltest3.spamassassin.org
+jykf2a5v6asavfel3stymlmieh4e66jeroxuw52mc5xhdylnyb7a.hashbltest3.spamassassin.org
+6a42acf4133289d595e3875a9d677f810e80b7b4.hashbltest4.spamassassin.org
+5c6205960a65b1f9078f0e12dcac970aab0015eb.hashbltest4.spamassassin.org
+1234567890.hashbltest5.spamassassin.org
+w3hcrlct6yshq5vq6gjv2hf3pzk3jvsk6ilj5iaks4qwewudrr6q.hashbltest6.spamassassin.org
+userpart.hashbltest7.spamassassin.org
+host.domain.com.hashbltest7.spamassassin.org
+domain.com.hashbltest7.spamassassin.org
+2qlyngefopecg66lt6pwfpegjaajbzasuxs5vzgii2vfbonj6rua.hashbltest8.spamassassin.org
+11231234567.hashbltest9.spamassassin.org
+);
+
+sub check_queries {
+  my %invalid;
+  my %found;
+  if (!open(WL, $current_checkfile)) {
+    diag("LOGFILE OPEN FAILED");
+    return 0;
+  }
+  while (<WL>) {
+    my $line = $_;
+    print STDERR $line if $line =~ /warn:/;
+    while ($line =~ m,([^\s/]+\.hashbltest\d\.spamassassin\.org)\b,g) {
+      my $query = $1;
+      if (!grep { $query eq $_ } @valid_queries) {
+        $invalid{$query}++;
+      } else {
+        $found{$query}++;
+      }
+    }
+  }
+  close WL;
+  diag("Unwanted query launched: $_") foreach (keys %invalid);
+  unless (keys %found == @valid_queries) {
+    foreach (@valid_queries) {
+      if (!exists $found{$_}) {
+        diag("Query not launched: $_");
+      }
+    }
+    return 0;
+  }
+  return !%invalid;
+}
+
+tstlocalrules(q{
+  rbl_timeout 30
+
+  clear_uridnsbl_skip_domain
+  uridnsbl_skip_domain trusted.com
+
+  header   X_HASHBL_EMAIL eval:check_hashbl_emails('hashbltest1.spamassassin.org')
+  tflags   X_HASHBL_EMAIL net
+
+  hashbl_acl_freemail gmail.com
+  header   X_HASHBL_OSENDR eval:check_hashbl_emails('hashbltest2.spamassassin.org/A', 'md5/max=10/shuffle', 'X-Original-Sender', '^127\.', 'freemail')
+  tflags   X_HASHBL_OSENDR net
+
+  body     X_HASHBL_BTC eval:check_hashbl_bodyre('hashbltest3.spamassassin.org', 'sha1/max=10/shuffle', '\b([13][a-km-zA-HJ-NP-Z1-9]{25,34})\b')
+  tflags   X_HASHBL_BTC net
+
+  body     X_HASHBL_NUM eval:check_hashbl_bodyre('hashbltest9.spamassassin.org', 'raw/max=10/shuffle/num', '\b(?:\+)?(?:\s)?((?:[0-9]{1,2})?(?:\s)?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6})\b', '127.0.0.2')
+  tflags   X_HASHBL_NUM net
+
+  # Not supposed to hit, @valid_queries just checks that sha256 is calculated correctly
+  body     X_HASHBL_SHA256 eval:check_hashbl_bodyre('hashbltest3.spamassassin.org', 'sha256/max=10/shuffle', '\b([13][a-km-zA-HJ-NP-Z1-9]{25,34})\b')
+  tflags   X_HASHBL_SHA256 net
+
+  header   X_HASHBL_URI eval:check_hashbl_uris('hashbltest4.spamassassin.org', 'sha1', '127.0.0.2')
+  tflags   X_HASHBL_URI net
+
+  header   __X_SOME_ID X-Some-ID =~ /^(?<XSOMEID>\d{10,20})$/
+  header   X_HASHBL_TAG eval:check_hashbl_tag('hashbltest5.spamassassin.org/A', 'raw', 'XSOMEID', '^127\.')
+  tflags   X_HASHBL_TAG net
+
+  # Not supposed to hit, @valid_queries just checks that they are launched
+  hashbl_ignore text/plain
+  body     X_HASHBL_ATT eval:check_hashbl_attachments('hashbltest6.spamassassin.org/A', 'sha256')
+  describe X_HASHBL_ATT Message contains attachment found on attbl
+  tflags   X_HASHBL_ATT net
+
+  # email user/host/domain
+  hashbl_acl_domacl host.domain.com
+  header __X_HASHBL_UHD1 eval:check_hashbl_emails('hashbltest7.spamassassin.org', 'raw/user', 'body', '^', 'domacl')
+  header __X_HASHBL_UHD2 eval:check_hashbl_emails('hashbltest7.spamassassin.org', 'raw/host', 'body', '^', 'domacl')
+  header __X_HASHBL_UHD3 eval:check_hashbl_emails('hashbltest7.spamassassin.org', 'raw/domain', 'body', '^', 'domacl')
+
+  hashbl_email_domain_alias domain.com aliasdomain.com
+  hashbl_acl_domaincom domain.com
+  header   X_HASHBL_ALIAS_NODOT eval:check_hashbl_emails('hashbltest8.spamassassin.org', 'sha256/nodot', 'body', '^127\.', 'domaincom')
+  tflags   X_HASHBL_ALIAS_NODOT net
+
+  # Bug 7897 - test that meta rules depending on net rules hit
+  meta META_HASHBL_EMAIL X_HASHBL_EMAIL
+  # It also needs to hit even if priority is lower than dnsbl (-100)
+  meta META_HASHBL_BTC X_HASHBL_BTC
+  priority META_HASHBL_BTC -500
+  # Or super high
+  meta META_HASHBL_URI X_HASHBL_URI
+  priority META_HASHBL_URI 2000
+  priority X_HASHBL_URI 2000
+});
+
+for (1 .. $iterations) {
+  clear_localrules() if $_ == 3; # do some tests without any other rules to check meta bugs
+  ok sarun ("-t -D async,dns,HashBL < data/spam/hashbl 2>&1", \&patterns_run_cb);
+  ok(check_queries());
+  ok_all_patterns();
+}
+
diff --git a/upstream/t/hashcash.t b/upstream/t/hashcash.t
deleted file mode 100755 (executable)
index 26e03ee..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-#!/usr/bin/perl -T
-
-use lib '.'; use lib 't';
-use SATest; sa_t_init("hashcash");
-
-# we need DB_File to support the double-spend db.
-use constant HAS_DB_FILE => eval { require DB_File };
-
-use Test::More;
-plan skip_all => "This test requires DB_File" unless HAS_DB_FILE;
-plan tests => 4;
-
-# ---------------------------------------------------------------------------
-
-%patterns = (
-q{ HASHCASH_24 }, 'hashcash24',
-);
-
-tstprefs ('
-    hashcash_accept test@example.com test1@example.com test2@example.com
-    hashcash_doublespend_path log/user_state/hashcash_seen
-    ');
-
-sarun ("-L -t < data/nice/001", \&patterns_run_cb);
-ok_all_patterns();
-
-%patterns = (
-q{ HASHCASH_20 }, 'hashcash20',
-);
-
-sarun ("-L -t < data/nice/001", \&patterns_run_cb);
-ok_all_patterns();
-
-%patterns = (
-q{ HASHCASH_2SPEND }, '2spend',
-);
-
-sarun ("-L -t < data/nice/001", \&patterns_run_cb);
-ok_all_patterns();
-
-# try again with a mail with 2 tokens, one unspent.
-%patterns = ();
-%anti_patterns = (
-q{ HASHCASH_2SPEND }, '2spend',
-);
-sarun ("-L -t < data/nice/016", \&patterns_run_cb);
-ok_all_patterns();
-
diff --git a/upstream/t/header.t b/upstream/t/header.t
new file mode 100755 (executable)
index 0000000..fdfeec2
--- /dev/null
@@ -0,0 +1,97 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("header");
+use Test::More tests => 23;
+
+# ---------------------------------------------------------------------------
+
+tstprefs('
+  # exists
+  header TEST_EXISTS1 exists:To
+  header TEST_EXISTS2 exists:Not-Exist
+
+  # if-unset
+  header TEST_UNSET1 Not-Exist =~ /./
+  header TEST_UNSET2 Not-Exist =~ /^UNSET$/ [if-unset: UNSET]
+  header TEST_UNSET3 Not-Exist =~ /^NOT$/ [if-unset: UNSET]
+
+  # exists should not leak to a redefined test
+  header TEST_LEAK1 exists:Not-Exist
+  header TEST_LEAK1 To =~ /notexist/
+
+  # if-unset should not leak to a redefined test
+  header TEST_LEAK2 Not-Exist =~ /^UNSET$/ [if-unset: UNSET]
+  header TEST_LEAK2 Not-Exist =~ /^UNSET$/
+
+  # op should not leak to a redefined test
+  header TEST_LEAK3 To !~ /./
+  header TEST_LEAK3 To =~ /notfound/
+
+  # Test 4.0 :first :last parser
+  header HEADER_FIRST1 X-Hashcash:first =~ /^0:040315:test@example.com:69781c87bae95c03$/
+  header HEADER_LAST1 X-Hashcash:last =~ /^1:20:040806:test1@example.com:test=foo:482b788d12eb9b56:2a3349$/
+  header HEADER_ALL1 X-Hashcash =~ /^0:040315:.*1:20:040806:/s
+
+  # Meta should evaluate all
+  meta TEST_META (TEST_EXISTS1 && TEST_UNSET2 && HEADER_FIRST1 && HEADER_LAST1 && HEADER_ALL1)
+');
+
+%patterns = (
+  q{ 1.0 TEST_EXISTS1 }, '',
+  q{ 1.0 TEST_UNSET2 }, '',
+  q{ 1.0 HEADER_FIRST1 }, '',
+  q{ 1.0 HEADER_LAST1 }, '',
+  q{ 1.0 HEADER_ALL1 }, '',
+  q{ 1.0 TEST_META }, '',
+);
+%anti_patterns = (
+  q{ TEST_EXISTS2 }, '',
+  q{ TEST_UNSET1 }, '',
+  q{ TEST_UNSET3 }, '',
+  q{ TEST_LEAK1 }, '',
+  q{ TEST_LEAK2 }, '',
+  q{ TEST_LEAK3 }, '',
+);
+
+ok (sarun ("-L -t < data/nice/001", \&patterns_run_cb));
+ok_all_patterns();
+
+##########################################
+
+tstprefs('
+  # Test 4.0 multiple :addr parser
+  header TO1 To:addr =~ /(?:@.*?){1}/s
+  header TONEG1 To:addr =~ /(?:@.*?){2}/s
+  header CC1 Cc:addr =~ /(?:@.*?){5}/s
+  header CCNEG1 Cc:addr =~ /(?:@.*?){6}/s
+  header TOCC1 ToCc:addr =~ /(?:@.*?){6}/s
+  header TOCCNEG1 ToCc:addr =~ /(?:@.*?){7}/s
+  header __TO_COUNT To:addr =~ /^.+$/m
+  tflags __TO_COUNT multiple
+  meta TO2 __TO_COUNT == 1
+  header __CC_COUNT Cc:addr =~ /^.+$/m
+  tflags __CC_COUNT multiple
+  meta CC2 __CC_COUNT == 5
+  header __TOCC_COUNT ToCc:addr =~ /^.+$/m
+  tflags __TOCC_COUNT multiple
+  meta TOCC2 __TOCC_COUNT == 6
+');
+
+%patterns = (
+  q{ 1.0 TO1 }, '',
+  q{ 1.0 CC1 }, '',
+  q{ 1.0 TOCC1 }, '',
+  q{ 1.0 TO2 }, '',
+  q{ 1.0 CC2 }, '',
+  q{ 1.0 TOCC2 }, '',
+);
+%anti_patterns = (
+  q{ TONEG }, '',
+  q{ CCNEG }, '',
+  q{ TOCCNEG }, '',
+);
+
+ok (sarun ("-L -t < data/nice/006", \&patterns_run_cb));
+ok_all_patterns();
+
diff --git a/upstream/t/header_utf8.t b/upstream/t/header_utf8.t
new file mode 100755 (executable)
index 0000000..f86acf1
--- /dev/null
@@ -0,0 +1,232 @@
+#!/usr/bin/perl -T
+
+###
+### UTF-8 CONTENT, edit with UTF-8 locale/editor
+###
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("header_utf8.t");
+
+use constant HAS_EMAIL_ADDRESS_XS => eval { require Email::Address::XS; };
+use constant HAS_LIBIDN => eval { require Net::LibIDN; };
+use constant HAS_LIBIDN2 => eval { require Net::LibIDN2; };
+
+if (!HAS_EMAIL_ADDRESS_XS) {
+  warn "Email::Address::XS is not installed, tests will be lacking\n";
+}
+if (!HAS_LIBIDN && !HAS_LIBIDN2) {
+  warn "Net::LibIDN or Net::LibIDN2 is not installed, tests will be lacking\n";
+}
+
+use Test::More;
+plan skip_all => "Test requires Perl 5.8" unless $] > 5.008; # TODO: SA already doesn't support anything below 5.8.1
+
+my $tests = 156;
+$tests = 305 if (HAS_EMAIL_ADDRESS_XS || (!HAS_EMAIL_ADDRESS_XS && HAS_LIBIDN && HAS_LIBIDN2));
+plan tests => $tests;
+
+# ---------------------------------------------------------------------------
+
+%mypatterns = (
+  ' 1.0 LT_RPATH '     => '',
+  ' 1.0 LT_ENVFROM '   => '',
+  ' 1.0 LT_FROM '      => '',
+  ' 1.0 LT_FROM_ADDR ' => '',
+  ' 1.0 LT_FROM_NAME ' => '',
+  ' 1.0 LT_FROM_RAW '  => '',
+  ' 1.0 LT_TO_ADDR '   => '',
+  ' 1.0 LT_TO_NAME '   => '',
+  ' 1.0 LT_CC_ADDR '   => '',
+  ' 1.0 LT_SUBJ '      => '',
+  ' 1.0 LT_SUBJ_RAW '  => '',
+  ' 1.0 LT_MESSAGEID ' => '',
+  ' 1.0 LT_MSGID '     => '',
+  ' 1.0 LT_CT '        => '',
+  ' 1.0 LT_CT_RAW '    => '',
+  ' 1.0 LT_AUTH_DOM '  => '',
+  ' 1.0 LT_NOTE '      => '',
+  ' 1.0 LT_UTF8SMTP_ANY '    => '',
+  ' 1.0 LT_SPLIT_UTF8_SUBJ ' => '',
+  ' 100 USER_IN_BLOCKLIST '  => '',
+);
+
+%mypatterns_utf8 = (  # as it appears in a report body
+  ' 1.0 LT_ANY_CHARS En-tête contient caractères' => 'LT_ANY_CHARS utf8',
+);
+
+%mypatterns_mime_qp = (  # as it appears in a mail header section
+  ' 1.0 LT_ANY_CHARS =?UTF-8?Q?En-t=C3=AAte_contient_caract=C3=A8res?=' => 'LT_ANY_CHARS mime encoded',
+);
+
+%mypatterns_mime_b64 = (  # as it appears in a mail header section
+  ' 1.0 LT_ANY_CHARS =?UTF-8?B?5a2X56ym6KKr5YyF5ZCr5Zyo5raI5oGv5oql5aS06YOo5YiG?=' => 'LT_ANY_CHARS mime encoded',
+);
+
+%mypatterns_mime_b64_bug7307 = (
+  ' 1.0 LT_SUBJ2 '      => '',
+  ' 1.0 LT_SUBJ2_RAW '  => '',
+);
+
+%anti_patterns = (
+  ' 1.0 NO_RELAYS '  => 'NO_RELAYS',
+# ' 1.0 INVALID_MSGID '  => 'INVALID_MSGID',
+);
+
+my $myrules = <<'END';
+  header USER_IN_BLOCKLIST  eval:check_from_in_blocklist()
+  tflags USER_IN_BLOCKLIST  userconf nice noautolearn
+  score USER_IN_BLOCKLIST 100
+  add_header all  AuthorDomain _AUTHORDOMAIN_
+  blocklist_from  Marilù.Gioffré@esempio-università.it
+  header LT_UTF8SMTP_ANY  Received =~ /\bwith\s*UTF8SMTPS?A?\b/mi
+  header LT_RPATH   Return-Path:addr =~ /^Marilù\.Gioffré\@esempio-università\.it\z/
+  header LT_ENVFROM EnvelopeFrom =~ /^Marilù\.Gioffré\@esempio-università\.it\z/
+  header LT_FROM      From =~ /^Marilù Gioffré ♥ <Marilù\.Gioffré\@esempio-università\.it>$/m
+  header LT_FROM_ADDR From:addr =~ /^Marilù\.Gioffré\@esempio-università\.it\z/
+  header LT_FROM_NAME From:name =~ /^Marilù Gioffré ♥\z/
+  header LT_FROM_RAW  From:raw  =~ /^\s*=\?ISO-8859-1\?Q\?Maril=F9\?= Gioffré ♥ <Marilù\.Gioffré\@esempio-università\.it>$/m
+  header LT_AUTH_DOM  X-AuthorDomain =~ /^xn--esempio-universit-4ob\.it\z/
+  header LT_TO_ADDR   To:addr =~ /^Dörte\@Sörensen\.example\.com\z/
+  header LT_TO_NAME   To:name =~ /^Dörte Å\. Sörensen, Jr\./
+  header LT_CC_ADDR   Cc:addr =~ /^θσερ\@εχαμπλε\.ψομ\z/
+  header LT_SUBJ      Subject =~ /^Domače omrežje$/m
+  header LT_SUBJ_RAW  Subject:raw  =~ /^\s*=\?iso-8859-2\*sl\?Q\?Doma=e8e\?=\s+=\?utf-8\*sl\?Q\?_omre=C5\?=/m
+  header LT_SUBJ2     Subject =~ /^【重要訊息】台電105年3月電費,委託金融機構扣繳成功電子繳費憑證\(電號07487616730\)$/m
+  header LT_SUBJ2_RAW Subject:raw  =~ /^\s*=\?UTF-8\?B\?44CQ6YeN6KaB6KiK5oGv44CR5Y\+w6Zu7MTA15bm0\?=\s*=\?UTF-8\?B\?M\+aciOmbu\+iyu\+\+8jOWnlOiol\+mHkeiejeapn\+ani\+aJow==\?=\s*=\?UTF-8\?B\?57mz5oiQ5Yqf6Zu75a2Q57mz6LK75oaR6K2JKOmbu\+iZnw==\?=\s*=\?UTF-8\?B\?MDc0ODc2MTY3MzAp\?=$/m
+  header LT_MSGID     Message-ID =~ /^<b497e6c2\@example\.срб>$/m
+  header LT_MESSAGEID MESSAGEID  =~ /^<b497e6c2\@example\.срб>$/m
+  header LT_CT        Content-Type =~ /документы для отдела кадров\.pdf/
+  header LT_CT_RAW    Content-Type:raw =~ /=\?utf-8\?B\?tdC70LAg0LrQsNC00YDQvtCyLnBkZg==\?="/
+  header LT_SPLIT_UTF8_SUBJ Subject:raw =~ m{(=\?UTF-8) (?: \* [^?=<>, \t]* )? (\?Q\?) [^ ?]* =[89A-F][0-9A-F] \?= \s* \1 (?: \* [^ ?=]* )? \2 =[89AB][0-9A-F]}xsmi
+  header LT_NOTE      X-Note =~ /^The above.*char =C5 =BE is invalid, .*wild$/m
+  header LT_ANY_CHARS From =~ /./
+  describe         LT_ANY_CHARS  Header contains characters
+  lang fr describe LT_ANY_CHARS  En-tête contient caractères
+  # sorry, Google translate:
+  lang zh describe LT_ANY_CHARS  字符被包含在消息报头部分
+END
+
+if (!HAS_LIBIDN && !HAS_LIBIDN2) {
+  # temporary fudge to prevent a test failing
+  # until the Net::LibIDN becomes a mandatory module
+  $myrules =~ s{^(\s*header LT_AUTH_DOM\s+X-AuthorDomain =~)\s*(/.*/)$}
+               {$1 /esempio-università\.it/}m
+}
+
+
+
+## Test 1 with internal parser, any libidn
+$ENV{'SA_HEADER_ADDRESS_PARSER'} = 1;
+if (HAS_LIBIDN) {
+  $ENV{'SA_LIBIDN'} = 1;
+} elsif (HAS_LIBIDN2) {
+  $ENV{'SA_LIBIDN'} = 2;
+  $libidn2_done++;
+}
+run_tests();
+## Test 2 with Email::Address::XS
+if (HAS_EMAIL_ADDRESS_XS) {
+  $ENV{'SA_HEADER_ADDRESS_PARSER'} = 2;
+  if (HAS_LIBIDN2 && !defined $libidn2_done) {
+    $ENV{'SA_LIBIDN'} = 2;
+    $libidn2_done++;
+  }
+  run_tests();
+} else {
+  ## .. or Test 2 with internal parser, libidn2
+  if (HAS_LIBIDN2 && !defined $libidn2_done) {
+    $ENV{'SA_LIBIDN'} = 2;
+    run_tests();
+  }
+}
+
+
+sub run_tests {
+
+$ENV{PERL_BADLANG} = 0;  # suppresses Perl warning about failed locale setting
+# see Mail::SpamAssassin::Conf::Parser::parse(), also Bug 6992
+$ENV{LANGUAGE} = $ENV{LANG} = 'fr_CH.UTF-8';
+
+#--- normalize_charset 1
+
+tstprefs ($myrules . '
+  report_safe 0
+  normalize_charset 1
+');
+
+%patterns = (%mypatterns, %mypatterns_mime_qp);
+sarun ("-L < data/nice/unicode1", \&patterns_run_cb);
+ok_all_patterns();
+
+tstprefs ($myrules . '
+  report_safe 1
+  normalize_charset 1
+');
+%patterns = (%mypatterns, %mypatterns_utf8);
+sarun ("-L < data/nice/unicode1", \&patterns_run_cb);
+ok_all_patterns();
+
+tstprefs ($myrules . '
+  report_safe 2
+  normalize_charset 1
+');
+%patterns = (%mypatterns, %mypatterns_utf8);
+sarun ("-L < data/nice/unicode1", \&patterns_run_cb);
+ok_all_patterns();
+
+#--- normalize_charset 0
+
+tstprefs ($myrules . '
+  report_safe 0
+  normalize_charset 0
+');
+%patterns = (%mypatterns, %mypatterns_mime_qp);
+sarun ("-L < data/nice/unicode1", \&patterns_run_cb);
+ok_all_patterns();
+
+tstprefs ($myrules . '
+  report_safe 1
+  normalize_charset 0
+');
+%patterns = (%mypatterns, %mypatterns_utf8);
+sarun ("-L < data/nice/unicode1", \&patterns_run_cb);
+ok_all_patterns();
+
+tstprefs ($myrules . '
+  report_safe 2
+  normalize_charset 0
+');
+%patterns = (%mypatterns, %mypatterns_utf8);
+sarun ("-L < data/nice/unicode1", \&patterns_run_cb);
+ok_all_patterns();
+
+#--- base64 encoded-words
+
+$ENV{PERL_BADLANG} = 0;  # suppresses Perl warning about failed locale setting
+# see Mail::SpamAssassin::Conf::Parser::parse(), also Bug 6992
+$ENV{LANGUAGE} = $ENV{LANG} = 'zh_CN.UTF-8';
+
+tstprefs ($myrules . '
+  report_safe 0
+  normalize_charset 1
+');
+%patterns = (%mypatterns, %mypatterns_mime_b64);
+sarun ("-L < data/nice/unicode1", \&patterns_run_cb);
+ok_all_patterns();
+
+#--- base64 encoded-words - Bug 7307
+
+$ENV{LANGUAGE} = $ENV{LANG} = 'en_US.UTF-8';
+
+tstprefs ($myrules . '
+  report_safe 0
+  normalize_charset 1
+');
+%patterns = (%mypatterns_mime_b64_bug7307);
+%anti_patterns = ();
+sarun ("-L < data/nice/unicode2", \&patterns_run_cb);
+ok_all_patterns();
+
+
+} ## run_tests
+
index b270126066aa5bd99b10550401dbfbede5fbe6f2..982c8e8851375aa8458ba07fa3d6b13a68c184fc 100755 (executable)
@@ -1,21 +1,8 @@
 #!/usr/bin/perl -w -T
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_tests.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
-
 use strict;
+use lib '.'; use lib 't';
+use SATest; sa_t_init("html_colors");
 use Test::More tests => 28;
 use Mail::SpamAssassin;
 use Mail::SpamAssassin::HTML;
index 777701aedae7fb98519dd85ef45ec683c1caa01a..5faefb9e1797ad32ebbdf1e38b9f7baec1cd6e09 100755 (executable)
@@ -7,33 +7,33 @@ use Test::More tests => 9;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-q{ MILLION_EMAIL } => 'MILLION_EMAIL',
-q{ GUARANTEE } => 'GUARANTEE',
-q{ NATURAL } => 'NATURAL',
-q{ OUR_AFFILIATE_PARTNERS } => 'OUR_AFFILIATE_PARTNERS',
-q{ VIAGRA } => 'VIAGRA',
+  q{ 1.0 MILLION_EMAIL } => '',
+  q{ 1.0 GUARANTEE } => '',
+  q{ 1.0 NATURAL } => '',
+  q{ 1.0 OUR_AFFILIATE_PARTNERS } => '',
+  q{ 1.0 VIAGRA } => '',
 );
 
 %anti_patterns = (
-q{ OPPORTUNITY } => 'OPPORTUNITY',
-q{ BUG5749_P_H2 } => 'BUG5749_P_H2',
-q{ BUG5749_H2_H3 } => 'BUG5749_H2_H3',
-q{ BUG6168_EXAMPLE } => 'BUG6168_EXAMPLE',
+  q{ OPPORTUNITY } => '',
+  q{ BUG5749_P_H2 } => '',
+  q{ BUG5749_H2_H3 } => '',
+  q{ BUG6168_EXAMPLE } => '',
 );
 
 tstlocalrules ('
-body NATURAL   /\b(?:100.|completely|totally|all) natural/i
-body GUARANTEE /\bGUARANTEE\b/
-body MILLION_EMAIL     /million (?:\w+ )?(?:e-?mail )?addresses/i
-body OUR_AFFILIATE_PARTNERS    /our affiliate partners/i
-body VIAGRA    /viagra/i
-body OPPORTUNITY       /OPPORTUNITY/
-
-body BUG5749_P_H2       /foobar/
-body BUG5749_H2_H3      /foobaz/
-body BUG6168_EXAMPLE    /example.orgexample.net/
-
+  body NATURAL         /\b(?:100.|completely|totally|all) natural/i
+  body GUARANTEE       /\bGUARANTEE\b/
+  body MILLION_EMAIL   /million (?:\w+ )?(?:e-?mail )?addresses/i
+  body OUR_AFFILIATE_PARTNERS  /our affiliate partners/i
+  body VIAGRA          /viagra/i
+  body OPPORTUNITY     /OPPORTUNITY/
+
+  body BUG5749_P_H2    /foobar/
+  body BUG5749_H2_H3   /foobaz/
+  body BUG6168_EXAMPLE /example.orgexample.net/
 ');
 
 sarun ("-L -t < data/spam/011", \&patterns_run_cb);
 ok_all_patterns();
+
index 19bdfd07bd6682e5dde5ed3ae46ca0479f4d669c..3d2fdf09ba420799c1965a28d10930b9857ad17e 100755 (executable)
@@ -1,26 +1,27 @@
 #!/usr/bin/perl -T
 
 use lib '.'; use lib 't';
-use SATest; sa_t_init("html_obfu");
+use SATest; sa_t_init("html_utf8");
 
 use Test::More;
-plan skip_all => "Test requires Perl 5.8.5" unless $] > 5.008004;
 plan tests => 2;
 
 # ---------------------------------------------------------------------------
 
 %patterns = (
-q{ QUOTE_YOUR } => 'QUOTE_YOUR',
+  q{ 1.0 QUOTE_YOUR } => '',
 );
 
 %anti_patterns = (
-q{ OPPORTUNITY } => 'OPPORTUNITY',
+  q{ OPPORTUNITY } => '',
 );
 
 tstlocalrules ('
-body OPPORTUNITY       /OPPORTUNITY/
-# body QUOTE_YOUR /\x{201c}Your/
-body QUOTE_YOUR /\xE2\x80\x9CYour/
+  body OPPORTUNITY     /OPPORTUNITY/
+  # body QUOTE_YOUR    /\x{201c}Your/
+  body QUOTE_YOUR      /\xE2\x80\x9CYour/
 ');
+
 sarun ("-L -t < data/spam/009", \&patterns_run_cb);
 ok_all_patterns();
+
index 73338aa081d3cc0d86cafb8d407c07e024ab833b..c0369b924fe38c923915459d99600366aad6c23e 100755 (executable)
@@ -2,62 +2,70 @@
 
 # test URIs with UTF8 IDNA-equivalent dots between domains instead of ordinary '.'
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_names.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
-
 use strict;
 use lib '.'; use lib 't';
 use SATest; sa_t_init("idn_dots.t");
-use Test::More tests => 6;
+use Test::More;
 use Mail::SpamAssassin;
 use vars qw(%patterns %anti_patterns);
 
-# initialize SpamAssassin
-my $sa = create_saobj({dont_copy_prefs => 1});
-$sa->init(0); # parse rules
+use constant HAS_LIBIDN => eval { require Net::LibIDN; };
+use constant HAS_LIBIDN2 => eval { require Net::LibIDN2; };
+
+plan skip_all => "module Net::LibIDN or Net::LibIDN2 not available, internationalized domain names with U-labels will not be recognized!"
+  unless HAS_LIBIDN||HAS_LIBIDN2;
+plan tests => 6 * ((HAS_LIBIDN||0) + (HAS_LIBIDN2||0));
 
 # load tests and write mail
 %patterns = ();
 %anti_patterns = ();
 my $message = write_mail();
+my $uris;
 
-my $mail = $sa->parse($message);
-my $msg = Mail::SpamAssassin::PerMsgStatus->new($sa, $mail);
+if (HAS_LIBIDN) {
+  $ENV{'SA_LIBIDN'} = 1;
+  check_sa();
+}
+if (HAS_LIBIDN2) {
+  $ENV{'SA_LIBIDN'} = 2;
+  check_sa();
+}
 
-my $uris = join("\n", $msg->get_uri_list(), "");
+sub check_sa {
+  # initialize SpamAssassin
+  my $sa = create_saobj({dont_copy_prefs => 1});
+  $sa->init(0); # parse rules
+  my $mail = $sa->parse($message);
+  my $msg = Mail::SpamAssassin::PerMsgStatus->new($sa, $mail);
+  $uris = join("\n", $msg->get_uri_list(), "");
+  $msg->finish();
+  $mail->finish();
+  $sa->finish();
+  check_patterns();
+}
 
-# run patterns and anti-patterns
-my $failures = 0;
-for my $pattern (keys %patterns) {
-  if (!ok($uris =~ /${pattern}/m)) {
-    warn "failure: did not find /$pattern/\n";
-    $failures++;
-  #} else {
-  #  warn "OK: did find /$pattern/\n";
+sub check_patterns {
+  # run patterns and anti-patterns
+  my $failures = 0;
+  for my $pattern (keys %patterns) {
+    if (!ok($uris =~ /${pattern}/m)) {
+      warn "failure: did not find /$pattern/\n";
+      $failures++;
+    #} else {
+    #  warn "OK: did find /$pattern/\n";
+    }
   }
-}
 
-for my $anti_pattern (keys %anti_patterns) {
-  if (!ok($uris !~ /${anti_pattern}/m)) {
-    warn "failure: did find /$anti_pattern/\n";
-    $failures++;
+  for my $anti_pattern (keys %anti_patterns) {
+    if (!ok($uris !~ /${anti_pattern}/m)) {
+      warn "failure: did find /$anti_pattern/\n";
+      $failures++;
+    }
   }
-}
 
-if ($failures) {
-  print "URIs in email from get_uri_list:\n$uris";
+  if ($failures) {
+    print "URIs in email from get_uri_list:\n$uris";
+  }
 }
 
 # function to write test email
old mode 100644 (file)
new mode 100755 (executable)
index 5d56b5e..5199d1f
@@ -2,80 +2,94 @@
 
 use lib '.'; use lib 't';
 use SATest; sa_t_init("if_can");
-use Test::More tests => 16;
+use Test::More tests => 19;
 
 # ---------------------------------------------------------------------------
 
 %patterns = (
 
-        q{ GTUBE }, 'gtube',
-        q{ SHOULD_BE_CALLED1 }, 'should_be_called1',
-        q{ SHOULD_BE_CALLED2 }, 'should_be_called2',
-        q{ SHOULD_BE_CALLED3 }, 'should_be_called3',
-        q{ SHOULD_BE_CALLED4 }, 'should_be_called4',
-        q{ SHOULD_BE_CALLED5 }, 'should_be_called5',
-        q{ SHOULD_BE_CALLED6 }, 'should_be_called6',
-        q{ SHOULD_BE_CALLED7 }, 'should_be_called7',
-        q{ SHOULD_BE_CALLED8 }, 'should_be_called8',
-        q{ SHOULD_BE_CALLED9 }, 'should_be_called9',
-        q{ SHOULD_BE_CALLED10 }, 'should_be_called10',
+  q{ 1000 GTUBE }, '',
+  q{ 1.0 SHOULD_BE_CALLED01 }, '',
+  q{ 1.0 SHOULD_BE_CALLED02 }, '',
+  q{ 1.0 SHOULD_BE_CALLED03 }, '',
+  q{ 1.0 SHOULD_BE_CALLED04 }, '',
+  q{ 1.0 SHOULD_BE_CALLED05 }, '',
+  q{ 1.0 SHOULD_BE_CALLED06 }, '',
+  q{ 1.0 SHOULD_BE_CALLED07 }, '',
+  q{ 1.0 SHOULD_BE_CALLED08 }, '',
+  q{ 1.0 SHOULD_BE_CALLED09 }, '',
+  q{ 1.0 SHOULD_BE_CALLED10 }, '',
+  q{ 1.0 SHOULD_BE_CALLED11 }, '',
+  q{ 1.0 SHOULD_BE_CALLED12 }, '',
 
 );
 %anti_patterns = (
 
-        q{ SHOULD_NOT_BE_CALLED1 }, 'should_not_be_called1',
-        q{ SHOULD_NOT_BE_CALLED2 }, 'should_not_be_called2',
-        q{ SHOULD_NOT_BE_CALLED3 }, 'should_not_be_called3',
-        q{ SHOULD_NOT_BE_CALLED4 }, 'should_not_be_called4',
+  q{ SHOULD_NOT_BE_CALLED01 }, '',
+  q{ SHOULD_NOT_BE_CALLED02 }, '',
+  q{ SHOULD_NOT_BE_CALLED03 }, '',
+  q{ SHOULD_NOT_BE_CALLED04 }, '',
+  q{ SHOULD_NOT_BE_CALLED05 }, '',
 
 );
 tstlocalrules (q{
 
-        loadplugin Mail::SpamAssassin::Plugin::Test
+  loadplugin Mail::SpamAssassin::Plugin::Test
 
-        if (has(Mail::SpamAssassin::Plugin::Test::check_test_plugin))
-          body SHOULD_BE_CALLED1 /./
-        endif
-        if (has(Mail::SpamAssassin::Plugin::Test::test_feature_xxxx_true))
-          body SHOULD_BE_CALLED2 /./
-        endif
-        if (has(Mail::SpamAssassin::Plugin::Test::test_feature_xxxx_false))
-          body SHOULD_BE_CALLED3 /./
-        endif
-        if (can(Mail::SpamAssassin::Plugin::Test::test_feature_xxxx_true))
-          body SHOULD_BE_CALLED4 /./
-        endif
-        if (!can(Mail::SpamAssassin::Plugin::Test::test_feature_xxxx_false))
-          body SHOULD_BE_CALLED5 /./
-        endif
-        if (!has(Mail::SpamAssassin::Plugin::Test::test_feature_xxxx_nosuch))
-          body SHOULD_BE_CALLED6 /./
-        endif
-        if (!can(Mail::SpamAssassin::Plugin::Test::test_feature_xxxx_nosuch))
-          body SHOULD_BE_CALLED7 /./
-        endif
-        if can(Mail::SpamAssassin::Plugin::Test::test_feature_xxxx_true) && version > 0.00000
-          body SHOULD_BE_CALLED8 /./
-        endif
-        if !can(Mail::SpamAssassin::Plugin::Test::test_feature_xxxx_false  ) && !(! version > 0.00000)
-          body SHOULD_BE_CALLED9 /./
-        endif
-        if has(Mail::SpamAssassin::Plugin::Test::test_feature_xxxx_true) && (!can(Mail::SpamAssassin::Plugin::Test::test_feature_xxxx_nosuch))
-          body SHOULD_BE_CALLED10 /./
-        endif
+  if (has(Mail::SpamAssassin::Plugin::Test::check_test_plugin))
+    body SHOULD_BE_CALLED01 /./
+  endif
+  if (has(Mail::SpamAssassin::Plugin::Test::test_feature_xxxx_true))
+    body SHOULD_BE_CALLED02 /./
+  endif
+  if (has(Mail::SpamAssassin::Plugin::Test::test_feature_xxxx_false))
+    body SHOULD_BE_CALLED03 /./
+  endif
+  if (can(Mail::SpamAssassin::Plugin::Test::test_feature_xxxx_true))
+    body SHOULD_BE_CALLED04 /./
+  endif
+  if (!can(Mail::SpamAssassin::Plugin::Test::test_feature_xxxx_false))
+    body SHOULD_BE_CALLED05 /./
+  endif
+  if (!has(Mail::SpamAssassin::Plugin::Test::test_feature_xxxx_nosuch))
+    body SHOULD_BE_CALLED06 /./
+  endif
+  if (!can(Mail::SpamAssassin::Plugin::Test::test_feature_xxxx_nosuch))
+    body SHOULD_BE_CALLED07 /./
+  endif
+  if can(Mail::SpamAssassin::Plugin::Test::test_feature_xxxx_true) && version > 0.00000
+    body SHOULD_BE_CALLED08 /./
+  endif
+  if !can(Mail::SpamAssassin::Plugin::Test::test_feature_xxxx_false  ) && !(! version > 0.00000)
+    body SHOULD_BE_CALLED09 /./
+  endif
+  if has(Mail::SpamAssassin::Plugin::Test::test_feature_xxxx_true) && (!can(Mail::SpamAssassin::Plugin::Test::test_feature_xxxx_nosuch))
+    body SHOULD_BE_CALLED10 /./
+  endif
 
-        if !has(Mail::SpamAssassin::Plugin::Test::check_test_plugin)
-          body SHOULD_NOT_BE_CALLED1 /./
-        endif
-        if (has(Mail::SpamAssassin::Plugin::Test::non_existent_method))
-          body SHOULD_NOT_BE_CALLED2 /./
-        endif
-        if (can(Mail::SpamAssassin::Plugin::Test::non_existent_method))
-          body SHOULD_NOT_BE_CALLED3 /./
-        endif
-        if (can(Mail::SpamAssassin::Plugin::Test::test_feature_xxxx_false))
-          body SHOULD_NOT_BE_CALLED4 /./
-        endif
+  if !has(Mail::SpamAssassin::Plugin::Test::check_test_plugin)
+    body SHOULD_NOT_BE_CALLED01 /./
+  endif
+  if (has(Mail::SpamAssassin::Plugin::Test::non_existent_method))
+    body SHOULD_NOT_BE_CALLED02 /./
+  endif
+  if (can(Mail::SpamAssassin::Plugin::Test::non_existent_method))
+    body SHOULD_NOT_BE_CALLED03 /./
+  endif
+  if can(Mail::SpamAssassin::Plugin::Test::test_feature_xxxx_true)
+  if (can(Mail::SpamAssassin::Plugin::Test::test_feature_xxxx_false))
+    body SHOULD_NOT_BE_CALLED04 /./
+  else
+    body SHOULD_BE_CALLED11 /./
+  endif
+  endif
+
+  if can(Mail::SpamAssassin::Conf::feature_local_tests_only) && local_tests_only
+    body SHOULD_BE_CALLED12 /./
+  endif
+  if can(Mail::SpamAssassin::Conf::feature_local_tests_only) && !local_tests_only
+    body SHOULD_NOT_BE_CALLED05 /./
+  endif
 
 });
 
diff --git a/upstream/t/if_else.t b/upstream/t/if_else.t
new file mode 100755 (executable)
index 0000000..eaa77d8
--- /dev/null
@@ -0,0 +1,114 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("if_else");
+use Test::More tests => 21;
+
+# ---------------------------------------------------------------------------
+
+%patterns = (
+
+  q{ 1000 GTUBE }, '',
+  q{ 1.0 SHOULD_BE_CALLED01 }, '',
+  q{ 1.0 SHOULD_BE_CALLED02 }, '',
+  q{ 1.0 SHOULD_BE_CALLED03 }, '',
+  q{ 1.0 SHOULD_BE_CALLED04 }, '',
+  q{ 1.0 SHOULD_BE_CALLED05 }, '',
+  q{ 1.0 SHOULD_BE_CALLED06 }, '',
+  q{ 1.0 SHOULD_BE_CALLED07 }, '',
+
+);
+%anti_patterns = (
+
+  q{ SHOULD_NOT_BE_CALLED01 }, '',
+  q{ SHOULD_NOT_BE_CALLED02 }, '',
+  q{ SHOULD_NOT_BE_CALLED03 }, '',
+  q{ SHOULD_NOT_BE_CALLED04 }, '',
+  q{ SHOULD_NOT_BE_CALLED05 }, '',
+  q{ SHOULD_NOT_BE_CALLED06 }, '',
+  q{ SHOULD_NOT_BE_CALLED07 }, '',
+  q{ SHOULD_NOT_BE_CALLED08 }, '',
+  q{ SHOULD_NOT_BE_CALLED09 }, '',
+  q{ SHOULD_NOT_BE_CALLED10 }, '',
+  q{ SHOULD_NOT_BE_CALLED11 }, '',
+  q{ SHOULD_NOT_BE_CALLED12 }, '',
+
+);
+
+tstlocalrules (q{
+
+  if (0)
+    body SHOULD_NOT_BE_CALLED01 /./
+  endif
+
+  if (1)
+    body SHOULD_BE_CALLED01 /./
+  endif
+
+  if (0)
+    body SHOULD_NOT_BE_CALLED02 /./
+  else
+    body SHOULD_BE_CALLED02 /./
+  endif
+
+  if (1)
+    body SHOULD_BE_CALLED03 /./
+  else
+    body SHOULD_NOT_BE_CALLED03 /./
+  endif
+
+  if (1)
+    if (1)
+      body SHOULD_BE_CALLED04 /./
+    else
+      body SHOULD_NOT_BE_CALLED04 /./
+    endif
+  else
+    body SHOULD_NOT_BE_CALLED05 /./
+  endif
+
+  if (0)
+    if (0)
+      body SHOULD_NOT_BE_CALLED06 /./
+    else
+      # Bug 7848
+      body SHOULD_NOT_BE_CALLED07 /./
+    endif
+  else
+    body SHOULD_BE_CALLED05 /./
+  endif
+
+  if (0)
+    if (1)
+      body SHOULD_NOT_BE_CALLED08 /./
+    else
+      if (1)
+        # Bug 7848
+        body SHOULD_NOT_BE_CALLED09 /./
+      endif
+    endif
+  else
+    body SHOULD_BE_CALLED06 /./
+  endif
+
+  if (1)
+    if (0)
+      body SHOULD_NOT_BE_CALLED10 /./
+    else
+      if (0)
+        body SHOULD_NOT_BE_CALLED11 /./
+      else
+        if (0)
+          body SHOULD_NOT_BE_CALLED12 /./
+        else
+          body SHOULD_BE_CALLED07 /./
+        endif
+      endif
+    endif
+  endif
+
+});
+
+ok (sarun ("-L -t < data/spam/gtube.eml", \&patterns_run_cb));
+ok_all_patterns();
+
index 6d8a598a22d5565b6d6765b77d03d9e7d5b5acb5..196a3bfb2400a85282fa18154ff4968037ff4d06 100755 (executable)
@@ -7,25 +7,21 @@ use Test::More tests => 4;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ GTUBE }, 'gtube',
-q{ SHOULD_BE_CALLED }, 'should_be_called'
-
+  q{ 1000 GTUBE }, '',
+  q{ 1.0 SHOULD_BE_CALLED }, ''
 );
 
 %anti_patterns = (
-
-q{ SHOULD_NOT_BE_CALLED }, 'should_not_be_called'
-
+  q{ SHOULD_NOT_BE_CALLED }, ''
 );
 
 tstlocalrules ("
-       if (version > 9.99999)
-         body SHOULD_NOT_BE_CALLED /./
-       endif
-       if (version <= 9.99999)
-         body SHOULD_BE_CALLED /./
-       endif
+  if (version > 9.99999)
+    body SHOULD_NOT_BE_CALLED /./
+  endif
+  if (version <= 9.99999)
+    body SHOULD_BE_CALLED /./
+  endif
 ");
 
 ok (sarun ("-L -t < data/spam/gtube.eml", \&patterns_run_cb));
index 2f9530e2c3bc72f2f85e8a0a4e6c8515b7593359..ce67a1684ab1dd88313ab3fa401b176185a7c4f8 100755 (executable)
@@ -1,20 +1,7 @@
 #!/usr/bin/perl -T
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_tests.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-    unshift(@INC, '../lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
+use lib '.'; use lib 't';
+use SATest; sa_t_init("ip_addrs");
 
 use strict;
 use Test::More tests => 105;
@@ -24,8 +11,8 @@ use Mail::SpamAssassin::NetSet;
 
 my $sa = Mail::SpamAssassin->new({
     require_rules => 1,
-    site_rules_filename => "$prefix/t/log/localrules.tmp",
-    rules_filename => "$prefix/rules",
+    site_rules_filename => $siterules,
+    rules_filename => $localrules,
     local_tests_only => 1,
     dont_copy_prefs => 1,
 });
index 3666bdc1d5850e968619a8edd44e478e7117910e..ca73287d37e9f3eff5c8c22f21459d4a5ff6a86e 100755 (executable)
@@ -9,8 +9,8 @@ plan tests => 8;
 
 # ---------------------------------------------------------------------------
 
-my  @locales = qw( de es fr it nl pl pl pt_BR );
-%patterns = ( q{  }, 'anything', );
+my @locales = qw( de es fr it nl pl pl pt_BR );
+%patterns = ( qr/^/, 'anything', );
 
 for $locale (@locales) {
   $ENV{'LANGUAGE'} = $locale;
@@ -18,3 +18,4 @@ for $locale (@locales) {
   sarun ("-L --lint", \&patterns_run_cb);
   ok_all_patterns();
 }
+
index dbc4787a4abe286fb3253d2a20050be7d26ec86b..ba48a64edf3e7581c71e1101a4d605c1be1e2520 100755 (executable)
@@ -1,9 +1,7 @@
 #!/usr/bin/perl -T
 
-use lib '.';
-use lib 't';
-use SATest;
-sa_t_init("lang_pl_tests");
+use lib '.'; use lib 't';
+use SATest; sa_t_init("lang_pl_tests");
 
 use Test::More;
 plan skip_all => "pl tests disabled" unless conf_bool('run_pl_tests');
@@ -12,9 +10,7 @@ plan tests => 1;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ X-Spam-Status: }, 'didnt_hang_at_least',
-
+  q{ X-Spam-Status: }, 'didnt_hang_at_least',
 );
 
 $ENV{'PERL_BADLANG'} = 0; # Sweep problems under the rug
@@ -22,3 +18,4 @@ $ENV{'LANGUAGE'} = 'pl_PL';
 $ENV{'LC_ALL'} = 'pl_PL';
 sarun ("-L -t < data/nice/004", \&patterns_run_cb);
 ok_all_patterns();
+
index 3f0d6d4acdb1a2c58b246709d1d26788b84ea61a..ba3483e31c2c0b20a275f7ead7c328bb6ff8f078 100755 (executable)
@@ -11,8 +11,8 @@ plan tests => 26;
 
 # Use a slightly modified gtube ...
 my $origtest = 'data/spam/gtube.eml';
-my $test = 'log/report_safe.eml';
-my $test2 = 'log/report_safe2.eml';
+my $test = "$workdir/report_safe.eml";
+my $test2 = "$workdir/report_safe2.eml";
 my $original = '';
 if (open(OTEST, $origtest) && open(TEST, ">$test") && open(TEST2, ">$test2")) {
   binmode OTEST;
index add49c3fdad268c361ce9feb7ef776bb3a2203fc..bf6c869d0156541a984f79dba6f9bc8307522be8 100755 (executable)
@@ -6,13 +6,13 @@ use Test::More tests => 2;
 
 # ---------------------------------------------------------------------------
 
-%patterns = ( q{  }, 'anything' );
+%patterns = ( qr/^/, 'anything' );
 
 # override locale for this test!
 $ENV{'LANGUAGE'} = $ENV{'LC_ALL'} = 'C';
 
-sarun ("-L --lint --prefspath=log/prefs", \&patterns_run_cb);
+sarun ("-L --lint --prefspath=$workdir/prefs", \&patterns_run_cb);
 ok_all_patterns();
 
-ok (!-f "log/prefs");
+ok (!-f "$workdir/prefs");
 
diff --git a/upstream/t/local_tests_only.t b/upstream/t/local_tests_only.t
new file mode 100755 (executable)
index 0000000..b44080c
--- /dev/null
@@ -0,0 +1,25 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("local_tests_only");
+
+use Test::More;
+plan tests => 1;
+
+# ---------------------------------------------------------------------------
+
+# Make sure no plugin is sending DNS with -L
+
+%anti_patterns = (
+ 'dns: bgsend' => 'dns',
+);
+
+tstprefs("
+  header DNSBL_TEST_TOP eval:check_rbl('test', 'dnsbltest.spamassassin.org.')
+  tflags DNSBL_TEST_TOP net
+");
+
+# we need -D output for patterns
+sarun ("-D dns -L -t < data/spam/dnsbl.eml 2>&1", \&patterns_run_cb);
+ok_all_patterns();
+
index 816b23c8e6ba948f7b578e6e29847c65d60924f7..8c439d27eea8b240ebd82bd0aa89af04b998aec1 100755 (executable)
@@ -2,21 +2,6 @@
 
 use constant HAVE_DEVEL_CYCLE => eval { require Devel::Cycle; };
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_tests.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
-
 use lib '.'; use lib 't';
 use SATest; sa_t_init("memory_cycles");
 
@@ -30,9 +15,9 @@ use Mail::SpamAssassin;
 # ---------------------------------------------------------------------------
 
 my $spamtest = Mail::SpamAssassin->new({
-    rules_filename => "$prefix/t/log/test_rules_copy",
-    site_rules_filename => "$prefix/t/log/test_default.cf",
-    userprefs_filename  => "$prefix/masses/spamassassin/user_prefs",
+    rules_filename => $localrules,
+    site_rules_filename => $siterules,
+    userprefs_filename  => $userrules,
     local_tests_only    => 1,
     debug             => 0,
     dont_copy_prefs   => 1,
index e7b9e26e2304399dcdbf8dc91ed0dc97a378d98a..84eb82e51731a9871b646d01497ab062dbcfdc86 100755 (executable)
@@ -7,15 +7,13 @@ use Test::More tests => 3;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ GTUBE }, 'gtube',
-q{ META_FOUND }, 'META_FOUND',
-
+  q{ GTUBE }, 'gtube',
+  q{ META_FOUND }, 'META_FOUND',
 );
 
-tstlocalrules ("
-        loadplugin myTestPlugin ../../data/testplugin.pm
-        header META_FOUND      Plugin-Meta-Test =~ /bar/
+tstprefs ("
+  loadplugin myTestPlugin ../../../data/testplugin.pm
+  header META_FOUND Plugin-Meta-Test =~ /bar/
 ");
 
 ok (sarun ("-L -t < data/spam/gtube.eml", \&patterns_run_cb));
index 317032b7d176b9bb1fbadc144caf9d2ddb0d8987..b187eef12dcb6f05719f9cac6947c13183dcc90b 100755 (executable)
@@ -2,34 +2,34 @@
 
 use lib '.'; use lib 't';
 use SATest; sa_t_init("mimeheader");
-use Test::More tests => 6;
-
-$ENV{'LANGUAGE'} = $ENV{'LC_ALL'} = 'C';             # a cheat, but we need the patterns to work
+use Test::More tests => 18;
 
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-  q{ MIMEHEADER_TEST1 }, q{ test1 },
-  q{ MIMEHEADER_TEST2 }, q{ test2 },
-  q{ MATCH_NL_NONRAW }, q{ match_nl_nonraw },
-  q{ MATCH_NL_RAW }, q{ match_nl_raw },
-  q{ MIMEHEADER_FOUND }, q{ unset_found },
-
+  q{ 1.0 MIMEHEADER_TEST1 }, '',
+  q{ 1.0 MIMEHEADER_TEST2 }, '',
+  q{ 1.0 MATCH_NL_NONRAW }, '',
+  q{ 1.0 MATCH_NL_RAW }, '',
+  q{ 1.0 MIMEHEADER_FOUND1 }, '',
+  q{ 1.0 MIMEHEADER_FOUND2 }, '',
+  q{ 1.0 MIMEHEADER_CONCAT1 }, '',
+  q{ 1.0 MIMEHEADER_RANGE1 }, '',
+  q{ 1.0 MIMEHEADER_RANGE2 }, '',
+  q{ 1.0 MIMEHEADER_RANGE3 }, '',
+  q{ 1.0 MIMEHEADER_RANGE4 }, '',
+  q{ 1.0 MIMEHEADER_MULTI1 }, '',
+  q{ 1.0 MIMEHEADER_MULTIMETA1 }, '',
+  q{ 1.0 MIMEHEADER_MULTI2 }, '',
+  q{ 1.0 MIMEHEADER_MULTIMETA2 }, '',
+  q{ 1.0 MIMEHEADER_CAPTURE1 }, '',
+  qr/tag MIMECAP1 is now ready, value: text\/plain\n/, '',
 );
 
 %anti_patterns = (
-
-  q{ MIMEHEADER_NOTFOUND }, q{ unset_notfound },
-
+  'MIMEHEADER_NOTFOUND', '',
 );
 
-tstpre(q{
-
-  loadplugin Mail::SpamAssassin::Plugin::MIMEHeader
-
-});
-
 tstprefs (q{
 
   mimeheader MIMEHEADER_TEST1 content-type =~ /application\/msword/
@@ -38,10 +38,38 @@ tstprefs (q{
   mimeheader MATCH_NL_NONRAW       Content-Type =~ /msword; name/
   mimeheader MATCH_NL_RAW   Content-Type:raw =~ /msword;\n\tname/
 
-  mimeheader MIMEHEADER_NOTFOUND xyzzy =~ /foobar/
-  mimeheader MIMEHEADER_FOUND xyzzy =~ /foobar/ [if-unset: xyzfoobarxyz]
-
-       });
+  mimeheader MIMEHEADER_NOTFOUND1 xyzzy =~ /foobar/
+  mimeheader MIMEHEADER_FOUND1 xyzzy =~ /foobar/ [if-unset: xyzfoobarxyz]
+
+  mimeheader MIMEHEADER_FOUND2 Content-Type !~ /xyzzy/
+
+  # ALL and concat
+  mimeheader MIMEHEADER_CONCAT1 ALL =~ /\nContent-Type: multipart\/mixed;.*?\nContent-Type: multipart\/alternative;.*?\nContent-Type: text\/plain/s
+  tflags MIMEHEADER_CONCAT1 concat
+
+  # range
+  mimeheader MIMEHEADER_RANGE1 Content-Type =~ /^multipart\/mixed;/
+  tflags MIMEHEADER_RANGE1 range=1
+  mimeheader MIMEHEADER_RANGE2 Content-Type =~ /^multipart\/alternative.*?text\/plain; charset="iso-8859-2"$/s
+  tflags MIMEHEADER_RANGE2 range=2-3 concat
+  mimeheader MIMEHEADER_RANGE3 Content-Type =~ /Jurek/
+  tflags MIMEHEADER_RANGE3 range=2- concat
+  mimeheader MIMEHEADER_RANGE4 Content-Type =~ /Jurek/
+  tflags MIMEHEADER_RANGE4 range=-10
+
+  # multiple
+  mimeheader MIMEHEADER_MULTI1 Content-Type =~ /-[82]/ # iso-8859-2, two matches
+  tflags MIMEHEADER_MULTI1 multiple
+  meta MIMEHEADER_MULTIMETA1 MIMEHEADER_MULTI1 == 2
+  mimeheader MIMEHEADER_MULTI2 ALL =~ /^X-/m # Count X- starting headers
+  tflags MIMEHEADER_MULTI2 multiple
+  meta MIMEHEADER_MULTIMETA2 MIMEHEADER_MULTI2 == 4
+
+  # named regex capture
+  mimeheader MIMEHEADER_CAPTURE1 Content-Type =~ /(?<MIMECAP1>text\/\w+)/
+});
 
-sarun ("-L -t < data/nice/004", \&patterns_run_cb);
+# Check debug needed for tag check
+sarun ("-D check -L -t < data/nice/004 2>&1", \&patterns_run_cb);
 ok_all_patterns();
+
index db65bb60b9eed33dc14fa1a115b25f831ef94193..218693b7f7e05ccf9fa28b93c7244eec49816756 100755 (executable)
@@ -1,95 +1,80 @@
 #!/usr/bin/perl -T
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_tests.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-    unshift(@INC, '../lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
+use lib '.';
+use lib 't';
+use SATest;
+sa_t_init("mimeparse");
 
 use strict;
 
 use Test::More tests => 33;
 use Mail::SpamAssassin;
-
-BEGIN {
-  eval { require Digest::SHA; import Digest::SHA qw(sha1_hex); 1 }
-  or do { require Digest::SHA1; import Digest::SHA1 qw(sha1_hex) }
-}
+use Digest::SHA qw(sha1_hex);
 
 my %files = (
-       "$prefix/t/data/nice/mime1" => [
+       "data/nice/mime1" => [
          join("\n", 'multipart/alternative','text/plain',
                     'multipart/mixed,text/plain','application/andrew-inset'),
        ],
 
-       "$prefix/t/data/nice/mime2" => [
+       "data/nice/mime2" => [
          join("\n",'audio/basic'),
        ],
 
-       "$prefix/t/data/nice/mime3" => [
+       "data/nice/mime3" => [
          join("\n", 'multipart/mixed','multipart/mixed,text/plain,audio/x-sun',
                     'multipart/mixed,image/gif,image/gif,application/x-be2,application/atomicmail',
                     'audio/x-sun'),
        ],
 
-       "$prefix/t/data/nice/mime4" => [
+       "data/nice/mime4" => [
          join("\n", 'multipart/mixed','text/plain','image/pgm'),
        ],
 
-       "$prefix/t/data/nice/mime5" => [
+       "data/nice/mime5" => [
          join("\n", 'multipart/mixed','text/plain','image/pbm'),
          'cfbc6b4dbe0d6fe764dd0e0f10023afb0eb0faa9',
          '6c41ae723b78e63e3763473cd737b84fae366f80'
        ],
 
-       "$prefix/t/data/nice/mime6" => [
+       "data/nice/mime6" => [
          join("\n",'application/postscript'),
        ],
 
-       "$prefix/t/data/nice/mime7" => [
+       "data/nice/mime7" => [
          join("\n",'multipart/mixed','audio/basic','audio/basic'),
        ],
 
-       "$prefix/t/data/nice/mime8" => [
+       "data/nice/mime8" => [
          join("\n",'multipart/mixed','application/postscript','binary','message/rfc822,multipart/mixed,text/plain,multipart/parallel,image/gif,audio/basic,application/atomicmail,message/rfc822,audio/x-sun'),
          '07fdde1c24f216b05813f6a1ae0c7c1c0f84c42b',
          '03e5acb518e8aca0b3a7b18f2d94b5efe73495b2'
        ],
 
-       "$prefix/t/data/nice/base64.txt" => [
+       "data/nice/base64.txt" => [
          join("\n",'multipart/mixed','text/plain','text/plain'),
          '0147e619903eb01721d04c4f05ab9c9d497be193',
          'a0f062b1992b25de8607df1b829d29ede5687126'
        ],
 
-       "$prefix/t/data/spam/badmime.txt" => [
+       "data/spam/badmime.txt" => [
          join("\n",'multipart/alternative','text/plain','text/html'),
          'fe56ab5c4b0199cd2811871adc89cf2a9a3d9748',
          '2e7fea381fe9f0b34f947ddb7a38b81ece68605d'
        ],
 
-       "$prefix/t/data/spam/badmime2.txt" => [
+       "data/spam/badmime2.txt" => [
          join("\n",'multipart/alternative','text/plain','text/html'),
          '05c9e1f1f3638a5191542b0c278debe38ac98a83',
          'e6e71e824aec0e204367bfdc9a9e227039f42815'
        ],
 
-       "$prefix/t/data/spam/badmime3.txt" => [
+       "data/spam/badmime3.txt" => [
          join("\n",'multipart/alternative','text/plain'),
          '1c9972d2708b27f4da2e2ef87dd64d53bd11d086'
        ],
 
-       "$prefix/t/data/nice/mime9" => [
+       "data/nice/mime9" => [
          join("\n",'multipart/mixed','text/plain','message/rfc822,message/rfc822,multipart/mixed,multipart/alternative,text/plain,text/html,image/jpeg'),
          '5cdcabdb89c5fbb3a5e0c0473599668927045d9c',
          'f80584aff917e03d54663422918b58e4689cf993',
@@ -100,9 +85,9 @@ my %files = (
 
 # initialize SpamAssassin
 my $sa = Mail::SpamAssassin->new({
-    rules_filename => "$prefix/t/log/test_rules_copy",
-    site_rules_filename => "$prefix/t/log/test_default.cf",
-    userprefs_filename  => "$prefix/masses/spamassassin/user_prefs",
+    rules_filename => $localrules,
+    site_rules_filename => $siterules,
+    userprefs_filename  => $userrules,
     local_tests_only    => 1,
     debug             => 0,
     dont_copy_prefs   => 1,
index 0f6a36355a886c49b98ce4f31725e6673e726f3f..db8bd2b546dec0c72039f148fe6a45c7a8a3f3ad 100755 (executable)
@@ -1,20 +1,5 @@
 #!/usr/bin/perl -w -T
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_tests.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
-
 use strict;
 
 use lib '.'; use lib 't';
diff --git a/upstream/t/mkrules.t b/upstream/t/mkrules.t
new file mode 100755 (executable)
index 0000000..6225379
--- /dev/null
@@ -0,0 +1,475 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("mkrules");
+use Test::More;
+plan tests => 97;
+use File::Copy;
+use File::Path;
+
+# ---------------------------------------------------------------------------
+print " script runs, even with nothing to do\n\n";
+
+$workdir =~ s!\\!/!g if $RUNNING_ON_WINDOWS;
+my $tdir = "$workdir/mkrules_t";
+
+mkpath ([$tdir, "$tdir/rulesrc", "$tdir/rules"]);
+
+write_file("$tdir/MANIFEST", [ ]);
+write_file("$tdir/MANIFEST.SKIP", [ "foo2\n" ]);
+write_file("$tdir/rules/active.list", [ "" ]);
+
+ok (mkrun ("--src $tdir/rulesrc --out $tdir/rules --manifest $tdir/MANIFEST --manifestskip $tdir/MANIFEST.SKIP --active $tdir/rules/active.list", \&patterns_run_cb));
+ok ok_all_patterns();
+save_tdir();
+
+# ---------------------------------------------------------------------------
+print " promotion of an active rule\n\n";
+
+%patterns = (
+  '72_active.cf: WARNING: not listed in manifest file' => manif_found,
+  "body GOOD /foo/"   => rule_line_1,
+  "describe GOOD desc_found"  => rule_line_2,
+);
+%anti_patterns = (
+  "describe T_GOOD desc_found"  => rule_line_2,
+);
+
+mkpath ([ "$tdir/rulesrc/sandbox/foo", "$tdir/rules" ]);
+
+write_file("$tdir/MANIFEST", [ ]);
+write_file("$tdir/MANIFEST.SKIP", [ "foo2\n" ]);
+write_file("$tdir/rules/active.list", [ "GOOD\n" ]);
+write_file("$tdir/rulesrc/sandbox/foo/20_foo.cf", [
+    "body GOOD /foo/\n",
+    "describe GOOD desc_found\n"
+]);
+
+ok (mkrun ("--src $tdir/rulesrc --out $tdir/rules --manifest $tdir/MANIFEST --manifestskip $tdir/MANIFEST.SKIP --active $tdir/rules/active.list 2>&1", \&patterns_run_cb));
+checkfile("$tdir/rules/72_active.cf", \&patterns_run_cb);
+checkfile("$tdir/rules/70_sandbox.cf", \&patterns_run_cb);
+ok ok_all_patterns();
+save_tdir();
+
+# ---------------------------------------------------------------------------
+print " non-promotion of an inactive rule\n\n";
+
+%patterns = (
+  '70_sandbox.cf: WARNING: not listed in manifest file' => manif_found,
+  "body T_GOOD /foo/"   => rule_line_1,
+  "describe T_GOOD desc_found"  => rule_line_2,
+);
+%anti_patterns = (
+  "describe GOOD desc_found"  => rule_line_2,
+);
+
+mkpath ([ "$tdir/rulesrc/sandbox/foo", "$tdir/rules" ]);
+
+write_file("$tdir/MANIFEST", [ ]);
+write_file("$tdir/MANIFEST.SKIP", [ "foo2\n" ]);
+write_file("$tdir/rules/active.list", [ "NOT_GOOD\n" ]);
+write_file("$tdir/rulesrc/sandbox/foo/20_foo.cf", [
+    "body GOOD /foo/\n",
+    "describe GOOD desc_found\n"
+]);
+
+ok (mkrun ("--src $tdir/rulesrc --out $tdir/rules --manifest $tdir/MANIFEST --manifestskip $tdir/MANIFEST.SKIP --active $tdir/rules/active.list 2>&1", \&patterns_run_cb));
+checkfile("$tdir/rules/72_active.cf", \&patterns_run_cb);
+checkfile("$tdir/rules/70_sandbox.cf", \&patterns_run_cb);
+ok ok_all_patterns();
+save_tdir();
+
+# ---------------------------------------------------------------------------
+print " non-promotion of an inactive rule with score set\n\n";
+
+%patterns = (
+  '70_sandbox.cf: WARNING: not listed in manifest file' => manif_found,
+  "body T_GOOD /foo/"   => rule_line_1,
+  "describe T_GOOD desc_found"  => rule_line_2,
+  "#score T_GOOD 4.0"  => score_good,
+);
+%anti_patterns = (
+  "describe GOOD desc_found"  => rule_line_2,
+  "score GOOD 4.0"  => 'score',
+);
+
+mkpath ([ "$tdir/rulesrc/sandbox/foo", "$tdir/rules" ]);
+
+write_file("$tdir/MANIFEST", [ ]);
+write_file("$tdir/MANIFEST.SKIP", [ "foo2\n" ]);
+write_file("$tdir/rules/active.list", [ "NOT_GOOD\n" ]);
+write_file("$tdir/rulesrc/sandbox/foo/20_foo.cf", [
+    "body GOOD /foo/\n",
+    "score GOOD 4.0\n",
+    "describe GOOD desc_found\n"
+]);
+
+ok (mkrun ("--src $tdir/rulesrc --out $tdir/rules --manifest $tdir/MANIFEST --manifestskip $tdir/MANIFEST.SKIP --active $tdir/rules/active.list 2>&1", \&patterns_run_cb));
+checkfile("$tdir/rules/72_active.cf", \&patterns_run_cb);
+checkfile("$tdir/rules/70_sandbox.cf", \&patterns_run_cb);
+ok ok_all_patterns();
+save_tdir();
+
+# ---------------------------------------------------------------------------
+print " non-promotion of a broken rule\n\n";
+
+%patterns = (
+  '70_sandbox.cf: WARNING: not listed in manifest file' => manif_found,
+  'LINT FAILED' => lint_failed,
+);
+%anti_patterns = (
+  "body GOOD"   => rule_line_1,
+  "describe GOOD desc_found"  => rule_line_2,
+);
+
+mkpath ([ "$tdir/rulesrc/sandbox/foo", "$tdir/rules" ]);
+
+write_file("$tdir/MANIFEST", [ ]);
+write_file("$tdir/MANIFEST.SKIP", [ "foo2\n" ]);
+write_file("$tdir/rules/active.list", [ "GOOD\n" ]);
+write_file("$tdir/rulesrc/sandbox/foo/20_foo.cf", [
+    "body GOOD /***\n",
+    "describe GOOD desc_found\n"
+]);
+
+ok (mkrun ("--src $tdir/rulesrc --out $tdir/rules --manifest $tdir/MANIFEST --manifestskip $tdir/MANIFEST.SKIP --active $tdir/rules/active.list 2>&1", \&patterns_run_cb));
+checkfile("$tdir/rules/70_sandbox.cf", \&patterns_run_cb);
+ok (-f "$tdir/rules/72_active.cf");
+ok (-s "$tdir/rules/72_active.cf" == 0);
+ok ok_all_patterns();
+save_tdir();
+
+# ---------------------------------------------------------------------------
+print " promotion of an active meta rule\n\n";
+
+%patterns = (
+  '70_sandbox.cf: WARNING: not listed in manifest file' => manif_found,
+  '20_foo.cf: 1 active rules, 1 other' => 'foundrule',
+  "body __GOOD /foo/"   => rule_line_1,
+  "meta GOOD (__GOOD)"   => rule_line_1a,
+  "describe GOOD desc_found"  => rule_line_2,
+);
+%anti_patterns = (
+  "describe T_GOOD desc_found"  => rule_line_2,
+);
+
+mkpath ([ "$tdir/rulesrc/sandbox/foo", "$tdir/rules" ]);
+
+write_file("$tdir/MANIFEST", [ ]);
+write_file("$tdir/MANIFEST.SKIP", [ "foo2\n" ]);
+write_file("$tdir/rules/active.list", [ "GOOD\n" ]);
+write_file("$tdir/rulesrc/sandbox/foo/20_foo.cf", [
+    "body __GOOD /foo/\n",
+    "meta GOOD (__GOOD)\n",
+    "describe GOOD desc_found\n"
+]);
+
+ok (mkrun ("--src $tdir/rulesrc --out $tdir/rules --manifest $tdir/MANIFEST --manifestskip $tdir/MANIFEST.SKIP --active $tdir/rules/active.list 2>&1", \&patterns_run_cb));
+checkfile("$tdir/rules/72_active.cf", \&patterns_run_cb);
+checkfile("$tdir/rules/70_sandbox.cf", \&patterns_run_cb);
+ok ok_all_patterns();
+save_tdir();
+
+# ---------------------------------------------------------------------------
+print " inactive meta rule\n\n";
+
+%patterns = (
+  '70_sandbox.cf: WARNING: not listed in manifest file' => manif_found,
+  '20_foo.cf: 0 active rules, 2 other' => 'foundrule',
+  "body __GOOD /foo/"   => rule_line_1,
+  "meta T_GOOD (__GOOD)"   => rule_line_1a,
+  "describe T_GOOD desc_found"  => rule_line_2,
+);
+%anti_patterns = (
+  "describe GOOD desc_found"  => rule_line_2,
+);
+
+mkpath ([ "$tdir/rulesrc/sandbox/foo", "$tdir/rules" ]);
+
+write_file("$tdir/MANIFEST", [ ]);
+write_file("$tdir/MANIFEST.SKIP", [ "foo2\n" ]);
+write_file("$tdir/rules/active.list", [ "NOT_GOOD\n" ]);
+write_file("$tdir/rulesrc/sandbox/foo/20_foo.cf", [
+    "body __GOOD /foo/\n",
+    "meta GOOD (__GOOD)\n",
+    "describe GOOD desc_found\n"
+]);
+
+ok (mkrun ("--src $tdir/rulesrc --out $tdir/rules --manifest $tdir/MANIFEST --manifestskip $tdir/MANIFEST.SKIP --active $tdir/rules/active.list 2>&1", \&patterns_run_cb));
+checkfile("$tdir/rules/72_active.cf", \&patterns_run_cb);
+checkfile("$tdir/rules/70_sandbox.cf", \&patterns_run_cb);
+ok ok_all_patterns();
+save_tdir();
+
+# ---------------------------------------------------------------------------
+print " active plugin in sandbox\n\n";
+
+%patterns = (
+  '70_sandbox.cf: WARNING: not listed in manifest file' => manif_found,
+  "loadplugin Good plugin.pm" => loadplugin_found,
+  "body GOOD eval:check_foo()"   => rule_line_1,
+  "describe GOOD desc_found"  => rule_line_2,
+  "ifplugin Good" => if1,
+  "endif" => endif_found,
+);
+%anti_patterns = (
+  "describe T_GOOD desc_found"  => rule_line_2,
+);
+
+mkpath ([ "$tdir/rulesrc/sandbox/foo", "$tdir/rules" ]);
+
+write_file("$tdir/MANIFEST", [ "rulesrc/sandbox/foo/20_foo.cf\n", "rulesrc/sandbox/foo/plugin.pm\n" ]);
+write_file("$tdir/MANIFEST.SKIP", [ "foo2\n" ]);
+write_file("$tdir/rules/active.list", [ "GOOD\n" ]);
+write_file("$tdir/rulesrc/sandbox/foo/20_foo.cf", [
+    "loadplugin Good plugin.pm\n",
+    "ifplugin Good\n",
+    "body GOOD eval:check_foo()\n",
+    "describe GOOD desc_found\n",
+    "endif\n",
+]);
+write_file("$tdir/rulesrc/sandbox/foo/plugin.pm", [
+    'package Good;',
+    'use Mail::SpamAssassin::Plugin; our @ISA = qw(Mail::SpamAssassin::Plugin);',
+    'sub new { my ($class, $m) = @_; $class = ref($class) || $class;',
+    'my $self = bless $class->SUPER::new($m), $class;',
+    '$self->register_eval_rule("check_foo"); return $self; }',
+    'sub check_foo { my ($self, $pms) = @_; return 1; }',
+]);
+
+ok (mkrun ("--src $tdir/rulesrc --out $tdir/rules --manifest $tdir/MANIFEST --manifestskip $tdir/MANIFEST.SKIP --active $tdir/rules/active.list 2>&1", \&patterns_run_cb));
+# checkfile("$tdir/rules/72_active.cf", \&patterns_run_cb);
+checkfile("$tdir/rules/70_sandbox.cf", \&patterns_run_cb);
+ok (-f "$tdir/rules/plugin.pm");
+ok ok_all_patterns();
+save_tdir();
+
+# ---------------------------------------------------------------------------
+print " inactive plugin\n\n";
+
+%patterns = (
+  '70_sandbox.cf: WARNING: not listed in manifest file' => manif_found,
+  # "WARNING: GOOD: renamed as T_GOOD due to missing T_ prefix" => warning_seen,
+  "loadplugin Good plugin.pm" => loadplugin_found,
+  "body T_GOOD eval:check_foo()" => rule_line_1,
+  "describe T_GOOD desc_found" => rule_line_2,
+  "ifplugin Good" => if1,
+  "endif" => endif_found,
+);
+%anti_patterns = (
+  "describe GOOD desc_found"  => rule_line_2,
+);
+
+mkpath ([ "$tdir/rulesrc/sandbox/foo", "$tdir/rules" ]);
+
+write_file("$tdir/MANIFEST", [ "rulesrc/sandbox/foo/20_foo.cf\n", "rulesrc/sandbox/foo/plugin.pm\n" ]);
+write_file("$tdir/MANIFEST.SKIP", [ "foo2\n" ]);
+write_file("$tdir/rules/active.list", [ "NOT_GOOD\n" ]);
+write_file("$tdir/rulesrc/sandbox/foo/20_foo.cf", [
+    "loadplugin Good plugin.pm\n",
+    "ifplugin Good\n",
+    "body GOOD eval:check_foo()\n",
+    "describe GOOD desc_found\n",
+    "endif\n",
+]);
+write_file("$tdir/rulesrc/sandbox/foo/plugin.pm", [
+    'package Good;',
+    'use Mail::SpamAssassin::Plugin; our @ISA = qw(Mail::SpamAssassin::Plugin);',
+    'sub new { my ($class, $m) = @_; $class = ref($class) || $class;',
+    'my $self = bless $class->SUPER::new($m), $class;',
+    '$self->register_eval_rule("check_foo"); return $self; }',
+    'sub check_foo { my ($self, $pms) = @_; return 1; }',
+]);
+
+ok (mkrun ("--src $tdir/rulesrc --out $tdir/rules --manifest $tdir/MANIFEST --manifestskip $tdir/MANIFEST.SKIP --active $tdir/rules/active.list 2>&1", \&patterns_run_cb));
+# checkfile("$tdir/rules/72_active.cf", \&patterns_run_cb);
+checkfile("$tdir/rules/70_sandbox.cf", \&patterns_run_cb);
+ok (-f "$tdir/rules/plugin.pm");
+ok ok_all_patterns();
+save_tdir();
+
+
+# ---------------------------------------------------------------------------
+print " active plugin, but the .pm file is AWOL\n\n";
+
+%patterns = (
+  "body GOOD eval:check_foo()"   => rule_line_1,
+  "describe GOOD desc_found"  => rule_line_2,
+  "ifplugin Good" => if1,
+  "endif" => endif_found,
+  "rulesrc/sandbox/foo/20_foo.cf: WARNING: plugin code file '$workdir/mkrules_t/rulesrc/sandbox/foo/plugin.pm' not found, line ignored: loadplugin Good plugin.pm" => plugin_not_found,
+);
+%anti_patterns = (
+  "describe T_GOOD desc_found"  => rule_line_2,
+);
+
+rmtree([ $tdir ]); mkpath ([ "$tdir/rulesrc/sandbox/foo", "$tdir/rules" ]);
+
+write_file("$tdir/MANIFEST", [ "rulesrc/sandbox/foo/20_foo.cf\n", "rulesrc/sandbox/foo/plugin.pm\n" ]);
+write_file("$tdir/MANIFEST.SKIP", [ "foo2\n" ]);
+write_file("$tdir/rules/active.list", [ "GOOD\n" ]);
+write_file("$tdir/rulesrc/sandbox/foo/20_foo.cf", [
+    "loadplugin Good plugin.pm\n",
+    "ifplugin Good\n",
+    "body GOOD eval:check_foo()\n",
+    "describe GOOD desc_found\n",
+    "endif\n",
+]);
+
+ok (mkrun ("--src $tdir/rulesrc --out $tdir/rules --manifest $tdir/MANIFEST --manifestskip $tdir/MANIFEST.SKIP --active $tdir/rules/active.list 2>&1", \&patterns_run_cb));
+checkfile("$tdir/rules/72_active.cf", \&patterns_run_cb);
+# checkfile("$tdir/rules/70_sandbox.cf", \&patterns_run_cb);
+ok (!-f "$tdir/rules/plugin.pm");
+ok ok_all_patterns();
+save_tdir();
+
+# ---------------------------------------------------------------------------
+print " active plugin, but the .pm file is not in MANIFEST\n\n";
+
+%patterns = (
+  "body GOOD eval:check_foo()"   => rule_line_1,
+  "describe GOOD desc_found"  => rule_line_2,
+  "ifplugin Good" => if1,
+  "endif" => endif_found,
+  "tryplugin Good plugin.pm" => 'tryplugin',
+  "$workdir/mkrules_t/rulesrc/sandbox/foo/20_foo.cf: WARNING: '$workdir/mkrules_t/rules/plugin.pm' not listed in manifest file, making 'tryplugin': loadplugin Good plugin.pm" => not_found_in_manifest_warning
+);
+%anti_patterns = (
+);
+
+rmtree([ $tdir ]); mkpath ([ "$tdir/rulesrc/sandbox/foo", "$tdir/rules" ]);
+
+write_file("$tdir/MANIFEST", [ "rulesrc/sandbox/foo/20_foo.cf\n" ]);
+write_file("$tdir/MANIFEST.SKIP", [ ]);
+write_file("$tdir/rules/active.list", [ "GOOD\n" ]);
+write_file("$tdir/rulesrc/sandbox/foo/20_foo.cf", [
+    "loadplugin Good plugin.pm\n",
+    "ifplugin Good\n",
+    "body GOOD eval:check_foo()\n",
+    "describe GOOD desc_found\n",
+    "endif\n",
+]);
+write_file("$tdir/rulesrc/sandbox/foo/plugin.pm", [
+    'package Good;',
+    'use Mail::SpamAssassin::Plugin; our @ISA = qw(Mail::SpamAssassin::Plugin);',
+    'sub new { my ($class, $m) = @_; $class = ref($class) || $class;',
+    'my $self = bless $class->SUPER::new($m), $class;',
+    '$self->register_eval_rule("check_foo"); return $self; }',
+    'sub check_foo { my ($self, $pms) = @_; return 1; }',
+]);
+
+ok (mkrun ("--src $tdir/rulesrc --out $tdir/rules --manifest $tdir/MANIFEST --manifestskip $tdir/MANIFEST.SKIP --active $tdir/rules/active.list 2>&1", \&patterns_run_cb));
+# checkfile("$tdir/rules/72_active.cf", \&patterns_run_cb);
+checkfile("$tdir/rules/70_sandbox.cf", \&patterns_run_cb);
+ok (-f "$tdir/rules/plugin.pm");
+ok ok_all_patterns();
+save_tdir();
+
+# ---------------------------------------------------------------------------
+print "meta rule depends on unpromoted subrule in lexically-earlier file\n\n";
+# (see mail from Sidney of Oct 16 2006, rules HS_INDEX_PARAM and HS_PHARMA_1)
+
+%patterns = (
+  "header T_GOOD_SUB"   => rule_line_1,
+  "header T_BAD_SUB"   => rule_line_2,
+  "meta GOOD (T_GOOD_SUB && !T_BAD_SUB)" => meta_found
+);
+%anti_patterns = (
+);
+
+rmtree([ $tdir ]); mkpath ([ "$tdir/rulesrc/sandbox/foo", "$tdir/rules" ]);
+
+write_file("$tdir/MANIFEST", [ "rules/72_active.cf\n" ]);
+write_file("$tdir/MANIFEST.SKIP", [ ]);
+write_file("$tdir/rules/active.list", [ "GOOD\n" ]);
+write_file("$tdir/rulesrc/sandbox/foo/20_aaa.cf", [
+    "meta GOOD (GOOD_SUB && !BAD_SUB)\n",
+]);
+write_file("$tdir/rulesrc/sandbox/foo/20_bbb.cf", [
+    "header GOOD_SUB Foo =~ /good/\n",
+    "header BAD_SUB Foo =~ /bad/\n",
+]);
+
+ok (mkrun ("--src $tdir/rulesrc --out $tdir/rules --manifest $tdir/MANIFEST --manifestskip $tdir/MANIFEST.SKIP --active $tdir/rules/active.list 2>&1", \&patterns_run_cb));
+checkfile("$tdir/rules/72_active.cf", \&patterns_run_cb);
+ok ok_all_patterns();
+save_tdir();
+
+# ---------------------------------------------------------------------------
+print " nested conditionals\n\n";
+
+%patterns = (
+  '72_active.cf: WARNING: not listed in manifest file' => manif_found,
+  "body GOOD /foo/"   => rule_line_1,
+  "describe GOOD desc_found"  => rule_line_2,
+  "ifplugin Mail::SpamAssassin::Plugin::DKIM" => 'ifplugin',
+  "if (version >= 3.002000)" => 'ifversion',
+);
+%anti_patterns = (
+  "describe T_GOOD desc_found"  => rule_line_2,
+);
+
+mkpath ([ "$tdir/rulesrc/sandbox/foo", "$tdir/rules" ]);
+
+write_file("$tdir/MANIFEST", [ ]);
+write_file("$tdir/MANIFEST.SKIP", [ "foo2\n" ]);
+write_file("$tdir/rules/active.list", [ "GOOD\n" ]);
+write_file("$tdir/rulesrc/sandbox/foo/20_foo.cf", [
+  "ifplugin Mail::SpamAssassin::Plugin::DKIM\n",
+  "if (version >= 3.002000)\n",
+  "body GOOD /foo/\n",
+  "describe GOOD desc_found\n",
+  "endif\n",
+  "endif\n",
+]);
+
+ok (mkrun ("--src $tdir/rulesrc --out $tdir/rules --manifest $tdir/MANIFEST --manifestskip $tdir/MANIFEST.SKIP --active $tdir/rules/active.list 2>&1", \&patterns_run_cb));
+checkfile("$tdir/rules/72_active.cf", \&patterns_run_cb);
+checkfile("$tdir/rules/70_sandbox.cf", \&patterns_run_cb);
+ok ok_all_patterns();
+save_tdir();
+
+# ---------------------------------------------------------------------------
+
+exit;
+
+sub write_file {
+  my $file = shift;
+  my $linesref = shift;
+  open (O, ">$file") or die "cannot write to $file";
+  print O @$linesref;
+  close O or die "cannot save $file";
+}
+
+
+sub mkrun {
+  my $args = shift;
+  my $read_sub = shift;
+
+  my $post_redir = '';
+  $args =~ s/ 2\>\&1$// and $post_redir = ' 2>&1';
+
+  rmtree ("$workdir/outputdir.tmp"); # some tests use this
+  mkdir ("$workdir/outputdir.tmp", 0755);
+
+  clear_pattern_counters();
+
+  my $scrargs = "$perl_path -I../lib ../build/mkrules $args";
+  print ("\t$scrargs\n");
+  my $test_number = test_number();
+  untaint_system ("$scrargs > $workdir/$testname.$test_number $post_redir");
+  $mk_exitcode = ($?>>8);
+  if ($mk_exitcode != 0) { return undef; }
+  &checkfile ("$workdir/$testname.$test_number", $read_sub) if (defined $read_sub);
+  1;
+}
+
+sub save_tdir {
+  my $test_number = test_number();
+
+  rmtree("$tdir.$test_number");
+  if (move( "$tdir", "$tdir.$test_number")) {
+    print "\ttest output tree copied to $tdir.$test_number\n";
+  }
+}
+
diff --git a/upstream/t/mkrules_else.t b/upstream/t/mkrules_else.t
new file mode 100755 (executable)
index 0000000..4cf4d39
--- /dev/null
@@ -0,0 +1,183 @@
+#!/usr/bin/perl -T
+# bug 6241
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("mkrules_else");
+use Test::More;
+plan tests => 18;
+use File::Copy;
+use File::Path;
+
+# ---------------------------------------------------------------------------
+print "\n rule with 'else'\n\n";
+
+$workdir =~ s!\\!/!g if $RUNNING_ON_WINDOWS;
+my $tdir = "$workdir/mkrules_else_t";
+mkdir($tdir);
+
+%patterns = (
+  # ensure these have the appropriate conditional attached
+  qr/ifplugin Mail::SpamAssassin::Plugin::WhateverNonExistent[^\n]*\ndie_with_a_syntax_error/s => 'die_with_a_syntax_error_found',
+  qr/if !plugin\(Mail::SpamAssassin::Plugin::WhateverNonExistent\)[^\n]*\nbody GOOD \/foo\//s => 'rule_GOOD',
+
+);
+%anti_patterns = (
+  'ERROR'        => 'ERROR_in_stdout',
+  'WARNING'      => 'WARNING_in_stdout',
+);
+
+mkpath ([ "$tdir/rulesrc/sandbox/foo", "$tdir/rules" ]);
+write_file("$tdir/MANIFEST", [ "$tdir/rules/70_sandbox.cf\n", "$tdir/rules/72_active.cf\n" ]);
+write_file("$tdir/rules/active.list", [ "GOOD\n" ]);
+write_file("$tdir/rulesrc/sandbox/foo/20_foo.cf", [
+
+    "ifplugin Mail::SpamAssassin::Plugin::WhateverNonExistent\n",
+        "die_with_a_syntax_error\n",        # shouldn't get here
+    "else\n",
+        "body GOOD /foo/\n",
+        "describe GOOD desc_found\n",
+    "endif\n",
+
+]);
+
+ok (mkrun ("--src $tdir/rulesrc --out $tdir/rules --manifest $tdir/MANIFEST --manifestskip $tdir/MANIFEST.SKIP --active $tdir/rules/active.list 2>&1", \&patterns_run_cb));
+checkfile("$tdir/rules/72_active.cf", \&patterns_run_cb);
+checkfile("$tdir/rules/70_sandbox.cf", \&patterns_run_cb);
+ok ok_all_patterns();
+save_tdir();
+
+# ---------------------------------------------------------------------------
+print "\n rule with 2 nested 'else's\n\n";
+
+rmtree([ $tdir ]);
+
+%patterns = (
+);
+%anti_patterns = (
+  qr/meta\s+T_B1\s+\S+\nmeta\s+T_B1\s+\S+/s => 'two_metas_in_one_ifplugin_scope',
+  'ERROR'        => 'ERROR_in_stdout',
+  'WARNING'      => 'WARNING_in_stdout',
+);
+
+mkpath ([ "$tdir/rulesrc/sandbox/foo", "$tdir/rules" ]);
+write_file("$tdir/rules/active.list", [ "A1\n", "A2\n" ]);
+write_file("$tdir/MANIFEST", [ "$tdir/rules/70_sandbox.cf\n", "$tdir/rules/72_active.cf\n" ]);
+write_file("$tdir/rulesrc/sandbox/foo/20_foo.cf", [
+    "body A1 /foo/\n",
+    "body A2 /foo/\n",
+
+    "ifplugin Mail::SpamAssassin::Plugin::SPF\n",
+      "ifplugin Mail::SpamAssassin::Plugin::DKIM\n",
+        "meta   B1   A1\n",
+      "else\n",
+        "meta   B1   A2\n",
+      "endif\n",
+    "else\n",
+      "ifplugin Mail::SpamAssassin::Plugin::DKIM\n",
+        "meta   B1   !A1\n",
+      "else\n",
+        "meta   B1   !A2\n",
+      "endif\n",
+    "endif\n",
+
+]);
+
+ok (mkrun ("--src $tdir/rulesrc --out $tdir/rules --manifest $tdir/MANIFEST --manifestskip $tdir/MANIFEST.SKIP --active $tdir/rules/active.list 2>&1", \&patterns_run_cb));
+checkfile("$tdir/rules/72_active.cf", \&patterns_run_cb);
+checkfile("$tdir/rules/70_sandbox.cf", \&patterns_run_cb);
+ok ok_all_patterns();
+save_tdir();
+
+# ---------------------------------------------------------------------------
+print "\n rule with 2 nested 'else's, with promoted meta rule from sandbox subrule\n\n";
+
+rmtree([ $tdir ]);
+
+%patterns = (
+);
+%anti_patterns = (
+  qr/meta\s+__B1\s+\S+\nmeta\s+__B1\s+\S+/s => 'two_metas_in_one_ifplugin_scope',
+  'ERROR'        => 'ERROR_in_stdout',
+  'WARNING'      => 'WARNING_in_stdout',
+);
+
+mkpath ([ "$tdir/rulesrc/sandbox/foo", "$tdir/rules" ]);
+write_file("$tdir/rules/active.list", [ "C1\n" ]);
+write_file("$tdir/MANIFEST", [ "$tdir/rules/70_sandbox.cf\n", "$tdir/rules/72_active.cf\n" ]);
+write_file("$tdir/rulesrc/sandbox/foo/20_foo.cf", [
+    "body A1 /foo/\n",
+    "body A2 /foo/\n",
+    "meta C1 __B1\n",
+
+    "ifplugin Mail::SpamAssassin::Plugin::SPF\n",
+      "ifplugin Mail::SpamAssassin::Plugin::DKIM\n",
+        "meta   __B1   A1\n",
+      "else\n",
+        "meta   __B1   A2\n",
+      "endif\n",
+    "else\n",
+      "ifplugin Mail::SpamAssassin::Plugin::DKIM\n",
+        "meta   __B1   !A1\n",
+      "else\n",
+        "meta   __B1   !A2\n",
+      "endif\n",
+    "endif\n",
+
+]);
+
+ok (mkrun ("--src $tdir/rulesrc --out $tdir/rules --manifest $tdir/MANIFEST --manifestskip $tdir/MANIFEST.SKIP --active $tdir/rules/active.list 2>&1", \&patterns_run_cb));
+checkfile("$tdir/rules/70_sandbox.cf", \&patterns_run_cb);
+
+%patterns = (
+  'body T_A1' => 'T_A1_defined',
+  'meta __B1' => '__B1_defined',
+);
+checkfile("$tdir/rules/72_active.cf", \&patterns_run_cb);
+ok ok_all_patterns();
+save_tdir();
+
+# ---------------------------------------------------------------------------
+
+exit;
+
+sub write_file {
+  my $file = shift;
+  my $linesref = shift;
+  open (O, ">$file") or die "cannot write to $file";
+  print O @$linesref;
+  close O or die "cannot save $file";
+}
+
+
+sub mkrun {
+  my $args = shift;
+  my $read_sub = shift;
+
+  my $post_redir = '';
+  $args =~ s/ 2\>\&1$// and $post_redir = ' 2>&1';
+
+  rmtree ("$workdir/outputdir.tmp"); # some tests use this
+  mkdir ("$workdir/outputdir.tmp", 0755);
+
+  clear_pattern_counters();
+
+  my $scrargs = "$perl_path -I../lib ../build/mkrules $args";
+  print ("\t$scrargs\n");
+
+  my $test_number = test_number();
+  untaint_system ("$scrargs > $workdir/$testname.$test_number $post_redir");
+  $mk_exitcode = ($?>>8);
+  if ($mk_exitcode != 0) { return undef; }
+  &checkfile ("$workdir/$testname.$test_number", $read_sub) if (defined $read_sub);
+  1;
+}
+
+sub save_tdir {
+  my $test_number = test_number();
+
+  rmtree("$tdir.$test_number");
+  if (move( "$tdir", "$tdir.$test_number")) {
+    print "\ttest output tree copied to $tdir.$test_number\n";
+  }
+}
+
index 14fbc4567383f067830894a41dbdc6d842b6edde..23be96ad5bc7c16d704d6f6275825dd61b71d239 100755 (executable)
@@ -7,10 +7,9 @@ use Test::More tests => 1;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
   q{ X-Spam-Status: No, }, 'nonspam'
-
 );
 
 sarun ("-L -t < data/nice/001", \&patterns_run_cb);
 ok_all_patterns();
+
index ec55dd3ff0dcd783c1925bb73dc535586b4f8550..9fda61229da93a1836ddcdac67d8b0e76ee5e850 100755 (executable)
@@ -9,17 +9,15 @@ use constant HAS_IO_STRING => eval { require IO::String; };
 use Test::More;
 plan skip_all => 'Need Archive::Zip for this test' unless HAS_ARCHIVE_ZIP;
 plan skip_all => 'Need IO::String for this test' unless HAS_IO_STRING;
-plan tests => 7;
-
-tstpre ("
-loadplugin Mail::SpamAssassin::Plugin::OLEVBMacro
-");
+plan tests => 12;
 
 tstlocalrules (q{
+  loadplugin Mail::SpamAssassin::Plugin::OLEVBMacro
+
   olemacro_extended_scan 1
 
-  body     OLEMACRO eval:check_olemacro()
-  score    OLEMACRO 0.1
+  body     OLEMACRO_FOUND eval:check_olemacro()
+  score    OLEMACRO_FOUND 0.1
   body     OLEMACRO_MALICE eval:check_olemacro_malice()
   score    OLEMACRO_MALICE 0.1
   body     OLEMACRO_RENAME eval:check_olemacro_renamed()
@@ -30,44 +28,49 @@ tstlocalrules (q{
   score    OLEMACRO_ZIP_PW 0.1
   body     OLEMACRO_CSV eval:check_olemacro_csv()
   score    OLEMACRO_CSV 0.1
+  body     OLEMACRO_TURI eval:check_olemacro_redirect_uri()
+  score    OLEMACRO_TURI 0.1
 });
 
 
 %patterns = (
-        q{ OLEMACRO }, 'OLEMACRO',
-            );
+  q{ 0.1 OLEMACRO_FOUND }, '',
+);
 
 sarun ("-L -t < data/spam/olevbmacro/macro.eml", \&patterns_run_cb);
 ok_all_patterns();
 clear_pattern_counters();
 
 %patterns = (
-        q{ OLEMACRO_MALICE }, 'OLEMACRO_MALICE',
-            );
+  q{ 0.1 OLEMACRO_FOUND }, '',
+  q{ 0.1 OLEMACRO_MALICE }, '',
+);
 
 sarun ("-L -t < data/spam/olevbmacro/malicemacro.eml", \&patterns_run_cb);
 ok_all_patterns();
 clear_pattern_counters();
 
 %patterns = (
-        q{ OLEMACRO_RENAME }, 'OLEMACRO_RENAME',
-            );
+  q{ 0.1 OLEMACRO_FOUND }, '',
+  q{ 0.1 OLEMACRO_RENAME }, '',
+);
 
 sarun ("-L -t < data/spam/olevbmacro/renamedmacro.eml", \&patterns_run_cb);
 ok_all_patterns();
 clear_pattern_counters();
 
 %patterns = (
-        q{ OLEMACRO_ENCRYPTED }, 'OLEMACRO_ENCRYPTED',
-            );
+  q{ 0.1 OLEMACRO_ENCRYPTED }, '',
+);
 
 sarun ("-L -t < data/spam/olevbmacro/encrypted.eml", \&patterns_run_cb);
 ok_all_patterns();
 clear_pattern_counters();
 
 %patterns = (
-        q{ OLEMACRO_ZIP_PW }, 'OLEMACRO_ZIP_PW',
-            );
+  q{ 0.1 OLEMACRO_FOUND }, '',
+  q{ 0.1 OLEMACRO_ZIP_PW }, '',
+);
 
 sarun ("-L -t < data/spam/olevbmacro/zippwmacro.eml", \&patterns_run_cb);
 ok_all_patterns();
@@ -75,16 +78,25 @@ clear_pattern_counters();
 
 %patterns = ();
 %anti_patterns = (
-        q{ OLEMACRO }, 'OLEMACRO',
-            );
+  q{ 0.1 OLEMACRO_FOUND }, '',
+);
 
 sarun ("-L -t < data/spam/olevbmacro/nomacro.eml", \&patterns_run_cb);
 ok_all_patterns();
 
 %patterns = ();
 %anti_patterns = (
-        q{ OLEMACRO_CSV }, 'OLEMACRO_CSV',
-            );
+  q{ 0.1 OLEMACRO_FOUND }, '',
+  q{ 0.1 OLEMACRO_CSV }, '',
+);
 
 sarun ("-L -t < data/spam/olevbmacro/goodcsv.eml", \&patterns_run_cb);
 ok_all_patterns();
+
+%patterns = (
+  q{ 0.1 OLEMACRO_TURI }, '',
+);
+%anti_patterns = ();
+
+sarun ("-L -t < data/spam/olevbmacro/target_uri.eml", \&patterns_run_cb);
+ok_all_patterns();
old mode 100644 (file)
new mode 100755 (executable)
index e058967..b379bbf
@@ -7,14 +7,17 @@ use Test::More tests => 9;
 # ---------------------------------------------------------------------------
 
 tstlocalrules (q{
+  clear_originating_ip_headers
+  originating_ip_headers X-Yahoo-Post-IP X-Apparently-From
+  originating_ip_headers X-Originating-IP X-SenderIP
   header TEST_ORIG_IP_H1 X-Spam-Relays-External =~ /\bip=198\.51\.100\.1\b/
   score  TEST_ORIG_IP_H1 0.1
   header TEST_ORIG_IP_H2 X-Spam-Relays-External =~ /\bip=198\.51\.100\.2\b/
   score  TEST_ORIG_IP_H2 0.1
 });
 
-%patterns      = ( q{ TEST_ORIG_IP_H1 }, 'test_orig_ip_h1' );
-%anti_patterns = ( q{ TEST_ORIG_IP_H2 }, 'test_orig_ip_h2' );
+%patterns      = ( q{ 0.1 TEST_ORIG_IP_H1 }, '' );
+%anti_patterns = ( q{ TEST_ORIG_IP_H2 }, '' );
 
 ok(sarun("-L -t < data/nice/orig_ip_hdr.eml", \&patterns_run_cb));
 ok_all_patterns();
@@ -32,8 +35,8 @@ tstlocalrules (q{
   score  TEST_ORIG_IP_H2 0.1
 });
 
-%patterns      = ( q{ TEST_ORIG_IP_H1 }, 'test_orig_ip_h1',
-                   q{ TEST_ORIG_IP_H2 }, 'test_orig_ip_h2' );
+%patterns      = ( q{ 0.1 TEST_ORIG_IP_H1 }, '',
+                   q{ TEST_ORIG_IP_H2 }, '' );
 %anti_patterns = ();
 
 ok(sarun("-L -t < data/nice/orig_ip_hdr.eml", \&patterns_run_cb));
@@ -50,8 +53,8 @@ tstlocalrules (q{
 });
 
 %patterns = ();
-%anti_patterns = ( q{ TEST_ORIG_IP_H1 }, 'test_orig_ip_h1',
-                   q{ TEST_ORIG_IP_H2 }, 'test_orig_ip_h2' );
+%anti_patterns = ( q{ 0.1 TEST_ORIG_IP_H1 }, '',
+                   q{ TEST_ORIG_IP_H2 }, '' );
 
 ok(sarun("-L -t < data/nice/orig_ip_hdr.eml", \&patterns_run_cb));
 ok_all_patterns();
diff --git a/upstream/t/pdfinfo.t b/upstream/t/pdfinfo.t
new file mode 100755 (executable)
index 0000000..466eb2b
--- /dev/null
@@ -0,0 +1,59 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("pdfinfo");
+
+use Test::More;
+
+plan tests => 17;
+
+%patterns = (
+ q{ 1.0 PDFINFO_NAMED_REANY }, '',
+ q{ 1.0 PDFINFO_DETAILS_CREATED }, '',
+ q{ 1.0 PDFINFO_DETAILS_PRODUCER }, '',
+ q{ 1.0 PDFINFO_DETAILS_CREATOR }, '',
+ q{ 1.0 PDFINFO_COUNT_1 }, '',
+ q{ 1.0 PDFINFO_EMPTY_BODY_0 }, '',
+ q{ 1.0 PDFINFO_EMPTY_BODY_1000 }, '',
+);
+%anti_patterns = (
+ q{ PDFINFO_DETAILS_AUTHOR }, '',
+ q{ PDFINFO_DETAILS_TITLE }, '',
+ q{ PDFINFO_COUNT_2_3 }, '',
+ q{ PDFINFO_IMAGE_COUNT }, '',
+ q{ PDFINFO_NAMED_FOO }, '',
+ q{ PDFINFO_DETAILS_MODIFIED }, '',
+ q{ PDFINFO_ENCRYPTED }, '',
+ q{ PDFINFO_DETAILS_MODIFIED }, '',
+ q{ PDFINFO_ENCRYPTED }, '',
+ q{ PDFINFO_MD5 }, '',
+ q{ PDFINFO_FUZZY_MD5 }, '',
+ q{ PDFINFO_PC }, ''
+);
+
+tstprefs("
+body PDFINFO_COUNT_1 eval:pdf_count(1)
+body PDFINFO_COUNT_2_3 eval:pdf_count(2,3)
+body PDFINFO_IMAGE_COUNT_1 eval:pdf_image_count(1)
+body PDFINFO_IMAGE_COUNT_2_3 eval:pdf_image_count(2,3)
+body PDFINFO_PC_1000 eval:pdf_pixel_coverage(1000)
+body PDFINFO_PC_10000_100000 eval:pdf_pixel_coverage(10000,100000)
+body PDFINFO_NAMED_FOO eval:pdf_named('foo.pdf')
+body PDFINFO_NAMED_REANY eval:pdf_name_regex('/.+/')
+body PDFINFO_MD5 eval:pdf_match_md5('XXYYZZ')
+body PDFINFO_FUZZY_MD5 eval:pdf_match_md5('XXYYZZ')
+body PDFINFO_DETAILS_AUTHOR eval:pdf_match_details('author', '/.+/')
+body PDFINFO_DETAILS_CREATOR eval:pdf_match_details('creator', '/^Writer\$/')
+body PDFINFO_DETAILS_CREATED eval:pdf_match_details('created', '/.+/')
+body PDFINFO_DETAILS_MODIFIED eval:pdf_match_details('modified', '/.+/')
+body PDFINFO_DETAILS_PRODUCER eval:pdf_match_details('producer', '/.+/')
+body PDFINFO_DETAILS_TITLE eval:pdf_match_details('title', '/.+/')
+body PDFINFO_ENCRYPTED eval:pdf_is_encrypted()
+body PDFINFO_EMPTY_BODY_0 eval:pdf_is_empty_body()
+body PDFINFO_EMPTY_BODY_1000 eval:pdf_is_empty_body(1000)
+");
+
+sarun ("-L -t < data/spam/extracttext/gtube_pdf.eml", \&patterns_run_cb);
+ok_all_patterns();
+clear_pattern_counters();
+
diff --git a/upstream/t/perlcritic.pl b/upstream/t/perlcritic.pl
new file mode 100755 (executable)
index 0000000..11addf7
--- /dev/null
@@ -0,0 +1,34 @@
+#!/usr/bin/perl
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init('perlcritic');
+
+use strict;
+use warnings;
+use Test::More;
+use English qw(-no_match_vars);
+
+plan skip_all => "This test requires Test::Perl::Critic" unless (eval { require Test::Perl::Critic; 1} );
+plan skip_all => "PerlCritic test cannot run in Taint mode" if (${^TAINT});
+
+open RC, ">../t/log/perlcritic.rc"  or die "cannot create t/log/perlcritic.rc";
+
+# we should remove some of these excludes if/when we feel like fixing 'em!
+print RC q{
+
+  severity = 5
+  verbose = 9
+  exclude = ValuesAndExpressions::ProhibitLeadingZeros InputOutput::ProhibitBarewordDirHandles InputOutput::ProhibitBarewordFileHandles InputOutput::ProhibitTwoArgOpen BuiltinFunctions::ProhibitStringyEval InputOutput::ProhibitInteractiveTest Bangs::ProhibitBitwiseOperators Bangs::ProhibitDebuggingModules Compatibility::ProhibitThreeArgumentOpen Lax::ProhibitStringyEval::ExceptForRequire Lax::ProhibitLeadingZeros::ExceptChmod ValuesAndExpressions::PreventSQLInjection ControlStructures::ProhibitReturnInDoBlock ValuesAndExpressions::ProhibitAccessOfPrivateData Policy::OTRS::
+
+  [TestingAndDebugging::ProhibitNoStrict]
+  allow = refs
+
+  [Perlsecret]
+  allow_secrets = Venus
+
+}  or die "cannot write t/log/perlcritic.rc";
+close RC  or die "cannot close t/log/perlcritic.rc";
+
+Test::Perl::Critic->import( -profile => "../t/log/perlcritic.rc" );
+all_critic_ok("../blib");
+
diff --git a/upstream/t/perlcritic.t b/upstream/t/perlcritic.t
new file mode 100644 (file)
index 0000000..13060b3
--- /dev/null
@@ -0,0 +1,14 @@
+#!/usr/bin/perl -T
+# Wrapper around test until perlcritic fixes bug running under -T
+
+# sa_t_init handles a number of necessary cross-platform initialization that is necessary
+# even though this wrapper doesn't need most things that are also in there
+use lib '.'; use lib 't';
+use SATest; sa_t_init('perlcritic');
+
+use strict;
+use warnings;
+
+-d "t" && "$^X t/perlcritic.pl" =~ /(.*)/ ||
+    "$^X perlcritic.pl" =~ /(.*)/;
+exec($1);
old mode 100644 (file)
new mode 100755 (executable)
index a05067a..e2c2d16
@@ -6,26 +6,25 @@ use SATest; sa_t_init("phishing");
 use Test::More;
 plan tests => 2;
 
-tstpre ("
-loadplugin Mail::SpamAssassin::Plugin::Phishing
-");
-
 tstprefs("
 
-phishing_openphish_feed data/phishing/openphish-feed.txt
-phishing_phishtank_feed data/phishing/phishtank-feed.csv
-body     URI_PHISHING   eval:check_phishing()
-describe URI_PHISHING   Url match phishing in feed
+  loadplugin Mail::SpamAssassin::Plugin::Phishing
+
+  phishing_openphish_feed data/phishing/openphish-feed.txt
+  phishing_phishtank_feed data/phishing/phishtank-feed.csv
+
+  body     URI_PHISHING   eval:check_phishing()
+  describe URI_PHISHING   Url match phishing in feed
 
 ");
 
 %patterns_openphish = (
-        q{ URI_PHISHING } => 'OpenPhish',
-            );
+  q{ URI_PHISHING } => 'OpenPhish',
+);
 
 %patterns_phishtank = (
-        q{ URI_PHISHING } => 'PhishTank',
-            );
+  q{ URI_PHISHING } => 'PhishTank',
+);
 
 %patterns = %patterns_openphish;
 sarun ("-L -t < data/spam/phishing_openphish.eml", \&patterns_run_cb);
@@ -35,3 +34,4 @@ clear_pattern_counters();
 %patterns = %patterns_phishtank;
 sarun ("-L -t < data/spam/phishing_phishtank.eml", \&patterns_run_cb);
 ok_all_patterns();
+
index bf0ed4a0cb346bf57f01989736611685c3e83ba8..38d705ef1c616874b3e190f48e0ea0855b45d2b4 100755 (executable)
@@ -7,28 +7,24 @@ use Test::More tests => 6;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ GTUBE }, 'gtube',
-q{ MY_TEST_PLUGIN }, 'plugin_called',
-q{ registered Mail::SpamAssassin::Plugin::Test }, 'registered',
-q{ Mail::SpamAssassin::Plugin::Test eval test called }, 'test_called',
-
+  q{ 1000 GTUBE }, 'gtube',
+  q{ 1.0 MY_TEST_PLUGIN }, 'plugin_called',
+  'registered Mail::SpamAssassin::Plugin::Test', 'registered',
+  'Mail::SpamAssassin::Plugin::Test eval test called', 'test_called',
 );
 
 %anti_patterns = (
-
-q{ SHOULD_NOT_BE_CALLED }, 'should_not_be_called'
-
+  'SHOULD_NOT_BE_CALLED', '',
 );
 
 tstlocalrules ("
-       loadplugin     Mail::SpamAssassin::Plugin::Test
-       ifplugin FooPlugin
-         header SHOULD_NOT_BE_CALLED   eval:doesnt_exist()
-       endif
-       if plugin(Mail::SpamAssassin::Plugin::Test)
-         header MY_TEST_PLUGIN         eval:check_test_plugin()
-       endif
+  loadplugin Mail::SpamAssassin::Plugin::Test
+  ifplugin FooPlugin
+    header SHOULD_NOT_BE_CALLED eval:doesnt_exist()
+  endif
+  if plugin(Mail::SpamAssassin::Plugin::Test)
+    header MY_TEST_PLUGIN eval:check_test_plugin()
+  endif
 ");
 
 ok (sarun ("-L -t < data/spam/gtube.eml", \&patterns_run_cb));
index 2843968df8075274556e36730afe7b71c30b5bed..40c0d39956cef4ba2537b05a27a5d5350a4e40c9 100755 (executable)
@@ -7,32 +7,27 @@ use Test::More tests => 9;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ GTUBE }, 'gtube',
-q{ MY_TEST_PLUGIN }, 'plugin_called',
-q{ registered myTestPlugin }, 'registered',
-q{ myTestPlugin eval test called }, 'test_called',
-q{ myTestPlugin finishing }, 'plugin_finished',
-
-q{ test: plugins loaded: Mail::SpamAssassin::Plugin::ASN=HASH }, 'plugins_loaded',
-q{ myTestPlugin=HASH }, 'plugins_loaded2',
-
+  q{ 1000 GTUBE },             'gtube',
+  q{ 1.0 MY_TEST_PLUGIN },     'plugin_called',
+  'registered myTestPlugin',   'registered',
+  'myTestPlugin eval test called', 'test_called',
+  'myTestPlugin finishing',    'plugin_finished',
+  'test: plugins loaded: Mail::SpamAssassin::Plugin::ASN=HASH', 'plugins_loaded',
+  'myTestPlugin=HASH',         'plugins_loaded2',
 );
 
 %anti_patterns = (
-
-q{ SHOULD_NOT_BE_CALLED }, 'should_not_be_called'
-
+  'SHOULD_NOT_BE_CALLED', 'should_not_be_called'
 );
 
 tstlocalrules ("
-       loadplugin myTestPlugin ../../data/testplugin.pm
-       ifplugin FooPlugin
-         header SHOULD_NOT_BE_CALLED   eval:doesnt_exist()
-       endif
-       if plugin(myTestPlugin)
-         header MY_TEST_PLUGIN         eval:check_test_plugin()
-       endif
+  loadplugin myTestPlugin ../../../data/testplugin.pm
+  ifplugin FooPlugin
+    header SHOULD_NOT_BE_CALLED eval:doesnt_exist()
+  endif
+  if plugin(myTestPlugin)
+    header MY_TEST_PLUGIN  eval:check_test_plugin()
+  endif
 ");
 
 ok (sarun ("-L -t < data/spam/gtube.eml", \&patterns_run_cb));
index eafe00497868f21b5ea1db2a465bd92aa010abb8..51119289cf51ad22956b840f600b58b9aaa25168 100755 (executable)
@@ -7,18 +7,15 @@ use Test::More tests => 2;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ META2_FOUND } => '',
-
+  q{ META2_FOUND } => '',
 );
 
 %anti_patterns = ();
 
 tstlocalrules ("
-        loadplugin myTestPlugin ../../data/testplugin.pm
-        loadplugin myTestPlugin2 ../../data/testplugin2.pm
-        header META2_FOUND       Plugin-Meta-Test2 =~ /bar2/
-
+  loadplugin myTestPlugin ../../../data/testplugin.pm
+  loadplugin myTestPlugin2 ../../../data/testplugin2.pm
+  header META2_FOUND Plugin-Meta-Test2 =~ /bar2/
 ");
 
 ok (sarun ("-L -t < data/spam/gtube.eml", \&patterns_run_cb));
diff --git a/upstream/t/podchecker.t b/upstream/t/podchecker.t
new file mode 100755 (executable)
index 0000000..04f480b
--- /dev/null
@@ -0,0 +1,13 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init('podchecker');
+
+use Test::More;
+
+eval "use Test::Pod 1.00";
+
+plan skip_all => "This test requires Test::Pod"  if $@;
+
+all_pod_files_ok("../blib");
+
index 33bbf20550f7970c6453a83245118515379cc390..993d99a83ead7b99d0eef8247862ef5d7a0e8309 100755 (executable)
@@ -2,29 +2,27 @@
 
 use lib '.'; use lib 't';
 use SATest; sa_t_init("prefs_include");
-use Test::More tests => 2;
+use Test::More tests => 3;
 
 $ENV{'LANGUAGE'} = $ENV{'LC_ALL'} = 'C';             # a cheat, but we need the patterns to work
 
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-    q{X-Spam-Report: =?ISO-8859-1?Q? }, 'qp-encoded-hdr',
-    q{ Invalid Date: header =ae =af =b0 foo }, 'qp-encoded-desc',
-
+  qr/^X-Spam-Report:\s*$/m, 'qp-encoded-hdr',
+  qr/^\t\*\s+[0-9.-]+ INVALID_DATE\s+Invalid Date: header =\?UTF-8\?B\?wq4gwq8gwrA=\?=$/m, 'qp-encoded-desc',
+  qr/^ [0-9.-]+ INVALID_DATE\s+Invalid Date: header ® ¯ °$/m, 'report-desc',
 );
 
 tstprefs ("
-        $default_cf_lines
-        include prefs_include.inc
-        ");
+  include prefs_include.inc
+");
 
-open (OUT, ">log/prefs_include.inc") or die "open log/prefs_include.inc failed";
+open (OUT, ">$localrules/prefs_include.inc") or die "open $workdir/prefs_include.inc failed";
 print OUT "
-        report_safe 0
-       describe INVALID_DATE   Invalid Date: header \xae \xaf \xb0 foo
-       ";
+  report_safe 0
+  describe INVALID_DATE Invalid Date: header ® ¯ °
+";
 close OUT;
 
 sarun ("-L -t < data/spam/001", \&patterns_run_cb);
old mode 100644 (file)
new mode 100755 (executable)
index 1cb83f9..93fd5ef
@@ -1,20 +1,5 @@
 #!/usr/bin/perl -T
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/priorities.t", kluge around ...
-    chdir 't';
-  }
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-    unshift(@INC, '../lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
-
 use lib '.'; use lib 't';
 use SATest; sa_t_init("priorities");
 use strict;
@@ -22,8 +7,81 @@ use Test::More tests => 10;
 
 use Mail::SpamAssassin;
 
+disable_compat "welcomelist_blocklist";
+
 tstlocalrules (q{
 
+  body BAYES_99                eval:check_bayes('0.99', '1.00')
+  tflags BAYES_99              learn
+  score BAYES_99 0 0 3.5 3.5
+
+  header USER_IN_BLOCKLIST             eval:check_from_in_blocklist()
+  describe USER_IN_BLOCKLIST           From: user is listed in the block-list
+  tflags USER_IN_BLOCKLIST             userconf nice noautolearn
+  score USER_IN_BLOCKLIST              100
+
+  if !can(Mail::SpamAssassin::Conf::compat_welcomelist_blocklist)
+    meta USER_IN_BLACKLIST             (USER_IN_BLOCKLIST)
+    describe USER_IN_BLACKLIST         DEPRECATED: See USER_IN_BLOCKLIST
+    tflags USER_IN_BLACKLIST           userconf nice noautolearn
+    score USER_IN_BLACKLIST            100
+    score USER_IN_BLOCKLIST            0.01
+  endif
+
+  header USER_IN_WELCOMELIST           eval:check_from_in_welcomelist()
+  describe USER_IN_WELCOMELIST         User is listed in 'welcomelist_from'
+  tflags USER_IN_WELCOMELIST           userconf nice noautolearn
+  score USER_IN_WELCOMELIST            -100
+    
+  if !can(Mail::SpamAssassin::Conf::compat_welcomelist_blocklist)
+    meta USER_IN_WHITELIST             (USER_IN_WELCOMELIST)
+    describe USER_IN_WHITELIST         DEPRECATED: See USER_IN_WELCOMELIST
+    tflags USER_IN_WHITELIST           userconf nice noautolearn
+    score USER_IN_WHITELIST            -100
+    score USER_IN_WELCOMELIST          -0.01
+  endif
+
+  header USER_IN_DEF_WELCOMELIST       eval:check_from_in_default_welcomelist()
+  describe USER_IN_DEF_WELCOMELIST     From: user is listed in the default welcome-list
+  tflags USER_IN_DEF_WELCOMELIST       userconf nice noautolearn
+  score USER_IN_DEF_WELCOMELIST                -15
+  
+  if !can(Mail::SpamAssassin::Conf::compat_welcomelist_blocklist)
+    meta USER_IN_DEF_WHITELIST         (USER_IN_DEF_WELCOMELIST)
+    describe USER_IN_DEF_WHITELIST     DEPRECATED: See USER_IN_WELCOMELIST 
+    tflags USER_IN_DEF_WHITELIST       userconf nice noautolearn
+    score USER_IN_DEF_WHITELIST                -15
+    score USER_IN_DEF_WELCOMELIST      -0.01
+  endif
+
+  header USER_IN_BLOCKLIST_TO          eval:check_to_in_blocklist()
+  describe USER_IN_BLOCKLIST_TO        User is listed in 'blocklist_to'
+  tflags USER_IN_BLOCKLIST_TO          userconf nice noautolearn
+  score USER_IN_BLOCKLIST_TO           10
+
+  if !can(Mail::SpamAssassin::Conf::compat_welcomelist_blocklist)
+    meta USER_IN_BLACKLIST_TO          (USER_IN_BLOCKLIST_TO)
+    describe USER_IN_BLACKLIST_TO      DEPRECATED: See USER_IN_BLOCKLIST_TO
+    tflags USER_IN_BLACKLIST_TO                userconf nice noautolearn
+    score USER_IN_BLACKLIST_TO         10
+    score USER_IN_BLOCKLIST_TO         0.01
+  endif
+  header USER_IN_WELCOMELIST_TO                eval:check_to_in_welcomelist()
+  describe USER_IN_WELCOMELIST_TO      User is listed in 'welcomelist_to'
+  tflags USER_IN_WELCOMELIST_TO                userconf nice noautolearn
+  score USER_IN_WELCOMELIST_TO         -6
+
+  if !can(Mail::SpamAssassin::Conf::compat_welcomelist_blocklist)
+    meta USER_IN_WHITELIST_TO          (USER_IN_WELCOMELIST_TO)
+    describe USER_IN_WHITELIST_TO      DEPRECATED: See USER_IN_WELCOMELIST_TO
+    tflags USER_IN_WHITELIST_TO                userconf nice noautolearn
+    score USER_IN_WHITELIST_TO         -6
+    score USER_IN_WELCOMELIST_TO       -0.01
+  endif
+
+  header USER_IN_ALL_SPAM_TO      eval:check_to_in_all_spam()
+  tflags USER_IN_ALL_SPAM_TO      userconf nice noautolearn
+
   priority USER_IN_WHITELIST     -1000
   priority USER_IN_DEF_WHITELIST -1000
   priority USER_IN_ALL_SPAM_TO   -1000
@@ -98,8 +156,8 @@ ok assert_rule_pri 'FOO1', -28;
 sub assert_rule_pri {
   my ($r, $pri) = @_;
 
-  if (defined $conf->{rbl_evals}->{$r}) {
-    # ignore rbl_evals; they do not use the priority system at all
+  if (defined $conf->{rbl_evals}->{$r} || defined $conf->{meta_tests}->{$r}) {
+    # ignore rbl_evals and metas; they do not use the priority system at all
     return 1;
   }
 
diff --git a/upstream/t/priorities_welcome_block.t b/upstream/t/priorities_welcome_block.t
new file mode 100755 (executable)
index 0000000..6ffbb84
--- /dev/null
@@ -0,0 +1,175 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("priorities_welcome_block");
+use strict;
+use Test::More tests => 10;
+
+use Mail::SpamAssassin;
+
+tstlocalrules (q{
+
+  body BAYES_99                eval:check_bayes('0.99', '1.00')
+  tflags BAYES_99              learn
+  score BAYES_99 0 0 3.5 3.5
+
+  if !can(Mail::SpamAssassin::Conf::compat_welcomelist_blocklist)
+    meta USER_IN_BLACKLIST             (USER_IN_BLOCKLIST)
+    describe USER_IN_BLACKLIST         DEPRECATED: See USER_IN_BLOCKLIST
+    tflags USER_IN_BLACKLIST           userconf nice noautolearn
+    score USER_IN_BLACKLIST            100
+    score USER_IN_BLOCKLIST            0.01
+  endif
+
+  header USER_IN_WELCOMELIST           eval:check_from_in_welcomelist()
+  describe USER_IN_WELCOMELIST         User is listed in 'welcomelist_from'
+  tflags USER_IN_WELCOMELIST           userconf nice noautolearn
+  score USER_IN_WELCOMELIST            -100
+    
+  if !can(Mail::SpamAssassin::Conf::compat_welcomelist_blocklist)
+    meta USER_IN_WHITELIST             (USER_IN_WELCOMELIST)
+    describe USER_IN_WHITELIST         DEPRECATED: See USER_IN_WELCOMELIST
+    tflags USER_IN_WHITELIST           userconf nice noautolearn
+    score USER_IN_WHITELIST            -100
+    score USER_IN_WELCOMELIST          -0.01
+  endif
+
+  header USER_IN_DEF_WELCOMELIST       eval:check_from_in_default_welcomelist()
+  describe USER_IN_DEF_WELCOMELIST     From: user is listed in the default welcome-list
+  tflags USER_IN_DEF_WELCOMELIST       userconf nice noautolearn
+  score USER_IN_DEF_WELCOMELIST                -15
+  
+  if !can(Mail::SpamAssassin::Conf::compat_welcomelist_blocklist)
+    meta USER_IN_DEF_WHITELIST         (USER_IN_DEF_WELCOMELIST)
+    describe USER_IN_DEF_WHITELIST     DEPRECATED: See USER_IN_WELCOMELIST 
+    tflags USER_IN_DEF_WHITELIST       userconf nice noautolearn
+    score USER_IN_DEF_WHITELIST                -15
+    score USER_IN_DEF_WELCOMELIST      -0.01
+  endif
+
+  header USER_IN_BLOCKLIST_TO          eval:check_to_in_blocklist()
+  describe USER_IN_BLOCKLIST_TO        User is listed in 'blocklist_to'
+  tflags USER_IN_BLOCKLIST_TO          userconf nice noautolearn
+  score USER_IN_BLOCKLIST_TO           10
+
+  if !can(Mail::SpamAssassin::Conf::compat_welcomelist_blocklist)
+    meta USER_IN_BLACKLIST_TO          (USER_IN_BLOCKLIST_TO)
+    describe USER_IN_BLACKLIST_TO      DEPRECATED: See USER_IN_BLOCKLIST_TO
+    tflags USER_IN_BLACKLIST_TO                userconf nice noautolearn
+    score USER_IN_BLACKLIST_TO         10
+    score USER_IN_BLOCKLIST_TO         0.01
+  endif
+  header USER_IN_WELCOMELIST_TO                eval:check_to_in_welcomelist()
+  describe USER_IN_WELCOMELIST_TO      User is listed in 'welcomelist_to'
+  tflags USER_IN_WELCOMELIST_TO                userconf nice noautolearn
+  score USER_IN_WELCOMELIST_TO         -6
+
+  if !can(Mail::SpamAssassin::Conf::compat_welcomelist_blocklist)
+    meta USER_IN_WHITELIST_TO          (USER_IN_WELCOMELIST_TO)
+    describe USER_IN_WHITELIST_TO      DEPRECATED: See USER_IN_WELCOMELIST_TO
+    tflags USER_IN_WHITELIST_TO                userconf nice noautolearn
+    score USER_IN_WHITELIST_TO         -6
+    score USER_IN_WELCOMELIST_TO       -0.01
+  endif
+
+  header USER_IN_ALL_SPAM_TO      eval:check_to_in_all_spam()
+  tflags USER_IN_ALL_SPAM_TO      userconf nice noautolearn
+
+  priority USER_IN_WELCOMELIST     -1000
+  priority USER_IN_DEF_WELCOMELIST -1000
+  priority USER_IN_ALL_SPAM_TO   -1000
+  priority SUBJECT_IN_WELCOMELIST  -1000
+
+  priority ALL_TRUSTED            -950
+
+  priority SUBJECT_IN_BLOCKLIST   -900
+  priority USER_IN_BLOCKLIST_TO   -900
+  priority USER_IN_BLOCKLIST      -900
+
+  priority BAYES_99               -400
+
+  header XX_RCVD_IN_SORBS_SMTP     eval:check_rbl_sub('sorbs', '127.0.0.5')
+  tflags XX_RCVD_IN_SORBS_SMTP     net
+  score  XX_RCVD_IN_SORBS_SMTP     1
+
+  meta SC_URIBL_SURBL  (URIBL_BLACK && (URIBL_SC_SURBL || URIBL_JP_SURBL || URIBL_OB_SURBL ) && RCVD_IN_SORBS_SMTP)
+  meta SC_URIBL_HASH   ((URIBL_BLACK || URIBL_SC_SURBL || URIBL_JP_SURBL || URIBL_OB_SURBL) && (RAZOR2_CHECK || DCC_CHECK || PYZOR_CHECK))
+  meta SC_URIBL_SBL    ((URIBL_BLACK || URIBL_SC_SURBL || URIBL_JP_SURBL || URIBL_OB_SURBL) && URIBL_SBL)
+  meta SC_URIBL_BAYES  ((URIBL_BLACK || URIBL_SC_SURBL || URIBL_JP_SURBL || URIBL_OB_SURBL) && BAYES_99)
+
+  shortcircuit SC_URIBL_SURBL        spam
+  shortcircuit SC_URIBL_HASH         spam
+  shortcircuit SC_URIBL_SBL          spam
+  shortcircuit SC_URIBL_BAYES        spam
+
+  priority SC_URIBL_SURBL            -530
+  priority SC_URIBL_HASH             -510
+  priority SC_URIBL_SBL              -510
+  priority SC_URIBL_BAYES            -510
+
+  shortcircuit DIGEST_MULTIPLE       spam
+  priority DIGEST_MULTIPLE           -300
+
+  meta FOO1 (FOO2 && FOO3)
+  meta FOO2 (1)
+  meta FOO3 (FOO4 && FOO5)
+  meta FOO4 (2)
+  meta FOO5 (3)
+  priority FOO5 -23
+  priority FOO1 -28
+
+});
+
+my $sa = create_saobj({
+  dont_copy_prefs => 1,
+  # debug => 1
+});
+
+$sa->init(0); # parse rules
+ok($sa);
+my $conf = $sa->{conf};
+sub assert_rule_pri;
+
+ok assert_rule_pri 'USER_IN_WELCOMELIST', -1000;
+
+ok assert_rule_pri 'SC_URIBL_SURBL', -530;
+ok assert_rule_pri 'SC_URIBL_HASH', -510;
+ok assert_rule_pri 'SC_URIBL_SBL', -510;
+ok assert_rule_pri 'SC_URIBL_BAYES', -510;
+ok assert_rule_pri 'XX_RCVD_IN_SORBS_SMTP', -530;
+
+# SC_URIBL_BAYES will have overridden its base priority setting
+ok assert_rule_pri 'BAYES_99', -510;
+
+ok assert_rule_pri 'FOO5', -28;
+ok assert_rule_pri 'FOO1', -28;
+
+# ---------------------------------------------------------------------------
+
+sub assert_rule_pri {
+  my ($r, $pri) = @_;
+
+  if (defined $conf->{rbl_evals}->{$r} || defined $conf->{meta_tests}->{$r}) {
+    # ignore rbl_evals and metas; they do not use the priority system at all
+    return 1;
+  }
+
+  foreach my $ruletype (qw(
+    body_tests head_tests meta_tests uri_tests rawbody_tests full_tests
+    full_evals rawbody_evals head_evals body_evals
+  ))
+  {
+    if (defined $conf->{$ruletype}->{$pri}->{$r}) {
+      return 1;
+    }
+    foreach my $foundpri (keys %{$conf->{priorities}}) {
+      next unless (defined $conf->{$ruletype}->{$foundpri}->{$r});
+      warn "FAIL: rule '$r' not found at priority $pri; found at $foundpri\n";
+      return 0;
+    }
+  }
+
+  warn "FAIL: no rule '$r' found of any type at any priority\n";
+  return 0;
+}
+
diff --git a/upstream/t/pyzor.t b/upstream/t/pyzor.t
new file mode 100755 (executable)
index 0000000..878e45e
--- /dev/null
@@ -0,0 +1,55 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("pyzor");
+
+use Mail::SpamAssassin::Util;
+use constant HAS_PYZOR =>  Mail::SpamAssassin::Util::find_executable_in_env_path('pyzor');
+
+use Test::More;
+plan skip_all => "Net tests disabled" unless conf_bool('run_net_tests');
+plan skip_all => "Pyzor executable not found in path" unless HAS_PYZOR;
+plan tests => 8;
+
+diag('Note: Failures may not be an SpamAssassin bug, as Pyzor tests can fail due to problems with the Pyzor servers.');
+
+# ---------------------------------------------------------------------------
+
+tstprefs ("
+  full PYZOR_CHECK     eval:check_pyzor()
+  tflags PYZOR_CHECK   net autolearn_body
+  dns_available no
+  use_pyzor 1
+  pyzor_count_min 1
+  score PYZOR_CHECK 3.3
+");
+
+#PYZOR file was from real-world spam in October 2021
+
+#TESTING FOR SPAM
+%patterns = (
+  q{ 3.3 PYZOR_CHECK }, 'spam',
+);
+
+# Windows cmd doesn't recognize ' character
+sarun ("--cf=\"pyzor_fork 0\" -t < data/spam/pyzor", \&patterns_run_cb);
+ok_all_patterns();
+# Same with fork
+sarun ("--cf=\"pyzor_fork 1\" -t < data/spam/pyzor", \&patterns_run_cb);
+ok_all_patterns();
+
+#TESTING FOR HAM
+%patterns = (
+  'pyzor: got response: public.pyzor.org' => 'response',
+  'pyzor: result: COUNT=0' => 'zerocount',
+);
+%anti_patterns = (
+  q{ 3.3 PYZOR_CHECK }, 'nonspam',
+);
+
+sarun ("-D pyzor --cf=\"pyzor_fork 0\" -t < data/nice/001 2>&1", \&patterns_run_cb);
+ok_all_patterns();
+# same with fork
+sarun ("-D pyzor --cf=\"pyzor_fork 1\" -t < data/nice/001 2>&1", \&patterns_run_cb);
+ok_all_patterns();
+
index a26557520c2142d52704b9f03f00f8f48bf90e03..54da16e4dbf3c93139d7eff67c3731b84b80c265 100755 (executable)
@@ -1,9 +1,7 @@
 #!/usr/bin/perl -T
 
-use lib '.'; 
-use lib 't';
-use SATest; 
-sa_t_init("razor2");
+use lib '.'; use lib 't';
+use SATest; sa_t_init("razor2");
 
 use constant HAS_RAZOR2 => eval { require Razor2::Client::Agent; };
 use constant HAS_RAZOR2_IDENT => eval { -r $ENV{'HOME'}.'/.razor/identity'; };
@@ -12,39 +10,45 @@ use Test::More;
 plan skip_all => "Net tests disabled" unless conf_bool('run_net_tests');
 plan skip_all => "Needs Razor2" unless HAS_RAZOR2;
 plan skip_all => "Needs Razor2 Identity File Needed. razor-register / razor-admin -register has not been run, or identity file ($ENV{'HOME'}/.razor/identity) is unreadable." unless HAS_RAZOR2_IDENT;
-plan tests => 2;
+plan tests => 8;
 
 diag('Note: Failures may not be an SpamAssassin bug, as Razor tests can fail due to problems with the Razor servers.');
 
 # ---------------------------------------------------------------------------
 
-#report the email as spam so it fails below.  This process is not likely to work and I can't find a test point for razor. KAM 2018-08-20
-#unless (HAS_RAZOR2 or HAS_RAZOR2_IDENT) {
-#  system ("razor-report < data/spam/001");
-#  if (($? >> 8) != 0) {
-#    warn "'razor-report < data/spam/001' failed. This may cause this test to fail.\n";
-#  }
-#}
-
-tstpre ("
-loadplugin Mail::SpamAssassin::Plugin::Razor2
+tstprefs ("
+  full RAZOR2_CHECK    eval:check_razor2()
+  tflags RAZOR2_CHECK  net autolearn_body
+  dns_available no
+  use_razor2 1
+  score RAZOR2_CHECK 3.3
 ");
 
-#RAZOR2 file was from real-world spam in June 2019
+#RAZOR2 file contains a test string supplied by Razor tech support
 
 #TESTING FOR SPAM
 %patterns = (
-        q{ Listed in Razor2 }, 'spam',
-            );
+  q{ 3.3 RAZOR2_CHECK }, 'spam',
+);
 
 sarun ("-t < data/spam/razor2", \&patterns_run_cb);
 ok_all_patterns();
+# Same with fork
+sarun ("--cf='razor_fork 1' -t < data/spam/razor2", \&patterns_run_cb);
+ok_all_patterns();
 
 #TESTING FOR HAM
-%patterns = ();
+%patterns = (
+  'Connection established', 'connection',
+  'razor2: part=0 engine=8 contested=0 confidence=0', 'result',
+);
 %anti_patterns = (
-       q{ Listed in Razor2 }, 'nonspam',
-                );
+  q{ 3.3 RAZOR2_CHECK }, 'nonspam',
+);
 
-sarun ("-t < data/nice/001", \&patterns_run_cb);
+sarun ("-D razor2 -t < data/nice/001 2>&1", \&patterns_run_cb);
 ok_all_patterns();
+# same with fork
+sarun ("-D razor2 --cf='razor_fork 1' -t < data/nice/001 2>&1", \&patterns_run_cb);
+ok_all_patterns();
+
index f38b35002fcaa598df771b8e81d5fff349a2c819..20d75d6620fa5be372292161362ad6bdc5187033 100755 (executable)
@@ -1,21 +1,5 @@
 #!/usr/bin/perl -T
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_tests.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-    unshift(@INC, '../lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
-
 use lib '.'; use lib 't';
 use SATest; sa_t_init("rcvd_parser");
 use Test::More tests => 147;
index d74ef84c229f522d8d5a79c1d5e094d0848edc64..fde8ea01dd621a00dcf809630498c87622adb72c 100755 (executable)
@@ -10,20 +10,9 @@ use strict;
 use warnings;
 
 my $debug = 0;
-my $running_perl56 = ($] < 5.007);
 
-# perl 5.6.1 on Solaris fails all tests here if PERL_DL_NONLAZY=1
-# but works fine if it is =0.  ho hum
-$ENV{'PERL_DL_NONLAZY'} = 0;
-
-close STDIN;    # inhibits noise from sa-compile
-
-BEGIN { 
-  if (-e 't/test_dir') { chdir 't'; } 
-  if (-e 'test_dir') { unshift(@INC, '../blib/lib'); }
-}
-
-use Test::More tests => 128;
+use Test::More;
+plan tests => 128;
 use lib '../lib';
 
 # ---------------------------------------------------------------------------
@@ -199,11 +188,7 @@ use lib '../lib';
 # skip this one for perl 5.6.*; it does not truncate the long strings in the
 # same place as 5.8.* and 5.9.*, although they still work fine
 
-($running_perl56) and ok(1);
-($running_perl56) and ok(1);
-($running_perl56) and ok(1);
-($running_perl56) and ok(1);
-(!$running_perl56) and try_extraction ('
+try_extraction ('
 
   body VIRUS_WARNING345                /(This message contained attachments that have been blocked by Guinevere|This is an automatic message from the Guinevere Internet Antivirus Scanner)\./
   body VIRUS_WARNING345I                /(This message contained attachments that have been blocked by Guinevere|This is an automatic message from the Guinevere Internet Antivirus Scanner)\./i
@@ -224,11 +209,7 @@ use lib '../lib';
 
 # ---------------------------------------------------------------------------
 
-# also not suitable for perl 5.6.x
-($running_perl56) and ok(1);
-($running_perl56) and ok(1);
-($running_perl56) and ok(1);
-(!$running_perl56) and try_extraction ('
+try_extraction ('
 
   body FOO /foobar\x{e2}\x{82}\x{ac}baz/
 
@@ -445,9 +426,9 @@ sub try_extraction {
   my ($rules, $params, $output, $notoutput) = @_;
 
   my $sa = Mail::SpamAssassin->new({
-    rules_filename => "log/test_rules_copy",
-    site_rules_filename => "log/test_default.cf",
-    userprefs_filename  => "log/userprefs.cf",
+    rules_filename => $localrules,
+    site_rules_filename => $siterules,
+    userprefs_filename  => $userrules,
     local_tests_only    => 1,
     debug               => $debug,
     dont_copy_prefs     => 1,
@@ -456,18 +437,19 @@ sub try_extraction {
   ok($sa);
 
   # remove all rules and plugins; we want just our stuff
-  untaint_system("rm -f log/test_rules_copy/*.pre");
-  untaint_system("rm -f log/test_rules_copy/*.pm");
+  foreach (<$siterules/*.pre>, <$siterules/*.pm>) {
+    unlink(untaint_var($_));
+  }
   # keep 20_aux_tlds.cf to suppress RB warnings
-  rename("log/test_rules_copy/20_aux_tlds.cf", "log/test_rules_copy/20_aux_tlds.cf.tmp");
-  untaint_system("rm -f log/test_rules_copy/*.cf");
-  rename("log/test_rules_copy/20_aux_tlds.cf.tmp", "log/test_rules_copy/20_aux_tlds.cf");
+  foreach (<$localrules/*.cf>) {
+    unlink(untaint_var($_)) unless $_ =~ /20_aux_tlds.cf$/;
+  }
 
   { # suppress unnecessary warning:
     #   "Filehandle STDIN reopened as STDOUT only for output"
     # See https://rt.perl.org/rt3/Public/Bug/Display.html?id=23838
     no warnings 'io';
-    open (OUT, ">log/test_rules_copy/00_test.cf")
+    open (OUT, ">$localrules/99_test.cf")
       or die "failed to write rule";
   }
   print OUT "
index 28b2168e7f55aae080cb8d1daaaa6a737145db41..df7b0870f90db706751c52ddb7d5a3452a5c27c5 100755 (executable)
@@ -7,22 +7,22 @@ use Test::More tests => 6;
 
 # ---------------------------------------------------------------------------
 
-%patterns = ( q{ SORTED_RECIPS } => 'SORTED_RECIPS',
-             q{ SUSPICIOUS_RECIPS } => 'SUSPICIOUS_RECIPS');
+%patterns = ( q{ SORTED_RECIPS } => '',
+             q{ SUSPICIOUS_RECIPS } => '');
 %anti_patterns = ( );
 
 sarun ("-L -t < data/spam/010", \&patterns_run_cb);
 ok_all_patterns();
 
-%patterns = ( q{ SUSPICIOUS_RECIPS } => 'SUSPICIOUS_RECIPS');
-%anti_patterns = ( q{ SORTED_RECIPS } => 'SORTED_RECIPS');
+%patterns = ( q{ SUSPICIOUS_RECIPS } => '');
+%anti_patterns = ( q{ SORTED_RECIPS } => '');
 
 sarun ("-L -t < data/spam/011", \&patterns_run_cb);
 ok_all_patterns();
 
 %patterns = ( );
-%anti_patterns = ( q{ SORTED_RECIPS } => 'SORTED_RECIPS',
-                  q{ SUSPICIOUS_RECIPS } => 'SUSPICIOUS_RECIPS');
+%anti_patterns = ( q{ SORTED_RECIPS } => '',
+                  q{ SUSPICIOUS_RECIPS } => '');
 
 sarun ("-L -t < data/nice/006", \&patterns_run_cb);
 ok_all_patterns();
index 2a2ecfffffa1896645c5c6b9791abdd1671d6a2e..67f339ebb4af6d30ebd2c962eebfd74cee1cfb3c 100755 (executable)
@@ -5,25 +5,10 @@ use lib '.'; use lib 't';
 use SATest; sa_t_init("recreate");
 use Test::More tests => 9;
 
-BEGIN { 
-  if (-e 't/test_dir') {
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {
-    unshift(@INC, '../blib/lib');
-  }
-};
-
 use strict;
 use warnings;
 use Mail::SpamAssassin;
 
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
-
 our $warning = 0;
 
 $SIG{'__WARN__'} = sub {
@@ -41,9 +26,9 @@ $SIG{'__WARN__'} = sub {
 };
 
 my $spamtest = Mail::SpamAssassin->new({
-    rules_filename => "$prefix/t/log/test_rules_copy",
-    site_rules_filename => "$prefix/t/log/localrules.tmp",
-    userprefs_filename  => "$prefix/masses/spamassassin/user_prefs",
+    rules_filename => $localrules,
+    site_rules_filename => $siterules,
+    userprefs_filename  => $userrules,
     local_tests_only    => 1,
     debug             => 0,
     dont_copy_prefs   => 1,
@@ -66,9 +51,9 @@ $mail->finish();
 $spamtest->finish();
 
 $spamtest = Mail::SpamAssassin->new({
-    rules_filename => "$prefix/t/log/test_rules_copy",
-    site_rules_filename => "$prefix/t/log/localrules.tmp",
-    userprefs_filename  => "$prefix/masses/spamassassin/user_prefs",
+    rules_filename => $localrules,
+    site_rules_filename => $siterules,
+    userprefs_filename  => $userrules,
     local_tests_only    => 1,
     debug             => 0,
     dont_copy_prefs   => 1,
index be8945d403af5f7e77156a00c2690c962c6575e2..db6951c943725afd9a571e6fab2339f944154347 100755 (executable)
@@ -81,7 +81,7 @@ sub create_test_message {
     $text = $newmsg;
   }
 
-  open (OUT, ">log/recurse.eml") or die;
+  open (OUT, ">$workdir/recurse.eml") or die;
   print OUT $text;
   close OUT or die;
 }
@@ -103,7 +103,7 @@ Subject: testing recursion 3
     $boundstr++;
   }
 
-  open (OUT, ">log/recurse.eml") or die;
+  open (OUT, ">$workdir/recurse.eml") or die;
   print OUT $text;
   close OUT or die;
 }
@@ -112,7 +112,7 @@ sub try_scan {
   my $fh = IO::File->new_tmpfile();
   ok($fh);
   open(STDERR, ">&=".fileno($fh)) || die "Cannot reopen STDERR";
-  sarun("-D -L -t < log/recurse.eml",
+  sarun("-D -L -t < $workdir/recurse.eml",
         \&patterns_run_cb);
   seek($fh, 0, 0);
   my $error = do {
@@ -134,4 +134,4 @@ try_scan();
 create_test_message_3();
 try_scan();
 
-ok(unlink 'log/recurse.eml');
+ok(unlink "$workdir/recurse.eml");
diff --git a/upstream/t/regexp_named_capture.t b/upstream/t/regexp_named_capture.t
new file mode 100755 (executable)
index 0000000..6cb453d
--- /dev/null
@@ -0,0 +1,50 @@
+#!/usr/bin/perl -T
+
+use lib '.'; 
+use lib 't';
+use SATest; sa_t_init("regexp_named_capture");
+
+use Test::More;
+plan tests => 14;
+
+# ---------------------------------------------------------------------------
+
+%patterns = (
+  q{ 1.0 TEST_CAPTURE_1 } => '',
+  q{ 1.0 TEST_CAPTURE_2 } => '',
+  q{ 1.0 TEST_CAPTURE_3 } => '',
+  q{ 1.0 TEST_CAPTURE_4 } => '',
+  q{ 1.0 TEST_CAPTURE_5 } => '',
+  q{ 1.0 TEST_CAPTURE_6 } => '',
+  q{ 1.0 TEST_CAPTURE_7 } => '',
+  qr/tag TESTCAP1 is now ready, value: Ximian\n/ => '',
+  qr/tag TESTCAP2 is now ready, value: Ximian\n/ => '',
+  qr/tag TESTCAP3 is now ready, value: gnome\.org\n/ => '',
+  qr/tag TESTCAP4 is now ready, value: milkplus\n/ => '',
+  qr/tag TESTCAP5 is now ready, value: release\n/ => '',
+);
+%anti_patterns = (
+  q{ warn: } => '',
+  q{ 1.0 TEST_CAPTURE_8 } => '',
+);
+
+tstlocalrules (q{
+   body TEST_CAPTURE_1 /release of (?<TESTCAP1>\w+)/
+   rawbody TEST_CAPTURE_2 /release of (?<TESTCAP2>\w+)/
+   uri TEST_CAPTURE_3 /ftp\.(?<TESTCAP3>[\w.]+)/
+   header TEST_CAPTURE_4 Message-ID =~ /@(?<TESTCAP4>\w+)/
+   full TEST_CAPTURE_5 /X-Spam-Status.* preview (?<TESTCAP5>\w+)/s
+
+   # Use some captured tag
+   body TEST_CAPTURE_6 m,www\.%{TESTCAP1}\.,i
+
+   # We can also use common tags like HEADER()
+   body TEST_CAPTURE_7 m{www\.%{HEADER(From:addr:domain)}/}
+
+   # Should not hit
+   body TEST_CAPTURE_8 m,www\.\%{TESTCAP1}\.,i
+});
+
+sarun ("-D check,config -L -t < data/nice/001 2>&1", \&patterns_run_cb);
+ok_all_patterns();
+
index 4858e5afbb82157db4b718bf8217a443731580dd..e1a3c75467e816d8d885962da0e0b5592dac2c06 100755 (executable)
@@ -1,20 +1,6 @@
 #!/usr/bin/perl -w -T
 # test regexp validation
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_names.t", kluge around ...
-    chdir 't';
-  }
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
-
 use strict;
 use lib '.'; use lib 't';
 use SATest; sa_t_init("regexp_valid");
index a56900e9f823d6de48dc3b24a0488936a9be8412..9583d3ec63b0ff484d2b28676d18c5b7986c0399 100755 (executable)
@@ -1,16 +1,5 @@
 #!/usr/bin/perl -T
 
-# Leave this part, or else it'll use the live modules which is BAD!
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_names.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib', '.');
-  }
-}
-
 use lib '.'; use lib 't';
 use SATest; sa_t_init("relative_scores");
 
@@ -18,21 +7,21 @@ use strict;
 use vars qw/ $error /;
 
 tstlocalrules ("
-       # test that a single relative score applies to all scoresets
-       body FOO /foo/
-       score FOO 1 2 3 4
-       score FOO (1)
+  # test that a single relative score applies to all scoresets
+  body FOO /foo/
+  score FOO 1 2 3 4
+  score FOO (1)
 
-       # test that multiple relative scores apply to the scoresets
-       # appropriately, also that # and #.0 are equal
-       body BAR /bar/
-       score BAR 1
-       score BAR (1.0) (2) (3) (4.0)
+  # test that multiple relative scores apply to the scoresets
+  # appropriately, also that # and #.0 are equal
+  body BAR /bar/
+  score BAR 1
+  score BAR (1.0) (2) (3) (4.0)
 
-       # verify that negative decimal versions work
-       body BAZ /bar/
-       score BAZ 1
-       score BAZ (-1.0) (-2.1) (-3.2) (-4.3)
+  # verify that negative decimal versions work
+  body BAZ /bar/
+  score BAZ 1
+  score BAZ (-1.0) (-2.1) (-3.2) (-4.3)
 ");
 
 my $sa = create_saobj();
@@ -78,3 +67,4 @@ foreach my $index (0..3) {
   }
 }
 ok($error);
+
diff --git a/upstream/t/relaycountry.t b/upstream/t/relaycountry.t
new file mode 100755 (executable)
index 0000000..ccb24b6
--- /dev/null
@@ -0,0 +1,102 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("relaycountry");
+
+my $tests = 0;
+my %has;
+eval { require MaxMind::DB::Reader;   $tests += 2; $has{GEOIP2}  = 1 };
+eval { require Geo::IP;               $tests += 2; $has{GEOIP}   = 1 };
+eval { require IP::Country::Fast;     $tests += 2; $has{FAST}    = 1 };
+eval { require IP::Country::DB_File;
+       if ($DB_File::db_ver > 1 and $DB_File::db_version > 1) {
+         $tests += 2;
+         $has{DB_FILE} = 1;
+       }
+     };
+
+use Test::More;
+
+plan skip_all => "No supported GeoDB module installed" unless $tests;
+plan tests => $tests;
+
+# ---------------------------------------------------------------------------
+
+tstpre ("
+  loadplugin Mail::SpamAssassin::Plugin::RelayCountry
+");
+
+if (defined $has{GEOIP2}) {
+  tstprefs ("
+    geodb_module GeoIP2
+    geodb_search_path data/geodb
+    add_header all Relay-Country _RELAYCOUNTRY_
+  ");
+  # Check for country of gmail.com mail server
+  %patterns = (
+    q{ X-Spam-Relay-Country: US }, '',
+  );
+  ok sarun ("-L -t < data/spam/relayUS.eml", \&patterns_run_cb);
+  ok_all_patterns();
+  clear_pattern_counters();
+}
+else {
+  diag "skipping MaxMind::DB::Reader (GeoIP2) tests (not installed)\n";
+}
+
+
+if (defined $has{GEOIP}) {
+  tstprefs ("
+    geodb_module Geo::IP
+    geodb_search_path data/geodb
+    add_header all Relay-Country _RELAYCOUNTRY_
+  ");
+  # Check for country of gmail.com mail server
+  %patterns = (
+    q{ X-Spam-Relay-Country: US }, '',
+  );
+  ok sarun ("-L -t < data/spam/relayUS.eml", \&patterns_run_cb);
+  ok_all_patterns();
+  clear_pattern_counters();
+}
+else {
+  diag "skipping Geo::IP tests (not installed)\n";
+}
+
+
+if (defined $has{DB_FILE}) {
+  tstprefs ("
+    geodb_module DB_File
+    geodb_options country:data/geodb/ipcc.db
+    add_header all Relay-Country _RELAYCOUNTRY_
+  ");
+  # Check for country of gmail.com mail server
+  %patterns = (
+    q{ X-Spam-Relay-Country: US }, '',
+  );
+  ok sarun ("-L -t < data/spam/relayUS.eml", \&patterns_run_cb);
+  ok_all_patterns();
+  clear_pattern_counters();
+}
+else {
+  diag "skipping IP::Country::DB_File tests (not installed or DB_File bdb version too old)\n";
+}
+
+
+if (defined $has{FAST}) {
+  tstprefs ("
+    geodb_module Fast
+    add_header all Relay-Country _RELAYCOUNTRY_
+  ");
+  # Check for country of gmail.com mail server
+  %patterns = (
+    q{ X-Spam-Relay-Country: US }, '',
+  );
+  ok sarun ("-L -t < data/spam/relayUS.eml", \&patterns_run_cb);
+  ok_all_patterns();
+  clear_pattern_counters();
+}
+else {
+  diag "skipping IP::Country::Fast tests (not installed)\n";
+}
+
diff --git a/upstream/t/relaycountry_fast.t b/upstream/t/relaycountry_fast.t
deleted file mode 100755 (executable)
index 6491e33..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-#!/usr/bin/perl -T
-
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_tests.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-    unshift(@INC, '../lib');
-  }
-}
-
-use lib '.'; use lib 't';
-use SATest; sa_t_init("relaycountry");
-
-use constant HAS_COUNTRY_FAST => eval { require IP::Country::Fast; };
-
-use Test::More;
-
-plan skip_all => "IP::Country::Fast not installed" unless HAS_COUNTRY_FAST;
-plan tests => 2;
-
-# ---------------------------------------------------------------------------
-
-tstpre ("
-loadplugin Mail::SpamAssassin::Plugin::RelayCountry
-");
-
-tstprefs ("
-        dns_available no
-        country_db_type Fast
-        add_header all Relay-Country _RELAYCOUNTRY_
-        ");
-
-# Check for country of gmail.com mail server
-%patterns = (
-        q{ X-Spam-Relay-Country: US }, '',
-            );
-
-ok sarun ("-L -t < data/spam/relayUS.eml", \&patterns_run_cb);
-ok_all_patterns();
diff --git a/upstream/t/relaycountry_geoip.t b/upstream/t/relaycountry_geoip.t
deleted file mode 100755 (executable)
index fc2dbcb..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-#!/usr/bin/perl -T
-
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_tests.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-    unshift(@INC, '../lib');
-  }
-}
-
-use lib '.'; use lib 't';
-use SATest; sa_t_init("relaycountry");
-
-use constant HAS_GEOIP => eval { require Geo::IP; };
-use constant HAS_GEOIP_CONF => eval { Geo::IP->new(Geo::IP::GEOIP_STANDARD); };
-
-use Test::More;
-
-plan skip_all => "Geo::IP not installed" unless HAS_GEOIP;
-plan skip_all => "Geo::IP not configured" unless HAS_GEOIP_CONF;
-
-plan tests => 2;
-
-# ---------------------------------------------------------------------------
-
-tstpre ("
-loadplugin Mail::SpamAssassin::Plugin::RelayCountry
-");
-
-tstprefs ("
-        dns_available no
-        country_db_type GeoIP
-        add_header all Relay-Country _RELAYCOUNTRY_
-        ");
-
-# Check for country of gmail.com mail server
-%patterns = (
-        q{ X-Spam-Relay-Country: US }, '',
-            );
-
-ok sarun ("-L -t < data/spam/relayUS.eml", \&patterns_run_cb);
-ok_all_patterns();
diff --git a/upstream/t/relaycountry_geoip2.t b/upstream/t/relaycountry_geoip2.t
deleted file mode 100755 (executable)
index 782aaec..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-#!/usr/bin/perl -T
-
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_tests.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-    unshift(@INC, '../lib');
-  }
-}
-
-use lib '.'; use lib 't';
-use SATest; sa_t_init("relaycountry");
-
-use constant HAS_GEOIP2 => eval { require GeoIP2::Database::Reader; };
-
-# TODO: get the list from RelayCountry.pm / geoip2_default_db_path
-use constant HAS_GEOIP2_DB => eval {
-  -f "/usr/local/share/GeoIP/GeoIP2-Country.mmdb" or
-  -f "/usr/share/GeoIP/GeoIP2-Country.mmdb" or
-  -f "/var/lib/GeoIP/GeoIP2-Country.mmdb" or
-  -f "/usr/local/share/GeoIP/GeoLite2-Country.mmdb" or
-  -f "/usr/share/GeoIP/GeoLite2-Country.mmdb" or
-  -f "/var/lib/GeoIP/GeoLite2-Country.mmdb"
-};
-
-use Test::More;
-
-plan skip_all => "GeoIP2::Database::Reader not installed" unless HAS_GEOIP2;
-plan skip_all => "GeoIP2 database not found from default locations" unless HAS_GEOIP2_DB;
-
-plan tests => 2;
-
-# ---------------------------------------------------------------------------
-
-tstpre ("
-loadplugin Mail::SpamAssassin::Plugin::RelayCountry
-");
-
-tstprefs ("
-        dns_available no
-        country_db_type GeoIP2
-        add_header all Relay-Country _RELAYCOUNTRY_
-        ");
-
-# Check for country of gmail.com mail server
-%patterns = (
-        q{ X-Spam-Relay-Country: US }, '',
-            );
-
-ok sarun ("-L -t < data/spam/relayUS.eml", \&patterns_run_cb);
-ok_all_patterns();
index be2e719fd875f91c03b6ff30911fb1b9d9ac7c39..809e63caf3f61043fd44616d1512c8b5862f945e 100755 (executable)
@@ -9,7 +9,7 @@ use Test::More tests => 8;
 
 # Use a slightly modified gtube ...
 my $origtest = 'data/spam/gtube.eml';
-my $test = 'log/report_safe.eml';
+my $test = "$workdir/report_safe.eml";
 my $original = '';
 if (open(OTEST, $origtest) && open(TEST, ">$test")) {
   local $/=undef;
@@ -61,3 +61,4 @@ $message = safe($boundary, '', 'text/plain', $description, 'inline');
 tstprefs ("report_safe 2\n");
 sarun ("-L < $test", \&patterns_run_cb);
 ok_all_patterns();
+
index 1801405d412ecb000756742c5dcea94d3d072b45..4aa022f2682967153c3409e47d168acceb46f047 100755 (executable)
@@ -10,25 +10,48 @@ $ENV{'LANGUAGE'} = $ENV{'LC_ALL'} = 'C';             # a cheat, but we need the
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ Spam detection software, running on the system "}, 'spam-report-body',
-q{ Subject: There yours for FREE!}, 'subj',
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-q{ From: ends in many numbers}, 'endsinnums',
-q{ From: does not include a real name}, 'noreal',
-q{ BODY: Nobody's perfect }, 'remove',
-q{ Message-Id is not valid, }, 'msgidnotvalid',
-q{ 'From' yahoo.com does not match }, 'fromyahoo',
-q{ Invalid Date: header (not RFC 2822) }, 'invdate',
-q{ Uses a dotted-decimal IP address in URL }, 'dotteddec',
-
-); #'
-
-tstprefs ("
-        $default_cf_lines
-        report_safe 0
-       ");
+  q{ Spam detection software, running on the system "}, 'spam-report-body',
+  q{ Subject: There yours for FREE!}, 'subj',
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
+  q{ From: ends in many numbers}, 'endsinnums',
+  q{ From: does not include a real name}, 'noreal',
+  q{ BODY: Nobody's perfect }, 'remove',
+  q{ Message-Id is not valid, }, 'msgidnotvalid',
+  q{ 'From' yahoo.com does not match }, 'fromyahoo',
+  q{ Invalid Date: header (not RFC 2822) }, 'invdate',
+  q{ Uses a dotted-decimal IP address in URL }, 'dotteddec',
+);
+
+# This test checks that the report template feature works.
+# Define a representative example default template here to test out
+tstprefs ('
+  clear_report_template
+  report Spam detection software, running on the system "_HOSTNAME_",
+  report has_YESNO(, NOT)_ identified this incoming email as_YESNO( possible,)_ spam.  The original
+  report message has been attached to this so you can view it or label
+  report similar future email.  If you have any questions, see
+  report _CONTACTADDRESS_ for details.
+  report
+  report Content preview:  _PREVIEW_
+  report
+  report Content analysis details:   (_SCORE_ points, _REQD_ required)
+  report
+  report " pts rule name              description"
+  report  ---- ---------------------- --------------------------------------------------
+  report _SUMMARY_
+
+  report_contact  @@CONTACT_ADDRESS@@
+
+  clear_headers
+
+  add_header all Checker-Version SpamAssassin _VERSION_ (_SUBVERSION_) on _HOSTNAME_
+  add_header spam Flag _YESNOCAPS_
+  add_header all Level _STARS(*)_
+  add_header all Status "_YESNO_, score=_SCORE_ required=_REQD_ tests=_TESTS_ autolearn=_AUTOLEARN_ version=_VERSION_"
+  report_safe 0
+');
 
 sarun ("-L -t < data/spam/001", \&patterns_run_cb);
 ok_all_patterns();
+
index d72e5422e497aeb716ca526b7ebffca942d5359a..7e75b4112c4869c9a7502bddf5f6b1e577bd56e8 100755 (executable)
@@ -2,24 +2,23 @@
 
 use lib '.'; use lib 't';
 use SATest; sa_t_init("reportheader");
-use Test::More tests => 2;
+
+use Test::More tests => 3;
 
 $ENV{'LANGUAGE'} = $ENV{'LC_ALL'} = 'C';             # a cheat, but we need the patterns to work
 
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-    q{X-Spam-Report: =?ISO-8859-1?Q? }, 'qp-encoded-hdr',
-    q{ Invalid Date: header =ae =af =b0 foo }, 'qp-encoded-desc',
-
+  qr/^X-Spam-Report:\s*$/m, 'qp-encoded-hdr',
+  qr/^\t\*\s+[0-9.-]+ INVALID_DATE\s+Invalid Date: header =\?UTF-8\?B\?wq4gwq8gwrA=\?= foo$/m, 'qp-encoded-desc',
+  qr/^ [0-9.-]+ INVALID_DATE\s+Invalid Date: header ® ¯ ° foo$/m, 'report-desc',
 );
 
 tstprefs ("
-        $default_cf_lines
-        report_safe 0
-       describe INVALID_DATE   Invalid Date: header \xae \xaf \xb0 foo
-       ");
+  report_safe 0
+  describe INVALID_DATE Invalid Date: header ® ¯ ° foo
+");
 
 sarun ("-L -t < data/spam/001", \&patterns_run_cb);
 ok_all_patterns();
old mode 100644 (file)
new mode 100755 (executable)
index b4199fe..20757f9
@@ -1,20 +1,5 @@
 #!/usr/bin/perl -w -T
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_tests.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
-
 use strict;
 use lib '.'; use lib 't';
 use SATest; sa_t_init("reuse");
@@ -25,6 +10,7 @@ use Mail::SpamAssassin;
 
 use Test::More;
 plan skip_all => "no mass check" unless (-e '../masses/mass-check');
+plan skip_all => "mass check script does not run on Windows" if $RUNNING_ON_WINDOWS;
 plan tests => 37;
 
 # Tests the following cases:
@@ -37,62 +23,68 @@ plan tests => 37;
 # - Reuse works in positive and negative cases
 # - Rules defined only by "reuse" can have arbitrary scores and priorities set
 
-tstlocalrules('
-
-# suppress RB warnings
-util_rb_tld com
+# Need all files under $localrules for mass-check
+foreach my $tainted (<$siterules/*.pre $siterules/languages>) {
+  $tainted =~ /(.*)/;
+  my $file = $1;
+  copy ($file, "$localrules")
+    or warn "cannot copy $file to $localrules: $!";
+}
 
-# Check that order of reuse/body lines for BODY_RULE_* does not matter
-reuse  BODY_RULE_1
+tstlocalrules('
+  # suppress RB warnings
+  util_rb_tld com
 
-body   BODY_RULE_1    /./
-score  BODY_RULE_1    1.0
+  # Check that order of reuse/body lines for BODY_RULE_* does not matter
+  reuse  BODY_RULE_1
 
-body   BODY_RULE_2    /\bfoobar\b/
-score  BODY_RULE_2    1.0
+  body   BODY_RULE_1    /./
+  score  BODY_RULE_1    1.0
 
-header HEADER_RULE_1  Subject =~ /\bmessage\b/
+  body   BODY_RULE_2    /\bfoobar\b/
+  score  BODY_RULE_2    1.0
 
-meta   META_RULE_1    BODY_RULE_1 || BODY_RULE_2
+  header HEADER_RULE_1  Subject =~ /\bmessage\b/
 
-reuse    BODY_RULE_2
-priority BODY_RULE_2  -2
-score    BODY_RULE_2  1.5
+  meta   META_RULE_1    BODY_RULE_1 || BODY_RULE_2
 
-reuse    NEW_RULE     OTHER_RULE
-priority NEW_RULE     -3
-score    NEW_RULE     0.5
+  reuse    BODY_RULE_2
+  priority BODY_RULE_2  -2
+  score    BODY_RULE_2  1.5
 
-reuse    OTHER_RULE
-priority OTHER_RULE   -4
+  reuse    NEW_RULE     OTHER_RULE
+  priority NEW_RULE     -3
+  score    NEW_RULE     0.5
 
-reuse    RENAMED_RULE OLD_RULE_1 OLD_RULE_2 OLD_RULE_3
+  reuse    OTHER_RULE
+  priority OTHER_RULE   -4
 
-reuse    SCORED_RULE  OLD_RULE_2
-score    SCORED_RULE  2.0
-priority SCORED_RULE -1
+  reuse    RENAMED_RULE OLD_RULE_1 OLD_RULE_2 OLD_RULE_3
 
+  reuse    SCORED_RULE  OLD_RULE_2
+  score    SCORED_RULE  2.0
+  priority SCORED_RULE -1
 ');
 
 # reuse on, mail has no X-Spam-Status
 write_mail(0);
-ok_system("$perl_path -w ../masses/mass-check -c=log/localrules.tmp --reuse --file log/mail.txt > log/noxss.out");
+ok_system("$perl_path -w ../masses/mass-check -c=$localrules --reuse --file $workdir/mail.txt > $workdir/noxss.out");
 
 %patterns = (
-             'BODY_RULE_1' => 'BODY_RULE_1',
-             'HEADER_RULE_1' => 'HEADER_RULE_1',
-             'META_RULE_1' => 'META_RULE_1'
-             );
+  'BODY_RULE_1' => '',
+  'HEADER_RULE_1' => '',
+  'META_RULE_1' => '',
+);
 %anti_patterns = (
-                  'NEW_RULE' => 'NEW_RULE',
-                  'OTHER_RULE' => 'OTHER_RULE',
-                  'RENAMED_RULE' => 'RENAMED_RULE',
-                  'NONEXISTANT_RULE' => 'NONEXISTANT_RULE',
-                  'BODY_RULE_2' => 'BODY_RULE_2',
-                  'SCORED_RULE' => 'SCORED_RULE'
-                  );
-
-checkfile("noxss.out", \&patterns_run_cb);
+  'NEW_RULE' => '',
+  'OTHER_RULE' => '',
+  'RENAMED_RULE' => '',
+  'NONEXISTANT_RULE' => '',
+  'BODY_RULE_2' => '',
+  'SCORED_RULE' => '',
+);
+
+checkfile("$workdir/noxss.out", \&patterns_run_cb);
 ok_all_patterns();
 clear_pattern_counters();
 
@@ -100,79 +92,77 @@ clear_pattern_counters();
 write_mail(1);
 
 # test without reuse
-ok_system("$perl_path -w ../masses/mass-check -c=log/localrules.tmp --file log/mail.txt > log/noreuse.out");
+ok_system("$perl_path -w ../masses/mass-check -c=$localrules --file $workdir/mail.txt > $workdir/noreuse.out");
 
 %patterns = (
-             'BODY_RULE_1' => 'BODY_RULE_1',
-             'HEADER_RULE_1' => 'HEADER_RULE_1',
-             'META_RULE_1' => 'META_RULE_1'
-             );
+  'BODY_RULE_1' => '',
+  'HEADER_RULE_1' => '',
+  'META_RULE_1' => '',
+);
 %anti_patterns = (
-                  'NEW_RULE' => 'NEW_RULE',
-                  'OTHER_RULE' => 'OTHER_RULE',
-                  'RENAMED_RULE' => 'RENAMED_RULE',
-                  'NONEXISTANT_RULE' => 'NONEXISTANT_RULE',
-                  'BODY_RULE_2' => 'BODY_RULE_2',
-                  'SCORED_RULE' => 'SCORED_RULE'
-                  );
-checkfile("noreuse.out", \&patterns_run_cb);
+  'NEW_RULE' => '',
+  'OTHER_RULE' => '',
+  'RENAMED_RULE' => '',
+  'NONEXISTANT_RULE' => '',
+  'BODY_RULE_2' => '',
+  'SCORED_RULE' => '',
+);
+checkfile("$workdir/noreuse.out", \&patterns_run_cb);
 ok_all_patterns();
 clear_pattern_counters();
 
 # test with reuse
-ok_system("$perl_path -w ../masses/mass-check -c=log/localrules.tmp --reuse --file log/mail.txt > log/reuse.out");
+ok_system("$perl_path -w ../masses/mass-check -c=$localrules --reuse --file $workdir/mail.txt > $workdir/reuse.out");
 
 
 %patterns = (
-             'HEADER_RULE_1' => 'HEADER_RULE_1',
-             'BODY_RULE_2' => 'BODY_RULE_2',
-             'META_RULE_1' => 'META_RULE_1',
-             'NEW_RULE' => 'NEW_RULE',
-             'OTHER_RULE' => 'OTHER_RULE',
-             'RENAMED_RULE' => 'RENAMED_RULE',
-             'SCORED_RULE' => 'SCORED_RULE',
-             'Y 8' => 'score'
-             );
+  'HEADER_RULE_1' => '',
+  'BODY_RULE_2' => '',
+  'META_RULE_1' => '',
+  'NEW_RULE' => '',
+  'OTHER_RULE' => '',
+  'RENAMED_RULE' => '',
+  'SCORED_RULE' => '',
+  'Y 8' => '',
+);
 %anti_patterns = (
-                  'BODY_RULE_1' => 'BODY_RULE_1',
-                  'NONEXISTANT_RULE' => 'NONEXISTANT_RULE'
-                  );
+  'BODY_RULE_1' => '',
+  'NONEXISTANT_RULE' => '',
+);
 
-checkfile("reuse.out", \&patterns_run_cb);
+checkfile("$workdir/reuse.out", \&patterns_run_cb);
 ok_all_patterns();
 clear_pattern_counters();
 
 tstlocalrules('
+  # suppress RB warnings
+  util_rb_tld com
 
-# suppress RB warnings
-util_rb_tld com
-
-meta META_RULE_1 RULE_A && !RULE_B
-
-body  RULE_A /./
-reuse RULE_B OTHER_RULE
+  meta META_RULE_1 RULE_A && !RULE_B
 
-body  RULE_C / does not hit /
+  body  RULE_A /./
+  reuse RULE_B OTHER_RULE
 
-meta META_RULE_2 (RULE_A && RULE_B) || RULE_C
+  body  RULE_C / does not hit /
 
+  meta META_RULE_2 (RULE_A && RULE_B) || RULE_C
 ');
 
 write_mail(1);
 
 # test with reuse
-ok_system("$perl_path -w ../masses/mass-check -c=log/localrules.tmp --reuse --file log/mail.txt > log/metareuse.out");
+ok_system("$perl_path -w ../masses/mass-check -c=$localrules --reuse --file $workdir/mail.txt > $workdir/metareuse.out");
 
 %patterns = (
-            'META_RULE_2' => 'META_RULE_2',
-            'RULE_A' => 'RULE_A',
-            'RULE_B' => 'RULE_B',
-             );
+  'META_RULE_2' => '',
+  'RULE_A' => '',
+  'RULE_B' => '',
+);
 %anti_patterns = (
-            'META_RULE_1' => 'META_RULE_1',
-            'RULE_C' => 'RULE_C',
-                );
-checkfile("metareuse.out", \&patterns_run_cb);
+  'META_RULE_1' => '',
+  'RULE_C' => '',
+);
+checkfile("$workdir/metareuse.out", \&patterns_run_cb);
 ok_all_patterns();
 clear_pattern_counters();
 
old mode 100644 (file)
new mode 100755 (executable)
index 5cf6705..4a0a032
@@ -1,11 +1,9 @@
 #!/usr/bin/perl -T
 
-# run with:   sudo prove -v t/root_spamd*
-
 use lib '.'; use lib 't';
 use SATest; sa_t_init("root_spamd");
 
-use constant HAS_SUDO => eval { $_ = untaint_cmd("which sudo 2>/dev/null"); chomp; -x };
+use constant HAS_SUDO => $RUNNING_ON_WINDOWS || eval { $_ = untaint_cmd("which sudo 2>/dev/null"); chomp; -x };
 
 use Test::More;
 plan skip_all => "root tests disabled" unless conf_bool('run_root_tests');
@@ -16,20 +14,21 @@ plan tests => 14;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ Return-Path: sb55sb55@yahoo.com}, 'firstline',
-q{ Subject: There yours for FREE!}, 'subj',
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-q{ X-Spam-Level: **********}, 'stars',
-q{ TEST_ENDSNUMS}, 'endsinnums',
-q{ TEST_NOREALNAME}, 'noreal',
-q{ This must be the very last line}, 'lastline',
-
+  q{ Return-Path: sb55sb55@yahoo.com}, 'firstline',
+  q{ Subject: There yours for FREE!}, 'subj',
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
+  q{ X-Spam-Level: **********}, 'stars',
+  q{ TEST_ENDSNUMS}, 'endsinnums',
+  q{ TEST_NOREALNAME}, 'noreal',
+  q{ This must be the very last line}, 'lastline',
 );
 
 # run spamc as unpriv uid
 $spamc = "sudo -u nobody $spamc";
+# ensure it is readable by all
+diag "Test will fail if run in directory not accessible by 'nobody' as is typical for a home directory";
+chmod 01755, $workdir;
 
 ok(start_spamd("-L"));
 
@@ -37,12 +36,12 @@ ok(spamcrun("< data/spam/001", \&patterns_run_cb));
 ok_all_patterns();
 
 %patterns = (
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-             );
-
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
+);
 
 ok (spamcrun("< data/spam/018", \&patterns_run_cb));
 ok_all_patterns();
 
 ok(stop_spamd());
+
old mode 100644 (file)
new mode 100755 (executable)
index eb2e1c9..56bba0b
@@ -3,7 +3,7 @@
 use lib '.'; use lib 't';
 use SATest; sa_t_init("root_spamd_tell");
 
-use constant HAS_SUDO => eval { $_ = untaint_cmd("which sudo 2>/dev/null"); chomp; -x };
+use constant HAS_SUDO => $RUNNING_ON_WINDOWS || eval { $_ = untaint_cmd("which sudo 2>/dev/null"); chomp; -x };
 
 use Test::More;
 plan skip_all => "root tests disabled" unless conf_bool('run_root_tests');
@@ -14,22 +14,24 @@ plan tests => 6;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-q{ Message successfully } => 'learned',
+  q{Message successfully } => 'learned',
 );
 
 # run spamc as unpriv uid
 $spamc = "sudo -u nobody $spamc";
 
 # remove these first
-unlink('log/user_state/bayes_seen.dir');
-unlink('log/user_state/bayes_toks.dir');
+unlink("$userstate/bayes_seen.dir");
+unlink("$userstate/bayes_toks.dir");
 
-# ensure it is writable by all
-use File::Path; mkpath("log/user_state"); chmod 01777, "log/user_state";
+# ensure it is readable/writeable by all
+diag "Test will fail if run in directory not accessible by 'nobody' as is typical for a home directory";
+chmod 01755, $workdir;
+chmod 01777, $userstate;
 
 # use SDBM so we do not need DB_File
-tstlocalrules ("
-        bayes_store_module Mail::SpamAssassin::BayesStore::SDBM
+tstprefs ("
+  bayes_store_module Mail::SpamAssassin::BayesStore::SDBM
 ");
 
 ok(start_spamd("-L --allow-tell"));
@@ -40,8 +42,8 @@ ok_all_patterns();
 ok(stop_spamd());
 
 # ensure these are not owned by root
-ok check_owner('log/user_state/bayes_seen.dir');
-ok check_owner('log/user_state/bayes_toks.dir');
+ok check_owner("$userstate/bayes_seen.dir");
+ok check_owner("$userstate/bayes_toks.dir");
 
 sub check_owner {
   my $f = shift;
@@ -61,3 +63,4 @@ sub check_owner {
     return 1;
   }
 }
+
old mode 100644 (file)
new mode 100755 (executable)
index b3b7290..57fd428
@@ -3,7 +3,7 @@
 use lib '.'; use lib 't';
 use SATest; sa_t_init("root_spamd_tell_paranoid");
 
-use constant HAS_SUDO => eval { $_ = untaint_cmd("which sudo 2>/dev/null"); chomp; -x };
+use constant HAS_SUDO => $RUNNING_ON_WINDOWS || eval { $_ = untaint_cmd("which sudo 2>/dev/null"); chomp; -x };
 
 use Test::More;
 plan skip_all => "root tests disabled" unless conf_bool('run_root_tests');
@@ -14,22 +14,24 @@ plan tests => 6;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-q{ Message successfully } => 'learned',
+  q{Message successfully } => 'learned',
 );
 
 # run spamc as unpriv uid
 $spamc = "sudo -u nobody $spamc";
 
 # remove these first
-unlink('log/user_state/bayes_seen.dir');
-unlink('log/user_state/bayes_toks.dir');
+unlink("$userstate/bayes_seen.dir");
+unlink("$userstate/bayes_toks.dir");
 
-# ensure it is writable by all
-use File::Path; mkpath("log/user_state"); chmod 01777, "log/user_state";
+# ensure it is readable/writeable by all
+diag "Test will fail if run in directory not accessible by 'nobody' as is typical for a home directory";
+chmod 01755, $workdir;
+chmod 01777, $userstate;
 
 # use SDBM so we do not need DB_File
-tstlocalrules ("
-        bayes_store_module Mail::SpamAssassin::BayesStore::SDBM
+tstprefs ("
+  bayes_store_module Mail::SpamAssassin::BayesStore::SDBM
 ");
 
 ok(start_spamd("-L --allow-tell --paranoid"));
@@ -40,8 +42,8 @@ ok_all_patterns();
 ok(stop_spamd());
 
 # ensure these are not owned by root
-ok check_owner('log/user_state/bayes_seen.dir');
-ok check_owner('log/user_state/bayes_toks.dir');
+ok check_owner("$userstate/bayes_seen.dir");
+ok check_owner("$userstate/bayes_toks.dir");
 
 sub check_owner {
   my $f = shift;
@@ -61,3 +63,4 @@ sub check_owner {
     return 1;
   }
 }
+
old mode 100644 (file)
new mode 100755 (executable)
index 13ad623..980cca0
@@ -3,7 +3,7 @@
 use lib '.'; use lib 't';
 use SATest; sa_t_init("root_spamd_tell_x");
 
-use constant HAS_SUDO => eval { $_ = untaint_cmd("which sudo 2>/dev/null"); chomp; -x };
+use constant HAS_SUDO => $RUNNING_ON_WINDOWS || eval { $_ = untaint_cmd("which sudo 2>/dev/null"); chomp; -x };
 
 use Test::More;
 plan skip_all => "root tests disabled" unless conf_bool('run_root_tests');
@@ -14,22 +14,24 @@ plan tests => 6;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-q{ Message successfully } => 'learned',
+  q{Message successfully } => 'learned',
 );
 
 # run spamc as unpriv uid
 $spamc = "sudo -u nobody $spamc";
 
 # remove these first
-unlink('log/user_state/bayes_seen.dir');
-unlink('log/user_state/bayes_toks.dir');
+unlink("$userstate/bayes_seen.dir");
+unlink("$userstate/bayes_toks.dir");
 
-# ensure it is writable by all
-use File::Path; mkpath("log/user_state"); chmod 01777, "log/user_state";
+# ensure it is readable/writeable by all
+diag "Test will fail if run in directory not accessible by 'nobody' as is typical for a home directory";
+chmod 01755, $workdir;
+chmod 01777, $userstate;
 
 # use SDBM so we do not need DB_File
-tstlocalrules ("
-        bayes_store_module Mail::SpamAssassin::BayesStore::SDBM
+tstprefs ("
+  bayes_store_module Mail::SpamAssassin::BayesStore::SDBM
 ");
 
 ok(start_spamd("-L --allow-tell --create-prefs -x"));
@@ -40,8 +42,8 @@ ok_all_patterns();
 ok(stop_spamd());
 
 # ensure these are not owned by root
-ok check_owner('log/user_state/bayes_seen.dir');
-ok check_owner('log/user_state/bayes_toks.dir');
+ok check_owner("$userstate/bayes_seen.dir");
+ok check_owner("$userstate/bayes_toks.dir");
 
 sub check_owner {
   my $f = shift;
@@ -61,3 +63,4 @@ sub check_owner {
     return 1;
   }
 }
+
old mode 100644 (file)
new mode 100755 (executable)
index 69eb83a..3e08d2e
@@ -3,7 +3,7 @@
 use lib '.'; use lib 't';
 use SATest; sa_t_init("root_spamd_tell_x_paranoid");
 
-use constant HAS_SUDO => eval { $_ = untaint_cmd("which sudo 2>/dev/null"); chomp; -x };
+use constant HAS_SUDO => $RUNNING_ON_WINDOWS || eval { $_ = untaint_cmd("which sudo 2>/dev/null"); chomp; -x };
 
 use Test::More;
 plan skip_all => "root tests disabled" unless conf_bool('run_root_tests');
@@ -14,22 +14,24 @@ plan tests => 6;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-q{ Message successfully } => 'learned',
+  q{Message successfully } => 'learned',
 );
 
 # run spamc as unpriv uid
 $spamc = "sudo -u nobody $spamc";
 
 # remove these first
-unlink('log/user_state/bayes_seen.dir');
-unlink('log/user_state/bayes_toks.dir');
+unlink("$userstate/bayes_seen.dir");
+unlink("$userstate/bayes_toks.dir");
 
-# ensure it is writable by all
-use File::Path; mkpath("log/user_state"); chmod 01777, "log/user_state";
+# ensure it is readable/writeable by all
+diag "Test will fail if run in directory not accessible by 'nobody' as is typical for a home directory";
+chmod 01755, $workdir;
+chmod 01777, $userstate;
 
 # use SDBM so we do not need DB_File
-tstlocalrules ("
-        bayes_store_module Mail::SpamAssassin::BayesStore::SDBM
+tstprefs ("
+  bayes_store_module Mail::SpamAssassin::BayesStore::SDBM
 ");
 
 ok(start_spamd("-L --allow-tell --create-prefs -x --paranoid"));
@@ -40,8 +42,8 @@ ok_all_patterns();
 ok(stop_spamd());
 
 # ensure these are not owned by root
-ok check_owner('log/user_state/bayes_seen.dir');
-ok check_owner('log/user_state/bayes_toks.dir');
+ok check_owner("$userstate/bayes_seen.dir");
+ok check_owner("$userstate/bayes_toks.dir");
 
 sub check_owner {
   my $f = shift;
@@ -61,3 +63,4 @@ sub check_owner {
     return 1;
   }
 }
+
index 5c8b5525e514ca8800d1b3cf81433e01c888aeb9..b37315f00164ec5756734127f8d17b988ba8b6c3 100755 (executable)
@@ -1,11 +1,9 @@
 #!/usr/bin/perl -T
 
-# run with:   sudo prove -v t/root_spamd*
-
 use lib '.'; use lib 't';
 use SATest; sa_t_init("root_spamd_u");
 
-use constant HAS_SUDO => eval { $_ = untaint_cmd("which sudo 2>/dev/null"); chomp; -x };
+use constant HAS_SUDO => $RUNNING_ON_WINDOWS || eval { $_ = untaint_cmd("which sudo 2>/dev/null"); chomp; -x };
 
 use Test::More;
 plan skip_all => "root tests disabled" unless conf_bool('run_root_tests');
@@ -16,20 +14,21 @@ plan tests => 11;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ Return-Path: sb55sb55@yahoo.com}, 'firstline',
-q{ Subject: There yours for FREE!}, 'subj',
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-q{ X-Spam-Level: **********}, 'stars',
-q{ TEST_ENDSNUMS}, 'endsinnums',
-q{ TEST_NOREALNAME}, 'noreal',
-q{ This must be the very last line}, 'lastline',
-
+  q{ Return-Path: sb55sb55@yahoo.com}, 'firstline',
+  q{ Subject: There yours for FREE!}, 'subj',
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
+  q{ X-Spam-Level: **********}, 'stars',
+  q{ TEST_ENDSNUMS}, 'endsinnums',
+  q{ TEST_NOREALNAME}, 'noreal',
+  q{ This must be the very last line}, 'lastline',
 );
 
 # run spamc as unpriv uid
 $spamc = "sudo -u nobody $spamc";
+# ensure it is readable by all
+diag "Test will fail if run in directory not accessible by 'nobody' as is typical for a home directory";
+chmod 01755, $workdir;
 
 ok(start_spamd("-L -u nobody"));
 
@@ -40,3 +39,4 @@ alarm 0;
 ok_all_patterns();
 
 ok(stop_spamd());
+
index e4c745fcbd6ae720b56448aa211ea966ec7d52e3..8110279e0ba3838a94afac1001947abba056d94f 100755 (executable)
@@ -1,12 +1,11 @@
 #!/usr/bin/perl -T
 #
 # test for http://issues.apache.org/SpamAssassin/show_bug.cgi?id=5574#c12 .
-# run with:   sudo prove -v t/root_spamd*
 
 use lib '.'; use lib 't';
 use SATest; sa_t_init("root_spamd_u_dcc");
 
-use constant HAS_SUDO => eval { $_ = untaint_cmd("which sudo 2>/dev/null"); chomp; -x };
+use constant HAS_SUDO => $RUNNING_ON_WINDOWS || eval { $_ = untaint_cmd("which sudo 2>/dev/null"); chomp; -x };
 
 use Test::More;
 plan skip_all => "root tests disabled" unless conf_bool('run_root_tests');
@@ -18,14 +17,12 @@ plan tests => 23;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-        q{ spam reported to DCC }, 'dcc report',
-            );
-
-tstpre ("
+  q{ spam reported to DCC }, 'dcc report',
+);
 
+tstprefs ("
   loadplugin Mail::SpamAssassin::Plugin::DCC
   dcc_timeout 30
-
 ");
 
 ok sarun ("-t -D info -r < data/spam/gtubedcc.eml 2>&1", \&patterns_run_cb);
@@ -43,6 +40,9 @@ q{ X-Spam-Level: **********}, 'stars',
 
 # run spamc as unpriv uid
 $spamc = "sudo -u nobody $spamc";
+# ensure it is readable by all
+diag "Test will fail if run in directory not accessible by 'nobody' as is typical for a home directory";
+chmod 01755, $workdir;
 
 $SIG{ALRM} = sub { stop_spamd(); die "timed out"; };
 alarm 60;
index c53ac2d145f044c88e5ae76aeb891d9936000543..95299cb93fab834afe3228553586e00fc50a4be4 100755 (executable)
@@ -1,11 +1,9 @@
 #!/usr/bin/perl -T
 
-# run with:   sudo prove -v t/root_spamd*
-
 use lib '.'; use lib 't';
 use SATest; sa_t_init("root_spamd_virtual");
 
-use constant HAS_SUDO => eval { $_ = untaint_cmd("which sudo 2>/dev/null"); chomp; -x };
+use constant HAS_SUDO => $RUNNING_ON_WINDOWS || eval { $_ = untaint_cmd("which sudo 2>/dev/null"); chomp; -x };
 
 use Test::More;
 plan skip_all => "root tests disabled" unless conf_bool('run_root_tests');
@@ -16,33 +14,34 @@ plan tests => 14;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ Return-Path: sb55sb55@yahoo.com}, 'firstline',
-q{ Subject: There yours for FREE!}, 'subj',
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-q{ X-Spam-Level: **********}, 'stars',
-q{ TEST_ENDSNUMS}, 'endsinnums',
-q{ TEST_NOREALNAME}, 'noreal',
-q{ This must be the very last line}, 'lastline',
-
+  q{ Return-Path: sb55sb55@yahoo.com}, 'firstline',
+  q{ Subject: There yours for FREE!}, 'subj',
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
+  q{ X-Spam-Level: **********}, 'stars',
+  q{ TEST_ENDSNUMS}, 'endsinnums',
+  q{ TEST_NOREALNAME}, 'noreal',
+  q{ This must be the very last line}, 'lastline',
 );
 
 # run spamc as unpriv uid
 $spamc = "sudo -u nobody $spamc";
+# ensure it is readable by all
+diag "Test will fail if run in directory not accessible by 'nobody' as is typical for a home directory";
+chmod 01755, $workdir;
 
-ok (start_spamd ("--virtual-config-dir=log/virtualconfig/%u -L -u nobody"));
+ok (start_spamd ("--virtual-config-dir=$workdir/virtualconfig/%u -L -u nobody"));
 
 ok(spamcrun("< data/spam/001", \&patterns_run_cb));
 ok_all_patterns();
 
 %patterns = (
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-             );
-
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
+);
 
 ok (spamcrun("< data/spam/018", \&patterns_run_cb));
 ok_all_patterns();
 
 ok(stop_spamd());
+
old mode 100644 (file)
new mode 100755 (executable)
index acb8aec..59327ce
@@ -3,7 +3,7 @@
 use lib '.'; use lib 't';
 use SATest; sa_t_init("root_spamd_x");
 
-use constant HAS_SUDO => eval { $_ = untaint_cmd("which sudo 2>/dev/null"); chomp; -x };
+use constant HAS_SUDO => $RUNNING_ON_WINDOWS || eval { $_ = untaint_cmd("which sudo 2>/dev/null"); chomp; -x };
 
 use Test::More;
 plan skip_all => "root tests disabled" unless conf_bool('run_root_tests');
@@ -14,20 +14,21 @@ plan tests => 14;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ Return-Path: sb55sb55@yahoo.com}, 'firstline',
-q{ Subject: There yours for FREE!}, 'subj',
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-q{ X-Spam-Level: **********}, 'stars',
-q{ TEST_ENDSNUMS}, 'endsinnums',
-q{ TEST_NOREALNAME}, 'noreal',
-q{ This must be the very last line}, 'lastline',
-
+  q{ Return-Path: sb55sb55@yahoo.com}, 'firstline',
+  q{ Subject: There yours for FREE!}, 'subj',
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
+  q{ X-Spam-Level: **********}, 'stars',
+  q{ TEST_ENDSNUMS}, 'endsinnums',
+  q{ TEST_NOREALNAME}, 'noreal',
+  q{ This must be the very last line}, 'lastline',
 );
 
 # run spamc as unpriv uid
 $spamc = "sudo -u nobody $spamc";
+# ensure it is readable by all
+diag "Test will fail if run in directory not accessible by 'nobody' as is typical for a home directory";
+chmod 01755, $workdir;
 
 ok(start_spamd("-L --create-prefs -x"));
 
@@ -35,12 +36,12 @@ ok(spamcrun("< data/spam/001", \&patterns_run_cb));
 ok_all_patterns();
 
 %patterns = (
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-             );
-
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
+);
 
 ok (spamcrun("< data/spam/018", \&patterns_run_cb));
 ok_all_patterns();
 
 ok(stop_spamd());
+
old mode 100644 (file)
new mode 100755 (executable)
index 4d2ccf3..b7aaabd
@@ -3,7 +3,7 @@
 use lib '.'; use lib 't';
 use SATest; sa_t_init("root_spamd_x_paranoid");
 
-use constant HAS_SUDO => eval { $_ = untaint_cmd("which sudo 2>/dev/null"); chomp; -x };
+use constant HAS_SUDO => $RUNNING_ON_WINDOWS || eval { $_ = untaint_cmd("which sudo 2>/dev/null"); chomp; -x };
 
 use Test::More;
 plan skip_all => "root tests disabled" unless conf_bool('run_root_tests');
@@ -14,20 +14,21 @@ plan tests => 14;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ Return-Path: sb55sb55@yahoo.com}, 'firstline',
-q{ Subject: There yours for FREE!}, 'subj',
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-q{ X-Spam-Level: **********}, 'stars',
-q{ TEST_ENDSNUMS}, 'endsinnums',
-q{ TEST_NOREALNAME}, 'noreal',
-q{ This must be the very last line}, 'lastline',
-
+  q{ Return-Path: sb55sb55@yahoo.com}, 'firstline',
+  q{ Subject: There yours for FREE!}, 'subj',
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
+  q{ X-Spam-Level: **********}, 'stars',
+  q{ TEST_ENDSNUMS}, 'endsinnums',
+  q{ TEST_NOREALNAME}, 'noreal',
+  q{ This must be the very last line}, 'lastline',
 );
 
 # run spamc as unpriv uid
 $spamc = "sudo -u nobody $spamc";
+# ensure it is readable by all
+diag "Test will fail if run in directory not accessible by 'nobody' as is typical for a home directory";
+chmod 01755, $workdir;
 
 ok(start_spamd("-L --create-prefs -x --paranoid"));
 
@@ -35,12 +36,12 @@ ok(spamcrun("< data/spam/001", \&patterns_run_cb));
 ok_all_patterns();
 
 %patterns = (
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-             );
-
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
+);
 
 ok (spamcrun("< data/spam/018", \&patterns_run_cb));
 ok_all_patterns();
 
 ok(stop_spamd());
+
index 6c3384b845f40ccb2670062fd243232bd6958844..e94363704b4cb0dc2213731cdc6a5f4ba6d38fca 100755 (executable)
@@ -1,11 +1,9 @@
 #!/usr/bin/perl -T
 
-# run with:   sudo prove -v t/root_spamd*
-
 use lib '.'; use lib 't';
 use SATest; sa_t_init("root_spamd_x_u");
 
-use constant HAS_SUDO => eval { $_ = untaint_cmd("which sudo 2>/dev/null"); chomp; -x };
+use constant HAS_SUDO => $RUNNING_ON_WINDOWS || eval { $_ = untaint_cmd("which sudo 2>/dev/null"); chomp; -x };
 
 use Test::More;
 plan skip_all => "root tests disabled" unless conf_bool('run_root_tests');
@@ -16,20 +14,21 @@ plan tests => 14;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ Return-Path: sb55sb55@yahoo.com}, 'firstline',
-q{ Subject: There yours for FREE!}, 'subj',
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-q{ X-Spam-Level: **********}, 'stars',
-q{ TEST_ENDSNUMS}, 'endsinnums',
-q{ TEST_NOREALNAME}, 'noreal',
-q{ This must be the very last line}, 'lastline',
-
+  q{ Return-Path: sb55sb55@yahoo.com}, 'firstline',
+  q{ Subject: There yours for FREE!}, 'subj',
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
+  q{ X-Spam-Level: **********}, 'stars',
+  q{ TEST_ENDSNUMS}, 'endsinnums',
+  q{ TEST_NOREALNAME}, 'noreal',
+  q{ This must be the very last line}, 'lastline',
 );
 
 # run spamc as unpriv uid
 $spamc = "sudo -u nobody $spamc";
+# ensure it is readable by all
+diag "Test will fail if run in directory not accessible by 'nobody' as is typical for a home directory";
+chmod 01755, $workdir;
 
 ok (start_spamd ("-L -x -u nobody"));
 
@@ -37,12 +36,12 @@ ok(spamcrun("< data/spam/001", \&patterns_run_cb));
 ok_all_patterns();
 
 %patterns = (
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-             );
-
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
+);
 
 ok (spamcrun("< data/spam/018", \&patterns_run_cb));
 ok_all_patterns();
 
 ok(stop_spamd());
+
index bc04dbe4809f750b16f916e55474529212af29cc..d2316c9e6d856243930e6e88c67a6c5d4cf29145 100755 (executable)
 
 use lib '.'; use lib 't';
 use SATest; sa_t_init("rule_multiple");
-use Test::More tests => 21;
+use Test::More tests => 42;
 
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ META_HEADER_RULE }, 'header',
-q{ META_URI_RULE }, 'uri',
-q{ META_BODY_RULE }, 'body',
-q{ META_RAWBODY_RULE }, 'rawbody',
-q{ META_FULL_RULE }, 'full',
-q{ META_META_RULE }, 'meta',
-q{ META_RULE_6 }, 'meta',
-q{ META_EVAL_RULE }, 'eval',
-
-q{ META_HEADER_RULE_MAX }, 'header_max',
-q{ META_URI_RULE_MAX }, 'uri_max',
-q{ META_BODY_RULE_MAX }, 'body_max',
-q{ META_RAWBODY_RULE_MAX }, 'rawbody_max',
-q{ META_FULL_RULE_MAX }, 'full_max',
-
+  q{ 1.0 META_BODY_RULE }, '',
+  q{ 1.0 META_BODY_RULE_MAX }, '',
+  q{ 1.0 META_EVAL_RULE }, '',
+  q{ 1.0 META_FULL_RULE }, '',
+  q{ 1.0 META_FULL_RULE_MAX }, '',
+  q{ 1.0 META_HEADER_RULE }, '',
+  q{ 1.0 META_HEADER_RULE_MAX }, '',
+  q{ 1.0 META_META_RULE }, '',
+  q{ 1.0 META_RAWBODY_RULE }, '',
+  q{ 1.0 META_RAWBODY_RULE_MAX }, '',
+  q{ 1.0 META_RULE_6 }, '',
+  q{ 1.0 META_URI_RULE }, '',
+  q{ 1.0 META_URI_RULE_MAX }, '',
 );
 
 %anti_patterns = (
-
-q{ META_HEADER_RULE_2 }, 'header_2',
-q{ META_BODY_RULE_2 }, 'body_2',
-q{ META_FULL_RULE_2 }, 'full_2',
-
-q{ META_HEADER_RULE_MAX_2 }, 'header_max_2',
-q{ META_URI_RULE_MAX_2 }, 'uri_max_2',
-q{ META_BODY_RULE_MAX_2 }, 'body_max_2',
-q{ META_RAWBODY_RULE_MAX_2 }, 'rawbody_max_2',
-q{ META_FULL_RULE_MAX_2 }, 'full_max_2',
-
+  q{ META_BODY_RULE_2 }, '',
+  q{ META_BODY_RULE_MAX_2 }, '',
+  q{ META_FULL_RULE_2 }, '',
+  q{ META_FULL_RULE_MAX_2 }, '',
+  q{ META_HEADER_RULE_2 }, '',
+  q{ META_HEADER_RULE_MAX_2 }, '',
+  q{ META_RAWBODY_RULE_MAX_2 }, '',
+  q{ META_URI_RULE_MAX_2 }, '',
 );
 
 tstlocalrules ('
+  header HEADER_RULE   Subject =~ /--/
+  tflags HEADER_RULE multiple
+  meta META_HEADER_RULE HEADER_RULE > 1
 
-header HEADER_RULE     Subject =~ /--/
-tflags HEADER_RULE multiple
-meta META_HEADER_RULE HEADER_RULE > 1
+  header HEADER_RULE_2 Subject =~ /--/
+  meta META_HEADER_RULE_2 HEADER_RULE_2 > 1
 
-header HEADER_RULE_2   Subject =~ /--/
-meta META_HEADER_RULE_2 HEADER_RULE_2 > 1
+  body BODY_RULE       /WWW.SUPERSITESCENTRAL.COM/i
+  tflags BODY_RULE     multiple
+  meta META_BODY_RULE BODY_RULE == 3
 
-uri URI_RULE           /WWW.SUPERSITESCENTRAL.COM/i
-tflags URI_RULE        multiple
-meta META_URI_RULE URI_RULE == 3
+  body BODY_RULE_2     /WWW.SUPERSITESCENTRAL.COM/i
+  meta META_BODY_RULE_2 BODY_RULE_2 > 2
 
-body BODY_RULE         /WWW.SUPERSITESCENTRAL.COM/i
-tflags BODY_RULE       multiple
-meta META_BODY_RULE BODY_RULE == 3
+  rawbody RAWBODY_RULE /WWW.SUPERSITESCENTRAL.COM/i
+  tflags RAWBODY_RULE  multiple
+  meta META_RAWBODY_RULE RAWBODY_RULE == 3
 
-rawbody RAWBODY_RULE   /WWW.SUPERSITESCENTRAL.COM/i
-tflags RAWBODY_RULE    multiple
-meta META_RAWBODY_RULE RAWBODY_RULE == 3
+  full FULL_RULE       /WWW.SUPERSITESCENTRAL.COM/i
+  tflags FULL_RULE     multiple
+  meta META_FULL_RULE FULL_RULE == 3
 
-body BODY_RULE_2       /WWW.SUPERSITESCENTRAL.COM/i
-meta META_BODY_RULE_2 BODY_RULE_2 > 2
+  full FULL_RULE_2     /WWW.SUPERSITESCENTRAL.COM/i
+  meta META_FULL_RULE_2 FULL_RULE_2 > 2
 
-full FULL_RULE         /WWW.SUPERSITESCENTRAL.COM/i
-tflags FULL_RULE       multiple
-meta META_FULL_RULE FULL_RULE == 3
+  header HEADER_RULE_MAX       Subject =~ /--/
+  tflags HEADER_RULE_MAX multiple maxhits=2
+  meta META_HEADER_RULE_MAX HEADER_RULE_MAX > 1
 
-full FULL_RULE_2               /WWW.SUPERSITESCENTRAL.COM/i
-meta META_FULL_RULE_2 FULL_RULE_2 > 2
+  header HEADER_RULE_MAX_2     Subject =~ /--/
+  tflags HEADER_RULE_MAX_2 multiple maxhits=1
+  meta META_HEADER_RULE_MAX_2 HEADER_RULE_MAX_2 > 1
 
-header HEADER_RULE_MAX Subject =~ /--/
-tflags HEADER_RULE_MAX multiple maxhits=2
-meta META_HEADER_RULE_MAX HEADER_RULE_MAX > 1
+  body BODY_RULE_MAX   /WWW.SUPERSITESCENTRAL.COM/i
+  tflags BODY_RULE_MAX multiple maxhits=3
+  meta META_BODY_RULE_MAX BODY_RULE_MAX == 3
 
-header HEADER_RULE_MAX_2       Subject =~ /--/
-tflags HEADER_RULE_MAX_2 multiple maxhits=1
-meta META_HEADER_RULE_MAX_2 HEADER_RULE_MAX_2 > 1
+  body BODY_RULE_MAX_2 /WWW.SUPERSITESCENTRAL.COM/i
+  tflags BODY_RULE_MAX_2       multiple maxhits=2
+  meta META_BODY_RULE_MAX_2 BODY_RULE_MAX_2 > 2
 
-uri URI_RULE_MAX       /WWW.SUPERSITESCENTRAL.COM/i
-tflags URI_RULE_MAX    multiple maxhits=2
-meta META_URI_RULE_MAX URI_RULE_MAX > 1
+  rawbody RAWBODY_RULE_MAX     /WWW.SUPERSITESCENTRAL.COM/i
+  tflags RAWBODY_RULE_MAX      multiple maxhits=3
+  meta META_RAWBODY_RULE_MAX RAWBODY_RULE_MAX == 3
 
-uri URI_RULE_MAX_2     /WWW.SUPERSITESCENTRAL.COM/i
-tflags URI_RULE_MAX_2  multiple maxhits=1
-meta META_URI_RULE_MAX_2 URI_RULE_MAX_2 > 1
+  rawbody RAWBODY_RULE_MAX_2   /WWW.SUPERSITESCENTRAL.COM/i
+  tflags RAWBODY_RULE_MAX_2    multiple maxhits=2
+  meta META_RAWBODY_RULE_MAX_2 RAWBODY_RULE_MAX_2 > 2
 
-body BODY_RULE_MAX     /WWW.SUPERSITESCENTRAL.COM/i
-tflags BODY_RULE_MAX   multiple maxhits=3
-meta META_BODY_RULE_MAX BODY_RULE_MAX == 3
+  full FULL_RULE_MAX   /WWW.SUPERSITESCENTRAL.COM/i
+  tflags FULL_RULE_MAX multiple maxhits=3
+  meta META_FULL_RULE_MAX FULL_RULE_MAX == 3
 
-body BODY_RULE_MAX_2   /WWW.SUPERSITESCENTRAL.COM/i
-tflags BODY_RULE_MAX_2 multiple maxhits=2
-meta META_BODY_RULE_MAX_2 BODY_RULE_MAX_2 > 2
+  full FULL_RULE_MAX_2 /WWW.SUPERSITESCENTRAL.COM/i
+  tflags FULL_RULE_MAX_2       multiple maxhits=2
+  meta META_FULL_RULE_MAX_2 FULL_RULE_MAX_2 > 2
 
-rawbody RAWBODY_RULE_MAX       /WWW.SUPERSITESCENTRAL.COM/i
-tflags RAWBODY_RULE_MAX        multiple maxhits=3
-meta META_RAWBODY_RULE_MAX RAWBODY_RULE_MAX == 3
+  # Note that this is supposed to hit 2 times -> 2 unique urls
+  uri URI_RULE         /WWW.SUPERSITESCENTRAL.COM/i
+  tflags URI_RULE      multiple
+  meta META_URI_RULE URI_RULE == 2
 
-rawbody RAWBODY_RULE_MAX_2     /WWW.SUPERSITESCENTRAL.COM/i
-tflags RAWBODY_RULE_MAX_2      multiple maxhits=2
-meta META_RAWBODY_RULE_MAX_2 RAWBODY_RULE_MAX_2 > 2
+  uri URI_RULE_MAX     /WWW.SUPERSITESCENTRAL.COM/i
+  tflags URI_RULE_MAX  multiple maxhits=1
+  meta META_URI_RULE_MAX URI_RULE_MAX == 1
 
-full FULL_RULE_MAX     /WWW.SUPERSITESCENTRAL.COM/i
-tflags FULL_RULE_MAX   multiple maxhits=3
-meta META_FULL_RULE_MAX FULL_RULE_MAX == 3
+  uri URI_RULE_MAX_2   /WWW.SUPERSITESCENTRAL.COM/i
+  tflags URI_RULE_MAX_2        multiple maxhits=1
+  meta META_URI_RULE_MAX_2 URI_RULE_MAX_2 > 1
 
-full FULL_RULE_MAX_2   /WWW.SUPERSITESCENTRAL.COM/i
-tflags FULL_RULE_MAX_2 multiple maxhits=2
-meta META_FULL_RULE_MAX_2 FULL_RULE_MAX_2 > 2
+  meta META_RULE       META_BODY_RULE + META_RAWBODY_RULE
+  meta META_META_RULE  META_RULE == 2
 
-meta META_RULE         META_BODY_RULE + META_RAWBODY_RULE
-meta META_META_RULE    META_RULE == 2
+  meta META_RULE_6     BODY_RULE + RAWBODY_RULE == 6
 
-meta META_RULE_6       META_BODY_RULE + META_RAWBODY_RULE == 6
+  loadplugin myTestPlugin ../../../data/testplugin.pm
+  header EVAL_RULE     eval:check_return_2()
+  meta META_EVAL_RULE  EVAL_RULE > 1
+');
 
-loadplugin myTestPlugin ../../data/testplugin.pm
-header EVAL_RULE       eval:check_return_2()
-meta META_EVAL_RULE    EVAL_RULE > 1
-    ');
+sarun ("-L -t < data/spam/002 2>&1", \&patterns_run_cb);
+ok_all_patterns();
 
-sarun ("-L -t < data/spam/002", \&patterns_run_cb);
+# do some tests without any other rules to check meta bugs
+clear_localrules();
+sarun ("-L -t < data/spam/002 2>&1", \&patterns_run_cb);
 ok_all_patterns();
+
index 7a9147450719f032f6e3dcfe6734a37062e47d4e..9d71ba24ab20c04954c20fd3aacca130b5c3a6d4 100755 (executable)
@@ -1,20 +1,5 @@
 #!/usr/bin/perl -w -T
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_names.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
-
 use lib '.'; use lib 't';
 use SATest; sa_t_init("rule_names");
 
@@ -22,8 +7,8 @@ use strict;
 use Mail::SpamAssassin;
 
 BEGIN {
-  eval { require Digest::SHA; import Digest::SHA qw(sha1); 1 }
-  or do { require Digest::SHA1; import Digest::SHA1 qw(sha1) }
+  eval { require Digest::SHA; Digest::SHA->import(qw(sha1)); 1 }
+  or do { require Digest::SHA1; Digest::SHA1->import(qw(sha1)) }
 }
 
 our $RUN_THIS_TEST;
@@ -49,7 +34,7 @@ while (my ($test, $type) = each %{ $sa->{conf}->{test_types} }) {
 }
 
 # run tests
-my $mail = 'log/rule_names.eml';
+my $mail = "$workdir/rule_names.eml";
 write_mail();
 %patterns = ();
 my $i = 1;
@@ -77,12 +62,12 @@ for my $test (@tests) {
 
 
 tstprefs ("
-       # set super low threshold, so always marked as spam
-       required_score -10000.0
-       # add two fake lexically high tests so every other hit will always be
-       # followed by a comma in the X-Spam-Status header
-       body ZZZZZZZZ /./
-       body zzzzzzzz /./
+  # set super low threshold, so always marked as spam
+  required_score -10000.0
+  # add two fake lexically high tests so every other hit will always be
+  # followed by a comma in the X-Spam-Status header
+  body ZZZZZZZZ /./
+  body zzzzzzzz /./
 ");
 sarun ("-L < $mail", \&patterns_run_cb);
 ok_all_patterns();
@@ -141,3 +126,4 @@ sub sha1_shuffle {
          map { [$_, sha1($_ . $i)] }
          @_;
 }
+
index c698619fb0e7697db673dd1213e8f0583d3157a2..3bd616195e6f3c6aa129a98f9ddcba57fe4c9684 100755 (executable)
@@ -25,17 +25,22 @@ q{ RELAYS }, 'RELAYS',
 # for the commandline scanner).   Try to exercise some of the
 # different rule types we support, header-name macros etc. (TODO: all ;)
 #
-tstprefs ('
+tstlocalrules ('
+  header LAST_RCVD_LINE  Received =~ /www.fasttrec.com/
 
-header LAST_RCVD_LINE  Received =~ /www.fasttrec.com/
-header MESSAGEID_MATCH MESSAGEID =~ /fasttrec.com/
-header ENV_FROM                EnvelopeFrom =~ /jm.netnoteinc.com/
-body SUBJ_IN_BODY      /YOUR BRAND NEW HOUSE/
-uri URI_RULE           /WWW.SUPERSITESCENTRAL.COM/i
-body BODY_LINE_WRAP    /making obscene amounts of money from the/
-header RELAYS          X-Spam-Relays-Untrusted =~ / helo=www.fasttrec.com /
+  header MESSAGEID_MATCH  MESSAGEID =~ /fasttrec.com/
 
-    ');
+  header ENV_FROM  EnvelopeFrom =~ /jm.netnoteinc.com/
+
+  body SUBJ_IN_BODY  /YOUR BRAND NEW HOUSE/
+
+  uri URI_RULE  /WWW.SUPERSITESCENTRAL.COM/i
+
+  body BODY_LINE_WRAP  /making obscene amounts of money from the/
+
+  header RELAYS  X-Spam-Relays-Untrusted =~ / helo=www.fasttrec.com /
+');
 
 sarun ("-L -t < data/spam/002", \&patterns_run_cb);
 ok_all_patterns();
+
index 34b68914bff2ab4dbb8513592e346dbd76d84f50..7bc40487f80fd3f2cb130ae9566fdf991533e469 100755 (executable)
@@ -12,16 +12,15 @@ use Test::More tests => 1;
 );
 
 tstprefs ("
-        $default_cf_lines
-        auto_whitelist_path ./log/awltest
-        auto_whitelist_file_mode 0755
+  auto_whitelist_path ./$userstate/awltest
+  auto_whitelist_file_mode 0755
 ");
 
 sarun("--add-addr-to-whitelist whitelist_test\@whitelist.spamassassin.taint.org",
       \&patterns_run_cb);
 
-untaint_system("pwd");
-saawlrun("--clean --min 9999 ./log/awltest");
+print cwd() . "\n";
+saawlrun("--clean --min 9999 ./$userstate/awltest");
 
 sarun ("-L -t < data/spam/004", \&patterns_run_cb);
 ok_all_patterns();
diff --git a/upstream/t/sa_awl_welcome_block.t b/upstream/t/sa_awl_welcome_block.t
new file mode 100755 (executable)
index 0000000..043550e
--- /dev/null
@@ -0,0 +1,27 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("sa_awl");
+
+use Test::More tests => 1;
+
+# ---------------------------------------------------------------------------
+
+%patterns = (
+  q{ X-Spam-Status: Yes}, 'isspam',
+);
+
+tstprefs ("
+  auto_welcomelist_path ./$userstate/awltest
+  auto_welcomelist_file_mode 0755
+");
+
+sarun("--add-addr-to-welcomelist whitelist_test\@whitelist.spamassassin.taint.org",
+      \&patterns_run_cb);
+
+print cwd() . "\n";
+saawlrun("--clean --min 9999 ./$userstate/awltest");
+
+sarun ("-L -t < data/spam/004", \&patterns_run_cb);
+ok_all_patterns();
+
index a313fa3902e4ec12e1f1ec50d284c5b8352a14a6..92491386ea092314b61d1ba379811800ef11356d 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/perl -T
 
 use lib '.'; use lib 't';
-use SATest; sa_t_init("sa-check_spamd");
+use SATest; sa_t_init("sa_check_spamd");
 
 use Test::More;
 plan skip_all => "Spamd tests disabled" if $SKIP_SPAMD_TESTS;
@@ -10,10 +10,8 @@ plan tests => 7;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
 );
 
 ok(start_spamd("-L"));
@@ -29,3 +27,4 @@ ok(stop_spamd());
 
 sacheckspamdrun("--hostname $spamdhost --port $p --verbose");
 ok (($? >> 8) != 0);
+
old mode 100644 (file)
new mode 100755 (executable)
index 5f6b531..b02e4ef
@@ -1,16 +1,14 @@
 #!/usr/bin/perl -T
 
-use lib '.'; 
-use lib 't';
+###
+### UTF-8 CONTENT, edit with UTF-8 locale/editor
+###
 
+use lib '.'; use lib 't';
 $ENV{'TEST_PERL_TAINT'} = 'no';     # inhibit for this test
-use SATest; 
-
-sa_t_init("sa_compile");
+use SATest; sa_t_init("sa_compile");
 
 use Config;
-use File::Basename;
-use File::Path qw/mkpath/;
 
 my $temp_binpath = $Config{sitebinexp};
 $temp_binpath =~ s|^\Q$Config{siteprefixexp}\E/||;
@@ -19,25 +17,16 @@ use Test::More;
 plan skip_all => "Long running tests disabled" unless conf_bool('run_long_tests');
 plan skip_all => "Tests don't work on windows" if $RUNNING_ON_WINDOWS;
 plan skip_all => "RE2C isn't new enough" unless re2c_version_new_enough();
-plan tests => 5;
-
-BEGIN {
-  if (-e 't/test_dir') {
-    chdir 't';
-  }
-  if (-e 'test_dir') {
-    unshift(@INC, '../blib/lib');
-  }
-}
+plan tests => 24;
 
 # -------------------------------------------------------------------
 
 use Cwd;
 my $cwd = getcwd;
-my $builddir = "$cwd/log/d.$testname/build";
-my $instbase = "$cwd/log/d.$testname/inst";
-untaint_system("rm -rf $instbase $builddir");
-untaint_system("mkdir -p $instbase $builddir");
+my $builddir = untaint_var("$cwd/$workdir/d.$testname/build");
+my $instbase = untaint_var("$cwd/$workdir/d.$testname/inst");
+rmtree("$instbase", "$builddir", { safe => 1 });
+mkpath("$instbase", "$builddir", { error  => \my $err_list });
 
 untaint_system("cd .. && make tardist >/dev/null");
 $? == 0  or die "tardist failed: $?";
@@ -54,44 +43,77 @@ system_or_die "cd $builddir && mv Mail-SpamAssassin-* x";
 $scr = "$instdir/$temp_binpath/spamassassin";
 $scr_localrules_args = $scr_cf_args = "";      # use the default rules dir, from our "install"
 
-&set_rules("body FOO /You have been selected to receive/");
+&set_rules('
+body FOO1 /You have been selected to receive/
+body FOO2 /You have bee[n] selected to receive/
+body FOO3 /You have bee(?:xyz|\x6e) selected to receive/
+body FOO4 /./
+body FOO5 /金融機/
+body FOO6 /金融(?:xyz|機)/
+body FOO7 /\xe9\x87\x91\xe8\x9e\x8d\xe6\xa9\x9f/
+body FOO8 /.\x87(?:\x91|\x00)[\xe8\x00]\x9e\x8d\xe6\xa9\x9f/
+# Test that meta rules work for sa-compiled body rules
+# (loosely related to Bug 7987)
+meta META1 FOO1 && FOO2 && FOO3 && FOO4
+meta META2 FOO5 && FOO6 && FOO7 && FOO8
+');
 
 # ensure we don't use compiled rules
-untaint_system("rm -rf $instdir/var/spamassassin/compiled");
+rmtree("$instdir/var/spamassassin/compiled", { safe => 1 });
 
 %patterns = (
-
-  q{ check: tests=FOO }, 'FOO'
-
+  qr/ check: tests=FOO1,FOO2,FOO3,FOO4,META1\n/, '',
 );
-
-print "\nRunning spam checks uncompiled\n";
-ok sarun ("-D -Lt < $cwd/data/spam/001 2>&1", \&patterns_run_cb);
+%anti_patterns = (
+  'zoom: able to use', '',
+);
+ok sarun ("-D check,zoom -L -t --cf 'normalize_charset 1' < $cwd/data/spam/001 2>&1", \&patterns_run_cb);
+ok_all_patterns();
+ok sarun ("-D check,zoom -L -t --cf 'normalize_charset 0' < $cwd/data/spam/001 2>&1", \&patterns_run_cb);
 ok_all_patterns();
 
-clear_pattern_counters();
-
-# -------------------------------------------------------------------
-
-print "\nRunning spam checks compiled\n";
-untaint_system "rm -rf \$HOME/.spamassassin/sa-compile.cache"; # reset test
-system_or_die "$instdir/$temp_binpath/sa-compile --keep-tmps 2>&1";  # --debug
 %patterns = (
+  qr/ check: tests=FOO4,FOO5,FOO6,FOO7,FOO8,META2\n/, '',
+);
+%anti_patterns = (
+  'zoom: able to use', '',
+);
+ok sarun ("-D check,zoom -L -t --cf 'normalize_charset 1' < $cwd/data/spam/unicode1 2>&1", \&patterns_run_cb);
+ok_all_patterns();
+ok sarun ("-D check,zoom -L -t --cf 'normalize_charset 0' < $cwd/data/spam/unicode1 2>&1", \&patterns_run_cb);
+ok_all_patterns();
 
-  q{ able to use 1/1 'body_0' compiled rules }, 'able-to-use',
-  q{ check: tests=FOO }, 'FOO'
+# -------------------------------------------------------------------
 
-);
+rmtree( glob "~/.spamassassin/sa-compile.cache". { safe => 1 }); # reset test
+system_or_die "TMP=$instdir TMPDIR=$instdir $instdir/$temp_binpath/sa-compile --quiet -p $cwd/$workdir/user.cf --keep-tmps -D 2>$instdir/sa-compile.debug";  # --debug
 $scr = "$instdir/$temp_binpath/spamassassin";
 $scr_localrules_args = $scr_cf_args = "";      # use the default rules dir, from our "install"
 
-ok sarun ("-D -Lt < $cwd/data/spam/001 2>&1", \&patterns_run_cb);
+%patterns = (
+  ' zoom: able to use 5/5 \'body_0\' compiled rules ', '',
+  qr/ check: tests=FOO1,FOO2,FOO3,FOO4,META1\n/, '',
+);
+%anti_patterns = ();
+ok sarun ("-D check,zoom -L -t --cf 'normalize_charset 1' < $cwd/data/spam/001 2>&1", \&patterns_run_cb);
+ok_all_patterns();
+ok sarun ("-D check,zoom -L -t --cf 'normalize_charset 0' < $cwd/data/spam/001 2>&1", \&patterns_run_cb);
+ok_all_patterns();
+
+%patterns = (
+  ' zoom: able to use 5/5 \'body_0\' compiled rules ', '',
+  qr/ check: tests=FOO4,FOO5,FOO6,FOO7,FOO8,META2\n/, '',
+);
+%anti_patterns = ();
+ok sarun ("-D check,zoom -L -t --cf 'normalize_charset 1' < $cwd/data/spam/unicode1 2>&1", \&patterns_run_cb);
+ok_all_patterns();
+ok sarun ("-D check,zoom -L -t --cf 'normalize_charset 0' < $cwd/data/spam/unicode1 2>&1", \&patterns_run_cb);
 ok_all_patterns();
 
 # -------------------------------------------------------------------
 
 # Cleanup after testing (todo, sa-compile should have option for userstatedir)
-untaint_system "rm -rf \$HOME/.spamassassin/sa-compile.cache";
+rmtree( glob "~/.spamassassin/sa-compile.cache". { safe => 1 }); # reset test
 
 # -------------------------------------------------------------------
 
@@ -113,7 +135,8 @@ sub re2c_version_new_enough {
 sub new_instdir {
   $instdir = untaint_var($instbase.".".(shift));
   print "\nsetting new instdir: $instdir\n";
-  untaint_system("rm -rf $instdir; mkdir $instdir");
+  rmtree("$instdir", { safe => 1 });
+  mkpath($instdir, { error => \my $listerrs });
 }
 
 sub run_makefile_pl {
@@ -143,13 +166,8 @@ sub set_rules {
 
   open RULES, ">$file"
           or die "cannot write $file - $!";
-  print RULES qq{
-
-    use_bayes 0
-
-    $rules
-
-  };
+  print RULES "use_bayes 0";
+  print RULES $rules;
   close RULES or die;
 
   #Create the dir for the pre file
@@ -181,3 +199,4 @@ sub set_rules {
   };
   close RULES or die;
 }
+
index 84ccc3bc7ad042bebdc9af975038357ec1c56888..d1b3d94e3225565c99cb41a81e44bac137daefe9 100755 (executable)
@@ -1,28 +1,11 @@
 #!/usr/bin/perl -w -T
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_tests.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
-
 use strict;
+use lib '.'; use lib 't';
+use SATest; sa_t_init("sha1");
 
 use Mail::SpamAssassin;
-
-BEGIN {
-  eval { require Digest::SHA; import Digest::SHA qw(sha1_hex); 1 }
-  or do { require Digest::SHA1; import Digest::SHA1 qw(sha1_hex) }
-}
+use Digest::SHA qw(sha1_hex);
 
 use Test::More tests => 15;
 
@@ -30,7 +13,7 @@ sub try {
   my ($data, $want) = @_;
 
   if ($want ne sha1_hex($data)) {
-    print "Digest::SHA(1) sha1 mismatch\n";
+    print "Digest::SHA sha1 mismatch\n";
     return 0;
   }
   return 1;
old mode 100644 (file)
new mode 100755 (executable)
index 3173cff..7115f17
@@ -8,16 +8,18 @@ use Test::More tests => 18;
 # ---------------------------------------------------------------------------
 
 %anti_patterns = (
-q{ autolearn=ham } => 'autolearned as ham'
+  q{ autolearn=ham } => 'autolearned as ham'
 );
 
 tstpre ('
-
   loadplugin Mail::SpamAssassin::Plugin::Shortcircuit
-
 ');
+
 tstlocalrules ('
 
+  header SHORTCIRCUIT             eval:check_shortcircuit()
+  describe SHORTCIRCUIT           Not all rules were run, due to a shortcircuited rule
+  tflags SHORTCIRCUIT             userconf noautolearn
   add_header all Status "_YESNO_, score=_SCORE_ required=_REQD_ tests=_TESTS_ shortcircuit=_SCTYPE_ autolearn=_AUTOLEARN_ version=_VERSION_"
 
   # hits spam/001
@@ -41,29 +43,29 @@ tstlocalrules ('
 ');
 
 %patterns = (
-  q{ SC_PRI_SPAM_001 }, 'hit',
-  q{ shortcircuit=spam }, 'sc',
-  q{ X-Spam-Status: Yes, score=103.0 required=5.0 }, 'shortcircuit_spam_score',
-  q{ 100 SHORTCIRCUIT Not all rules were run }, 'shortcircuit rule desc',
-
+  ' 1.0 SC_PRI_SPAM_001 ', 'hit',
+  'shortcircuit=spam', 'sc',
+  qr/X-Spam-Status: Yes/m, 'shortcircuit_spam_header',
+  ' 100 SHORTCIRCUIT Not all rules were run', 'shortcircuit rule desc',
 );
 ok (sarun ("-L -t < data/spam/001", \&patterns_run_cb));
 ok_all_patterns();
 
 %patterns = (
-  q{ SC_002 }, 'hit',
-  q{ shortcircuit=spam }, 'sc',
-  q{ X-Spam-Status: Yes, score=50.0 required=5.0 }, 'SC_002 score',
-  q{ 0.0 SHORTCIRCUIT Not all rules were run }, 'shortcircuit rule desc',
+  ' 50 SC_002 ', 'hit',
+  'shortcircuit=spam', 'sc',
+  qr/^X-Spam-Status: Yes/m, 'SC_002 header',
+  ' 0.0 SHORTCIRCUIT Not all rules were run', 'shortcircuit rule desc',
 );
 ok (sarun ("-L -t < data/spam/002", \&patterns_run_cb));
 ok_all_patterns();
 
 %patterns = (
-  q{ SC_HAM_001 }, 'SC_HAM_001',
-  q{ shortcircuit=ham }, 'sc_ham',
-  q{ X-Spam-Status: No, score=-101.0 required=5.0 }, 'SC_HAM_001 score',
-  q{ -100 SHORTCIRCUIT Not all rules were run }, 'shortcircuit rule desc',
+  ' -1.0 SC_HAM_001 ', 'SC_HAM_001',
+  'shortcircuit=ham', 'sc_ham',
+  qr/^X-Spam-Status: No/m, 'SC_HAM_001 header',
+  ' -100 SHORTCIRCUIT Not all rules were run', 'shortcircuit rule desc',
 );
 ok (sarun ("-L -t < data/nice/001", \&patterns_run_cb));
 ok_all_patterns();
+
diff --git a/upstream/t/shortcircuit_before_dns.t b/upstream/t/shortcircuit_before_dns.t
new file mode 100755 (executable)
index 0000000..38adff2
--- /dev/null
@@ -0,0 +1,69 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("shortcircuit_before_dns");
+
+use Test::More;
+plan skip_all => "Net tests disabled" unless conf_bool('run_net_tests');
+plan skip_all => "Can't use Net::DNS Safely" unless can_use_net_dns_safely();
+plan tests => 5;
+
+# ---------------------------------------------------------------------------
+
+%patterns = (
+ q{ 1.0 SC_TEST_NO_DNS } => '',
+);
+
+%anti_patterns = (
+ q{ DNSBL_TEST_TOP } => '',
+ 'dns: bgsend' => '',
+);
+
+
+my $conf = "
+
+  loadplugin Mail::SpamAssassin::Plugin::Shortcircuit
+
+  rbl_timeout 60
+
+  clear_trusted_networks
+  trusted_networks 127.
+  trusted_networks 10.
+  trusted_networks 150.51.53.1
+
+  header DNSBL_TEST_TOP eval:check_rbl('test', 'dnsbltest.spamassassin.org.')
+  tflags DNSBL_TEST_TOP net
+
+  # No DNS lookups are supposed to start before priority -100,
+  # so our shortcircuit is at -101 ..
+
+  body SC_TEST_NO_DNS /./
+  priority SC_TEST_NO_DNS -101
+  shortcircuit SC_TEST_NO_DNS on
+
+";
+
+tstprefs($conf);
+
+# we need -D output for patterns
+sarun ("-D dns,async -t < data/spam/dnsbl.eml 2>&1", \&patterns_run_cb);
+ok_all_patterns();
+clear_pattern_counters();
+
+#
+# Try again, this time we want to see DNS
+#
+
+# Should see DNS at -100
+$conf =~ s/SC_TEST_NO_DNS -101/SC_TEST_NO_DNS -100/;
+
+%patterns = (
+ q{ 1.0 SC_TEST_NO_DNS } => '',
+ 'dns: bgsend' => '',
+);
+%anti_patterns = ();
+
+tstprefs($conf);
+sarun ("-D dns -t < data/spam/dnsbl.eml 2>&1", \&patterns_run_cb);
+ok_all_patterns();
+
index 59d6e04ef4b8ce3dfce7c436f14105726bbde95e..953006fa5352948d3f1d5e1fb8de3d829155d210 100755 (executable)
@@ -8,15 +8,14 @@ use Test::More tests => 7;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ Subject: There yours for FREE!}, 'subj',
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-q{ X-Spam-Level: **********}, 'stars',
-q{ TEST_ENDSNUMS }, 'endsinnums',
-q{ TEST_NOREALNAME }, 'noreal',
-
+  q{ Subject: There yours for FREE!}, 'subj',
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
+  q{ X-Spam-Level: **********}, 'stars',
+  q{ TEST_ENDSNUMS }, 'endsinnums',
+  q{ TEST_NOREALNAME }, 'noreal',
 );
 
 ok (sarun ("-L -t < data/spam/001", \&patterns_run_cb));
 ok_all_patterns();
+
index bef40422935fafba810c0ee51f3d9c6682927f79..360642ee5cee0355176827b3470e2fe3cfd673b7 100755 (executable)
@@ -10,9 +10,7 @@ plan tests => 2;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ hello world }, 'spamc',
-
+  'hello world', 'spamc', 
 );
 
 # connect on port 9 (discard): should always fail
diff --git a/upstream/t/spamc_H.t b/upstream/t/spamc_H.t
new file mode 100755 (executable)
index 0000000..39a0042
--- /dev/null
@@ -0,0 +1,26 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("spamc_H");
+
+use Test::More;
+plan skip_all => "Spamd tests disabled" if $SKIP_SPAMD_TESTS;
+plan skip_all => "Net tests disabled" unless conf_bool('run_net_tests');
+plan skip_all => "Spam host is not loopback" if $spamdhost ne '127.0.0.1';
+plan tests => 5;
+
+# ---------------------------------------------------------------------------
+
+%patterns = (
+  q{ X-Spam-Flag: YES}, 'flag',
+  q{ TEST_ENDSNUMS}, 'endsinnums',
+);
+
+ok(start_spamd("-L"));
+
+$spamdhost = 'multihomed.dnsbltest.spamassassin.org';
+ok(spamcrun("--connect-retries=100 -H < data/spam/001",
+            \&patterns_run_cb));
+ok_all_patterns();
+ok(stop_spamd());
+
index 3263762042ae4f34581f44899d355f107d26b417..331d32b46615c236a63806aadee9e40c618dcf9d 100755 (executable)
@@ -12,9 +12,7 @@ plan tests => 2;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ TO GET THE EVOLUTION PREVIEW RELEASE }, 'evolution',
-
+  q{ TO GET THE EVOLUTION PREVIEW RELEASE }, 'evolution',
 );
 
 # connect on port 9 (discard): should always fail.
index 3303c7c9decc758ae07ea32c9e01107b9cd43c9d..b55c184bde15c8015e548c1492464e84c50519f4 100755 (executable)
@@ -10,23 +10,19 @@ plan tests => 4;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ Subject: There yours for FREE!}, 'subj',
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-
-
+  q{ Subject: There yours for FREE!}, 'subj',
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
 );
 
-my $sockpath = mk_safe_tmpdir()."/spamd.sock";
+my $sockpath = mk_socket_tempdir()."/spamd.sock";
 start_spamd("-D -L --socketpath=$sockpath");
 
-open (OUT, ">log/spamc_cf.cf");
+open (OUT, ">$workdir/spamc_cf.cf");
 print OUT "-U $sockpath\n";
 close OUT;
 
-ok (spamcrun ("-F log/spamc_cf.cf < data/spam/001", \&patterns_run_cb));
+ok (spamcrun ("-F $workdir/spamc_cf.cf < data/spam/001", \&patterns_run_cb));
 ok_all_patterns();
 stop_spamd();
-cleanup_safe_tmpdir();
 
index f80cbcd4ca420e9cf987fa390b698f95666ea13b..aea7bc77052bc3936312fd9b409be33f3aeebeea 100755 (executable)
@@ -10,16 +10,13 @@ plan tests => 5;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-  q{ Message-Id: <78w08.t365th3y6x7h@yahoo.com> } => 'msgid',
-  q{ X-Spam-Status: Yes, } => 'xss',
-  q{ TEST_NOREALNAME}, 'noreal',
-  q{ subscription cancelable at anytime } => 'body',
-
+  qr/^Message-Id: <78w08\.t365th3y6x7h\@yahoo\.com>/m => 'msgid',
+  qr/^X-Spam-Status: Yes/m => 'xss',
+  'TEST_NOREALNAME', 'noreal',
+  'subscription cancelable at anytime' => 'body',
 );
 
 %anti_patterns = (
-
 );
 
 start_spamd("-L --cf='report_safe 0'");
index 2610da7dd40d7981015d8886748253cc22892f9c..836564d1b314cb4bd73e7d6c279fed3a5e6f5164 100755 (executable)
@@ -7,18 +7,14 @@ use Test::More;
 plan skip_all => "No SPAMC exe" if $NO_SPAMC_EXE;
 plan tests => 4;
 
-diag("NOTE: Failure might be because some other process is running on port 8.  Test assumes nothing is listening on port 8.");
-
 # ---------------------------------------------------------------------------
 
 my $errmsg = ($RUNNING_ON_WINDOWS?"10061":"Connection refused");
 
 %patterns = (
-
-q{ hello world }, 'spamc_l',
-q{ spamc: connect to spamd on }, 'connfailed_a',
-q{ failed, retrying (#1 of 3): } . $errmsg, 'connfailed_b',
-
+  q{ hello world }, 'spamc_l',
+  q{ spamc: connect to spamd on }, 'connfailed_a',
+  q{ failed, retrying (#1 of 3): } . $errmsg, 'connfailed_b',
 );
 
 # connect on port 8 (unassigned): should always fail
index 4c10b67da99fa9dfca2467668a6c5c373796c872..21bade9cb643b7cf5f9772a916b6fe4de85a7178 100755 (executable)
@@ -9,10 +9,12 @@ plan tests => 9;
 
 # ---------------------------------------------------------------------------
 
-tstlocalrules ("
-       loadplugin reporterplugin ../../data/reporterplugin.pm
+tstprefs ("
+  loadplugin reporterplugin ../../../data/reporterplugin.pm
 ");
 
+unlink "log/rptfail";
+
 start_spamd("-L --allow-tell");
 
 %patterns = ( 'Message successfully reported/revoked' => 'reported spam' );
@@ -41,4 +43,5 @@ ok_all_patterns();
 
 stop_spamd();
 
-ok(unlink 'log/rptfail'); # need a little cleanup
+ok(unlink "log/rptfail"); # need a little cleanup
+
index 223b7e83054c88e646bc955c2d9a05612c627849..3f9f94927dbe698f7adedba21305ab8ed2bf456b 100755 (executable)
@@ -2,6 +2,7 @@
 
 use lib '.'; use lib 't';
 use SATest; sa_t_init("spamc_optL");
+
 use constant HAS_SDBM_FILE => eval { require SDBM_File; };
 
 use Test::More;
@@ -11,8 +12,8 @@ plan tests => 18;
 
 # ---------------------------------------------------------------------------
 
-tstlocalrules ("
-        bayes_store_module Mail::SpamAssassin::BayesStore::SDBM
+tstprefs ("
+  bayes_store_module Mail::SpamAssassin::BayesStore::SDBM
 ");
 
 start_spamd("-L --allow-tell");
@@ -55,3 +56,4 @@ ok (spamcrun ("-L forget < data/nice/001", \&patterns_run_cb));
 ok_all_patterns();
 
 stop_spamd();
+
index cdd34d120ca081ff79b209a2b051cc56a29a2041..7378761532f1614e794127017ff0493a94c9ac83 100755 (executable)
@@ -47,10 +47,10 @@ stop_spamd(); # just to be sure
 # max-size of 512 bytes; EX_TOOBIG, pass through message despite -x
 
 %patterns = (
-  q{ Subject: There yours for FREE!}, 'subj',
+  'Subject: There yours for FREE!', 'subj',
 );
 %anti_patterns = (
-  q{ X-Spam-Flag: }, 'flag',
+  'X-Spam-Flag:', 'flag',
 );
 
 # this should have exit code == 0, and pass through the full
@@ -66,11 +66,11 @@ ok(scrun("-s 512 -x -E < data/spam/001", \&patterns_run_cb));
 ok ok_all_patterns();
 
 %patterns = (
-  q{ 0/0 }, '0/0',
+  '0/0', '0/0',
 );
 %anti_patterns = (
-  q{ Subject: There yours for FREE!}, 'subj',
-  q{ X-Spam-Flag: }, 'flag',
+  'Subject: There yours for FREE!', 'subj',
+  'X-Spam-Flag:', 'flag',
 );
 
 # this should have exit code == 0, and emit "0/0"
@@ -94,8 +94,8 @@ ok(scrun("--connect-retries 1 -R < data/spam/001", \&patterns_run_cb));
 # we do not want to see the output with -x on error
 %patterns = ();
 %anti_patterns = (
-  q{ Subject: There yours for FREE!}, 'subj',
-  q{ X-Spam-Flag: YES}, 'flag',
+  'Subject: There yours for FREE!', 'subj',
+  'X-Spam-Flag: YES', 'flag',
 );
 
 # this should have exit code != 0
@@ -117,3 +117,4 @@ ok ok_all_patterns();
 clear_pattern_counters();
 ok(scrunwantfail("--connect-retries 1 -x -E < data/spam/001", \&patterns_run_cb));
 ok ok_all_patterns();
+
index 97934a2ff163a5a13c363ee07360f7fc14a72355..5c65d67513e6b86817031051a904c15fa61e0378 100755 (executable)
@@ -6,7 +6,7 @@ use SATest; sa_t_init("spamc_x_e");
 use Test::More;
 plan skip_all => "Spamd tests disabled" if $SKIP_SPAMD_TESTS;
 plan tests => 7;
-diag "Failure may indicate some other process running on port 8.  Test assumes nothing is listening on port 8.";
+
 # ---------------------------------------------------------------------------
 # test case for bug 5478: spamc -x -e
 
index e13585e293a9018cea23ddbd7225edda8971278c..51ce8f28f5b0479cd5242e54ce2235be16ef470c 100755 (executable)
@@ -10,14 +10,11 @@ plan tests => 2;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
 );
 
 %anti_patterns = (
-
   # the text should NOT be output, bug 4991
-  q{ hello world }, 'spamc_y',
-
+  'hello world', 'spamc_y',
 );
 
 # connect on port 8 (unassigned): should always fail
old mode 100644 (file)
new mode 100755 (executable)
index 026bee1..119a64e
@@ -5,29 +5,27 @@ use constant HAVE_ZLIB => eval { require Compress::Zlib; };
 use lib '.'; use lib 't';
 use SATest; sa_t_init("spamc_z");
 
-untaint_system("$spamc -z < /dev/null");
-my $SPAMC_Z_AVAILABLE = ($? >> 8 == 0);
-
 use Test::More;
 plan skip_all => "Spamd tests disabled" if $SKIP_SPAMD_TESTS;
 plan skip_all => "ZLIB REQUIRED" unless HAVE_ZLIB;
+
+untaint_system("$spamc -z < /dev/null");
+my $SPAMC_Z_AVAILABLE = ($? >> 8 == 0);
+
 plan skip_all => "SPAMC Z unavailable" unless $SPAMC_Z_AVAILABLE;
 plan tests => 9;
 
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ Return-Path: sb55sb55@yahoo.com}, 'firstline',
-q{ Subject: There yours for FREE!}, 'subj',
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-q{ X-Spam-Level: **********}, 'stars',
-q{ TEST_ENDSNUMS}, 'endsinnums',
-q{ TEST_NOREALNAME}, 'noreal',
-q{ This must be the very last line}, 'lastline',
-
-
+  q{ Return-Path: sb55sb55@yahoo.com}, 'firstline',
+  q{ Subject: There yours for FREE!}, 'subj',
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
+  q{ X-Spam-Level: **********}, 'stars',
+  q{ TEST_ENDSNUMS}, 'endsinnums',
+  q{ TEST_NOREALNAME}, 'noreal',
+  q{ This must be the very last line}, 'lastline',
 );
 
 ok (sdrun ("-L",
index a636e4014a32d06873a240c10717c28ce4e7e9dd..0c36dc0ff10419a1af16f3209a913833e88e0cf4 100755 (executable)
@@ -10,17 +10,14 @@ plan tests => 14;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ Return-Path: sb55sb55@yahoo.com}, 'firstline',
-q{ Subject: There yours for FREE!}, 'subj',
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-q{ X-Spam-Level: **********}, 'stars',
-q{ TEST_ENDSNUMS}, 'endsinnums',
-q{ TEST_NOREALNAME}, 'noreal',
-q{ This must be the very last line}, 'lastline',
-
-
+  q{ Return-Path: sb55sb55@yahoo.com}, 'firstline',
+  q{ Subject: There yours for FREE!}, 'subj',
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
+  q{ X-Spam-Level: **********}, 'stars',
+  q{ TEST_ENDSNUMS}, 'endsinnums',
+  q{ TEST_NOREALNAME}, 'noreal',
+  q{ This must be the very last line}, 'lastline',
 );
 
 ok(start_spamd("-L"));
@@ -29,12 +26,12 @@ ok(spamcrun("< data/spam/001", \&patterns_run_cb));
 ok_all_patterns();
 
 %patterns = (
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-             );
-
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
+);
 
 ok (spamcrun("< data/spam/018", \&patterns_run_cb));
 ok_all_patterns();
 
 ok(stop_spamd());
+
index 3e752c7e84f56cfc55036519327dab709fadefa1..c8de52d5ed55b8c65c6ebf705905eba5bcfe3c9b 100755 (executable)
@@ -10,23 +10,21 @@ plan tests => 5;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ 1.0 MYFOO }, 'myfoo',
-
+  q{ 1.0 MYFOO }, 'myfoo',
 );
 
 %anti_patterns = (
-q{  redefined at }, 'redefined_errors_in_spamd_log',
+  'redefined at', 'redefined_errors_in_spamd_log',
 );
 
-tstlocalrules ("
-       allow_user_rules 1
-        loadplugin myTestPlugin ../../data/testplugin.pm
+tstprefs ("
+  allow_user_rules 1
+  loadplugin myTestPlugin ../../../data/testplugin.pm
 ");
 
-rmtree ("log/virtualconfig/testuser", 0, 1);
-mkpath ("log/virtualconfig/testuser", 0, 0755);
-open (OUT, ">log/virtualconfig/testuser/user_prefs");
+rmtree ("$workdir/virtualconfig/testuser", 0, 1);
+mkpath ("$workdir/virtualconfig/testuser", 0, 0755);
+open (OUT, ">$workdir/virtualconfig/testuser/user_prefs");
 print OUT q{
 
        header MYFOO Content-Transfer-Encoding =~ /quoted-printable/
@@ -53,7 +51,7 @@ print OUT q{
 };
 close OUT;
 
-ok (start_spamd ("--virtual-config-dir=log/virtualconfig/%u -L -u $spamd_run_as_user"));
+ok (start_spamd ("--virtual-config-dir=$workdir/virtualconfig/%u -L -u $spamd_run_as_user"));
 ok (spamcrun ("-u testuser < data/spam/009", \&patterns_run_cb));
 ok (stop_spamd ());
 
index f4aaa0571a01270bac38b7be21dadeab279907b0..e6e05f428cdc9c708e4b5b7fb5650a45b0eda40f 100755 (executable)
@@ -1,21 +1,5 @@
 #!/usr/bin/perl -T
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_tests.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-    unshift(@INC, '../lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
-
 use lib '.'; use lib 't';
 use SATest; sa_t_init("spamd_client");
 
@@ -47,9 +31,9 @@ my $testmsg = getmessage("data/spam/gtube.eml");
 ok($testmsg);
 
 %patterns = (
-q{ X-Spam-Flag: YES}, 'flag',
-q{ BODY: Generic Test for Unsolicited Bulk Email }, 'gtube',
-q{ XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X }, 'gtube string',
+  qr/^X-Spam-Flag: YES/m, 'flag',
+  q{ 1000 GTUBE }, 'gtube',
+  'XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X', 'gtube string',
 );
 
 ok(start_spamd("-L"));
@@ -82,11 +66,11 @@ ok_all_patterns();
 
 clear_pattern_counters();
 %patterns = (
-q{ X-Spam-Flag: YES}, 'flag',
+qr/^X-Spam-Flag: YES/m, 'flag',
 );
 
 %anti_patterns = (
-q{ XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X }, 'gtube string',
+  'XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X', 'gtube string',
 );
 
 $result = $client->headers($testmsg);
@@ -106,14 +90,14 @@ if (!$RUNNING_ON_WINDOWS) {
   $spamd_already_killed = undef;
 
   %patterns = (
-    q{ X-Spam-Flag: YES}, 'flag',
-    q{ BODY: Generic Test for Unsolicited Bulk Email }, 'gtube',
-    q{ XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X }, 'gtube string',
+    qr/^X-Spam-Flag: YES/m, 'flag',
+    q{ 1000 GTUBE }, 'gtube',
+    'XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X', 'gtube string',
       );
 
   %anti_patterns = ();
 
-  my $sockpath = mk_safe_tmpdir()."/spamd.sock";
+  my $sockpath = mk_socket_tempdir()."/spamd.sock";
   ok(start_spamd("-L --socketpath=$sockpath"));
 
   $client = create_clientobj({
@@ -142,15 +126,15 @@ if (!$RUNNING_ON_WINDOWS) {
   ok_all_patterns();
 
   ok(stop_spamd());
-  cleanup_safe_tmpdir();
 }
 
 if (HAS_SDBM_FILE) {
 
   clear_pattern_counters();
   $spamd_already_killed = undef;
-  tstlocalrules ("
-        bayes_store_module Mail::SpamAssassin::BayesStore::SDBM
+
+  tstprefs ("
+    bayes_store_module Mail::SpamAssassin::BayesStore::SDBM
   ");
 
   ok(start_spamd("-L --allow-tell"));
@@ -218,3 +202,4 @@ sub getmessage {
 
   return $msg;
 }
+
index b65530c9276b9c148590766988c6c1848e2c4ce8..49a248b186bd2c5b75cbaf5fd558b858cd08b2e9 100755 (executable)
@@ -12,17 +12,16 @@ plan tests => 110;
 
 # ---------------------------------------------------------------------------
 
-my $pid_file = "log/spamd.pid";
 my($pid1, $pid2);
 
 dbgprint "Starting spamd...\n";
-start_spamd("-L -r ${pid_file}");
+start_spamd("-L");
 sleep 1;
 
 for $retry (0 .. 9) {
-  ok ($pid1 = read_from_pidfile($pid_file));
-  ok (-e $pid_file) or warn "$pid_file is not there before SIGHUP";
-  ok (!-z $pid_file) or warn "$pid_file is empty before SIGHUP";
+  ok ($pid1 = read_from_pidfile($spamd_pidfile));
+  ok (-e $spamd_pidfile) or warn "$spamd_pidfile is not there before SIGHUP";
+  ok (!-z $spamd_pidfile) or warn "$spamd_pidfile is empty before SIGHUP";
   ok ($pid1 != 0);
   dbgprint "HUPing spamd at pid $pid1, loop try $retry...\n";
 
@@ -31,18 +30,18 @@ for $retry (0 .. 9) {
   # load we could have missed the unlink, exec, create part.
 
   dbgprint "Waiting for PID file to change...\n";
-  wait_for_file_to_change_or_disappear($pid_file, 20, sub {
+  wait_for_file_to_change_or_disappear($spamd_pidfile, 20, sub {
           $pid1 and kill ('HUP', $pid1);
         });
 
   dbgprint "Waiting for spamd at pid $pid1 to restart...\n";
     # 26 iterations is 98 seconds, RPi ARM6 takes about 66 seconds
-  wait_for_file_to_appear ($pid_file, 26);
+  wait_for_file_to_appear ($spamd_pidfile, 26);
 
-  ok (-e $pid_file) or warn "$pid_file does not exist post restart";
-  ok (!-z $pid_file) or warn "$pid_file is empty post restart";
+  ok (-e $spamd_pidfile) or warn "$spamd_pidfile does not exist post restart";
+  ok (!-z $spamd_pidfile) or warn "$spamd_pidfile is empty post restart";
 
-  ok ($pid2 = read_from_pidfile($pid_file));
+  ok ($pid2 = read_from_pidfile($spamd_pidfile));
   dbgprint "Looking for new spamd at pid $pid2...\n";
   #ok ($pid2 != $pid1);     # no longer guaranteed with SIGHUP
   ok ($pid2 != 0 and kill (0, $pid2));
index 539f64c208b90a1d9c9e72bed5c9249c7efb6191..0902ba392daac72c136d98fde5fa7ef926199992 100755 (executable)
@@ -13,21 +13,20 @@ use File::Spec;
 
 # ---------------------------------------------------------------------------
 
-my $pid_file = "log/spamd.pid";
 my($pid1, $pid2);
 
-tstlocalrules("
-    use_auto_whitelist 0
-  ");
+tstprefs("
+  use_auto_whitelist 0
+");
 
 dbgprint "Starting spamd...\n";
-start_spamd("-L -r ${pid_file}");
+start_spamd("-L");
 sleep 1;
 
 for $retry (0 .. 9) {
-  ok ($pid1 = read_from_pidfile($pid_file));
-  ok (-e $pid_file) or warn "$pid_file is not there before SIGINT";
-  ok (!-z $pid_file) or warn "$pid_file is empty before SIGINT";
+  ok ($pid1 = read_from_pidfile($spamd_pidfile));
+  ok (-e $spamd_pidfile) or warn "$spamd_pidfile is not there before SIGINT";
+  ok (!-z $spamd_pidfile) or warn "$spamd_pidfile is empty before SIGINT";
   ok ($pid1 != 0);
   dbgprint "killing spamd at pid $pid1, loop try $retry...\n";
 
@@ -36,25 +35,25 @@ for $retry (0 .. 9) {
   # load we could have missed the unlink, exec, create part.
 
   dbgprint "Waiting for PID file to change...\n";
-  wait_for_file_to_change_or_disappear($pid_file, 20, sub {
+  wait_for_file_to_change_or_disappear($spamd_pidfile, 20, sub {
           $pid1 and kill ('INT', $pid1);
         });
 
   # in the SIGINT case, the file will not change -- it will be unlinked
-  ok (!-e $pid_file);
+  ok (!-e $spamd_pidfile);
 
   # override this so the old logs are still visible and the new
   # spamd will be started even though stop_spamd() was not called
   $spamd_pid = 0;
 
   dbgprint "starting new spamd, loop try $retry...\n";
-  start_spamd("-D -L -r ${pid_file}");
+  start_spamd("-D -L");
 
   dbgprint "Waiting for spamd at pid $pid1 to restart...\n";
-  wait_for_file_to_appear ($pid_file, 20);
-  ok (-e $pid_file) or warn "$pid_file does not exist post restart";
-  ok (!-z $pid_file) or warn "$pid_file is empty post restart";
-  ok ($pid2 = read_from_pidfile($pid_file));
+  wait_for_file_to_appear ($spamd_pidfile, 20);
+  ok (-e $spamd_pidfile) or warn "$spamd_pidfile does not exist post restart";
+  ok (!-z $spamd_pidfile) or warn "$spamd_pidfile is empty post restart";
+  ok ($pid2 = read_from_pidfile($spamd_pidfile));
 
   dbgprint "Looking for new spamd at pid $pid2...\n";
   ok ($pid2 != 0 and kill (0, $pid2));
@@ -77,4 +76,3 @@ for $retry (0 .. 9) {
 dbgprint "Stopping spamd...\n";
 stop_spamd;
 
-
index 13b2d0df33ab1fd85c9ff53c37654e4a3c6d91a9..689e6973988f4c42eaf949a98c7e0a8fc8b58f57 100755 (executable)
@@ -13,21 +13,20 @@ use File::Spec;
 
 # ---------------------------------------------------------------------------
 
-my $pid_file = "log/spamd.pid";
 my($pid1, $pid2);
 
-tstlocalrules("
-      use_auto_whitelist 0
-  ");
+tstprefs("
+  use_auto_whitelist 0
+");
 
 dbgprint "Starting spamd...\n";
-start_spamd("-L --round-robin -r ${pid_file}");
+start_spamd("-L --round-robin");
 sleep 1;
 
 for $retry (0 .. 9) {
-  ok ($pid1 = read_from_pidfile($pid_file));
-  ok (-e $pid_file) or warn "$pid_file is not there before SIGINT";
-  ok (!-z $pid_file) or warn "$pid_file is empty before SIGINT";
+  ok ($pid1 = read_from_pidfile($spamd_pidfile));
+  ok (-e $spamd_pidfile) or warn "$spamd_pidfile is not there before SIGINT";
+  ok (!-z $spamd_pidfile) or warn "$spamd_pidfile is empty before SIGINT";
   ok ($pid1 != 0);
   dbgprint "killing spamd at pid $pid1, loop try $retry...\n";
 
@@ -36,12 +35,12 @@ for $retry (0 .. 9) {
   # load we could have missed the unlink, exec, create part.
 
   dbgprint "Waiting for PID file to change...\n";
-  wait_for_file_to_change_or_disappear($pid_file, 20, sub {
+  wait_for_file_to_change_or_disappear($spamd_pidfile, 20, sub {
           $pid1 and kill ('INT', $pid1);
         });
 
   # in the SIGINT case, the file will not change -- it will be unlinked
-  ok (!-e $pid_file);
+  ok (!-e $spamd_pidfile);
 
   # override this so the old logs are still visible and the new
   # spamd will be started even though stop_spamd() was not called
@@ -49,14 +48,14 @@ for $retry (0 .. 9) {
 
   dbgprint "starting new spamd, loop try $retry...\n";
   my $startat = time;
-  start_spamd("-D -L --round-robin -r ${pid_file}");
+  start_spamd("-D -L --round-robin");
 
   dbgprint "Waiting for spamd at pid $pid1 to restart...\n";
-  wait_for_file_to_appear ($pid_file, 40);
-  ok (-e $pid_file) or warn "$pid_file does not exist post restart; started at $startat, gave up at ".time;
+  wait_for_file_to_appear ($spamd_pidfile, 40);
+  ok (-e $spamd_pidfile) or warn "$spamd_pidfile does not exist post restart; started at $startat, gave up at ".time;
 
-  ok (!-z $pid_file) or warn "$pid_file is empty post restart";
-  ok ($pid2 = read_from_pidfile($pid_file));
+  ok (!-z $spamd_pidfile) or warn "$spamd_pidfile is empty post restart";
+  ok ($pid2 = read_from_pidfile($spamd_pidfile));
 
   dbgprint "Looking for new spamd at pid $pid2...\n";
   ok ($pid2 != 0 and kill (0, $pid2));
@@ -79,4 +78,3 @@ for $retry (0 .. 9) {
 dbgprint "Stopping spamd...\n";
 stop_spamd;
 
-
index 4a44cf2e35ae4920aaa4670dbb07879fcbd57040..ecbfd4f175016efa494ab75d61ec3be1054a2442 100755 (executable)
@@ -12,22 +12,19 @@ plan tests => 8;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ Subject: There yours for FREE!}, 'subj',
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-q{ X-Spam-Level: **********}, 'stars',
-q{ X-Spam-Foo: LDAP read}, 'ldap_config_read',
-q{ TEST_ENDSNUMS}, 'endsinnums',
-q{ TEST_NOREALNAME}, 'noreal',
-
-
+  q{ Subject: There yours for FREE!}, 'subj',
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
+  q{ X-Spam-Level: **********}, 'stars',
+  q{ X-Spam-Foo: LDAP read}, 'ldap_config_read',
+  q{ TEST_ENDSNUMS}, 'endsinnums',
+  q{ TEST_NOREALNAME}, 'noreal',
 );
 
-tstlocalrules ("
-    user_scores_dsn ldap://localhost/o=stooges?spamassassin?sub?uid=__USERNAME__
-    user_scores_ldap_username cn=StoogeAdmin,o=stooges
-    user_scores_ldap_password secret1
+tstprefs ("
+  user_scores_dsn ldap://localhost/o=stooges?spamassassin?sub?uid=__USERNAME__
+  user_scores_ldap_username cn=StoogeAdmin,o=stooges
+  user_scores_ldap_password secret1
 ");
 
 ok (sdrun ("-L --ldap-config", "-u curley < data/spam/001", \&patterns_run_cb));
index ae7461876d5a6b663d89be15330ef0a8013e2c7e..712ae5f1539b581392f58b76600ed91aa320e12f 100755 (executable)
@@ -10,14 +10,11 @@ plan tests => 22;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-q{ X-Spam-Level: **********}, 'stars',
-q{ TEST_ENDSNUMS}, 'endsinnums',
-q{ TEST_NOREALNAME}, 'noreal',
-
-
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
+  q{ X-Spam-Level: **********}, 'stars',
+  q{ TEST_ENDSNUMS}, 'endsinnums',
+  q{ TEST_NOREALNAME}, 'noreal',
 );
 
 start_spamd("-L -m1");
@@ -37,4 +34,3 @@ ok (spamcrun ("< data/spam/001", \&patterns_run_cb));
 ok_all_patterns();
 ok (stop_spamd());
 
-
index 192683eb0914a7d528074da144579fdc4b27d1c9..ae9dcc2285aeceec5c7a3529a453499b755bd15b 100755 (executable)
@@ -13,9 +13,7 @@ plan tests => 1;
 # test for size limit issues like in Bug 5412
 
 %patterns = (
-
-q{ Subject: There yours for FREE! }, 'subj',
-
+  q{ Subject: There yours for FREE! }, 'subj',
 );
 
 sdrun ("-L", "-s 512 < data/spam/001", \&patterns_run_cb);
index e1230e94692b6d7a4d57c0106464929f3e4452d5..2e995b644f147b4190fabd1b3c5af1f34e53eaf4 100755 (executable)
@@ -10,14 +10,11 @@ plan tests => 20;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-q{ X-Spam-Level: **********}, 'stars',
-q{ TEST_ENDSNUMS}, 'endsinnums',
-q{ TEST_NOREALNAME}, 'noreal',
-
-
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
+  q{ X-Spam-Level: **********}, 'stars',
+  q{ TEST_ENDSNUMS}, 'endsinnums',
+  q{ TEST_NOREALNAME}, 'noreal',
 );
 
 start_spamd("-L");
@@ -35,4 +32,3 @@ ok (spamcrun ("< data/spam/001", \&patterns_run_cb));
 ok_all_patterns();
 stop_spamd();
 
-
index 91d6d67ffc2dedfcc2ae67f21e914c517c3b7d78..6fefa1a70d22e2922feb3fd35ee1dc4a18f82482 100755 (executable)
@@ -9,22 +9,23 @@ plan skip_all => "Tests don't work on windows" if $RUNNING_ON_WINDOWS;
 plan skip_all => "UID nobody tests" if $SKIP_SETUID_NOBODY_TESTS;
 plan tests => 6;
 
+diag("NOTE: A rare single failure in this test may be a race condition in the test that can be ignored");
 # ---------------------------------------------------------------------------
 
-tstlocalrules ('
-        loadplugin myTestPlugin ../../data/testplugin.pm
-        header MY_TEST_PLUGIN eval:check_test_plugin()
+tstprefs ('
+  loadplugin myTestPlugin ../../../data/testplugin.pm
+  header MY_TEST_PLUGIN eval:check_test_plugin()
 ');
 
 # create a shared counter file for this test
 use Cwd;
-$ENV{'SPAMD_PLUGIN_COUNTER_FILE'} = getcwd."/log/spamd_plugin.tmp";
-open(COUNTER,">log/spamd_plugin.tmp");
+$ENV{'SPAMD_PLUGIN_COUNTER_FILE'} = getcwd."/$workdir/spamd_plugin.tmp";
+open(COUNTER,">$workdir/spamd_plugin.tmp");
 print COUNTER "0";
 close COUNTER;
-chmod (0666, "log/spamd_plugin.tmp");
+chmod (0666, "$workdir/spamd_plugin.tmp");
 
-my $sockpath = mk_safe_tmpdir()."/spamd.sock";
+my $sockpath = mk_socket_tempdir()."/spamd.sock";
 start_spamd("-D -L --socketpath=$sockpath");
 
 %patterns = (
@@ -50,5 +51,4 @@ checkfile($spamd_stderr, \&patterns_run_cb);
 ok_all_patterns();
 
 stop_spamd();
-cleanup_safe_tmpdir();
 
index 4075c2e6587a4d62441775fa0bafa98cdce349d5..ec152839a6c22312dbf22c114ec2af434e021ed2 100755 (executable)
@@ -10,14 +10,12 @@ plan tests => 4;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ Subject: There yours for FREE!}, 'subj',
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-
-
+  q{ Subject: There yours for FREE!}, 'subj',
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
 );
 
 my $port = probably_unused_spamd_port();
 ok(sdrun ("-L -p $port", "-p $port < data/spam/001", \&patterns_run_cb));
 ok_all_patterns();
+
index fd79eb017cbf207a30fe23ef8f4a2bc10570e3f6..58001d0381a12654cc6d55253b604cd92504eb83 100755 (executable)
@@ -32,14 +32,11 @@ if ($? >> 8 == 0) {
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-q{ X-Spam-Level: **********}, 'stars',
-q{ TEST_ENDSNUMS}, 'endsinnums',
-q{ TEST_NOREALNAME}, 'noreal',
-
-
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
+  q{ X-Spam-Level: **********}, 'stars',
+  q{ TEST_ENDSNUMS}, 'endsinnums',
+  q{ TEST_NOREALNAME}, 'noreal',
 );
 
 start_spamd("-L -m1");
@@ -58,4 +55,3 @@ ok (spamcrun ("< data/spam/001", \&patterns_run_cb));
 ok_all_patterns();
 ok (stop_spamd());
 
-
index af72f05d02f15930b3df21929e2807d27e28559a..3699299d7c89bd59f0bdf2627fb8ad5810c6875d 100755 (executable)
@@ -30,14 +30,11 @@ if ($? >> 8 == 0) {
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-q{ X-Spam-Level: **********}, 'stars',
-q{ TEST_ENDSNUMS}, 'endsinnums',
-q{ TEST_NOREALNAME}, 'noreal',
-
-
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
+  q{ X-Spam-Level: **********}, 'stars',
+  q{ TEST_ENDSNUMS}, 'endsinnums',
+  q{ TEST_NOREALNAME}, 'noreal',
 );
 
 start_spamd("-L -m1 --round-robin");
@@ -57,4 +54,3 @@ ok (spamcrun ("< data/spam/001", \&patterns_run_cb));
 ok_all_patterns();
 ok (stop_spamd());
 
-
index ec6ce6e4b7525a0a4e59a7a948cbf0e39add5041..32cbe2f7cb3166bfb8383052667938e477fba628 100755 (executable)
@@ -4,7 +4,6 @@ use lib '.'; use lib 't';
 use SATest; sa_t_init("spamd_prefork_stress_3");
 
 use Test::More;
-diag("NOTE: this test requires both 'run_spamd_prefork_stress_test' and 'run_long_tests' set to 'y'.");
 
 plan skip_all => "Spamd tests disabled" if $SKIP_SPAMD_TESTS;
 plan skip_all => "Long running tests disabled" unless conf_bool('run_long_tests');
@@ -13,20 +12,17 @@ plan tests => 291;
 
 # ---------------------------------------------------------------------------
 
-tstlocalrules ('
-        loadplugin myTestPlugin ../../data/testplugin.pm
-        header PLUGIN_SLEEP eval:sleep_based_on_header()
+tstprefs ('
+  loadplugin myTestPlugin ../../../data/testplugin.pm
+  header PLUGIN_SLEEP eval:sleep_based_on_header()
 ');
 
-
 %patterns = (
-
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-q{ X-Spam-Level: **********}, 'stars',
-q{ TEST_ENDSNUMS}, 'endsinnums',
-q{ TEST_NOREALNAME}, 'noreal',
-
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
+  q{ X-Spam-Level: **********}, 'stars',
+  q{ TEST_ENDSNUMS}, 'endsinnums',
+  q{ TEST_NOREALNAME}, 'noreal',
 );
 
 my $tmpnum = 0;
@@ -74,7 +70,7 @@ sub test_bg {
 sub mk_mail {
   my $secs = shift;
 
-  my $tmpf = "log/tmp.$testname.$tmpnum"; $tmpnum++;
+  my $tmpf = "$workdir/tmp.$testname.$tmpnum"; $tmpnum++;
 
   open (IN, "<data/spam/001");
   open (OUT, ">$tmpf") or die "cannot write $tmpf";
@@ -92,4 +88,3 @@ sub clean_pending_unlinks {
   @pending_unlinks = ();
 }
 
-
index e87f2a3e4b9b2a3cc205cdea243bf35723efbb51..7cb1f18adcbe14b0dc05df32812c161dec61c221 100755 (executable)
@@ -4,7 +4,7 @@ use lib '.'; use lib 't';
 use SATest; sa_t_init("spamd_prefork_stress_4");
 
 use Test::More;
-diag("NOTE: this test requires both 'run_spamd_prefork_stress_test' and 'run_long_tests' set to 'y'.");
+
 plan skip_all => "Spamd tests disabled" if $SKIP_SPAMD_TESTS;
 plan skip_all => "Long running tests disabled" unless conf_bool('run_long_tests');
 plan skip_all => "Spamd prefork stress tests disabled" unless conf_bool('run_spamd_prefork_stress_test');
@@ -12,20 +12,18 @@ plan tests => 43;
 
 # ---------------------------------------------------------------------------
 
-# tstlocalrules ('
-        # loadplugin myTestPlugin ../../data/testplugin.pm
+# tstprefs ('
+        # loadplugin myTestPlugin ../../../data/testplugin.pm
         # header PLUGIN_SLEEP eval:sleep_based_on_header()
 # ');
 
 
 %patterns = (
-
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-q{ X-Spam-Level: **********}, 'stars',
-q{ TEST_ENDSNUMS}, 'endsinnums',
-q{ TEST_NOREALNAME}, 'noreal',
-
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
+  q{ X-Spam-Level: **********}, 'stars',
+  q{ TEST_ENDSNUMS}, 'endsinnums',
+  q{ TEST_NOREALNAME}, 'noreal',
 );
 
 my $tmpnum = 0;
@@ -113,5 +111,3 @@ sub test_spamc {
   ok (spamcrun ("<data/spam/001", \&patterns_run_cb));
 }
 
-
-
index 4a76752119504d7da9500337f08eb3a28639c646..6b8cc95bda3da54efe31d5d0820c2f8bbe2b86bf 100755 (executable)
@@ -13,11 +13,9 @@ use IO::Socket;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ SPAMD/1.1 0 EX_OK }, 'response-11',
-q{ Spam: True ; }, 'spamheader',       # we use a regexp later for the rest
-q{ GTUBE }, 'gtube',
-
+  qr/^SPAMD\/1.1 0 EX_OK/m, 'response-11',
+  'Spam: True ;', 'spamheader',        # we use a regexp later for the rest
+  'GTUBE', 'gtube',
 );
 
 
index 7cfb5c043d69e9630c1c0848e0e998341c281b9b..ef8202cd4d757fe6dae9a61d97fb45dbbaa32829 100755 (executable)
@@ -10,11 +10,9 @@ plan tests => 6;
 # ---------------------------------------------------------------------------
 
 %is_spam_patterns = (
-
-q{ TEST_INVALID_DATE}, 'date',
-q{ TEST_ENDSNUMS}, 'endsinnums',
-q{ TEST_NOREALNAME}, 'noreal',
-
+  q{ TEST_INVALID_DATE}, 'date',
+  q{ TEST_ENDSNUMS}, 'endsinnums',
+  q{ TEST_NOREALNAME}, 'noreal',
 );
 
 %patterns = %is_spam_patterns;
index b4b44ebc314ac3436a8f558d7dc44e2399572d60..63de05a863057b397b5ea4f7727718bcf59acf72 100755 (executable)
@@ -10,11 +10,9 @@ plan tests => 10;
 # ---------------------------------------------------------------------------
 
 %is_spam_patterns = (
-
-q{ TEST_INVALID_DATE}, 'date',
-q{ TEST_ENDSNUMS}, 'endsinnums',
-q{ TEST_NOREALNAME}, 'noreal',
-
+  q{ 5.0 TEST_INVALID_DATE }, 'date',
+  q{ 5.0 TEST_ENDSNUMS }, 'endsinnums',
+  q{ 5.0 TEST_NOREALNAME }, 'noreal',
 );
 
 %patterns = %is_spam_patterns;
index 51d580fda46b9757e5687b51ea0d9900269019bd..85145b2bc2fbddfc6ed325a8b4766f36d38185c4 100755 (executable)
@@ -3,7 +3,7 @@
 use lib '.'; use lib 't';
 use SATest; sa_t_init("spamd_sql_prefs");
 use constant HAS_DBI => eval { require DBI; };
-use constant HAS_DBD_SQLITE => eval { require DBD::SQLite; };
+use constant HAS_DBD_SQLITE => eval { require DBD::SQLite; DBD::SQLite->VERSION(1.59_01); };
 
 use Test::More;
 plan skip_all => "Spamd tests disabled" if $SKIP_SPAMD_TESTS;
@@ -14,7 +14,7 @@ plan tests => 32;
 
 # ---------------------------------------------------------------------------
 
-my $userprefdb = mk_safe_tmpdir()."/userpref.db";
+my $userprefdb = $workdir."/userpref.db";
 
 my $dbh = DBI->connect("dbi:SQLite:dbname=$userprefdb","","");
 ok($dbh);
@@ -26,36 +26,34 @@ ok($dbh->do("INSERT INTO userpref VALUES('testuser', 'score', 'MSGID_RANDY 0')")
 ok($dbh->do("INSERT INTO userpref VALUES('testuser', 'score', 'DATE_IN_PAST_03_06 0')"));
 ok($dbh->do("INSERT INTO userpref VALUES('testuser', 'add_header', 'all tTEST2 FOO2')"));
 
-tstlocalrules ("
-    user_scores_dsn dbi:SQLite:dbname=$userprefdb
+tstprefs ("
+  user_scores_dsn dbi:SQLite:dbname=$userprefdb
 ");
 
 ok(start_spamd("-L --sql-config -u $spamd_run_as_user"));
 
 %patterns = (
-            q{ X-Spam-tTEST1: FOO1 }, 'Added Header tTEST1',
-            q{ X-Spam-Flag: YES}, 'Spam Flag',
-            q{ BODY: Generic Test for Unsolicited Bulk Email }, 'GTUBE Test',
-            q{ XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X }, 'GTUBE String',
+  qr/^X-Spam-tTEST1: FOO1$/m, 'Added Header tTEST1',
+  qr/^X-Spam-Flag: YES/m, 'Spam Flag',
+  q{ 1000 GTUBE }, 'GTUBE Test',
+  'XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X', 'GTUBE String',
 );
-
 %anti_patterns = (
-                 q{ X-Spam-tTEST2: FOO2 }, 'Added Header',
-                 );
+  'X-Spam-tTEST2: FOO2', 'Added Header',
+);
 ok (spamcrun("-u nobody < data/spam/018", \&patterns_run_cb));
 ok_all_patterns();
 clear_pattern_counters();
 
 %patterns = (
-            q{ X-Spam-tTEST1: FOO1 }, 'Added Header tTEST1',
-            q{ X-Spam-tTEST2: FOO2 }, 'Added Header tTEST2',
-            q{ XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X }, 'GTUBE String',
-            );
+  qr/^X-Spam-tTEST1: FOO1$/m, 'Added Header tTEST1',
+  qr/^X-Spam-tTEST2: FOO2$/m, 'Added Header tTEST2',
+  'XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X', 'GTUBE String',
+);
 %anti_patterns = (
-            q{ BODY: Generic Test for Unsolicited Bulk Email }, 'GTUBE Test',
-            q{ X-Spam-Flag: YES}, 'Spam Flag',
-            );
-
+  q{ 1000 GTUBE }, 'GTUBE Test',
+  'X-Spam-Flag: YES', 'Spam Flag',
+);
 ok (spamcrun("-u testuser < data/spam/018", \&patterns_run_cb));
 ok_all_patterns();
 clear_pattern_counters();
@@ -63,32 +61,29 @@ clear_pattern_counters();
 ok($dbh->do("INSERT INTO userpref VALUES('testuser', 'required_score', '1000')"));
 
 %patterns = (
-            q{ X-Spam-tTEST1: FOO1 }, 'Added Header tTEST1',
-            q{ X-Spam-tTEST2: FOO2 }, 'Added Header tTEST2',
-            q{ X-Spam-Status: No }, 'Spam Status No',
-            q{ XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X }, 'GTUBE String',
-            );
+  qr/^X-Spam-tTEST1: FOO1\n/m, 'Added Header tTEST1',
+  qr/^X-Spam-tTEST2: FOO2\n/m, 'Added Header tTEST2',
+  qr/^X-Spam-Status: No/m, 'Spam Status No',
+  'XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X', 'GTUBE String',
+);
 %anti_patterns = (
-                 q{ X-Spam-Flag: YES}, 'Spam Flag YES',
-                 q{ BODY: Generic Test for Unsolicited Bulk Email }, 'GTUBE Test',
-                 );
-
+  'X-Spam-Flag: YES', 'Spam Flag YES',
+  q{ 1000 GTUBE }, 'GTUBE Test',
+);
 ok (spamcrun("-u testuser < data/spam/018", \&patterns_run_cb));
 ok_all_patterns();
 clear_pattern_counters();
 
 %patterns = (
-            q{ dbg: config: retrieving prefs for }, 'Retrieving Prefs',
-            );
+  q{ dbg: config: retrieving prefs for }, 'Retrieving Prefs',
+);
 %anti_patterns = (
-                 q{ warn: closing dbh with active statement handles }, 'Closing Active Handles',
-                 );
-
+  q{ warn: closing dbh with active statement handles }, 'Closing Active Handles',
+);
 checkfile ($spamd_stderr, \&patterns_run_cb);
 ok_all_patterns();
 
 ok(stop_spamd());
 
-cleanup_safe_tmpdir();
-
 ok($dbh->disconnect());
+
index 7f1dbe2cb930f557b787a986629141d43f1aa338..3a8fc000d4cd633bd4211557dbf7e102485d1788 100755 (executable)
@@ -11,17 +11,14 @@ plan tests => 9;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ Return-Path: sb55sb55@yahoo.com}, 'firstline',
-q{ Subject: There yours for FREE!}, 'subj',
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-q{ X-Spam-Level: **********}, 'stars',
-q{ TEST_ENDSNUMS}, 'endsinnums',
-q{ TEST_NOREALNAME}, 'noreal',
-q{ This must be the very last line}, 'lastline',
-
-
+  q{ Return-Path: sb55sb55@yahoo.com}, 'firstline',
+  q{ Subject: There yours for FREE!}, 'subj',
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
+  q{ X-Spam-Level: **********}, 'stars',
+  q{ TEST_ENDSNUMS}, 'endsinnums',
+  q{ TEST_NOREALNAME}, 'noreal',
+  q{ This must be the very last line}, 'lastline',
 );
 
 my $port = probably_unused_spamd_port();
@@ -29,3 +26,4 @@ ok (sdrun ("-L --ssl --port $port --server-key data/etc/testhost.key --server-ce
            "--ssl --port $port < data/spam/001",
            \&patterns_run_cb));
 ok_all_patterns();
+
index 24807634ee5f3e1ef7f8be748fae0c8ebea5c651..ea8fba98534ac13edbf6630e0d6202d4ab698662 100755 (executable)
@@ -12,22 +12,20 @@ plan tests => 12;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ Return-Path: sb55sb55@yahoo.com}, 'firstline',
-q{ Subject: There yours for FREE!}, 'subj',
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-q{ X-Spam-Level: **********}, 'stars',
-q{ TEST_ENDSNUMS}, 'endsinnums',
-q{ TEST_NOREALNAME}, 'noreal',
-q{ This must be the very last line}, 'lastline',
-
-
+  q{ Return-Path: sb55sb55@yahoo.com}, 'firstline',
+  q{ Subject: There yours for FREE!}, 'subj',
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
+  q{ X-Spam-Level: **********}, 'stars',
+  q{ TEST_ENDSNUMS}, 'endsinnums',
+  q{ TEST_NOREALNAME}, 'noreal',
+  q{ This must be the very last line}, 'lastline',
 );
 
 my $port = probably_unused_spamd_port();
 ok (start_spamd ("-L --ssl --port $port --server-key data/etc/testhost.key --server-cert data/etc/testhost.cert"));
 ok (spamcrun ("--port $port < data/spam/001", \&patterns_run_cb));
+sleep(1);
 ok (spamcrun ("--ssl --port $port < data/spam/001", \&patterns_run_cb));
 ok (stop_spamd ());
 
diff --git a/upstream/t/spamd_ssl_z.t b/upstream/t/spamd_ssl_z.t
new file mode 100755 (executable)
index 0000000..9eca6c7
--- /dev/null
@@ -0,0 +1,37 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("spamd_ssl_z");
+
+use constant HAVE_ZLIB => eval { require Compress::Zlib; };
+
+use Test::More;
+plan skip_all => "Spamd tests disabled" if $SKIP_SPAMD_TESTS;
+plan skip_all => "SSL is unavailble" unless $SSL_AVAILABLE;
+plan skip_all => "ZLIB REQUIRED" unless HAVE_ZLIB;
+
+untaint_system("$spamc -z < /dev/null");
+my $SPAMC_Z_AVAILABLE = ($? >> 8 == 0);
+plan skip_all => "SPAMC Z unavailable" unless $SPAMC_Z_AVAILABLE;
+
+plan tests => 9;
+
+# ---------------------------------------------------------------------------
+
+%patterns = (
+  q{ Return-Path: sb55sb55@yahoo.com}, 'firstline',
+  q{ Subject: There yours for FREE!}, 'subj',
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
+  q{ X-Spam-Level: **********}, 'stars',
+  q{ TEST_ENDSNUMS}, 'endsinnums',
+  q{ TEST_NOREALNAME}, 'noreal',
+  q{ This must be the very last line}, 'lastline',
+);
+
+my $port = probably_unused_spamd_port();
+ok (sdrun ("-L --ssl --port $port --server-key data/etc/testhost.key --server-cert data/etc/testhost.cert",
+           "-z -t 5 --ssl --port $port < data/spam/001",
+           \&patterns_run_cb));
+ok_all_patterns();
+
index 56690c7cb49ea52731c0af24e70260741e095b3b..52f511bb70698f4f3f141346b87217dcc7c1f068 100755 (executable)
@@ -10,9 +10,7 @@ plan tests => 2;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ X-Spam-Status: Yes,}, 'status',
-
+  q{ X-Spam-Status: Yes,}, 'status',
 );
 
 ok (sdrun ("-L", "< data/spam/001", \&patterns_run_cb));
index 68224905ec5b58d261684e27b1dde2795deb38c3..41cc5f660c7d7828a275749bfbe96f38f6145bd5 100755 (executable)
@@ -10,11 +10,8 @@ plan tests => 3;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ TEST_ENDSNUMS, }, 'endsinnums',
-q{ TEST_NOREALNAME, }, 'noreal',
-
-
+  ',TEST_ENDSNUMS,', 'endsinnums',
+  ',TEST_NOREALNAME,', 'noreal',
 );
 
 ok (sdrun ("-L", "-y < data/spam/001", \&patterns_run_cb));
index d01532c235d76d238790bd15062d2ea602c7b829..9dfc7026986ac1559ae26cb1dd189a6d934ce330 100755 (executable)
@@ -10,15 +10,12 @@ plan tests => 7;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ Subject: There yours for FREE!}, 'subj',
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-q{ X-Spam-Level: **********}, 'stars',
-q{ TEST_ENDSNUMS}, 'endsinnums',
-q{ TEST_NOREALNAME}, 'noreal',
-
-
+  q{ Subject: There yours for FREE!}, 'subj',
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
+  q{ X-Spam-Level: **********}, 'stars',
+  q{ TEST_ENDSNUMS}, 'endsinnums',
+  q{ TEST_NOREALNAME}, 'noreal',
 );
 
 $spamd_inhibit_log_to_err = 1;
index efb00aea2bb8c61a407f644d59d8eaac3dc3f6d9..20d7d52b23479affe6e433a58a0035cab3bddef9 100755 (executable)
@@ -11,22 +11,17 @@ plan tests => 4;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ Subject: There yours for FREE!}, 'subj',
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-
-
+  q{ Subject: There yours for FREE!}, 'subj',
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
 );
 
-tstlocalrules("
-      use_auto_whitelist 0
-    ");
+tstprefs("
+  use_auto_whitelist 0
+");
 
-my $sockpath = mk_safe_tmpdir()."/spamd.sock";
+my $sockpath = mk_socket_tempdir()."/spamd.sock";
 start_spamd("-D -L --socketpath=$sockpath");
 ok (spamcrun ("-U $sockpath < data/spam/001", \&patterns_run_cb));
 ok_all_patterns();
 stop_spamd();
-cleanup_safe_tmpdir();
-
index 5fefe3ffd3afd79550e643b0e73382968957b3e8..1e5bb213fa1670e8cdf67bba09260eb6450e0d0b 100755 (executable)
@@ -10,7 +10,7 @@ plan tests => 10;
 
 # ---------------------------------------------------------------------------
 
-my $sockpath = mk_safe_tmpdir()."/spamd.sock";
+my $sockpath = mk_socket_tempdir()."/spamd.sock";
 start_spamd("-D -L --socketpath=$sockpath --port $spamdport -A $spamdhost -i $spamdhost");
 %patterns = (
   q{ Subject: There yours for FREE!}, 'subj',
@@ -42,5 +42,4 @@ ok (spamcrun ("< data/spam/gtube.eml", \&patterns_run_cb));
 ok_all_patterns();
 
 stop_spamd();
-cleanup_safe_tmpdir();
 
index 1857f63ccf08c24d67c693f8f00346d63ab6ad55..b9ea3add060a0d03f737cdb1ab2a5ddbecd141f0 100755 (executable)
@@ -6,24 +6,24 @@ use SATest; sa_t_init("spamd_user_rules_leak");
 
 use Test::More;
 plan skip_all => 'Spamd tests disabled' if $SKIP_SPAMD_TESTS;
-plan tests => 28;
+plan tests => 20;
 
 # ---------------------------------------------------------------------------
 # If user A defines a user rule (when allow_user_rules is enabled) it affects
 # user B if they also set a score for that same rule name or create a user rule
 # with the same name.
 
-tstlocalrules ("
-       allow_user_rules 1
+tstprefs ("
+  allow_user_rules 1
 ");
 
-rmtree ("log/virtualconfig/testuser1", 0, 1);
-mkpath ("log/virtualconfig/testuser1", 0, 0755);
-rmtree ("log/virtualconfig/testuser2", 0, 1);
-mkpath ("log/virtualconfig/testuser2", 0, 0755);
-rmtree ("log/virtualconfig/testuser3", 0, 1);
-mkpath ("log/virtualconfig/testuser3", 0, 0755);
-open (OUT, ">log/virtualconfig/testuser1/user_prefs");
+rmtree ("$workdir/virtualconfig/testuser1", 0, 1);
+mkpath ("$workdir/virtualconfig/testuser1", 0, 0755);
+rmtree ("$workdir/virtualconfig/testuser2", 0, 1);
+mkpath ("$workdir/virtualconfig/testuser2", 0, 0755);
+rmtree ("$workdir/virtualconfig/testuser3", 0, 1);
+mkpath ("$workdir/virtualconfig/testuser3", 0, 0755);
+open (OUT, ">$workdir/virtualconfig/testuser1/user_prefs");
 print OUT q{
 
        header MYFOO Content-Transfer-Encoding =~ /quoted-printable/
@@ -37,7 +37,7 @@ print OUT q{
 
 };
 close OUT;
-open (OUT, ">log/virtualconfig/testuser2/user_prefs");
+open (OUT, ">$workdir/virtualconfig/testuser2/user_prefs");
 print OUT q{
 
         # create a new user rule with same name
@@ -50,7 +50,7 @@ print OUT q{
 
 };
 close OUT;
-open (OUT, ">log/virtualconfig/testuser3/user_prefs");
+open (OUT, ">$workdir/virtualconfig/testuser3/user_prefs");
 print OUT q{
 
         # no user rules here
@@ -59,33 +59,29 @@ print OUT q{
 close OUT;
 
 %patterns = (
-  q{ 3.0 MYFOO }, 'MYFOO',
-  q{ 3.0 MYBODY }, 'MYBODY',
-  q{ 3.0 MYRAWBODY }, 'MYRAWBODY',
-  q{ 3.0 MYFULL }, 'MYFULL',
+  q{ 3.0 MYFOO }, '',
+  q{ 3.0 MYBODY }, '',
+  q{ 3.0 MYRAWBODY }, '',
+  q{ 3.0 MYFULL }, '',
 );
 %anti_patterns = (
-  q{  redefined at }, 'redefined_errors_in_spamd_log',
+  'redefined at', 'redefined_errors_in_spamd_log',
 );
 
 # use -m1 so all scans use the same child
-ok (start_spamd ("--virtual-config-dir=log/virtualconfig/%u -L -u $spamd_run_as_user -m1"));
+ok (start_spamd ("--virtual-config-dir=$workdir/virtualconfig/%u -L -u $spamd_run_as_user -m1"));
 ok (spamcrun ("-u testuser1 < data/spam/009", \&patterns_run_cb));
 ok_all_patterns();
 clear_pattern_counters();
 
 %patterns = (
-  q{ does not include a real name }, 'TEST_NOREALNAME',
+  q{ does not include a real name }, '',
 );
 %anti_patterns = (
-  q{ 1.0 MYFOO }, 'MYFOO',
-  q{ 1.0 MYBODY }, 'MYBODY',
-  q{ 1.0 MYRAWBODY }, 'MYRAWBODY',
-  q{ 1.0 MYFULL }, 'MYFULL',
-  q{ 3.0 MYFOO }, 'MYFOO',
-  q{ 3.0 MYBODY }, 'MYBODY',
-  q{ 3.0 MYRAWBODY }, 'MYRAWBODY',
-  q{ 3.0 MYFULL }, 'MYFULL',
+  qr/\d MYFOO /, '',
+  qr/\d MYBODY /, '',
+  qr/\d MYRAWBODY /, '',
+  qr/\d MYFULL /, '',
 );
 ok (spamcrun ("-u testuser2 < data/spam/009", \&patterns_run_cb));
 checkfile ($spamd_stderr, \&patterns_run_cb);
@@ -93,17 +89,13 @@ ok_all_patterns();
 clear_pattern_counters();
 
 %patterns = (
-  q{ does not include a real name }, 'TEST_NOREALNAME',
+  q{ does not include a real name }, '',
 );
 %anti_patterns = (
-  q{ 1.0 MYFOO }, 'MYFOO',
-  q{ 1.0 MYBODY }, 'MYBODY',
-  q{ 1.0 MYRAWBODY }, 'MYRAWBODY',
-  q{ 1.0 MYFULL }, 'MYFULL',
-  q{ 3.0 MYFOO }, 'MYFOO',
-  q{ 3.0 MYBODY }, 'MYBODY',
-  q{ 3.0 MYRAWBODY }, 'MYRAWBODY',
-  q{ 3.0 MYFULL }, 'MYFULL',
+  qr/\d MYFOO /, '',
+  qr/\d MYBODY /, '',
+  qr/\d MYRAWBODY /, '',
+  qr/\d MYFULL /, '',
 );
 ok (spamcrun ("-u testuser3 < data/spam/009", \&patterns_run_cb));
 ok (stop_spamd ());
index 1153b45bc9e00129b1d66564277a13c7201b7ca4..518c322e85540f64a0c6008eb5fce35cc5d5c56b 100755 (executable)
@@ -26,18 +26,11 @@ BEGIN {
 
 $ENV{'LANG'} = $testlocale;
 
-# ---------------------------------------------------------------------------
-
 %patterns = (
-
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-
-
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
 );
 
 ok (sdrun ("-L", "< data/spam/008", \&patterns_run_cb));
 ok_all_patterns();
-exit;
 
-# ---------------------------------------------------------------------------
diff --git a/upstream/t/spamd_welcomelist_leak.t b/upstream/t/spamd_welcomelist_leak.t
new file mode 100755 (executable)
index 0000000..ac5f5c2
--- /dev/null
@@ -0,0 +1,60 @@
+#!/usr/bin/perl -T
+# bug 4179
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("spamd_welcomelist_leak");
+
+use Test::More;
+plan skip_all => 'Spamd tests disabled.' if $SKIP_SPAMD_TESTS;
+plan tests => 8;
+
+# ---------------------------------------------------------------------------
+# bug 6003
+
+tstlocalrules (q{
+  header USER_IN_WELCOMELIST           eval:check_from_in_welcomelist()
+  tflags USER_IN_WELCOMELIST           userconf nice noautolearn
+  score USER_IN_WELCOMELIST            -100
+  body MYBODY /LOSE WEIGHT/
+  score MYBODY 99
+});
+
+rmtree ("$workdir/virtualconfig/testuser1", 0, 1);
+mkpath ("$workdir/virtualconfig/testuser1", 0, 0755);
+rmtree ("$workdir/virtualconfig/testuser2", 0, 1);
+mkpath ("$workdir/virtualconfig/testuser2", 0, 0755);
+open (OUT, ">$workdir/virtualconfig/testuser1/user_prefs");
+print OUT q{
+  welcomelist_from    sb55sb123456789@yahoo.com
+  welcomelist_from_rcvd sb55sb123456789@yahoo.com  cgocable.ca
+  welcomelist_from_rcvd sb55sb123456789@yahoo.com  webnote.net
+};
+close OUT;
+open (OUT, ">$workdir/virtualconfig/testuser2/user_prefs");
+print OUT '';
+close OUT;
+
+%patterns = (
+  q{ 99 MYBODY }, '',
+  q{ -100 USER_IN_WELCOMELIST }, '',
+);
+%anti_patterns = (
+);
+
+# use -m1 so all scans use the same child
+ok (start_spamd ("--virtual-config-dir=$workdir/virtualconfig/%u -L -u $spamd_run_as_user -m1"));
+ok (spamcrun ("-u testuser1 < data/spam/001", \&patterns_run_cb));
+ok_all_patterns();
+clear_pattern_counters();
+
+%patterns = (
+  q{ 99 MYBODY }, '',
+);
+%anti_patterns = (
+  qr/\d USER_IN_WELCOMELIST /, '',
+);
+ok (spamcrun ("-u testuser2 < data/spam/001", \&patterns_run_cb));
+checkfile ($spamd_stderr, \&patterns_run_cb);
+ok_all_patterns();
+ok stop_spamd();
+
index e6174feec40c32cf18d8ca2a66de83ba73e1d3a4..49f176fac81ca6223bd919162fd3c70f5c80df46 100755 (executable)
@@ -11,50 +11,55 @@ plan tests => 8;
 # ---------------------------------------------------------------------------
 # bug 6003
 
-tstlocalrules (q{
-
-        body MYBODY /LOSE WEIGHT/
-        score MYBODY 99
+disable_compat "welcomelist_blocklist";
 
-  });
+tstlocalrules (q{
+  header USER_IN_WELCOMELIST           eval:check_from_in_welcomelist()
+  tflags USER_IN_WELCOMELIST           userconf nice noautolearn
+  meta USER_IN_WHITELIST               (USER_IN_WELCOMELIST)
+  tflags USER_IN_WHITELIST             userconf nice noautolearn
+  score USER_IN_WHITELIST              -100
+  score USER_IN_WELCOMELIST            -0.01
+  body MYBODY /LOSE WEIGHT/
+  score MYBODY 99
+});
 
-rmtree ("log/virtualconfig/testuser1", 0, 1);
-mkpath ("log/virtualconfig/testuser1", 0, 0755);
-rmtree ("log/virtualconfig/testuser2", 0, 1);
-mkpath ("log/virtualconfig/testuser2", 0, 0755);
-open (OUT, ">log/virtualconfig/testuser1/user_prefs");
+rmtree ("$workdir/virtualconfig/testuser1", 0, 1);
+mkpath ("$workdir/virtualconfig/testuser1", 0, 0755);
+rmtree ("$workdir/virtualconfig/testuser2", 0, 1);
+mkpath ("$workdir/virtualconfig/testuser2", 0, 0755);
+open (OUT, ">$workdir/virtualconfig/testuser1/user_prefs");
 print OUT q{
-
-        whitelist_from      sb55sb123456789@yahoo.com
-        whitelist_from_rcvd sb55sb123456789@yahoo.com  cgocable.ca
-        whitelist_from_rcvd sb55sb123456789@yahoo.com  webnote.net
-
+  whitelist_from      sb55sb123456789@yahoo.com
+  whitelist_from_rcvd sb55sb123456789@yahoo.com  cgocable.ca
+  whitelist_from_rcvd sb55sb123456789@yahoo.com  webnote.net
 };
 close OUT;
-open (OUT, ">log/virtualconfig/testuser2/user_prefs");
+open (OUT, ">$workdir/virtualconfig/testuser2/user_prefs");
 print OUT '';
 close OUT;
 
 %patterns = (
-  q{ 99 MYBODY }, 'MYBODY',
-  q{-100 USER_IN_WHITELIST }, 'USER_IN_WHITELIST',
+  q{ 99 MYBODY }, '',
+  q{ -100 USER_IN_WHITELIST }, '',
 );
 %anti_patterns = (
 );
 
 # use -m1 so all scans use the same child
-ok (start_spamd ("--virtual-config-dir=log/virtualconfig/%u -L -u $spamd_run_as_user -m1"));
+ok (start_spamd ("--virtual-config-dir=$workdir/virtualconfig/%u -L -u $spamd_run_as_user -m1"));
 ok (spamcrun ("-u testuser1 < data/spam/001", \&patterns_run_cb));
 ok_all_patterns();
 clear_pattern_counters();
 
 %patterns = (
-  q{ 99 MYBODY }, 'MYBODY',
+  q{ 99 MYBODY }, '',
 );
 %anti_patterns = (
-  q{-100 USER_IN_WHITELIST }, 'USER_IN_WHITELIST',
+  q{ -100 USER_IN_WHITELIST }, '',
 );
 ok (spamcrun ("-u testuser2 < data/spam/001", \&patterns_run_cb));
 checkfile ($spamd_stderr, \&patterns_run_cb);
 ok_all_patterns();
 ok stop_spamd();
+
index 780ecacdd3ea6a827de94fb848ccac4686eaa494..10840dfe8ef302b473159f39ba41056eccd93ce3 100755 (executable)
@@ -4,38 +4,43 @@ use lib '.'; use lib 't';
 use SATest; sa_t_init("spf");
 use Test::More;
 
-use constant HAS_SPFQUERY => eval { require Mail::SPF::Query; };
 use constant HAS_MAILSPF => eval { require Mail::SPF; };
 
-# bug 3806:
-# Do not run this test with version of Sys::Hostname::Long older than 1.4
-# on non-Linux unices as root, due to a bug in Sys::Hostname::Long
-# (it is used by Mail::SPF::Query, which is now obsoleted by Mail::SPF)
-use constant IS_LINUX   => $^O eq 'linux';
-use constant IS_OPENBSD => $^O eq 'openbsd';
-use constant IS_WINDOWS => ($^O =~ /^(mswin|dos|os2)/i);
-use constant AM_ROOT    => $< == 0;
-
-use constant HAS_UNSAFE_HOSTNAME =>  # Bug 3806 - module exists and is old
-  eval { require Sys::Hostname::Long && Sys::Hostname::Long->VERSION < 1.4 };
-
 plan skip_all => "Long running tests disabled" unless conf_bool('run_long_tests');
 plan skip_all => "Net tests disabled" unless conf_bool('run_net_tests');
-plan skip_all => "Need Mail::SPF or Mail::SPF::Query" unless (HAS_SPFQUERY || HAS_MAILSPF);
-plan skip_all => "Sys::Hostname::Long > 1.4 required." if HAS_UNSAFE_HOSTNAME;
-plan skip_all => "Test only designed for Windows, Linux or OpenBSD" unless (IS_LINUX || IS_OPENBSD || IS_WINDOWS);
+plan skip_all => "Need Mail::SPF" unless HAS_MAILSPF;
+plan skip_all => "Can't use Net::DNS Safely" unless can_use_net_dns_safely();
 
-if(HAS_SPFQUERY && HAS_MAILSPF) {
-  plan tests => 110;
-}
-else {
-  plan tests => 62; # TODO: These should be skips down in the code, not changing the test count.
-}
+plan tests => 72;
 
 # ---------------------------------------------------------------------------
 
+disable_compat "welcomelist_blocklist";
+
 # ensure all rules will fire
 tstlocalrules ("
+  header SPF_PASS              eval:check_for_spf_pass()
+  header SPF_NEUTRAL           eval:check_for_spf_neutral()
+  header SPF_FAIL              eval:check_for_spf_fail()
+  header SPF_SOFTFAIL          eval:check_for_spf_softfail()
+  header SPF_HELO_PASS         eval:check_for_spf_helo_pass()
+  header SPF_HELO_NEUTRAL      eval:check_for_spf_helo_neutral()
+  header SPF_HELO_FAIL         eval:check_for_spf_helo_fail()
+  header SPF_HELO_SOFTFAIL     eval:check_for_spf_helo_softfail()
+  tflags SPF_PASS              nice userconf net
+  tflags SPF_HELO_PASS         nice userconf net
+  tflags SPF_NEUTRAL           net
+  tflags SPF_FAIL              net
+  tflags SPF_SOFTFAIL          net
+  tflags SPF_HELO_NEUTRAL       net
+  tflags SPF_HELO_FAIL         net
+  tflags SPF_HELO_SOFTFAIL     net
+  header USER_IN_SPF_WELCOMELIST eval:check_for_spf_welcomelist_from()
+  tflags USER_IN_SPF_WELCOMELIST userconf nice noautolearn net
+  header USER_IN_DEF_SPF_WL    eval:check_for_def_spf_welcomelist_from()
+  tflags USER_IN_DEF_SPF_WL    userconf nice noautolearn net
+  meta USER_IN_SPF_WHITELIST   (USER_IN_SPF_WELCOMELIST)
+  tflags USER_IN_SPF_WHITELIST userconf nice noautolearn net
   score SPF_FAIL 0.001
   score SPF_HELO_FAIL 0.001
   score SPF_HELO_NEUTRAL 0.001
@@ -45,372 +50,333 @@ tstlocalrules ("
   score SPF_PASS -0.001
   score SPF_HELO_PASS -0.001
   score USER_IN_DEF_SPF_WL -0.001
+  score USER_IN_SPF_WELCOMELIST -0.001
   score USER_IN_SPF_WHITELIST -0.001
 ");
 
-# test both of the SPF modules we support
-for $disable_an_spf_module ('do_not_use_mail_spf 1', 'do_not_use_mail_spf_query 1') {
+%patterns = (
+  q{ -0.0 SPF_HELO_PASS }, 'helo_pass',
+  q{ -0.0 SPF_PASS }, 'pass',
+);
 
-  # only do the tests if the module that wasn't disabled is available
-  next if ($disable_an_spf_module eq 'do_not_use_mail_spf 1' && !HAS_SPFQUERY);
-  next if ($disable_an_spf_module eq 'do_not_use_mail_spf_query 1' && !HAS_MAILSPF);
+sarun ("-t < data/nice/spf1", \&patterns_run_cb);
+ok_all_patterns();
 
-  tstprefs("
-    $disable_an_spf_module
-  ");
+%patterns = (
+  q{ 0.0 SPF_NEUTRAL }, 'neutral',
+  q{ 0.0 SPF_HELO_NEUTRAL }, 'helo_neutral',
+);
 
-  %patterns = (
-    q{ SPF_HELO_PASS }, 'helo_pass',
-    q{ SPF_PASS }, 'pass',
-  );
+sarun ("-t < data/spam/spf1", \&patterns_run_cb);
+ok_all_patterns();
 
-  sarun ("-t < data/nice/spf1", \&patterns_run_cb);
-  ok_all_patterns();
+%patterns = (
+  q{ 0.0 SPF_SOFTFAIL }, 'softfail',
+  q{ 0.0 SPF_HELO_SOFTFAIL }, 'helo_softfail',
+);
 
-  %patterns = (
-    q{ SPF_NEUTRAL }, 'neutral',
-    q{ SPF_HELO_NEUTRAL }, 'helo_neutral',
-  );
+sarun ("-t < data/spam/spf2", \&patterns_run_cb);
+ok_all_patterns();
+%patterns = (
+  q{ 0.0 SPF_FAIL }, 'fail',
+  q{ 0.0 SPF_HELO_FAIL }, 'helo_fail',
+);
 
-  sarun ("-t < data/spam/spf1", \&patterns_run_cb);
-  ok_all_patterns();
+sarun ("-t < data/spam/spf3", \&patterns_run_cb);
+ok_all_patterns();
 
-  %patterns = (
-    q{ SPF_SOFTFAIL }, 'softfail',
-    q{ SPF_HELO_SOFTFAIL }, 'helo_softfail',
-  );
 
-  sarun ("-t < data/spam/spf2", \&patterns_run_cb);
-  ok_all_patterns();
-  %patterns = (
-    q{ SPF_FAIL }, 'fail',
-    q{ SPF_HELO_FAIL }, 'helo_fail',
-  );
+# Test using an assortment of trusted and internal network definitions
 
-  sarun ("-t < data/spam/spf3", \&patterns_run_cb);
-  ok_all_patterns();
+# 9-10: Trusted networks contain first header.
 
+tstprefs("
+  clear_trusted_networks
+  clear_internal_networks
+  trusted_networks 65.214.43.157
+  always_trust_envelope_sender 1
+");
 
-  # Test using an assortment of trusted and internal network definitions
-
-  # 9-10: Trusted networks contain first header.
+%patterns = (
+  q{ -0.0 SPF_HELO_PASS }, 'helo_pass',
+  q{ -0.0 SPF_PASS }, 'pass',
+);
 
-  tstprefs("
-    clear_trusted_networks
-    clear_internal_networks
-    trusted_networks 65.214.43.157
-    always_trust_envelope_sender 1
-    $disable_an_spf_module
-  ");
+sarun ("-t < data/nice/spf2", \&patterns_run_cb);
+ok_all_patterns();
 
-  %patterns = (
-    q{ SPF_HELO_PASS }, 'helo_pass',
-    q{ SPF_PASS }, 'pass',
-  );
 
-  sarun ("-t < data/nice/spf2", \&patterns_run_cb);
-  ok_all_patterns();
+# 11-12: Internal networks contain first header.
+#         Trusted networks not defined.
 
+tstprefs("
+  clear_trusted_networks
+  clear_internal_networks
+  internal_networks 65.214.43.157
+  always_trust_envelope_sender 1
+");
 
-  # 11-12: Internal networks contain first header.
-  #       Trusted networks not defined.
+%patterns = (
+  q{ -0.0 SPF_HELO_PASS }, 'helo_pass',
+  q{ -0.0 SPF_PASS }, 'pass',
+);
 
-  tstprefs("
-    clear_trusted_networks
-    clear_internal_networks
-    internal_networks 65.214.43.157
-    always_trust_envelope_sender 1
-    $disable_an_spf_module
-  ");
+sarun ("-t < data/nice/spf2", \&patterns_run_cb);
+ok_all_patterns();
 
-  %patterns = (
-    q{ SPF_HELO_PASS }, 'helo_pass',
-    q{ SPF_PASS }, 'pass',
-  );
 
-  sarun ("-t < data/nice/spf2", \&patterns_run_cb);
-  ok_all_patterns();
+# 13-14: Internal networks contain first header.
+#         Trusted networks contain some other IP.
+#        jm: commented; this is now an error condition.
 
+tstprefs("
+  clear_trusted_networks
+  clear_internal_networks
+  trusted_networks 1.2.3.4
+  internal_networks 65.214.43.157
+  always_trust_envelope_sender 1
+");
 
-  # 13-14: Internal networks contain first header.
-  #       Trusted networks contain some other IP.
-  #        jm: commented; this is now an error condition.
-
-  tstprefs("
-    clear_trusted_networks
-    clear_internal_networks
-    trusted_networks 1.2.3.4
-    internal_networks 65.214.43.157
-    always_trust_envelope_sender 1
-    $disable_an_spf_module
-  ");
-
-  %patterns = (
-    q{ SPF_HELO_NEUTRAL }, 'helo_neutral',
-    q{ SPF_NEUTRAL }, 'neutral',
-  );
-
-  if (0) {
-    sarun ("-t < data/nice/spf2", \&patterns_run_cb);
-    ok_all_patterns();
-  } else {
-    ok(1);        # skip the tests
-    ok(1);
-  }
-
-
-  # 15-16: Trusted+Internal networks contain first header.
-
-  tstprefs("
-    clear_trusted_networks
-    clear_internal_networks
-    trusted_networks 65.214.43.157
-    internal_networks 65.214.43.157
-    always_trust_envelope_sender 1
-    $disable_an_spf_module
-  ");
-
-  %patterns = (
-    q{ SPF_HELO_PASS }, 'helo_pass',
-    q{ SPF_PASS }, 'pass',
-  );
+%patterns = (
+  q{ 0.0 SPF_HELO_NEUTRAL }, 'helo_neutral',
+  q{ 0.0 SPF_NEUTRAL }, 'neutral',
+);
 
+if (0) {
   sarun ("-t < data/nice/spf2", \&patterns_run_cb);
   ok_all_patterns();
+} else {
+  ok(1);        # skip the tests
+  ok(1);
+}
 
 
-  # 17-18: Trusted networks contain first and second header.
-  #       Internal networks contain first header.
+# 15-16: Trusted+Internal networks contain first header.
 
-  tstprefs("
-    clear_trusted_networks
-    clear_internal_networks
-    trusted_networks 65.214.43.157 64.142.3.173
-    internal_networks 65.214.43.157
-    always_trust_envelope_sender 1
-    $disable_an_spf_module
-  ");
+tstprefs("
+  clear_trusted_networks
+  clear_internal_networks
+  trusted_networks 65.214.43.157
+  internal_networks 65.214.43.157
+  always_trust_envelope_sender 1
+");
 
-  %patterns = (
-    q{ SPF_HELO_PASS }, 'helo_pass',
-    q{ SPF_PASS }, 'pass',
-  );
+%patterns = (
+  q{ -0.0 SPF_HELO_PASS }, 'helo_pass',
+  q{ -0.0 SPF_PASS }, 'pass',
+);
 
-  sarun ("-t < data/nice/spf2", \&patterns_run_cb);
-  ok_all_patterns();
+sarun ("-t < data/nice/spf2", \&patterns_run_cb);
+ok_all_patterns();
 
 
-  # 19-26: Trusted networks contain first and second header.
-  #       Internal networks contain first and second header.
-
-  tstprefs("
-    clear_trusted_networks
-    clear_internal_networks
-    trusted_networks 65.214.43.157 64.142.3.173
-    internal_networks 65.214.43.157 64.142.3.173
-    always_trust_envelope_sender 1
-    $disable_an_spf_module
-  ");
-
-  %anti_patterns = (
-    q{ SPF_HELO_PASS }, 'helo_pass',
-    q{ SPF_HELO_FAIL }, 'helo_fail',
-    q{ SPF_HELO_SOFTFAIL }, 'helo_softfail',
-    q{ SPF_HELO_NEUTRAL }, 'helo_neutral',
-    q{ SPF_PASS }, 'pass',
-    q{ SPF_FAIL }, 'fail',
-    q{ SPF_SOFTFAIL }, 'softfail',
-    q{ SPF_NEUTRAL }, 'neutral',
-  );
-  %patterns = ();
+# 17-18: Trusted networks contain first and second header.
+#         Internal networks contain first header.
 
-  sarun ("-t < data/nice/spf2", \&patterns_run_cb);
-  ok_all_patterns();
+tstprefs("
+  clear_trusted_networks
+  clear_internal_networks
+  trusted_networks 65.214.43.157 64.142.3.173
+  internal_networks 65.214.43.157
+  always_trust_envelope_sender 1
+");
 
+%patterns = (
+  q{ -0.0 SPF_HELO_PASS }, 'helo_pass',
+  q{ -0.0 SPF_PASS }, 'pass',
+);
 
-  # 27-28: Trusted networks contain first header.
-  #       Internal networks contain first and second header.
+sarun ("-t < data/nice/spf2", \&patterns_run_cb);
+ok_all_patterns();
 
-  tstprefs("
-    clear_trusted_networks
-    clear_internal_networks
-    trusted_networks 65.214.43.157
-    internal_networks 65.214.43.157 64.142.3.173
-    always_trust_envelope_sender 1
-    $disable_an_spf_module
-  ");
 
-  %anti_patterns = ();
-  %patterns = (
-    q{ SPF_HELO_PASS }, 'helo_pass',
-    q{ SPF_PASS }, 'pass',
-  );
+# 19-26: Trusted networks contain first and second header.
+#         Internal networks contain first and second header.
 
-  sarun ("-t < data/nice/spf2", \&patterns_run_cb);
-  ok_all_patterns();
+tstprefs("
+  clear_trusted_networks
+  clear_internal_networks
+  trusted_networks 65.214.43.157 64.142.3.173
+  internal_networks 65.214.43.157 64.142.3.173
+  always_trust_envelope_sender 1
+");
 
+%anti_patterns = (
+  q{ SPF_HELO_PASS }, 'helo_pass',
+  q{ SPF_HELO_FAIL }, 'helo_fail',
+  q{ SPF_HELO_SOFTFAIL }, 'helo_softfail',
+  q{ SPF_HELO_NEUTRAL }, 'helo_neutral',
+  q{ SPF_PASS }, 'pass',
+  q{ SPF_FAIL }, 'fail',
+  q{ SPF_SOFTFAIL }, 'softfail',
+  q{ SPF_NEUTRAL }, 'neutral',
+);
+%patterns = ();
 
-  # 29-30: Trusted networks contain top 5 headers.
-  #       Internal networks contain first header.
+sarun ("-t < data/nice/spf2", \&patterns_run_cb);
+ok_all_patterns();
 
-  tstprefs("
-    clear_trusted_networks
-    clear_internal_networks
-    trusted_networks 65.214.43.158 64.142.3.173 65.214.43.155 65.214.43.156 65.214.43.157
-    internal_networks 65.214.43.158
-    always_trust_envelope_sender 1
-    $disable_an_spf_module
-  ");
 
-  %anti_patterns = ();
-  %patterns = (
-    q{ SPF_HELO_PASS }, 'helo_pass',
-    q{ SPF_PASS }, 'pass',
-  );
+# 27-28: Trusted networks contain first header.
+#         Internal networks contain first and second header.
 
-  sarun ("-t < data/nice/spf3", \&patterns_run_cb);
-  ok_all_patterns();
+tstprefs("
+  clear_trusted_networks
+  clear_internal_networks
+  trusted_networks 65.214.43.157
+  internal_networks 65.214.43.157 64.142.3.173
+  always_trust_envelope_sender 1
+");
 
+%anti_patterns = ();
+%patterns = (
+  q{ -0.0 SPF_HELO_PASS }, 'helo_pass',
+  q{ -0.0 SPF_PASS }, 'pass',
+);
 
-  # 31-32: Trusted networks contain top 5 headers.
-  #       Internal networks contain top 2 headers.
+sarun ("-t < data/nice/spf2", \&patterns_run_cb);
+ok_all_patterns();
 
-  tstprefs("
-    clear_trusted_networks
-    clear_internal_networks
-    trusted_networks 65.214.43.158 64.142.3.173 65.214.43.155 65.214.43.156 65.214.43.157
-    internal_networks 65.214.43.158 64.142.3.173
-    always_trust_envelope_sender 1
-    $disable_an_spf_module
-  ");
 
-  %anti_patterns = ();
-  %patterns = (
-    q{ SPF_HELO_FAIL }, 'helo_fail',
-    q{ SPF_FAIL }, 'fail',
-  );
+# 29-30: Trusted networks contain top 5 headers.
+#         Internal networks contain first header.
 
-  sarun ("-t < data/nice/spf3", \&patterns_run_cb);
-  ok_all_patterns();
+tstprefs("
+  clear_trusted_networks
+  clear_internal_networks
+  trusted_networks 65.214.43.158 64.142.3.173 65.214.43.155 65.214.43.156 65.214.43.157
+  internal_networks 65.214.43.158
+  always_trust_envelope_sender 1
+");
 
+%anti_patterns = ();
+%patterns = (
+  q{ -0.0 SPF_HELO_PASS }, 'helo_pass',
+  q{ -0.0 SPF_PASS }, 'pass',
+);
 
-  # 33-34: Trusted networks contain top 5 headers.
-  #       Internal networks contain top 3 headers.
+sarun ("-t < data/nice/spf3", \&patterns_run_cb);
+ok_all_patterns();
 
-  tstprefs("
-    clear_trusted_networks
-    clear_internal_networks
-    trusted_networks 65.214.43.158 64.142.3.173 65.214.43.155 65.214.43.156 65.214.43.157
-    internal_networks 65.214.43.158 64.142.3.173 65.214.43.155
-    always_trust_envelope_sender 1
-    $disable_an_spf_module
-  ");
 
-  %anti_patterns = ();
-  %patterns = (
-    q{ SPF_HELO_SOFTFAIL }, 'helo_softfail',
-    q{ SPF_SOFTFAIL }, 'softfail',
-  );
+# 31-32: Trusted networks contain top 5 headers.
+#         Internal networks contain top 2 headers.
 
-  sarun ("-t < data/nice/spf3", \&patterns_run_cb);
-  ok_all_patterns();
+tstprefs("
+  clear_trusted_networks
+  clear_internal_networks
+  trusted_networks 65.214.43.158 64.142.3.173 65.214.43.155 65.214.43.156 65.214.43.157
+  internal_networks 65.214.43.158 64.142.3.173
+  always_trust_envelope_sender 1
+");
 
+%anti_patterns = ();
+%patterns = (
+  q{ 0.0 SPF_HELO_FAIL }, 'helo_fail',
+  q{ 0.0 SPF_FAIL }, 'fail',
+);
 
-  # 35-36: Trusted networks contain top 5 headers.
-  #       Internal networks contain top 4 headers.     
+sarun ("-t < data/nice/spf3", \&patterns_run_cb);
+ok_all_patterns();
 
-  tstprefs("
-    clear_trusted_networks
-    clear_internal_networks
-    trusted_networks 65.214.43.158 64.142.3.173 65.214.43.155 65.214.43.156 65.214.43.157
-    internal_networks 65.214.43.158 64.142.3.173 65.214.43.155 65.214.43.156
-    always_trust_envelope_sender 1
-    $disable_an_spf_module
-  ");
 
-  %anti_patterns = ();
-  %patterns = (
-    q{ SPF_HELO_NEUTRAL }, 'helo_neutral',
-    q{ SPF_NEUTRAL }, 'neutral',
-  );
+# 33-34: Trusted networks contain top 5 headers.
+#         Internal networks contain top 3 headers.
 
-  sarun ("-t < data/nice/spf3", \&patterns_run_cb);
-  ok_all_patterns();
+tstprefs("
+  clear_trusted_networks
+  clear_internal_networks
+  trusted_networks 65.214.43.158 64.142.3.173 65.214.43.155 65.214.43.156 65.214.43.157
+  internal_networks 65.214.43.158 64.142.3.173 65.214.43.155
+  always_trust_envelope_sender 1
+");
 
+%anti_patterns = ();
+%patterns = (
+  q{ 0.0 SPF_HELO_SOFTFAIL }, 'helo_softfail',
+  q{ 0.0 SPF_SOFTFAIL }, 'softfail',
+);
 
-  # 37-40: same as test 1-2 with some spf whitelisting added
+sarun ("-t < data/nice/spf3", \&patterns_run_cb);
+ok_all_patterns();
 
-  tstprefs("
-    whitelist_from_spf newsalerts-noreply\@dnsbltest.spamassassin.org
-    def_whitelist_from_spf *\@dnsbltest.spamassassin.org
-    $disable_an_spf_module
-  ");
 
-  %patterns = (
-    q{ SPF_HELO_PASS }, 'helo_pass',
-    q{ SPF_PASS }, 'pass',
-    q{ USER_IN_SPF_WHITELIST }, 'spf_whitelist',
-    q{ USER_IN_DEF_SPF_WL }, 'default_spf_whitelist',
-  );
+# 35-36: Trusted networks contain top 5 headers.
+#         Internal networks contain top 4 headers.     
 
-  sarun ("-t < data/nice/spf1", \&patterns_run_cb);
-  ok_all_patterns();
+tstprefs("
+  clear_trusted_networks
+  clear_internal_networks
+  trusted_networks 65.214.43.158 64.142.3.173 65.214.43.155 65.214.43.156 65.214.43.157
+  internal_networks 65.214.43.158 64.142.3.173 65.214.43.155 65.214.43.156
+  always_trust_envelope_sender 1
+");
 
+%anti_patterns = ();
+%patterns = (
+  q{ 0.0 SPF_HELO_NEUTRAL }, 'helo_neutral',
+  q{ 0.0 SPF_NEUTRAL }, 'neutral',
+);
 
-  # 41-44: same as test 1-2 with some spf whitelist entries that don't match
+sarun ("-t < data/nice/spf3", \&patterns_run_cb);
+ok_all_patterns();
 
-  tstprefs("
-    whitelist_from_spf *\@example.com
-    def_whitelist_from_spf nothere\@dnsbltest.spamassassin.org
-    $disable_an_spf_module
-  ");
 
-  %patterns = (
-    q{ SPF_HELO_PASS }, 'helo_pass',
-    q{ SPF_PASS }, 'pass',
-  );
+# 37-40: same as test 1-2 with some spf whitelisting added
 
-  %anti_patterns = (
-    q{ USER_IN_SPF_WHITELIST }, 'spf_whitelist',
-    q{ USER_IN_DEF_SPF_WL }, 'default_spf_whitelist',
-  );
+tstprefs("
+  whitelist_from_spf newsalerts-noreply\@dnsbltest.spamassassin.org
+  def_whitelist_from_spf *\@dnsbltest.spamassassin.org
+");
 
-  sarun ("-t < data/nice/spf1", \&patterns_run_cb);
-  ok_all_patterns();
+%patterns = (
+  q{ -0.0 SPF_HELO_PASS }, 'helo_pass',
+  q{ -0.0 SPF_PASS }, 'pass',
+  q{ -0.0 USER_IN_SPF_WHITELIST }, 'spf_whitelist',
+  q{ -0.0 USER_IN_DEF_SPF_WL }, 'default_spf_whitelist',
+);
 
-  # clear these out before we loop
-  %anti_patterns = ();
-  %patterns = ();
+sarun ("-t < data/nice/spf1", \&patterns_run_cb);
+ok_all_patterns();
 
 
-  # 45-48: same as test 37-40 with whitelist_auth added
+# 41-44: same as test 1-2 with some spf whitelist entries that don't match
 
-  tstprefs("
-    whitelist_auth newsalerts-noreply\@dnsbltest.spamassassin.org
-    def_whitelist_auth *\@dnsbltest.spamassassin.org
-    $disable_an_spf_module
-  ");
+tstprefs("
+  whitelist_from_spf *\@example.com
+  def_whitelist_from_spf nothere\@dnsbltest.spamassassin.org
+");
 
-  %patterns = (
-    q{ SPF_HELO_PASS }, 'helo_pass',
-    q{ SPF_PASS }, 'pass',
-    q{ USER_IN_SPF_WHITELIST }, 'spf_whitelist',
-    q{ USER_IN_DEF_SPF_WL }, 'default_spf_whitelist',
-  );
+%patterns = (
+  q{ -0.0 SPF_HELO_PASS }, 'helo_pass',
+  q{ -0.0 SPF_PASS }, 'pass',
+);
 
-  sarun ("-t < data/nice/spf1", \&patterns_run_cb);
-  ok_all_patterns();
+%anti_patterns = (
+  q{ USER_IN_SPF_WHITELIST }, 'spf_whitelist',
+  q{ USER_IN_DEF_SPF_WL }, 'default_spf_whitelist',
+);
 
-} # for each SPF module
+sarun ("-t < data/nice/spf1", \&patterns_run_cb);
+ok_all_patterns();
 
+# clear these out before we loop
+%anti_patterns = ();
+%patterns = ();
 
-# test to see if the plugin will select an SPF module on its own
 
-tstprefs("");
+# 45-48: same as test 37-40 with whitelist_auth added
+
+tstprefs("
+  whitelist_auth newsalerts-noreply\@dnsbltest.spamassassin.org
+  def_whitelist_auth *\@dnsbltest.spamassassin.org
+");
 
 %patterns = (
-    q{ SPF_HELO_PASS }, 'helo_pass',
-    q{ SPF_PASS }, 'pass',
+  q{ -0.0 SPF_HELO_PASS }, 'helo_pass',
+  q{ -0.0 SPF_PASS }, 'pass',
+  q{ -0.0 USER_IN_SPF_WHITELIST }, 'spf_whitelist',
+  q{ -0.0 USER_IN_DEF_SPF_WL }, 'default_spf_whitelist',
 );
 
 sarun ("-t < data/nice/spf1", \&patterns_run_cb);
@@ -430,12 +396,21 @@ tstprefs("
 
 %anti_patterns = ();
 %patterns = (
-  q{ SPF_HELO_PASS }, 'helo_pass',
-  q{ SPF_PASS }, 'pass',
+  q{ -0.0 SPF_HELO_PASS }, 'helo_pass',
+  q{ -0.0 SPF_PASS }, 'pass',
 );
 
 sarun ("-t < data/nice/spf3-received-spf", \&patterns_run_cb);
 ok_all_patterns();
+# Test same with nonfolded headers
+sarun ("-t < data/nice/spf4-received-spf-nofold", \&patterns_run_cb);
+ok_all_patterns();
+# Test same with crlf line endings
+sarun ("-t < data/nice/spf5-received-spf-crlf", \&patterns_run_cb);
+ok_all_patterns();
+# Test same with crlf line endings (bug 7785)
+sarun ("-t < data/nice/spf6-received-spf-crlf2", \&patterns_run_cb);
+ok_all_patterns();
 
 
 # test usage of Received-SPF headers added by internal relays
@@ -452,12 +427,15 @@ tstprefs("
 
 %anti_patterns = ();
 %patterns = (
-  q{ SPF_HELO_FAIL }, 'helo_fail_ignore_header',
-  q{ SPF_FAIL }, 'fail_ignore_header',
+  q{ 0.0 SPF_HELO_FAIL }, 'helo_fail_ignore_header',
+  q{ 0.0 SPF_FAIL }, 'fail_ignore_header',
 );
 
 sarun ("-t < data/nice/spf3-received-spf", \&patterns_run_cb);
 ok_all_patterns();
+# Test same with nonfolded headers
+sarun ("-t < data/nice/spf4-received-spf-nofold", \&patterns_run_cb);
+ok_all_patterns();
 
 
 # test usage of Received-SPF headers added by internal relays
@@ -473,12 +451,15 @@ tstprefs("
 
 %anti_patterns = ();
 %patterns = (
-  q{ SPF_HELO_SOFTFAIL }, 'helo_softfail_from_header',
-  q{ SPF_NEUTRAL }, 'neutral_from_header',
+  q{ 0.0 SPF_HELO_SOFTFAIL }, 'helo_softfail_from_header',
+  q{ 0.0 SPF_NEUTRAL }, 'neutral_from_header',
 );
 
 sarun ("-t < data/nice/spf3-received-spf", \&patterns_run_cb);
 ok_all_patterns();
+# Test same with nonfolded headers
+sarun ("-t < data/nice/spf4-received-spf-nofold", \&patterns_run_cb);
+ok_all_patterns();
 
 
 # test usage of Received-SPF headers added by internal relays
@@ -495,12 +476,14 @@ tstprefs("
 
 %anti_patterns = ();
 %patterns = (
-  q{ SPF_HELO_SOFTFAIL }, 'helo_softfail_from_header',
-  q{ SPF_FAIL }, 'fail_from_header',
+  q{ 0.0 SPF_HELO_SOFTFAIL }, 'helo_softfail_from_header',
+  q{ 0.0 SPF_FAIL }, 'fail_from_header',
 );
 
 sarun ("-t < data/nice/spf3-received-spf", \&patterns_run_cb);
-
+ok_all_patterns();
+# Test same with nonfolded headers
+sarun ("-t < data/nice/spf4-received-spf-nofold", \&patterns_run_cb);
 ok_all_patterns();
 
 
@@ -517,8 +500,8 @@ tstprefs("
 ");
 
 %patterns = (
-  q{ SPF_HELO_PASS }, 'helo_pass',
-  q{ SPF_PASS }, 'pass',
+  q{ -0.0 SPF_HELO_PASS }, 'helo_pass',
+  q{ -0.0 SPF_PASS }, 'pass',
 );
 
 %anti_patterns = (
diff --git a/upstream/t/spf_welcome_block.t b/upstream/t/spf_welcome_block.t
new file mode 100755 (executable)
index 0000000..5b67966
--- /dev/null
@@ -0,0 +1,511 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("spf_welcome_block");
+use Test::More;
+
+use constant HAS_MAILSPF => eval { require Mail::SPF; };
+
+plan skip_all => "Long running tests disabled" unless conf_bool('run_long_tests');
+plan skip_all => "Net tests disabled" unless conf_bool('run_net_tests');
+plan skip_all => "Need Mail::SPF" unless HAS_MAILSPF;
+plan skip_all => "Can't use Net::DNS Safely" unless can_use_net_dns_safely();
+
+plan tests => 72;
+
+# ---------------------------------------------------------------------------
+
+# ensure all rules will fire
+tstlocalrules ("
+  header SPF_PASS              eval:check_for_spf_pass()
+  header SPF_NEUTRAL           eval:check_for_spf_neutral()
+  header SPF_FAIL              eval:check_for_spf_fail()
+  header SPF_SOFTFAIL          eval:check_for_spf_softfail()
+  header SPF_HELO_PASS         eval:check_for_spf_helo_pass()
+  header SPF_HELO_NEUTRAL      eval:check_for_spf_helo_neutral()
+  header SPF_HELO_FAIL         eval:check_for_spf_helo_fail()
+  header SPF_HELO_SOFTFAIL     eval:check_for_spf_helo_softfail()
+  tflags SPF_PASS              nice userconf net
+  tflags SPF_HELO_PASS         nice userconf net
+  tflags SPF_NEUTRAL           net
+  tflags SPF_FAIL              net
+  tflags SPF_SOFTFAIL          net
+  tflags SPF_HELO_NEUTRAL       net
+  tflags SPF_HELO_FAIL         net
+  tflags SPF_HELO_SOFTFAIL     net
+  header USER_IN_SPF_WELCOMELIST eval:check_for_spf_welcomelist_from()
+  tflags USER_IN_SPF_WELCOMELIST userconf nice noautolearn net
+  header USER_IN_DEF_SPF_WL    eval:check_for_def_spf_welcomelist_from()
+  tflags USER_IN_DEF_SPF_WL    userconf nice noautolearn net
+  meta USER_IN_SPF_WHITELIST   (USER_IN_SPF_WELCOMELIST)
+  tflags USER_IN_SPF_WHITELIST userconf nice noautolearn net
+  score SPF_FAIL 0.001
+  score SPF_HELO_FAIL 0.001
+  score SPF_HELO_NEUTRAL 0.001
+  score SPF_HELO_SOFTFAIL 0.001
+  score SPF_NEUTRAL 0.001
+  score SPF_SOFTFAIL 0.001
+  score SPF_PASS -0.001
+  score SPF_HELO_PASS -0.001
+  score USER_IN_DEF_SPF_WL -0.001
+  score USER_IN_SPF_WELCOMELIST -0.001
+");
+
+%patterns = (
+  q{ -0.0 SPF_HELO_PASS }, 'helo_pass',
+  q{ -0.0 SPF_PASS }, 'pass',
+);
+
+sarun ("-t < data/nice/spf1", \&patterns_run_cb);
+ok_all_patterns();
+
+%patterns = (
+  q{ 0.0 SPF_NEUTRAL }, 'neutral',
+  q{ 0.0 SPF_HELO_NEUTRAL }, 'helo_neutral',
+);
+
+sarun ("-t < data/spam/spf1", \&patterns_run_cb);
+ok_all_patterns();
+
+%patterns = (
+  q{ 0.0 SPF_SOFTFAIL }, 'softfail',
+  q{ 0.0 SPF_HELO_SOFTFAIL }, 'helo_softfail',
+);
+
+sarun ("-t < data/spam/spf2", \&patterns_run_cb);
+ok_all_patterns();
+%patterns = (
+  q{ 0.0 SPF_FAIL }, 'fail',
+  q{ 0.0 SPF_HELO_FAIL }, 'helo_fail',
+);
+
+sarun ("-t < data/spam/spf3", \&patterns_run_cb);
+ok_all_patterns();
+
+
+# Test using an assortment of trusted and internal network definitions
+
+# 9-10: Trusted networks contain first header.
+
+tstprefs("
+  clear_trusted_networks
+  clear_internal_networks
+  trusted_networks 65.214.43.157
+  always_trust_envelope_sender 1
+");
+
+%patterns = (
+  q{ -0.0 SPF_HELO_PASS }, 'helo_pass',
+  q{ -0.0 SPF_PASS }, 'pass',
+);
+
+sarun ("-t < data/nice/spf2", \&patterns_run_cb);
+ok_all_patterns();
+
+
+# 11-12: Internal networks contain first header.
+#         Trusted networks not defined.
+
+tstprefs("
+  clear_trusted_networks
+  clear_internal_networks
+  internal_networks 65.214.43.157
+  always_trust_envelope_sender 1
+");
+
+%patterns = (
+  q{ -0.0 SPF_HELO_PASS }, 'helo_pass',
+  q{ -0.0 SPF_PASS }, 'pass',
+);
+
+sarun ("-t < data/nice/spf2", \&patterns_run_cb);
+ok_all_patterns();
+
+
+# 13-14: Internal networks contain first header.
+#         Trusted networks contain some other IP.
+#        jm: commented; this is now an error condition.
+
+tstprefs("
+  clear_trusted_networks
+  clear_internal_networks
+  trusted_networks 1.2.3.4
+  internal_networks 65.214.43.157
+  always_trust_envelope_sender 1
+");
+
+%patterns = (
+  q{ 0.0 SPF_HELO_NEUTRAL }, 'helo_neutral',
+  q{ 0.0 SPF_NEUTRAL }, 'neutral',
+);
+
+if (0) {
+  sarun ("-t < data/nice/spf2", \&patterns_run_cb);
+  ok_all_patterns();
+} else {
+  ok(1);        # skip the tests
+  ok(1);
+}
+
+
+# 15-16: Trusted+Internal networks contain first header.
+
+tstprefs("
+  clear_trusted_networks
+  clear_internal_networks
+  trusted_networks 65.214.43.157
+  internal_networks 65.214.43.157
+  always_trust_envelope_sender 1
+");
+
+%patterns = (
+  q{ -0.0 SPF_HELO_PASS }, 'helo_pass',
+  q{ -0.0 SPF_PASS }, 'pass',
+);
+
+sarun ("-t < data/nice/spf2", \&patterns_run_cb);
+ok_all_patterns();
+
+
+# 17-18: Trusted networks contain first and second header.
+#         Internal networks contain first header.
+
+tstprefs("
+  clear_trusted_networks
+  clear_internal_networks
+  trusted_networks 65.214.43.157 64.142.3.173
+  internal_networks 65.214.43.157
+  always_trust_envelope_sender 1
+");
+
+%patterns = (
+  q{ -0.0 SPF_HELO_PASS }, 'helo_pass',
+  q{ -0.0 SPF_PASS }, 'pass',
+);
+
+sarun ("-t < data/nice/spf2", \&patterns_run_cb);
+ok_all_patterns();
+
+
+# 19-26: Trusted networks contain first and second header.
+#         Internal networks contain first and second header.
+
+tstprefs("
+  clear_trusted_networks
+  clear_internal_networks
+  trusted_networks 65.214.43.157 64.142.3.173
+  internal_networks 65.214.43.157 64.142.3.173
+  always_trust_envelope_sender 1
+");
+
+%anti_patterns = (
+  q{ SPF_HELO_PASS }, 'helo_pass',
+  q{ SPF_HELO_FAIL }, 'helo_fail',
+  q{ SPF_HELO_SOFTFAIL }, 'helo_softfail',
+  q{ SPF_HELO_NEUTRAL }, 'helo_neutral',
+  q{ SPF_PASS }, 'pass',
+  q{ SPF_FAIL }, 'fail',
+  q{ SPF_SOFTFAIL }, 'softfail',
+  q{ SPF_NEUTRAL }, 'neutral',
+);
+%patterns = ();
+
+sarun ("-t < data/nice/spf2", \&patterns_run_cb);
+ok_all_patterns();
+
+
+# 27-28: Trusted networks contain first header.
+#         Internal networks contain first and second header.
+
+tstprefs("
+  clear_trusted_networks
+  clear_internal_networks
+  trusted_networks 65.214.43.157
+  internal_networks 65.214.43.157 64.142.3.173
+  always_trust_envelope_sender 1
+");
+
+%anti_patterns = ();
+%patterns = (
+  q{ -0.0 SPF_HELO_PASS }, 'helo_pass',
+  q{ -0.0 SPF_PASS }, 'pass',
+);
+
+sarun ("-t < data/nice/spf2", \&patterns_run_cb);
+ok_all_patterns();
+
+
+# 29-30: Trusted networks contain top 5 headers.
+#         Internal networks contain first header.
+
+tstprefs("
+  clear_trusted_networks
+  clear_internal_networks
+  trusted_networks 65.214.43.158 64.142.3.173 65.214.43.155 65.214.43.156 65.214.43.157
+  internal_networks 65.214.43.158
+  always_trust_envelope_sender 1
+");
+
+%anti_patterns = ();
+%patterns = (
+  q{ -0.0 SPF_HELO_PASS }, 'helo_pass',
+  q{ -0.0 SPF_PASS }, 'pass',
+);
+
+sarun ("-t < data/nice/spf3", \&patterns_run_cb);
+ok_all_patterns();
+
+
+# 31-32: Trusted networks contain top 5 headers.
+#         Internal networks contain top 2 headers.
+
+tstprefs("
+  clear_trusted_networks
+  clear_internal_networks
+  trusted_networks 65.214.43.158 64.142.3.173 65.214.43.155 65.214.43.156 65.214.43.157
+  internal_networks 65.214.43.158 64.142.3.173
+  always_trust_envelope_sender 1
+");
+
+%anti_patterns = ();
+%patterns = (
+  q{ 0.0 SPF_HELO_FAIL }, 'helo_fail',
+  q{ 0.0 SPF_FAIL }, 'fail',
+);
+
+sarun ("-t < data/nice/spf3", \&patterns_run_cb);
+ok_all_patterns();
+
+
+# 33-34: Trusted networks contain top 5 headers.
+#         Internal networks contain top 3 headers.
+
+tstprefs("
+  clear_trusted_networks
+  clear_internal_networks
+  trusted_networks 65.214.43.158 64.142.3.173 65.214.43.155 65.214.43.156 65.214.43.157
+  internal_networks 65.214.43.158 64.142.3.173 65.214.43.155
+  always_trust_envelope_sender 1
+");
+
+%anti_patterns = ();
+%patterns = (
+  q{ 0.0 SPF_HELO_SOFTFAIL }, 'helo_softfail',
+  q{ 0.0 SPF_SOFTFAIL }, 'softfail',
+);
+
+sarun ("-t < data/nice/spf3", \&patterns_run_cb);
+ok_all_patterns();
+
+
+# 35-36: Trusted networks contain top 5 headers.
+#         Internal networks contain top 4 headers.     
+
+tstprefs("
+  clear_trusted_networks
+  clear_internal_networks
+  trusted_networks 65.214.43.158 64.142.3.173 65.214.43.155 65.214.43.156 65.214.43.157
+  internal_networks 65.214.43.158 64.142.3.173 65.214.43.155 65.214.43.156
+  always_trust_envelope_sender 1
+");
+
+%anti_patterns = ();
+%patterns = (
+  q{ 0.0 SPF_HELO_NEUTRAL }, 'helo_neutral',
+  q{ 0.0 SPF_NEUTRAL }, 'neutral',
+);
+
+sarun ("-t < data/nice/spf3", \&patterns_run_cb);
+ok_all_patterns();
+
+
+# 37-40: same as test 1-2 with some spf whitelisting added
+
+tstprefs("
+  whitelist_from_spf newsalerts-noreply\@dnsbltest.spamassassin.org
+  def_whitelist_from_spf *\@dnsbltest.spamassassin.org
+");
+
+%patterns = (
+  q{ -0.0 SPF_HELO_PASS }, 'helo_pass',
+  q{ -0.0 SPF_PASS }, 'pass',
+  q{ -0.0 USER_IN_SPF_WELCOMELIST }, 'spf_whitelist',
+  q{ -0.0 USER_IN_DEF_SPF_WL }, 'default_spf_whitelist',
+);
+
+sarun ("-t < data/nice/spf1", \&patterns_run_cb);
+ok_all_patterns();
+
+
+# 41-44: same as test 1-2 with some spf whitelist entries that don't match
+
+tstprefs("
+  whitelist_from_spf *\@example.com
+  def_whitelist_from_spf nothere\@dnsbltest.spamassassin.org
+");
+
+%patterns = (
+  q{ -0.0 SPF_HELO_PASS }, 'helo_pass',
+  q{ -0.0 SPF_PASS }, 'pass',
+);
+
+%anti_patterns = (
+  q{ USER_IN_SPF_WELCOMELIST }, 'spf_whitelist',
+  q{ USER_IN_DEF_SPF_WL }, 'default_spf_whitelist',
+);
+
+sarun ("-t < data/nice/spf1", \&patterns_run_cb);
+ok_all_patterns();
+
+# clear these out before we loop
+%anti_patterns = ();
+%patterns = ();
+
+
+# 45-48: same as test 37-40 with welcomelist_auth added
+
+tstprefs("
+  welcomelist_auth newsalerts-noreply\@dnsbltest.spamassassin.org
+  def_welcomelist_auth *\@dnsbltest.spamassassin.org
+");
+
+%patterns = (
+  q{ -0.0 SPF_HELO_PASS }, 'helo_pass',
+  q{ -0.0 SPF_PASS }, 'pass',
+  q{ -0.0 USER_IN_SPF_WELCOMELIST }, 'spf_whitelist',
+  q{ -0.0 USER_IN_DEF_SPF_WL }, 'default_spf_whitelist',
+);
+
+sarun ("-t < data/nice/spf1", \&patterns_run_cb);
+ok_all_patterns();
+
+
+# test usage of Received-SPF headers added by internal relays
+# the Received-SPF headers shouldn't be used in this test
+
+tstprefs("
+  clear_trusted_networks
+  clear_internal_networks
+  trusted_networks 65.214.43.158
+  internal_networks 65.214.43.158
+  always_trust_envelope_sender 1
+");
+
+%anti_patterns = ();
+%patterns = (
+  q{ -0.0 SPF_HELO_PASS }, 'helo_pass',
+  q{ -0.0 SPF_PASS }, 'pass',
+);
+
+sarun ("-t < data/nice/spf3-received-spf", \&patterns_run_cb);
+ok_all_patterns();
+# Test same with nonfolded headers
+sarun ("-t < data/nice/spf4-received-spf-nofold", \&patterns_run_cb);
+ok_all_patterns();
+# Test same with crlf line endings
+sarun ("-t < data/nice/spf5-received-spf-crlf", \&patterns_run_cb);
+ok_all_patterns();
+# Test same with crlf line endings (bug 7785)
+sarun ("-t < data/nice/spf6-received-spf-crlf2", \&patterns_run_cb);
+ok_all_patterns();
+
+
+# test usage of Received-SPF headers added by internal relays
+# the Received-SPF headers shouldn't be used in this test
+
+tstprefs("
+  clear_trusted_networks
+  clear_internal_networks
+  trusted_networks 65.214.43.158 64.142.3.173
+  internal_networks 65.214.43.158 64.142.3.173
+  always_trust_envelope_sender 1
+  ignore_received_spf_header 1
+");
+
+%anti_patterns = ();
+%patterns = (
+  q{ 0.0 SPF_HELO_FAIL }, 'helo_fail_ignore_header',
+  q{ 0.0 SPF_FAIL }, 'fail_ignore_header',
+);
+
+sarun ("-t < data/nice/spf3-received-spf", \&patterns_run_cb);
+ok_all_patterns();
+# Test same with nonfolded headers
+sarun ("-t < data/nice/spf4-received-spf-nofold", \&patterns_run_cb);
+ok_all_patterns();
+
+
+# test usage of Received-SPF headers added by internal relays
+# the bottom 2 Received-SPF headers should be used in this test
+
+tstprefs("
+  clear_trusted_networks
+  clear_internal_networks
+  trusted_networks 65.214.43.158 64.142.3.173
+  internal_networks 65.214.43.158 64.142.3.173
+  always_trust_envelope_sender 1
+");
+
+%anti_patterns = ();
+%patterns = (
+  q{ 0.0 SPF_HELO_SOFTFAIL }, 'helo_softfail_from_header',
+  q{ 0.0 SPF_NEUTRAL }, 'neutral_from_header',
+);
+
+sarun ("-t < data/nice/spf3-received-spf", \&patterns_run_cb);
+ok_all_patterns();
+# Test same with nonfolded headers
+sarun ("-t < data/nice/spf4-received-spf-nofold", \&patterns_run_cb);
+ok_all_patterns();
+
+
+# test usage of Received-SPF headers added by internal relays
+# the top 2 Received-SPF headers should be used in this test
+
+tstprefs("
+  clear_trusted_networks
+  clear_internal_networks
+  trusted_networks 65.214.43.158 64.142.3.173
+  internal_networks 65.214.43.158 64.142.3.173
+  use_newest_received_spf_header 1
+  always_trust_envelope_sender 1
+");
+
+%anti_patterns = ();
+%patterns = (
+  q{ 0.0 SPF_HELO_SOFTFAIL }, 'helo_softfail_from_header',
+  q{ 0.0 SPF_FAIL }, 'fail_from_header',
+);
+
+sarun ("-t < data/nice/spf3-received-spf", \&patterns_run_cb);
+ok_all_patterns();
+# Test same with nonfolded headers
+sarun ("-t < data/nice/spf4-received-spf-nofold", \&patterns_run_cb);
+ok_all_patterns();
+
+
+# test unwelcomelist_auth and unwhitelist_from_spf
+
+tstprefs("
+  welcomelist_auth newsalerts-noreply\@dnsbltest.spamassassin.org
+  def_welcomelist_auth newsalerts-noreply\@dnsbltest.spamassassin.org
+  unwelcomelist_auth newsalerts-noreply\@dnsbltest.spamassassin.org
+
+  whitelist_from_spf *\@dnsbltest.spamassassin.org
+  def_whitelist_from_spf *\@dnsbltest.spamassassin.org
+  unwhitelist_from_spf *\@dnsbltest.spamassassin.org
+");
+
+%patterns = (
+  q{ -0.0 SPF_HELO_PASS }, 'helo_pass',
+  q{ -0.0 SPF_PASS }, 'pass',
+);
+
+%anti_patterns = (
+  q{ USER_IN_SPF_WELCOMELIST }, 'spf_whitelist',
+  q{ USER_IN_DEF_SPF_WL }, 'default_spf_whitelist',
+);
+
+sarun ("-t < data/nice/spf1", \&patterns_run_cb);
+ok_all_patterns();
+
diff --git a/upstream/t/sql_based_welcomelist.t b/upstream/t/sql_based_welcomelist.t
new file mode 100755 (executable)
index 0000000..efe5729
--- /dev/null
@@ -0,0 +1,144 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest;
+use Test::More;
+
+use constant HAS_DBI => eval { require DBI; };
+use constant HAS_DBD_SQLITE => eval { require DBD::SQLite; DBD::SQLite->VERSION(1.59_01); };
+
+use constant SQLITE => (HAS_DBI && HAS_DBD_SQLITE);
+use constant SQL => conf_bool('run_awl_sql_tests');
+
+plan skip_all => "run_awl_sql_tests not enabled or DBI/SQLite not found" unless (SQLITE || SQL);
+
+my $tests = 0;
+$tests += 23 if (SQLITE);
+$tests += 23 if (SQL);
+plan tests => $tests;
+
+diag "Note: If there is a failure it may be due to an incorrect SQL configuration." if (SQL);
+
+sa_t_init("sql_based_welcomelist");
+
+# only use rules defined here in tstprefs()
+clear_localrules();
+
+my $rules = q(
+    add_header all Status "_YESNO_, score=_SCORE_ required=_REQD_ tests=_TESTS_ autolearn=_AUTOLEARN_ version=_VERSION_"
+    # Needed for AWL to run
+    header AWL eval:check_from_in_auto_welcomelist()
+    priority AWL 1000
+    # Fixed message scores to keep track of correct scoring
+    body NICE_002 /happy mailing list/
+    score NICE_002 -1.2
+    body SPAM_004_007 /MAKE MONEY FAST/
+    score SPAM_004_007 5.5
+);
+
+if (SQLITE) {
+  my $dbh = DBI->connect("dbi:SQLite:dbname=$workdir/awl.db","","");
+  $dbh->do("
+  CREATE TABLE awl (
+    username varchar(100) NOT NULL default '',
+    email varchar(255) NOT NULL default '',
+    ip varchar(40) NOT NULL default '',
+    msgcount bigint NOT NULL default '0',
+    totscore float NOT NULL default '0',
+    signedby varchar(255) NOT NULL default '',
+    last_hit timestamp NOT NULL default CURRENT_TIMESTAMP,
+    PRIMARY KEY (username,email,signedby,ip)
+  );
+  ") or die "Failed to create $workdir/awl.db";
+
+  tstprefs ("
+    use_auto_welcomelist 1
+    auto_welcomelist_factory Mail::SpamAssassin::SQLBasedAddrList
+    user_awl_dsn dbi:SQLite:dbname=$workdir/awl.db
+    $rules
+  ");
+
+  run_awl();
+}
+
+if (SQL) {
+  my $dbconfig = '';
+  foreach my $setting (qw(
+      user_awl_dsn
+      user_awl_sql_username
+      user_awl_sql_password
+      user_awl_sql_table
+      )) {
+    my $val = conf($setting);
+    $dbconfig .= "$setting $val\n" if $val;
+  }
+
+  my $testuser = 'tstusr.'.$$.'.'.time();
+
+  tstprefs ("
+    use_auto_welcomelist 1
+    auto_welcomelist_factory Mail::SpamAssassin::SQLBasedAddrList
+    $dbconfig
+    user_awl_sql_override_username $testuser
+    $rules
+  ");
+
+  run_awl();
+}
+
+# ---------------------------------------------------------------------------
+sub run_awl {
+
+%is_nonspam_patterns = (
+  ' Subject: Re: [SAtalk] auto-whitelisting', 'subj',
+);
+%is_spam_patterns = (
+  'Subject: 4000           Your Vacation Winning !', 'subj',
+);
+%is_spam_patterns2 = (
+  ' X-Spam-Status: Yes', 'status',
+);
+
+%patterns = %is_nonspam_patterns;
+ok(sarun ("--remove-addr-from-welcomelist whitelist_test\@whitelist.spamassassin.taint.org", \&patterns_run_cb));
+
+# 3 times, to get into the welcomelist: # verify correct ip/score/msgcount from debug output
+%patterns = (%is_nonspam_patterns,
+  ('sql-based whitelist_test@whitelist.spamassassin.taint.org|144.137 scores 0.0, msgcount 0' => 'scores'));
+ok(sarun ("-L -t -D auto-welcomelist < data/nice/002 2>&1", \&patterns_run_cb));
+ok_all_patterns();
+%patterns = (%is_nonspam_patterns,
+  ('sql-based whitelist_test@whitelist.spamassassin.taint.org|144.137 scores -1.2, msgcount 1' => 'scores'));
+ok(sarun ("-L -t -D auto-welcomelist < data/nice/002 2>&1", \&patterns_run_cb));
+ok_all_patterns();
+%patterns = (%is_nonspam_patterns,
+  ('sql-based whitelist_test@whitelist.spamassassin.taint.org|144.137 scores -2.4, msgcount 2' => 'scores'));
+ok(sarun ("-L -t -D auto-welcomelist < data/nice/002 2>&1", \&patterns_run_cb));
+ok_all_patterns();
+
+# Now check
+%patterns = (%is_nonspam_patterns,
+  ('sql-based whitelist_test@whitelist.spamassassin.taint.org|144.137 scores -3.6, msgcount 3' => 'scores'));
+ok(sarun ("-L -t -D auto-welcomelist < data/nice/002 2>&1", \&patterns_run_cb));
+ok_all_patterns();
+
+%patterns = (%is_spam_patterns,
+  ('sql-based whitelist_test@whitelist.spamassassin.taint.org|144.137 scores -4.8, msgcount 4' => 'scores'));
+ok(sarun ("-L -t -D auto-welcomelist < data/spam/004 2>&1", \&patterns_run_cb));
+ok_all_patterns();
+
+# Should be raised after last spam
+%patterns = (%is_spam_patterns,
+  ('sql-based whitelist_test@whitelist.spamassassin.taint.org|144.137 scores 0.7, msgcount 5' => 'scores'));
+ok(sarun ("-L -t -D auto-welcomelist < data/spam/004 2>&1", \&patterns_run_cb));
+ok_all_patterns();
+
+%patterns = (%is_spam_patterns2,
+  ('sql-based whitelist_test@whitelist.spamassassin.taint.org|210.73 scores 0.0, msgcount 0' => 'scores'));
+ok(sarun ("-L -t -D auto-welcomelist < data/spam/007 2>&1", \&patterns_run_cb));
+ok_all_patterns();
+
+ok(sarun ("--remove-addr-from-welcomelist whitelist_test\@whitelist.spamassassin.taint.org", \&patterns_run_cb));
+
+}
+# ---------------------------------------------------------------------------
index c4194aa849405d00e348f2ac0dcdad68b4d6574b..97f01d96075a1a887fe0a46ee89e9d540677fb66 100755 (executable)
 
 use lib '.'; use lib 't';
 use SATest;
-
 use Test::More;
-plan skip_all => 'AWL SQL Tests not enabled.' if conf_bool('run_awl_sql_tests');
-plan tests => 11;
-diag "Note: Failure may be due to an incorrect config";
+
+use constant HAS_DBI => eval { require DBI; };
+use constant HAS_DBD_SQLITE => eval { require DBD::SQLite; DBD::SQLite->VERSION(1.59_01); };
+
+use constant SQLITE => (HAS_DBI && HAS_DBD_SQLITE);
+use constant SQL => conf_bool('run_awl_sql_tests');
+
+plan skip_all => "run_awl_sql_tests not enabled or DBI/SQLite not found" unless (SQLITE || SQL);
+
+my $tests = 0;
+$tests += 23 if (SQLITE);
+$tests += 23 if (SQL);
+plan tests => $tests;
+
+diag "Note: If there is a failure it may be due to an incorrect SQL configuration." if (SQL);
 
 sa_t_init("sql_based_whitelist");
 
-my $dbconfig = '';
-foreach my $setting (qw(
-                  user_awl_dsn
-                  user_awl_sql_username
-                  user_awl_sql_password
-                  user_awl_sql_table
-                ))
-{
-  my $val = conf($setting);
-  $dbconfig .= "$setting $val\n" if $val;
-}
+# only use rules defined here in tstprefs()
+clear_localrules();
+
+my $rules = q(
+    add_header all Status "_YESNO_, score=_SCORE_ required=_REQD_ tests=_TESTS_ autolearn=_AUTOLEARN_ version=_VERSION_"
+    # Needed for AWL to run
+    header AWL eval:check_from_in_auto_whitelist()
+    priority AWL 1000
+    # Fixed message scores to keep track of correct scoring
+    body NICE_002 /happy mailing list/
+    score NICE_002 -1.2
+    body SPAM_004_007 /MAKE MONEY FAST/
+    score SPAM_004_007 5.5
+);
 
-my $testuser = 'tstusr.'.$$.'.'.time();
+if (SQLITE) {
+  my $dbh = DBI->connect("dbi:SQLite:dbname=$workdir/awl.db","","");
+  $dbh->do("
+  CREATE TABLE awl (
+    username varchar(100) NOT NULL default '',
+    email varchar(255) NOT NULL default '',
+    ip varchar(40) NOT NULL default '',
+    msgcount bigint NOT NULL default '0',
+    totscore float NOT NULL default '0',
+    signedby varchar(255) NOT NULL default '',
+    last_hit timestamp NOT NULL default CURRENT_TIMESTAMP,
+    PRIMARY KEY (username,email,signedby,ip)
+  );
+  ") or die "Failed to create $workdir/awl.db";
+
+  tstprefs ("
+    use_auto_whitelist 1
+    auto_whitelist_factory Mail::SpamAssassin::SQLBasedAddrList
+    user_awl_dsn dbi:SQLite:dbname=$workdir/awl.db
+    $rules
+  ");
+
+  run_awl();
+}
 
-tstlocalrules ("
-use_auto_whitelist 1
-auto_whitelist_factory Mail::SpamAssassin::SQLBasedAddrList
-$dbconfig
-user_awl_sql_override_username $testuser
-");
+if (SQL) {
+  my $dbconfig = '';
+  foreach my $setting (qw(
+      user_awl_dsn
+      user_awl_sql_username
+      user_awl_sql_password
+      user_awl_sql_table
+      )) {
+    my $val = conf($setting);
+    $dbconfig .= "$setting $val\n" if $val;
+  }
+
+  my $testuser = 'tstusr.'.$$.'.'.time();
+
+  tstprefs ("
+    use_auto_whitelist 1
+    auto_whitelist_factory Mail::SpamAssassin::SQLBasedAddrList
+    $dbconfig
+    user_awl_sql_override_username $testuser
+    $rules
+  ");
+
+  run_awl();
+}
 
 # ---------------------------------------------------------------------------
+sub run_awl {
 
 %is_nonspam_patterns = (
-q{ Subject: Re: [SAtalk] auto-whitelisting}, 'subj',
+  ' Subject: Re: [SAtalk] auto-whitelisting', 'subj',
 );
 %is_spam_patterns = (
-q{Subject: 4000           Your Vacation Winning !}, 'subj',
+  'Subject: 4000           Your Vacation Winning !', 'subj',
 );
-
 %is_spam_patterns2 = (
-q{ X-Spam-Status: Yes}, 'status',
+  ' X-Spam-Status: Yes', 'status',
 );
 
-
 %patterns = %is_nonspam_patterns;
-
 ok(sarun ("--remove-addr-from-whitelist whitelist_test\@whitelist.spamassassin.taint.org", \&patterns_run_cb));
 
-# 3 times, to get into the whitelist:
-ok(sarun ("-L -t < data/nice/002", \&patterns_run_cb));
-ok(sarun ("-L -t < data/nice/002", \&patterns_run_cb));
-ok(sarun ("-L -t < data/nice/002", \&patterns_run_cb));
+# 3 times, to get into the whitelist: # verify correct ip/score/msgcount from debug output
+%patterns = (%is_nonspam_patterns,
+  ('sql-based whitelist_test@whitelist.spamassassin.taint.org|144.137 scores 0.0, msgcount 0' => 'scores'));
+ok(sarun ("-L -t -D auto-welcomelist < data/nice/002 2>&1", \&patterns_run_cb));
+ok_all_patterns();
+%patterns = (%is_nonspam_patterns,
+  ('sql-based whitelist_test@whitelist.spamassassin.taint.org|144.137 scores -1.2, msgcount 1' => 'scores'));
+ok(sarun ("-L -t -D auto-welcomelist < data/nice/002 2>&1", \&patterns_run_cb));
+ok_all_patterns();
+%patterns = (%is_nonspam_patterns,
+  ('sql-based whitelist_test@whitelist.spamassassin.taint.org|144.137 scores -2.4, msgcount 2' => 'scores'));
+ok(sarun ("-L -t -D auto-welcomelist < data/nice/002 2>&1", \&patterns_run_cb));
+ok_all_patterns();
 
 # Now check
-ok(sarun ("-L -t < data/nice/002", \&patterns_run_cb));
+%patterns = (%is_nonspam_patterns,
+  ('sql-based whitelist_test@whitelist.spamassassin.taint.org|144.137 scores -3.6, msgcount 3' => 'scores'));
+ok(sarun ("-L -t -D auto-welcomelist < data/nice/002 2>&1", \&patterns_run_cb));
 ok_all_patterns();
 
-%patterns = %is_spam_patterns;
-ok(sarun ("-L -t < data/spam/004", \&patterns_run_cb));
+%patterns = (%is_spam_patterns,
+  ('sql-based whitelist_test@whitelist.spamassassin.taint.org|144.137 scores -4.8, msgcount 4' => 'scores'));
+ok(sarun ("-L -t -D auto-welcomelist < data/spam/004 2>&1", \&patterns_run_cb));
 ok_all_patterns();
 
-%patterns = %is_spam_patterns2;
-ok(sarun ("-L -t < data/spam/007", \&patterns_run_cb));
+# Should be raised after last spam
+%patterns = (%is_spam_patterns,
+  ('sql-based whitelist_test@whitelist.spamassassin.taint.org|144.137 scores 0.7, msgcount 5' => 'scores'));
+ok(sarun ("-L -t -D auto-welcomelist < data/spam/004 2>&1", \&patterns_run_cb));
+ok_all_patterns();
+
+%patterns = (%is_spam_patterns2,
+  ('sql-based whitelist_test@whitelist.spamassassin.taint.org|210.73 scores 0.0, msgcount 0' => 'scores'));
+ok(sarun ("-L -t -D auto-welcomelist < data/spam/007 2>&1", \&patterns_run_cb));
 ok_all_patterns();
 
 ok(sarun ("--remove-addr-from-whitelist whitelist_test\@whitelist.spamassassin.taint.org", \&patterns_run_cb));
+
+}
+# ---------------------------------------------------------------------------
old mode 100644 (file)
new mode 100755 (executable)
index 8761507..8d42f65
@@ -1,16 +1,5 @@
 #!/usr/bin/perl -T
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_tests.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-    unshift(@INC, '../lib');
-  }
-}
-
 use lib '.'; use lib 't';
 use SATest; sa_t_init("stop_always_matching_regexps");
 use Test::More tests => 12;
index fb268e2107052ec9dfe850afab09d256c5ae92ee..e2f9c42f76448e4fceb4e1e0f56781b03c2fa743 100755 (executable)
@@ -4,13 +4,14 @@ use lib '.'; use lib 't';
 use SATest; sa_t_init("strip2");
 
 use Test::More;
-plan skip_all => 'Long running tests disabled' if conf_bool('run_long_tests');
+plan skip_all => 'Long running tests disabled' unless conf_bool('run_long_tests');
 plan tests => 98;
 
 # ---------------------------------------------------------------------------
 
 use File::Copy;
 use File::Compare qw(compare_text);
+use Text::Diff;
 
 my @files = qw(
        data/nice/crlf-endings
@@ -28,7 +29,6 @@ my $input;
 # Make sure all the files can do "report_safe 0" and "report_safe 1"
 foreach $input (@files) {
   tstprefs ("
-        $default_cf_lines
         report_safe 0
         body TEST_ALWAYS /./
         score TEST_ALWAYS 100
@@ -36,7 +36,7 @@ foreach $input (@files) {
 
   # create report_safe 0 output
   my $test_number = test_number();
-  my $d_input = "log/d.$testname/$test_number";
+  my $d_input = "$workdir/d.$testname/$test_number";
   unlink $d_input;
   ok sarun ("-L < $input");
 
@@ -46,16 +46,15 @@ foreach $input (@files) {
   {
     print "output: $d_input\n";
                $test_number = test_number();
-    my $d_output = "log/d.$testname/$test_number";
+    my $d_output = "$workdir/d.$testname/$test_number";
     unlink $d_output;
-    ok sarun ("-d < $d_input");
+    ok sarun ("-L -d < $d_input");
     ok (-f $d_output);
     ok(!compare_text($input,$d_output))
         or diffwarn( $input, $d_output );
   }
 
   tstprefs ("
-        $default_cf_lines
         report_safe 1
         body TEST_ALWAYS /./
         score TEST_ALWAYS 100
@@ -63,16 +62,16 @@ foreach $input (@files) {
 
   # create report_safe 1 and -t output
        $test_number = test_number();
-  $d_input = "log/d.$testname/$test_number";
+  $d_input = "$workdir/d.$testname/$test_number";
   unlink $d_input;
   ok sarun ("-L -t < $input");
   ok (-f $d_input);
   {
     print "output: $d_input\n";
                $test_number = test_number();
-    my $d_output = "log/d.$testname/$test_number";
+    my $d_output = "$workdir/d.$testname/$test_number";
     unlink $d_output;
-    ok sarun ("-d < $d_input");
+    ok sarun ("-L -d < $d_input");
     ok (-f $d_output);
     ok(!compare_text($input,$d_output))
         or diffwarn( $input, $d_output );
@@ -84,7 +83,6 @@ foreach $input (@files) {
 $input = $files[0];
 
 tstprefs ("
-        $default_cf_lines
         report_safe 2
         body TEST_ALWAYS /./
         score TEST_ALWAYS 100
@@ -92,16 +90,16 @@ tstprefs ("
 
 # create report_safe 2 output
 my $test_number = test_number();
-$d_input = "log/d.$testname/$test_number";
+$d_input = "$workdir/d.$testname/$test_number";
 unlink $d_input;
 ok sarun ("-L < $input");
 ok (-f $d_input);
 {
   print "output: $d_input\n";
   $test_number = test_number();
-  my $d_output = "log/d.$testname/$test_number";
+  my $d_output = "$workdir/d.$testname/$test_number";
   unlink $d_output;
-  ok sarun ("-d < $d_input");
+  ok sarun ("-L -d < $d_input");
   ok (-f $d_output);
   ok(!compare_text($input,$d_output))
         or diffwarn( $input, $d_output );
@@ -109,18 +107,17 @@ ok (-f $d_input);
 
 # Work directly on regular message, as though it was not spam
 $test_number = test_number();
-my $d_output = "log/d.$testname/$test_number";
+my $d_output = "$workdir/d.$testname/$test_number";
 unlink $d_output;
-ok sarun ("-d < $input");
+ok sarun ("-L -d < $input");
 ok (-f $d_output);
 ok(!compare_text($input,$d_output))
         or diffwarn( $input, $d_output );
 
-
 sub diffwarn {
   my ($f1, $f2) = @_;
-  print "# Diff is as follows:\n";
-  untaint_system "diff -u $f1 $f2";
+  print STDERR "# Diff is as follows:\n";
+  diff ($f1, $f2, { STYLE => 'unified', OUTPUT => \*STDERR });
   print "\n\n";
 }
 
index 5b50cc7620039c01b5731f0ade44bf1c775eabdc..43d3cc7402688fe8d3e63f32da10d0168317defb 100755 (executable)
@@ -4,7 +4,6 @@ use lib '.'; use lib 't';
 use SATest; sa_t_init("strip_no_subject");
 
 use Test::More;
-plan skip_all => "Long running tests disabled" unless conf_bool('run_long_tests');
 plan tests => 4;
 
 # ---------------------------------------------------------------------------
@@ -13,10 +12,9 @@ use File::Copy;
 use File::Compare qw(compare_text);
 
 my $INPUT = 'data/spam/014';
-my $MUNGED = 'log/strip_no_subject.munged';
+my $MUNGED = "$workdir/strip_no_subject.munged";
 
 tstprefs ("
-        $default_cf_lines
         report_safe 1
         rewrite_header subject ***SPAM***
        ");
@@ -24,9 +22,9 @@ tstprefs ("
 # create report_safe 1 and -t output
 sarun ("-L -t < $INPUT");
 my $test_number = test_number();
-if (move("log/d.$testname/$test_number", $MUNGED)) {
+if (move("$workdir/d.$testname/$test_number", $MUNGED)) {
   sarun ("-d < $MUNGED");
-  ok(!compare_text($INPUT,"log/d.$testname/$test_number"));
+  ok(!compare_text($INPUT,"$workdir/d.$testname/$test_number"));
 }
 else {
   warn "move failed: $!\n";
@@ -34,7 +32,6 @@ else {
 }
 
 tstprefs ("
-        $default_cf_lines
         report_safe 2
         rewrite_header subject ***SPAM***
        ");
@@ -42,9 +39,9 @@ tstprefs ("
 # create report_safe 2 output
 sarun ("-L < $INPUT");
 $test_number = test_number();
-if (move("log/d.$testname/$test_number", $MUNGED)) {
+if (move("$workdir/d.$testname/$test_number", $MUNGED)) {
   sarun ("-d < $MUNGED");
-  ok(!compare_text($INPUT,"log/d.$testname/$test_number"));
+  ok(!compare_text($INPUT,"$workdir/d.$testname/$test_number"));
 }
 else {
   warn "move failed: $!\n";
@@ -52,7 +49,6 @@ else {
 }
 
 tstprefs ("
-        $default_cf_lines
         report_safe 0
         rewrite_header subject ***SPAM***
        ");
@@ -60,9 +56,9 @@ tstprefs ("
 # create report_safe 0 output
 sarun ("-L < $INPUT");
 $test_number = test_number();
-if (move("log/d.$testname/$test_number", $MUNGED)) {
+if (move("$workdir/d.$testname/$test_number", $MUNGED)) {
   sarun ("-d < $MUNGED");
-  ok(!compare_text($INPUT,"log/d.$testname/$test_number"));
+  ok(!compare_text($INPUT,"$workdir/d.$testname/$test_number"));
 }
 else {
   warn "move failed: $!\n";
@@ -72,4 +68,4 @@ else {
 # Work directly on regular message, as though it was not spam
 sarun ("-d < $INPUT");
 $test_number = test_number();
-ok(!compare_text($INPUT,"log/d.$testname/$test_number"));
+ok(!compare_text($INPUT,"$workdir/d.$testname/$test_number"));
index 4d56dd124bc76cfd7929eee2c65aecb1b95dd405..c7a645e6a0db09d0e3583ffe06809d7498d4e9ef 100755 (executable)
@@ -7,22 +7,15 @@ use Test::More tests => 4;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-q{ Content-Type: text/html }, 'contenttype',
-q{ 
-  Sender: pertand@email.mondolink.com
-  Content-Type: text/html
-
-  <HTML></P>
-  }, 'startofbody',
-
-  q{Subject: "100% HERBALSENSATION"}, 'subj',
-
+  qr/^Content-Type: text\/html$/m, 'contenttype',
+  qr/\nSender: pertand\@email\.mondolink\.com\nContent-Type: text\/html\n\n<HTML><\/P>/, 'startofbody',
+  qr/^Subject: "100% HERBALSENSATION"$/m, 'subj',
 );
 
 tstprefs ( "
-rewrite_header subject *****SPAM*****
+  rewrite_header subject *****SPAM*****
 " );
 
 ok (sarun ("-d < data/spam/003", \&patterns_run_cb));
 ok_all_patterns();
+
index be21085a2a688e78a99df76048b9883c365193ae..17e2f0609ba74607bae6ddd64be50f0a9002c086 100755 (executable)
@@ -1,17 +1,5 @@
 #!/usr/bin/perl -T
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_tests.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-    unshift(@INC, '../lib');
-  }
-}
-
-$NO_SPAMD_REQUIRED=1;
 use lib '.'; use lib 't';
 use SATest; sa_t_init("tainted_msg");
 
@@ -24,7 +12,6 @@ plan tests => 9;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
   q{ tainted get_header found } => '',
   q{ tainted get_pristine found } => '',
   q{ tainted get_pristine_body found } => '',
@@ -33,12 +20,11 @@ plan tests => 9;
   q{ tainted get_visible_rendered_body_text_array found } => '',
   q{ tainted get_decoded_body_text_array found } => '',
   q{ tainted get_rendered_body_text_array found } => '',
-
 );
 %anti_patterns = ();
 
-tstlocalrules ("
-    loadplugin myTestPlugin ../../data/taintcheckplugin.pm
+tstprefs ("
+  loadplugin myTestPlugin ../../../data/taintcheckplugin.pm
 ");
 
 use Mail::SpamAssassin::Util;
diff --git a/upstream/t/testrules.yml b/upstream/t/testrules.yml
new file mode 100644 (file)
index 0000000..f8b5692
--- /dev/null
@@ -0,0 +1,21 @@
+---
+seq:
+  # run some common tests first
+  - par:
+    - t/all_modules.t
+    - t/basic*.t
+    - t/html*.t
+    - t/mime*.t
+    - t/uri*.t
+    - t/get*.t
+    - t/header*.t
+    - t/regexp*.t
+    - t/*dns*.t
+    - t/rule*.t
+  # tests that are not parallel-ready (will run in isolation)
+  - seq:
+    - t/extracttext.t
+    - t/spamd_prefork_stress.t
+    - t/spamd_prefork_stress_2.t
+  # rest of the tests
+  - par: **
index 1ef55eb73be9830242442f70a97cd0eea76aacdf..20e24cee6ed70630dac5ee7da835c1c28bb74b74 100755 (executable)
@@ -10,7 +10,7 @@ tstlocalrules ('
   body NATURAL /\btotally <br> natural/i
 ');
 
-%patterns = ( q{ NATURAL } => 'NATURAL',);
+%patterns = ( q{ 1.0 NATURAL } => 'NATURAL',);
 %anti_patterns = ();
 sarun ("-L -t < data/spam/badctype1", \&patterns_run_cb);
 ok_all_patterns();
@@ -19,3 +19,4 @@ ok_all_patterns();
 %anti_patterns = ( q{ NATURAL } => 'NATURAL',);
 sarun ("-L -t < data/spam/badctype2", \&patterns_run_cb);
 ok_all_patterns();
+
index 5fd86bace8d4e8fa083dd96433e6751029b93024..7f5202318cfd8cd4e091e9023c6947b6c71570f5 100755 (executable)
@@ -1,21 +1,5 @@
 #!/usr/bin/perl -T
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_tests.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-    unshift(@INC, '../lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
-
 use lib '.'; use lib 't';
 use SATest; sa_t_init("timeout");
 use Test::More tests => 33;
@@ -29,11 +13,16 @@ require Mail::SpamAssassin::Timeout;
 
 # attempt to circumvent an advice not to mix alarm() with sleep();
 # interaction between alarms and sleeps is unspecified;
-# select() might be restarted on a signal
+# select() might be restarted on a signal.
+# Windows alarm emulation works with sleep, doesn't work with select() timeouts
 #
 sub mysleep($) {
   my($dt) = @_;
-  select(undef, undef, undef, 0.1)  for 1..int(10*$dt);
+  if ($RUNNING_ON_WINDOWS) {
+    sleep($dt);
+  } else {
+    select(undef, undef, undef, 0.1) for 1..int(10*$dt);
+  }
 }
 
 my($r,$t,$t1,$t2);
index fb3d6d28c2adab3ebd723479fdcd21d5e68966bd..3c03d64118377b6bd92fea5dcc98eaeab9e9e101 100755 (executable)
@@ -1,29 +1,13 @@
 #!/usr/bin/perl -T
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_tests.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-    unshift(@INC, '../lib');
-  }
-}
-
 our $have_patricia = 0;
 eval {
   require Net::Patricia;
   Net::Patricia->VERSION(1.16);  # need AF_INET6 support
-  import Net::Patricia;
+  Net::Patricia->import;
   $have_patricia = 1;
 };
 
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
-
 use lib '.'; use lib 't';
 use SATest; sa_t_init("trust_path");
 
@@ -41,6 +25,13 @@ open(OLDERR, ">&STDERR") || die "Cannot copy STDERR file handle";
 # quiet "used only once" warnings
 1 if *OLDERR;
 
+tstlocalrules ("
+  clear_originating_ip_headers
+  originating_ip_headers X-Yahoo-Post-IP X-Originating-IP X-Apparently-From
+  originating_ip_headers X-SenderIP X-AOL-IP
+  originating_ip_headers X-MS-Exchange-CrossTenant-OriginalAttributedTenantConnectingIp
+");
+
 my @data = (
 
 # ---------------------------------------------------------------------------
@@ -672,7 +663,7 @@ while (1) {
   }
 
   my $sa = create_saobj({
-              userprefs_filename => "log/tst.cf",
+              userprefs_filename => $userrules,
               # debug => 1
             });
 
index d9277cd7d0f0f3c9a1fb37ba4c4cc08c07b8e98c..158863f5c62c77537491cb3729374b3e936e17f4 100755 (executable)
@@ -1,38 +1,29 @@
 #!/usr/bin/perl -w -T
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_tests.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
-
 use strict;
-use Test::More tests => 102;
+use Test::More;
 use lib '.'; use lib 't';
 use SATest; sa_t_init("uri");
 
+use constant HAS_LIBIDN => eval { require Net::LibIDN; };
+use constant HAS_LIBIDN2 => eval { require Net::LibIDN2; };
+
+my $tests = 104;
+$tests += 7 if (HAS_LIBIDN);
+$tests += 7 if (HAS_LIBIDN2);
+
+plan tests => $tests;
+
 use Mail::SpamAssassin;
 use Mail::SpamAssassin::HTML;
 use Mail::SpamAssassin::Util;
 
 ##############################################
 
-
-tstlocalrules ('
-
+tstlocalrules ("
   util_rb_2tld live.com
   util_rb_3tld three.3ldlive.com
-
-');
+");
 
 # initialize SpamAssassin
 my $sa = create_saobj({'dont_copy_prefs' => 1,
@@ -116,6 +107,33 @@ ok(try_domains('http://foo..bar@example.com', 'example.com'));
 ok(try_domains('bar..example.com', undef));
 ok(try_domains('http://example..com', undef));
 
+sub try_libidn {
+  ok(try_domains("Cin\x{E9}ma.ca", 'xn--cinma-dsa.ca'));
+  ok(try_domains("marcaespa\x{F1}a.es", 'xn--marcaespaa-19a.es'));
+  ok(try_domains("\x{E4}k\x{E4}slompolo.fi", 'xn--kslompolo-u2ab.fi'));
+  ok(try_domains("\N{U+00E4}k\N{U+00E4}slompolo.fi", 'xn--kslompolo-u2ab.fi'));
+  ok(try_domains("\x{C3}\x{A4}k\x{C3}\x{A4}slompolo.fi", 'xn--kslompolo-u2ab.fi'));
+  ok(try_domains("foo.xn--fiqs8s", 'foo.xn--fiqs8s'));
+  ok(try_domains("foo\x2e\xe9\xa6\x99\xe6\xb8\xaf", 'foo.xn--j6w193g'));
+}
+
+if (HAS_LIBIDN) {
+  $Mail::SpamAssassin::Util::have_libidn = 1;
+  $Mail::SpamAssassin::Util::have_libidn2 = 0;
+  try_libidn();
+}
+if (HAS_LIBIDN2) {
+  $Mail::SpamAssassin::Util::have_libidn = 0;
+  $Mail::SpamAssassin::Util::have_libidn2 = 1;
+  try_libidn();
+}
+
+# Without LibIDN, should not produce results,
+# as is_fqdn_valid() will fail
+$Mail::SpamAssassin::Util::have_libidn = 0;
+$Mail::SpamAssassin::Util::have_libidn2 = 0;
+ok(try_domains("Cin\x{E9}ma.ca", undef));
+
 ##############################################
 
 sub array_cmp {
@@ -130,7 +148,7 @@ sub array_cmp {
 sub try_canon {
   my($input, $expect) = @_;
   my $redirs = $sa->{conf}->{redirector_patterns};
-  my @input = sort { $a cmp $b } Mail::SpamAssassin::Util::uri_list_canonify($redirs, @{$input});
+  my @input = sort { $a cmp $b } Mail::SpamAssassin::Util::uri_list_canonicalize($redirs, $input, $sa->{registryboundaries});
   my @expect = sort { $a cmp $b } @{$expect};
 
   # output what we want/get for debugging
@@ -284,6 +302,12 @@ ok (try_canon([
    'http://foo/',
    'http://www.foo.com/',
        ]));
+# Bug 7891
+ok (try_canon([
+   'http://www.ch/',
+   ], [
+   'http://www.ch/'
+       ]));
 
 ##############################################
 
index eeb53afb521d12848b7b5ddddbc44c231d778953..6f680036ff40b450c0f33cdd9f6058798e377091 100755 (executable)
@@ -2,21 +2,6 @@
 
 # test URI redirecton patterns
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_names.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
-
 use strict;
 use lib '.'; use lib 't';
 use SATest; sa_t_init("uri_html");
old mode 100644 (file)
new mode 100755 (executable)
index 7801e48..b01c5bf
@@ -2,27 +2,12 @@
 
 # Tests for Bug #7591, which is actually a bug seen in the EL7 build of Perl.
 # The real root cause is obscure, so we test for the bug not the Perl version.
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_names.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
 
-use lib '.'; use lib 't';
 use strict;
+use lib '.'; use lib 't';
+use SATest; sa_t_init("uri_list");
 use Test::More tests=> 12;
 use Mail::SpamAssassin::Util;
-use SATest; sa_t_init("uri_list");
 use warnings;
 use Cwd;
 
@@ -126,8 +111,7 @@ http://host5.example.com
 http://host6.example.com
 
 EOT
-my $tmpdir = mk_safe_tmpdir();
-warn "temp dir is $tmpdir\n";
+my $tmpdir = $workdir;
 
 for my $mail  ($twoplus, $threeurls, $threeplus, $foururls, $fiveurls, $sixurls) {
   my @urls = grep(/\bhttp:/m,$mail);
@@ -146,8 +130,9 @@ for my $mail  ($twoplus, $threeurls, $threeplus, $foururls, $fiveurls, $sixurls)
   # this is ugly, but it actually demos the bug. 
   open (my $mfh, ">", "$tmpdir/msg");
   print $mfh "$mail";
-  my $haverules = (  -f "../rules/25_uribl.cf" ) ;
-  my  $sarcnt = qx/..\/spamassassin -D all < $tmpdir\/msg 2>&1 |grep -c 'uridnsbl:.*skip'/;
+  my $haverules = (  -f "../rules/25_uribl.cf" );
+  use vars qw($sarcnt);
+  sarun("-D all < $tmpdir/msg 2>&1", \&sarcount);
   # test isn't very useful without this component, but this will at least skip the subtest when it can't be run
   SKIP: {
     skip  "No rules found!\n", 1 if (! $haverules ); 
@@ -155,6 +140,8 @@ for my $mail  ($twoplus, $threeurls, $threeplus, $foururls, $fiveurls, $sixurls)
       warn "Simple grep for http:// found $count URLs, get_uri_list found $ulcnt URLs, spamassassin script found $sarcnt\n";
     }
   }
+  sub sarcount {
+    $sarcnt = grep(/uridnsbl:.*skip/, <IN>);
+  }
 }
 
-cleanup_safe_tmpdir();
index 1bb32daf7c63d88c26c369c6fba0383f9b415377..be2f89d05ea6b0cf83b20525b50dc05c1fa94b5b 100755 (executable)
@@ -2,21 +2,6 @@
 
 # test URI redirecton patterns
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_names.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
-
 use strict;
 use lib '.'; use lib 't';
 use SATest; sa_t_init("uri_html");
index d5c7e717e0b2b9db9fd04e53dac2df39740e543c..3f849d79495590d0419ab583c465cc056d4ebe43 100755 (executable)
@@ -2,21 +2,6 @@
 
 # test URIs as grabbed from text/plain messages
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_names.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
-
 use strict;
 use lib '.'; use lib 't';
 use SATest; sa_t_init("uri_text");
@@ -27,8 +12,8 @@ use vars qw(%patterns %anti_patterns);
 # initialize SpamAssassin
 my $sa = create_saobj({
     require_rules => 0,
-    site_rules_filename => "$prefix/t/log/localrules.tmp",
-    rules_filename => "$prefix/rules",
+    site_rules_filename => $siterules,
+    rules_filename => $localrules,
     local_tests_only => 1,
     dont_copy_prefs => 1,
 });
index 6fcb5d89ca7939b406a6d72c256b8bdfacdadbb3..bd31d2411e184b03b255c8474a97e7b7087873a7 100755 (executable)
@@ -4,23 +4,28 @@ use lib '.'; use lib 't';
 use SATest; sa_t_init("uribl");
 
 use Test::More;
-plan skip_all => "Long running tests disabled" unless conf_bool('run_long_tests');
 plan skip_all => "Net tests disabled"          unless conf_bool('run_net_tests');
 plan skip_all => "Can't use Net::DNS Safely"   unless can_use_net_dns_safely();
-plan tests => 7;
+
+# run many times to catch some random natured failures
+my $iterations = 5;
+plan tests => 10 * $iterations;
 
 # ---------------------------------------------------------------------------
 
 %patterns = (
- q{ X_URIBL_A } => 'A',
- q{ X_URIBL_B } => 'B',
- q{ X_URIBL_NS } => 'NS',
- q{ X_URIBL_DOMSONLY } => 'X_URIBL_DOMSONLY',
- q{ META_URIBL_A } => 'META_URIBL_A',
+ q{ 1.0 X_URIBL_A } => '',
+ q{ 1.0 X_URIBL_B } => '',
+ q{ 1.0 X_URIBL_NS } => '',
+ q{ 1.0 X_URIBL_DOMSONLY } => '',
+ q{ 1.0 META_URIBL_A } => '',
+ q{ 1.0 META_URIBL_B } => '',
+ q{ 1.0 META_URIBL_NS } => '',
+ q{ 1.0 X_URIBL_NOTRIM } => '',
 );
 
 %anti_patterns = (
- q{ X_URIBL_FULL_NS } => 'FULL_NS',
+ q{ X_URIBL_FULL_NS } => '',
 );
 
 tstlocalrules(q{
@@ -51,10 +56,24 @@ tstlocalrules(q{
 
   # Bug 7897 - test that meta rules depending on net rules hit
   meta META_URIBL_A X_URIBL_A
+  # It also needs to hit even if priority is lower than dnsbl (-100)
+  meta META_URIBL_B X_URIBL_B
+  priority META_URIBL_B -500
+  # Or super high
+  meta META_URIBL_NS X_URIBL_NS
+  priority META_URIBL_NS 2000
+  priority X_URIBL_NS 2000
+
+  # Bug 7835 - tflags notrim
+  urirhssub  X_URIBL_NOTRIM  dnsbltest.spamassassin.org.    A 16
+  body       X_URIBL_NOTRIM  eval:check_uridnsbl('X_URIBL_NOTRIM')
+  tflags     X_URIBL_NOTRIM  net domains_only notrim
 
 });
 
-# note: don't leave -D here, it causes spurious passes
-ok sarun ("-t < data/spam/dnsbl.eml 2>&1", \&patterns_run_cb);
-ok_all_patterns();
+for (1 .. $iterations) {
+  clear_localrules() if $_ == 3; # do some tests without any other rules to check meta bugs
+  ok sarun ("-t < data/spam/dnsbl.eml", \&patterns_run_cb);
+  ok_all_patterns();
+}
 
old mode 100644 (file)
new mode 100755 (executable)
index 4fa289b..2851608
@@ -6,7 +6,6 @@ use lib '.'; use lib 't';
 use SATest; sa_t_init("uribl_all_types");
 
 use Test::More;
-plan skip_all => "Long running tests disabled" unless conf_bool('run_long_tests');
 plan skip_all => "Net tests disabled"                unless conf_bool('run_net_tests');
 plan skip_all => "Can't use Net::DNS Safely"   unless can_use_net_dns_safely();
 plan tests => 3;
@@ -14,12 +13,9 @@ plan tests => 3;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-   q{ X_URIBL_IPSONLY [URIs: 144.137.3.98] } => 'X_URIBL_IPSONLY',
-
-   # can be either uribl-example-b.com or uribl-example-c.com
-   q{ X_URIBL_DOMSONLY [URIs: uribl-example} => 'X_URIBL_DOMSONLY',
-
+ q{ X_URIBL_IPSONLY [URI: 144.137.3.98] } => 'X_URIBL_IPSONLY',
+ # can be either uribl-example-b.com or uribl-example-c.com
+ q{ X_URIBL_DOMSONLY [URI: uribl-example} => 'X_URIBL_DOMSONLY',
 );
 
 tstlocalrules(q{
old mode 100644 (file)
new mode 100755 (executable)
index 5115eae..b3925d4
@@ -5,14 +5,15 @@ use lib '.'; use lib 't';
 use SATest; sa_t_init("uribl_domains_only");
 
 use Test::More;
-plan skip_all => "Long running tests disabled" unless conf_bool('run_long_tests');
 plan skip_all => "Net tests disabled"          unless conf_bool('run_net_tests');
 plan skip_all => "Can't use Net::DNS Safely"   unless can_use_net_dns_safely();
 plan tests => 4;
 
 # ---------------------------------------------------------------------------
 
-%anti_patterns = ( q{ X_URIBL_DOMSONLY } => 'A' );
+%anti_patterns = (
+  q{ X_URIBL_DOMSONLY } => 'A'
+);
 
 tstlocalrules(q{
 
@@ -30,7 +31,7 @@ tstlocalrules(q{
 ok sarun ("-t < data/spam/dnsbl_domsonly.eml 2>&1", \&patterns_run_cb);
 ok_all_patterns();
 
-%patterns = ( q{ X_URIBL_DOMSONLY } => 'A' );
+%patterns = ( q{ 1.0 X_URIBL_DOMSONLY } => 'A' );
 %anti_patterns = ();
 
 clear_pattern_counters();
old mode 100644 (file)
new mode 100755 (executable)
index c220422..5ffc4bc
@@ -5,7 +5,6 @@ use lib '.'; use lib 't';
 use SATest; sa_t_init("uribl_ips_only");
 
 use Test::More;
-plan skip_all => "Long running tests disabled" unless conf_bool('run_long_tests');
 plan skip_all => "Net tests disabled"          unless conf_bool('run_net_tests');
 plan skip_all => "Can't use Net::DNS Safely"   unless can_use_net_dns_safely();
 plan tests => 4;
@@ -13,7 +12,7 @@ plan tests => 4;
 # ---------------------------------------------------------------------------
 
 %anti_patterns = (
- q{ X_URIBL_IPSONLY } => 'A',
 q{ X_URIBL_IPSONLY } => 'A',
 );
 
 tstlocalrules(q{
@@ -32,7 +31,7 @@ tstlocalrules(q{
 ok sarun ("-t < data/spam/dnsbl_ipsonly.eml 2>&1", \&patterns_run_cb);
 ok_all_patterns();
 
-%patterns = ( q{ X_URIBL_IPSONLY } => 'A' );
+%patterns = ( q{ 1.0 X_URIBL_IPSONLY } => 'A' );
 %anti_patterns = ();
 
 clear_pattern_counters();
diff --git a/upstream/t/urilocalbl.t b/upstream/t/urilocalbl.t
new file mode 100755 (executable)
index 0000000..b9ae950
--- /dev/null
@@ -0,0 +1,186 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("urilocalbl");
+
+$tests = 0;
+eval { require MaxMind::DB::Reader;   $tests += 8; $has{GEOIP2}  = 1 };
+eval { require Geo::IP;               $tests += 8; $has{GEOIP}   = 1 };
+eval { require IP::Country::DB_File;  $tests += 8; $has{DB_FILE} = 1 };
+eval { require IP::Country::Fast;     $tests += 8; $has{FAST}    = 1 };
+
+use Test::More;
+
+plan skip_all => "Net tests disabled" unless conf_bool('run_net_tests');
+plan skip_all => "No supported GeoDB module installed" unless $tests;
+
+$net = conf_bool('run_net_tests');
+$ipv6 = $net && conf_bool('run_ipv6_dns_tests');
+
+$tests *= 2 if $net;
+$tests += 1 if $ipv6 && defined $has{GEOIP2};
+$tests += 1 if $ipv6 && defined $has{DB_FILE};
+
+plan tests => $tests;
+
+# ---------------------------------------------------------------------------
+
+tstpre ("
+loadplugin Mail::SpamAssassin::Plugin::URILocalBL
+");
+
+%patterns_ipv4 = (
+  q{ X_URIBL_USA } => 'USA',
+  q{ X_URIBL_FINEG } => 'except Finland',
+  q{ X_URIBL_NA } => 'north America',
+  q{ X_URIBL_EUNEG } => 'except Europe',
+  q{ X_URIBL_CIDR1 } => 'our TestIP1',
+  q{ X_URIBL_CIDR2 } => 'our TestIP2',
+  q{ X_URIBL_CIDR3 } => 'our TestIP3',
+);
+
+%patterns_ipv6 = (
+  q{ X_URIBL_CIDR4 } => 'our TestIP4',
+);
+
+my $rules = "
+
+  dns_query_restriction allow google.com
+
+  uri_block_cc X_URIBL_USA us
+  describe X_URIBL_USA uri located in USA
+  
+  uri_block_cc X_URIBL_FINEG !fi
+  describe X_URIBL_FINEG uri located anywhere except Finland
+
+  uri_block_cont X_URIBL_NA na
+  describe X_URIBL_NA uri located in north America
+
+  uri_block_cont X_URIBL_EUNEG !eu !af
+  describe X_URIBL_EUNEG uri located anywhere except Europe/Africa
+
+  uri_block_cidr X_URIBL_CIDR1 8.0.0.0/8 1.2.3.4
+  describe X_URIBL_CIDR1 uri is our TestIP1
+
+  uri_block_cidr X_URIBL_CIDR2 8.8.8.8
+  describe X_URIBL_CIDR2 uri is our TestIP2
+
+  uri_block_cidr X_URIBL_CIDR3 8.8.8.0/24
+  describe X_URIBL_CIDR3 uri is our TestIP3
+";
+
+my $rules_ipv6 = "
+
+  uri_block_cidr X_URIBL_CIDR4 2001:4860:4860::8888
+  describe X_URIBL_CIDR4 uri is our TestIP4
+";
+
+if (defined $has{GEOIP2}) {
+  my $lrules = "
+    geodb_module GeoIP2
+    geodb_search_path data/geodb
+    $rules
+  ";
+  tstlocalrules ($lrules);
+  %patterns = %patterns_ipv4;
+  ok sarun ("-L -t < data/spam/relayUS.eml", \&patterns_run_cb);
+  ok_all_patterns();
+  clear_pattern_counters();
+
+  if ($net) {
+    $lrules .= $rules_ipv6 if $ipv6;
+    tstlocalrules ($lrules);
+    if ($ipv6) {
+      %patterns = (%patterns_ipv4, %patterns_ipv6);
+    } else {
+      %patterns = %patterns_ipv4;
+      warn "skipping IPv6 DNS lookup tests (run_ipv6_dns_tests=n)\n";
+    }
+    ok sarun ("-t < data/spam/urilocalbl_net.eml", \&patterns_run_cb);
+    ok_all_patterns();
+    clear_pattern_counters();
+  } else {
+    warn "skipping DNS lookup tests (run_net_tests=n)\n";
+  }
+} else {
+  warn "skipping MaxMind::DB::Reader (GeoIP2) tests (not installed)\n";
+}
+
+
+if (defined $has{GEOIP}) {
+  tstlocalrules ("
+    geodb_module Geo::IP
+    geodb_search_path data/geodb
+    $rules
+  ");
+  %patterns = %patterns_ipv4;
+  ok sarun ("-L -t < data/spam/relayUS.eml", \&patterns_run_cb);
+  ok_all_patterns();
+  clear_pattern_counters();
+
+  if ($net) {
+    ok sarun ("-t < data/spam/urilocalbl_net.eml", \&patterns_run_cb);
+    ok_all_patterns();
+    clear_pattern_counters();
+  } else {
+    warn "skipping DNS lookup tests (run_net_tests=n)\n";
+  }
+} else {
+  warn "skipping Geo::IP tests (not installed)\n";
+}
+
+
+if (defined $has{DB_FILE}) {
+  my $lrules = "
+    geodb_module DB_File
+    geodb_options country:data/geodb/ipcc.db
+    $rules
+  ";
+  tstlocalrules ($lrules);
+  %patterns = %patterns_ipv4;
+  ok sarun ("-L -t < data/spam/relayUS.eml", \&patterns_run_cb);
+  ok_all_patterns();
+  clear_pattern_counters();
+
+  if ($net) {
+    $lrules .= $rules_ipv6 if $ipv6;
+    tstlocalrules ($lrules);
+    if ($ipv6) {
+      %patterns = (%patterns_ipv4, %patterns_ipv6);
+    } else {
+      %patterns = %patterns_ipv4;
+      warn "skipping IPv6 DNS lookup tests (run_ipv6_dns_tests=n)\n";
+    }
+    ok sarun ("-t < data/spam/urilocalbl_net.eml", \&patterns_run_cb);
+    ok_all_patterns();
+    clear_pattern_counters();
+  } else {
+    warn "skipping DNS lookup tests (run_net_tests=n)\n";
+  }
+} else {
+  warn "skipping IP::Country::DB_File tests (not installed)\n";
+}
+
+
+if (defined $has{FAST}) {
+  tstlocalrules ("
+    geodb_module Fast
+    $rules
+  ");
+  %patterns = %patterns_ipv4;
+  ok sarun ("-L -t < data/spam/relayUS.eml", \&patterns_run_cb);
+  ok_all_patterns();
+  clear_pattern_counters();
+
+  if ($net) {
+    ok sarun ("-t < data/spam/urilocalbl_net.eml", \&patterns_run_cb);
+    ok_all_patterns();
+    clear_pattern_counters();
+  } else {
+    warn "skipping DNS lookup tests (run_net_tests=n)\n";
+  }
+} else {
+  warn "skipping IP::Country::Fast tests (not installed)\n";
+}
+
+
diff --git a/upstream/t/urilocalbl_geoip.t b/upstream/t/urilocalbl_geoip.t
deleted file mode 100755 (executable)
index f328009..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-#!/usr/bin/perl -T
-
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_tests.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-    unshift(@INC, '../lib');
-  }
-}
-
-use lib '.'; use lib 't';
-use SATest; sa_t_init("urilocalbl");
-
-use constant HAS_CIDR_LITE => eval { require Net::CIDR::Lite; };
-use constant HAS_GEOIP => eval { require Geo::IP; };
-use constant HAS_GEOIP_CONF => eval { Geo::IP->new(); };
-
-use Test::More;
-
-plan skip_all => "Net::CIDR::Lite not installed" unless HAS_CIDR_LITE;
-plan skip_all => "Geo::IP not installed" unless HAS_GEOIP;
-plan skip_all => "Geo::IP not configured" unless HAS_GEOIP_CONF;
-plan tests => 3;
-
-# ---------------------------------------------------------------------------
-
-tstpre ("
-loadplugin Mail::SpamAssassin::Plugin::URILocalBL
-");
-
-%patterns = (
-  q{ X_URIBL_USA } => 'USA',
-  q{ X_URIBL_NA } => 'north America',
-);
-
-tstlocalrules (q{
-  dns_available no
-  uri_block_cc X_URIBL_USA us
-  describe X_URIBL_USA uri located in USA
-  
-  uri_block_cont X_URIBL_NA na
-  describe X_URIBL_NA uri located in north America
-});
-
-ok sarun ("-t < data/spam/relayUS.eml", \&patterns_run_cb);
-ok_all_patterns();
index 77243a9f118cad228bbb550c1210e5b66a93a51c..075d79e8843ad51e8322add4a728718812ea00ef 100755 (executable)
@@ -1,18 +1,55 @@
 #!/usr/bin/perl -T
 
+###
+### UTF-8 CONTENT, edit with UTF-8 locale/editor
+###
+
 use lib '.'; use lib 't';
 use SATest; sa_t_init("utf8");
-use Test::More tests => 4;
+use Test::More tests => 14;
 
 # ---------------------------------------------------------------------------
 
 %patterns = (
+  q{ X-Spam-Status: Yes, score=}, 'status',
+  q{ X-Spam-Flag: YES}, 'flag',
+  q{ X-Spam-Level: ****}, 'stars',
+);
+%anti_patterns = ();
+
+ok (sarun ("-L -t < data/spam/009", \&patterns_run_cb));
+ok_all_patterns();
+
+# ---------------------------------------------------------------------------
 
-q{ X-Spam-Status: Yes, score=}, 'status',
-q{ X-Spam-Flag: YES}, 'flag',
-q{ X-Spam-Level: ****}, 'stars',
+my $rules = '
+  body FOO1 /金融機/
+  body FOO2 /金融(?:xyz|機)/
+  body FOO3 /\xe9\x87\x91\xe8\x9e\x8d\xe6\xa9\x9f/
+  body FOO4 /.\x87(?:\x91|\x00)[\xe8\x00]\x9e\x8d\xe6\xa9\x9f/
+';
 
+%patterns = (
+  q{ 1.0 FOO1 }, '',
+  q{ 1.0 FOO2 }, '',
+  q{ 1.0 FOO3 }, '',
+  q{ 1.0 FOO4 }, '',
 );
+%anti_patterns = ();
 
-ok (sarun ("-L -t < data/spam/009", \&patterns_run_cb));
+# normalize_charset 1
+tstprefs("
+  $rules
+  normalize_charset 1
+");
+ok (sarun ("-L -t < data/spam/unicode1", \&patterns_run_cb));
 ok_all_patterns();
+
+# normalize_charset 0
+tstprefs("
+  $rules
+  normalize_charset 0
+");
+ok (sarun ("-L -t < data/spam/unicode1", \&patterns_run_cb));
+ok_all_patterns();
+
index d40553bb304522df27496901ba0ccf2a29ae80fd..b7916b9aa80e60b99d3607667e553fa41618a756 100755 (executable)
@@ -1,21 +1,5 @@
 #!/usr/bin/perl -T
 
-BEGIN {
-  if (-e 't/test_dir') { # if we are running "t/rule_tests.t", kluge around ...
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {            # running from test directory, not ..
-    unshift(@INC, '../blib/lib');
-    unshift(@INC, '../lib');
-  }
-}
-
-my $prefix = '.';
-if (-e 'test_dir') {            # running from test directory, not ..
-  $prefix = '..';
-}
-
 use lib '.'; use lib 't';
 use SATest; sa_t_init("util_wrap");
 use Test::More tests => 5;
diff --git a/upstream/t/welcomelist_addrs.t b/upstream/t/welcomelist_addrs.t
new file mode 100755 (executable)
index 0000000..8716adc
--- /dev/null
@@ -0,0 +1,244 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("welcomelist_addrs");
+use IO::File;
+
+use constant HAS_DB_FILE => eval { require DB_File };
+
+use Test::More;
+plan skip_all => 'Long running tests disabled' unless conf_bool('run_long_tests');
+plan skip_all => 'Need DB_File for this test'  unless HAS_DB_FILE;
+plan tests => 35;
+
+# ---------------------------------------------------------------------------
+
+tstprefs ("
+  header AWL        eval:check_from_in_auto_welcomelist()
+  tflags AWL        userconf noautolearn
+  priority AWL      1000
+");
+
+%added_address_welcomelist_patterns = (
+  q{SpamAssassin auto-welcomelist: adding address to welcomelist:}, 'added address to welcomelist',
+);
+%added_address_blocklist_patterns = (
+  q{SpamAssassin auto-welcomelist: adding address to blocklist:}, 'added address to blocklist',
+);
+%removed_address_patterns = (
+  q{SpamAssassin auto-welcomelist: removing address:}, 'removed address',
+);
+%is_nonspam_patterns = (
+  q{X-Spam-Status: No}, 'spamno',
+);
+%is_spam_patterns = (
+  q{X-Spam-Status: Yes}, 'spamyes',
+);
+
+
+%patterns = %added_address_welcomelist_patterns;
+ok(sarun ("--add-addr-to-welcomelist whitelist_test\@whitelist.spamassassin.taint.org", \&patterns_run_cb));
+ok_all_patterns();
+%patterns = %is_nonspam_patterns;
+ok (sarun ("-L < data/nice/002", \&patterns_run_cb));
+ok_all_patterns();
+%patterns = %is_nonspam_patterns;
+sarun ("-L < data/spam/004", \&patterns_run_cb);
+ok_all_patterns();
+
+%patterns = %removed_address_patterns;
+ok(sarun ("--remove-addr-from-welcomelist whitelist_test\@whitelist.spamassassin.taint.org", \&patterns_run_cb));
+ok_all_patterns();
+%patterns = %is_spam_patterns;
+sarun ("-L < data/spam/004", \&patterns_run_cb);
+ok_all_patterns();
+
+%patterns = %added_address_blocklist_patterns;
+ok(sarun ("--add-addr-to-blocklist whitelist_test\@whitelist.spamassassin.taint.org", \&patterns_run_cb));
+ok_all_patterns();
+%patterns = %is_spam_patterns;
+sarun ("-L < data/nice/002", \&patterns_run_cb);
+ok_all_patterns();
+
+ok(sarun ("--remove-addr-from-welcomelist whitelist_test\@whitelist.spamassassin.taint.org", \&patterns_run_cb));
+
+
+# The following section tests the object oriented interface to adding/removing welcomelist
+# and blocklist entries.  Primarily this is testing basic functionality and that the
+# "print" commands that are present in the command line interface are not being printed
+# when you call the methods directly.  This is why we are manipulating STDOUT.
+
+open my $oldout, ">&STDOUT" || die "Cannot dup STDOUT";
+
+my $fh = IO::File->new_tmpfile();
+ok($fh);
+open(STDOUT, ">&=".fileno($fh)) || die "Cannot reopen STDOUT";
+select STDOUT; $| = 1;
+
+my $sa = create_saobj();
+
+$sa->init();
+
+$sa->add_address_to_welcomelist("whitelist_test\@whitelist.spamassassin.taint.org");
+
+seek($fh, 0, 0);
+
+my $error = do {
+  local $/;
+  <$fh>;
+};
+$fh->close();
+open STDOUT, ">&".fileno($oldout) || die "Cannot dupe \$oldout: $!";
+select STDOUT; $| = 1;
+
+#warn "# $error\n";
+ok($error !~ /SpamAssassin auto-welcomelist: /);
+
+%patterns = %is_nonspam_patterns;
+ok (sarun ("-L < data/nice/002", \&patterns_run_cb));
+ok_all_patterns();
+%patterns = %is_nonspam_patterns;
+sarun ("-L < data/spam/004", \&patterns_run_cb);
+ok_all_patterns();
+
+$fh = IO::File->new_tmpfile();
+ok($fh);
+open(STDOUT, ">&=".fileno($fh)) || die "Cannot reopen STDOUT";
+select STDOUT; $| = 1;
+
+$sa->remove_address_from_welcomelist("whitelist_test\@whitelist.spamassassin.taint.org");
+
+seek($fh, 0, 0);
+
+$error = do {
+  local $/;
+  <$fh>;
+};
+$fh->close();
+open STDOUT, ">&".fileno($oldout) || die "Cannot dupe \$oldout: $!";
+select STDOUT; $| = 1;
+
+#warn "# $error\n";
+ok($error !~ /SpamAssassin auto-welcomelist: /);
+
+%patterns = %is_spam_patterns;
+sarun ("-L < data/spam/004", \&patterns_run_cb);
+ok_all_patterns();
+
+$fh = IO::File->new_tmpfile();
+ok($fh);
+open(STDOUT, ">&=".fileno($fh)) || die "Cannot reopen STDOUT";
+select STDOUT; $| = 1;
+
+$sa->add_address_to_blocklist("whitelist_test\@whitelist.spamassassin.taint.org");
+
+seek($fh, 0, 0);
+
+$error = do {
+  local $/;
+  <$fh>;
+};
+$fh->close();
+open STDOUT, ">&".fileno($oldout) || die "Cannot dupe \$oldout: $!";
+select STDOUT; $| = 1;
+
+#warn "# $error\n";
+ok($error !~ /SpamAssassin auto-welcomelist: /);
+
+%patterns = %is_spam_patterns;
+sarun ("-L < data/nice/002", \&patterns_run_cb);
+ok_all_patterns();
+
+$sa->remove_address_from_welcomelist("whitelist_test\@whitelist.spamassassin.taint.org");
+
+# Now we can test the "all" methods
+
+open(MAIL,"< data/nice/002");
+
+my $raw_message = do {
+  local $/;
+  <MAIL>;
+};
+
+close(MAIL);
+ok($raw_message);
+
+my $mail = $sa->parse( $raw_message );
+
+$fh = IO::File->new_tmpfile();
+ok($fh);
+open(STDOUT, ">&=".fileno($fh)) || die "Cannot reopen STDOUT";
+select STDOUT; $| = 1;
+
+$sa->add_all_addresses_to_welcomelist($mail);
+
+seek($fh, 0, 0);
+
+$error = do {
+  local $/;
+  <$fh>;
+};
+$fh->close();
+open STDOUT, ">&".fileno($oldout) || die "Cannot dupe \$oldout: $!";
+select STDOUT; $| = 1;
+
+#warn "# $error\n";
+ok($error !~ /SpamAssassin auto-welcomelist: /);
+
+%patterns = %is_nonspam_patterns;
+ok (sarun ("-L < data/nice/002", \&patterns_run_cb));
+ok_all_patterns();
+%patterns = %is_nonspam_patterns;
+sarun ("-L < data/spam/004", \&patterns_run_cb);
+ok_all_patterns();
+
+$fh = IO::File->new_tmpfile();
+ok($fh);
+open(STDOUT, ">&=".fileno($fh)) || die "Cannot reopen STDOUT";
+select STDOUT; $| = 1;
+
+$sa->remove_all_addresses_from_welcomelist($mail);
+
+seek($fh, 0, 0);
+
+$error = do {
+  local $/;
+  <$fh>;
+};
+$fh->close();
+open STDOUT, ">&".fileno($oldout) || die "Cannot dupe \$oldout: $!";
+select STDOUT; $| = 1;
+
+#warn "# $error\n";
+ok($error !~ /SpamAssassin auto-welcomelist: /);
+
+%patterns = %is_spam_patterns;
+sarun ("-L < data/spam/004", \&patterns_run_cb);
+ok_all_patterns();
+
+$fh = IO::File->new_tmpfile();
+ok($fh);
+open(STDOUT, ">&=".fileno($fh)) || die "Cannot reopen STDOUT";
+select STDOUT; $| = 1;
+
+$sa->add_all_addresses_to_blocklist($mail);
+
+seek($fh, 0, 0);
+
+$error = do {
+  local $/;
+  <$fh>;
+};
+$fh->close();
+open STDOUT, ">&".fileno($oldout) || die "Cannot dupe \$oldout: $!";
+select STDOUT; $| = 1;
+
+#warn "# $error\n";
+ok($error !~ /SpamAssassin auto-welcomelist: /);
+
+%patterns = %is_spam_patterns;
+sarun ("-L < data/nice/002", \&patterns_run_cb);
+ok_all_patterns();
+
+$sa->remove_all_addresses_from_welcomelist($mail);
+
diff --git a/upstream/t/welcomelist_from.t b/upstream/t/welcomelist_from.t
new file mode 100755 (executable)
index 0000000..4ca8881
--- /dev/null
@@ -0,0 +1,91 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("welcomelist_from");
+
+use Test::More;
+plan skip_all => 'Long running tests disabled' unless conf_bool('run_long_tests');
+plan tests => 32;
+
+# ---------------------------------------------------------------------------
+
+tstprefs ("
+  header USER_IN_WELCOMELIST           eval:check_from_in_welcomelist()
+  tflags USER_IN_WELCOMELIST           userconf nice noautolearn
+  score USER_IN_WELCOMELIST            -100
+  header USER_IN_DEF_WELCOMELIST       eval:check_from_in_default_welcomelist()
+  tflags USER_IN_DEF_WELCOMELIST       userconf nice noautolearn
+  score USER_IN_DEF_WELCOMELIST                -15
+  def_welcomelist_from_rcvd *\@paypal.com paypal.com
+  def_welcomelist_from_rcvd *\@paypal.com ebay.com
+  def_welcomelist_from_rcvd mumble\@example.com example.com
+  welcomelist_from_rcvd foo\@example.com spamassassin.org
+  welcomelist_from_rcvd foo\@example.com example.com
+  welcomelist_from_rcvd bar\@example.com example.com
+  welcomelist_allows_relays bar\@example.com
+  welcomelist_from baz\@example.com
+  welcomelist_from bam\@example.com
+  unwelcomelist_from bam\@example.com
+  unwelcomelist_from_rcvd mumble\@example.com
+");
+
+# tests 1 - 4 does welcomelist_from work?
+%patterns = (
+  q{ -100 USER_IN_WELCOMELIST }, '',
+);
+
+%anti_patterns = (
+  q{ FORGED_IN_WELCOMELIST }, '',
+  q{ USER_IN_DEF_WELCOMELIST }, '',
+  q{ FORGED_IN_DEF_WELCOMELIST }, '',
+);
+sarun ("-L -t < data/nice/008", \&patterns_run_cb);
+ok_all_patterns();
+
+# tests 5 - 8 does welcomelist_from_rcvd work?
+sarun ("-L -t < data/nice/009", \&patterns_run_cb);
+ok_all_patterns();
+
+# tests 9 - 12 second relay specified for same addr in welcomelist_from_rcvd
+sarun ("-L -t < data/nice/010", \&patterns_run_cb);
+ok_all_patterns();
+
+%patterns = (
+  q{ -15 USER_IN_DEF_WELCOMELIST }, '',
+);
+
+%anti_patterns = (
+  q{ USER_IN_WELCOMELIST }, '',
+  q{ FORGED_IN_WELCOMELIST }, '',
+  q{ FORGED_IN_DEF_WELCOMELIST }, '',
+);
+
+# tests 13 - 16 does def_welcomelist_from_rcvd work?
+sarun ("-L -t < data/nice/011", \&patterns_run_cb);
+ok_all_patterns();
+
+# tests 17 - 20 second relay specified for same addr in def_welcomelist_from_rcvd
+sarun ("-L -t < data/nice/012", \&patterns_run_cb);
+ok_all_patterns();
+
+%patterns = ();
+
+%anti_patterns = (
+  q{ USER_IN_WELCOMELIST }, '',
+  q{ FORGED_IN_WELCOMELIST }, '',
+  q{ USER_IN_DEF_WELCOMELIST }, '',
+  q{ FORGED_IN_DEF_WELCOMELIST }, '',
+);
+# tests 21 - 24 does welcomelist_allows_relays suppress the forged rule without
+#  putting the address on the welcomelist?
+sarun ("-L -t < data/nice/013", \&patterns_run_cb);
+ok_all_patterns();
+
+# tests 25 - 28 does unwelcomelist_from work?
+sarun ("-L -t < data/nice/014", \&patterns_run_cb);
+ok_all_patterns();
+
+# tests 29 - 32 does unwelcomelist_from_rcvd work?
+sarun ("-L -t < data/nice/015", \&patterns_run_cb);
+ok_all_patterns();
+
diff --git a/upstream/t/welcomelist_subject.t b/upstream/t/welcomelist_subject.t
new file mode 100755 (executable)
index 0000000..28025b8
--- /dev/null
@@ -0,0 +1,45 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("welcomelist_subject");
+use Test::More tests => 4;
+
+# ---------------------------------------------------------------------------
+
+%is_welcomelist_patterns = (
+  q{ SUBJECT_IN_WELCOMELIST }, 'welcomelist-subject'
+);
+
+%is_blocklist_patterns = (
+  q{ SUBJECT_IN_BLOCKLIST }, 'blocklist-subject'
+);
+
+tstprefs ("
+  loadplugin Mail::SpamAssassin::Plugin::WelcomeListSubject
+  header SUBJECT_IN_WELCOMELIST                eval:check_subject_in_welcomelist()
+  tflags SUBJECT_IN_WELCOMELIST                userconf nice noautolearn
+  score SUBJECT_IN_WELCOMELIST         -100
+  header SUBJECT_IN_BLOCKLIST          eval:check_subject_in_blocklist()
+  tflags SUBJECT_IN_BLOCKLIST          userconf noautolearn
+  score SUBJECT_IN_BLOCKLIST           100
+
+  # Check that rename backwards compatibility works with if's
+  ifplugin Mail::SpamAssassin::Plugin::WhiteListSubject
+  if plugin(Mail::SpamAssassin::Plugin::WelcomeListSubject)
+  welcomelist_subject [HC Anno*]
+  blocklist_subject whitelist test
+  endif
+  endif
+");
+
+%patterns = %is_welcomelist_patterns;
+
+ok(sarun ("-L -t < data/nice/016", \&patterns_run_cb));
+ok_all_patterns();
+
+%patterns = %is_blocklist_patterns;
+
+# force us to blocklist a nice msg
+ok(sarun ("-L -t < data/nice/015", \&patterns_run_cb));
+ok_all_patterns();
+
diff --git a/upstream/t/welcomelist_to.t b/upstream/t/welcomelist_to.t
new file mode 100755 (executable)
index 0000000..e6d1e4c
--- /dev/null
@@ -0,0 +1,22 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("welcomelist_to");
+use Test::More tests => 1;
+
+# ---------------------------------------------------------------------------
+
+%patterns = (
+  q{ USER_IN_WELCOMELIST_TO }, 'hit-wl',
+);
+
+tstprefs ("
+  header USER_IN_WELCOMELIST_TO                eval:check_to_in_welcomelist()
+  tflags USER_IN_WELCOMELIST_TO                userconf nice noautolearn
+  score USER_IN_WELCOMELIST_TO         -6
+  welcomelist_to announce*
+");
+
+sarun ("-L -t < data/nice/016", \&patterns_run_cb);
+ok_all_patterns();
+
index 7ed6165069d160d7de2f45b211f3b6003c08a7c8..ec2a78c9c71f03e1d891211973b27eeceff90213 100755 (executable)
@@ -6,16 +6,6 @@ use IO::File;
 
 use constant HAS_DB_FILE => eval { require DB_File };
 
-BEGIN {
-  if (-e 't/test_dir') {
-    chdir 't';
-  }
-
-  if (-e 'test_dir') {
-    unshift(@INC, '../blib/lib');
-  }
-}
-
 use Test::More;
 plan skip_all => 'Long running tests disabled' unless conf_bool('run_long_tests');
 plan skip_all => 'Need DB_File for this test'  unless HAS_DB_FILE;
@@ -23,20 +13,26 @@ plan tests => 35;
 
 # ---------------------------------------------------------------------------
 
+tstprefs ("
+  header AWL        eval:check_from_in_auto_welcomelist()
+  tflags AWL        userconf noautolearn
+  priority AWL      1000
+");
+
 %added_address_whitelist_patterns = (
-q{SpamAssassin auto-whitelist: adding address to whitelist:}, 'added address to whitelist',
+  q{SpamAssassin auto-welcomelist: adding address to welcomelist:}, 'added address to welcomelist',
 );
 %added_address_blacklist_patterns = (
-q{SpamAssassin auto-whitelist: adding address to blacklist:}, 'added address to blacklist',
+  q{SpamAssassin auto-welcomelist: adding address to blocklist:}, 'added address to blocklist',
 );
 %removed_address_patterns = (
-q{SpamAssassin auto-whitelist: removing address:}, 'removed address',
+  q{SpamAssassin auto-welcomelist: removing address:}, 'removed address',
 );
 %is_nonspam_patterns = (
-q{X-Spam-Status: No}, 'spamno',
+  q{X-Spam-Status: No}, 'spamno',
 );
 %is_spam_patterns = (
-q{X-Spam-Status: Yes}, 'spamyes',
+  q{X-Spam-Status: Yes}, 'spamyes',
 );
 
 
@@ -96,7 +92,7 @@ open STDOUT, ">&".fileno($oldout) || die "Cannot dupe \$oldout: $!";
 select STDOUT; $| = 1;
 
 #warn "# $error\n";
-ok($error !~ /SpamAssassin auto-whitelist: /);
+ok($error !~ /SpamAssassin auto-welcomelist: /);
 
 %patterns = %is_nonspam_patterns;
 ok (sarun ("-L < data/nice/002", \&patterns_run_cb));
@@ -123,7 +119,7 @@ open STDOUT, ">&".fileno($oldout) || die "Cannot dupe \$oldout: $!";
 select STDOUT; $| = 1;
 
 #warn "# $error\n";
-ok($error !~ /SpamAssassin auto-whitelist: /);
+ok($error !~ /SpamAssassin auto-welcomelist: /);
 
 %patterns = %is_spam_patterns;
 sarun ("-L < data/spam/004", \&patterns_run_cb);
@@ -147,7 +143,7 @@ open STDOUT, ">&".fileno($oldout) || die "Cannot dupe \$oldout: $!";
 select STDOUT; $| = 1;
 
 #warn "# $error\n";
-ok($error !~ /SpamAssassin auto-whitelist: /);
+ok($error !~ /SpamAssassin auto-welcomelist: /);
 
 %patterns = %is_spam_patterns;
 sarun ("-L < data/nice/002", \&patterns_run_cb);
@@ -187,7 +183,7 @@ open STDOUT, ">&".fileno($oldout) || die "Cannot dupe \$oldout: $!";
 select STDOUT; $| = 1;
 
 #warn "# $error\n";
-ok($error !~ /SpamAssassin auto-whitelist: /);
+ok($error !~ /SpamAssassin auto-welcomelist: /);
 
 %patterns = %is_nonspam_patterns;
 ok (sarun ("-L < data/nice/002", \&patterns_run_cb));
@@ -214,7 +210,7 @@ open STDOUT, ">&".fileno($oldout) || die "Cannot dupe \$oldout: $!";
 select STDOUT; $| = 1;
 
 #warn "# $error\n";
-ok($error !~ /SpamAssassin auto-whitelist: /);
+ok($error !~ /SpamAssassin auto-welcomelist: /);
 
 %patterns = %is_spam_patterns;
 sarun ("-L < data/spam/004", \&patterns_run_cb);
@@ -238,10 +234,11 @@ open STDOUT, ">&".fileno($oldout) || die "Cannot dupe \$oldout: $!";
 select STDOUT; $| = 1;
 
 #warn "# $error\n";
-ok($error !~ /SpamAssassin auto-whitelist: /);
+ok($error !~ /SpamAssassin auto-welcomelist: /);
 
 %patterns = %is_spam_patterns;
 sarun ("-L < data/nice/002", \&patterns_run_cb);
 ok_all_patterns();
 
 $sa->remove_all_addresses_from_whitelist($mail);
+
index b894c25bf2e5387bdd48c26c9885368e1ec41158..d4b29aa3afb757c3feb5c1a3e6cc8c694e201ee7 100755 (executable)
@@ -9,30 +9,44 @@ plan tests => 32;
 
 # ---------------------------------------------------------------------------
 
+disable_compat "welcomelist_blocklist";
+
 tstprefs ("
-        def_whitelist_from_rcvd *\@paypal.com paypal.com
-        def_whitelist_from_rcvd *\@paypal.com ebay.com
-        def_whitelist_from_rcvd mumble\@example.com example.com
-        whitelist_from_rcvd foo\@example.com spamassassin.org
-        whitelist_from_rcvd foo\@example.com example.com
-        whitelist_from_rcvd bar\@example.com example.com
-        whitelist_allows_relays bar\@example.com
-        whitelist_from baz\@example.com
-        whitelist_from bam\@example.com
-        unwhitelist_from bam\@example.com
-        unwhitelist_from_rcvd mumble\@example.com
-       ");
+  header USER_IN_WELCOMELIST           eval:check_from_in_welcomelist()
+  tflags USER_IN_WELCOMELIST           userconf nice noautolearn
+  header USER_IN_DEF_WELCOMELIST       eval:check_from_in_default_welcomelist()
+  tflags USER_IN_DEF_WELCOMELIST       userconf nice noautolearn
+  meta USER_IN_WHITELIST               (USER_IN_WELCOMELIST)
+  tflags USER_IN_WHITELIST             userconf nice noautolearn
+  score USER_IN_WHITELIST              -100
+  score USER_IN_WELCOMELIST            -0.01
+  meta USER_IN_DEF_WHITELIST           (USER_IN_DEF_WELCOMELIST)
+  tflags USER_IN_DEF_WHITELIST userconf nice noautolearn
+  score USER_IN_DEF_WHITELIST          -15
+  score USER_IN_DEF_WELCOMELIST        -0.01
+  def_whitelist_from_rcvd *\@paypal.com paypal.com
+  def_whitelist_from_rcvd *\@paypal.com ebay.com
+  def_whitelist_from_rcvd mumble\@example.com example.com
+  whitelist_from_rcvd foo\@example.com spamassassin.org
+  whitelist_from_rcvd foo\@example.com example.com
+  whitelist_from_rcvd bar\@example.com example.com
+  whitelist_allows_relays bar\@example.com
+  whitelist_from baz\@example.com
+  whitelist_from bam\@example.com
+  unwhitelist_from bam\@example.com
+  unwhitelist_from_rcvd mumble\@example.com
+");
 
 # tests 1 - 4 does whitelist_from work?
 %patterns = (
-             q{ USER_IN_WHITELIST }, 'w1'
-             );
+  q{ -100 USER_IN_WHITELIST }, '',
+);
 
 %anti_patterns = (
-             q{ FORGED_IN_WHITELIST }, 'a2',
-             q{ USER_IN_DEF_WHITELIST }, 'a3',
-             q{ FORGED_IN_DEF_WHITELIST }, 'a4'
-             );
+  q{ FORGED_IN_WHITELIST }, '',
+  q{ USER_IN_DEF_WHITELIST }, '',
+  q{ FORGED_IN_DEF_WHITELIST }, '',
+);
 sarun ("-L -t < data/nice/008", \&patterns_run_cb);
 ok_all_patterns();
 
@@ -45,14 +59,14 @@ sarun ("-L -t < data/nice/010", \&patterns_run_cb);
 ok_all_patterns();
 
 %patterns = (
-             q{ USER_IN_DEF_WHITELIST }, 'w5'
-             );
+  q{ -15 USER_IN_DEF_WHITELIST }, '',
+);
 
 %anti_patterns = (
-             q{ USER_IN_WHITELIST }, 'a6',
-             q{ FORGED_IN_WHITELIST }, 'a7',
-             q{ FORGED_IN_DEF_WHITELIST }, 'a8'
-             );
+  q{ USER_IN_WHITELIST }, '',
+  q{ FORGED_IN_WHITELIST }, '',
+  q{ FORGED_IN_DEF_WHITELIST }, '',
+);
 
 # tests 13 - 16 does def_whitelist_from_rcvd work?
 sarun ("-L -t < data/nice/011", \&patterns_run_cb);
@@ -65,11 +79,11 @@ ok_all_patterns();
 %patterns = ();
 
 %anti_patterns = (
-             q{ USER_IN_WHITELIST }, 'a9',
-             q{ FORGED_IN_WHITELIST }, 'a10',
-             q{ USER_IN_DEF_WHITELIST }, 'a11',
-             q{ FORGED_IN_DEF_WHITELIST }, 'a12'
-             );
+  q{ USER_IN_WHITELIST }, '',
+  q{ FORGED_IN_WHITELIST }, '',
+  q{ USER_IN_DEF_WHITELIST }, '',
+  q{ FORGED_IN_DEF_WHITELIST }, '',
+);
 # tests 21 - 24 does whitelist_allows_relays suppress the forged rule without
 #  putting the address on the whitelist?
 sarun ("-L -t < data/nice/013", \&patterns_run_cb);
index 74e4b72e963ba9695a5f91e84b971250f6619394..1cf7786a4161ae87f0a327b2f8afad7ec65a6986 100755 (executable)
@@ -6,25 +6,48 @@ use Test::More tests => 4;
 
 # ---------------------------------------------------------------------------
 
+disable_compat "welcomelist_blocklist";
+
 %is_whitelist_patterns = (
-q{ SUBJECT_IN_WHITELIST }, 'whitelist-subject'
+  q{ SUBJECT_IN_WHITELIST }, 'whitelist-subject'
 );
 
 %is_blacklist_patterns = (
-q{ SUBJECT_IN_BLACKLIST }, 'blacklist-subject'
+  q{ SUBJECT_IN_BLACKLIST }, 'blacklist-subject'
 );
 
-tstpre("
-loadplugin Mail::SpamAssassin::Plugin::WhiteListSubject
-");
-
 tstprefs ("
-use_bayes 0
-use_auto_whitelist 0
-$default_cf_lines
-whitelist_subject [HC Anno*]
-blacklist_subject whitelist test
-       ");
+  loadplugin Mail::SpamAssassin::Plugin::WhiteListSubject
+  header SUBJECT_IN_WELCOMELIST                eval:check_subject_in_welcomelist()
+  tflags SUBJECT_IN_WELCOMELIST                userconf nice noautolearn
+  score SUBJECT_IN_WELCOMELIST         -100
+
+  if !can(Mail::SpamAssassin::Conf::compat_welcomelist_blocklist)
+    meta SUBJECT_IN_WHITELIST          (SUBJECT_IN_WELCOMELIST)
+    tflags SUBJECT_IN_WHITELIST                userconf nice noautolearn
+    score SUBJECT_IN_WHITELIST         -100
+    score SUBJECT_IN_WELCOMELIST       -0.01
+  endif
+
+  header SUBJECT_IN_BLOCKLIST          eval:check_subject_in_blocklist()
+  tflags SUBJECT_IN_BLOCKLIST          userconf noautolearn
+  score SUBJECT_IN_BLOCKLIST           100
+
+  if !can(Mail::SpamAssassin::Conf::compat_welcomelist_blocklist)
+    meta SUBJECT_IN_BLACKLIST          (SUBJECT_IN_BLOCKLIST)
+    tflags SUBJECT_IN_BLACKLIST                userconf noautolearn
+    score SUBJECT_IN_BLACKLIST         100
+    score SUBJECT_IN_BLOCKLIST         0.01
+  endif
+
+  # Check that rename backwards compatibility works with if's
+  ifplugin Mail::SpamAssassin::Plugin::WhiteListSubject
+  if plugin(Mail::SpamAssassin::Plugin::WelcomeListSubject)
+  whitelist_subject [HC Anno*]
+  blacklist_subject whitelist test
+  endif
+  endif
+");
 
 %patterns = %is_whitelist_patterns;
 
@@ -36,3 +59,4 @@ ok_all_patterns();
 # force us to blacklist a nice msg
 ok(sarun ("-L -t < data/nice/015", \&patterns_run_cb));
 ok_all_patterns();
+
index e2010260667b3da11c4becaceff32d9a2a0294f8..af215be13b4d977cefe4fd3fea6cd05d18ebfdd9 100755 (executable)
@@ -7,15 +7,16 @@ use Test::More tests => 1;
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
-  q{ USER_IN_WHITELIST_TO }, 'hit-wl',
-
+  q{ USER_IN_WELCOMELIST_TO }, 'hit-wl',
 );
 
 tstprefs ("
-        $default_cf_lines
-        whitelist_to announce*
-       ");
+  header USER_IN_WELCOMELIST_TO                eval:check_to_in_welcomelist()
+  tflags USER_IN_WELCOMELIST_TO                userconf nice noautolearn
+  score USER_IN_WELCOMELIST_TO         -6
+  whitelist_to announce*
+");
 
 sarun ("-L -t < data/nice/016", \&patterns_run_cb);
 ok_all_patterns();
+
diff --git a/upstream/t/wlbl_uri.t b/upstream/t/wlbl_uri.t
new file mode 100755 (executable)
index 0000000..a442789
--- /dev/null
@@ -0,0 +1,109 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("wlbl_uri");
+use Test::More tests => 12;
+
+# copied from 60_welcome.cf
+# should do the right thing with the different disable/enable compat settings
+my $myrules = <<'END';
+  if can(Mail::SpamAssassin::Conf::feature_welcomelist_blocklist)
+    body URI_HOST_IN_BLOCKLIST         eval:check_uri_host_in_blocklist()
+    tflags URI_HOST_IN_BLOCKLIST               userconf noautolearn
+    score URI_HOST_IN_BLOCKLIST                100
+
+    if !can(Mail::SpamAssassin::Conf::compat_welcomelist_blocklist)
+      meta URI_HOST_IN_BLACKLIST               (URI_HOST_IN_BLOCKLIST)
+      tflags URI_HOST_IN_BLACKLIST     userconf noautolearn
+      score URI_HOST_IN_BLACKLIST              100
+      score URI_HOST_IN_BLOCKLIST              0.01
+    endif
+  endif
+  if !can(Mail::SpamAssassin::Conf::feature_welcomelist_blocklist)
+    if (version >= 3.004000)
+      body URI_HOST_IN_BLOCKLIST               eval:check_uri_host_in_blacklist()
+      tflags URI_HOST_IN_BLOCKLIST     userconf noautolearn
+      score URI_HOST_IN_BLOCKLIST              0.01
+
+      meta URI_HOST_IN_BLACKLIST               (URI_HOST_IN_BLOCKLIST)
+      tflags URI_HOST_IN_BLACKLIST     userconf noautolearn
+      score URI_HOST_IN_BLACKLIST              100
+    endif
+  endif
+
+  if can(Mail::SpamAssassin::Conf::feature_welcomelist_blocklist)
+    body URI_HOST_IN_WELCOMELIST       eval:check_uri_host_in_welcomelist()
+    tflags URI_HOST_IN_WELCOMELIST     userconf nice noautolearn
+    score URI_HOST_IN_WELCOMELIST              -100
+
+    if !can(Mail::SpamAssassin::Conf::compat_welcomelist_blocklist)
+      meta URI_HOST_IN_WHITELIST               (URI_HOST_IN_WELCOMELIST)
+      tflags URI_HOST_IN_WHITELIST     userconf nice noautolearn
+      score URI_HOST_IN_WHITELIST              -100
+      score URI_HOST_IN_WELCOMELIST    -0.01
+    endif
+  endif
+  if !can(Mail::SpamAssassin::Conf::feature_welcomelist_blocklist)
+    if (version >= 3.004000)
+      body URI_HOST_IN_WELCOMELIST     eval:check_uri_host_in_whitelist()
+      tflags URI_HOST_IN_WELCOMELIST   userconf nice noautolearn
+      score URI_HOST_IN_WELCOMELIST    -0.01
+
+      meta URI_HOST_IN_WHITELIST               (URI_HOST_IN_WELCOMELIST)
+      tflags URI_HOST_IN_WHITELIST     userconf nice noautolearn
+      score URI_HOST_IN_WHITELIST              -100
+    endif
+  endif
+END
+    
+disable_compat "welcomelist_blocklist";
+
+%patterns = (
+  q{ 0.0 URI_HOST_IN_BLOCKLIST }, '',
+  q{ 100 URI_HOST_IN_BLACKLIST }, '',
+  q{ -0.0 URI_HOST_IN_WELCOMELIST }, '',
+  q{ -100 URI_HOST_IN_WHITELIST }, '',
+);
+
+###
+
+tstprefs($myrules . "
+  blocklist_uri_host ximian.com
+  welcomelist_uri_host helixcode.com
+");
+
+sarun ("-L -t < data/nice/001", \&patterns_run_cb);
+ok_all_patterns();
+
+###
+
+tstprefs($myrules . "
+  blacklist_uri_host ximian.com
+  whitelist_uri_host helixcode.com
+");
+
+sarun ("-L -t < data/nice/001", \&patterns_run_cb);
+ok_all_patterns();
+
+###
+
+%patterns = (
+  q{ 100 URI_HOST_IN_BLOCKLIST }, '',
+  q{ -100 URI_HOST_IN_WELCOMELIST }, '',
+);
+%anti_patterns = (
+  q{ URI_HOST_IN_BLACKLIST }, '',
+  q{ URI_HOST_IN_WHITELIST }, '',
+);
+
+tstpre("
+  enable_compat welcomelist_blocklist
+");
+tstprefs($myrules . "
+  blocklist_uri_host ximian.com
+  welcomelist_uri_host helixcode.com
+");
+
+sarun ("-L -t < data/nice/001", \&patterns_run_cb);
+ok_all_patterns();
+
diff --git a/upstream/t/zz_cleanup.t b/upstream/t/zz_cleanup.t
deleted file mode 100755 (executable)
index e323bd3..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/usr/bin/perl -T
-
-use lib '.'; use lib 't';
-use SATest; sa_t_init("zz_cleanup");
-use Test::More tests => 1;
-
-use File::Path;
-
-# jm: off! we want to keep the logs around in case something failed,
-# so we can see what it was. esp. important in case of intermittent
-# failures.
-# rmtree ("log");
-
-ok (1);