#!/usr/bin/python -OQnew


import sys
import os
import socket
import select
import string
import traceback
import urllib
import time

import msnlib
import msncb


"""
MSN Client

This is a fully usable msn client, which also serves as reference
implementation for msnlib.
For further information refer to the documentation or the source (which is
always preferred).
"""

#
# colors, for nice output
#
class color:
	def __init__(self):
		self.black = 	'\x1b[0;30m'
		self.red =	'\x1b[0;31m'
		self.green =	'\x1b[0;32m'
		self.yellow =	'\x1b[0;33m'
		self.blue =	'\x1b[0;34m'
		self.magenta =	'\x1b[0;35m'
		self.cyan =	'\x1b[0;36m'
		self.white =	'\x1b[0;37m'
		self.normal =	'\x1b[0m'
		self.bold =	'\x1b[1m'
		self.clear =	'\x1b[J'

c = color()


#
# different useful prints
#

def printl(line, color = c.normal, bold = 0):
	"Prints a line with a color"
	out = ''
	if line and line[0] == '\r':
		clear_line()
	if bold:
		out = c.bold
	out = color + out + line + c.normal
	sys.stdout.write(out)
	sys.stdout.flush()

def perror(line):
	"Prints an error"
	out = ''
	out += c.yellow + c.bold + '!' + c.normal
	out += c.red + c.bold + '!' + c.normal
	out += c.blue + c.bold + '!' + c.normal
	out += ' ' + c.green + c.bold + line + c.normal + '\a'
	sys.stdout.write(out)
	sys.stdout.flush()

def pexc(line):
	"Prints an exception"
	out = '\n'
	out += ( c.cyan + c.bold + '!' + c.normal ) * 3
	sys.stdout.write(out)
	sys.stdout.write(c.bold + line)
	sys.stdout.flush()
	traceback.print_exc()
	sys.stdout.write(c.normal)
	sys.stdout.write('\n')
	sys.stdout.flush()

def print_list(md, only_online = None, userlist = None, include_emails = 0):
	"Prints the user list"
	if not userlist:
		userlist = md.users
	ul = userlist.keys()
	ul.sort()
	for email in ul:
		u = userlist[email]
		if u.status != 'FLN': 
			sys.stdout.write(c.bold)
		else:
			if only_online:		# this is not nice, but it's simple enough
				continue
		status = msnlib.reverse_status[u.status]
		print '%7.7s :: %s' % (status, u.nick),
		if include_emails: print '(%s)' % (email),
		print c.normal

def print_user_info(email):
	"Prints the user information, and pending messages"
	u = m.users[email]
	out = c.bold
	out += c.bold + 'User info for ' + email + '\n'
	out += c.bold + 'Nick:\t\t' + c.normal + u.nick + '\n'
	out += c.bold + 'Status:\t\t' + c.normal + msnlib.reverse_status[u.status] + '\n'
	if u.homep: out += c.bold + 'Home phone:\t' + c.normal + u.homep + '\n'
	if u.workp: out += c.bold + 'Work phone:\t' + c.normal + u.workp + '\n'
	if u.mobilep: out += c.bold + 'Mobile phone:\t' + c.normal + u.mobilep + '\n'
	if u.priv.has_key('typing') and u.priv['typing']:
		out += c.bold + 'Last typing at:\t' + c.normal
		out += time.asctime(time.localtime(u.priv['typing'])) + '\n'
	if u.sbd: 
		out += c.bold + 'Switchboard:\t' + c.normal + str(u.sbd) + '\n'
		if u.sbd.msgqueue:
			out += c.bold + 'Pending messages:' + '\n'
			for msg in u.sbd.msgqueue:
				out += c.bold + '\t>>> ' + c.normal + msg + '\n'
	printl(out)
	
def print_prompt():
	"Prints the user prompt"
	sys.stdout.write('\r' + c.red + c.bold + '[msn] ' + c.normal)
	sys.stdout.flush()

