#!/usr/bin/env python

#
# Pyne - Python Newsreader and Emailer
#
# Copyright (c) 2000-2002 Tom Morton
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#        
# Tom Morton <tom@moretom.net>
#

import utils
import gtk
import gobject
import cPickle
import time
import os
import os.path
import sys
import string
import glob
import webbrowser
from copy import copy

# remember location of pyne modules
pyne_path = os.path.split( os.path.abspath(sys.argv[0]) )[0]

from addressbook import *
import pynei18n
import boxtypes
import boxtypes.superbox
import boxtypes.loader
import mainwin
from pyneheaders import *
import pynemsg
import personality
import userconfig
import ptk.misc_widgets

class pyne_user:
	"""
	Each user of pyne will have one of these objects.
	They may contain mailboxes, outboxes, NNTPboxes, etc.
	"""
	def save(self, save_all=0):
		"""
		save_all=1 if you want it to recursively save all folders.
		"""	
		tosave = {}
		
		# Folder tree structure
		ftree = []
		
		def _add_node(folder, kid_list):
			this = (folder.uid, [])
			kid_list.append(this)

			if save_all:
				folder.save(self)
				folder.shutdown(self)

			if not folder.__dict__.has_key("contents"):
				return
			for fol in folder.contents:
				_add_node(fol, this[1])
		for f in self.contents:
			_add_node(f, ftree)

		tosave["foldertree"] = ftree
		
		def _saveme(key, tosave=tosave, self=self):
			tosave[key] = self.__dict__[key]
		
		# Stuff we want to save
		_saveme("personalities")
		_saveme("bodyfont")
		_saveme("linewrap")
		_saveme("replyhead")
		_saveme("addressbook")
		_saveme("col_text")
		_saveme("col_quote")
		_saveme("col_header")
		_saveme("col_mnormal")
		_saveme("col_mnobody")
		_saveme("col_mreplied")
		_saveme("col_mmarked")
		_saveme("sort_type")
		_saveme("opts")
		_saveme("printer")
		_saveme("last_expiry")
		_saveme("tab_pos")
		_saveme("ui_style")
		_saveme("default_dir")
		_saveme("html_parser")
		_saveme("edit_cmd")
		_saveme("mime_types")
		_saveme("window_setup")

		f = open("newuser.dat", "w")

		cPickle.dump(ver_stamp, f, 1)
		cPickle.dump("USER", f, 1)
		cPickle.dump(tosave, f, 1)

		f.close()

		try:
			# If old user.dat and .bak exist then remove them
			os.remove("user.dat")
			os.remove("user.bak")
		except OSError:
			pass

	def load(self):
		self.__dict__ = {}
		version = ver_stamp

		try:
			f = open("newuser.dat", "r")
		except IOError:
			# User does not exist
			pass
		else:
			version = cPickle.load(f)
			type = cPickle.load(f)
			self.__dict__ = cPickle.load(f)
			f.close()
		
		def _ifmissing(key, value, self=self):
			if not self.__dict__.has_key(key):
				self.__dict__[key] = value

		# User personalities
		_ifmissing("personalities", {})

		# Font used for message body text
		_ifmissing("bodyfont", "")
		_ifmissing("linewrap", 72)
		_ifmissing("replyhead", "On $DATE, $FROM wrote:")

		# Address book
		_ifmissing("addressbook", [])

		# Texty colours
		_ifmissing("col_text", '#000000')
		_ifmissing("col_quote", '#0000cc')
		_ifmissing("col_header", '#cc0000')

		# Message view colours
		_ifmissing("col_mnormal", '#000000')
		_ifmissing("col_mnobody", '#7f7f7f')
		_ifmissing("col_mmarked", '#cc0000')
		_ifmissing("col_mreplied", '#0000cc')

		# default: sort messages by date, newest to top (2==date)
		_ifmissing("sort_type", (2, 0))

		# user interface style
		_ifmissing("ui_style", UI_DEFAULT)

		# various boolean options. see OPT_xx at top
		_ifmissing("opts", 0)

		# printer command
		_ifmissing("printer", 'lpr')

		# When we last expired stuff
		_ifmissing("last_expiry", time.localtime(time.time())[:3])

		# position of tabs in quickview and composer
		_ifmissing("tab_pos", int (gtk.POS_TOP))

		# Default (attachment load/save, etc) directory
		_ifmissing("default_dir", "~")
		# parse html bodies with this:
		_ifmissing("html_parser", "lynx -dump")
		# alternative editor command
		_ifmissing("edit_cmd", "xterm -e vim")

		# Attachment handlers
		_ifmissing("mime_types", [ ("image/*", "", 1),
					("text/plain", "", 1),
					("text/html", "mozilla", 0) ] )
		# Window size info (width, height, hpane position, vpane position)
		_ifmissing("window_setup", [])
		
		###### Temporary stuff
		self.contents = []
		# List of open windows
		self.windows = {}
		# List of to-be-deleted temporary files
		self.tempfiles = []

		if self.__dict__.has_key("foldertree"):
			ftree = self.foldertree

			def _recurse_load_folders(conts, folder_node):
				folder = boxtypes.loader.loader(folder_node[0], self)
				conts.append(folder)
				if len(folder_node[1]) > 0:
					# It has children. Load them too
					for fnode in folder_node[1]:
						_recurse_load_folders(folder.contents, fnode)
			
			for fnode in ftree:
				_recurse_load_folders(self.contents, fnode)
			del self.foldertree
		else:
			# New user. Give him some cute starting folders to play with :o)
			# 'special' uids for non-deletable special folders (outbox,
			a = boxtypes.outbox.outbox(self, "outbox")
			self.contents.append(a)

			a = boxtypes.storebox.storebox(self, "drafts")
			a.name = _("Drafts")
			self.contents.append(a)

			a = boxtypes.storebox.storebox(self, "sent")
			a.name = _("Sent")
			self.contents.append(a)
		
			a = boxtypes.storebox.storebox(self, "saved")
			a.name = _("Saved")
			self.contents.append(a)
		
			a = boxtypes.storebox.storebox(self, "deleted")
			a.name = _("Deleted")
			self.contents.append(a)
	
			# For safety...
			self.save(save_all=1)

	def recover(self, path):
		"""
		Not appropriate to pyne mailboxes...
		"""
		pass

		box.save ()
	def parent_of(self, folder):
		"""
		Return the parent object of 'folder'.
		"""
		uid = folder.uid
		def find_parent(folder, uid=uid):
			if folder.__dict__.has_key("contents"):
				for x in folder.contents:
					if x.uid == uid:
						return folder
		return utils.recurse_apply( [self], find_parent)[0]

	def get_folder_by_uid(self, uid):
		"""
		Return folder in user.contents with uid==uid :-)
		"""
		def get_uid_matches(folder, uid=uid):
			if folder.uid == uid:
				return folder
		folders = utils.recurse_apply(self.contents, get_uid_matches)
		if len(folders) == 0:
			return None
		else:
			return folders[0]

	def set_preferences(self, parent_win):
		"""
		Allow the user to set his preferences like fonts, etc.
		"""
		userconfig.UserConfigWin(self, parent_win)


	def update(self, update_type=0):
		"""
		Update all windows' folder lists.

		By default update changed stuff only.
		"""
		for x in self.windows.keys():
			self.windows[x].update(self, update_type)
		# removed changed markers
		def remove_changed(folder):
			if folder.__dict__.has_key("changed"):
				del folder.changed
		# from 'utils.py'
		utils.recurse_apply(self.contents, remove_changed)


	def expire(self):
		"""
		Expire messages collected longer ago than self.expire_after
		days.
		"""
		def expire_msgs(expire_msgs, folder, this_day, expire_after):
			"""
			Recursively remove links to msg_id in object.
			"""
			# if the object contains messages, remove msg_ids from
			# them
			if folder.__dict__.has_key("messages"):
				messages = copy(folder.messages)
				for x in messages:
					msg = folder.load_header(x)
					# fucked up headers
					try:
						len(msg)
					except TypeError, e:
						print "*",
						try:
							folder.delete_article(x)
						except ValueError, e:
							pass
						continue
					# test age
					date_received = msg[HEAD_DATE_RECEIVED]
					day = int(date_received/86400.0)
					old = this_day - day
					if old >= expire_after:
						# unread messages: fix num_unread
						if folder.__dict__.has_key ("num_unread") and \
						   not (msg[HEAD_OPTS] & MSG_ISREAD):
							folder.num_unread -= 1
						# delete it
						try:
							folder.delete_article(x)
						except ValueError, e:
							pass

		############################################################
		this_day = int(time.time()/86400.0)

		# only test for expiry in folders with expire_after
		def _pre_expire(folder, expire_msgs=expire_msgs, this_day=this_day):
			if not folder.__dict__.has_key("expire_after"):
				return
			if folder.expire_after == None:
				return
			print "Pyne: Expiring folder ", folder.name
			# recurse into folder looking for messages that have
			# expired.
			expire_msgs(expire_msgs, folder, this_day, folder.expire_after)
		utils.recurse_apply(self.contents, _pre_expire)
					
	def kill_window(self, num):
		# First save its pane positions
		if self.windows[num].__dict__.has_key("vpaned"):
			size = list(self.window_setup[num])
			if not self.windows[num].__dict__.has_key("notebook"):
				# No panes if in tabbed mode
				size[VPANE_POS] = self.windows[num].vpaned.get_position()
				size[HPANE_POS] = self.windows[num].hpaned.get_position()
			self.window_setup[num] = tuple(size)
		# Destroy it
		self.windows[num].destroy()
		del self.windows[num]
		# All windows closed: quit
		if len(self.windows) == 0:
			gtk.main_quit()
		return

	def new_window(self, display_msg = None):
		"""
		Open new window, with maximised quickview pane and showing
		message (folder, msg-id) 'display_msg' if it is != None.
		"""
		# Find unused windows number
		x = 0
		while self.windows.has_key(x):
			x = x + 1
		if len(self.window_setup) <= x:
			self.window_setup.append( (600, 400, 200, 200) )
		win = mainwin.pyne_window(self, ver_string, self.window_setup[x], x, display_msg = display_msg)
		win.update(self, mainwin.UPDATE_ALL)
		self.windows[x] = win

	def get_personality_uid(self):
		i = 0
		ids = self.personalities.keys()
		while 1:
			if not str(i) in ids:
				return str(i)
			i = i + 1
	
	def get_personality(self, uid):
		return self.personalities[uid]

	def get_uid(self, prefix):
		"""
		Return unique id for new object.
		"""
		# build list of current ids
		def get_uids(folder):
			return folder.uid
		uids = utils.recurse_apply(self.contents, get_uids)
		# return an unused id
		uid = 0
		while 1:
			if not prefix+str(uid) in uids:
				return prefix+str(uid)
			uid = uid + 1
	
	def queue_action(self, act_flag):
		self.act_flags = self.act_flags | act_flag
	
	def register_child (self, pid):
		"""
		Stick child pids in here after forking and the timeout
		function will poll them for exits so we are zombieless
		"""
		self.child_pids.append (pid)
	
	def timeout_func(self):
		# Update message/folder views.
		if self.act_flags & ACT_UPDATE:
			self.act_flags = self.act_flags & ~ACT_UPDATE
			self.update()
	
		for f in self.timeout_funcs:
			f (self)

		for i in xrange (len(self.child_pids)-1, -1, -1):
			# waitpid returns (pid, status)
			if self.child_pids[i] == os.waitpid (self.child_pids[i], os.WNOHANG)[0]:
				del self.child_pids[i]
				i = i+1
			
		
		return True

	def timeout_add (self, some_function):
		"""
		Functions should take 1 argument, user.
		"""
		if not (some_function in self.timeout_funcs):
			self.timeout_funcs.append (some_function)

	def timeout_remove (self, some_function):
		if some_function in self.timeout_funcs:
			self.timeout_funcs.remove (some_function)
	
	def open_url (self, url):
		"""
		I love bacon :o)
		"""
		cmd = None
		for mimehandler in self.mime_types:
			if mimehandler[0] == "text/html":
				# pyne internal (which means webbrowser interface)
				if mimehandler[2] == 1:
					break
				cmd = mimehandler[1]
				break
		if cmd:
			pid = os.fork ()
			if pid == 0:
				try:
					os.execvp(cmd, (cmd, url))
				except OSError:
					os._exit(0)
			else:
				self.register_child (pid)
		else:
			pid = os.fork ()
			if pid == 0:
				webbrowser.open(url, 1)
				os._exit(0)
			else:
				self.register_child (pid)
	
	def start(self):
		"""
		Just get on with it...
		"""
		# Key format "%d%d%d" % (cached, isread, isreplied, ismarked)
		self.msg_icons = {}
		self.msg_cols = {}
		
		self.act_flags = 0
		self.child_pids = []

		# just to shut it up with all the damn warnings we make this...
		# shame on me :-(
		i = gtk.Image()
		i.set_from_file(os.path.join (pyne_path,"icons","msg_read.xpm"))
		self.msg_icons["1100"] = i
		self.msg_cols["1100"] = "col_mnormal"
		i = gtk.Image()
		i.set_from_file(os.path.join (pyne_path,"icons","msg_unread.xpm"))
		self.msg_icons["1000"] = i
		self.msg_cols["1000"] = "col_mnormal"
		i = gtk.Image()
		i.set_from_file(os.path.join (pyne_path,"icons","msg_read_replied.xpm"))
		self.msg_icons["1110"] = i
		self.msg_cols["1110"] = "col_mreplied"
		i = gtk.Image()
		i.set_from_file(os.path.join (pyne_path,"icons","msg_uncached.xpm"))
		self.msg_icons["0000"] = i
		self.msg_cols["0000"] = "col_mnobody"
		i = gtk.Image()
		i.set_from_file(os.path.join (pyne_path,"icons","msg_unread_replied.xpm"))
		self.msg_icons["1010"] = i
		self.msg_cols["1010"] = "col_mreplied"
		i = gtk.Image()
		i.set_from_file(os.path.join (pyne_path,"icons","msg_read_marked.xpm"))
		self.msg_icons["1101"] = i
		self.msg_cols["1101"] = "col_mmarked"
		i = gtk.Image()
		i.set_from_file(os.path.join (pyne_path,"icons","msg_unread_marked.xpm"))
		self.msg_icons["1001"] = i
		self.msg_cols["1001"] = "col_mmarked"
		i = gtk.Image()
		i.set_from_file(os.path.join (pyne_path,"icons","msg_read_replied_marked.xpm"))
		self.msg_icons["1111"] = i
		self.msg_cols["1111"] = "col_mmarked"
		i = gtk.Image()
		i.set_from_file(os.path.join (pyne_path,"icons","msg_uncached_marked.xpm"))
		self.msg_icons["0001"] = i
		self.msg_cols["0001"] = "col_mmarked"
		i = gtk.Image()
		i.set_from_file(os.path.join (pyne_path,"icons","msg_unread_replied_marked.xpm"))
		self.msg_icons["1011"] = i
		self.msg_cols["1011"] = "col_mmarked"

		# Start up main window
		self.new_window()

		# There are many depraved things we wish to perform
		self.timeout_funcs = []
		gobject.timeout_add(50, self.timeout_func)

		# Input loop
		gtk.main()
	
		# Trash temporary files
		for x in self.tempfiles:
			# delete temporary files
			try:
				os.remove(x)
			except OSError:
				pass
		self.tempfiles = []

		# expire if not done today
		if self.last_expiry != time.localtime(time.time())[:3]:
			self.last_expiry = time.localtime(time.time())[:3]
			self.expire()
	
		# remove stuff we don't want to save
		del self.msg_icons
		del self.act_flags

		# Save datafile
		self.save(save_all=1)

		# End
		print "Pyne exited."

