1#------------------------------------------------------------------------------ 2# askmain.py -- Main class for ASK 3# 4# (C) 2001-2006 by Marco Paganini (paganini@paganini.net) 5# 6# This file is part of ASK - Active Spam Killer 7# 8# ASK is free software; you can redistribute it and/or modify 9# it under the terms of the GNU General Public License as published by 10# the Free Software Foundation; either version 2 of the License, or 11# (at your option) any later version. 12# 13# ASK is distributed in the hope that it will be useful, 14# but WITHOUT ANY WARRANTY; without even the implied warranty of 15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16# GNU General Public License for more details. 17# 18# You should have received a copy of the GNU General Public License 19# along with ASK; if not, write to the Free Software 20# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 21# 22# $Id: askmain.py,v 1.49 2006/01/09 04:22:26 paganini Exp $ 23#------------------------------------------------------------------------------ 24 25import sys 26import askversion 27import asklog 28import askconfig 29import askmail 30import askmessage 31import askremote 32 33#------------------------------------------------------------------------------ 34# MAIN 35#------------------------------------------------------------------------------ 36 37class AskMain: 38 """ 39 This is the 'master' class for ask. The typical ASK invocation 40 should be something like: 41 42 import asklog 43 import askconfig 44 import sys 45 46 config = askconfig.AskConfig(sys.argv) 47 log = asklog.AskLog(config.logfile) 48 log.loglevel = config.loglevel 49 50 ask = askmain.AskMain() 51 rc = ask.filter(sys.stdin) 52 53 sys.exit(rc) 54 55 56 Attributes: 57 58 - config: CONFIG object 59 - log: LOG object 60 - msg: MESSAGE object 61 - rmt: REMOTE_COMMAND object 62 """ 63 64 def __init__(self, config, log): 65 """ 66 This is the constructor for the ASK tool. It will create instances 67 of all objects needed for proper operation. The caller is supposed to 68 pass valid config and log instances. 69 """ 70 71 self.config = config 72 self.log = log 73 self.msg = askmessage.AskMessage(config, log) 74 self.rmt = askremote.AskRemote(self.msg, config, log) 75 76 def filter(self, filehandle): 77 """ 78 Reads (and process) the contents of the mail message pointed to by 79 the file object 'filehandle'. The return code MUST be used by 80 the caller to exit the program. 81 """ 82 83 ## Initial Logging 84 self.log.write(1, "") 85 self.log.write(1, "----- ASK v%s Started -----" % askversion.AskVersion.version) 86 87 ## Warn if incompatible version of Python is found 88 if (self.config.python_major_version < 2 and 89 self.config.python_minor_version < 2 and 90 self.config.python_minor_version < 2): 91 self.log.write(1, "WARNING: Incompatible version of Python detected (%d.%d.%d). Proceed at your own risk!" % 92 (self.config.python_major_version, self.config.python_minor_version, self.config.python_minor_version)) 93 94 self.msg.read(filehandle) 95 96 ## Header Logging 97 98 self.log.write(1, "Message from: %s <%s>" % self.msg.get_sender()) 99 100 for (recipient_name, recipient_mail) in self.msg.get_recipients(): 101 self.log.write(1, "Message to: %s <%s>" % (recipient_name, recipient_mail)) 102 103 self.log.write(1, "Message Subject: %s" % self.msg.get_subject()) 104 105 ## If the address is in the whitelist, deliver immediately, 106 ## unless the message is a confirmation return or any other 107 ## "control" message. Delivering blindly would make it 108 ## impossible for people with two pending messages to confirm 109 ## more than one as subsequent confirmation returns would be 110 ## blindly delivered. 111 112 if ((not self.msg.is_confirmation_return()) and 113 (not self.msg.is_from_mailerdaemon()) and 114 (not self.rmt.is_remote_command()) and 115 self.msg.is_in_whitelist()): 116 117 self.log.write(1, "Whitelist match [%s]. Delivering..." % self.msg.list_match) 118 self.msg.deliver_mail("Whitelist match [%s]" % self.msg.list_match) 119 return(self.config.RET_PROCMAIL_CONTINUE) 120 else: 121 self.log.write(1, "Not matched in the whitelist") 122 123 ## People in ignorelist are just thrown into the void... 124 if self.msg.is_in_ignorelist(): 125 self.log.write(1, "Ignorelist Match. Throwing message away...") 126 self.msg.discard_mail() 127 return(self.config.RET_PROCMAIL_STOP) 128 129 ## Ignore messages coming from MAILER-DAEMON (or postmaster) containing the 130 ## X-AskVersion header (they are most certainly a result of 131 ## an invalid sender address) 132 133 if self.msg.is_from_mailerdaemon(): 134 self.log.write(1, "Sender is mailer-daemon") 135 136 if self.msg.sent_by_ask(): 137 self.log.write(1, "Header X-AskVersion found. Probable invalid reply from a confirmation message. Ignoring.") 138 self.msg.discard_mail() 139 return(self.config.RET_PROCMAIL_STOP) 140 else: 141 self.log.write(1, "Header X-AskVersion not found. Copying directly to mailbox.") 142 self.msg.deliver_mail("Message from Mailer-Daemon") 143 return(self.config.RET_PROCMAIL_CONTINUE) 144 else: 145 self.log.write(1, "Sender is not mailer-daemon") 146 147 ## Auth: 148 ## - MD5 matches, Time matches: Definitely sent by US. Deliver to mailbox 149 ## - MD5 matches, Time does not: May be a fake or too old. Junk. 150 ## - MD5 does not match: Definitely not ours. Try to process normally 151 152 if (self.msg.validate_auth_md5()): 153 if (self.msg.validate_auth_time(7)): 154 self.log.write(1, "Message authenticated (sent by ASK). Delivering...") 155 self.msg.deliver_mail("Authenticated Message from ASK") 156 return(self.config.RET_PROCMAIL_CONTINUE) 157 else: 158 self.log.write(1, "Message is authentic but seems too old (could be a fake). Delivering to Junk...") 159 self.msg.junk_mail("(Junk) Authentication time expired") 160 self.msg.discard_mail() 161 return(self.config.RET_PROCMAIL_STOP) 162 163 ## If remote commands are enabled, we check for a command request 164 165 if self.config.rc_remote_cmd_enabled: 166 self.log.write(1, "Checking for remote commands") 167 168 if self.rmt.process_remote_commands(): 169 ## In filter mode, we always tell the filter to discard 170 ## the message. The results of remote commands are always 171 ## delivered directly to the mailbox, as procmail cannot 172 ## deal with more than one email anyways. 173 174 if self.config.filter_mode: 175 self.msg.discard_mail() 176 177 return(self.config.RET_PROCMAIL_STOP) 178 179 ## Is the message a confirmation return message? 180 181 if self.msg.is_confirmation_return(): 182 183 ## If we find a queued message using the confirmation MD5 184 ## the user is added to the whitelist and the message 185 ## is appended to my mailbox 186 187 if self.msg.confirmation_msg_queued(): 188 self.msg.add_to_whitelist() 189 self.msg.add_queued_msg_to_whitelist() 190 self.msg.dequeue_mail("Confirmed by User") 191 else: 192 self.log.write(1, "Queued message not found for this confirmation number. Delivering...") 193 self.msg.deliver_mail("Invalid confirmation") 194 195 return(self.config.RET_PROCMAIL_CONTINUE) 196 else: 197 self.log.write(5, "Message is not a confirmation return") 198 199 ## If the message contains our MAILKEY, we assume it is a 200 ## response of some kind, so we let it pass (and optionally, 201 ## add to whitelist too) 202 203 if (self.msg.has_our_key()): 204 205 ## Add to whitelist if configured to do so (and not already there) 206 207 if self.config.rc_whitelist_on_mailkey: 208 self.msg.add_to_whitelist() 209 self.log.write(1, "Found mailkey. Adding to whitelist") 210 211 self.log.write(1, "Our key was found in the message. Delivering...") 212 self.msg.deliver_mail("Our key was found in the mail") 213 return(self.config.RET_PROCMAIL_CONTINUE) 214 215 ## At this point, messages with our mailkey should have been processed. 216 ## If a message comes from US (without the mailkey) either it's a fake 217 ## From: line or a reply of some kind that does not include the original 218 219 if self.msg.is_from_ourselves(): 220 self.log.write(1, "Message comes from us but does not contain our key. Delivering to Junk") 221 self.msg.junk_mail("(Junk) Message from self without the mailkey") 222 self.msg.discard_mail() 223 return(self.config.RET_PROCMAIL_STOP) 224 225 ## Before we send out the confirmation, we check for mailing lists 226 ## If it's a mailing list (not treated by our whitelist above), it 227 ## will be Junked (confirmations to mailing lists is a big no no) 228 229 if self.msg.is_mailing_list_message(): 230 self.log.write(1, "Message from %s looks like a mailing list message" % self.msg.get_sender()[1]) 231 self.msg.bulk_mail("(Bulk) Mailing list message (not in whitelist)") 232 self.msg.discard_mail() 233 return(self.config.RET_PROCMAIL_STOP) 234 235 ## Message already queued? If so, don't send confirmation 236 237 if self.msg.is_queued(): 238 self.log.write(1, "Message is already queued. Won't resend.") 239 self.msg.discard_mail() 240 return(self.config.RET_PROCMAIL_STOP) 241 242 ## Attempt to validate the sender via SMTP, if configured to do so. 243 ## If the sender is deemed invalid, discard this message. 244 245 if self.config.rc_smtp_validate: 246 if self.msg.valid_smtp_sender(): 247 self.log.write(1, " SMTP validation could not determine that sender is invalid.") 248 else: 249 self.log.write(1, " SMTP validation returned invalid sender. Discarding...") 250 self.msg.discard_mail() 251 return(self.config.RET_PROCMAIL_STOP) 252 253 # Junk the email if it's not in the list of known recipients 254 # (and configured to do so in the .askrc file) 255 256 if self.config.rc_junk_unknown_recipients and not self.msg.match_recipient(): 257 self.msg.junk_mail("(Junk) Message with none of our addresses in recipient list") 258 self.msg.discard_mail() 259 return(self.config.RET_PROCMAIL_STOP) 260 261 ## All clear, queue, send confirmation and discard. 262 263 self.log.write(1, "Sending confirmation.") 264 self.msg.queue_mail() 265 self.msg.send_confirmation() 266 267 self.msg.discard_mail() 268 return(self.config.RET_PROCMAIL_STOP) 269 270 271## EOF ## 272