def print_inc_msg(email, lines, eoh = 0, quiet = 0, ptime = 1, recvtime = 0):
	"""Prints an incoming message from a list of lines and an optional
	end-of-header pointer.  You can also pass the original received time as
	a parameter, this is used for history printed."""
	nick = email2nick(email)
	if not nick: nick = email
	if ptime:
		if recvtime:
			ctime = time.strftime('%I:%M:%S%p', time.localtime(recvtime))
		else:
			ctime = time.strftime('%I:%M:%S%p')
		printl('%s ' % ctime, c.blue)
	printl('%s' % nick, c.cyan, 1)
	if len(lines[eoh:]) == 1:
		printl(' <<< %s\n' % lines[eoh], c.yellow, 1)
	else:
		printl(' <<< \n\t', c.yellow, 1)
		msg = string.join(lines[eoh:], '\n\t')
		msg = msg.replace('\r', '')
		printl(msg + '\n', c.bold)
	beep(quiet)

def print_out_msg(nick, msg):
	"Prints an outgoing message"
	ctime = time.strftime('%I:%M:%S%p')
	printl('%s ' % ctime, c.blue)
	printl('%s' % nick, c.cyan, 1)
	printl(' >>> ', c.yellow, 1)
	printl('%s' % msg)


def beep(q = 0):
	"Beeps unless it's told to be quiet"
	if not q:
		printl('\a')
	
	
#
# useful functions
#

def quit(code = 0):
	"Exits"
	printl('Closing\n', c.green, 1)
	try:
		m.disconnect()
		global oldtermattr
		termios.tcsetattr(sys.stdin.fileno(), termios.TCSAFLUSH, oldtermattr)
	except:
		pass
	redraw_cli()
	sys.exit(code)

def nick2email(nick):
	"Returns an email according to the given nick, or None if noone	matches"
	for email in m.users.keys():
		if m.users[email].nick == nick:
			return email
	if nick in m.users.keys():
		return nick
	return None

def email2nick(email):
	"Returns a nick accoriding to the given email, or None if noone matches"
	if email in m.users.keys():
		return m.users[email].nick
	else:
		return None

def get_config(file):
	"Parses a simple config file, and returns a var:value dict"
	try:
		fd = open(file)
	except:
		return None
	lines = fd.readlines()
	config = {}
	for i in lines:
		i = i.strip()
		if i.find('=') < 0:
			continue
		if i[0] == '#':
			continue
		var, value = i.split('=', 1)
		var = var.strip()
		value = value.strip()
		config[var] = value
	return config

def null(s):
	"Null function, useful to void debug ones"
	pass
		
def log_msg(email, type, msg, mtime = 0):
	file = config['history directory'] + '/' + email
	if not mtime:
		mtime = time.time()
	out = ''
	out += time.strftime('%d/%b/%Y %H:%M:%S ', time.localtime(mtime))
	out += email + ' '
	if type == 'in':
		out += '<<< '
		msg = msg.replace('\r', '')
		lines = msg.split('\n')
		if len(lines) == 1:
			 out += msg + '\n'
		else:
			out += '\n\t'
			out += string.join(lines[:], '\n\t')
			out += '\n'
	elif type == 'out':
		out += '>>> ' + msg + '\n'
	elif type == 'status':
		out += '*** ' + msg + '\n'
	
	fd = open(file, 'a')
	fd.write(out)
	fd.close()
	del(fd)
	return 1



#
# terminal handling
#

# all this is _ugly_, a real mess; luckily it's pretty much self contained.
# if you're trying to follow the code, i highly recommend you to skip this
# section; you really don't need to know it, just think of redraw_cli() pretty
# much as print_prompt(), stdin_read() as sys.stdin.readline(), and
# clear_line() as printf('\r'). actually that's quite near true when we don't
# use termios.
# it has been written in a way that if termios is not available, we fall back
# to the normal and old behaviour which is guaranteed to work.

try:
	# all of this disables line-buffering on the terminal (thus allowing
	# char-by-char reads) and echoing (so we output whatever we want); and
	# finally sets the file nonblocking so we can read all that's
	# available without complications.
	# you should read termios and fcntl manpages to find out the details
	import termios
	stdinfd = sys.stdin.fileno()
	oldtermattr = termios.tcgetattr(stdinfd)
	newtermattr = termios.tcgetattr(stdinfd)
	newtermattr[3] = newtermattr[3] & ~termios.ICANON & ~termios.ECHO
	termios.tcsetattr(stdinfd, termios.TCSANOW, newtermattr)
	import fcntl, os
	fcntl.fcntl(stdinfd, fcntl.F_SETFL, os.O_NONBLOCK)
	del(newtermattr)
	use_termios = 1
