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