if __name__ == '__main__':
	gtk.threads_init()
	gtk.threads_enter()
	# Help
	if sys.argv[-1] == "--help":
		print "Usage: pyne [option] [user location]"
		print "Pyne is a GTK+ Newsreader/Emailer written in Python."
		print
		print "User location is optional, and defaults to ~/.pyne"
		print
		print "  -f, --force      Force startup after a crash by removing the lockfile"
		print
		print "Report bugs to <tom@moretom.net>"
		sys.exit(0)
	# get alternative location of .pyne:
	for arg in sys.argv[1:]:
		# skip other args
		if arg[0] == "-":
			continue
		user_home = sys.argv[1]
		break
	else:
		user_home = os.path.join(os.environ["HOME"], ".pyne-1.0")
	# Remember this as location of pyne modules
	sys.path.append(pyne_path)
	# Change to the working directory
	try:
		os.chdir(user_home)
	except OSError, e:
		# It's either non-existant, or not a directory (eek!)
		try:
			os.mkdir(user_home)
			os.chdir(user_home)
		except OSError, e:
			print "Error. Cannot create ~/%s/, or file exists with that name. HELP!!! :~{" % user_home
			sys.exit(0)
	# other arguments
	for arg in sys.argv[1:]:
		if arg == "-f" or arg == "--force":
			try:
				os.remove("pyne.lock")
			except OSError, e: pass
	# Check for a lock file
	try:
		f = open("pyne.lock", "r")
	except IOError, e:
		# None. make one.
		f = open("pyne.lock", "w")
		f.close()
	else:
		if ptk.misc_widgets.ReturnDialog ("Warning!", "There is already an instance of Pyne running or a Pyne\nsession terminated abnormally (crashed :-)\nIf so delete the %s/pyne.lock file." % user_home, ((gtk.STOCK_QUIT,0), ("Ignore",1))) == 0:
			sys.exit()
	# Get user.
	user = pyne_user()
	user.load()
	# start main thread: user.start() 
	user.start()

	# remove the lock file
	os.remove("pyne.lock")
	
	gtk.threads_leave()
	


syntax highlighted by Code2HTML, v. 0.9.1