except:
	use_termios = 0

# now we try to find out the console size; if we fail we fall back to
# the good old 80x24.
# note that the (' ' * 10) is just awful, but there is no sane way of
# doing this without using a C module. it's based on 'struct winsize',
# but as we only use the first 4 bytes, we don't ask for more; then we
# unpack the two shorts into (lenght, width)
try:
	import struct
	winsize = fcntl.ioctl(stdinfd, termios.TIOCGWINSZ, ' ' * 10)
	winsize = struct.unpack('hh', winsize[:4])
except:
	winsize = (24, 80)
screenwidth = winsize[1]

# input buffer, where all the characters written by the user are stored in
inbuf = ''

# input history buffer, to store previous commands. 
# we use a list [buffer, pointer] to avoid namespace pollution
inbuf_history = [[], -1]

def stdin_read():
	"""Reads from stdin, and acts in consecuense. If you don't use
	termios, it's almost the same as calling readline(); but otherwise it
	handles all the input reading."""
	global inbuf
	if not use_termios:
		inbuf = sys.stdin.readline()
		tmpbuf = inbuf
		inbuf = ''
		out = parse_cmd(tmpbuf)
		printl(out + '\n', c.green, 1)
		redraw_cli()
		return
	
	in_esc = 0
	input = sys.stdin.read()
	for char in input:
		inbuf = inbuf + char
		if char == '\n':
			# command history
			if len(inbuf_history[0]) > config['input history size']:
				del(inbuf_history[0][0])
			inbuf_history[0].append(inbuf[:-1])
			inbuf_history[1] = len(inbuf_history[0]) - 1 # moves the pointer
			
			sys.stdout.write(char)
			tmpbuf = inbuf
			inbuf = ''
			out = parse_cmd(tmpbuf)
			printl(out + '\n', c.green, 1)
			redraw_cli()
		elif char == '\b' or ord(char) == 127:		# ^H / DEL
			inbuf = inbuf[:-2]
			redraw_cli()
		elif char == '\t':				# tab
			if len(inbuf) == 1 and email2nick(last_sent):
				inbuf = 'm ' + email2nick(last_sent) + ' '
			elif len(inbuf) == 1 and email2nick(last_received):
				inbuf = 'm ' + email2nick(last_received) + ' '
			else:
				inbuf = inbuf[:-1]
				beep()
			redraw_cli()
		elif ord(char) == 4:				# EOT
			sys.stdout.write('\n')
			out = parse_cmd('')
			printl(out + '\n', c.green, 1)
		elif ord(char) == 27:				# ESC
			# we use in_esc for escape secuenses (composed of
			# ESC + '[' + LETTER). 1 means got ESC, 2 means got
			# '['. Here we set to 1, and the rest are in the
			# generic handling
			in_esc = 1
			inbuf = inbuf[:-1]
		elif ord(char) < 32:				# unhandled control
			print 'Got weird char: %d' % ord(char)
			redraw_cli_cond(char)
		else:						# normal
			if not in_esc:
				redraw_cli_cond(char)
				continue

			# comes from a escape code
			elif in_esc == 1:
				if char == '[':
					in_esc = 2
				else:
					in_esc = 0
			elif in_esc == 2:
				if char == 'A':			# up
					if inbuf_history[1] == -1:
						# hit the top, or it's empty
						pass
					else:
						clear_line()
						pos = inbuf_history[1]
						inbuf = inbuf_history[0][pos]
						inbuf_history[1] -= 1
						redraw_cli()
				elif char == 'B':		# down
					if not inbuf_history[0]:
						# empty
						pass
					elif inbuf_history[1] == len(inbuf_history[0]) - 1:
						# hit the bottom, clear the buffer
						clear_line()
						inbuf = ''
						redraw_cli()
					else:
						inbuf_history[1] += 1
						clear_line()
						pos = inbuf_history[1]
						inbuf = inbuf_history[0][pos]
						redraw_cli()
				in_esc = 0
			inbuf = inbuf[:-1]

