#
# Incoming mail filters.
#

#
# Some things need to be added:
#      * action mark message (with mark and/or as read)
#      * ability to temporarily disable a filter
#

CONDITION_EQUAL =	0
CONDITION_NOTEQUAL =	1
CONDITION_CONTAINS =	2
CONDITION_LARGERTHAN =	3
CONDITION_REGEX_MATCH = 4
CONDITION_MATCH_ALL =	5

ACTION_MOVE = 0
ACTION_LEAVE = 1
ACTION_DELETE = 2
ACTION_FORWARD = 3
ACTION_LEAVE_COLLECT = 4

f_headers = [ "to", "from", "subject", "x-from_", "envelope-to" ]

f_conditions = [ "equals", "not equals", "contains", "size > (Kb)", "regex match", "match all" ]

f_actions = [ "Move to folder:",
	    "Leave on server",
            "Delete from server",
            "Forward to address:",
	    "Collect & leave on server"]

import time
import re
import gtk

import ptk.folder_tree
from pyneheaders import *
import pynemsg
import boxtypes
import pynei18n
from boxtypes import superbox
import utils

# a filter is a tuple like so:
# ( string name, string header to match, integer condition,
#   string value to match, integer action, string action argument )
# eg: ( "Ignore annoying guy", "from", CONDITION_EQUAL, "twat@aol.com",
#       ACTION_MOVE, <Deleted folder UID as string> )
# will move mail from 'twat@aol.com' to the deleted folder

