| 1 | ### -*-m4-*- |
| 2 | ### |
| 3 | ### Spam filtering for distorted.org.uk Exim configuration |
| 4 | ### |
| 5 | ### (c) 2012 Mark Wooding |
| 6 | ### |
| 7 | |
| 8 | ###----- Licensing notice --------------------------------------------------- |
| 9 | ### |
| 10 | ### This program is free software; you can redistribute it and/or modify |
| 11 | ### it under the terms of the GNU General Public License as published by |
| 12 | ### the Free Software Foundation; either version 2 of the License, or |
| 13 | ### (at your option) any later version. |
| 14 | ### |
| 15 | ### This program is distributed in the hope that it will be useful, |
| 16 | ### but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 17 | ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 18 | ### GNU General Public License for more details. |
| 19 | ### |
| 20 | ### You should have received a copy of the GNU General Public License |
| 21 | ### along with this program; if not, write to the Free Software Foundation, |
| 22 | ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. |
| 23 | |
| 24 | DIVERT(null) |
| 25 | ###-------------------------------------------------------------------------- |
| 26 | ### Spam filtering. |
| 27 | |
| 28 | SECTION(global, policy)m4_dnl |
| 29 | spamd_address = CONF_spamd_address CONF_spamd_port |
| 30 | |
| 31 | SECTION(routers, allspam)m4_dnl |
| 32 | ## If we're verifying an address and the recipient has a `~/.mail/spam-limit' |
| 33 | ## file, then look up the recipient and sender addresses to find a plausible |
| 34 | ## limit and insert it into the `address_data' where the RCPT ACL can find |
| 35 | ## it. This router always declines, so it doesn't affect the overall outcome |
| 36 | ## of the verification. |
| 37 | fetch_spam_limit: |
| 38 | driver = redirect |
| 39 | data = :unknown: |
| 40 | verify_only = true |
| 41 | local_part_suffix = CONF_user_suffix_list |
| 42 | local_part_suffix_optional = true |
| 43 | check_local_user |
| 44 | address_data = \ |
| 45 | ${if def:address_data {$address_data}{}} \ |
| 46 | ${if and {{!eq{$acl_c_mode}{submission}} \ |
| 47 | {exists {CONF_userconf_dir/spam-limit}}} \ |
| 48 | {${lookup {$local_part_prefix\ |
| 49 | $local_part\ |
| 50 | $local_part_suffix\ |
| 51 | @$domain/\ |
| 52 | $sender_address} \ |
| 53 | nwildlsearch {CONF_userconf_dir/spam-limit} \ |
| 54 | {${if match{$value}{\N^-?[0-9]+$\N} \ |
| 55 | {spam_limit=$value} \ |
| 56 | {}}} \ |
| 57 | {}}} \ |
| 58 | {}} \ |
| 59 | ${if and {{!eq{$acl_c_mode}{submission}} \ |
| 60 | {exists {CONF_userconf_dir/spam-limit.userv}}} \ |
| 61 | {${run {timeout 5s -- \ |
| 62 | userv $local_part exim-spam-limit \ |
| 63 | $sender_address \ |
| 64 | $local_part_prefix \ |
| 65 | $local_part \ |
| 66 | $local_part_suffix \ |
| 67 | @$domain} \ |
| 68 | {${if match{$value}{\N^-?[0-9]+$\N} \ |
| 69 | {spam_limit=$value} \ |
| 70 | {}}} \ |
| 71 | {}}} \ |
| 72 | {}} |
| 73 | |
| 74 | SECTION(acl, rcpt-hooks)m4_dnl |
| 75 | ## Do per-recipient spam-filter processing. |
| 76 | require acl = rcpt_spam |
| 77 | |
| 78 | SECTION(acl, misc)m4_dnl |
| 79 | skip_spam_check: |
| 80 | |
| 81 | ## If the client is trusted, or this is a new submission, don't |
| 82 | ## bother with any of this. We will have verified the sender |
| 83 | ## fairly aggressively before granting this level of trust. |
| 84 | accept hosts = +trusted |
| 85 | accept condition = ${if eq{$acl_c_mode}{submission}} |
| 86 | |
| 87 | ## Otherwise we should check. |
| 88 | deny |
| 89 | |
| 90 | rcpt_spam: |
| 91 | |
| 92 | ## See if we should do this check. |
| 93 | accept acl = skip_spam_check |
| 94 | |
| 95 | ## Always accept mail to `postmaster'. Currently this is not |
| 96 | ## negotiable; maybe a tweak can be added to `domains.conf' if |
| 97 | ## necessary. |
| 98 | accept local_parts = postmaster |
| 99 | |
| 100 | ## Collect the user's spam threshold from the `address_data' |
| 101 | ## variable, where it was left by the `fetch_spam_limit' router |
| 102 | ## during recipient verification. (This just saves duplicating this |
| 103 | ## enormous expression.) |
| 104 | warn set acl_m_this_spam_limit = \ |
| 105 | ${sg {${extract {spam_limit} \ |
| 106 | {${if def:address_data \ |
| 107 | {$address_data}{}}} \ |
| 108 | {$value}{nil}}} \ |
| 109 | {^(|.*\\D.*)\$}{CONF_spam_max}} |
| 110 | |
| 111 | ## If there's a spam limit already established, and it's different |
| 112 | ## from this user's limit, then the sender will have to try this user |
| 113 | ## again later. |
| 114 | defer !hosts = +trusted |
| 115 | message = "You'd better try this one later" |
| 116 | condition = ${if def:acl_m_spam_limit {true}{false}} |
| 117 | condition = ${if ={$acl_m_spam_limit} \ |
| 118 | {$acl_m_this_spam_limit} \ |
| 119 | {false}{true}} |
| 120 | |
| 121 | ## There's no limit set yet, or the user's limit is the same as the |
| 122 | ## existing one, or the client's local and we're not checking for |
| 123 | ## spam anyway. Whichever way, it's safe to set it now. |
| 124 | warn set acl_m_spam_limit = $acl_m_this_spam_limit |
| 125 | |
| 126 | ## All done. |
| 127 | accept |
| 128 | |
| 129 | SECTION(acl, data-spam)m4_dnl |
| 130 | ## Do spam checking. |
| 131 | require acl = data_spam |
| 132 | |
| 133 | SECTION(acl, misc)m4_dnl |
| 134 | data_spam: |
| 135 | |
| 136 | ## See if we should do this check. |
| 137 | accept acl = skip_spam_check |
| 138 | |
| 139 | ## Check header validity. |
| 140 | require verify = header_syntax |
| 141 | |
| 142 | ## Check the message for spam, comparing to the configured limit. |
| 143 | deny spam = exim:true |
| 144 | message = Tinned meat product detected ($spam_score) |
| 145 | condition = ${if >{$spam_score_int}{$acl_m_spam_limit} \ |
| 146 | {true}{false}} |
| 147 | |
| 148 | ## Insert headers from the spam check now that we've decided to |
| 149 | ## accept the message. |
| 150 | warn |
| 151 | |
| 152 | ## Convert the limit (currently 10x fixed point) into a |
| 153 | ## decimal for presentation. |
| 154 | set acl_m_spam_limit_presentation = \ |
| 155 | ${sg{$acl_m_spam_limit}{\N(\d)$\N}{.\$1}} |
| 156 | |
| 157 | ## Convert the report into something less obnoxious. Plain |
| 158 | ## old SpamAssassin has an `X-Spam-Status' header which |
| 159 | ## lists the matched rules and provides some other basic |
| 160 | ## information. Try to extract something similar from the |
| 161 | ## report. |
| 162 | ## |
| 163 | ## This is rather fiddly. |
| 164 | |
| 165 | ## Firstly, escape angle brackets, because we'll be using |
| 166 | ## them for our own purposes. |
| 167 | set acl_m_spam_tests = ${sg{$spam_report}{([!<>])}{!\$1}} |
| 168 | |
| 169 | ## Trim off the blurb paragraph and the preview. The rest |
| 170 | ## should be fairly well behaved. Wrap double angle- |
| 171 | ## brackets around the remainder; these can't appear in the |
| 172 | ## body because we escaped them all earlier. |
| 173 | set acl_m_spam_tests = \ |
| 174 | ${sg{$acl_m_spam_tests} \ |
| 175 | {\N^(?s).*\n Content analysis details:(.*)$\N} \ |
| 176 | {<<\$1>>}} |
| 177 | |
| 178 | ## Extract the information about the matching rules and |
| 179 | ## their scores. Leave `<<...>>' around everything else. |
| 180 | set acl_m_spam_tests = \ |
| 181 | ${sg{$acl_m_spam_tests} \ |
| 182 | {\N(?s)\n\s*(-?[\d.]+)\s+([-\w]+)\s\N} \ |
| 183 | {>>\$2:\$1,<<}} |
| 184 | |
| 185 | ## Strip everything still in `<<...>>' pairs, including any |
| 186 | ## escaped characters inside. |
| 187 | set acl_m_spam_tests = \ |
| 188 | ${sg{$acl_m_spam_tests}{\N(?s)<<([^!>]+|!.)*>>\N}{}} |
| 189 | |
| 190 | ## Trim off a trailing comma. |
| 191 | set acl_m_spam_tests = ${sg{$acl_m_spam_tests}{,\s*\$}{}} |
| 192 | |
| 193 | ## Undo the escaping. |
| 194 | set acl_m_spam_tests = ${sg{$acl_m_spam_tests}{!(.)}{\$1}} |
| 195 | |
| 196 | ## Insert the headers. |
| 197 | add_header = X-SpamAssassin-Score: \ |
| 198 | $spam_score/$acl_m_spam_limit_presentation \ |
| 199 | ($spam_bar) |
| 200 | add_header = X-SpamAssassin-Status: \ |
| 201 | score=$spam_score, \ |
| 202 | limit=$acl_m_spam_limit_presentation, \n\t\ |
| 203 | tests=$acl_m_spam_tests |
| 204 | |
| 205 | ## We're good. |
| 206 | accept |
| 207 | |
| 208 | DIVERT(null) |
| 209 | ###----- That's all, folks -------------------------------------------------- |