def redraw_cli():
	"""Redraws the current prompt line, including user input; it first
	clears the line, either automatically or up to 'lenght' chars."""
	global inbuf, screenwidth
	clear_line()
	print_prompt()
	lenght = screenwidth - 7	# we subsctract the prompt lenght + 1
	sys.stdout.write(inbuf[-lenght:])
	sys.stdout.flush()

def redraw_cli_cond(char):
	"""Same as redraw_cli, but conditional over the lenght of stdin. That
	means that if inbuf is getting too big, we redraw; otherwise we just
	write the character. It's used mostly to avoid innecesary redraw
	overhead (it avoids 90% of cases)."""
	global inbuf, screenwidth
	if len(inbuf) >= (screenwidth - 7):
		redraw_cli()
	else:
		sys.stdout.write(char)
		sys.stdout.flush()

def clear_line():
	"""Clears the current line by overwriting it with spaces."""
	global inbuf, screenwidth
	if use_termios:
		sys.stdout.write('\r' + (screenwidth - 1) * ' ' + '\r')



#
# stdin command parser
#

def parse_cmd(cmd):
	"""Parses the commands introduced by the user. It's pretty long and
	boring, as expected."""
	
	global c, last_sent, last_received	# ugly but necesary
	
	if len(cmd) == 0:
		quit()
	elif len(cmd) == 1:
		return ''
			
	# cut trailing newline and clean up
	if cmd[-1] == '\n':
		cmd = cmd[:-1]
	cmd = cmd.lstrip()
	cmd = cmd.split()
	if len(cmd) > 1:
		cmd, params = cmd[0], string.join(cmd[1:])
	else:
		if not cmd: return ''
		cmd = cmd[0]
		params = ''
	
	# parse
	if   cmd == 'status': 		# change status
		if not params:
			return 'Your current status is %s' % msnlib.reverse_status[m.status]
		if not m.change_status(params):
			return 'Status must be one of: online, away, busy, brb, phone, lunch, invisible, idle or offline'
		return 'Status changed to: %s' % params
		
	elif cmd == 'q':		# quit
		quit()
	
	elif cmd == 'reload':		# reload callbacks
		reload(msncb)
		m.cb = msncb.cb()
	
	elif cmd == 'w':		# list
		print_list(m)
	
	elif cmd == 'ww':		# list, include emails
		print_list(m, include_emails = 1)
	
	elif cmd == 'wr':		# reverse list
		print_list(m, userlist = m.reverse, include_emails = 1)
	
	elif cmd == 'e':		# list (online only)
		print_list(m, only_online = 1)

	elif cmd == 'ee':
		print_list(m, only_online = 1, include_emails = 1)

	elif cmd == 'raw':		# send a raw message
		try:
			c = params[0:3]
			p = params[4:]
		except:
			return 'Error parsing cmd'
		m._send(c, p)
	
	elif cmd == 'debug':		# enable/disable debugging
		p = params.split()
		if len(p) != 1:
			return 'Error parsing cmd'
		if p[0] == 'off':
			msnlib.debug = null
			msncb.debug = null
			return 'Debugging disabled'
		elif p[0] == 'on':
			reload(msnlib)
			reload(msncb)
			return 'Debugging enabled'
		else:
			return 'Unknown parameter - must be "on" or "off"'
	
	elif cmd == 'config':		# show config variables
		keys = config.keys()
		keys.sort()
		for var in keys:
			value = str(config[var])
			if var == 'password':
				value = '<not displayed>'
			printl(c.bold + var + ' = ' + c.normal + value + '\n')
	
	elif cmd == 'close':		# close a connection
		p = params.split()
		if len(p) != 1: 
			return 'Error parsing cmd'
		email = nick2email(p[0])
		if not email: 
			return 'Unknown nick (%s)' % p[0]
		if not m.users[email].sbd:
			return 'No socket opened for %s' % p[0]
		desc = str(m.users[email].sbd)
		m.close(m.users[email].sbd)
		return 'Closed socket %s' % desc
	
	elif cmd == 'privacy':		# set privacy mode
		p = params.split()
		if len(p) != 2: 
			return 'Error parsing cmd'
		try:
			public = int(p[0])
			auth = int(p[1])
			if public not in (0, 1) or auth not in (0, 1):
				return 'Error: both parameters must be 1 or 0'
		except:
			return 'Error: both parameters must be 1 or 0'
		m.privacy(public, auth)
	
	elif cmd == 'add':		# add a user
		p = params.split()
		if   len(p) == 0: 
			return 'Error parsing cmd'
		elif len(p) == 1: 
			email = nick = p[0]
		else: 
			email = p[0]
			nick = p[1]
		m.useradd(email, nick)
	
	elif cmd == 'del':		# delete a user
		p = params.split()
		if len(p) != 1: return 'Error parsing cmd'
		email = nick2email(p[0])
		if not email:
			return 'Unknown nick (%s)' % p[0]
		m.userdel(email)
	
	elif cmd == 'nick':		# change our nick
		p = params.split()
		if len(p) != 1: return 'Error parsing cmd'
		nick = p[0]
		m.change_nick(nick)
	
	elif cmd == 'info':		# user info
		p = params.split()
		if len(p) != 1:
			out = ''
			out += c.bold + 'Info for ' + m.email + '\n'
			out += c.bold + 'Nick:\t\t' + c.normal + m.nick + '\n'
			out += c.bold + 'Status:\t\t' + c.normal + msnlib.reverse_status[m.status] + '\n'
			out += c.bold + 'Home phone:\t' + c.normal + str(m.homep) + '\n'
			out += c.bold + 'Work phone:\t' + c.normal + str(m.workp) + '\n'
			out += c.bold + 'Mobile phone:\t' + c.normal + str(m.mobilep) + '\n'
			out += c.bold + 'Users in contact list: ' + str(len(m.users)) + '\n'
			out += c.bold + 'Users in reverse list: ' + str(len(m.reverse)) + '\n'
			out += c.bold + 'Notification server: ' + c.normal + str(m) + '\n'
			if m.sb_fds:
				out += c.bold + 'Switchboard connections:\n'
				for i in m.sb_fds:
					out += c.bold + '\tSB: ' + c.normal + str(i) + '\n'
			printl(out)
		else:
			email = nick2email(p[0])
			if not email:
				return 'Unknown nick (%s)' % str(p[0])
			print_user_info(email)
	
	elif cmd == 'sync':		# manual sync
		m.sync()
	
	elif cmd == 'h':		# show history
		printl('Incoming Message History (last %d messages)\n' % config['history size'], c.green, 1)
		for i in history_ring:
			rtime = i[0]
			email = i[1]
			msg = i[2]
			print_inc_msg(email, msg, quiet = 1, ptime = 1, recvtime = rtime)

	# send a message
	elif cmd == 'm' or cmd == 'msg' or cmd == 'r' or cmd == 'a':
		if cmd == 'm' or cmd == 'msg':
			p = params.split()
			if len(p) < 1:
				return 'Please enter a nick and a message'
			nick = p[0]
			email = nick2email(nick)
			msg = string.join(p[1:])
		elif cmd == 'r':
			email = last_received
			nick = email2nick(email)
			if not nick: nick = email
			msg = params
		elif cmd == 'a':
			email = last_sent
			nick = email2nick(email)
			if not nick: nick = email
			msg = params
		if not email:
			if cmd == 'a': return 'Please write a message first'
			if cmd == 'r': return 'Please reply a message first'
			else: return 'Unknown nick %s' % str(p[0])
		r = m.sendmsg(email, msg)
		last_sent = email
		if r == 1:
			return 'Message for %s queued for delivery' % nick
		elif r == 2:
			print_out_msg(nick, msg)
			log_msg(email, 'out', msg)
		elif r == -2:
			return 'Message too big'
		else:
			return 'Error %d sending message' % r
		
	elif cmd == 'help' or cmd == '?':
		r = ''
		r += 'Command list:\n'
		r += 'status [mode]\t Shows the current status, or changes it to "mode", which can be one of:\n'
		r += '\t\t\t online, away, busy, brb, phone, lunch, invisible, idle or offline\n'
		r += 'q\t\t Quits the program\n'
		r += 'w\t\t Prints your entire contact list\n'
		r += 'ww\t\t Prints your entire contact list, including email addresses\n'
		r += 'e\t\t Prints your online contacts\n'
		r += 'ee\t\t Prints your online contacts, including email addresses\n'
		r += 'wr\t\t Prints your reverse contact list\n'
		r += 'h\t\t Shows your incoming message history\n'
		r += 'add email nick\t Adds the user "email" with the nick "nick"\n'
		r += 'del nick\t Deletes the user with nick "nick"\n'
		r += 'info [nick]\t Shows the user information and pending messages (if any), or our personal info\n'
		r += 'close nick\t Closes the switchboard connection with "nick"\n'
		r += 'config\t\t Shows the configuration\n'
		r += 'nick newnick\t Changes your nick to "newnick"\n'
		r += 'privacy p a\t Sets whether accept messages from people not on your list (p) and require authorization (a)\n'
		r += 'm nick text\t Sends a message to "nick" with the "text"\n'
		r += 'a text\t\t Sends a message with "text" to the last person you sent a message to\n'
		r += 'r text\t\t Sends a message with "text" to the last person that sent you a message\n'
		r += '\n'
		r += 'In most cases, where you are asked for a nick, you can alternatively enter the email.\n'
		r += 'This makes it easier to handle people with weird or long nicks.\n'
		return r
	else:
		return 'Unknown command, type "help" for help'
	
	return ''



