-Copyright (C) 2021 The Apache Software Foundation
+Copyright (C) 2022 The Apache Software Foundation
Project Management Committee (PMC):
------------------------------------------------------------------------
-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
------------------------------------------------------------------------
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
[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"
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)
----------------------------------------------------------
- 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.
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
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.
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:
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.
----
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:
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
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
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?
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.
- 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
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
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
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
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
lib/Mail/SpamAssassin/Util/TieOneStringHash.pm
lib/spamassassin-run.pod
procmailrc.example
-rules.README
rules/active.list
rules/init.pre
rules/languages
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
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
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
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
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
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
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
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
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
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
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
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
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
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
\.pid$
\.so$
\.svn/
+\.gitattributes$
\.gitignore$
\.swp$
\.tmp$
\btmon\.out$
\b[Oo][Ll][Dd]$
\b[Oo][Uu][Tt]$
+^.github/
^blib/
^blibdirs$
^build/3\.\d\.\d_change_summary$
^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/
"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"
}
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'
#!/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; };
'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)"',
},
# 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
'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',
'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',
)
# 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
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}
# 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;
}
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,
+ },
+ },
+ },
};
#######################################################################
$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";
#######################################################################
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).
$(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)
---------
[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):
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
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.
- $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:
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
-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:
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.
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.)
------------------------
- - 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
- 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:
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
- Lots more ways to integrate SpamAssassin can be read at
- http://wiki.SpamAssassin.org/ .
+ https://wiki.apache.org/spamassassin/ .
(end of USAGE)
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
};
#!/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;
#!/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;
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.
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
------------
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'
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.
******
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
=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
# use bytes;
use re 'taint';
-require 5.006_001;
+require v5.14.0;
use Mail::SpamAssassin::Logger;
use Mail::SpamAssassin::Constants;
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.
# 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'));
}
$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;
# 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;
if (exists $opts->{$k}) { $self->{$v} = $opts->{$k}; }
}
+ # Set flag which can be checked from plugins etc
+ $self->{learning} = 1;
+
return \%ret;
}
sub finish_learner {
my $self = shift;
$self->{bayes_scanner}->force_close(1) if $self->{bayes_scanner};
+ delete $self->{learning};
1;
}
$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};
}
###########################################################################
-=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)) {
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.
=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
###########################################################################
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.
}
# 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);
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.
$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);
=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.
$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 });
$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}) {
$self->{resolver}->finish() if $self->{resolver};
+ $self->{conf}->finish(); delete $self->{conf};
+
$self->timer_end("finish");
%{$self} = ();
}
# 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;
}
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/) {
# 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};
# 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
}
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);
}
}
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)) {
$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");
}
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");
$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>,
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 ] )
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];
}
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;
$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;
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 $_ }
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 {
/\.${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;
}
}
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
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
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);
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 );
=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,
}
);
###########################################################################
-=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
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
$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}) {
=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
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 {
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;
}
############################################################################
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) {
# 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;
}
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;
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)));
}
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, $_);
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;
}
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)));
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, $_);
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;
}
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)));
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
}
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 {
$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}]");
# 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);
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);
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
}
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 '');
$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)) {
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;
}
}
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;
}
}
#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
$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}) {
$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)) {
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;
}
}
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: $!";
}
# 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 .= $_;
}
}
# go onto the next message
- seek(INPUT, $offset + $size, 0)
+ seek($fh, $offset + $size, 0)
or die "cannot reposition file to $offset + $size: $!";
}
else {
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}) {
use Mail::SpamAssassin;
use Mail::SpamAssassin::Logger;
+use Mail::SpamAssassin::Util qw(idn_to_ascii domain_to_search_list);
our @ISA = qw();
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"
};
$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
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);
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, $@);
};
}
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;
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;
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}, $@);
};
}
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};
}
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) {
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'
$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}}++;
$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.
$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;
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};
}
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;
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}, $@);
};
}
=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
=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,
--- /dev/null
+# <@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
+++ /dev/null
-# <@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
{
# 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;
#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 );
sub DESTROY {
my $self = shift;
+
$self->_close_db;
}
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) = ("", "");
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;
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);
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;
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();
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,
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;
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;
return 0;
}
- $id = $self->{_dbh}->{'mysql_insertid'};
+ $id = $self->{_dsn} =~ /^DBI:MariaDB/i ?
+ $self->{_dbh}->{'mariadb_insertid'} : $self->{_dbh}->{'mysql_insertid'};
$self->{_dbh}->commit();
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());
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());
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());
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());
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;
# 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
# 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;
=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
# 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;
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;
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(@_); }
$self->{username} = $args->{username};
}
+ if ($args->{max_size}) {
+ $self->{max_size} = $args->{max_size};
+ }
+
if ($args->{timeout}) {
$self->{timeout} = $args->{timeout} || 30;
}
message
+report
+
=cut
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)
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;
}
}
message (if available)
+report (if available)
+
=cut
sub _filter {
my ($self, $msg, $command) = @_;
my %data;
+ my $msgsize;
$self->_clear_errors();
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";
$!==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: $!";
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
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();
"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
push (@cmds, {
setting => 'score',
- is_frequent => 1,
code => sub {
my ($self, $key, $value, $line) = @_;
my($rule, @scores) = split(/\s+/, $value);
=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:
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
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) = @_;
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) = @_;
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) = @_;
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:
=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, {
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) = @_;
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));
}
});
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);
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);
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.
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;
}
}
});
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
}
});
-=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,
});
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
push (@cmds, {
setting => 'normalize_charset',
- default => 0,
+ default => 1,
type => $CONF_TYPE_BOOL,
code => sub {
my ($self, $key, $value, $line) = @_;
$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;
- }
}
});
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
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
=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.
}
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"));
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
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
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
}
});
+=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
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
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;
+ }
}
});
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
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
(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.)
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.
push (@cmds, {
setting => 'report_charset',
- default => '',
+ default => 'UTF-8',
type => $CONF_TYPE_STRING,
});
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;
}
});
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
=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:
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
=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
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
push (@cmds, {
setting => 'header',
- is_frequent => 1,
is_priv => 1,
code => sub {
my ($self, $key, $value, $line) = @_;
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.
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.
push (@cmds, {
setting => 'body',
- is_frequent => 1,
is_priv => 1,
code => sub {
my ($self, $key, $value, $line) = @_;
push (@cmds, {
setting => 'rawbody',
- is_frequent => 1,
is_priv => 1,
code => sub {
my ($self, $key, $value, $line) = @_;
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.
push (@cmds, {
setting => 'meta',
- is_frequent => 1,
is_priv => 1,
code => sub {
my ($self, $key, $value, $line) = @_;
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
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,
});
=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
=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
return $INVALID_VALUE;
}
foreach (split(/\s+/, $value)) {
- $self->{valid_tlds}{lc $_} = 1;
+ $self->{valid_tlds}{idn_to_ascii($_)} = 1;
}
}
});
=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
return $INVALID_VALUE;
}
foreach (split(/\s+/, $value)) {
- $self->{two_level_domains}{lc $_} = 1;
+ $self->{two_level_domains}{idn_to_ascii($_)} = 1;
}
}
});
=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
return $INVALID_VALUE;
}
foreach (split(/\s+/, $value)) {
- $self->{three_level_domains}{lc $_} = 1;
+ $self->{three_level_domains}{idn_to_ascii($_)} = 1;
}
}
});
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");
}
});
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);
}
});
} 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);
}
});
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
}
});
+=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
'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
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:
# 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'
$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} = { };
$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} = { };
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;
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))
}
###########################################################################
-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"; }
###########################################################################
}
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");
+ }
+ }
}
###########################################################################
# 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
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};
}
}
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)
###########################################################################
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)
=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
}
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};
}
=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.
- $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
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
};
$self->{command_luts} = { };
- $self->{command_luts}->{frequent} = { };
- $self->{command_luts}->{remaining} = { };
bless ($self, $class);
$self;
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;
}
}
}
} # (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
}
# 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)';
}
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;
$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;
}
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\@\@");
#$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;
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.
}
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 {
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};
}
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 . " ";
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]};
}
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;
}
# 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");
}
}
}
return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
}
- push(@{$conf->{$key}}, split(' ', $value));
+ push(@{$conf->{$key}}, split(/\s+/, $value));
}
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;
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 {
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 {
$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
$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) {
$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;
}
}
-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;
}
}
# 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);
}
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");
$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);
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}) {
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;
}
# 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) {
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) = @_;
$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;
}
}
$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 ];
}
}
=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
}
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};
}
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
)
(?![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;
)
(?![a-f0-9:])
)
- /oxi;
+ /xi;
+
+# exact match
+use constant IS_LOCALHOST => qr/^${\(LOCALHOST)}$/;
# ---------------------------------------------------------------------------
# an IP address, in IPv4 format only.
(?: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/
(?:
)
(?![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
!=| # NEQ
[\+\-\*\/]| # Mathematical Operator
[\?:] # ? : Operator
- )/ox;
+ )/x;
# ArchiveIterator
'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;
($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;
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;
}
$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;
}
$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};
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;
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
};
# local ($^W) = 0;
no warnings;
- eval {
- require Net::DNS;
- require Net::DNS::Resolver;
- };
eval {
require MIME::Base64;
};
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}
+ );
}
###########################################################################
# 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");
}
}
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;
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");
}
}
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]);
+ }
}
}
$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) = @_;
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 });
# explicitly abort anything left
$self->{async}->abort_remaining_lookups();
$self->{async}->log_lookups_timing();
- $self->mark_all_async_rules_complete();
1;
}
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;
$self->set_rbl_tag_data();
- delete $self->{dnspost};
+ delete $self->{rbl_subs};
delete $self->{dnsuri};
}
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;
# 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
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
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;
$self->{resolver}->available_nameservers(@good_nameservers);
}
-done:
- # jm: leaving this in!
dbg("dns: is DNS available? " . $IS_DNS_AVAILABLE);
return $IS_DNS_AVAILABLE;
}
###########################################################################
-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;
# 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 }) {
};
bless ($self, $class);
- $self->load_resolver();
$self;
}
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};
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",
}
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
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",
$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()
}
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}}) {
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;
# 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 = "::";
$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;
}
$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;
}
# 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';
}
}
# 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);
}
}
=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.
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
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);
$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;
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") }
# 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
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");
}
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));
}
}
}
}
}
- 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);
}
###########################################################################
# 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};
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};
}
}
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;
}
--- /dev/null
+# <@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;
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);
# 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
# 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"
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) {
}
}
}
+ 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
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",
{
$self->{charsets} .= exists $self->{charsets} ? " $1" : $1;
}
+
+ # todo: capture URI from meta refresh tag
}
sub display_text {
return "/" . $r_path;
}
else {
- if ($base_path =~ m|/|) {
+ if (index($base_path, '/') >= 0) {
$base_path =~ s|(?<=/)[^/]*$||;
}
else {
$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');
Time::HiRes::sleep(rand(1.0) + 0.5);
}
+sub jittery_half_second_sleep {
+ my ($self) = @_;
+ Time::HiRes::sleep(rand(0.5) + 0.25);
+}
+
###########################################################################
1;
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
use File::Spec;
use Time::Local;
use Fcntl qw(:DEFAULT :flock);
+use Errno qw(EEXIST);
our @ISA = qw(Mail::SpamAssassin::Locker);
$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();
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)) {
$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: $!";
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)) {
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;
}
}
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
{
# 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) {
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]);
# 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
# 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(?:$|::)#);
}
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]);
}
}
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;
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
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;
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";
}
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";
}
$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: $!";
use POSIX ();
use Time::HiRes ();
+use Mail::SpamAssassin::Logger;
our @ISA = ();
my %params = @_;
$self->{timestamp_fmt} = $params{timestamp_fmt};
+ $self->{escape} = $params{escape} if exists $params{escape};
return($self);
}
}
$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: $!";
$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";
$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 {
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;
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)
# 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";
}
}
# 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");
}
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
}
}
- 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 {
# 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/)
{
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)
# 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
#
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 {
$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);
# 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
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'} = [];
my $header;
my $part_array;
my $found_end_boundary;
+ my $found_last_end_boundary;
my $partcnt = 0;
my $line_count = @{$body};
# 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
$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 : ''));
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;
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;
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 ...
elsif ($ct[3]) {
$msg->{'name'} = $ct[3];
}
- if ($msg->{'name'}) {
- $msg->{'name'} = Encode::decode("MIME-Header", $msg->{'name'});
- }
$msg->{'boundary'} = $boundary;
# 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;
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};
# 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;
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
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 {
}
}
- 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)
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 = '';
# ---------------------------------------------------------------------------
$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"
# 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;
}
# 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;
}
# 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;
}
# 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
}
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;
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;
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;
$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++;
}
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 {
# 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]);
# 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;
/^(?: (?: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
}
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
$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
# 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
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",
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);
}
}
}
# 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
=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
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});
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"
}
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) = @_;
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;
}
eval {
require Net::Patricia;
Net::Patricia->VERSION(1.16); # need AF_INET6 support
- import Net::Patricia;
+ Net::Patricia->import;
$have_patricia = 1;
};
}
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});
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:
=head1 SYNOPSIS
- my $spamtest = new Mail::SpamAssassin ({
+ my $spamtest = Mail::SpamAssassin->new({
'rules_filename' => '/etc/spamassassin.rules',
'userprefs_filename' => $ENV{HOME}.'/.spamassassin/user_prefs'
});
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",
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);
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);
=head1 SYNOPSIS
- my $spamtest = new Mail::SpamAssassin ({
+ my $spamtest = Mail::SpamAssassin->new({
'rules_filename' => '/etc/spamassassin.rules',
'userprefs_filename' => $ENV{HOME}.'/.spamassassin/user_prefs'
});
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;
###########################################################################
$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];
HEADER => sub {
my $pms = shift;
my $hdr = shift;
- return if !$hdr;
- $pms->get($hdr,undef);
+ return '' if !$hdr;
+ $pms->get($hdr, '');
},
TIMING => sub {
);
}
+my $IP_ADDRESS = IP_ADDRESS;
+
sub new {
my $class = shift;
$class = ref($class) || $class;
'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,
'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",
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: $!");
+ }
}
###########################################################################
$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};
# 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 ...
# 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.
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()
- 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'
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) = @_;
# 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");
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
$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}) {
# 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;
}
#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
# 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};
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;
# 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
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;
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
# 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';
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
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}) {
# 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;
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;
}
}
- $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);
}
}
$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
=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
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:
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
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.
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
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
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};
}
@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()
$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);
}
}
$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 });
=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
=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 ;
=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
=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
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),
}
# 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
# $_[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%)
} 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 ? () : '';
+ }
}
###########################################################################
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
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
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}}, $_;
+ }
}
}
}
# 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);
+ }
}
}
}
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
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 {}
###########################################################################
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
});
# 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) {
$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 {
# 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;
}
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:
#$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.
}
}
$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;
}
###########################################################################
# 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'",
# 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) {
}
# 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) {
# 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) {
$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) {
}
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);
+ }
}
###########################################################################
=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: $!";
#
}
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};
}
}
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
}
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
###########################################################################
+# 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__
=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.
sub new_checker {
my ($factory, $main) = @_;
- die "auto-whitelist: unimplemented base method"; # override this
+ die "auto-welcomelist: unimplemented base method"; # override this
}
###########################################################################
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;
}
=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
}
###########################################################################
=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
=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
=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
=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.
=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.
=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
=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)
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:
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.
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
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
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
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;
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;
}
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
}
});
+ 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
}
}
- # 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;
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;
$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;
# 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++;
}
}
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};
%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($_) }
}
}
+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;
=head1 NAME
-Mail::SpamAssassin::Plugin::AWL - Normalize scores via auto-whitelist
+Mail::SpamAssassin::Plugin::AWL - Normalize scores via auto-welcomelist
=head1 SYNOPSIS
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
# 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;
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});
=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:
=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
=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
=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 {
} 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
=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 {
} 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;
}
});
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
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.
=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
});
=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) = @_;
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:
=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
=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,
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);
}
});
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
$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
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),
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}));
}
# 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
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;
};
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;
}
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;
}
# 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
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
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
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);
BADMODE => 19, BADNAME => 20, BADALG => 21, BADTRUNC => 22,
);
-our $txtdata_can_provide_a_list;
-
sub new {
my($class,$sa_main) = @_;
$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;
}
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+)
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';
$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,
# 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
# 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) {
} 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 {
# 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;
--- /dev/null
+# <@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;
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
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
# 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;
| 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
# 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;
###########################################################################
$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}) {
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)) {
my $isspam;
if (!defined $msgid) {
- @msgid = $self->get_msgid($msg);
+ @msgid = ( $msg->generate_msgid(), $msg->get_msgid() );
}
while( $msgid = shift @msgid ) {
sub scan {
my ($self, $permsgstatus, $msg) = @_;
- my $score;
return unless $self->{conf}->{use_learner};
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;
###########################################################################
# 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) = @_;
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 { };
}
# 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;
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;
my $region = $_[3];
local ($_) = $_[1];
+ my $conf = $self->{conf};
my @rettokens;
# include quotes, .'s and -'s for URIs, and [$,]'s for Nigerian-scam strings,
# 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
# 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);
# 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) {
# 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
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;
# 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 ||= '';
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;
+ }
}
}
} 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;
}
}
# 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;
}
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);
}
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;
}
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 {
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);
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();
}
# 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;
}
}
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 {
$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
# 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;
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};
+ }
}
###########################################################################
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);
1;
} or do {
my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
+ $eval_stat =~ s/ at .*//s;
dbg("zoom: giving up on regexp: $eval_stat");
};
}
}
- 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} = { };
}
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";
}
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;
# 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";
$output .= "\"$esc\"";
}
}
- else {
+ elsif ($fixup_re_test) {
print "PRE: $pre\nTOK: $tok\n" or die "error writing: $!";
}
}
$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;
}
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;
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}};
$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),
!defined $tflags ? () : (tflags => $tflags),
!defined $description ? () : (description => $description),
ruletype => $ruletype);
+ delete $pms->{meta_pending}->{$rulename};
+ delete $pms->{meta_check_ready}->{$rulename};
}
}
# 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();
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;
} 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};
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) {
# 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}--;
###########################################################################
-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;
$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}++;
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}};
# 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);
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);
sub $chunk_methodname {
my \$self = shift;
my \$hits = 0;
+ my \%captures;
EOT
$evalstr .= ' '.$_ for @{$self->{evalstr_chunk_prefix}};
$self->{evalstr} = $evalstr;
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
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
my ($self, $pms, $conf, %opts) = @_;
$self->push_evalstr_prefix($pms, '
no warnings q(uninitialized);
- my $hval;
+ my $hval; my @harr;
');
},
post_loop_body => sub
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};
$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" : '').'
}
');
}
{
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.'});
';
{
# 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;
$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.';' : '') .'
}
$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") .'
}
';
}
+ # 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").'
+ }
}
');
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',
{
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.';' : '') .'
$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") .'
}
';
}
+ # 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'));
}
);
}
{
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.';' : '') .'
$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") . '
}
';
}
+ # 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").'
+ }
}
');
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").'
}
');
}
return;
} elsif ($self->{main}->call_plugins("have_shortcircuited",
{ permsgstatus => $pms })) {
+ $pms->{shortcircuited} = 1;
return;
}
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
$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.
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
';
}
- # 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);
};
';
';
}
+ # 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);
+ }
}
}
';
my (\$self, \@extraevalargs) = \@_;
my \$testptr = \$self->{conf}->{$evalname}->{$priority};
+ my \$scoresptr = \$self->{conf}->{scores};
my \$prepend2desc = q#$prepend2desc#;
my \$rulename;
my \$result;
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}";
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);
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 {
###########################################################################
+sub compile_now_start {
+ my ($self, $params) = @_;
+ $self->{am_compiling} = 1;
+}
+
+sub compile_now_finish {
+ my ($self, $params) = @_;
+ delete $self->{am_compiling};
+}
+
+###########################################################################
+
1;
proc_status_ok exit_status_str);
use Errno qw(ENOENT EACCES);
use IO::Socket;
+use IO::Select;
our @ISA = qw(Mail::SpamAssassin::Plugin);
# 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});
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,
});
=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
});
=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
push (@cmds, {
setting => 'dcc_timeout',
is_admin => 1,
- default => 8,
+ default => 5,
type => $Mail::SpamAssassin::Conf::CONF_TYPE_DURATION,
});
$conf->{parser}->register_commands(\@cmds);
}
-
-
-
sub ck_dir {
my ($self, $dir, $tgt, $src) = @_;
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;
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 : '';
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",
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');
# 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;
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} ||
$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
$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");
# 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);
}
# 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;
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" };
$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;
}
}
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");
}
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)
use Mail::SpamAssassin::Plugin;
use Mail::SpamAssassin::Logger;
use Mail::SpamAssassin::Timeout;
+use Mail::SpamAssassin::Util qw(idn_to_ascii);
use strict;
use warnings;
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
=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
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>.
=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
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) = @_;
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) = @_;
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) = @_;
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);
}
});
}
});
- # 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,
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};
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};
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
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
# ---------------------------------------------------------------------------
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.
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!");
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};
}
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};
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 =
# (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
@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';
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");
}
}
}
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};
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;
}
}
-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
}
}
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//
#
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]);
}
}
}
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';
}
}
- 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
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
$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.
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) {
return ($any_match_at_all, \%any_match_by_wl);
}
+# Version features
+sub has_arc { 1 }
+
1;
--- /dev/null
+# <@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;
+
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;
our @ISA = qw(Mail::SpamAssassin::Plugin);
my $IP_ADDRESS = IP_ADDRESS;
-my $IP_PRIVATE = IP_PRIVATE;
# constructor: register the eval rule
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',
$self->set_config($mailsaobject->{conf});
foreach(@{$self->{'evalrules'}}) {
- $self->register_eval_rule($_);
+ $self->register_eval_rule($_, $Mail::SpamAssassin::Conf::TYPE_RBL_EVALS);
}
return $self;
# 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;
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;
}
$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.
# 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);
}
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;
}
else
{
my @tips;
- foreach my $ip (@originating) {
+ foreach my $ip (@{$pms->{dnseval_originating}}) {
if ($ip && !$trusted->contains_ip($ip)) {
push(@tips, $ip);
}
}
# 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 {
@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 {
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
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
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
}
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 {
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) {
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);
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} );
}
}
+ 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 {
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 {
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;
}
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)":
--- /dev/null
+# <@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;
--- /dev/null
+# <@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;
use warnings;
use re 'taint';
-my $VERSION = 2.003;
+my $VERSION = 4.000;
=head1 NAME
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'])
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;
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
)\@
/xi;
-sub dbg { Mail::SpamAssassin::Plugin::dbg ("FreeMail: @_"); }
+sub dbg { my $msg = shift; Mail::SpamAssassin::Plugin::dbg("FreeMail: $msg", @_); }
sub new {
my ($class, $mailsa) = @_;
$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;
}
}
);
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,
}
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 {
}
}
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();
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;
}
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;
}
}
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 {
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;
}
$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)) {
else {
dbg("HIT! $email is freemail");
}
- $self->_got_hit($pms, $email, "Header $header is freemail");
+ $self->_test_log($pms, $email, $rulename);
return 1;
}
}
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;
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;
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;
}
}
# 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') {
}
}
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;
}
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;
+ }
}
}
}
=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
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
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;
$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;
}
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;
}
});
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));
}
});
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));
}
});
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);
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;
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;
}
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 {
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;
}
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;
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;
}
# ("<" 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;
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:
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
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) = @_;
$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;
}
});
- $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 {
# 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;
}
}
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 = '';
}
}
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 {
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;
}
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) {
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) {
$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)");
}
# 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 {
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) {
$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;
}
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 {
$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 '';
}
}
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;
}
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;
}
}
# 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;
+++ /dev/null
-# <@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
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;
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 --
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
$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)
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;
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; }
# 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; }
{ 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)
$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:
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;
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; }
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;
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;
}
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;
}
sub image_named {
my ($self,$pms,$body,$name) = @_;
- return unless (defined $name);
+ return 0 unless (defined $name);
# make sure we have image data read in.
if (!exists $pms->{'imageinfo'}) {
sub image_name_regex {
my ($self,$pms,$body,$re) = @_;
- return unless (defined $re);
+ return 0 unless (defined $re);
# make sure we have image data read in.
if (!exists $pms->{'imageinfo'}) {
sub image_count {
my ($self,$pms,$body,$type,$min,$max) = @_;
- return unless defined $min;
+ return 0 unless defined $min;
# make sure we have image data read in.
if (!exists $pms->{'imageinfo'}) {
sub pixel_coverage {
my ($self,$pms,$body,$type,$min,$max) = @_;
- return unless (defined $type && defined $min);
+ return 0 unless (defined $type && defined $min);
# make sure we have image data read in.
if (!exists $pms->{'imageinfo'}) {
sub image_to_text_ratio {
my ($self,$pms,$body,$type,$min,$max) = @_;
- return unless (defined $type && defined $min && defined $max);
+ return 0 unless (defined $type && defined $min && defined $max);
# make sure we have image data read in.
if (!exists $pms->{'imageinfo'}) {
sub image_size_exact {
my ($self,$pms,$body,$type,$height,$width) = @_;
- return unless (defined $type && defined $height && defined $width);
+ return 0 unless (defined $type && defined $height && defined $width);
# make sure we have image data read in.
if (!exists $pms->{'imageinfo'}) {
sub image_size_range {
my ($self,$pms,$body,$type,$minh,$minw,$maxh,$maxw) = @_;
- return unless (defined $type && defined $minh && defined $minw);
+ return 0 unless (defined $type && defined $minh && defined $minw);
# make sure we have image data read in.
if (!exists $pms->{'imageinfo'}) {
}
my $name = 'dems_'.$type;
- return unless (exists $pms->{'imageinfo'}->{$name});
+ return 0 unless (exists $pms->{'imageinfo'}->{$name});
foreach my $dem ( keys %{$pms->{'imageinfo'}->{"dems_$type"}}) {
my ($h,$w) = split(/x/,$dem);
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;
}
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);
$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
$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)/))
{
$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()}) {
# 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
# }
# 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
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');
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
$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.' {
# ---------------------------------------------------------------------------
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;
}
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
+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;
=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
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
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
BEGIN
{
eval{
- import Archive::Zip qw( :ERROR_CODES :CONSTANTS )
+ Archive::Zip->import(qw( :ERROR_CODES :CONSTANTS ))
};
eval{
- import IO::String
+ IO::String->import
};
}
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/
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:HTP:\\1&";
+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;
$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,
=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
=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
=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
=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
=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
=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
}
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
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) = @_;
}
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;
},
=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
}
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
}
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
}
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 {
my $cttname = '';
my $ctdname = '';
- if($ctt =~ m/(?:file)?name\s*=\s*["']?([^"';]*)["']?/is){
+ if ($ctt =~ m/name\s*=\s*["']?([^"';]*)/is) {
$cttname = $1;
$cttname =~ s/\s+$//;
}
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+$//;
}
}
}
- 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
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;
return 1;
}
+ # Check for Office 2007 markers
if (index($data, 'w:macrosPresent="yes"') > -1) {
dbg('XML macros marker found');
return 1;
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;
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;
}
}
sub _zip_error_handler {
- 1;
+ 1;
}
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;
}
}
+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;
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.';
}
';
}
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") . '
}
}
+ # 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'));
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
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);
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;
}
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;
-
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);
=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.
#
# 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
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);
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) = @_;
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;
}
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);
}
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} ) ) {
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";
}
}
@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";
}
}
: die "error reading config file: $!";
close(F) or die "error closing config file: $!";
}
+
}
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);
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;
}
}
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 {
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});
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
=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
push (@cmds, {
setting => 'pyzor_timeout',
is_admin => 1,
- default => 3.5,
+ default => 5,
type => $Mail::SpamAssassin::Conf::CONF_TYPE_DURATION
});
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 {
return 1;
}
+# Version features
+sub has_fork { 1 }
+
1;
=back
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 {
}
}
- $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});
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
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,
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');
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};
}
}
+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');
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 0 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;
}
}
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
=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
use Mail::SpamAssassin::Plugin;
use Mail::SpamAssassin::Logger;
-use Mail::SpamAssassin::Constants qw(:ip);
use strict;
use warnings;
# use bytes;
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;
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;
}
}
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;
}
}
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;
}
$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;
our @ISA = qw(Mail::SpamAssassin::Plugin);
+my $IPV4_ADDRESS = IPV4_ADDRESS;
+
# constructor: register the eval rule
sub new {
my $class = shift;
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;
}
}
}
-sub _helo_forgery_whitelisted {
+sub _helo_forgery_welcomelisted {
my ($helo, $rdns) = @_;
if ($helo eq 'msn.com' && $rdns eq 'hotmail.com') { return 1; }
0;
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;
}
}
# 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;
}
###########################################################################
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
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
# 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}++;
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}++;
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);
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};
# 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");
}
=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:
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;
reuse NETWORK_RULE [ NETWORK_RULE_OLD_NAME ]
+ run_reuse_tests_only 0/1
+
endif
=head1 DESCRIPTION
--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;
# 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;
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;
# 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);
}
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
}
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;
}
# 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 ];
}
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();
# 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);
}
}
}
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];
}
}
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 {
$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;
}
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) = @_;
}
}
-
+1;
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
$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 {
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;
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});
=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
=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) = @_;
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));
}
});
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
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 {
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 {
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) = @_;
$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);
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:
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 ".
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
$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' : '');
my $ip = $lasthop->{ip}; # always present
my $helo = $lasthop->{helo}; # could be missing
- $scanner->{sender} = '' unless $scanner->{sender_got};
if ($ishelo) {
unless ($helo) {
}
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
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;
return 0;
}
-
$result ||= 'timeout'; # bug 5077
$comment ||= '';
$comment =~ s/\s+/ /gs; # no newlines please
}
}
- 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;
}
}
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;
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
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,
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
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
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." " : "") .
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});
$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);
=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
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
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);
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");
}
=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.
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);
}
# 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) &&
}
}
- $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});
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
# 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
$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;
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));
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?(?:\:\/\/|://|%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
=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
=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
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
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
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.
$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
});
-=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
=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;
}
});
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
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
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
});
-=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.
=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
});
(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.
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
});
-=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
=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:
=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
=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,
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);
}
});
$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;
{$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;}
}
-=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");}
###########################################################################
# 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}) {
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/;
} 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",
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.
$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;
# 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
$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
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).
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
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>.
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:
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
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
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;
$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;
} 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;
}
}
# 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;
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;
}
# 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,
# 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 {
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;
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;
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;
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;
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;
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;
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;
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;
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};
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)
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)
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;
}
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;
# 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;
}
: 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');
}
# ---------------------------------------------------------------------------
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;
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:
dbg("uri: criteria for $test met");
}
- $permsg->got_hit($test);
-
# reset hash
keys %uri_detail;
- return 0;
+ return 1;
}
return 0;
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;
}
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");
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;
+ }
}
}
}
# 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;
=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
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.
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:
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
=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;
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 {
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;
}
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,
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;
}
});
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;
}
});
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;
}
});
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;
}
});
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;
-
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});
=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
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
});
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);
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);
}
}
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;
}
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;
}
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;
}
}
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;
}
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;
}
}
}
###########################################################################
+#
-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) {
}
}
- $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) {
}
}
- $pms->{from_in_default_whitelist} = $found_match;
+ $pms->{from_in_default_welcomelist} = $found_match;
return;
}
###########################################################################
-# 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
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} ) {
$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);
# 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?
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 {
}
}
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
}
}
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;
}
###########################################################################
-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;
}
}
###########################################################################
-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) = @_;
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'};
--- /dev/null
+# <@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;
+++ /dev/null
-# <@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;
}
$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
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 {
=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.
"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 = '';
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
}
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);
}
###########################################################################
-=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.
"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.
}
# 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);
}
=head1 NAME
-Mail::SpamAssassin::SQLBasedAddrList - SpamAssassin SQL Based Auto Whitelist
+Mail::SpamAssassin::SQLBasedAddrList - SpamAssassin SQL Based Auto Welcomelist
=head1 SYNOPSIS
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 (
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;
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;
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;
}
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,
}
}
$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);
}
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;
$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;
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;
}
}
}
- 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} ".
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;
}
# 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};
}
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 {
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();
}
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
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);
# 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;
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();
# 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
###########################################################################
+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
###########################################################################
+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.
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");
+ }
}
}
}
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;
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");
###########################################################################
# 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
# 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;
${$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;
###########################################################################
# 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//;
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) {
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) {
###########################################################################
+# 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
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) {
# 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;
local $_ = shift;
if (HAS_MIME_BASE64) {
- return MIME::Base64::encode_base64($_);
+ return MIME::Base64::encode_base64($_,'');
}
$_ = pack("u57", $_);
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 {
);
}
+###########################################################################
+# 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
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; }
}
# 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();
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
);
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);
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);
}
# 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):
# 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)$/))
{
# 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;
}
###########################################################################
###########################################################################
+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
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
*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;
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";
}
}
}
- # 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
# (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));
+ }
}
}
}
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");
}
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");
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 {
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
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)
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
}
$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:
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
# 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');
}
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");
die "$_[0]\n";
}
};
- $compiled_re = qr/$re/;
+ $compiled_re = $qr_sa->($re);
1;
};
if ($ok && ref($compiled_re) eq 'Regexp') {
elsif ($re =~ /(?<!\\)\|\|/) {
return "contains '||'";
}
- elsif ($re =~ /^\|/) {
- return "starts with '|'";
- }
- elsif ($re =~ /\|(?<!\\\|)$/) {
- return "ends with '|'";
+ elsif ($re =~ /^\||\|(?<!\\\|)$/) {
+ return "starts or ends with '|'";
}
return "";
###########################################################################
+###########################################################################
+
sub get_my_locales {
my ($ok_locales) = @_;
###########################################################################
+# 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;
+}
###########################################################################
###########################################################################
+# 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;
use strict;
use warnings;
-# use bytes;
use re 'taint';
use version 0.77;
+use Mail::SpamAssassin::Util;
+
our ( $EXIT_STATUS, $WARNINGS );
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',
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.',
},
{
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,
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',
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',
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,
{
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,
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',
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 = ();
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',
}
);
+#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
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; }
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";
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) {
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;
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;
# 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;
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};
$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;
--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
-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
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
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,
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.
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>).
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>
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
=head1 BUGS
-See <http://issues.apache.org/SpamAssassin/>
+See <https://issues.apache.org/SpamAssassin/>
=head1 AUTHORS
--- /dev/null
+
+#
+# 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
--- /dev/null
+Rules in this directory are NOT processed by masschecks or sa-update
+Use at your own risk.
+
+++ /dev/null
-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!
# 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
#
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
util_rb_3tld no-ip.co.uk
#
util_rb_3tld mobile.web.tr
+util_rb_3tld ct.sendgrid.net
endif
# 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
# tflags publish
ADMITS_SPAM
+# tflags publish
+ADULT_DATING_COMPANY
+
# tflags publish
ADVANCE_FEE_2_NEW_FORM
# 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
# tflags learn
BAYES_999
+# tflags publish
+BEBEE_IMG_NOT_RCVD_BB
+
# tflags publish
BIGNUM_EMAILS_FREEM
# good enough
BITCOIN_MALF_HTML
-# tflags net
+# tflags publish
BITCOIN_MALWARE
# tflags publish
# tflags publish
BITCOIN_YOUR_INFO
-# good enough
-BODY_SINGLE_URI
-
-# good enough
-BODY_SINGLE_WORD
-
-# tflags net
+# tflags publish
BODY_URI_ONLY
# tflags publish
# tflags userconf
CHARSET_FARAWAY_HEADER
+# good enough
+CK_HELO_GENERIC
+
# tflags publish
CN_B2B_SPAMMER
# tflags publish
CONTENT_AFTER_HTML
+# tflags publish
+CONTENT_AFTER_HTML_WEAK
+
# tflags publish
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
# good enough
DEAR_BENEFICIARY
+# good enough
+DEAR_WINNER
+
# tflags net
DIGEST_MULTIPLE
# 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
# 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
# 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
# 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
# tflags net
FROM_MISSP_SPF_FAIL
-# good enough
-FROM_MISSP_TO_UNDISC
-
# good enough
FROM_MISSP_USER
# tflags net
FROM_NUMBERO_NEWDOMAIN
-# good enough
-FROM_NUMERIC_TLD
-
# tflags net
FROM_PAYPAL_SPOOF
# 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
# 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
# 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
# tflags publish
HAS_X_OUTGOING_SPAM_STAT
-# tflags net
-HDRS_LCASE
-
# good enough
HDRS_LCASE_IMGONLY
# tflags userconf
HEAD_LONG
-# good enough
-HELO_LH_HOME
-
# good enough
HELO_LOCALHOST
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
# tflags publish
HK_SCAM
-# good enough
-HK_WIN
-
# tflags publish
HOSTED_IMG_DIRECT_MX
# 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
# 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
# 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
# tflags publish
MALW_ATTACH
-# tflags net
-MANY_HDRS_LCASE
-
# tflags publish
MANY_SPAN_IN_TEXT
# tflags publish
MIXED_CENTER_CASE
-# good enough
-MIXED_CTYPE_CASE
-
# tflags publish
MIXED_ES
# 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
# tflags publish
NEWEGG_IMG_NOT_RCVD_NEGG
+# tflags publish
+NEW_PRODUCTS
+
# good enough
NICE_REPLY_A
# tflags net
NML_ADSP_CUSTOM_MED
-# tflags net
+# good enough
NORDNS_LOW_CONTRAST
# tflags publish
NSL_RCVD_HELO_USER
# good enough
-NUMBEREND_LINKBAIT
+NUMBERONLY_BITCOIN_EXP
# tflags publish
OBFU_BITCOIN
# tflags publish
ODD_FREEM_REPTO
-# good enough
-OFFER_ONLY_AMERICA
-
-# good enough
-ORDER_TODAY
-
# good enough
PDS_BAD_THREAD_QP_64
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
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
# 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
# tflags publish
PHISH_FBASEAPP
-# good enough
-PHOTO_EDITING_DIRECT
-
-# good enough
-PHOTO_EDITING_FREEM
-
# tflags publish
PHP_NOVER_MUA
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# tflags publish
TO_NO_BRKTS_HTML_ONLY
-# tflags net
+# good enough
TO_NO_BRKTS_MSFT
# tflags publish
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
# tflags net
TVD_SPACE_RATIO_MINFP
-# tflags net
-TVD_SUBJ_NUM_OBFU_MINFP
-
-# good enough
-TVD_VISIT_PHARMA
-
# tflags publish
TW_GIBBERISH_MANY
# tflags userconf
UNPARSEABLE_RELAY
-# tflags net
-UPGRADE_MAILBOX
+# tflags publish
+UNSUB_GOOG_FORM
# tflags net
URIBL_ABUSE_SURBL
# tflags publish
URI_DATA
-# good enough
-URI_DOTDOT_LOW_CNTRST
-
# tflags publish
URI_DOTEDU
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
# tflags userconf
URI_NOVOWEL
-# tflags net
+# good enough
+URI_OBFU_DOM
+
+# tflags publish
URI_ONLY_MSGID_MALF
# tflags publish
# tflags net
URI_WP_HACKED
-# tflags net
+# tflags publish
URI_WP_HACKED_2
# tflags publish
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
# tflags publish
WALMART_IMG_NOT_RCVD_WAL
-# good enough
-WANT_TO_ORDER
-
-# good enough
-WIKI_IMG
-
# tflags publish
WORD_INVIS
# tflags publish
XM_RANDOM
-# good enough
-XM_RECPTID
-
# tflags net
XPRIO
# 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
#
#
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
#
#loadplugin Mail::SpamAssassin::Plugin::AntiVirus
-# AWL - do auto-whitelist checks
+# AWL - do auto-welcomelist checks
#
#loadplugin Mail::SpamAssassin::Plugin::AWL
#
#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
#
# 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
###########################################################################
# 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
+
###########################################################################
# 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
# 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
--- /dev/null
+# 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
+
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];
}
=head1 NAME
-sa-awl - examine and manipulate SpamAssassin's auto-whitelist db
+sa-awl - examine and manipulate SpamAssassin's auto-welcomelist db
=head1 SYNOPSIS
=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
# 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
).join("\n", @{$opt{'cf'}})."\n";
-my $spamtest = new Mail::SpamAssassin(
+my $spamtest = Mail::SpamAssassin->new(
{
rules_filename => $opt{'configpath'},
site_rules_filename => $opt{'siteconfigpath'},
AV *results;
CODE:
- pstart = (unsigned char *) SvPVutf8(psv, plen);
+ pstart = (unsigned char *) SvPV(psv, plen);
pend = pstart + plen;
results = (AV *) sv_2mortal((SV *) newAV());
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
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>
=head1 BUGS
-See <http://issues.apache.org/SpamAssassin/>
+See <https://issues.apache.org/SpamAssassin/>
=head1 AUTHORS
&& !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'} ) {
$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'},
###########################################################################
- 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},
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" );
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
--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
--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
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.
=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>
=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>
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".
=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>
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.
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
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.
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
# 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);
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 {
'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'},
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;
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({
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
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);
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
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);
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;
}
}
- 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;
}
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;
}
# 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
$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");
}
# 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;
# 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,
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
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
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,
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
=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>
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:
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>
Mail::SpamAssassin::Conf(3)
spamassassin(1)
spamd(1)
-<http://wiki.apache.org/spamassassin/RuleUpdates>
+<https://wiki.apache.org/spamassassin/RuleUpdates>
=head1 PREREQUISITES
=head1 BUGS
-See <http://issues.apache.org/SpamAssassin/>
+See <https://issues.apache.org/SpamAssassin/>
=head1 AUTHORS
# - 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;
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'},
'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'},
}
# 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'},
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,
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: $!";
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();
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;
}
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
# 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: $!";
}
$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();
=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
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
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
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
else
echo "$as_me: WARNING: no configuration information is in $ac_dir" >&2
fi
- cd "$ac_popdir"
+ cd $ac_popdir
done
fi
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
cat confdefs.h >>conftest.$ac_ext
cat >>conftest.$ac_ext <<_ACEOF
/* end confdefs.h. */
+#include <stdio.h>
#include <netdb.h>
int
main ()
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],
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]),
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.
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;
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 {
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;
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 {
* */
#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
/* 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
/* 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;
" [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");
{ "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' },
{ 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)
{
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);
* 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.
*/
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).
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.
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...
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.
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
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
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
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
# 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;
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
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);
}
require Pod::Usage;
- import Pod::Usage;
+ Pod::Usage->import;
pod2usage(
-verbose => 0,
-message => $message,
# 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
);
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;
'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'},
'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'},
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
# 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,
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
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';
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';
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
);
$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;
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 ),
# 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({
}
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
# 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 $<");
}
}
# 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($$);
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");
$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) {
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);
}
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;
}
}
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");
}
}
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(
# 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;
}
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";
}
};
# 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;
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}) {
$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;
}
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);
}
}
$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) ) {
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;
}
###########################################################################
-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 {
}
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
# 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 {
# 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;
-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
-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
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>
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).
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,...>
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>
=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>
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
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.
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
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
-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:
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.
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
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
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:
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
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 (
--- /dev/null
+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`);
--- /dev/null
+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);
--- /dev/null
+-- Manual database creation for SQLite is not necessary,
+-- DecodeShortURLs plugin will create and clean database automatically.
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();
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);
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
-----------------------
SPAMD_HOST
SPAMD_PORT
SPAMD_LOCALHOST
+SPAMD_SCRIPT
SPAMASSASSIN_SCRIPT
SPAMC_SCRIPT
-SPAMD_SCRIPT
SAAWL_SCRIPT
SACHECKSPAMD_SCRIPT
SALEARN_SCRIPT
# imported into main for ease of use.
package main;
+require v5.14.0;
+
# use strict;
# use warnings;
# use re 'taint';
use File::Copy;
use File::Path;
use File::Spec;
+use File::Temp qw(tempdir);
use Test::Builder ();
use Test::More ();
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);
$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
#
sub sa_t_init {
my $tname = shift;
+ $mainpid = $$;
if ($config{PERL_PATH}) {
$perl_path = $config{PERL_PATH};
$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";
$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) {
}
$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
$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'});
$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;
@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;
}
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
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>).
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;
}
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;
}
$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);
%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);
$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;
}
}
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;
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 ?
# 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);
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;
}
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;
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);
# 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',
);
$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);
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);
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;
# ---------------------------------------------------------------------------
-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}}++;
+ }
}
}
}
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++;
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++;
}
if ($wasfailure) {
warn "Output can be examined in: ".
join(' ', @files_checked)."\n" if @files_checked;
+ $keep_workdir = 1;
return 0;
} else {
return 1;
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
}
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
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 {
}
}
+END {
+ # Cleanup workdir (but not if inside forked process)
+ if (defined $workdir && !$keep_workdir && $$ == $mainpid) {
+ rmtree($workdir);
+ }
+}
+
1;
#!/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 = (
);
%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')) {
--- /dev/null
+#!/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();
--- /dev/null
+#!/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();
+
--- /dev/null
+#!/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();
+
# ---------------------------------------------------------------------------
%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
bayes_auto_learn 1
bayes_auto_learn_threshold_spam 6.0
-');
+");
ok (sarun ("-L -t < data/nice/001", \&patterns_run_cb));
ok_all_patterns();
+
# ---------------------------------------------------------------------------
%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
bayes_auto_learn 1
bayes_auto_learn_threshold_spam 6.0
-');
+");
ok (sarun ("-L -t < data/nice/001", \&patterns_run_cb));
ok_all_patterns();
+
# ---------------------------------------------------------------------------
%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
bayes_auto_learn 1
bayes_auto_learn_threshold_spam 12.0
-');
+");
ok (sarun ("-L -t < data/nice/001", \&patterns_run_cb));
ok_all_patterns();
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();
--- /dev/null
+#!/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();
+
# ---------------------------------------------------------------------------
%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,
sarun ("-L --lint", \&patterns_run_cb);
ok_all_patterns();
+
#!/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;
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);
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/
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();
+}
#!/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;
#!/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;
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;
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');
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' );
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 ($_);
$_ = 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;
+}
+
#!/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;
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');
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' );
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 ($_);
$_ = 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;
+}
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;
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());
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
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' );
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 ($_);
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;
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());
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
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' );
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 ($_);
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;
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());
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
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' );
# 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;
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 ($_);
#!/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();
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));
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();
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();
ok($sa);
+$learner = $sa->call_plugins("learner_get_implementation");
+
ok($sa->{bayes_scanner});
ok(!$sa->{bayes_scanner}->is_scan_available());
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);
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));
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' );
$sa->init();
+$learner = $sa->call_plugins("learner_get_implementation");
+
open(MAIL,"< ../sample-nonspam.txt");
$raw_message = do {
$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
$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
# 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;
$_ = 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 {
return 1;
}
-
# ---------------------------------------------------------------------------
-%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();
+
--- /dev/null
+#!/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();
+
#!/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");
# 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");
#!/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");
# 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;
#!/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 ($@) {
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.";
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";
+}
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
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)
# ---------------------------------------------------------------------------
-# 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
# ---------------------------------------------------------------------------
-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");
#!/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;
# ---------------------------------------------------------------------------
#!/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;
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,
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";
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";
# 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
" 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;
}
}
# 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
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
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
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
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
-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
+
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;
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+
+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
+
--- /dev/null
+
+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
+
--- /dev/null
+#!/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 .
+
--- /dev/null
+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
+
+
--- /dev/null
+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
+
+
+
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
-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]
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
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
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
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
--- /dev/null
+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"
+
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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/
--- /dev/null
+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>
+
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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--
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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--
+
--- /dev/null
+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
--- /dev/null
+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; &nbs=
+p;<FONT size=3D7> </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>© 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> </DIV></DIV>
+<DIV class=3Dv1base-card-footer> </DIV>
+<DIV> </DIV></BODY></HTML>
-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
-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
--- /dev/null
+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月電費,委託金融機構扣繳成功電子繳費憑證
--- /dev/null
+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>
--- /dev/null
+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.
+
+
--- /dev/null
+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]
+
--- /dev/null
+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]
+
+
--- /dev/null
+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-----
+
+
--- /dev/null
+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)
+
--- /dev/null
+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
+
+
+
--- /dev/null
+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
+
+
--- /dev/null
+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
+
+
--- /dev/null
+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
+
+
--- /dev/null
+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">
+
+ </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>™</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>
+
+
+ <img src="http://machina.imakenews.com/E178767,5114587XXmediaunspunXX18970XXXXindex000018970.cfmXXemailXX5114587XXXX0Y0XX1" alt="" height="0" width="0">
+
+
+ </p>
+
+
+
+
+
+</div>
+
+
+</body>
+</html>
+
+
+
+------------=_1029331990-31627-4--
+
+
--- /dev/null
+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
--- /dev/null
+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> <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> <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> 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> </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. <BR></FONT></DIV>
+<DIV align=center><FONT face=Arial><STRONG><FONT color=#ff0000 size=4> -
+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--
+
--- /dev/null
+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]
--- /dev/null
+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
+
+
--- /dev/null
+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]
+
+
+
--- /dev/null
+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)
+
+
--- /dev/null
+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
+
+
--- /dev/null
+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&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&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 "moving to Oracle9i JDeveloper".
+ </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&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&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&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 & 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 & 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 – 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> </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, & 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, & 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 & Live @ Your Desktop<br>
+ </b> Attend a FREE Oracle APAC iSeminar to learn more about how Oracle9i
+ - Application Server, Database & 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 & click "reset").
+ </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 & Luxembourg<br>
+ </font></b><font size="2" face="Arial, Helvetica, sans-serif">Join us
+ for the Oracle Technology Days - Featuring Oracle9i Release 2 –
+ 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ège (session en franç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>
+
+
+
+
+
+
+
+
+
+
--- /dev/null
+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>
+
--- /dev/null
+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.
+
+
--- /dev/null
+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
+
+
+
--- /dev/null
+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
+
--- /dev/null
+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>
+
+
--- /dev/null
+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™</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™ 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> </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'>
+ This issue of WinXPnews™ contains:<br>
+</font>
+</td>
+</tr>
+<tr>
+<td><font size='1'> </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> </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'>
+ SPONSOR: iHateSpam - Eliminate Irritating Junk Email<br>
+</font>
+</td>
+</tr>
+<tr>
+<td><font size='1'> </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> </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'>
+ EDITOR'S CORNER<br>
+</font>
+</td>
+</tr>
+<tr>
+<td><font size='1'> </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> </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'>
+ SPONSOR: Is Your PC Spying On You?<br>
+</font>
+</td>
+</tr>
+<tr>
+<td><font size='1'> </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> </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'>
+ HINTS, TIPS, TRICKS & TWEAKS<br>
+</font>
+</td>
+</tr>
+<tr>
+<td><font size='1'> </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> </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'>
+ HOW TO'S: ALL THE NEW XP FEATURES<br>
+</font>
+</td>
+</tr>
+<tr>
+<td><font size='1'> </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> </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'>
+ WINXP SECURITY: UPDATES & PATCHES<br>
+</font>
+</td>
+</tr>
+<tr>
+<td><font size='1'> </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> </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'>
+ UPGRADING & COMPATIBILITY ISSUES<br>
+</font>
+</td>
+</tr>
+<tr>
+<td><font size='1'> </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> </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'>
+ WINXP CONFIGURING & TROUBLESHOOTING<br>
+</font>
+</td>
+</tr>
+<tr>
+<td><font size='1'> </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> </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'>
+ FAVE LINKS<br>
+</font>
+</td>
+</tr>
+<tr>
+<td><font size='1'> </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> </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'>
+ BOOK OF THE WEEK<br>
+</font>
+</td>
+</tr>
+<tr>
+<td><font size='1'> </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> </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'>
+ ABOUT WINXPNEWS™<br>
+</font>
+</td>
+</tr>
+<tr>
+<td><font size='1'> </td>
+</tr>
+<tr>
+<td><font size='1'> </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'> </font><br>
+<font face='verdana, sans-serif' size=3><b>
+About Your Subscription to WinXPnews™</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>
+
--- /dev/null
+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!]
+
+++ /dev/null
-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.
-
-
+++ /dev/null
-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]
-
+++ /dev/null
-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]
-
-
+++ /dev/null
-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-----
-
-
+++ /dev/null
-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)
-
+++ /dev/null
-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
-
-
-
+++ /dev/null
-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
-
-
+++ /dev/null
-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
-
-
+++ /dev/null
-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
-
-
+++ /dev/null
-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">
-
- </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>™</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>
-
-
- <img src="http://machina.imakenews.com/E178767,5114587XXmediaunspunXX18970XXXXindex000018970.cfmXXemailXX5114587XXXX0Y0XX1" alt="" height="0" width="0">
-
-
- </p>
-
-
-
-
-
-</div>
-
-
-</body>
-</html>
-
-
-
-------------=_1029331990-31627-4--
-
-
+++ /dev/null
-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
+++ /dev/null
-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> <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> <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> 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> </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. <BR></FONT></DIV>
-<DIV align=center><FONT face=Arial><STRONG><FONT color=#ff0000 size=4> -
-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--
-
+++ /dev/null
-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]
+++ /dev/null
-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
-
-
+++ /dev/null
-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]
-
-
-
+++ /dev/null
-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)
-
-
+++ /dev/null
-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
-
-
+++ /dev/null
-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&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&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 "moving to Oracle9i JDeveloper".
- </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&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&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&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 & 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 & 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 – 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> </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, & 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, & 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 & Live @ Your Desktop<br>
- </b> Attend a FREE Oracle APAC iSeminar to learn more about how Oracle9i
- - Application Server, Database & 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 & click "reset").
- </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 & Luxembourg<br>
- </font></b><font size="2" face="Arial, Helvetica, sans-serif">Join us
- for the Oracle Technology Days - Featuring Oracle9i Release 2 –
- 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ège (session en franç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>
-
-
-
-
-
-
-
-
-
-
+++ /dev/null
-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>
-
+++ /dev/null
-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.
-
-
+++ /dev/null
-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
-
-
-
+++ /dev/null
-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
-
+++ /dev/null
-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>
-
-
+++ /dev/null
-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™</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™ 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> </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'>
- This issue of WinXPnews™ contains:<br>
-</font>
-</td>
-</tr>
-<tr>
-<td><font size='1'> </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> </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'>
- SPONSOR: iHateSpam - Eliminate Irritating Junk Email<br>
-</font>
-</td>
-</tr>
-<tr>
-<td><font size='1'> </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> </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'>
- EDITOR'S CORNER<br>
-</font>
-</td>
-</tr>
-<tr>
-<td><font size='1'> </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> </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'>
- SPONSOR: Is Your PC Spying On You?<br>
-</font>
-</td>
-</tr>
-<tr>
-<td><font size='1'> </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> </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'>
- HINTS, TIPS, TRICKS & TWEAKS<br>
-</font>
-</td>
-</tr>
-<tr>
-<td><font size='1'> </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> </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'>
- HOW TO'S: ALL THE NEW XP FEATURES<br>
-</font>
-</td>
-</tr>
-<tr>
-<td><font size='1'> </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> </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'>
- WINXP SECURITY: UPDATES & PATCHES<br>
-</font>
-</td>
-</tr>
-<tr>
-<td><font size='1'> </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> </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'>
- UPGRADING & COMPATIBILITY ISSUES<br>
-</font>
-</td>
-</tr>
-<tr>
-<td><font size='1'> </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> </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'>
- WINXP CONFIGURING & TROUBLESHOOTING<br>
-</font>
-</td>
-</tr>
-<tr>
-<td><font size='1'> </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> </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'>
- FAVE LINKS<br>
-</font>
-</td>
-</tr>
-<tr>
-<td><font size='1'> </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> </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'>
- BOOK OF THE WEEK<br>
-</font>
-</td>
-</tr>
-<tr>
-<td><font size='1'> </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> </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'>
- ABOUT WINXPNEWS™<br>
-</font>
-</td>
-</tr>
-<tr>
-<td><font size='1'> </td>
-</tr>
-<tr>
-<td><font size='1'> </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'> </font><br>
-<font face='verdana, sans-serif' size=3><b>
-About Your Subscription to WinXPnews™</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>
-
+++ /dev/null
-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!]
-
#!/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;
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',
# 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();
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
--- /dev/null
+#!/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
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;
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";
--- /dev/null
+#!/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";
--- /dev/null
+#!/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();
+
--- /dev/null
+#!/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();
+
# ---------------------------------------------------------------------------
%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;
%patterns = %is_spam_patterns;
ok (sarun ("-L -t < data/spam/004", \&patterns_run_cb));
ok_all_patterns();
+
# ---------------------------------------------------------------------------
%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;
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.');
# ---------------------------------------------------------------------------
%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();
+
#!/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");
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"));
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) {
--- /dev/null
+#!/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/');
+
+}
+
);
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));
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));
ok ($matched_output =~ /^\s+\* .{0,60}very very/m);
ok ($matched_output =~ /^\s+\* .{0,60}keystroke!/m);
-
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;
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;
# 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
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',
--- /dev/null
+#!/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();
+
#!/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
# 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("
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
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();
+}
+
# ---------------------------------------------------------------------------
%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 = (
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)
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',
+++ /dev/null
-#!/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();
--- /dev/null
+#!/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();
+
--- /dev/null
+#!/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();
+}
+
#!/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();
%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();
+
--- /dev/null
+#!/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();
+
--- /dev/null
+#!/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();
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"; }
+
#!/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) {
} 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;
}
}
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'));
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;
#!/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));
--- /dev/null
+#!/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();
+}
+
+++ /dev/null
-#!/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();
-
--- /dev/null
+#!/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();
+
--- /dev/null
+#!/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
+
#!/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;
# ---------------------------------------------------------------------------
%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();
+
#!/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();
+
# 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
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
});
--- /dev/null
+#!/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();
+
# ---------------------------------------------------------------------------
%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));
#!/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;
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,
});
# ---------------------------------------------------------------------------
-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;
sarun ("-L --lint", \&patterns_run_cb);
ok_all_patterns();
}
+
#!/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');
# ---------------------------------------------------------------------------
%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
$ENV{'LC_ALL'} = 'pl_PL';
sarun ("-L -t < data/nice/004", \&patterns_run_cb);
ok_all_patterns();
+
# 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;
# ---------------------------------------------------------------------------
-%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");
--- /dev/null
+#!/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();
+
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");
# ---------------------------------------------------------------------------
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,
# ---------------------------------------------------------------------------
%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));
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/
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();
+
#!/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',
# 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,
#!/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';
--- /dev/null
+#!/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";
+ }
+}
+
--- /dev/null
+#!/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";
+ }
+}
+
# ---------------------------------------------------------------------------
%patterns = (
-
q{ X-Spam-Status: No, }, 'nonspam'
-
);
sarun ("-L -t < data/nice/001", \&patterns_run_cb);
ok_all_patterns();
+
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()
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();
%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();
# ---------------------------------------------------------------------------
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();
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));
});
%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();
--- /dev/null
+#!/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();
+
--- /dev/null
+#!/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");
+
--- /dev/null
+#!/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);
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);
%patterns = %patterns_phishtank;
sarun ("-L -t < data/spam/phishing_phishtank.eml", \&patterns_run_cb);
ok_all_patterns();
+
# ---------------------------------------------------------------------------
%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));
# ---------------------------------------------------------------------------
%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));
# ---------------------------------------------------------------------------
%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));
--- /dev/null
+#!/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");
+
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);
#!/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;
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
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;
}
--- /dev/null
+#!/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;
+}
+
--- /dev/null
+#!/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();
+
#!/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'; };
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();
+
#!/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;
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';
# ---------------------------------------------------------------------------
# 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
# ---------------------------------------------------------------------------
-# 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/
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,
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 "
# ---------------------------------------------------------------------------
-%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();
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 {
};
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,
$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,
$text = $newmsg;
}
- open (OUT, ">log/recurse.eml") or die;
+ open (OUT, ">$workdir/recurse.eml") or die;
print OUT $text;
close OUT or die;
}
$boundstr++;
}
- open (OUT, ">log/recurse.eml") or die;
+ open (OUT, ">$workdir/recurse.eml") or die;
print OUT $text;
close OUT or die;
}
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 {
create_test_message_3();
try_scan();
-ok(unlink 'log/recurse.eml');
+ok(unlink "$workdir/recurse.eml");
--- /dev/null
+#!/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();
+
#!/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");
#!/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");
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();
}
}
ok($error);
+
--- /dev/null
+#!/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";
+}
+
+++ /dev/null
-#!/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();
+++ /dev/null
-#!/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();
+++ /dev/null
-#!/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();
# 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;
tstprefs ("report_safe 2\n");
sarun ("-L < $test", \&patterns_run_cb);
ok_all_patterns();
+
# ---------------------------------------------------------------------------
%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();
+
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();
#!/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");
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:
# - 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();
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();
#!/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');
# ---------------------------------------------------------------------------
%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"));
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());
+
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');
# ---------------------------------------------------------------------------
%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"));
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;
return 1;
}
}
+
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');
# ---------------------------------------------------------------------------
%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"));
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;
return 1;
}
}
+
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');
# ---------------------------------------------------------------------------
%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"));
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;
return 1;
}
}
+
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');
# ---------------------------------------------------------------------------
%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"));
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;
return 1;
}
}
+
#!/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');
# ---------------------------------------------------------------------------
%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"));
ok_all_patterns();
ok(stop_spamd());
+
#!/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');
# ---------------------------------------------------------------------------
%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);
# 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;
#!/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');
# ---------------------------------------------------------------------------
%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());
+
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');
# ---------------------------------------------------------------------------
%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"));
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());
+
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');
# ---------------------------------------------------------------------------
%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"));
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());
+
#!/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');
# ---------------------------------------------------------------------------
%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"));
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());
+
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();
+
#!/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");
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;
}
# run tests
-my $mail = 'log/rule_names.eml';
+my $mail = "$workdir/rule_names.eml";
write_mail();
%patterns = ();
my $i = 1;
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();
map { [$_, sha1($_ . $i)] }
@_;
}
+
# 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();
+
);
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();
--- /dev/null
+#!/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();
+
#!/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;
# ---------------------------------------------------------------------------
%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"));
sacheckspamdrun("--hostname $spamdhost --port $p --verbose");
ok (($? >> 8) != 0);
+
#!/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/||;
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: $?";
$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
# -------------------------------------------------------------------
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 {
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
};
close RULES or die;
}
+
#!/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;
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;
# ---------------------------------------------------------------------------
%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
');
%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();
+
--- /dev/null
+#!/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();
+
# ---------------------------------------------------------------------------
%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();
+
# ---------------------------------------------------------------------------
%patterns = (
-
-q{ hello world }, 'spamc',
-
+ 'hello world', 'spamc',
);
# connect on port 9 (discard): should always fail
--- /dev/null
+#!/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());
+
# ---------------------------------------------------------------------------
%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.
# ---------------------------------------------------------------------------
%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();
# ---------------------------------------------------------------------------
%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'");
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
# ---------------------------------------------------------------------------
-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' );
stop_spamd();
-ok(unlink 'log/rptfail'); # need a little cleanup
+ok(unlink "log/rptfail"); # need a little cleanup
+
use lib '.'; use lib 't';
use SATest; sa_t_init("spamc_optL");
+
use constant HAS_SDBM_FILE => eval { require SDBM_File; };
use Test::More;
# ---------------------------------------------------------------------------
-tstlocalrules ("
- bayes_store_module Mail::SpamAssassin::BayesStore::SDBM
+tstprefs ("
+ bayes_store_module Mail::SpamAssassin::BayesStore::SDBM
");
start_spamd("-L --allow-tell");
ok_all_patterns();
stop_spamd();
+
# 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
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"
# 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
clear_pattern_counters();
ok(scrunwantfail("--connect-retries 1 -x -E < data/spam/001", \&patterns_run_cb));
ok ok_all_patterns();
+
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
# ---------------------------------------------------------------------------
%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
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",
# ---------------------------------------------------------------------------
%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"));
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());
+
# ---------------------------------------------------------------------------
%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/
};
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 ());
#!/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");
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"));
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);
$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({
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"));
return $msg;
}
+
# ---------------------------------------------------------------------------
-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";
# 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));
# ---------------------------------------------------------------------------
-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";
# 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));
dbgprint "Stopping spamd...\n";
stop_spamd;
-
# ---------------------------------------------------------------------------
-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";
# 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
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));
dbgprint "Stopping spamd...\n";
stop_spamd;
-
# ---------------------------------------------------------------------------
%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));
# ---------------------------------------------------------------------------
%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");
ok_all_patterns();
ok (stop_spamd());
-
# 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);
# ---------------------------------------------------------------------------
%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");
ok_all_patterns();
stop_spamd();
-
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 = (
ok_all_patterns();
stop_spamd();
-cleanup_safe_tmpdir();
# ---------------------------------------------------------------------------
%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();
+
# ---------------------------------------------------------------------------
%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");
ok_all_patterns();
ok (stop_spamd());
-
# ---------------------------------------------------------------------------
%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");
ok_all_patterns();
ok (stop_spamd());
-
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');
# ---------------------------------------------------------------------------
-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;
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";
@pending_unlinks = ();
}
-
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');
# ---------------------------------------------------------------------------
-# 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;
ok (spamcrun ("<data/spam/001", \&patterns_run_cb));
}
-
-
# ---------------------------------------------------------------------------
%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',
);
# ---------------------------------------------------------------------------
%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;
# ---------------------------------------------------------------------------
%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;
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;
# ---------------------------------------------------------------------------
-my $userprefdb = mk_safe_tmpdir()."/userpref.db";
+my $userprefdb = $workdir."/userpref.db";
my $dbh = DBI->connect("dbi:SQLite:dbname=$userprefdb","","");
ok($dbh);
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();
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());
+
# ---------------------------------------------------------------------------
%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();
"--ssl --port $port < data/spam/001",
\&patterns_run_cb));
ok_all_patterns();
+
# ---------------------------------------------------------------------------
%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 ());
--- /dev/null
+#!/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();
+
# ---------------------------------------------------------------------------
%patterns = (
-
-q{ X-Spam-Status: Yes,}, 'status',
-
+ q{ X-Spam-Status: Yes,}, 'status',
);
ok (sdrun ("-L", "< data/spam/001", \&patterns_run_cb));
# ---------------------------------------------------------------------------
%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));
# ---------------------------------------------------------------------------
%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;
# ---------------------------------------------------------------------------
%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();
-
# ---------------------------------------------------------------------------
-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',
ok_all_patterns();
stop_spamd();
-cleanup_safe_tmpdir();
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/
};
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
};
close OUT;
-open (OUT, ">log/virtualconfig/testuser3/user_prefs");
+open (OUT, ">$workdir/virtualconfig/testuser3/user_prefs");
print OUT q{
# no user rules here
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);
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 ());
$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;
-# ---------------------------------------------------------------------------
--- /dev/null
+#!/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();
+
# ---------------------------------------------------------------------------
# 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();
+
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
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);
%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
%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
%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
%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();
");
%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 = (
--- /dev/null
+#!/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();
+
--- /dev/null
+#!/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));
+
+}
+# ---------------------------------------------------------------------------
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));
+
+}
+# ---------------------------------------------------------------------------
#!/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;
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
# 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
# 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");
{
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
# 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 );
$input = $files[0];
tstprefs ("
- $default_cf_lines
report_safe 2
body TEST_ALWAYS /./
score TEST_ALWAYS 100
# 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 );
# 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";
}
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;
# ---------------------------------------------------------------------------
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***
");
# 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";
}
tstprefs ("
- $default_cf_lines
report_safe 2
rewrite_header subject ***SPAM***
");
# 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";
}
tstprefs ("
- $default_cf_lines
report_safe 0
rewrite_header subject ***SPAM***
");
# 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";
# 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"));
# ---------------------------------------------------------------------------
%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();
+
#!/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");
# ---------------------------------------------------------------------------
%patterns = (
-
q{ tainted get_header found } => '',
q{ tainted get_pristine found } => '',
q{ tainted get_pristine_body found } => '',
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;
--- /dev/null
+---
+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: **
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();
%anti_patterns = ( q{ NATURAL } => 'NATURAL',);
sarun ("-L -t < data/spam/badctype2", \&patterns_run_cb);
ok_all_patterns();
+
#!/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;
# 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);
#!/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");
# 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 = (
# ---------------------------------------------------------------------------
}
my $sa = create_saobj({
- userprefs_filename => "log/tst.cf",
+ userprefs_filename => $userrules,
# debug => 1
});
#!/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,
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 {
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
'http://foo/',
'http://www.foo.com/',
]));
+# Bug 7891
+ok (try_canon([
+ 'http://www.ch/',
+ ], [
+ 'http://www.ch/'
+ ]));
##############################################
# 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");
# 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;
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);
# 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 );
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();
# 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");
# 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");
# 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,
});
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{
# 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();
+}
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;
# ---------------------------------------------------------------------------
%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{
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{
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();
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;
# ---------------------------------------------------------------------------
%anti_patterns = (
- q{ X_URIBL_IPSONLY } => 'A',
+ q{ X_URIBL_IPSONLY } => 'A',
);
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();
--- /dev/null
+#!/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";
+}
+
+
+++ /dev/null
-#!/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();
#!/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();
+
#!/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;
--- /dev/null
+#!/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);
+
--- /dev/null
+#!/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();
+
--- /dev/null
+#!/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();
+
--- /dev/null
+#!/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();
+
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;
# ---------------------------------------------------------------------------
+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',
);
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));
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);
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);
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));
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);
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);
+
# ---------------------------------------------------------------------------
+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();
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);
%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);
# ---------------------------------------------------------------------------
+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;
# force us to blacklist a nice msg
ok(sarun ("-L -t < data/nice/015", \&patterns_run_cb));
ok_all_patterns();
+
# ---------------------------------------------------------------------------
%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();
+
--- /dev/null
+#!/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();
+
+++ /dev/null
-#!/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);