def filter_collect(user, _mailbox, connection, index):
	"""
	Collect message 'index' from `connection`, applying
	filters.
	"""
	def _do_server_policy(msg, _mailbox=_mailbox, index=index, connection=connection):
		"""
		What do we want to do with the message on the server
		now it has been collected?
		"""
		if _mailbox.collect_policy[0] == SERVER_DELETE:
			connection.delete_msg(index)
			return
		elif _mailbox.collect_policy[0] == SERVER_LEAVE:
			return
		elif _mailbox.collect_policy[0] == SERVER_EXPIRE:
			# Delete if older than some days...
			days = _mailbox.collect_policy[1]
			if days == None:
				return
			howold = utils.days_between(time.time(), msg.date)
			#print howold
			#if msg.headers.has_key("date"):
			#	print msg.headers["date"]
			if howold >= days:
				print "Expiring "+str(index)
				connection.delete_msg(index)
	
	# return message
	r = ""
	# download headers only ATM
	msg = pynemsg.pynemsg()
	
	head = connection.get_header(index)
	if head == None:
		return ""
	msg.body = head + "\n\n"
	# have we appended the body yet?
	gotbody = 0
	# Parse headers
	msg.parseheaders(user, headers_only=1)
	msg.date_received = int(time.time())
	# Have we already downloaded the thing?
	# XXX broken if we direct stuff to other boxes
	if msg.headers["message-id"] in _mailbox.messages:
		#print "Already got "+msg.headers["message-id"]
		_do_server_policy(msg)
		return r
		
	# Does it match any filters (multiple matches possible)
	nomatches = 1
	for filtr in _mailbox.filters:
		name, head, cond, carg, actn, aarg = filtr
		match = 0
		# this condition tests not filthy headers...
		if cond != CONDITION_LARGERTHAN:
			if not msg.headers.has_key(head):
				print "Debug: filter: no such header (%s)" % head
				continue
			head = msg.headers[head]
		#print "Debug: Comparing %s with %s." % (repr(head.lower()), repr(carg.lower()))
		if cond == CONDITION_LARGERTHAN:
			# get body and check size
			if isinstance(_mailbox, boxtypes.nntpbox.newsgroup):
				# don't bother if we are only collecting headers
				if (_mailbox.opts & superbox.OPT_HEADERS_ONLY):
					continue
				if gotbody == 0:
					body = connection.get_body(index)
					msg.opts = msg.opts & (~MSG_NO_BODY)
					msg.body = msg.body + body
					gotbody = 1
				## including headers XXX
				if len(msg.body) > (1024*int(carg)):
					match = 1
			elif isinstance(_mailbox, boxtypes.mailbox.mailbox):
				# get message size from our {id: size} message  dict
				size = _mailbox.pending_msgs[index]
				if int(size) > (1024*int(carg)):
					match = 1
		elif cond == CONDITION_EQUAL:
			if head.lower() == carg.lower():
				match = 1
		elif cond == CONDITION_NOTEQUAL:
			if head.lower() != carg.lower():
				match = 1
		elif cond == CONDITION_CONTAINS:
			if head.lower().find(carg.lower()) != -1:
				match = 1
		elif cond == CONDITION_REGEX_MATCH:
			if re.match(carg.lower(), head.lower()) != None:
				match = 1
		elif cond == CONDITION_MATCH_ALL:
			match = 1
		
		#if match: print "Match found"
		#else: print "NO MATCH"
		
		if match == 1:
			r = "\""+name+"\" "+_("filter match.")
			nomatches = 0

			# It matched a filter. do action
			if actn == ACTION_MOVE:
				# get body
				if (gotbody == 0) and not (_mailbox.opts & superbox.OPT_HEADERS_ONLY):
					body = connection.get_body(index)
					msg.body = msg.body + body
					msg.opts = msg.opts & (~MSG_NO_BODY)
					gotbody = 1
				# get folder to move to, from UID
				folder = user.get_folder_by_uid(aarg)
				if folder == None:
					print "FILTER: Error. move_to folder not found."
					print "FILTER: "+str(filtr)
				
					_mailbox.num_unread += 1
					_mailbox.save_new_article(msg)
					_do_server_policy(msg)
				else:
					# Add message to mailbox
					folder.num_unread += 1
					folder.save_new_article(msg)

					# What to do with copy on server
					_do_server_policy(msg)

					# Mark folder for update
					folder.changed = 1
				# some obsoleted comment clinging to existence
				return r

			elif actn == ACTION_LEAVE:
				# leave on server. don't store
				return r
			elif actn == ACTION_DELETE:
				# Delete from server. don't store
				connection.delete_msg(index)
				return r
			elif actn == ACTION_FORWARD:
				# get body
				if (gotbody == 0) and not (_mailbox.opts & superbox.OPT_HEADERS_ONLY):
					body = connection.get_body(index)
					msg.body = msg.body + body
					msg.opts = msg.opts & (~MSG_NO_BODY)
					gotbody = 1
				msg.parseheaders(user)
				outbox = user.get_folder_by_uid("outbox")
				outbox._reply(user, _mailbox, _mailbox, msg, \
				    REPLY_FORWARD, send_to=aarg, dont_open_editbox=1)
				# What to do with copy on server
				_do_server_policy(msg)
				return r
			elif actn == ACTION_LEAVE_COLLECT:
				# collect but leave on server
				if (gotbody == 0) and not (_mailbox.opts & superbox.OPT_HEADERS_ONLY):
					body = connection.get_body(index)
					msg.body = msg.body + body
					msg.opts = msg.opts & (~MSG_NO_BODY)
					gotbody = 1
				# Add message to mailbox
				_mailbox.num_unread += 1
				_mailbox.save_new_article(msg)
				
				return r
	if nomatches == 1:
		# default action. store to mailbox and trash
		# from server
		if (gotbody == 0) and not (_mailbox.opts & superbox.OPT_HEADERS_ONLY):
			body = connection.get_body(index)
			msg.body = msg.body + body
			msg.opts = msg.opts & (~MSG_NO_BODY)
			gotbody = 1
		# Add message to mailbox
		_mailbox.num_unread += 1
		_mailbox.save_new_article(msg)

		# What to do with copy on server
		_do_server_policy(msg)
	
		return r