#
# This are the callback replacements, which only handle the output and then
# call the original callbacks to do the lower level stuff
#

# basic classes
m = msnlib.msnd()
m.cb = msncb.cb()

# status change
def cb_iln(md, type, tid, params):
	t = params.split()
	status = msnlib.reverse_status[t[0]]
	email = t[1]
	nick = md.users[email].nick
	ctime = time.strftime('%I:%M:%S%p')
	printl('\r%s ' % ctime, c.blue)
	printl(nick, c.blue, 1)
	printl(' changed status to ', c.magenta)
	printl('%s\n' % status, c.magenta, 1)
	log_msg(email, 'status', status)
	msncb.cb_iln(md, type, tid, params)
m.cb.iln = cb_iln

def cb_nln(md, type, tid, params):
	status = msnlib.reverse_status[tid]
	t = string.split(params)
	email = t[0]
	nick = md.users[email].nick
	ctime = time.strftime('%I:%M:%S%p')
	printl('\r%s ' % ctime, c.blue)
	printl(nick, c.blue, 1)
	printl(' changed status to ', c.magenta)
	printl('%s\n' % status, c.magenta, 1)
	log_msg(email, 'status', status)
	msncb.cb_nln(md, type, tid, params)
m.cb.nln = cb_nln

def cb_fln(md, type, tid, params):
	email = tid
	nick = md.users[email].nick
	ctime = time.strftime('%I:%M:%S%p')
	printl('\r%s ' % ctime, c.blue)
	printl(nick, c.blue, 1)
	printl(' disconnected\n', c.magenta)
	log_msg(email, 'status', 'disconnect')
	msncb.cb_fln(md, type, tid, params)
