1#------------------------------------------------------------------------------
2#	askremote.py
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: askremote.py,v 1.59 2006/01/09 04:22:27 paganini Exp $
23#------------------------------------------------------------------------------
24
25import os
26import os.path
27import re
28import string
29import tempfile
30import time
31import md5
32import re
33import asklog
34import askconfig
35import askmessage
36import askmail
37import HTMLParser
38
39#------------------------------------------------------------------------------
40
41class AskRemote:
42	"""
43	This class deals with remote execution messages.
44
45	Attributes:
46
47	- askmsg:	AskMessage Object
48	- config:  	CONFIG object
49	- log:     	LOG object
50	"""
51
52	#----------------------------------------------------------------------------------
53	def __init__(self, askmsg, config, log):
54		"""
55		Initializes the class instance (Duh!)
56		"""
57
58		## Initialize the LOG, CONFIG and MAIL objects
59
60		self.config        = config
61		self.askmsg        = askmsg
62		self.log           = log
63
64		self.user_command  = ''				## The command as sent by the user
65		self.user_args     = ''				## The command arguments
66
67		self.edit_help = """
68You've requested to edit one of your ASK lists. To complete the
69request:
70
711) Hit the Reply button.
72
732) The original contents of your list file are shown between the delimiters
74   below. Edit the contents at will but do not remove the "start" and "end"
75   delimiters.
76
773) Send the mail back.
78
79ASK knows how to handle most "quoting" chars (the ">" signs your mailer
80inserts before each line). They'll be removed automatically.
81
82ASK will refuse to save the list if your original list was modified in the
83meantime (for instance, if someone replied to a confirmation message and
84was added to the list). In that case, another message will be sent back
85indicating this fact and you'll be presented the opportunity to re-edit the
86list.
87
88Some list syntax examples:
89
90#Match all users at sourceforge.net
91from .*@sourceforge.net
92
93#Match all users at all hosts in the fsf.org domain
94from .*@.*fsf.org
95"""
96
97		self.edit_failed = """
98EDIT FAILED!
99
100Your original ASK list (white/ignore) was modified on your server
101after you made the original edit request. This is normally caused by a user
102responding to a confirmation (which causes his email to be added to the
103list). Please edit your list text below and re-submit the request.
104
105"""
106	#------------------------------------------------------------------------------
107	def set_user_command(self, str):
108		"""
109		Saves the passed string (usually the user command as found in the
110		"Subject:" field) to the "user_command" attribute.
111		"""
112
113		self.user_command = str
114
115	#------------------------------------------------------------------------------
116	def get_user_command(self, str):
117		"""
118		Returns the command saved in the "user_command" attribute.
119		"""
120
121		return(self.user_command)
122
123	#------------------------------------------------------------------------------
124	def set_user_args(self, str):
125		"""
126		Saves the passed string (usually the user command argument as found in the
127		"Subject:" field) to the "user_args" attribute.
128		"""
129
130		self.user_args = str
131
132	#------------------------------------------------------------------------------
133	def get_user_args(self):
134		"""
135		Returns the command saved in the "user_args" attribute.
136		"""
137
138		return(self.user_args)
139
140	#------------------------------------------------------------------------------
141	def is_remote_command(self):
142		"""
143		Returns true if the current message is a remote command request. False
144		otherwise.
145		"""
146
147		self.log.write(1, "  is_remote_command(): Verifying the subject...")
148		return self.process_remote_commands(check_only = 1)
149
150	#------------------------------------------------------------------------------
151	def	process_remote_commands(self, check_only = 0):
152		"""
153		Verify if the current message contains remote commands. If so, process
154		accordingly. Returns true if delivery happened, false otherwise.
155
156		A special case happens when the 'check_only' parameter is set. In this
157		case, the method will return 1 if the current email contains remote
158		commands or 0 otherwise.
159		"""
160
161		## If the message does not come from us, ignore it
162		if not self.askmsg.is_from_ourselves():
163			self.log.write(10, "  process_remote_commands(): Message is not from ourselves")
164			return 0
165
166		## IMPORTANT NOTE:
167		##
168		## cmds *MUST* be defined locally as it references objects defined within its
169		## own instance. Defining cmds as self.cmds in the constructor will create a cyclic
170		## reference that prevents the deletion of this object.
171
172		cmds = [
173			("ask process q", 		  						self.process_queue),
174			("ask queue report",    						self.process_textmode_commands),
175			("ask edit whitelist[#:]([a-f0-9]{32})$",		self.edit_whitelist),
176			("ask edit ignorelist[#:]([a-f0-9]{32})$", 		self.edit_ignorelist),
177			("ask edit blacklist[#:]([a-f-0-9]{32})$", 		self.edit_ignorelist), ## Compat ##
178			("ask edit whitelist",							self.edit_whitelist),
179			("ask edit ignorelist", 						self.edit_ignorelist),
180			("ask edit blacklist",  						self.edit_ignorelist), ## Compat ##
181			("ask help",  									self.send_help),
182			("ask command forward[#:]([a-f0-9]{32})$", 		self.command_forward_mail),
183			("ask command delete[#:]([a-f0-9]{32})$", 		self.command_delete_mail),
184			("ask command whitelist[#:]([a-f0-9]{32})$", 	self.command_whitelist),
185			("ask command ignorelist[#:]([a-f0-9]{32})$",	self.command_ignorelist),
186			("ask command blacklist[#:]([a-f0-9]{32})$", 	self.command_ignorelist), ## Compat ##
187		]
188
189		subject = self.askmsg.get_subject()
190		self.log.write(10, "  process_remote_commands(): Subject=" + subject)
191
192		ret = 0		## Default == No delivery
193
194		for (subject_re, method) in cmds:
195			res = re.search(subject_re, subject, re.IGNORECASE)
196
197			if res:
198				self.log.write(1, "  process_remote_commands(): Found subject=\"%s\"" % subject)
199
200				if check_only:
201					self.log.write(1, "  process_remote_commands(): Checking only. Returning true")
202					ret = 1
203					break
204
205				## Save the command and the argument requested by the user
206				self.set_user_command(subject)
207
208				if string.find(subject_re, "(") != -1:
209					self.set_user_args(res.group(1))
210					self.log.write(10, "  process_remote_commands(): User args = %s" % res.group(1))
211				else:
212					self.set_user_args(None)
213					self.log.write(10, "  process_remote_commands(): User args = None")
214
215
216				method() 	## Execute method
217				ret = 1		## All methods cause some type of delivery
218				break
219
220		del cmds
221		return ret
222
223	#------------------------------------------------------------------------------
224	def	command_forward_mail(self):
225		"""
226		Delivers the queued email to the user's mailbox, unless we're operating
227		in 'procmail' or 'filter' mode *and* using text mode for the remote commands.
228		In that case, the file will be re-sent to the user's primary address using sendmail
229		instead. This is necessary to make allow procmail/filter users to dequeue multiple
230		messages at once. The email has the X-ASK-Auth header added, so it will
231		pass directly thru the next invocation of ASK and be correctly processed
232		by the mail filter.
233		"""
234
235		## Set the effective MD5 to the one passed on the Subject
236		self.askmsg.set_conf_md5(self.get_user_args())
237
238		if ((self.config.procmail_mode or self.config.filter_mode) and
239		    (not self.config.rc_remote_cmd_htmlmail)):
240			self.log.write(10, "  command_forward_mail: Text mode AND procmail/filter. Will forward to self")
241			via_smtp = 1
242		else:
243			self.log.write(10, "  command_forward_mail: Delivering directly")
244			via_smtp = 0
245
246		self.askmsg.dequeue_mail("Forwarded by Command",
247								 mailbox = self.config.rc_mymailbox,
248								 via_smtp = via_smtp)
249
250	#------------------------------------------------------------------------------
251	def command_delete_mail(self):
252		"""
253		Deletes the email pointed to by the user_args.
254		"""
255
256		## Set the effective MD5 to the one passed on the Subject
257		self.askmsg.set_conf_md5(self.get_user_args())
258
259		self.askmsg.delete_mail
260
261	#------------------------------------------------------------------------------
262	def	process_textmode_commands(self):
263		"""
264		This function will go through the mail text and process everything that
265		looks like a remote command in textmode.
266		"""
267
268		self.askmsg.fh.seek(0)
269
270		while 1:
271
272			buf = self.askmsg.fh.readline()
273
274			if (buf == ''):
275				break
276
277			## Look for anything like N... Id: MD5
278			res = re.search("([nibwrd]).*Id: ([a-f0-9]{32})", buf, re.IGNORECASE)
279
280			if not res:
281				continue
282
283			action = res.group(1)
284			md5    = res.group(2)
285
286			self.log.write(1, "  process_textmode_commands(): Action [%s], MD5 [%s]" % (action, md5))
287
288			## We set the user_args to the md5 we need. Note that the
289			## idea was to 'encapsulate' this in such a way that we
290			## don't have to mess with 'md5' inside the askmessage
291			## object, but for now, it's still confusing so we'll
292			## set on both.
293
294			## MD5 used by this module's methods
295			self.set_user_args(md5)
296
297			## MD5 used by the askmessage class
298			self.askmsg.set_conf_md5(md5)
299
300			if self.askmsg.confirmation_msg_queued():
301				if action == 'I':	## Add sender to IgnoreList
302					self.command_ignorelist()
303				elif action == 'B':	## Old "Blacklist" command now adds to ignorelist
304					self.command_ignorelist()
305				elif action == 'W':	## Add sender to Whitelist
306					self.command_whitelist()
307				elif action == 'R':	## Remove mail from queue
308					self.askmsg.delete_mail()
309				elif action == 'D':	## Deliver but don't whitelist
310					self.command_forward_mail()
311			else:
312				self.log.write(1, "  process_textmode_commands(): Queued message was not found. Ignoring...")
313
314	#------------------------------------------------------------------------------
315	def process_queue(self, htmlmode = -1):
316		"""
317		Will go through the queue and send the user a list of options
318		available for each message.  If the 'textmode' parameter is set,
319		the email will be sent in text format (as opposed to HTML).
320		"""
321
322		## If no htmlmode is specified, use the settings found
323		## in self.config.rc_remote_cmd_htmlmail
324
325		if htmlmode == -1:
326			htmlmode = self.config.rc_remote_cmd_htmlmail
327
328		aMail          = askmail.AskMail(self.config, self.log)
329		aMail.fullname = self.config.rc_myfullname
330		aMail.mailfrom = self.config.rc_mymails[0]
331
332		tempFile       = "%s.%d" % (tempfile.mktemp(), os.getpid())
333		tempFileHandle = open(tempFile, "w")
334
335		queueDir       = self.config.rc_msgdir
336		queueFiles     = os.listdir(queueDir)
337
338		## Reverse sort list of files by mtime
339
340		def mtime_file(x,queue=queueDir):  return(os.stat(queue + "/" + x)[8], x)	## Convert to (mtime,filename)
341		def strip_mtime(x):	return(x[1])											## Strip mtime
342
343		queueFiles = map(mtime_file, queueFiles)	## Convert to (mtime,filename)[]
344		queueFiles.sort()							## sort (on mtime)
345		queueFiles.reverse()						## Descending
346		queueFiles = map(strip_mtime, queueFiles)	## Strip mtime
347
348		##
349
350		if htmlmode:
351			tempFileHandle.write("<html>\n")
352			tempFileHandle.write("<body>\n")
353
354		if len(queueFiles) > 0:
355
356			if not htmlmode:
357				tempFileHandle.write("""
358ASK QUEUE REPORT
359
360These are the contents of your ASK queue. These emails are sitting in the
361queue waiting for a confirmation from the sender. Change the "N" on the left
362of each email with the desired action. Actions are:
363
364N - Do Nothing. Leave it queued.
365D - Deliver this message to my In-Box.
366W - Deliver this message to my In-Box and add sender to Whitelist.
367R - Delete this message from the Queue.
368I - Delete this message from the Queue and ignore future emails.
369
370Just edit the message as you wish and reply. Quotes are OK.
371Queue contents:
372
373""")
374			#---
375
376			for oneMessageFile in queueFiles:
377
378				aFileHandle = open(queueDir + "/" + oneMessageFile, "r")
379				aMessage    = askmessage.AskMessage(self.config, self.log)
380				aMessage.read(aFileHandle)
381				aFileHandle.close()
382
383				## Printable Date & Time
384
385				msg_date = ""
386				try:
387					msg_date = time.ctime()
388				except:
389					pass
390
391				if not msg_date:
392					msg_date = "[Invalid]"
393
394				## Get last "Received: from" header that contains an IP and
395				## is not from localhost (127.x.x.x)
396
397				received_from = ''
398
399				headerlist = aMessage.msg.getallmatchingheaders("Received")
400				headerlist.reverse()
401
402				for header in headerlist:
403					header = string.strip(header)
404
405					if (re.search(" from.*[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*", header) and
406						(not re.search(" from.*127\.[0-9]*\.[0-9]*\.[0-9]*", header))):
407						received_from = header[9:]	## Strip "Received:"
408						break
409
410				## Get all X-ASK-Info headers.
411
412				askinfolist = []
413
414				for header in aMessage.msg.getallmatchingheaders("X-ASK-Info"):
415					header = string.strip(header)
416					askinfolist.append(header[12:])	## Strip "X-ASK-Info:"
417
418				## File Size
419
420				filesize     = os.path.getsize(queueDir + "/" + oneMessageFile)
421				filesize_str = "%d bytes" % filesize
422
423				if filesize > 1024:
424					filesize = filesize / 1024
425					filesize_str = "%s KB" % filesize
426
427				if filesize > 1048576:
428					filesize = filesize / 1024
429					filesize_str = "%s MB" % filesize
430
431
432				## Grab the MD5 from the filename (user may have changed key, etc, etc)
433				res = re.search("([a-f0-9]{32})", oneMessageFile, re.IGNORECASE)
434
435				if not res:
436					self.log.write(1, "  process_queue(): Could not find MD5 for file %s. Ignored" % oneMessageFile)
437					continue
438
439				file_md5 = res.group(1)
440
441				self.log.write(10, "  process_queue(): filename=%s, file_md5=%s" % (oneMessageFile, file_md5))
442
443				if htmlmode:
444					tempFileHandle.write('<hr><br>\n')
445					tempFileHandle.write('<table border=0 width="100%">\n')
446
447					tempFileHandle.write('<td width="5%" bgcolor="#B0B0FF"><b>From:</b></td>\n')
448					tempFileHandle.write('<td>%s</td>\n' % aMessage.strip_html(aMessage.get_sender()[1]))
449					tempFileHandle.write('<td></td>\n')
450					tempFileHandle.write('<tr>\n')
451
452					tempFileHandle.write('<td width="5%" bgcolor="#B0B0FF"><b>Subject:</b></td>\n')
453					tempFileHandle.write('<td>%s</td>\n' % aMessage.strip_html(aMessage.get_subject()))
454					tempFileHandle.write('<td></td>\n')
455					tempFileHandle.write('<tr>\n')
456
457					tempFileHandle.write('<td width="5%" bgcolor="#B0B0FF"><b>Date:</b></td>\n')
458					tempFileHandle.write('<td>%s</td>\n' % msg_date)
459					tempFileHandle.write('<td></td>\n')
460					tempFileHandle.write('<tr>\n')
461
462					tempFileHandle.write('<td width="5%" bgcolor="#B0B0FF"><b>Received:</b></td>\n')
463					tempFileHandle.write('<td>%s</td>\n' % received_from)
464					tempFileHandle.write('<td></td>\n')
465					tempFileHandle.write('<tr>\n')
466
467					first = 1
468					for x_ask_info in askinfolist:
469						if first:
470							tempFileHandle.write('<td width="5%" bgcolor="#B0B0FF"><b>X-ASK-Info:</b></td>\n')
471							first = 0
472						else:
473							tempFileHandle.write('<td width="5%" bgcolor="#B0B0FF"><br></td>\n')
474
475						tempFileHandle.write('<td>%s</td>\n' % x_ask_info)
476						tempFileHandle.write('<td></td>\n')
477						tempFileHandle.write('<tr>\n')
478
479					tempFileHandle.write('<td width="5%" bgcolor="#B0B0FF"><b>Size:</b></td>\n')
480					tempFileHandle.write('<td>%s</td>\n' % filesize_str)
481					tempFileHandle.write('<td></td>\n')
482					tempFileHandle.write('<tr>\n')
483
484					## Read Preview
485
486					summary = aMessage.summary(300)
487
488					tempFileHandle.write('<td colspan=3 bgcolor="#F0F0F0">\n')
489					tempFileHandle.write('<font size=-1><b>Message Preview</b><p>\n')
490
491					tempFileHandle.write(summary)
492
493					tempFileHandle.write('</td>\n')
494					tempFileHandle.write('</tr>\n')
495
496					tempFileHandle.write('<td align="left" colspan=3>\n')
497					tempFileHandle.write('<table border=0 width="100%">\n')
498					tempFileHandle.write('<td align="center" bgcolor="#E0E0E0"><font size="-1"><a href="mailto:%s?subject=ask command forward:%s">Deliver to my Inbox</font></td>\n' % (self.config.rc_mymails[0], file_md5))
499					tempFileHandle.write('<td align="center" bgcolor="#E0E0E0"><font size="-1"><a href="mailto:%s?subject=ask command whitelist:%s">Deliver to my Inbox<br>and Add Sender to Whitelist</font></td>\n' % (self.config.rc_mymails[0], file_md5))
500					tempFileHandle.write('<td align="center" bgcolor="#E0E0E0"><font size="-1"><a href="mailto:%s?subject=ask command delete:%s">Delete Message<br>From the Queue</font></td>\n' % (self.config.rc_mymails[0], file_md5))
501					tempFileHandle.write('<td align="center" bgcolor="#E0E0E0"><font size="-1"><a href="mailto:%s?subject=ask command ignorelist:%s">Delete Message from the Queue<br>and Ignore Future e-mails</font></td>\n' % (self.config.rc_mymails[0], file_md5))
502					tempFileHandle.write('</table>')
503
504					tempFileHandle.write('</td>\n')
505					tempFileHandle.write('</tr>\n')
506
507					tempFileHandle.write('</table>\n')
508					tempFileHandle.write('<br><br>\n')
509
510					del aMessage
511
512				else:
513
514					## Default action == D (delete) if the file is older than
515					## rc_remote_cmd_max_age days.
516
517					filetime = os.path.getmtime(os.path.join(queueDir, oneMessageFile))
518					now      = time.time()
519
520					if (filetime < (now - (self.config.rc_remote_cmd_max_age * 86400))):
521						default_action = "R"
522					else:
523						default_action = "N"
524
525					tempFileHandle.write("%s  Id: %s\n" % (default_action, file_md5))
526					tempFileHandle.write("   From:       %s\n" % aMessage.get_sender()[1])
527					tempFileHandle.write("   Subject:    %-65.65s\n" % aMessage.get_subject())
528					tempFileHandle.write("   Date:       %s,    Size: %s\n" % (msg_date, filesize_str))
529
530					## X-ASK-Info
531
532					for x_ask_info in askinfolist:
533						tempFileHandle.write("   X-ASK-Info: %-65.65s\n" % x_ask_info)
534
535					## Read summary
536					summary = aMessage.summary(300)
537
538					tempFileHandle.write("\n")
539					tempFileHandle.write("%s\n" % summary)
540
541					tempFileHandle.write("\n------------------------------------------------------------\n\n")
542
543		else:
544			tempFileHandle.write("The message queue is empty\n")
545
546		#---
547
548		if htmlmode:
549			tempFileHandle.write("</body>\n")
550			tempFileHandle.write("</html>\n")
551
552		tempFileHandle.close()
553
554		aMail.deliver_mail(mailbox        = self.config.rc_mymailbox,
555						   mailto         = self.config.rc_mymails[0],
556						   subject        = "ASK queue report",
557						   body_filenames = [tempFile],
558						   custom_headers = [ "X-ASK-Auth: %s" % self.askmsg.generate_auth(), "Precedence: bulk" ],
559						   html_mail      = htmlmode)
560		os.unlink(tempFile)
561
562	#------------------------------------------------------------------------------
563	def send_help(self):
564		"""
565		Sends the help file the the sender.
566		"""
567
568		boundary_text = "=_ASKMessageSegment-AOK_="
569		mail_to       = self.config.rc_mymails[0]
570
571		help_text = """
572ASK HELP Message
573
574ASK recognizes the following "special" subjects:
575
576ask help
577	Returns this message
578
579ask process queue
580	Sends you a list of mail in your queue and lets you act on them
581
582ask edit whitelist
583	Allows you to edit your whitelist
584
585ask edit ignorelist
586	Allows you to edit your ignorelist
587
588For more information about ASK (Active Spam Killer), please visit:
589http://www.paganini.net/ask
590"""
591		################   END PLAINTEXT - START HTML #############
592
593		help_html = """
594<html>
595  <head>
596  <title>ASK HELP</title>
597  <meta http-equiv="content-type" content="text/html\; charset=ISO-8859-1">
598</head>
599<body>
600<br>
601ASK understands several commands. All these commands are
602issued by sending mail to yourself with special subjects. These subjects are:
603<p>
604<dl>
605  <dt><a href="mailto:%s?subject=ask%%20help">ask help</a></dt>
606  <dd><p>Returns this message</dd>
607  <br>
608
609  <dt><a href="mailto:%s?subject=ask%%20process%%20queue">ask process queue</a></dt>
610  <dd><p>Sends you a list of mail in your queue and lets you act on them</dd>
611  <br>
612
613  <dt><a href="mailto:%s?subject=ask%%20edit%%20whitelist">ask edit whitelist</a></dt>
614  <dd><p>Allows you to edit your whitelist</dd>
615  <br>
616
617  <dt><a href="mailto:%s?subject=ask%%20edit%%20ignorelist">ask edit ignorelist</a></dt>
618  <dd><p>Allows you to edit your ignorelist</dd>
619</dl>
620<p>
621For more information about ASK (Active Spam Killer), please visit
622<a href="http://www.paganini.net/ask">ASK's Homepage</a>
623<p>
624</body>
625</html>
626""" % (mail_to, mail_to, mail_to, mail_to)
627
628		full_message = help_text + "\n--" + boundary_text + "\nContent-Type: text/plain; charset=\"iso-8859-1\"\n\n" + help_text + "\n--" + boundary_text + "\nContent-Type: text/html; charset=\"iso-8859-1\"\n\n" + help_html + "\n--" + boundary_text + "--\n\n"
629		tempFile = "%s.%d" % (tempfile.mktemp(), os.getpid())
630		tempFileHandle = open(tempFile, "w")
631		tempFileHandle.write(full_message)
632		tempFileHandle.close()
633
634		aMail = askmail.AskMail(self.config, self.log)
635
636		aMail.fullname = self.config.rc_myfullname
637		aMail.mailfrom = mail_to
638
639		aMail.deliver_mail(mailbox        = self.config.rc_mymailbox,
640						   mailto         = mail_to,
641						   subject        = "ASK Help",
642						   body_filenames = [tempFile],
643						   custom_headers = ["Content-Type: multipart/alternative;\n\tboundary=\"" + boundary_text + "\"",
644											 "MIME-Version: 1.0",
645											 "X-ASK-Auth: %s" % self.askmsg.generate_auth(),
646											 "Precedence: bulk" ])
647		os.unlink(tempFile)
648
649	#------------------------------------------------------------------------------
650	def command_ignorelist(self):
651		"""
652		Adds the message pointed at by the md5 value to the
653		ignorelist and then deletes the message.
654		"""
655
656		## Set the effective MD5 to the one passed on the Subject
657		self.askmsg.set_conf_md5(self.get_user_args())
658
659		queueFileName   = self.askmsg.queue_file(self.askmsg.conf_md5)
660
661		## Create a new AskMessage instance with the queued file
662		aMessage        = askmessage.AskMessage(self.config, self.log)
663		queueFilehandle = open(queueFileName, "r")
664
665		aMessage.read(queueFilehandle)
666		queueFilehandle.close()
667
668		self.log.write(1, "  command_ignorelist(): Adding message %s to ignorelist" % queueFileName)
669		aMessage.add_to_ignorelist()
670
671		self.askmsg.delete_mail()
672
673	#------------------------------------------------------------------------------
674	def command_whitelist(self):
675		"""
676		Adds the message pointed at by the md5 stored in
677		user_args to the whitelist, and delivers the message.
678		"""
679
680		## Set the effective MD5 to the one passed on the Subject
681		self.askmsg.set_conf_md5(self.get_user_args())
682
683		## Create a new AskMessage instance with the queued file,
684		## so we can get add the sender's email to the whitelist.
685
686		queued_fname = self.askmsg.queue_file(self.askmsg.conf_md5)
687		askmsg       = askmessage.AskMessage(self.config, self.log)
688		queued_fh    = open(queued_fname, "r")
689
690		askmsg.read(queued_fh)
691		queued_fh.close()
692
693		self.log.write(1, "  command_whitelist(): Adding message %s to whitelist" % queued_fname)
694		askmsg.add_to_whitelist()
695
696		## Dequeue and remove the queued file
697		self.command_forward_mail()
698
699	#------------------------------------------------------------------------------
700	def edit_whitelist(self):
701		"""
702		Sends the whitelist file to the sender.
703		"""
704
705		whitelistFileName = self.config.rc_whitelist[0]
706		self.__edit_file(whitelistFileName, "Whitelist")
707
708	#------------------------------------------------------------------------------
709	def edit_ignorelist(self):
710		"""
711		Sends the ignorelist file to the sender.
712		"""
713		ignorelistFileName = self.config.rc_ignorelist[0]
714		self.__edit_file(ignorelistFileName, "Ignorelist")
715
716	#------------------------------------------------------------------------------
717	def __edit_file(self, file_path, file_description):
718		"""
719		Do the actual work of checking if the file exists, matching
720		the md5 value from self.askmsg, and then calling
721		__save_file and __send_file as appropriate
722		"""
723
724		message_subject = "ASK Edit " + file_description
725
726		## User args will contain the file MD5 if this is a request to
727		## Save the file or None if it's a request to Send the file to self
728
729		if os.path.exists(file_path):
730			if self.get_user_args():
731				if self.fileMatchesMD5(file_path, self.get_user_args()):
732					self.__save_file(file_path)
733					self.__send_file(file_path, message_subject, "FILE SAVED!\n\n" + self.edit_help)
734				else:
735					self.__send_file(file_path, message_subject, self.edit_failed + "\n" + self.edit_help)
736			else:
737				self.log.write(1, "  edit_file(): Sending whitelist to self...")
738				self.__send_file(file_path, message_subject, self.edit_help)
739		else:
740			self.log.write(1, "  edit_file(): Sending whitelist to self...")
741			self.__send_file(file_path, message_subject, self.edit_help)
742
743	#------------------------------------------------------------------------------
744	def fileMatchesMD5(self, fileName, asciiMD5):
745		"""
746		Check to see if the file at the given path matches the md5
747		checksum
748		"""
749
750		if (os.access(fileName, os.F_OK) != 1):
751			return 0
752
753		fileHandle = open(fileName, "r")
754
755		## Create a new MD5 object
756		md5sum = md5.new()
757
758		while 1:
759			buf = fileHandle.readline()
760
761			if (buf == ''):
762				break
763
764			md5sum.update(buf)
765
766		fileHandle.close()
767
768		ascii_digest = ''
769		binary_digest = md5sum.digest()
770
771		for ch in range(0,len(binary_digest)):
772			ascii_digest = ascii_digest + "%02.2x" % ord(binary_digest[ch])
773
774		return ascii_digest == asciiMD5
775
776	#------------------------------------------------------------------------------
777	def __save_file(self, fileName):
778		"""
779		Saves part of the current message between delimiters to the passed
780		filename. "Quoting" chars are removed in the process.
781		"""
782
783		self.log.write(1, "  Saving file %s" % fileName)
784
785		fileHandle = open(fileName + ".new", "w")
786
787		buf = ''
788		self.askmsg.fh.seek(0)
789
790		## Read all the way until "-- start file" tag is found
791
792		while 1:
793			buf = self.askmsg.fh.readline()
794
795			if (buf == ''):
796				fileHandle.close()
797				return 0
798
799			buf = string.strip(buf)
800			buf = self.__dequote(buf)
801
802			if string.find(buf, "--- start file") == 0:
803				break
804
805		## Read contents until "--- end file" tag is found
806
807		while 1:
808			buf = self.askmsg.fh.readline()
809
810			if (buf == ''):
811				fileHandle.close()
812				return 0
813
814			buf = string.strip(buf)
815			buf = self.__dequote(buf)
816
817			if string.find(buf, "--- end file") == 0:
818				break
819
820			fileHandle.write("%s\n" % buf)
821
822
823		fileHandle.close()
824
825		# Let's be atomic
826		os.rename(fileName + ".new", fileName)
827
828	#------------------------------------------------------------------------------
829	def __send_file(self, filename, subject, bonus_text = ""):
830		"""
831		Sends the given filename with the given subject.
832		This file will be in the appropriate format to be edited and
833		resubmitted for update
834		"""
835
836		localTempFile       = "%s.%d" % (tempfile.mktemp(), os.getpid())
837		localTempFileHandle = open(localTempFile, "w")
838
839		localTempFileHandle.write("%s\n" % bonus_text)
840		localTempFileHandle.write("\n")
841		localTempFileHandle.write("--- start file %s ---\n" % filename)
842
843		## Better create a file if it isn't there yet
844		if not os.path.exists(filename):
845			sendFileHandle = open(filename, "w")
846			sendFileHandle.close()
847
848		sendFileHandle = open(filename, "r")
849
850		## Create a new MD5 object
851		md5sum = md5.new()
852
853		while 1:
854			buf = sendFileHandle.readline()
855
856			if (buf == ''):
857				break
858
859			localTempFileHandle.write(buf)
860			md5sum.update(buf)
861
862		ascii_digest = ''
863		binary_digest = md5sum.digest()
864
865		for ch in range(0,len(binary_digest)):
866			ascii_digest = ascii_digest + "%02.2x" % ord(binary_digest[ch])
867
868
869		localTempFileHandle.write("--- end file %s ---\n" % filename)
870		localTempFileHandle.close()
871		sendFileHandle.close()
872
873		aMail          = askmail.AskMail(self.config, self.log)
874		aMail.fullname = self.config.rc_myfullname
875		aMail.mailfrom = self.config.rc_mymails[0]
876
877		aMail.deliver_mail(mailbox 		  = self.config.rc_mymailbox,
878						   mailto  		  = self.config.rc_mymails[0],
879						   subject 		  = "%s#%s" % (subject, ascii_digest),
880						   body_filenames = [localTempFile],
881						   custom_headers = [ "X-ASK-Auth: %s" % self.askmsg.generate_auth(), "Precedence: bulk" ])
882
883		os.unlink(localTempFile)
884
885	#------------------------------------------------------------------------------
886	def __dequote(self, str):
887		"""
888		This method will remove most quoting chars inserted by mail agents and
889		return the unquoted part.
890		"""
891
892		res = re.match("^([ \t]*[|>:}][ \t]*)+(.*)", str)
893
894		if res:
895			return res.group(2)
896		else:
897			return str
898
899