class filter_editbox:
	"""
	A gtk.Box full of shit for editing a mailbox.filters
	"""

	def sel_folder(self, w, event, tree):
		folder = self.user.get_folder_by_uid(tree.user_selected)
		if folder != None:
			self.aarg_in.set_text(folder.name)
			self.f_aarg = folder.uid
		self.update_state()

	def ch_condition(self, _a):
		self.f_cond = self.cond_combo.get_active ()
	
	def ch_header(self, _a):
		self.f_head = self.header_combo.get_active ()
		self.update_state()

	def update_state(self):
		# f_actn has changed. update widgets.
		# also f_head
		if self.f_head == len(f_headers):
			self.header_in.show()
		else:
			self.header_in.hide()
		
		if self.f_actn == ACTION_MOVE:
			self.folder_list.show()
			self.aarg_in.show()
			self.aarg_in.set_sensitive(False)
			# No folder selected to move to. BAD!!
			folder = self.user.get_folder_by_uid(self.f_aarg)
			if folder == None:
				self.aarg_in.set_text("")
				self.apply_button.set_sensitive(False)
				self.ok_button.set_sensitive(False)
			else:
				self.aarg_in.set_text(folder.name)
				self.apply_button.set_sensitive(True)
				self.ok_button.set_sensitive(True)
		elif self.f_actn == ACTION_FORWARD:
			self.folder_list.hide()
			self.aarg_in.set_text(self.f_aarg)
			self.aarg_in.show()
			self.aarg_in.set_sensitive(True)
		elif self.f_actn == ACTION_LEAVE or self.f_actn == ACTION_DELETE \
				or self.f_actn == ACTION_LEAVE_COLLECT:
			self.folder_list.hide()
			self.aarg_in.hide()

	def ch_action(self, item):
		self.f_actn = self.act_combo.get_active ()
		self.update_state()

	def __init__(self, user, filter_list_box, index):
		import copy
		self.user = user
		
		self.filter = copy.deepcopy(filter_list_box.filters[index])
		self.filter_index = index
		self.user = filter_list_box.user
		self.filter_list_box = filter_list_box
		
		# Load filter into widgets
		self.f_name = self.filter[0]
		if self.filter[1] in f_headers:
			self.f_head = f_headers.index(self.filter[1])
		else:
			# Custom...
			self.f_head = len(f_headers)
		self.f_cond = self.filter[2]
		self.f_carg = self.filter[3]
		self.f_actn = self.filter[4]
		self.f_aarg = self.filter[5]
		
		self.win = gtk.Window()
		self.win.set_title(_("Editing filter"))
		self.win.set_size_request(400,400)

		box = gtk.VBox(spacing=5)
		box.set_border_width(5)
		self.win.add(box)
		box.show()

		# Disgusting over-use of gtk.Boxes seems like a nice way
		# of constructing this ;-)
		box0 = gtk.HBox(spacing=5) ; box0.set_border_width(5)
		box1 = gtk.HBox(spacing=5) ; box1.set_border_width(5)
		box2 = gtk.HBox(spacing=5) ; box2.set_border_width(5)
		box3 = gtk.HBox(spacing=5) ; box3.set_border_width(5)
		box4 = gtk.HBox()
		box4_1 = gtk.VBox(spacing=5) ; box4_1.set_border_width(5)
		box4_2 = gtk.VBox(spacing=5) ; box4_2.set_border_width(5)

		# Box0: filter name
		label = gtk.Label(_("Filter name: "))
		box0.pack_start(label, expand=False)
		label.show()

		self.name_in = gtk.Entry()
		box0.pack_start(self.name_in)
		self.name_in.show()

		box.pack_start(box0, expand=False)
		box0.show()
	
		# Box1: if [text-in] <suggested headers>
		# eg:   if "subject"
		label = gtk.Label(_("If header"))
		box1.pack_start(label, expand=False)
		label.show()
	
		self.header_combo = gtk.combo_box_new_text ()
		for i in range (0, len (f_headers)):
			self.header_combo.append_text (f_headers[i])
			if self.f_head == i:
				self.header_combo.set_active (i)
		self.header_combo.append_text ("Custom:")
		self.header_combo.connect ("changed", self.ch_header)
		box1.pack_start(self.header_combo, expand=False)
		self.header_combo.show ()
		
		self.header_in = gtk.Entry()
		box1.pack_start(self.header_in)
		self.header_in.show()
	
		box.pack_start(box1, expand=False)
		box1.show()
	
		# Box2: <condition> <textin>
		# eg:   Equals "Here to torment you"
		self.cond_combo = gtk.combo_box_new_text ()
		for i in range (0, len (f_conditions)):
			self.cond_combo.append_text (f_conditions[i])
		self.cond_combo.set_active (self.f_cond)
		self.cond_combo.connect ("changed", self.ch_condition)
		box2.pack_start(self.cond_combo, expand=False)
		self.cond_combo.show ()

		self.carg_in = gtk.Entry()
		box2.pack_end(self.carg_in)
		self.carg_in.show()
	
		box.pack_start(box2, expand=False)
		box2.show()

		# Box3: then <action> <argument>
		# eg:   then "move to" deleted
		label = gtk.Label(_("then"))
		box3.pack_start(label, expand=False)
		label.show()

		self.act_combo = gtk.combo_box_new_text ()
		for i in range (0, len (f_actions)):
			self.act_combo.append_text (f_actions[i])
		self.act_combo.set_active (self.f_actn)
		self.act_combo.connect ("changed", self.ch_action)
		box3.pack_start (self.act_combo, expand=False)
		self.act_combo.show ()

		self.aarg_in = gtk.Entry()
		box3.pack_end(self.aarg_in)
		self.aarg_in.show()

		box.pack_start(box3, expand=False)
		box3.show()

		# Box4_1: bunch of buttons and folder selector
		# for move_to rule
		self.folder_list = ptk.folder_tree.folder_tree(user)
		self.folder_list.update(self.user, update_all=1)
		self.folder_list.connect_button_press_event(self.sel_folder)
		swin = gtk.ScrolledWindow()
		swin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
		swin.show()
		swin.add(self.folder_list)
		box4_1.pack_start(swin)#, expand=False)
		swin.show()
		self.folder_list_win = swin

		def _save_filter(_button, self=self):
			self.f_name = self.name_in.get_text()
			if self.f_head < len(f_headers):
				# predefined header
				head_to_match = f_headers[self.f_head]
			else:
				# custom one
				head_to_match = self.header_in.get_text()
			# (if action is move then f_aarg is already a folder UID)
			if self.f_actn != ACTION_MOVE:
				self.f_aarg = self.aarg_in.get_text()
			self.f_carg = self.carg_in.get_text()
			
			self.filter = (self.f_name, head_to_match, self.f_cond, 
				                self.f_carg, self.f_actn, self.f_aarg)
			self.filter_list_box.filters[self.filter_index] = self.filter
			self.filter_list_box.update_filter_list()

		def _save_filter_close(_button, self=self, _save_filter=_save_filter):
			_save_filter(_button)
			# remove this window from the list
			self.filter_list_box.edit_windows.remove(self)
			self.win.destroy()
	
		def _cancel(_button, self=self):
			self.win.destroy()
	
		# buttons horizontal in yet another box...
		button_box = gtk.HBox(spacing=5)
		box.pack_end(button_box, expand=False)
		button_box.show()

		button = gtk.Button(stock="gtk-cancel")
		button.connect("clicked", _cancel)
		button_box.pack_end(button, expand=False)
		button.show()

		button = gtk.Button(stock="gtk-apply")
		button.connect("clicked", _save_filter)
		button_box.pack_end(button, expand=False)
		button.show()
		self.apply_button = button

		button = gtk.Button(stock="gtk-ok")
		button.connect("clicked", _save_filter_close)
		button_box.pack_end(button, expand=False)
		button.show()
		self.ok_button = button

		box.pack_start(box4)
		box4.show()
		box4.pack_start(box4_1, expand=False)
		box4_1.show()

		# Stuff the Gtk thingies with filter junk
		self.name_in.set_text(self.f_name)
		self.header_in.set_text(self.filter[1])
		self.carg_in.set_text(self.f_carg)
		
		self.update_state()

		self.win.show()