m.cb.fln = cb_fln

# server disconnect
def cb_out(md, type, tid, params):
	printl('\rServer sent disconnect (probably you logged in somewhere else)\n', c.green, 1)
	msncb.cb_out(md, type, tid, params)
m.cb.out = cb_out


# message
def cb_msg(md, type, tid, params, sbd):
	global last_received
	t = string.split(tid)
	email = t[0]
	
	# messages from hotmail are only when we connect, and send things
	# regarding, aparently, hotmail issues. we ignore them (basically
	# because i couldn't care less; however if somebody has intrest in
	# these and provides some debug output i'll be happy to implement
	# parsing).
	if email == 'Hotmail':
		return

	# parse
	lines = string.split(params, '\n')
	headers = {}
	eoh = 0
	for i in lines:
		# end of headers
		if i == '\r':
			break
		tv = string.split(i, ':')
		type = tv[0]
		value = string.join(tv[1:], ':')
		value = string.strip(value)
		headers[type] = value
		eoh += 1
	
	eoh +=1
	if headers.has_key('Content-Type') and headers['Content-Type'] == 'text/x-msmsgscontrol':
		# the typing notices
		nick = email2nick(email)
		if not nick: nick = email
		if not m.users[email].priv.has_key('typing'):
			m.users[email].priv['typing'] = 0
		if not m.users[email].priv['typing']:
			printl('\r')
			ctime = time.strftime('%I:%M:%S%p')
			printl('%s ' % ctime, c.blue)
			printl('%s' % nick, c.cyan, 1)
			printl(' is typing\n', c.magenta)
		m.users[email].priv['typing'] = time.time()
	else:
		# messages
		m.users[email].priv['typing'] = 0
		printl('\r')
		print_inc_msg(email, lines, eoh)
		log_msg(email, 'in', string.join(lines[eoh:], '\n'))

		# append the message to the history, keeping it below the configured limit
		if len(history_ring) > config['history size']: 
			del(history_ring[0])
		history_ring.append((time.time(), email, lines[eoh:]))
	
	last_received = email
	msncb.cb_msg(md, type, tid, params, sbd)
m.cb.msg = cb_msg


# join a conversation and send pending messages
def cb_joi(md, type, tid, params, sbd):
	email = tid
	if len(sbd.msgqueue) > 0:
		nick = email2nick(email)
		if not nick: nick = email
		printl('\rFlushing messages for %s:\n' % nick, c.green, 1)
		for msg in sbd.msgqueue:
			print_out_msg(nick, msg)
			printl('\n')
			log_msg(email, 'out', msg)
	msncb.cb_joi(md, type, tid, params, sbd)
m.cb.joi = cb_joi

# server errors
def cb_err(md, errno, params):
	if not msncb.error_table.has_key(errno):
		desc = 'Unknown'
	else:
		desc = msncb.error_table[errno]
	desc = '\rServer sent error %d: %s\n' % (errno, desc)
	perror(desc)
	msncb.cb_err(md, errno, params)
m.cb.err = cb_err
	
# users add, delete and modify
def cb_add(md, type, tid, params):
	t = params.split()
	type = t[0]
	if type == 'RL' or type == 'FL':
		email = t[2]
		nick = urllib.unquote(t[3])
	if type == 'RL':
		out = '\r' + c.blue + c.bold + ('%s (%s) ' % (email, nick)) + c.magenta + 'has added you to his contact list\n'
		printl(out)
		beep()
	elif type == 'FL':
		out = '\r' + c.blue + c.bold + ('%s (%s) ' % (email, nick)) + c.magenta + 'has been added to your contact list\n'
		printl(out)
	msncb.cb_add(md, type, tid, params)
m.cb.add = cb_add

def cb_rem(md, type, tid, params):
	t = params.split()
	type = t[0]
	if type == 'RL' or type == 'FL':
		email = t[2]
	if type == 'RL':
		out = '\r' + c.blue + c.bold + email + ' ' + c.magenta + 'has removed you from his contact list\n'
		printl(out)
		beep()
	elif type == 'FL':
		out = '\r' + c.blue + c.bold + email + ' ' + c.magenta + 'has been removed from your contact list\n'
		printl(out)
	msncb.cb_rem(md, type, tid, params)
m.cb.rem = cb_rem



#
# now the real thing
#
printl('Starting up MSN Client D1\n', c.yellow, 1)

# first, the configuration
printl('Loading config... ', c.green, 1)
if len(sys.argv) > 1:
	file = sys.argv[1]
else:
	file = os.environ['HOME'] + '/.msn/msnrc'

config = get_config(file)
if not config:
	perror('Error opening config file (%s), try running "msnsetup"\n' % file)
	quit(1)

# set the mandatory values
if config.has_key('email'): 
	m.email = config['email']
else: 
	perror('Error: email not specified in config file\n')
	quit(1)

if config.has_key('password'):
	m.pwd = config['password']
else:
	perror('Error: password not specified in config file\n')
	quit(1)

# and the optional ones, setting the defaults if not present
# history size
if not config.has_key('history size'): 
	config['history size'] = 10