class filters_editlist(gtk.VBox):
	"""
	A gtk.Box full of shit for editing a mailbox.filters
	"""
	def update_filter_list(self):
		self.filter_list.freeze()
		self.filter_list.clear()
		for x in self.filters:
			self.filter_list.append( [ x[0] ] )
		self.filter_list.thaw()

	def move_filter(self, old_pos, new_pos):
		# Correct filter index of edit windows
		for win in self.edit_windows:
			if win.filter_index == old_pos:
				win.filter_index = new_pos
			else:
				if win.filter_index >= new_pos:
					win.filter_index = win.filter_index + 1
				elif win.filter_index > old_pos and win.filter_index <= new_pos:
					win.filter_index = win.filter_index - 1
		
		filter = self.filters[old_pos]
		del self.filters[old_pos]
		self.filters.insert(new_pos, filter)
		self.update_filter_list()
	
	def apply_changes(self):
		self.box.filters = self.filters

	def __init__(self, mailbox, user):
		gtk.VBox.__init__(self, spacing=5)

		self.box = mailbox

		self.set_border_width(5)
		self.user = user

		import copy
		self.filters = copy.deepcopy(mailbox.filters)
		self.cur_edit = None	# currently editing which filter?

		# Windows open editing filters
		self.edit_windows = []

		# Last item selected by the user
		self.user_selected = []
		
		# Widgets to keep disabled until a filter is selected
		self.disabled_until_selection = []

		# buttons horizontal in yet another box...
		button_box = gtk.HBox(spacing=5)
		self.pack_end(button_box, expand=False)
		button_box.show()
		
		def new_filter(button, self=self):
			self.filters.append(("New filter","to",CONDITION_EQUAL,"",ACTION_MOVE,"deleted"))
			self.update_filter_list()
			self.edit_windows.append(filter_editbox(self.user, self, len(self.filters)-1))
		
		button = gtk.Button(stock="gtk-new")
		button.connect("clicked", new_filter)
		button_box.pack_end(button, expand=False)
		button.show()

		def copy_filter(button, self=self):
			x = self.filter_list.selection[0]
			self.filters.append(self.filters[x])
			self.update_filter_list()
		
		button = gtk.Button(stock="gtk-copy")
		button.connect("clicked", copy_filter)
		button_box.pack_end(button, expand=False)
		# disabled until you click on an item
		button.set_sensitive(False)
		button.show()
		self.disabled_until_selection.append(button)

		def edit_filter(button, self=self):
			i = self.filter_list.selection[0]
			self.edit_windows.append(filter_editbox(self.user, self, i))

		button = gtk.Button(" "+_("Edit")+" ")
		button.connect("clicked", edit_filter)
		button_box.pack_end(button, expand=False)
		# disabled until you click on an item
		button.set_sensitive(False)
		button.show()
		self.disabled_until_selection.append(button)
			
		def delete_filter(button, self=self):
			index = self.filter_list.selection[0]
			# delete edit windows that are open and editing this filter
			for win in self.edit_windows:
				if win.filter_index == index:
					win.win.destroy()
					del win
				elif win.filter_index > index:
					win.filter_index = win.filter_index - 1
			del self.filters[index]
			self.update_filter_list()
			
		button = gtk.Button(stock="gtk-delete")
		button.connect("clicked", delete_filter)
		button_box.pack_end(button, expand=False)
		# disabled until you click on an item
		button.set_sensitive(False)
		button.show()
		self.disabled_until_selection.append(button)

		def _clicked_filter(_button, event, self=self):
			# What did we select?
			selected = self.filter_list.get_selection_info(int(event.x), int(event.y))
			if selected:
				# Enable some buttons
				for widget in self.disabled_until_selection:
					widget.set_sensitive(True)
			else:
				for widget in self.disabled_until_selection:
					widget.set_sensitive(False)
				return
			index = selected[0]
			self.user_selected = [index]
			# Open edit window on double-click
			if event.type == gtk.gdk._2BUTTON_PRESS:
				# Open filter edit box
				self.edit_windows.append(filter_editbox(self.user, self, index))

		def _dnd_drag_data_get(w, context, selection_data, info, time, self=self):
			#
			selected = str(self.user_selected[0])
			selection_data.set(selection_data.target, 8, selected)
		
		def _dnd_drag_motion(w, context, x, y, time, self=self):
			# get target (pyne_filter)
			content_type = str(context.targets[0])
			y = y-20 # see mainwin for similar hack...
			selected = self.filter_list.get_selection_info(x,y)
			if selected:
				row = selected[0]
				self.filter_list.select_row(row, 0)

		def _dnd_drag_data_received(w, context, x, y, data, info, time, self=self):
			# get target
			content_type = str(context.targets[0])
			old_pos = int(data.data)
			if not self.filter_list.selection:
				return
			new_pos = self.filter_list.selection[0]
			# Move filter from old_pos to new_pos
			self.move_filter(old_pos, new_pos)

		targets = [ ("pyne_filter", 0, -1) ]
		# Box4_2: List of rules
		self.filter_list = gtk.CList(1, [ _("Filters (highest evaluated first)") ])
		self.filter_list.connect("button_press_event", _clicked_filter)
		self.filter_list.connect("drag_data_received", _dnd_drag_data_received)
		self.filter_list.drag_dest_set(gtk.DEST_DEFAULT_ALL, targets, gtk.gdk.ACTION_MOVE)
		self.filter_list.connect("drag_motion", _dnd_drag_motion)
		self.filter_list.connect("drag_data_get", _dnd_drag_data_get)
		self.filter_list.drag_source_set(gtk.gdk.BUTTON1_MASK|gtk.gdk.BUTTON3_MASK, targets, gtk.gdk.ACTION_MOVE)
		swin = gtk.ScrolledWindow()
		swin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
		swin.show()
		swin.add(self.filter_list)
		self.filter_list.show()
		self.pack_start(swin)
		swin.show()

		# prepare for input
		self.update_filter_list()

class policy_editbox(gtk.VBox):
	def __init__(self, box):
		self.box = box
		self.collect_policy = list(box.collect_policy)

		self.collect_policy_entry = gtk.Entry()
		self.collect_policy_entry.set_text(str(self.collect_policy[1]))

		# Server policy thing
		gtk.VBox.__init__(self, spacing=5)
		self.set_border_width(5)

		self.policy_combo = gtk.combo_box_new_text ()
		for i in server_policies:
			self.policy_combo.append_text (i)
		self.policy_combo.set_active ( self.collect_policy [0] )
		self.policy_combo.connect ("changed", self.ch_server_policy)
		self.policy_combo.show ()
		
		hbox = gtk.HBox(spacing=5)
		self.pack_start(hbox, expand=False)
		
		hbox.pack_end(self.collect_policy_entry)
		self.collect_policy_entry.show()
		self.update_entry()
		
		label = gtk.Label(_("After collection:"))
		hbox.pack_start(label, expand=False)
		hbox.pack_start(self.policy_combo, expand=False)
		label.show()
		hbox.show()
		self.policy_combo.show()
		
	def apply_changes(self):
		self.collect_policy[1] = self.collect_policy_entry.get_text()
		
		# Should be integer days if SERVER_EXPIRE
		if self.collect_policy[0] == SERVER_EXPIRE:
			try:
				self.collect_policy[1] = int(self.collect_policy[1])
			except ValueError:
				# default to bix expire_after on failure to parse
				self.collect_policy[1] = self.box.expire_after
		self.box.collect_policy = tuple(self.collect_policy)

	def update_entry(self):
		if self.collect_policy[0] == SERVER_EXPIRE:
			self.collect_policy_entry.show()
		else:
			self.collect_policy_entry.hide()

	def ch_server_policy(self, item):
		self.collect_policy[0] = self.policy_combo.get_active ()
		self.update_entry()
			


syntax highlighted by Code2HTML, v. 0.9.1