else:
	try:
		config['history size'] = int(config['history size'])
	except:
		perror('history size must be integer, using default\n')
		config['history size'] = 10

# input history size
if not config.has_key('input history size'):
	config['input history size'] = 10
else:
	try:
		config['history size'] = int(config['history size'])
	except:
		error('input history size must be integer, using default\n')
		config['input history size'] = 10

# initial status
if not config.has_key('initial status'): 
	config['initial status'] = 'online'
elif config['initial status'] not in msnlib.status_table.keys():
	perror('unknown initial status, using default\n')
	config['initial status'] = 'online'

# debug
if not config.has_key('debug'):
	config['debug'] = 0
elif config['debug'] != 'yes':
	config['debug'] = 0

# log history
if not config.has_key('log history'):
	config['log history'] = 1
elif config['log history'] != 'yes':
	config['log history'] = 0

# history directory
if not config.has_key('history directory'):
	config['history directory'] = os.environ['HOME'] + '/.msn/history'

# auto away time
if not config.has_key('auto away'):
	config['auto away'] = 0
else:
	try:
		config['auto away'] = int(config['auto away'])
	except:
		perror('auto away must be integer, using default\n')
		config['auto away'] = 0
if config['auto away'] and config['auto away'] < 60:	# sanity check
	perror('Warning: auto away time was set to less than a minute!\n')

printl('done\n', c.green, 1)


# set or void the debug
if not config['debug']:
	msnlib.debug = null
	msncb.debug = null


# login to msn
printl('Logging in... ', c.green, 1)
try:
	m.login()
	printl('done\n', c.green, 1)
except 'AuthError', info:
	errno = int(info[0])
	if not msncb.error_table.has_key(errno):
		desc = 'Unknown'
	else:
		desc = msncb.error_table[errno]
	perror('Error: %s\n' % desc)
	quit(1)
except KeyboardInterrupt:
	quit()
except ('SocketError', socket.error), info:
	desc = info[1]
	perror('Network error: ' + desc + '\n')
	quit()
except:
	pexc('Exception logging in\n')
	quit()


# call sync to get the lists and refresh
printl('Syncing... ', c.green, 1)
if m.sync():
	printl('done\n', c.green, 1)
else:
	perror('Error syncing users\n')

if m.change_status(config['initial status']):
	printl('Status set to %s\n' % config['initial status'], c.green, 1)
else:
	perror('Error setting status: unknown status %s\n' % config['initial status'])


# global variables
history_ring = []	# history buffer
last_sent = ''		# email of the last person we sent a message to
last_received = ''	# email of the last person we received a message from

# auto-away
timeout = config['auto away']
if not timeout:
	timeout = None		# must be None, not 0 because of select() semantics
auto_away = 0


# loop
redraw_cli()
while 1:
	fds = m.pollable()
	infd = fds[0]
	outfd = fds[1]
	infd.append(sys.stdin)
	try:
		fds = select.select(infd, outfd, [], timeout)
	except KeyboardInterrupt:
		quit()
	
	if timeout and len(fds[0] + fds[1]) == 0:
		# timeout, set auto away
		if m.status == 'NLN':
			m.change_status('away')
			auto_away = 1
			printl('\rAutomatically changing status to away\n', c.green, 1)
	
	for i in fds[0] + fds[1]:		# see msnlib.msnd.pollable.__doc__
		if i == sys.stdin:
			# auto away revival
			if auto_away:
				auto_away = 0
				m.change_status('online')
				printl('\rAutomatically changing status back to online\n', c.green, 1)
			# read from stdin
			stdin_read()
		else:
			try:
				m.read(i)
			except ('SocketError', socket.error), err:
				if i != m:
					if i.msgqueue:
						nick = email2nick(i.emails[0])
						printl("\rConnection with %s closed - the following messages couldn't be sent:\n" % (nick), c.green, 1)
						for msg in i.msgqueue:
							printl(c.bold + '\t>>> ' + c.normal + msg + '\n')
					m.close(i)
				else:
					printl('\nMain socket closed (%s)\n' % str(err), c.red)
					quit(1)
			# always redraw after network event
			redraw_cli()


