git » msnlib » commit c75db00

Initial import.

author Alberto Bertogli
2005-04-09 17:20:49 UTC
committer Alberto Bertogli
2005-04-09 17:20:49 UTC

Initial import.

INSTALL.txt +37 -0
README +42 -0
doc/Changelog +455 -0
doc/CodingStyle +10 -0
doc/FAQ +130 -0
doc/LICENSE +186 -0
doc/TODO +43 -0
doc/URL +10 -0
doc/commands +119 -0
doc/dependencies +6 -0
doc/icons +46 -0
doc/log_format +44 -0
doc/msg_states +47 -0
doc/msncd_protocol +128 -0
doc/portability +39 -0
doc/profiles +20 -0
doc/reporting_bugs +38 -0
install +31 -0
msn +1623 -0
msncb.py +507 -0
msnlib.py +743 -0
msnrc.sample +65 -0
msnsetup +87 -0
setup.py +12 -0
utils/clean +5 -0
utils/hmerge +110 -0
utils/msnbot +127 -0
utils/msncd +417 -0
utils/msnlog.vim +30 -0
utils/msntk +468 -0
utils/vicl +19 -0

diff --git a/INSTALL.txt b/INSTALL.txt
new file mode 100644
index 0000000..cb05367
--- /dev/null
+++ b/INSTALL.txt
@@ -0,0 +1,37 @@
+
+To install the library and the client, run the 'install' file as root
+(probably using something like './install').
+
+It will copy the python modules to the proper place, and the client to
+/usr/local/bin.
+
+Then, you can run (as your user) 'msnsetup' to create the initial
+configuration file and directories (placed in ~/.msn); and finally run the
+client with 'msn'. Skip this step if you are upgrading.
+
+Alternatively, you can create your own ~/.msn/msnrc file based on an example
+named 'msnrc.sample'.
+
+
+Here is a command line summary:
+
+# we became root
+su
+
+# then install everything
+./install
+
+# now drop the root privileges
+exit
+
+# run the setup to create the configuration (only if you are not upgrading)
+msnsetup
+
+# and finally start the client
+msn
+
+
+I would really like to hear your opinion, so please drop any comments to
+msnlib-devel@auriga.wearlab.de.
+
+
diff --git a/README b/README
new file mode 100644
index 0000000..5ef35b6
--- /dev/null
+++ b/README
@@ -0,0 +1,42 @@
+
+msnlib
+Alberto Bertogli (albertogli@telpin.com.ar)
+----------------------------------------------
+
+This is a python implementation for the msn messenger protocol (version 8),
+it's pretty simple and straightforward; but it works well.
+
+Please read the 'INSTALL' file to see how to install and use both the client
+and the library.
+
+The client is really simple, uses a text-mode interface with commands similar
+to 'micq' (http://micq.ukeer.de/), which is a great ICQ client. 
+
+If you're looking for a good messaging system, forget about messenger and try
+Jabber (http://www.jabber.org), it's the only one which is run in the open
+based on public standards, it's safe, fast, has a lot of features and it's
+quite scalable. 
+
+But if you're stucked with messenger for whatever reason (your friends,
+family, the dog, all use it =), I hope you find this useful.
+
+
+The basic idea for the library is a main class which represents a connection
+with the server and holds all the relevant information, it has only the a few
+methods to login, change some info and status, and send messages; all the rest
+is driven by an asyncronous callback scheme registered at runtime which can be
+changed on the fly.
+
+If you're intrested, read the code to find out more; most of the documentation
+is there either as command or as python documentation strings. You might also
+want to check out the doc directory; specially if you use a non-unix platform,
+take a look at the 'portability' file.
+
+
+Comments and patches are always welcome; please send them to the mailing list,
+msnlib-devel@auriga.wearlab.de.
+
+Thanks,
+		Alberto
+
+
diff --git a/doc/Changelog b/doc/Changelog
new file mode 100644
index 0000000..65742d5
--- /dev/null
+++ b/doc/Changelog
@@ -0,0 +1,455 @@
+10 Mar 05 00.34.03 - Alberto <albertogli@telpin.com.ar>
+ * msn: fix documentation for the add command.
+
+27 Jun 04 14.12.24 - Alberto <albertogli@telpin.com.ar>
+ * msntk: fixes from the console client.
+
+29 Mar 04 23.55.18 - Alberto <albertogli@telpin.com.ar>
+ * tag: 3.4 tag
+
+22 Mar 04 17.45.29 - Alberto <albertogli@telpin.com.ar>
+ * msn: change initial status after we got the full user list; this should fix
+	the problem that all the list appeared offline, but it needs testing
+
+21 Mar 04 21.43.39 - Alberto <albertogli@telpin.com.ar>
+ * msn: remove space after the first line which that messed up NetBSD. Thanks
+	to Cameron Kaiser.
+ * msn: allow Ctrl+U to clear the command line. Patch by Cameron Kaiser.
+
+01 Mar 04 04.33.38 - Alberto <albertogli@telpin.com.ar>
+ * msn: add 'ren' command to rename users
+
+20 Dec 03 22.29.01 - Alberto <albertogli@telpin.com.ar>
+ * msn: handle a crash when typing space and tab by doing proper param
+	checking in tab handling
+
+10 Dec 03 23.16.52 - Alberto <albertogli@telpin.com.ar>
+ * doc: update license to OSL v2.0
+
+13 Nov 03 11.47.09 - Alberto <albertogli@telpin.com.ar>
+ * msncb, msn: add more information to XFRError to see if we can get what is
+	causing it
+
+12 Nov 03 13.13.27 - Alberto <albertogli@telpin.com.ar>
+ * doc: update URL
+
+12 Nov 03 12.52.27 - Alberto <albertogli@telpin.com.ar>
+ * tag: 3.3 tag
+
+12 Nov 03 00.06.32 - Alberto <albertogli@telpin.com.ar>
+ * msn: handle socket exceptions more properly so we cleanup the console on
+	the exit path even when the md is down
+
+11 Nov 03 23.35.05 - Alberto <albertogli@telpin.com.ar>
+ * msnlib: handle some server errors while logging in
+
+08 Nov 03 18.29.47 - Alberto <albertogli@telpin.com.ar>
+ * msn: handle \r input for mac keyboards
+
+03 Nov 03 20.19.07 - Alberto <albertogli@telpin.com.ar>
+ * msntk: add msntk, a tk-based client
+
+30 Oct 03 02.48.30 - Alberto <albertogli@telpin.com.ar>
+ * doc: various small updates
+
+29 Oct 03 22.46.13 - Alberto <albertogli@telpin.com.ar>
+ * msn: fix a crash when passing an unknown nick to block/unblock
+ * msn: do proper nick checking in lignore
+
+28 Oct 03 23.13.56 - Alberto <albertogli@telpin.com.ar>
+ * msn: fix a crash when the configuration file was passed as a parameter
+
+27 Oct 03 12.49.14 - Alberto <albertogli@telpin.com.ar>
+ * msnlib: do proper connect() checking using getsockopt()
+ * msnlib: fix user.__repr__()
+
+18 Oct 03 10.12.12 - Alberto <albertogli@telpin.com.ar>
+ * msn: mark blocked users in listings
+ * msn: use string.join() for multi-user chat filenames
+ * msnlib: remove FIXME from message lenght check; the way it's done is safe
+ * msnlib: allow sendmsg to get a destination sbd directly
+
+30 Sep 03 15.06.19 - Alberto <albertogli@telpin.com.ar>
+ * doc: upgrade documentation to reflect the need for at least Python 2.2.2
+ * doc: minor change to TODO list
+
+29 Sep 03 09.17.33 - Alberto <albertogli@telpin.com.ar>
+ * tag: 3.2 tag
+
+28 Sep 03 11.39.59 - Alberto <albertogli@telpin.com.ar>
+ * msn: handle some Hotmail messages, now we display unread mail and new mail
+	notifications
+ * msnsetup: display profile name in the final message
+ * msn: implement multi-user chat logging, so now chats that involve multiple
+	users get logged properly to a different file
+ * msnlib: fix the login procedure to handle redirects properly, and clean up
+	and comment the code so we don't get lost in the parsing
+
+28 Sep 03 10.01.35 - Alberto <albertogli@telpin.com.ar>
+ * tag: 3.1 tag
+
+28 Sep 03 09.59.14 - Alberto <albertogli@telpin.com.ar>
+ * msnlib: fix SSL error handling, now raise the error properly
+
+27 Sep 03 15.58.21 - Alberto <albertogli@telpin.com.ar>
+ * msn: ignore gaim's x-clientcaps messages
+ * msn: allow spaces in nick
+
+25 Sep 03 23.02.38 - Alberto <albertogli@telpin.com.ar>
+ * msn: fix crashes when sending a null message
+ * msn: fix message content mangling when the first character matched the nick
+
+24 Sep 03 14.29.17 - Alberto <albertogli@telpin.com.ar>
+ * tag: 3.0
+ * doc: small updates
+
+22 Sep 03 23.55.26 - Alberto <albertogli@telpin.com.ar>
+ * msn: preserve whitespace in messages
+
+22 Sep 03 02.10.15 - Alberto <albertogli@telpin.com.ar>
+ * msn, msnlib: implement user blocking and unblocking
+ * msncb: fix MSNP8 SYN so it doesn't step over existing users
+ * msncb: use the same user object for both reverse and forward lists
+ * msncb: fix MSNP8 ADD and BPR so both work well with the new SYN
+
+22 Sep 03 00.14.15 - Alberto <albertogli@telpin.com.ar>
+ * msnlib, msncb: implement MSNP8
+
+25 Aug 03 23.14.08 - Alberto <albertogli@telpin.com.ar>
+ * tag: 2.1
+
+25 Aug 03 14.09.17 - Alberto <albertogli@telpin.com.ar>
+ * msnlib: keep message queues when submitting sbds, to avoid loosing messages
+	when two XFRs happen at the 'same' time
+ * msn: priorize users with sbd for tab completion
+
+25 Aug 03 00.05.22 - Alberto <albertogli@telpin.com.ar>
+ * msn: use email instead of nick for tab completion when nick has spaces
+ * msn: implement tab completion for emails and nicks
+
+04 Aug 03 15.07.53 - Alberto <albertogli@telpin.com.ar>
+ * msn: fix user info to avoid crashing when asking for info on users
+	which are not on our list
+ * msn: some indentation work
+ * msncb: close sbd when deleting a user
+
+08 Jun 03 19.11.18 - Alberto <albertogli@telpin.com.ar>
+ * tag: 2.0
+
+07 Jun 03 15.40.18 - Alberto <albertogli@telpin.com.ar>
+ * msn, msnlib, msncb: add group support
+
+05 Jun 03 17.16.21 - Alberto <albertogli@telpin.com.ar>
+ * doc: add an icons table
+
+05 Jun 03 00.22.48 - Alberto <albertogli@telpin.com.ar>
+ * msn: move the help to a separate string
+ * msn, msnlib, msncb: fix several weird corner cases with multi-user chats
+ 
+04 Jun 03 12.54.31 - Alberto <albertogli@telpin.com.ar>
+ * msncb: don't close the sbd on fln, so we can keep talking while invisible
+
+04 Jun 03 12.38.42 - Alberto <albertogli@telpin.com.ar>
+ * msn, msnlib: add invitation support for multi-user chats
+ * msncb: fix fln sbd handling
+
+04 Jun 03 11.00.04 - Alberto <albertogli@telpin.com.ar>
+ * msncb: add email to sbd.emails on joi and iro
+ * msncb: handle bye messages properly for multi-user sbds
+
+04 Jun 03 10.50.54 - Alberto <albertogli@telpin.com.ar>
+ * msncb, msn: fix flushing for sbd joins
+ * msncb, msn: implement cb_iro, for joining a multi-user sbd
+
+03 Jun 03 19.15.34 - Alberto <albertogli@telpin.com.ar>
+ * msn: fix a bug when sending raw commands
+
+02 Jun 03 17.14.56 - Alberto <albertogli@telpin.com.ar>
+ * utils: add hmerge utility
+ * msn: add notification for sbd joins
+
+24 May 03 19.57.58 - Alberto <albertogli@telpin.com.ar>
+ * msncb: use debug() from msnlib
+ * msnlib: fix a bug in _recv() that didn't decoded the parameters properly
+ * msnlib, msncb: remember real nicks
+ * msn: show real nicks
+ * doc: update the TODO list
+
+21 May 03 19.01.27 - Alberto <albertogli@telpin.com.ar>
+ * msnlib, msncb: sbd connection handling cleanup, also fixed a weird bug
+	that raised EINPROGRESS after connect
+
+20 May 03 18.51.25 - Alberto <albertogli@telpin.com.ar>
+ * license: change the license from GPL to OSL
+
+11 May 03 15.42.38 - Alberto <albertogli@telpin.com.ar>
+ * msn: allow to send messages when either we or the receiver is offline if we
+	already have a sbd
+
+11 May 03 02.30.09 - Alberto <albertogli@telpin.com.ar>
+ * msn, msnlib, msncb: handle a protocol bug that allows line feeds (0x0C) in
+	user nicks, now we split fields explicitly only by a space (' ');
+	thanks to menetas@menetas.net for the report
+
+01 May 03 19.20.41 - Alberto <albertogli@telpin.com.ar>
+ * tag: 1.0 tag
+
+01 May 03 19.16.39 - Alberto <albertogli@telpin.com.ar>
+ * utils: add msnbot and msncd to the utilities
+ * doc: small documentation updates
+
+23 Apr 03 18.08.25 - Alberto <albertogli@telpin.com.ar>
+ * msn: fix a very odd but important bug that could cause a protocol break by
+	miscalculating the lenght of an encoded message. Thanks to Ahilan
+	Sinnarajah for the report
+
+10 Apr 03 01.30.06 - Alberto <albertogli@telpin.com.ar>
+ * msn: implement color themes
+
+09 Apr 03 00.55.43 - Alberto <albertogli@telpin.com.ar>
+ * msn, msnsetup: implement profile support
+
+26 Mar 03 13.06.55 - Alberto <albertogli@telpin.com.ar>
+ * msn: when logging in, print the error code along with description
+
+18 Mar 03 15.45.37 - Alberto <albertogli@telpin.com.ar>
+ * msn: ask for password if not given in the configuration
+
+05 Mar 03 14.01.34 - Alberto <albertogli@telpin.com.ar>
+ * msnlib: don't send an extra new line in sendmsg
+
+16 Feb 03 16.48.08 - Alberto <albertogli@telpin.com.ar>
+ * msn: fix an exception in socket error handling
+
+20 Jan 03 14.31.43 - Alberto <albertogli@telpin.com.ar>
+ * tag: d4 tag (changelog entry not included in d4's tarballs)
+
+20 Jan 03 10.39.06 - Alberto <albertogli@telpin.com.ar>
+ * doc: update url and add mailing list information
+ * install: allow the user to specify an installation root directory
+ * install: install documentation
+ * setup: add url
+
+16 Jan 03 13.48.40 - Alberto <albertogli@telpin.com.ar>
+ * msnlib: moved the encoding support from the client to the library
+ * msncb: close the sbd when the user goes offline
+ * msnlib: print the discarded messages when the user goes offline
+ 	it fixes an annoying race where a user goes offline and we have an
+	open sbd. Thanks to Peter Stuge for the report
+
+14 Jan 03 23.04.23 - Alberto <albertogli@telpin.com.ar>
+ * msn: implemented local ignore
+ * msn: now show internal variables in 'config'
+ * doc: added a 'commands' doc to explan the commands in more detail
+
+13 Jan 03 20.52.13 - Alberto <albertogli@telpin.com.ar>
+ * msn: encoding support, thanks to Peter Stuge for the suggestion and code.
+
+05 Jan 03 14.35.17 - Alberto <albertogli@telpin.com.ar>
+ * msn: fixes to time.strftime to make it work under 2.0
+ * doc: small documentation updates, including a new license note.
+ * msn: now use "/usr/bin/env python" to run msn
+ * msn: run python without the -Q switch
+
+26 Nov 02 11.36.00 - Alberto <albertogli@telpin.com.ar>
+ * tag: d3 tag
+
+24 Nov 02 12.50.11 - Alberto <albertogli@telpin.com.ar>
+ * msn: completed the FAQ, and added a reporting_bugs document
+ * msn: small changes to the INSTALL file
+ * msn: return nothing on log_msg as the return value is not important
+
+19 Nov 02 15.55.52 - Alberto <albertogli@telpin.com.ar>
+ * msn: fix two big bugs with terminal handling; using nonblocking io makes
+	flushes and write fail under certain conditions (quite easily
+	triggered when running into a X terminal), so now they're replaced
+	with safe wrappers. Thanks to kusamochi@msn.com for the report, and
+	ameoba@opn for the huge help with the fix.
+ * msnlib: improved the close call, so now it won't raise an exception on a
+	sbd that has a non-established socket
+
+17 Nov 02 10.26.31 - Alberto <albertogli@telpin.com.ar>
+ * msn: enabling debug prints the terminal size and termios use
+
+12 Nov 02 00.42.58 - Alberto <albertogli@telpin.com.ar>
+ * msn: changed some command output to look less cryptic
+ * doc: add the FAQ
+
+08 Nov 02 00.03.43 - Alberto <albertogli@telpin.com.ar>
+ * msn: improved tab completion adding basic cycling
+ * msn: be able to disable logging using the already-existing 'log history'
+	configuration variable
+
+31 Oct 02 15.38.45 - Alberto <albertogli@telpin.com.ar>
+ * msnlib: remove unused constant BSIZE
+ * msnlib: update VERSION
+
+30 Oct 02 18.35.28 - Alberto <albertogli@telpin.com.ar>
+ * msn: esthetic code changes
+ * tag: d2 tag
+
+29 Oct 02 08.50.14 - Alberto <albertogli@telpin.com.ar>
+ * msn: fix a small bug with the up and down keys
+ * msn: another small fix to ignore unknown escape sequences
+ * msn: change the order of tab keystrokes, now received goes first
+ * msn: don't print the prompt on exit
+
+28 Oct 02 18.45.12 - Alberto <albertogli@telpin.com.ar>
+ * msn: handle screen width appropiatedly
+ * tag: d1 tag
+
+24 Oct 02 13.22.34 - Alberto <albertogli@telpin.com.ar>
+ * msn: implemented command line history using escape codes, and added a
+	configuration option for its size
+ * msn: implemented basic tab completion
+
+23 Oct 02 21.43.08 - Alberto <albertogli@telpin.com.ar>
+ * msn: framework for advanced terminal handling
+ * doc: added a 'portability' doc with some info
+ * doc: several minor modifications
+ * msn: minor modifications to the help text
+
+23 Oct 02 09.33.51 - Alberto <albertogli@telpin.com.ar>
+ * msn: improve output on server disconnect
+ * msn: change the interpreter to /usr/bin/python, which is a more standard
+	location
+
+22 Oct 02 21.49.07 - Alberto <albertogli@telpin.com.ar>
+ * msn: use quit() instead of sys.exit() in several places
+ * msn: handle network errors when logging in
+ * tag: c2 tag
+ 
+10 Oct 02 18.43.59 - Alberto <albertogli@telpin.com.ar>
+ * msn: be able to use emails as nicks (can be useful for weird nicks)
+ * msn: fix a simple bug when tried to close a socket that didn't exist
+ * msn: unified the quit procedure and handle a keyboard interrupt properly
+ * msn: improved parameter handling in 'del' and 'privacy' commands, avoiding
+	crashes with incorrect user input
+ * msn: added 'ww' and 'ee' commands, which are like 'w' and 'e' but show also
+	email addresses
+ * msn: added some help
+ * msn: improved 'status' a bit
+ * msnlib: added a 'priv' field in the user class (as a dictionary) which can
+	be used by the client for private purposes
+ * msn: display 'typing' only once, and show the last typing time in the user
+	info
+ * msn: improved some code indentation, documentation and comments
+ * doc: minor changes in the INSTALL and README files
+ * msn: display time with status changes
+
+04 Oct 02 10.20.46 - Alberto <albertogli@telpin.com.ar>
+ * msn: now lstrip before parsing the command, so we don't crash on spaces
+
+28 Sep 02 13.26.23 - Alberto <albertogli@telpin.com.ar>
+ * msn: added a handler for a socket.socket exception when reading from
+	sockets, and now print the error
+
+24 Sep 02 16.56.12 - Alberto <albertogli@telpin.com.ar>
+ * msn: fixed some small error displays on unknown nick.
+ * msn: fixed a crash when not giving enough parameters to 'm'
+
+24 Sep 02 10.50.59 - Alberto <albertogli@telpin.com.ar>
+ * msnlib: fixed the only (known =) remaining bug, now connections are no
+	longer closed when we send a message
+ * tag: c1 tag
+
+23 Sep 02 13.30.08 - Alberto <albertogli@telpin.com.ar>
+ * msn: fixed a bug in the 'del' command which passed the nick instead of the
+	email to msnlib, so removals when nick != email (the common case)
+	failed
+ * msn: print emails when showing the the reverse user list
+ * msn: added command 'debug' to toggle debugging at runtime
+ * msnlib, msncb: print debugging output to stderr to make capturing easier
+
+22 Sep 02 13.54.29 - Alberto <albertogli@telpin.com.ar>
+ * msn: implement auto-away
+ * msn: fixed the configuration variable handling when set to an invalid value
+ * msn: added command 'config' to display the configuration
+ * msn: remove '\r' from the incoming messages
+
+18 Sep 02 21.18.17 - Alberto <albertogli@telpin.com.ar>
+ * msnlog.vim: added a vim syntax file to make log reading nicer
+ * scripts - utils: renamed 'scripts' to 'utils'
+
+17 Sep 02 15.48.50 - Alberto <albertogli@telpin.com.ar>
+ * doc: modified several doc files
+ * tag: b3 tag - released, thanks a lot to Henne Vogelsang for providing the hosting
+
+16 Sep 02 14.53.34 - Alberto <albertogli@telpin.com.ar>
+ * msnsetup: create a simple setup script for the configuration file
+ * install: create the installer scripts for the python modules and the client
+ * msn: fixed a multi-line message logging bug
+
+13 Sep 02 12.53.15 - Alberto <albertogli@telpin.com.ar>
+ * msn: implement history (and status change) logging
+ * msn: ignore Hotmail messages
+ * tag: b2 tag
+
+12 Sep 02 16.41.26 - Alberto <albertogli@telpin.com.ar>
+ * msncb.py: when flushing pending messages we used to keep the message in the
+	sbd msgqueue, fixed
+ * msn: show the pending messages on switchboard disconnect
+ * config: moved the configuration files default location to ~/.msn
+
+11 Sep 02 12.25.41 - Alberto <albertogli@telpin.com.ar>
+ * msnclient.py - msn: renamed msnclient.py to msn
+ * cb.py - msncb.py: renamed cb.py to msncb.py
+ * msn: s/cb/msncb/g to reflect the rename
+ * msn: added a configuration file, which led to a reorganization of some
+	variables and init procedures
+ * msn: some new-line changes to the print* functions
+ * msn: create print functions for outgoing and incoming messages, and now
+	print the time
+ * msn: display more information on some commands now that we have config
+ * msn: added 'close' command, which closes a sbd connection manually
+ 
+11 Sep 02 08.36.36 - Alberto <albertogli@telpin.com.ar>
+ * msnclient.py: create a nick2email and email2nick functions
+ * msnclient.py: added 'r' (reply) and 'a' (send to the last person you sent a
+	message to) commands
+ * msnclient.py: added 'info' command to display user info and simplified the
+	user list
+ * msnclient.py: modified several commands to accept and display nick instead
+	of email
+
+11 Sep 02 00.16.16 - Alberto <albertogli@telpin.com.ar>
+ * msnlib.py: make status_change return 0 if error
+ * msnclient.py: improve the status command
+ * msnclient.py: reverse list view (command 'wr')
+
+10 Sep 02 20.41.53 - Alberto <albertogli@telpin.com.ar>
+ * msnclient.py: change message command from 'msg' to 'm'
+ * msnclient.py: use nicks instead of email to send messages
+
+04 Sep 02 10.30.59 - Alberto <albertogli@telpin.com.ar>
+ * msnclient.py: create print_msg for message printing
+ * msnclient.py: add command: e, display only online contacts
+ * msnclient.py: add incoming message history through a size-limited list
+ * msnclient.py: defer outgoing message display until flushing
+
+02 Sep 02 21.48.28 - Alberto <albertogli@telpin.com.ar>
+ * tag: b1 tag; a lot of things got fixed, so far the library is solid (never
+	had a crash, but didn't get more test than myself)
+
+27 Aug 02 20.59.27 - Alberto <albertogli@telpin.com.ar>
+ * msnlib.py: added version number
+ * msnclient.py: make up an usable client, tons of changes
+ * cb.py: make the error table global
+
+25 Aug 02 13.17.59 - Alberto <albertogli@telpin.com.ar>
+ * msnlib.py: fixed a small bug that left the sb sockets nonblocking which
+	might bring some problems
+ * msnlib.py: fixed an important bug for sending messages that wrote two
+	newlines after a message, and apparently the server didn't like it
+
+29 Jul 02 00.44.30 - Alberto <albertogli@telpin.com.ar>
+ * tag: a1 tag; fixed several bugs regarding messaging and now it's probably
+	done (as in 'everything kinda works'). Obviously the interface still
+	sucks but i think i'm done with the callbacks. Also added a bit of
+	documentation, the basic README and cleaned up a little bit
+
+28 Jul 02 04.04.22 - Alberto <albertogli@telpin.com.ar>
+ * tag: Initial a0 tag. The basic working structure is there, it
+	hasn't been tested enough (ie. never received or sent a message) but
+	there is no need because i know it works =)
+
diff --git a/doc/CodingStyle b/doc/CodingStyle
new file mode 100644
index 0000000..c6c2a18
--- /dev/null
+++ b/doc/CodingStyle
@@ -0,0 +1,10 @@
+
+Please, if you are going to submit any patches, use the linux kernel coding
+style (for those which are not familiar with it, it's basically just k&r).
+
+If you haven't, read it from linux/Documentation/CodingStyle; it's an
+excellent doc and everything applies here.
+
+Yeah i'm aware that this is python and not C, but the style idea is the same
+except a few minor things.
+
diff --git a/doc/FAQ b/doc/FAQ
new file mode 100644
index 0000000..9a3927d
--- /dev/null
+++ b/doc/FAQ
@@ -0,0 +1,130 @@
+
+* How do I subscribe to the mailing list, or browse the archives?
+
+You can use the mailing list's web interface,
+http://lists.auriga.wearlab.de/cgi-bin/mailman/listinfo/msnlib-devel
+for both reading the archives or subscribing.
+Note that you don't need to be subscribed to post.
+
+
+* What are the 'Message for NNN queued for delivery' and 'Flushing messages
+	to...' and what do they do?
+
+When you send a message to someone, the client has to open a connection first;
+as this takes time, the message is put in a queue until the connection is
+ready, and then they are sent.
+The 'Message for NNN queued for delivery' tells you that the message has been
+queued, then, a connection is tried to establish, and once this is done, the
+messages in the queue are sent, and you are told with 'Flushing messages
+to...'.
+The connection is not done for every message, so you don't see this every
+time, but usually only once for the first message.
+
+
+* What's the meaning of 'User NNN is typing' and why only appears once?
+
+Those are special messages telling you that the user 'NNN' is typing a
+message, and are sent by the client. Note that this msn client does _not_ send
+these messages.
+You only see them one because they are usually sent every 5 seconds, which
+makes them highly annoying, so the client only displays one. If you want to
+know the last time one of these messages was sent, you can do it using the
+'info' command.
+
+
+* How does tab completion work?
+
+At this moment, it's simple and very basic: you can cycle through the person
+who last you received a message from, and the one you last sent one to.
+Note that this cycling only works when the commandline is empty, or when you
+have previosuly tabbed.
+
+
+* How does auto-away work?
+
+It's quite simple: you configure it in your msnrc file, using a line like
+'auto away = SECS' where SECS is the number of seconds after that, if you
+didn't type anything, you're automatically set to away.
+Then, when you come back and type, your status is automatically changed back
+to online.
+Note that this will work only when your status is online, because if i'm
+'busy' i don't want it going back to away =)
+
+
+* Where are my log files, and what's the format to read them?
+
+The log files are kept by default in $HOME/.msn/history, but it can be changed
+using the 'history directory' option in your msnrc file.
+The format is described in doc/log_format.
+
+
+* What are the different ways of sending messages?
+
+You have three basic commands for sending messages:
+The first one is 'm' (or its alias, 'msg'), which takes a nick or an email as
+a parameter, and sends a message to that person. This is what tab completion
+types for you.
+The other two are the 'a' and 'r' messages; 'a' sends a message to the last
+person you sent a message to; and 'r' replies a message to the last person you
+received a message from.
+
+
+* What's the reverse contact list?
+
+It's simply the list of all the people who have you on their contact list. You
+can see it with the 'wr' command.
+
+
+* How do the privacy option work? What is it for?
+
+The privacy option lets you set if you want people to ask you for
+authorization to add you (i never implemented user authorization, so i have no
+idea about how or if this works), and to block messages from people which are
+not on your list. This is all done by the server so no permanent configuration
+is needed.
+
+The command is called 'privacy', and has two parameters, called 'a' and 'p';
+which are set to 1 (yes) or 0 (no), and come from 'Authorization' and
+'Privacy', the two policies we just talked about'.
+
+So, if you don't want to receive messages from people which are not on your
+list, but you want people to add you without any authorization, the command
+would be 'privacy 0 1'.
+
+
+* Is there a way of enabling debugging without it getting in the middle of my
+	session?
+
+Sure, just run "msn 2> msn.debug.output"; it will redirect stderr (where debug
+messages go) to the file called 'msn.debug.output'. Remember to enable
+debugging too, using either the 'debug' command or adding 'debug = yes' to the
+msnrc file.
+
+
+* Why does the contact list has this weird order?
+
+The order in the contact list is given by the email addresses, which obviously
+has no reference whatsoever to the nicks; so even if it looks unsorted, you
+notice an alphabetic sort quite clearly when you do 'ww'.
+
+
+* I get 'Main socket closed' followed by a strange error everyonce in a while
+
+Output sample:
+	[msn] status invisible
+	Status changed to: invisible
+	[msn]
+	Main socket closed ((104, 'Connection reset by peer'))
+	Closing
+	; 
+
+This happens when some unexpected network error occurs. It depends highly on
+the error, but most of the times it's because the MSN server has closed our
+main connection without reason, or our internet connection just dropped. There
+isn't much we can do about it; we could reconnect and that's on the TODO list.
+But if you get these errors normally, after checking your internet connection
+(it could be just that) please send me the error report (a copy/paste of the
+output would be just fine).
+
+
+
diff --git a/doc/LICENSE b/doc/LICENSE
new file mode 100644
index 0000000..bdf1e48
--- /dev/null
+++ b/doc/LICENSE
@@ -0,0 +1,186 @@
+
+This project, 'msnlib', is copyrighted by Alberto Bertogli and licensed under
+the Open Software License version 2.0 as obtained from www.opensource.org (and
+included here-in for easy reference) (that license itself is copyrighted by
+Larry Rosen).
+
+Note that the "Original Work" that this license covers is only the library
+itself and the client, along with the sample callbacks. Thus just the act of
+linking/importing this library into another program does NOT in itself make
+that program considered a derivative work of this Original Work.
+
+		Alberto Bertogli
+		20 May 2003
+
+-------------------------------------------------------------------------
+
+
+Open Software License
+ v. 2.0
+
+This Open Software License (the "License") applies to any original work of
+authorship (the "Original Work") whose owner (the "Licensor") has placed the
+following notice immediately following the copyright notice for the Original
+Work:
+
+	Licensed under the Open Software License version 2.0
+
+
+1) Grant of Copyright License. Licensor hereby grants You a world-wide,
+royalty-free, non-exclusive, perpetual, sublicenseable license to do the
+following:
+
+  a) to reproduce the Original Work in copies;
+
+  b) to prepare derivative works ("Derivative Works") based upon the
+     Original Work;
+
+  c) to distribute copies of the Original Work and Derivative Works to
+     the public, with the proviso that copies of Original Work or
+     Derivative Works that You distribute shall be licensed under the
+     Open Software License;
+
+  d) to perform the Original Work publicly; and
+
+  e) to display the Original Work publicly.
+
+2) Grant of Patent License. Licensor hereby grants You a world-wide,
+royalty-free, non-exclusive, perpetual, sublicenseable license, under patent
+claims owned or controlled by the Licensor that are embodied in the Original
+Work as furnished by the Licensor, to make, use, sell and offer for sale the
+Original Work and Derivative Works.
+
+3) Grant of Source Code License. The term "Source Code" means the preferred
+form of the Original Work for making modifications to it and all available
+documentation describing how to modify the Original Work. Licensor hereby
+agrees to provide a machine-readable copy of the Source Code of the Original
+Work along with each copy of the Original Work that Licensor distributes.
+Licensor reserves the right to satisfy this obligation by placing a
+machine-readable copy of the Source Code in an information repository
+reasonably calculated to permit inexpensive and convenient access by You for
+as long as Licensor continues to distribute the Original Work, and by
+publishing the address of that information repository in a notice immediately
+following the copyright notice that applies to the Original Work.
+
+4) Exclusions From License Grant. Neither the names of Licensor, nor the names
+of any contributors to the Original Work, nor any of their trademarks or
+service marks, may be used to endorse or promote products derived from this
+Original Work without express prior written permission of the Licensor.
+Nothing in this License shall be deemed to grant any rights to trademarks,
+copyrights, patents, trade secrets or any other intellectual property of
+Licensor except as expressly stated herein. No patent license is granted to
+make, use, sell or offer to sell embodiments of any patent claims other than
+the licensed claims defined in Section 2. No right is granted to the
+trademarks of Licensor even if such marks are included in the Original Work.
+Nothing in this License shall be interpreted to prohibit Licensor from
+licensing under different terms from this License any Original Work that
+Licensor otherwise would have a right to license.
+
+5) External Deployment. The term "External Deployment" means the use or
+distribution of the Original Work or Derivative Works in any way such that the
+Original Work or Derivative Works may be used by anyone other than You,
+whether the Original Work or Derivative Works are distributed to those persons
+or made available as an application intended for use over a computer network.
+As an express condition for the grants of license hereunder, You agree that
+any External Deployment by You of a Derivative Work shall be deemed a
+distribution and shall be licensed to all under the terms of this License, as
+prescribed in section 1(c) herein.
+
+6) Attribution Rights. You must retain, in the Source Code of any Derivative
+Works that You create, all copyright, patent or trademark notices from the
+Source Code of the Original Work, as well as any notices of licensing and any
+descriptive text identified therein as an "Attribution Notice." You must cause
+the Source Code for any Derivative Works that You create to carry a prominent
+Attribution Notice reasonably calculated to inform recipients that You have
+modified the Original Work. 
+
+7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that
+the copyright in and to the Original Work and the patent rights granted herein
+by Licensor are owned by the Licensor or are sublicensed to You under the
+terms of this License with the permission of the contributor(s) of those
+copyrights and patent rights. Except as expressly stated in the immediately
+proceeding sentence, the Original Work is provided under this License on an
+"AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including,
+without limitation, the warranties of NON-INFRINGEMENT, MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY OF THE
+ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an
+essential part of this License. No license to Original Work is granted
+hereunder except under this disclaimer. 
+
+8) Limitation of Liability. Under no circumstances and under no legal theory,
+whether in tort (including negligence), contract, or otherwise, shall the
+Licensor be liable to any person for any direct, indirect, special,
+incidental, or consequential damages of any character arising as a result of
+this License or the use of the Original Work including, without limitation,
+damages for loss of goodwill, work stoppage, computer failure or malfunction,
+or any and all other commercial damages or losses. This limitation of
+liability shall not apply to liability for death or personal injury resulting
+from Licensor's negligence to the extent applicable law prohibits such
+limitation. Some jurisdictions do not allow the exclusion or limitation of
+incidental or consequential damages, so this exclusion and limitation may not
+apply to You. 
+
+9) Acceptance and Termination. If You distribute copies of the Original Work
+or a Derivative Work, You must make a reasonable effort under the
+circumstances to obtain the express assent of recipients to the terms of this
+License. Nothing else but this License (or another written agreement between
+Licensor and You) grants You permission to create Derivative Works based upon
+the Original Work or to exercise any of the rights granted in Section 1
+herein, and any attempt to do so except under the terms of this License (or
+another written agreement between Licensor and You) is expressly prohibited by
+U.S.  copyright law, the equivalent laws of other countries, and by
+international treaty. Therefore, by exercising any of the rights granted to
+You in Section 1 herein, You indicate Your acceptance of this License and all
+of its terms and conditions. This License shall terminate immediately and you
+may no longer exercise any of the rights granted to You by this License upon
+Your failure to honor the proviso in Section 1(c) herein.
+
+10) Termination for Patent Action. This License shall terminate automatically
+and You may no longer exercise any of the rights granted to You by this
+License as of the date You commence an action, including a cross-claim or
+counterclaim, for patent infringement (i) against Licensor with respect to a
+patent applicable to software or (ii) against any entity with respect to a
+patent applicable to the Original Work (but excluding combinations of the
+Original Work with other software or hardware).
+
+11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this
+License may be brought only in the courts of a jurisdiction wherein the
+Licensor resides or in which Licensor conducts its primary business, and under
+the laws of that jurisdiction excluding its conflict-of-law provisions. The
+application of the United Nations Convention on Contracts for the
+International Sale of Goods is expressly excluded. Any use of the Original
+Work outside the scope of this License or after its termination shall be
+subject to the requirements and penalties of the U.S. Copyright Act, 17 U.S.C.
+101 et seq., the equivalent laws of other countries, and international treaty.
+This section shall survive the termination of this License.
+
+12) Attorneys Fees. In any action to enforce the terms of this License or
+seeking damages relating thereto, the prevailing party shall be entitled to
+recover its costs and expenses, including, without limitation, reasonable
+attorneys' fees and costs incurred in connection with such action, including
+any appeal of such action. This section shall survive the termination of this
+License.
+
+13) Miscellaneous. This License represents the complete agreement concerning
+the subject matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent necessary
+to make it enforceable.
+
+14) Definition of "You" in This License. "You" throughout this License,
+whether in upper or lower case, means an individual or a legal entity
+exercising rights under, and complying with all of the terms of, this License.
+For legal entities, "You" includes any entity that controls, is controlled by,
+or is under common control with you. For purposes of this definition,
+"control" means (i) the power, direct or indirect, to cause the direction or
+management of such entity, whether by contract or otherwise, or (ii) ownership
+of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial
+ownership of such entity.
+
+15) Right to Use. You may use the Original Work in all ways not otherwise
+restricted or conditioned by this License or by law, and Licensor promises not
+to interfere with or be responsible for such uses by You.
+
+This license is Copyright (C) 2003 Lawrence E. Rosen. All rights reserved.
+Permission is hereby granted to copy and distribute this license without
+modification. This license may not be modified without the express written
+permission of its copyright owner.
diff --git a/doc/TODO b/doc/TODO
new file mode 100644
index 0000000..fe7cca6
--- /dev/null
+++ b/doc/TODO
@@ -0,0 +1,43 @@
+
+
+package TODO
+* more documentation (client manpages, basic api, etc.)
+* allow installer to detect python location
+	it is a possibility to have special installers for non-unix platforms
+
+msn client TODO
+* be able to use nick with spaces
+	this can cause a lot of damage, is it worthy?
+* line editing
+* make tab completion work with any command besides 'm'
+* handle SIGWINCH for terminal resizes if signals are available
+* colours for the user list
+
+
+Future / In doubt
+-----------------
+(things listed here are either marked to do in some future (because we have to
+wait on some feature becoming popular) or are in doubt of ever being
+implemented at all)
+
+msn client
+* signal handling
+	when the signal module becames popular on distros, we can get rid of
+	the select() ugly stuff for auto-away and re-implement it using
+	alarm() or things like that; add support for SIGWINCH, send syncs
+	often, etc.
+
+
+msn lib 
+* file transfer
+	this is waaaaay below in my priority lists. there are thousand of
+	better ways to do file transfer between two hosts; plus the protocol
+	is even more ugly than the messaging one (yes, it really is)
+* micq integration
+	in a perfect world, this wouldn't even exist and we would have micq
+	talk the msn protocol. so the ideal solution would be to rewrite all
+	this in C and integrate it with micq; probably working in two
+	different levels: one would be the C implementation of this, and the
+	other one micq multiprotocol support, that allows a clean integration
+	with it
+
diff --git a/doc/URL b/doc/URL
new file mode 100644
index 0000000..9527ce1
--- /dev/null
+++ b/doc/URL
@@ -0,0 +1,10 @@
+
+The URL for the project is http://users.auriga.wearlab.de/~alb/msnlib
+I want to thank Henne Vogelsang and Auriga for providing the hosting.
+
+There is also a freshmeat project page that it's easier to remember:
+http://freshmeat.net/projects/msnlib
+
+And finally, there's the mailing list, msnlib-devel@auriga.wearlab.de, with url
+http://lists.auriga.wearlab.de/cgi-bin/mailman/listinfo/msnlib-devel.
+
diff --git a/doc/commands b/doc/commands
new file mode 100644
index 0000000..e5afc64
--- /dev/null
+++ b/doc/commands
@@ -0,0 +1,119 @@
+
+This is a more detailed explanation of the commands provided in the client.
+A brief list is available at runtime with the 'help' command.
+
+Note that the runtime help gets updated more frequently than this one.
+
+
+status [mode]	
+	Shows the current status, or changes it to "mode", which can be one of:
+	online, away, busy, brb, phone, lunch, invisible or idle.
+
+q
+	Quits the program.
+
+w
+	Prints your entire contact list.
+	Contacts connected are highlighted in bold.
+
+ww
+	Prints your entire contact list, including email addresses.
+
+e
+	Prints your online contacts.
+
+ee
+	Prints your online contacts, including email addresses.
+
+eg
+	Prints your online contacts with the groups
+	
+wr
+	Prints your reverse contact list.
+	(see the FAQ for more details)
+
+h
+	Shows your incoming message history
+	This is a list of the last N messages that you received. The amount is
+	configurable with the directive "input history size" in the msnrc
+	file, which defaults to 10.
+
+add email nick
+	Adds the user "email" with the nick "nick".
+
+del nick
+	Deletes the user with nick "nick".
+
+info [nick]
+	Shows the user information and pending messages (if any), or our
+	personal info. It also includes some advanced information (like the
+	server socket or tid).
+
+lignore [nick]
+	Locally ignores the user, or display the locally ignored users list.
+	This makes the messages coming from the user not to be displayed, but
+	they are still logged so you can see them later.
+
+lunignore nick
+	Removes a user from the locally ignored users list.
+
+block nick
+	Blocks a user
+
+unblock nick
+	Unblocks a blocked user
+
+g
+	Shows the group list
+
+gadd gname
+	Adds the group "gname"
+
+gdel gname
+	Deletes the group "gname". Note that all the users in the group will
+	be deleted too.
+
+gren old new
+	Renames the group "old" with the name "new"
+
+color [theme]
+	Shows the available color themes, or set the color theme to "theme".
+
+close nick
+	Closes the switchboard connection with "nick". There is no need to use
+	this command and is included just for advanced users and debugging.
+
+config
+	Shows the configuration, in the internal representation, along with
+	other special automatic variables. This means that many commands and
+	values won't have the same format; for instance, instead of 'yes' you
+	will often see a 1. This command is mostly exclusively included for
+	bug reporting.
+
+nick newnick
+	Changes your nick to "newnick".
+
+privacy p a
+	Sets whether accept messages from people not on your list (p) and
+	require authorization (a).
+	(see the FAQ for more details)
+
+m nick text
+	Sends a message to "nick" with the "text". The standard and most
+	practical way of sending a message. Tab completion expands to this.
+
+a text
+	Sends a message with "text" to the last person you sent a message to.
+
+r text
+	Sends a message with "text" to the last person that sent you a
+	message.
+
+invite u1 to u2
+	Invites u1 into the chat with u2
+
+
+In most cases, where you are asked for a nick, you can alternatively enter the email.
+This makes it easier to handle people with weird or long nicks.
+
+
diff --git a/doc/dependencies b/doc/dependencies
new file mode 100644
index 0000000..00db0df
--- /dev/null
+++ b/doc/dependencies
@@ -0,0 +1,6 @@
+
+The only thing you need is a working python installation with SSL support
+(most of them have it by default).
+
+Since release 3.0, it requires at least Python 2.2.2 because of SSL support.
+
diff --git a/doc/icons b/doc/icons
new file mode 100644
index 0000000..c2fee09
--- /dev/null
+++ b/doc/icons
@@ -0,0 +1,46 @@
+(#)	sun
+(%)	handcuffs
+(&)	dog
+(*)	star
+(0)	clock, same as (O)
+(6)	devil
+(8)	musical note
+(@)	cat
+(A)	angel
+(B)	beer
+(C)	cup of something
+(E)	envelope/email
+(F)	rose
+(G)	gift
+(H)	smile with sunglasses
+(I)	light bulb
+(K)	kiss/lips
+(L)	heart
+(M)	msn icon
+(N)	thumb down
+(O)	clock, same as (0)
+(P)	photo camera
+(R)	rainbow
+(S)	moon
+(T)	phone
+(U)	broken heart
+(W)	fallen rose
+(X)	woman
+(Y)	thumb up
+(Z)	man
+(^)	cake
+({)	man hug
+(})	woman hug
+(~)	film
+:$	blushed
+:'(	crying
+:(	sad
+:)	normal smile
+:-O	surprise
+:@	angry
+:D	wide smile
+:P	tongue
+:S	confused
+:|	surprised, astonished
+;)	wink
+
diff --git a/doc/log_format b/doc/log_format
new file mode 100644
index 0000000..61ee96a
--- /dev/null
+++ b/doc/log_format
@@ -0,0 +1,44 @@
+
+This document describes the msn client log format.
+
+The files are named only with the email address of the sender/receiver; except
+for the multi-user chats (that is, a chat with more than you and somebody else
+involved) where the name is composed of 'M::' and then the list of
+participants (excluding yourself) sorted in alphabetical order, separated by
+commas (',').
+
+
+The format is very simple:
+
+Day/Month/Year HH:MM:SS email ID text
+
+where ID is one of:
+	'>>>' if the text is an outgoing message
+	'<<<' if the text is an incoming message
+	'***' if the text refers to a new status
+	'+++' if the text refers to multi-user chats (joining and leaving)
+
+and email is the email of the user you send the message to/got a message from,
+except on the multi-user chat case, where for messages you sent your own
+email address is displayed.
+
+
+For example:
+Day/Month/Year HH:MM:SS email *** online
+Day/Month/Year HH:MM:SS email <<< incoming message
+Day/Month/Year HH:MM:SS email >>> outgoing message
+Day/Month/Year HH:MM:SS email *** offline
+Day/Month/Year HH:MM:SS email +++ join
+
+
+For multi-line messages, the message is indented with a tab, like:
+Day/Month/Year HH:MM:SS email <<<
+	line1
+	line2
+	...
+	linen
+
+
+In the utils directory you can find the file "msnlog.vim", which is a vim
+syntax highlighting file for this log format.
+
diff --git a/doc/msg_states b/doc/msg_states
new file mode 100644
index 0000000..25124fe
--- /dev/null
+++ b/doc/msg_states
@@ -0,0 +1,47 @@
+
+
+The graph below is an attempt of the simple state diagram that rules message
+delivery.
+
+It's split in a half according to whether you are inviting or answering; and
+on the sides are the different sbf's state.
+
+The middle zone (for both types) is the message type with an arrow indicatiing
+the destination (left is us, right is the server); except for the only two
+two-letter codes right in the middle, which show the common state to both
+sides.
+
+Once the 'es' state is reached, the difference is forgotten (even tho it's
+kept, it's never used) and both types of sbf are handled the same way.
+
+
+------------------------+------------------------
+     invite		|	    answer	 
+------------------------+------------------------ 
+state	|		|		|  state 
+------------------------+------------------------
+    ----+	xfr->			
+    xf	|	  |			
+    ----+	<-xfr		<-rng	+----
+    cp	|	     \	     /		|  cp
+   	|	       \   /		|
+    ----+		cp		+----
+   	|	      /	   \		|
+    re	|	    /	     \		|  re
+   	|	  /	       \	|
+    ----+	usr->		ans->	+----
+   	|	  |		  |	|  
+    us	|	<-usr		<-iro	|  an
+   	|	  |		  |	|
+    ----+	cal->		<-ans	+----
+    ca	|	  |		  |	|
+    ----+	<-cal		 /	|
+    jo	|	  |	       /	|  es
+    ----+	<-joi	     /		|
+    es	|	      \    /		|
+   	V		es		V
+
+
+
+
+	
diff --git a/doc/msncd_protocol b/doc/msncd_protocol
new file mode 100644
index 0000000..7fddb93
--- /dev/null
+++ b/doc/msncd_protocol
@@ -0,0 +1,128 @@
+This is a description of the protocol used by msncd.
+
+It is still in an experimental stage as only few people dared to try it; if
+you do please let me know by sending a message to the mailing list.
+
+
+Add user:
+->	ADD nick email\n
+
+Delete user:
+->	DEL email\n
+
+Change our nick:
+->	NICK newnick\n
+
+Change the privacy behaviour:
+->	PRIV p a\n
+
+Change our status:
+->	STATUS newstatus\n
+
+Log in:
+->	LOGIN email password\n
+
+Log off:
+->	LOGOFF\n
+
+Info requests:
+->	INFO email\n
+<-	AttributeA = ValueA\n
+<-	AttributeB = ValueB\n
+<-	...
+<-	\n
+
+Send message:
+->	SENDMSG number_of_lines email\n
+->	line1\n
+->	line2\n
+->	...
+->	lineN\n
+
+Poll for events:
+->	POLL\n
+	(begin to send first message)
+<-	MSG number_of_lines end_of_header src_email\n
+<-	line1\n
+<-	line2\n
+<-	...
+<-	lineN\n
+	(and repeat for number_of_messages)
+	(then send status changes)
+<-	STCH email newstatus [number_of_messages_discarded]\n
+	(a user has added us)
+<-	UADD email\n
+	(a user has deleted us)
+<-	UDEL email\n
+	(a user was added)
+<-	ADDFL email\n
+	(a user was deleted)
+<-	DELFL email\n
+	(flushed messages)
+	MFLUSH email\n
+	(typing notifications)
+	TYPING email\n
+	(a socket was closed, type is either 'MAIN' or 'USER')
+	SCLOSE type [email number_of_messages_discarded]\n
+	(unexpected errors)
+<-	ERR code description\n
+	(finally, end the poll)
+<-	POLLEND\n
+
+Get contact list:
+->	GETCL\n
+<-	CL number_of_contacts\n
+<-	status1 email1 nick1\n
+<-	status2 email2 nick2\n
+<-	...
+<-	statusN emailN nickN\n
+
+Get reverse contact list:
+->	GETRCL\n
+	(same behaviour as GETCL)
+
+
+And I think that'd be pretty much it. Anyway, it's easily extensible.
+
+Note that:
+ * In most places, email can be exchanged with the url-encoded nick. The
+	server _always_ replies using the email
+ * The 'server' (that is, the real client, that reads from the pipe) responses
+	an 'OK\n' for most commands, or 'ERR description\n'; here they are
+	ommited for brevity.
+ * If considered necesary, a timestamp could be sent before some responses to
+	indicate time. I'm not sure about this, because polling often (like
+	250ms) has enough granularity and it doesn't represent any load, so we
+	could just avoid the overhead. Even with 1s poll time, there are no
+	problems regarding times.
+ * New pollable stuff could also be added later (files, for instance). Poll
+	responses can come in any order.
+ * This is syncronous and events get queued on the server side. Server _never_
+	issues a pipe write without a request, that's what we have POLL for.
+	This avoid a huge load of races, and the client code to avoid them.
+ * Unexpected errors also come out from POLL, specially the network ones that
+	tend to be quite async to everything else.
+
+
+So now, we start offline and then, on 'LOGIN' we connect and after we log in,
+the 'OK' response is sent. Then the client changes the status to whatever he
+wants, for instance with 'STATUS away'.
+
+The client will mostly like do a GETCL and GETRCL after that, and then start
+polling for events, which now becomes the common path. This is very efficient
+when no new events are pending (again, the common case), just:
+->	POLL\n
+<-	POLLEND\n
+
+Disconnect is as simple as 'LOGOFF'. Note that this is not the same as
+'STATUS offline', as the former closes everything and returns to the initial
+state.
+
+Much simpler than everything else (remember that most other stablished
+protocols require to have a library, a python binding and so on), it can be
+implemented quite fast in any language, and it's simple and efficient.
+
+No connection or user tracking is required on the client, no state machines,
+nothing. Just a frontend for a text protocol.
+
+
diff --git a/doc/portability b/doc/portability
new file mode 100644
index 0000000..cd17896
--- /dev/null
+++ b/doc/portability
@@ -0,0 +1,39 @@
+
+The library itself should be portable, as it doesn't contain any specific
+stuff that might have problems, and it has been reported to work under
+different unixes and even under windows.
+
+I tend to code based on posix/sus, but I think it's pretty much generic python
+both the library and the client, specially the former.  I'm almost sure it
+will run unmodified on unix platforms (and the only reason i say 'almost' is
+because i didn't test it myself, but it certanly should).
+
+
+About the text mode client the only thing that is tied to a unix environment
+is the client terminal handling (which requires termios and fcntl modules),
+but it's isolated and has runtime detection, so if you don't have any of these
+modules, or they fail for some reason, the client will fall back to the normal
+behaviour. Also, doing select() on stdin isn't ok for some platforms (windows
+being the most popular one), but it's really safe for unix.
+
+Another thing that might be conflictive for non-unix platforms is that I
+assume the python interpreter is callable using "/usr/bin/env python"; these
+are the closest thing to a standard location on unix boxes. If you need to
+change this, the places are the first line of 'msn', and somewhere inside
+'install'.
+
+The next possible problem (always talking about non-unix platforms) is
+'msnsetup' and the configuration file location; the first one requires bash,
+so if you don't have it, you can just create your own msnrc file based on
+'msnrc.sample'; but the location is assumed to be $HOME/.msn/msnrc, and maybe
+you don't have '$HOME' (or you don't even have environment variables at all),
+in this case you specify the location on the command line, as the first and
+only argument to msn: "msn /path/to/msnrc".
+
+
+If you run it under a different platform, please let me know; specially if you
+had (or have) any problems.
+
+Thanks,
+		Alberto
+
diff --git a/doc/profiles b/doc/profiles
new file mode 100644
index 0000000..40e37c1
--- /dev/null
+++ b/doc/profiles
@@ -0,0 +1,20 @@
+
+The msn client, from release D5, supports different profiles.
+
+What this means is that now you can have several configurations under the same
+unix user in a very simple way.
+
+Just create a profile like you normally create your configuration, telling the
+setup the profile name, like:
+
+msnsetup profilename
+
+
+Then, to run msn under an specific profile, do:
+
+msn profilename
+
+and you're done.
+
+
+
diff --git a/doc/reporting_bugs b/doc/reporting_bugs
new file mode 100644
index 0000000..8a4f8a0
--- /dev/null
+++ b/doc/reporting_bugs
@@ -0,0 +1,38 @@
+
+How to report bugs
+------------------
+
+If you think you've got a bug (or you are sure about it =), please report it
+to msnlib-devel@auriga.wearlab.de; specifying:
+
+* msnlib version
+* python version
+* operating system information
+* platform information
+* obviously, a bug description
+
+Now, depending on what's the bug about, i'd like you to to include some of
+these:
+
+* a copy of the output when the bug hits (see below for a complete to do this)
+* reports on other versions (does this happens with an older version?)
+* instructions on how to reproduce it
+* terminal information (ie. running under normal console, some kind of xterm,
+	cygwin terminal, beos one, etc.)
+* an strace -tt of the session
+* the debug output
+
+On these last two, they're oftenly quite useful. Just in case you don't know
+how to do it, a nice way of doing both at the same time and save to files
+(which then you can send to me as attachments) is:
+
+* first, add 'debug = yes' to $HOME/.msn/msnrc; that enables debugging output
+* then, run the command:
+	strace -tt -o msn_output-strace msn 2>msn_output-debug | tee msn_output-stdout
+	
+and try to reproduce the bug, or just wait for it to happen =)
+
+Then, when reporting, send me these 3 files (msn_output-strace,
+msn_output-debug, and msn_output-stdout), compressed if necesary. They are
+really helpful, because I can see what's going on when the bug happened.
+
diff --git a/install b/install
new file mode 100644
index 0000000..93260e8
--- /dev/null
+++ b/install
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+# the first argument is the destination root directory
+# for the package, defaults to /usr/local if not present
+
+DESTDIR="/usr/local"
+if [ ".$1" != "." ]; then
+	DESTDIR="$1"
+fi
+
+echo "*** Installing the library"
+/usr/bin/env python setup.py install
+echo
+
+echo "*** Installing the documentation"
+rm -r $DESTDIR/doc/msnlib 2>/dev/null
+mkdir $DESTDIR/doc/msnlib 2>/dev/null
+cp -v README INSTALL $DESTDIR/doc/msnlib/
+cp -Rv doc/* $DESTDIR/doc/msnlib/
+echo
+
+echo "*** Installing the client"
+mkdir $DESTDIR/bin 2>/dev/null
+cp -v msn $DESTDIR/bin
+cp -v msnsetup $DESTDIR/bin
+echo
+
+echo "*** Done"
+echo "Please read the INSTALL file to see the next step"
+echo
+
diff --git a/msn b/msn
new file mode 100644
index 0000000..1070f80
--- /dev/null
+++ b/msn
@@ -0,0 +1,1623 @@
+#!/usr/bin/env python
+
+
+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-based code.
+For further information refer to the documentation or the source (which is
+always preferred).
+Please direct any comments to the msnlib mailing list,
+msnlib-devel@auriga.wearlab.de.
+You can find more information, and the package itself, at
+http://users.auriga.wearlab.de/~alb/msnlib
+"""
+
+
+#
+# constant strings
+#
+
+help = """\
+Command list:
+status [mode]	Shows the current status, or changes it to "mode", which can
+		be one of: online away busy brb phone lunch invisible idle
+q		Quits the program
+w		Prints your entire contact list
+ww		Prints your entire contact list, including email addresses
+e		Prints your online contacts
+ee		Prints your online contacts, including email addresses
+eg		Prints your online contacts with the groups
+wr		Prints your reverse contact list
+h		Shows your incoming message history
+add e [n] [g] 	Adds the user "e" with the nick "n" to the group "g"
+del nick	Deletes the user with nick "nick"
+ren nick new	Renames the user with nick "nick" to appear as "new"
+lignore [nick]	Locally ignores the user, or display the locally ignored users
+lunignore nick	Removes a user from the locally ignored users list
+block nick	Blocks a user
+unblock nick	Unblocks a blocked user
+g		Shows the group list
+gadd gname	Adds the group "gname"
+gdel gname	Deletes the group "gname". Note that all the users in the
+		group will be deleted too.
+gren old new	Renames the group "old" with the name "new"
+color [theme]	Shows or set the color theme to "theme"
+close nick	Closes the switchboard connection with "nick"
+config		Shows the configuration
+info [nick]	Shows the user information and pending messages (if any), 
+		or our personal info
+nick newnick	Changes your nick to "newnick"
+privacy p a	Sets whether accept messages from people not on your list (p)
+		and require authorization (a)
+m nick text	Sends a message to "nick" with the "text"
+a text		Sends a message to the last person you sent a message to
+r text		Sends a message to the last person that sent you a message
+invite u1 to u2	Invites u1 into the chat with u2
+
+In most cases, where you are asked for a nick, you can alternatively enter the
+email.  This makes it easier to handle people with weird or long nicks.
+"""
+
+
+#
+# colors, for nice output
+#
+
+class color_default:
+	def __init__(self):
+		self.name =	'default'
+		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'
+
+class color_bw:
+	def __init__(self):
+		self.name =	'bw'
+		self.black =	'\x1b[0;30m'
+		self.red =	'\x1b[0m'
+		self.green =	'\x1b[0m'
+		self.yellow =	'\x1b[0m'
+		self.blue =	'\x1b[0m'
+		self.magenta =	'\x1b[0m'
+		self.cyan =	'\x1b[0m'
+		self.white =	'\x1b[0m'
+		self.normal =	'\x1b[0m'
+		self.bold =	'\x1b[0m'
+		self.clear =	'\x1b[J'
+
+color_classes = {
+	'default':	color_default,
+	'bw':		color_bw
+}
+c = color_classes['default']()
+
+
+#
+# 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
+	safe_write(out)
+	safe_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'
+	safe_write(out)
+	safe_flush()
+
+def pexc(line):
+	"Prints an exception"
+	out = '\n'
+	out += ( c.cyan + c.bold + '!' + c.normal ) * 3
+	safe_write(out)
+	safe_write(c.bold + line)
+	safe_flush()
+	traceback.print_exc()
+	safe_write(c.normal)
+	safe_write('\n')
+	safe_flush()
+
+def print_list(md, only_online = 0, 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': 
+			hl = 1
+		else:
+			if only_online:	continue
+			hl = 0
+		status = msnlib.reverse_status[u.status]
+		printl('%7.7s :: %s ' % (status, u.nick), bold = hl)
+		if include_emails:
+			printl('(%s) ' % (email), bold = hl)
+		if 'B' in u.lists:
+			printl('[!]')
+		printl('\n')
+
+def print_group_list(md):
+	"Prints the group list"
+	gids = md.groups.keys()
+	gids.sort()
+	for gid in gids:
+		printl('%3.3s :: %s\n' % (gid, md.groups[gid]))
+
+def print_grouped_list(md, only_online = 0, include_emails = 0):
+	db = {}
+	for gid in md.groups.keys():
+		db[gid] = []
+	for gid in md.groups.keys():
+		for e in md.users.keys():
+			if md.users[e].gid == gid:
+				db[gid].append(e)
+	gids = db.keys()
+	gids.sort()
+	for gid in gids:
+		printl(':: %s ::\n' % md.groups[gid], bold = 1)
+		ul = db[gid]
+		ul.sort()
+		for email in ul:
+			u = m.users[email]
+			if u.status != 'FLN':
+				hl = 1
+			else:
+				if only_online: continue
+				hl = 0
+			status = msnlib.reverse_status[u.status]
+			printl('\t%7.7s :: %s ' % (status, u.nick), bold = hl)
+			if include_emails:
+				printl('(%s) ' % (email), bold = hl)
+			if 'B' in u.lists:
+				printl('[!]')
+			printl('\n')
+			
+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 'B' in u.lists:
+		out += c.bold + 'Mode:\t\t' + c.normal + 'blocked' + '\n'
+	if u.gid != None:
+		out += c.bold + 'Group:\t\t' + c.normal + m.groups[u.gid] + '\n'
+	if u.realnick:
+		out += c.bold + 'Real Nick:\t' + c.normal + u.realnick + '\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"
+	safe_write('\r' + c.red + c.bold + '[msn] ' + c.normal)
+	safe_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 email in ignored:
+		return
+	if ptime:
+		if recvtime:
+			ctime = time.strftime('%I:%M:%S%p', time.localtime(recvtime))
+		else:
+			ctime = time.strftime('%I:%M:%S%p', now())
+		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', now())
+	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')
+	
+
+def safe_flush():
+	"""Safely flushes stdout. It fixes a strange issue with flush and
+	nonblocking io, when flushing too fast."""
+	c = 0
+	while c < 100:
+		try:
+			sys.stdout.flush()
+			return
+		except IOError:
+			c +=1
+			time.sleep(0.01 * c)
+	raise Exception, 'flushed too many times, giving up. Please report!'
+
+def safe_write(text):
+	"""Safely writes to stdout. It fixes the same issue that safe_flush,
+	that is, writing too fast raises errors due to nonblocking fd."""
+	c = 1
+	while c:
+		try:
+			sys.stdout.write(text)
+			return
+		except IOError:
+			c += 1
+			time.sleep(0.01 * c)
+	raise Exception, 'wrote too many times, giving up. Please report!'
+
+
+#
+# useful functions
+#
+
+def quit(code = 0):
+	"Exits"
+	printl('Closing\n', c.green, 1)
+	try:
+		try:
+			m.disconnect()
+		except:
+			pass
+		global oldtermattr
+		termios.tcsetattr(sys.stdin.fileno(), termios.TCSAFLUSH, oldtermattr)
+	except:
+		pass
+	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 matchemail(begin):
+	""""Returns a matching email/nick for the given beginning; it avoids
+	nicks with spaces"""
+	l = len(begin)
+	# first try the ones with sbd
+	for email in m.users.keys():
+		if not m.users[email].sbd:
+			continue
+		nick = m.users[email].nick
+		if ' ' in nick:
+			nick = email
+		if len(nick) >= l and nick[0:l] == begin:
+			return nick
+	# then the nicks
+	for email in m.users.keys():
+		nick = m.users[email].nick
+		if ' ' in nick:
+			continue
+		if len(nick) >= l and nick[0:l] == begin:
+			return nick
+	# and finally the emails
+	for email in m.users.keys():
+		if len(email) >= l and email[0:l] == begin:
+			return email
+	return None
+
+def gname2gid(gname):
+	"Returns a group name according to the given group id"
+	for gid in m.groups.keys():
+		if m.groups[gid] == gname:
+			return gid
+	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, users = []):
+	"""Logs the message or event of the 'type', related to 'email',
+	with the content 'msg', to a file in the specified directory.  See
+	documentation for more specific details, specially about
+	formatting."""
+	
+	if not config['log history']:
+		return
+	
+	if config['profile']:
+		prepend = config['profile'] + '::'
+	else:
+		prepend = ''
+	if users:
+		# copy and sort the user list, so we log always to the same
+		# file regarding the order the users were joined
+		# FIXME: sometimes we crash because filename is too long
+		usorted = users[:]
+		usorted.sort()
+		file = config['history directory'] + '/' + prepend + 'M::'
+		file += string.join(usorted, ',')
+	else:
+		file = config['history directory'] + '/' + prepend + 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'
+	elif type == 'multi':
+		out += '+++ ' + msg + '\n'
+	
+	fd = open(file, 'a')
+	fd.write(out)
+	fd.close()
+	del(fd)
+	return
+
+def now():
+	"Returns the current time, in tuple format"
+	return time.localtime(time.time())
+
+
+#
+# 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:
+		if char == '\r':
+			# replace \r with \n, so we handle mac keyboard input
+			# properly (it breaks \r\n tho, but nobody uses it)
+			char = '\n'
+		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
+			
+			safe_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 ord(char) == 21:				# ^U
+			inbuf = ''
+			redraw_cli()
+			
+		elif char == '\t':				# tab
+			# we do a basic cycling between the last received and
+			# last sent; first we build the two strings and then
+			# we see which one applies according to some messy
+			# logic
+			if email2nick(last_received):
+				nick = email2nick(last_received)
+				if ' ' in nick:
+					nick = last_received
+				mtolrecv = 'm ' + nick + ' '
+			else:
+				mtolrecv = None
+
+			if email2nick(last_sent):
+				nick = email2nick(last_sent)
+				if ' ' in nick:
+					nick = last_sent
+				mtolsent = 'm ' + nick + ' '
+			else:
+				mtolsent = None
+			
+			if len(inbuf) == 1:
+				if mtolsent:
+					inbuf = mtolsent
+				elif mtolrecv:
+					inbuf = mtolrecv
+				else:
+					inbuf = inbuf[:-1]
+					beep()
+			else:
+				# if we have mtolsent, replace with mtolrecv
+				# (if possible, otherwise beep)
+				if mtolsent and inbuf.strip() == mtolsent.strip():
+					if mtolrecv:
+						inbuf = mtolrecv
+					else:
+						inbuf = inbuf[:-1]
+						beep()
+				# the opposite case
+				elif mtolrecv and inbuf.strip() == mtolrecv.strip():
+					if mtolsent:
+						inbuf = mtolsent
+					else:
+						inbuf = inbuf[:-1]
+						beep()
+				# we have something that is neither mtolsent or
+				# mtolrecv, we try to find a matching
+				# email/nick
+				else:
+					p = inbuf.split()
+					if len(p) < 2:
+						# space + TAB or equivalent,
+						# just beep and ignore it
+						inbuf = inbuf[:-1]
+						beep()
+					elif p[0] != 'm' or len(p) != 2:
+						inbuf = inbuf[:-1]
+						beep()
+					else:
+						begin = p[1]
+						email = matchemail(begin)
+						if not email:
+							inbuf = inbuf[:-1]
+							beep()
+						else:
+							inbuf = 'm ' + email + ' '
+			redraw_cli()
+			
+		elif ord(char) == 4:				# EOT
+			safe_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
+			msnlib.debug('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
+				inbuf = inbuf[:-1]
+			elif in_esc == 2:
+				if char == 'A':			# up
+					if inbuf_history[1] == -1:
+						# hit the top, or it's empty;
+						# remove it from the buffer
+						inbuf = inbuf[:-1]
+					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]:
+						# it's empty, so we only
+						# remove it from the buffer
+						inbuf = inbuf[:-1]
+					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()
+				else:				# unhandled esc
+					inbuf = inbuf[:-1]
+				in_esc = 0
+
+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
+	safe_write(inbuf[-lenght:])
+	safe_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:
+		safe_write(char)
+		safe_flush()
+
+def clear_line():
+	"""Clears the current line by overwriting it with spaces."""
+	global inbuf, screenwidth
+	if use_termios:
+		safe_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()
+	orig_cmd = cmd
+	s = cmd.split()
+	if len(s) > 1:
+		cmd = s[0]
+		# recover original params to preserve whitespace
+		# use as index the first parameter to the command
+		params = orig_cmd[orig_cmd.find(s[1]):]
+	else:
+		if not cmd: return ''
+		cmd = s[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):
+			out = 'Status must be one of:\n'
+			out += '\tonline, away, busy, brb, phone, lunch, invisible or idle'
+			return out
+		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_grouped_list(m)
+	
+	elif cmd == 'ww':		# list, include emails
+		print_grouped_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 == 'eg':
+		print_grouped_list(m, only_online = 1)
+
+	elif cmd == 'ee':
+		print_grouped_list(m, only_online = 1, include_emails = 1)
+	
+	elif cmd == 'g':		# list groups
+		print_group_list(m)
+
+	elif cmd == 'raw':		# send a raw message
+		try:
+			cmd = params[0:3]
+			pars = params[4:]
+		except:
+			return 'Error parsing command'
+		m._send(cmd, pars)
+	
+	elif cmd == 'debug':		# enable/disable debugging
+		p = params.split()
+		if len(p) != 1:
+			return 'Error parsing command'
+		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')
+		printl(c.bold + 'use_termios = ' + str(use_termios) + '\n')
+		printl(c.bold + 'screensize = ' + str(winsize) + '\n')
+	
+	elif cmd == 'color':		# configure/show colors
+		p = params.split()
+		if len(p) != 1:
+			printl(c.bold + "Currently using theme " + c.name + '\n')
+			printl(c.bold + "Available themes:\n")
+			for i in color_classes.keys():
+				printl(c.bold + "\t* " + i + '\n')
+		elif p[0] not in color_classes.keys():
+			return "The specified theme is not available"
+		else:
+			c = color_classes[p[0]]()
+			return "Changed theme to " + p[0]
+	
+	elif cmd == 'close':		# close a connection
+		p = params.split()
+		if len(p) != 1: 
+			return 'Error parsing command'
+		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 command'
+		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 == 'lignore':		# ignore a user locally
+		p = params.split()
+		if len(p) == 0:
+			printl(c.bold + 'Locally ignored users\n')
+			for e in ignored:
+				printl(email2nick(e) + ' (' + e + ')\n')
+			return ''
+		email = nick2email(p[0])
+		if not email:
+			return 'Unknown nick (%s)' % p[0]
+		if email in ignored: 
+			return 'User is already being locally ignored'
+		ignored.append(email)
+		return 'User is now being locally ignored'
+	
+	elif cmd == 'lunignore':	# unignore a locally ignored user
+		p = params.split()
+		if len(p) == 0:
+			return 'Error parsing command'
+		email = nick2email(p[0])
+		if email not in ignored:
+			return 'User is not being locally ignored' 
+		ignored.remove(email)
+		return 'User is no longer locally ignored'
+	
+	elif cmd == 'block':
+		p = params.split()
+		if len(p) == 0:
+			return 'Error parsing command'
+		email = nick2email(p[0])
+		if not email:
+			return 'Unknown nick (%s)' % p[0]
+		m.userblock(email)
+		return 'User %s blocked' % email
+	
+	elif cmd == 'unblock':
+		p = params.split()
+		if len(p) == 0:
+			return 'Error parsing command'
+		email = nick2email(p[0])
+		if not email:
+			return 'Unknown nick (%s)' % p[0]
+		m.userunblock(email)
+		return 'User %s unblocked' % email
+	
+	elif cmd == 'add':		# add a user
+		p = params.split()
+		if   len(p) == 0: 
+			return 'Error parsing command'
+		elif len(p) == 1: 
+			email = nick = p[0]
+			gid = '0'
+		elif len(p) == 2: 
+			email = p[0]
+			nick = p[1]
+			gid = '0'
+		else:
+			email = p[0]
+			nick = p[1]
+			group = p[2]
+			gid = gname2gid(group)
+			if not gid: gid = group
+			if gid not in m.groups.keys():
+				return 'Unknown group'
+		m.useradd(email, nick, gid)
+	
+	elif cmd == 'del':		# delete a user
+		p = params.split()
+		if len(p) != 1: return 'Error parsing command'
+		email = nick2email(p[0])
+		if not email:
+			return 'Unknown nick (%s)' % p[0]
+		m.userdel(email)
+	
+	elif cmd == 'ren':		# rename a user
+		p = params.split()
+		if len(p) != 2: return 'Error parsing command'
+		email = nick2email(p[0])
+		if not email:
+			return 'Unkown nick (%s)' % p[0]
+		newnick = p[1]
+		u = m.users[email]
+		m.userdel(email)
+		m.useradd(email, newnick, u.gid)
+	
+	elif cmd == 'gadd':		# add a group
+		p = params.split()
+		if len(p) != 1: return 'Error parsing command'
+		m.groupadd(p[0])
+	
+	elif cmd == 'gdel':		# delete a group
+		p = params.split()
+		if len(p) != 1: return 'Error parsing command'
+		gname = p[0]
+		gid = gname2gid(gname)
+		if not gid: gid = gname
+		if gid not in m.groups.keys():
+			return 'Unknown group'
+		for e in m.users.keys():
+			u = m.users[e]
+			if u.gid == gid:
+				printl('User %s (%s) will be deleted\n' % \
+					(u.nick, e), bold = 1)
+		m.groupdel(gid)
+	
+	elif cmd == 'gren':		# rename a group
+		p = params.split()
+		if len(p) != 2: return 'Error parsing command'
+		newname = p[1]
+		origname = p[0]
+		gid = gname2gid(origname)
+		if not gid: gid = origname
+		if gid not in m.groups.keys():
+			return 'Unknown group'
+		m.groupren(gid, newname)
+	
+	elif cmd == 'invite':		# invite a user to an existing sbd
+		p = params.split()
+		if len(p) != 3: return 'Error parsing command'
+		if p[1] != 'to': return 'Error parsing command'
+		email = nick2email(p[0])
+		if not email: email = p[0]
+		dst = nick2email(p[2])
+		if not dst: dst = p[2]
+		for i in (email, dst):
+			if i not in m.users.keys():
+				return 'User %s unknown' % i
+		dst_sbd = m.users[dst].sbd
+		if not dst_sbd:
+			return 'No current chat with user %s' % dst
+		m.invite(email, dst_sbd)
+			
+	elif cmd == 'nick':		# change our nick
+		if len(params) < 1: return 'Error parsing command'
+		nick = params
+		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)
+			# begin the message content after the nick
+			begin = len(nick + ' ')
+			msg = params[begin:]
+		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])
+		if m.users[email].status == 'FLN' and not m.users[email].sbd:
+			return 'Unable to send message: User is offline'
+		if (m.status == 'FLN' or m.status == 'HDN') and not m.users[email].sbd:
+			return 'Unable to send message: Not allowed when offline'
+			
+		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)
+			if len(m.users[email].sbd.emails) > 1:
+				log_msg(m.email, 'out', msg, \
+					users = m.users[email].sbd.emails)
+			else:
+				log_msg(email, 'out', msg)
+		elif r == -2:
+			return 'Message too big'
+		else:
+			return 'Error %d sending message' % r
+		
+	elif cmd == 'help' or cmd == '?':
+		return help
+	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', now())
+	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 = params.split(' ')
+	email = t[0]
+	nick = md.users[email].nick
+	ctime = time.strftime('%I:%M:%S%p', now())
+	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', now())
+	printl('\r%s ' % ctime, c.blue)
+	printl(nick, c.blue, 1)
+	printl(' disconnected\n', c.magenta)
+	u = m.users[email]
+	if u.sbd and u.sbd.msgqueue:
+		printl(c.bold + "The following messages for " + nick + " will be discarded:\n")
+		for msg in u.sbd.msgqueue:
+			printl(c.bold + '\t>>> ' + c.normal + msg + '\n')
+	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
+
+def cb_bye(md, type, tid, params, sbd):
+	email = tid
+	if email != sbd.emails[0]:
+		nick = email2nick(email)
+		if not nick: nick = email
+		first_nick = email2nick(sbd.emails[0])
+		if not first_nick: first_nick = sbd.emails[0]
+		printl('\rUser %s left the chat with %s\n' % (nick, first_nick), c.green, 1)
+		log_msg(email, 'multi', 'left', users = sbd.emails)
+	msncb.cb_bye(md, type, tid, params, sbd)
+m.cb.bye = cb_bye
+
+
+# message
+def cb_msg(md, type, tid, params, sbd):
+	global last_received
+	t = tid.split(' ')
+	email = t[0]
+	
+	# parse
+	lines = params.split('\n')
+	headers = {}
+	eoh = 0
+	for i in lines:
+		# end of headers
+		if i == '\r':
+			break
+		tv = i.split(':', 1)
+		type = tv[0]
+		value = tv[1].strip()
+		headers[type] = value
+		eoh += 1
+	eoh +=1
+	
+	# handle special hotmail messages
+	if email == 'Hotmail':
+		if not headers.has_key('Content-Type'):
+			return
+		hotmail_info = {}
+		
+		# parse the body
+		for i in lines:
+			i = i.strip()
+			if not i:
+				continue
+			tv = i.split(':', 1)
+			type = tv[0]
+			value = tv[1].strip()
+			hotmail_info[type] = value
+					
+		msnlib.debug(params)
+		if headers['Content-Type'] == 'text/x-msmsgsinitialemailnotification; charset=UTF-8':
+			newmsgs = int(hotmail_info['Inbox-Unread'])
+			if not newmsgs:
+				return
+			printl('\rYou have %s unread email(s)' % str(newmsgs) \
+				+ ' in your Hotmail account\n', c.green, 1)
+		elif headers['Content-Type'] == 'text/x-msmsgsemailnotification; charset=UTF-8':
+			from_name = hotmail_info['From']
+			from_addr = hotmail_info['From-Addr']
+			subject = hotmail_info['Subject']
+			printl('\rYou have just received an email in your' + \
+				' Hotmail account:\n', c.green, 1)
+			printl('\r\tFrom: %s (%s)\n' % (from_name, from_addr),
+				c.green, 1)
+			printl('\r\tSubject: %s\n' % subject, c.green, 1)
+		return
+	
+	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'] and email not in ignored:
+			printl('\r')
+			ctime = time.strftime('%I:%M:%S%p', now())
+			printl('%s ' % ctime, c.blue)
+			printl('%s' % nick, c.cyan, 1)
+			printl(' is typing\n', c.magenta)
+		m.users[email].priv['typing'] = time.time()
+	elif headers.has_key('Content-Type') and headers['Content-Type'] == 'text/x-clientcaps':
+		# ignore the x-clientcaps messages generated from gaim
+		pass
+	else:
+		# messages
+		m.users[email].priv['typing'] = 0
+		printl('\r')
+		print_inc_msg(email, lines, eoh)
+		if len(sbd.emails) > 1:
+			log_msg(email, 'in', string.join(lines[eoh:], '\n'), \
+				users = sbd.emails)
+		else:
+			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
+	nick = email2nick(email)
+	if not nick: nick = email
+	if sbd.emails and email != sbd.emails[0]:
+		first_nick = email2nick(sbd.emails[0])
+		if not first_nick: first_nick = sbd.emails[0]
+		printl('\rUser %s has joined the chat with %s\n' % \
+			(nick, first_nick), c.green, 1)
+		log_msg(email, 'multi', 'join', \
+			users = sbd.emails + [email])
+	elif len(sbd.msgqueue) > 0:
+		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
+
+def cb_iro(md, type, tid, params, sbd):
+	p = params.split(' ')
+	uid, ucount, email, realnick = p
+	nick = email2nick(email)
+	if not nick: nick = email
+	
+	if ucount == '1':
+		# do nothing if we only have one participant
+		pass
+	else:
+		first_nick = email2nick(sbd.emails[0])
+		if not first_nick: first_nick = sbd.emails[0]
+		# print a special message for the first user
+		if uid == '1':
+			printl('\rUser %s has invited us to a multi-user chat\n' % \
+				first_nick, c.green, 1)
+		else:
+			printl('\rUser %s has joined the chat with %s\n' % \
+				(nick, first_nick), c.green, 1)
+			log_msg(email, 'multi', 'join', \
+				users = sbd.emails + [email])
+	msncb.cb_iro(md, type, tid, params, sbd)
+m.cb.iro = cb_iro
+
+# 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
+
+def cb_adg(md, type, tid, params):
+	t = params.split(' ')
+	lver, name, gid = t[0:3]
+	name = urllib.unquote(name)
+	out = '\r' + c.magenta + 'Group '
+	out += c.blue + c.bold + '%s (%s)' % (name, gid) + c.clear
+	out += c.magenta + ' has been added\n'
+	printl(out)
+	msncb.cb_adg(md, type, tid, params)
+m.cb.adg = cb_adg
+
+def cb_rmg(md, type, tid, params):
+        t = params.split(' ')
+        lver, gid = t[0:2]
+	name = md.groups[gid]
+	out = '\r' + c.magenta + 'Group '
+	out += c.blue + c.bold + '%s (%s)' % (name, gid) + c.clear
+	out += c.magenta + ' has been removed\n'
+	printl(out)
+	msncb.cb_rmg(md, type, tid, params)
+m.cb.rmg = cb_rmg
+
+def cb_reg(md, type, tid, params):
+	t = params.split(' ')
+	gid = t[1]
+	origname = md.groups[gid]
+	origname = urllib.unquote(origname)
+	newname = t[2]
+	newname = urllib.unquote(newname)
+	out = '\r' + c.magenta + 'Group '
+	out += c.blue + c.bold + '%s (%s)' % (origname, gid) + c.clear
+	out += c.magenta + ' has been renamed to '
+	out += c.blue + c.bold + '%s' % newname + '\n'
+	printl(out)
+	msncb.cb_reg(md, type, tid, params)
+m.cb.reg = cb_reg
+
+
+
+#
+# now the real thing
+#
+printl('* MSN Client (3.4) *\n', c.yellow, 1)
+
+# first, the configuration
+printl('Loading config... ', c.green, 1)
+if len(sys.argv) > 1:
+	# first, try the arg as file
+	config = get_config(sys.argv[1])
+	profile = None
+	if not config:
+		# then, as the profile
+		profile = sys.argv[1]
+		file = os.environ['HOME'] + '/.msn/msnrc-' + profile
+		config = get_config(file)
+else:
+	profile = None
+	config = get_config(os.environ['HOME'] + '/.msn/msnrc')
+
+if not config:
+	perror('Error opening config file (%s), try running "msnsetup"\n' % file)
+	quit(1)
+
+config['profile'] = profile
+
+# 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:
+	# we ask for the password, setting, if necesary, blocking IO over
+	# stdin (which was disabled by the terminal handling stuff)
+	import getpass
+	try: fcntl.fcntl(stdinfd, fcntl.F_SETFL, os.O_SYNC)
+	except: pass
+	m.pwd = getpass.getpass(c.green + c.bold + "\nPassword: ")
+	try: fcntl.fcntl(stdinfd, fcntl.F_SETFL, os.O_NONBLOCK)
+	except: pass
+
+# 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
+
+# colors
+if not config.has_key('color theme'):
+	config['color theme'] = 'default'
+try:
+	c = color_classes[config['color theme']]()
+except:
+	perror("Unknown color theme, type 'color' for help\n")
+
+# 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')
+
+# encoding
+if not config.has_key('encoding'):
+	# we use posix standard way of defining standard locale, or just fall
+	# back to iso-8859-1; see locale(7) for more details
+	if os.environ.has_key('LC_ALL') and os.environ['LC_ALL']:
+		config['encoding'] = os.environ['LC_ALL']
+	elif os.environ.has_key('LANG') and os.environ['LANG']:
+		config['encoding'] = os.environ['LANG']
+	else:	
+		config['encoding'] = 'iso-8859-1'
+m.encoding = config['encoding']
+
+printl('done\n', c.green, 1)
+
+
+# set or void the debug
+if not config['debug']:
+	msnlib.debug = null
+	msncb.debug = null
+
+# debug some internal variables
+msnlib.debug("Terminal Handling: %d" % use_termios)
+msnlib.debug("Terminal Size: %s" % str(winsize))
+
+# 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 (%s)\n' % (desc, errno))
+	quit(1)
+except KeyboardInterrupt:
+	quit()
+except ('SocketError', socket.error), info:
+	perror('Network error: ' + str(info) + '\n')
+	quit(1)
+except:
+	pexc('Exception logging in\n')
+	quit(1)
+
+
+# call sync to get the lists and refresh
+printl('Sending user list request... ', c.green, 1)
+if m.sync():
+	printl('done\n', c.green, 1)
+	list_complete = 0
+else:
+	perror('Error syncing users\n')
+
+
+# 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
+ignored = []		# people being locally ignored
+
+# 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)
+				
+				# see if we got all the user list, so we can
+				# change our initial status (doing it earlier
+				# as we used to seems to break things for some
+				# people)
+				if not list_complete and \
+						m.lst_total == m.syn_total:
+					list_complete = 1
+					if m.change_status(config['initial status']):
+						printl('\rStatus set to %s\n' % config['initial status'], c.green, 1)
+					else:
+						perror('\rError setting status: unknown status %s\n' % config['initial status'])
+
+				
+					
+			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)
+			except 'XFRError', err:
+				printl("\rXFR Error: %s\n" % str(err))
+
+			# always redraw after network event
+			redraw_cli()
+
+
diff --git a/msncb.py b/msncb.py
new file mode 100644
index 0000000..a05047a
--- /dev/null
+++ b/msncb.py
@@ -0,0 +1,507 @@
+
+
+import string
+import urllib
+import md5
+
+import socket
+
+import msnlib
+
+"""
+This is the home for the msn callback class and examples (that might move to
+another file in the near future).
+
+There are three types of callbacks: the error one (this is only one), the
+server ones (handle connections, notifications, lists and stuff like that),
+and the switchboard ones (which handle messaging).
+
+All of them receive as their first argument an 'md' (msn descriptor) that is
+the main connection object; you probably already know what it is.
+
+The models are:
+error: 		def cb_err(md, errno, params)
+server:		def cb_def(md, type, tid, params)
+switchboard:	def cb_usr(md, type, tid, params, sbd)
+
+See below for more examples.
+
+Probably you should base your own callbacks on these ones, at least they were
+thought with that in mind, so you can use yours as wrappers that handle only
+your app-specific code and forget about the protocol-specific mess.
+
+		Alberto (albertogli@telpin.com.ar)
+"""
+
+
+# use the debug function from msnlib
+debug = msnlib.debug
+
+
+class cb:
+	def __init__(self):
+		self.unk = cb_unk	# unknown
+		self.err = cb_err	# server error
+		self.msg = cb_msg	# get a message
+		self.chl = cb_chl	# challenge
+		self.qry = cb_ign	# query response
+		self.iln = cb_iln	# status notification
+		self.chg = cb_ign	# status change
+		self.nln = cb_nln	# status notification
+		self.fln = cb_fln	# status offline
+		self.out = cb_out	# disconnect
+		self.blp = cb_ign	# privacy mode change
+		self.lst = cb_lst	# list requests
+		self.bpr = cb_bpr	# user info
+		self.gtc = cb_ign	# add notification
+		self.syn = cb_syn	# list sync confirmation
+		self.prp = cb_prp	# private info
+		self.lsg = cb_lsg	# group list
+		self.add = cb_add	# user add
+		self.rem = cb_rem	# user remove
+		self.adg = cb_adg	# group add
+		self.rmg = cb_rmg	# group del
+		self.reg = cb_reg	# group rename
+		self.rea = cb_rea	# nick change
+		self.rng = cb_rng	# switchboard invitation
+		self.iro = cb_iro	# multi-user chat
+		self.ans = cb_ans	# answer confirmation
+		self.xfr = cb_xfr	# switchboard request
+		self.usr = cb_usr	# sb request initial identification
+		self.cal = cb_ign	# call confirmation
+		self.joi = cb_joi	# session join
+		self.ack = cb_ack	# message acknowledge
+		self.nak = cb_nak	# message negative acknowledge
+		self.bye = cb_bye	# switchboard user disconnect
+
+
+	
+error_table = {
+	-10: 'Local error',
+	200: 'Syntax error',
+	201: 'Invalid parameter',
+	205: 'Invalid user',
+	206: 'Domain name missing',
+	207: 'Already logged in',
+	208: 'Invalid username',
+	209: 'Invalid fusername',
+	210: 'User list full',
+	215: 'User already there',
+	216: 'User already on list',
+	217: 'User not online',
+	218: 'Already in mode',
+	219: 'User is in the opposite list',
+	280: 'Switchboard failed',
+	281: 'Transfer to switchboard failed',
+	300: 'Required field missing',
+	302: 'Not logged in',
+	500: 'Internal server error',
+	501: 'Database server error',
+	510: 'File operation failed',
+	520: 'Memory allocation failed',
+	600: 'Server is busy',
+	601: 'Server is unavaliable',
+	602: 'Peer nameserver is down',
+	603: 'Database connection failed',
+	604: 'Server is going down',
+	707: 'Could not create connection',
+	711: 'Write is blocking',
+	712: 'Session is overloaded',
+	713: 'Too many active users',
+	714: 'Too many sessions',
+	715: 'Not expected',
+	717: 'Bad friend file',
+	911: 'Authentication failed',
+	913: 'Not allowed when offline',
+	920: 'Not accepting new users',
+}
+
+def cb_err(md, errno, params):
+	"Handle server errors"
+	if not error_table.has_key(errno):
+		desc = 'Unknown error %d' % errno
+	else:
+		desc = error_table[errno]
+
+	debug('SERVER ERROR %d: %s - %s' % (errno, desc, params))
+
+
+def cb_def(md, type, tid, params):
+	"Default callback. It just prints the args"
+	debug('DEFAULT type: ' + type + ' :: Params: ' + str(params))
+
+
+def cb_unk(md, type, tid, params):
+	"Handles the unknown types"
+	debug('Error! unknown event type "%s"' % type)
+	debug('params: ' + str(params))
+
+	
+def cb_chl(md, type, tid, params):
+	"Handles the challenges"
+	if type != 'CHL': raise 'CallbackMess', (md, type, params)
+	hash = params + 'VT6PX?UQTM4WM%YR' # magic from www.hypothetic.org
+	hash = md5.md5(hash).hexdigest()
+	md._send('QRY', 'PROD0038W!61ZTF9 32')
+	md.fd.send(hash)
+
+
+def cb_ign(md, type, tid, params, nd = None):
+	"Ignores"
+	pass
+
+
+def cb_out(md, type, tid, params):
+	"Server disconnected us"
+	debug('!!! Server closed the connection: ' + params)
+	
+
+def cb_iln(md, type, tid, params):
+	"Handles a friend status change"
+	t = params.split(' ')
+	status = t[0]
+	email = t[1]
+	if len(params) > 2: nick = urllib.unquote(t[2])
+	else: nick = ''
+
+	md.users[email].status = status
+	md.users[email].realnick = nick
+	debug('FRIEND %s (%s) changed status to :%s:' % (nick, email, status))
+
+
+def cb_fln(md, type, tid, params):
+	"Handles a friend disconnection"
+	email = tid
+	debug('FRIEND %s disconnected (%s)' % (email, type))
+	md.users[email].status = type
+
+
+def cb_nln(md, type, tid, params):
+	"Handles a friend status change"
+	status = tid
+	t = params.split(' ')
+	email = t[0]
+	if len(t) > 1: nick = urllib.unquote(t[1])
+	else: nick = ''
+	
+	md.users[email].status = status
+	md.users[email].realnick = nick
+	debug('FRIEND %s (%s) changed status to :%s:' % (nick, email, status))
+
+
+def cb_bpr(md, type, tid, params):
+	"Update friend info"
+	# the email is deduced from the last lst we got; if it's None it means
+	# that we come from an add (the protocol behaves different if coming
+	# from SYN or ADD)
+	email = md._last_lst
+	if email:
+		# we come from SYN
+		type = tid
+		param = urllib.unquote(params)
+	else:
+		# we come from ADD
+		t = params.split(' ')
+		email = t[0]
+		type = t[1]
+		if len(t) >= 3:
+			param = urllib.unquote(t[2])
+		else:
+			param = ''
+
+	if not md.users.has_key(email): return
+
+	if   type == 'PHH': md.users[email].homep = param
+	elif type == 'PHW': md.users[email].workp = param
+	elif type == 'PHM': md.users[email].mobilep = param
+	else: pass
+
+
+def cb_syn(md, type, tid, params):
+	"Receive a SYN notification"
+	t = params.split()
+	if len(t) != 3:
+		raise "SYNError"
+	
+	lver = int(t[0])
+	total = int(t[1])
+	ngroups = int(t[2])
+	
+	md.syn_lver = lver
+	md.syn_total = total
+	md.syn_ngroups = ngroups
+	
+
+def cb_lst(md, type, tid, params):
+	p = params.split(' ')
+	email = tid
+	nick = urllib.unquote(p[0])
+	listmask = int(p[1])
+	if len(p) == 3:
+		groups = p[2]
+	else:
+		groups = '0'
+	
+	# we only use one main group id
+	gid = groups.split(',')[0]
+	
+	if email in md.users.keys():
+		user = md.users[email]
+	else:
+		user = msnlib.user(email, nick, gid)
+	
+	# the list mask is a bitmask, composed of:
+	# FL: 1
+	# AL: 2
+	# BL: 4
+	# RL: 8
+
+	# in forward
+	if listmask & 1:
+		user.lists.append('F')
+		md.users[email] = user
+	
+	# in reverse
+	if listmask & 8:
+		user.lists.append('R')
+		md.reverse[email] = user
+	
+	# in allow
+	if listmask & 2:
+		user.lists.append('A')
+	
+	# in block
+	if listmask & 4:
+		user.lists.append('B')
+	
+	md.lst_total += 1
+	
+	# save in the global last_lst the email, because BPRs might need it
+	md._last_lst = email
+
+def cb_lsg(md, type, tid, params):
+	"Handles group list"
+	p = params.split(' ')
+	gid = tid
+	name, unk = p[0:]
+	# if we get the group 0, start from scratch
+	if gid == '0':
+		md.groups = {}
+	name = urllib.unquote(name)
+	md.groups[gid] = name
+
+
+def cb_prp(md, type, tid, params):
+	"Handles private info"
+	t = params.split(' ')
+	type = t[0]
+	if len(t) > 1: param = urllib.unquote(t[1])
+	else: param = ''
+	
+	if   type == 'PHH': md.homep = param
+	elif type == 'PHW': md.workp = param
+	elif type == 'PHM': md.mobilep = param
+	else: pass
+
+
+def cb_add(md, type, tid, params):
+	"Handles a user add; both you adding a user and a user adding you" 
+	t = params.split(' ')
+	type = t[0]
+	if type == 'RL':
+		email = t[2]
+		nick = urllib.unquote(t[3])
+		debug('ADD: %s (%s) added you' % (nick, email))
+	elif type == 'FL':
+		email = t[2]
+		nick = urllib.unquote(t[3])
+		gid = t[4]
+		md.users[email] = msnlib.user(email, nick, gid)
+		# put None in last_lst so BPRs know it's not coming from sync
+		md._last_lst = None
+		debug('ADD: adding %s (%s)' % (email, nick))
+	else:
+		pass
+
+def cb_rem(md, type, tid, params):
+	"""Handles a user del.
+	Only make something in the case of a user removing you"""
+	t = params.split(' ')
+	type = t[0]
+	if type == 'RL':
+		email = t[2]
+		debug('REM: %s removed you' % email)
+	elif type == 'FL':
+		email = t[2]
+		if md.users[email].sbd:
+			md.close(md.users[email].sbd)
+		del(md.users[email])
+		debug('REM: removing %s' % email)
+	else:
+		pass
+
+def cb_adg(md, type, tid, params):
+	"Handle a group add"
+	t = params.split(' ')
+	lver, name, gid = t[0:3]
+	md.groups[gid] = name
+	debug('ADG: group %s (%s) added' % (name, gid))
+
+def cb_rmg(md, type, tid, params):
+	"Handle a group del"
+	t = params.split(' ')
+	lver, gid = t[0:2]
+	for e in md.users.keys():
+		if md.users[e].gid == gid:
+			if md.users[e].sbd:
+				md.close(md.users[e].sbd)
+			del(md.users[e])
+	del(md.groups[gid])
+	debug('RMG: group %s removed' % gid)
+
+def cb_reg(md, type, tid, params):
+	"Handle a group rename"
+	t = params.split(' ')
+	gid = t[1]
+	name = t[2]
+	md.groups[gid] = name
+	debug('REG: group %s renamed to %s' % (name, gid))
+
+def cb_rea(md, type, tid, params):
+	"Handles our info change"
+	t = params.split(' ')
+	email = t[1]
+	nick = urllib.unquote(t[2])
+	if email != md.email:
+		md.users[email].nick = nick
+	else:
+		md.nick = nick
+	debug('NICK CHANGE: email %s - nick %s' % (email, nick))
+
+
+def cb_rng(md, type, tid, params):
+	"Handles switchboard invitations."
+	t = params.split(' ')
+	sid = tid
+	ip, port = t[0].split(':')
+	port = int(port)
+	hash = t[2]
+	email = t[3]
+	
+	fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+	# we set the socket nonblocking so we don't block (duh!) on connect();
+	# it will be picked up later from the select loop and handled via the
+	# main read() call, which you will have to see to find out the rest.
+	fd.setblocking(0)
+	fd.connect_ex((ip, port))
+	
+	sbd = msnlib.sbd()
+	sbd.fd = fd
+	sbd.block = 0
+	sbd.state = 'cp'
+	sbd.type = 'answer'
+	sbd.endpoint = (ip, port)
+	sbd.emails.append(email)
+	sbd.hash = hash
+	sbd.session_id = sid
+	md.submit_sbd(sbd) 		# it has the connect pending
+
+
+def cb_xfr(md, type, tid, params):
+	"Handles switchboard requests"
+	t = params.split(' ')
+	ip, port = t[1].split(':')
+	port = int(port)
+	hash = t[3]
+
+	fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+	fd.setblocking(0)		# see cb_rng
+	fd.connect_ex((ip, port))
+	
+	# look for the sbd, matching the tid
+	sbd = None
+	for i in md.sb_fds:
+		if i.state == 'xf' and i.orig_tid == tid:
+			sbd = i
+			break
+	if not sbd:
+		debug('AIEEE: XFR without sbd!')
+		raise 'XFRError', (type, tid, params)
+	
+	sbd.fd = fd
+	sbd.block = 0
+	sbd.state = 'cp'
+	sbd.endpoint = (ip, port)
+	sbd.hash = hash
+
+def cb_iro(md, type, tid, params, sbd):
+	"Handles the switchboard participant list"
+	p = params.split(' ')
+	uid, ucount, email, nick = p
+	if ucount == '1':
+		# do nothing if we only have one participant
+		return
+	else:
+		if email not in md.users.keys():
+			md.users[email] = msnlib.user(email)
+		if email not in sbd.emails:
+			sbd.emails.append(email)
+		debug("FRIEND %s joined chat with %s" % (email, sbd.emails[0]))
+
+def cb_usr(md, type, tid, params, sbd):
+	"Handles switchboard requests initial identification"
+	email = sbd.emails[0]
+	md._send('CAL', email, sbd)
+	sbd.state = 'ca'
+
+
+def cb_joi(md, type, tid, params, sbd):
+	"Handles a switchboard join, and sends the pending messages"
+	email = tid
+	# if it's a multi-user chat, just append it to the list
+	if sbd.emails and email != sbd.emails[0]:
+		sbd.emails.append(email)
+		if email not in md.users.keys():
+			md.users[email] = msnlib.user(email)
+		debug('CALL: user %s joined chat with %s' % \
+			(email, sbd.emails[0]))
+	# otherwise (common path) set up the sbd and flush the messages
+	else:
+		sbd.state = 'es'
+		debug('CALL: user %s replied your chat request; flushing' % email)
+		md.sendmsg(email)
+		debug('CALL: message queue for %s flushed' % email)
+
+
+def cb_ans(md, type, tid, params, sbd):
+	"""Answer confirmation to an invitation, replied after the connect()
+	ending by read()"""
+	sbd.state = 'es'
+
+
+def cb_msg(md, type, tid, params, sbd):
+	"Get a message"
+	debug('MESSAGE\n+++ Header: %s\n%s\n\n' % (str(tid), str(params)))
+
+
+def cb_ack(md, type, tid, params, sbd):
+	"Get a message acknowledge"
+	debug('ACK: tid:%s' % tid)
+
+
+def cb_nak(md, type, tid, params, sbd):
+	"Get a message negative acknowledge"
+	debug('NAK: tid:%s' % tid)
+	
+
+def cb_bye(md, type, tid, params, sbd):
+	"Handles a user sb disconnect"
+	email = tid
+	if email != sbd.emails[0]:
+		debug('BYE: user %s leaving sbd' % email)
+		if email in sbd.emails:
+			sbd.emails.remove(email)
+	else:
+		debug('BYE: closing %s' % str(sbd))
+		md.close(sbd)
+
diff --git a/msnlib.py b/msnlib.py
new file mode 100644
index 0000000..1bedaac
--- /dev/null
+++ b/msnlib.py
@@ -0,0 +1,743 @@
+
+
+import sys
+import time
+import string
+import socket
+import select
+import md5
+import urllib
+
+"""
+MSN Messenger Client Library
+by Alberto Bertogli (albertogli@telpin.com.ar)
+"""
+
+# constants
+VERSION = 0x0304
+LOGIN_HOST = 'messenger.hotmail.com'
+LOGIN_PORT = 1863
+
+status_table = {
+	'online':       'NLN',
+	'away':         'AWY',
+	'busy':         'BSY',
+	'brb':          'BRB',
+	'phone':        'PHN',
+	'lunch':        'LUN',
+	'invisible':    'HDN',
+	'idle':         'IDL',
+	'offline':      'FLN',
+}
+
+reverse_status = {
+	'NLN':		'online',
+	'AWY':		'away',
+	'BSY':		'busy',
+	'BRB':		'brb',
+	'PHN':		'phone',
+	'LUN':		'lunch',
+	'HDN':		'invisible',
+	'IDL':		'idle',
+	'FLN':		'offline',
+}
+
+
+def debug(s):
+	sys.stderr.write('\r' + str(s) + '\n')
+	sys.stderr.flush()
+
+
+class user:
+	"""User class, used to store your 'friends'"""
+	def __init__(self, email = '', nick = '', gid = None):
+		self.email = email
+		self.nick = nick
+		self.realnick = ''
+		self.status = 'FLN'
+		self.online = 0
+		self.gid = gid
+		self.homep = None
+		self.workp = None
+		self.mobilep = None
+		self.sbd = None
+		self.priv = {}
+		self.lists = []
+	
+	def __repr__(self):
+		return '<user email:%s nick:"%s" gid:%s>' % (self.email,
+				self.nick, self.gid)
+
+
+class sbd:
+	"""SwitchBoard Descriptor
+	Used as a pseudo-fd to store per-switchboard connection information.
+	The state is either one of (too many):
+	
+	[answer]
+	cp	connect pending (just came from rng)
+	re	ready (just came from connect)
+	an	waiting for answer reply
+	
+	[invite]
+	xf	waiting for xfr response (not even connected yet)
+	us	waiting for usr response
+	ca	waiting for cal response
+	jo	waiting for a join response
+	
+	es	established (waiting in boredom)
+
+	You will find more information in the doc directory.
+	"""
+
+	def __init__(self):
+		self.fd = None		# connection fd
+		self.state = None	# connection's state (see doc above)
+		self.emails = []	# emails we talk to through
+		self.msgqueue = []	# outgoing message queue
+		self.hash = None	# server-sent hash
+		self.session_id = None	# server-sent sid
+		self.endpoint = ()	# remote end (ip, port)
+		self.type = None	# either 'answer' or 'invite'
+		self.tid = 1		# the transaction id, it needs to be
+					# unique for consistency
+		self.block = 1		# blocking state
+		self.orig_tid = None	# tid of the original XFR
+	
+	def __repr__(self):
+		return '<sbd: emails:%s state:%s fd:%d endpoint:%s>' % \
+			(str(self.emails), self.state, \
+			self.fileno(), self.endpoint)
+	
+	def fileno(self):
+		return self.fd.fileno()
+
+	def get_tid(self):
+		"Returns a valid tid as string"
+		self.tid = self.tid + 1
+		return str(self.tid - 1)
+
+	
+
+class msnd:
+	"""MSN Descriptor
+	This is the main and most important class; it represents a msn
+	instance.
+	
+	It's, afaik, nonblocking (not through setblocking() but mainly because
+	it forces a select() i/o model (which you would probably have used
+	anyway, unless you think async/signal io worths the mess for a stupid
+	messenger protocol, or you are a thread freak)), then the reads should
+	always succed. Note that we sanely assume that writes do not block.
+
+	Yes yes, you can use poll() too =)
+	
+	The only blocking call is the login() which is in charge of doing the
+	initial connection and setup, all the rest are cpu bound.
+	
+	Once you have created an instance you should assign an email and a
+	password at least, then do the login and i recommend you to call sync
+	after that (and everyonce in a while doesn't hurt either). Finally you
+	change your status and you're ready to idle.
+
+	Oh, and don't forget to set the callbacks: they are the most important
+	part, they are the ones which allow you to control the protocol and
+	make this useful.
+
+	They are completely asyncronous, are driven by the read method, and
+	never block. A special care should be taken if you use threads (which
+	you shouldn't need, that was the whole idea behind this), because
+	there is not a single lock on these lines, and it will remain that way.
+
+	There is an example (a very bad one, but you'll see how it would work)
+	that should have come with this file; also the callback file has good
+	working code.
+	"""
+	
+	def __init__(self):
+		self.fd = None			# socket fd
+		self.sb_fds = []		# switchboard fds
+		self.tid = 1			# transaction id
+		
+		self.email = None		# login email
+		self.pwd = None			# login pwd
+		self.nick = None		# nick
+
+		self.homep = None		# home phone
+		self.workp = None		# work phone
+		self.mobilep = None		# mobile phone
+
+		self.status = 'FLN'		# status
+		self.encoding = 'iso-8859-1'	# local encoding
+		
+		self.lhost = LOGIN_HOST
+		self.lport = LOGIN_PORT
+		self.ns = (None, None)		# notification server
+		self.hash = None		# hash used to authenticate
+
+		self.syn_lver = 0		# user list version
+		self.syn_total = 10000		# qty. of users from SYN
+		self.syn_ngroups = 0		# qty. of groups from SYN
+
+		self.lst_total = 0		# qty. of LSTs got
+		
+		self.cb = None			# callbacks
+
+		self.users = {}			# forward user list
+		self.reverse = {}		# reverse user list
+		self.groups = {}		# group list
+		
+	
+	def __repr__(self):
+		return '<msnd object, fd:%s, email:%s, tid:%s>' % (self.fd,
+			self.email, self.tid)
+	
+	def fileno(self):
+		"Useful for select()"
+		return self.fd.fileno()
+
+	def encode(self, s):
+		"Encodes a string from local encoding to utf8"
+		try:
+			return s.decode(self.encoding).encode('utf-8')
+		except:
+			return s
+
+	def decode(self, s):
+		"Decodes a string from utf8 to local encoding"
+		try:
+			return s.decode('utf-8').encode(self.encoding)
+		except:
+			return s
+
+	
+	def pollable(self):
+		"""Return a pair of lists of poll()/select()ables network
+		descriptors (ie. they are not fds, but actually classes that
+		implement fileno() methods, like this one and the sbd). We do
+		it this way because then it's simpler to read().
+		
+		The reason behind the tuple is that for connect-pending fds we
+		need to wait for writing readiness, so we must tell the
+		userspace so. Notice that it still goes with the read() path.
+
+		Yes, it is a mess but i couldn't find anything better yet. It
+		works, it's efficient; let's pretend it's correct =)
+		
+		It includes the main file descriptor, and all the switchboards
+		connections; then you call self.read(fd) on what this returns,
+		and magic happens."""
+		
+		iwtd = []
+		owtd = []
+		iwtd.append(self)
+		for nd in self.sb_fds:
+			if nd.state == 'cp':	# connect is pending
+				owtd.append(nd)
+			elif nd.state == 'xf':	# skip this case because it's
+						# not connected yet
+				pass
+			else:			# readable!
+				iwtd.append(nd)
+		return (iwtd, owtd)
+		
+	
+	def get_tid(self):
+		"Returns a valid tid as string"
+		self.tid = self.tid + 1
+		return str(self.tid - 1)
+	
+	
+	def _send(self, cmd, params = '', nd = None, raw = 0):
+		"""Sends a command to the server, building it first as a
+		string; uses, if specified, the pseudo fd (it can be either
+		msnd or sbd).""" 
+		if not nd:
+			nd = self
+		tid = nd.get_tid()
+		fd = nd.fd
+		c = cmd + ' ' + tid
+		if params: c = c + ' ' + params
+		debug(str(fd.fileno()) + ' >>> ' + c)
+		if not raw:
+			c = c + '\r\n'
+		c = self.encode(c)
+		return fd.send(c)
+	
+	
+	def _recv(self, fd = None):
+		"Reads a command from the server, returns (cmd, tid, params)"
+		if not fd:
+			fd = self.fd
+		# cheap and dirty readline, FIXME
+		buf = ''
+		c = fd.recv(1)
+		while c != '\n' and c != '':
+			buf = buf + c
+			c = fd.recv(1)
+		
+		if c == '':
+			raise 'SocketError'
+		
+		buf = buf.strip()
+		pbuf = buf.split(' ')
+		
+		cmd = pbuf[0]
+		
+		# it's possible that we don't have any params (errors being
+		# the most common) so we cover our backs
+		if len(pbuf) >= 3:
+			tid = pbuf[1]
+			params = self.decode(string.join(pbuf[2:]))
+		elif len(pbuf) == 2:
+			tid = pbuf[1]
+			params = ''
+		else:
+			tid = '0'
+			params = ''
+		
+		debug(str(fd.fileno()) + ' <<< ' + buf)
+		return (cmd, tid, params)
+	
+	
+	def _recvmsg(self, msglen, fd = None):
+		"Read a message from the server, returns it"
+		if not fd:
+			fd = self.fd
+		left = msglen
+		buf = ''
+		while len(buf) != msglen:
+			c = fd.recv(left)
+			#debug(str(fd.fileno()) + ' <<< ' + buf)
+			buf = buf + c
+			left = left - len(c)
+			
+		return self.decode(buf)
+	
+	
+	def submit_sbd(self, sbd):
+		"""Submits a switchboard descriptor to add to our list; it is
+		also put on our global list.
+
+		Note that if there is no such user, we create it in order to
+		be able to do operations on users that are not in our server
+		list."""
+		
+		self.sb_fds.append(sbd)
+		email = sbd.emails[0]
+		if email not in self.users.keys():
+			self.users[email] = user(email)
+		if self.users[email].sbd and self.users[email].sbd != sbd:
+			# override the sbd, but keep the message queue
+			sbd.msgqueue = self.users[email].sbd.msgqueue[:]
+			self.close(self.users[email].sbd)
+		self.users[email].sbd = sbd
+		return
+	
+	
+	def change_status(self, st):
+		"""Changes the current status to: online, away, busy, brb,
+		phone, lunch, invisible, idle, offline"""
+		if not status_table.has_key(st): return 0
+		self.status = status_table[st]
+		self._send('CHG', self.status)
+		return 1
+	
+	
+	def privacy(self, public = 1, auth = 0):
+		"""Sets our privacy state. First parameter define if you get
+		messages from everybody or only from people on your list; the
+		second defines if you want users to ask for authorization or
+		let everybody add you"""
+		if public:	self._send('BLP', 'AL') # be social
+		else:		self._send('BLP', 'BL') # live in a cave
+
+		if auth:	self._send('GTC', 'A')	# ask for auth
+		else:		self._send('GTC', 'N')	# let them add you
+		
+		return 1
+	
+	
+	def change_nick(self, nick):
+		"Changes our nick"
+		nick = urllib.quote(nick)
+		self._send('REA', self.email + ' ' + nick)
+		return 1
+	
+	
+	def sync(self):
+		"Syncronizes the tables"
+		self._send('SYN', '0')
+		return 1
+	
+
+	def useradd(self, email, nick = None, gid = '0'):
+		"Adds a user"
+		if not nick: nick = email
+		nick = urllib.quote(nick)
+		self._send('ADD', 'AL ' + email + ' ' + nick)
+		self._send('ADD', 'FL ' + email + ' ' + nick + ' ' + gid)
+		return 1
+	
+
+	def userdel(self, email):
+		"Removes a user"
+		self._send('REM', 'AL ' + email)
+		self._send('REM', 'FL ' + email)
+		return 1
+	
+	def userblock(self, email):
+		self._send('REM', 'AL ' + email)
+		self._send('ADD', 'BL ' + email + ' ' + email)
+		if 'B' not in self.users[email].lists:
+			self.users[email].lists.append('B')
+	
+	def userunblock(self, email):
+		self._send('REM', 'BL ' + email)
+		self._send('ADD', 'AL ' + email + ' ' + email)
+		if 'B' in self.users[email].lists:
+			self.users[email].lists.remove('B')
+	
+	def groupadd(self, name):
+		"Adds a group"
+		name = urllib.quote(name)
+		self._send('ADG', name + ' 0')
+		return 1
+	
+	def groupdel(self, gid):
+		"Removes a group"
+		self._send('RMG', gid)
+		return 1
+	
+	def groupren(self, gid, newname):
+		newname = urllib.quote(newname)
+		self._send('REG', gid + ' ' + newname)
+		return 1
+
+	def disconnect(self):
+		"Disconnect from the server"
+		self.fd.send('OUT\r\n')
+		self.fd.close()
+	
+	
+	def close(self, sb):
+		"Closes a given sbd"
+		self.sb_fds.remove(sb)
+		self.users[sb.emails[0]].sbd = None
+		try:
+			self._send('BYE', self.email, nd = sb)
+			sb.fd.close()
+		except:
+			pass
+		del(sb)
+	
+	
+	def invite(self, email, sbd):
+		"Invites a user into an existing sbd"
+		self._send('CAL', email, nd = sbd)
+	
+	def login(self):
+		"Logins to the server, really boring"
+
+		# open socket
+		self.fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+		self.fd.connect((self.lhost, self.lport))
+		
+		# version information
+		self._send('VER', 'MSNP8 CVR0')
+		
+		r = self._recv()
+		if r[0] != 'VER' and r[2][0:4] != 'MSNP8':
+			raise 'VersionError', r
+		
+		# lie the version, just in case
+		self._send('CVR', '0x0409 win 4.10 i386 MSNMSGR 5.0.0544 MSMSGS ' + self.email)
+		self._recv()	# we just don't care what we get
+	
+		# ask for notification server
+		self._send('USR', 'TWN I ' + self.email)
+
+		r = self._recv()
+		if r[0] != 'XFR' and r[2][0:2] != 'NS':
+			raise 'NSError', r
+
+		# parse the notification server ip and port (as int)
+		ns = string.split(r[2])[1]
+		self.ns = ns.split(':')
+		self.ns[1] = int(self.ns[1])
+		self.ns = tuple(self.ns)
+		
+		# close the fd and reopen it on the ns
+		self.fd.close()
+		self.fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+		self.fd.connect(self.ns)
+
+		# version, same as before
+		self._send('VER', 'MSNP8 CVR0')
+		r = self._recv()
+		if r[0] != 'VER' and r[2][0:4] != 'MSNP8':
+			raise 'VersionError', r
+		
+		# lie the version, just in case
+		self._send('CVR', '0x0409 win 4.10 i386 MSNMSGR 5.0.0544 MSMSGS	' + self.email)
+		self._recv()	# we just don't care what we get
+	
+		# auth: send user, get hash
+		self._send('USR', 'TWN I ' + self.email)
+		
+		r = self._recv()
+		if r[0] != 'USR':
+			raise 'AuthError', r
+		hash = string.split(r[2])[2]
+		
+		# get and use the passport id
+		passportid = self.passport_auth(hash)
+		self._send('USR', 'TWN S ' + passportid)
+
+		r = self._recv()
+		if r[0] != 'USR' and r[2][0:2] != 'OK':
+			raise 'AuthError', r
+		self.nick = string.split(r[2])[2]
+		self.nick = urllib.unquote(self.nick)
+		
+		return 1
+
+
+	def passport_auth(self, hash):
+		"""Logins into passport and obtains an ID used for
+		authorization; it's a helper function for login"""
+		import urllib
+		import httplib
+		
+		# initial connection
+		debug('PASSPORT begin')
+		nexus = urllib.urlopen('https://nexus.passport.com/rdr/pprdr.asp')
+		h = nexus.headers
+		purl = h['PassportURLs']
+		
+		# parse the info
+		d = {}
+		for i in purl.split(','):
+		        key, val = i.split('=', 1)
+			d[key] = val
+		
+		# get the login server
+		login_server = 'https://' + d['DALogin']
+		login_host = d['DALogin'].split('/')[0]
+		
+		# build the authentication headers
+		ahead =  'Passport1.4 OrgVerb=GET'
+		ahead += ',OrgURL=http%3A%2F%2Fmessenger%2Emsn%2Ecom'
+		ahead += ',sign-in=' + urllib.quote(self.email)
+		ahead += ',pwd=' + urllib.quote(self.pwd)
+		ahead += ',lc=1033,id=507,tw=40,fs=1,'
+		ahead += 'ru=http%3A%2F%2Fmessenger%2Emsn%2Ecom,ct=1062764229,'
+		ahead += 'kpp=1,kv=5,ver=2.1.0173.1,tpf=' + hash
+		headers = { 'Authorization': ahead }
+		
+		# connect to the given server
+		debug('SSL Connect to %s' % login_server)
+		ls = httplib.HTTPSConnection(login_host)
+		
+		# make the request
+		debug('SSL GET')
+		ls.request('GET', login_server, '', headers)
+		resp = ls.getresponse()
+		
+		# loop if we get redirects until we get a definitive answer
+		debug('SSL Response %d' % resp.status)
+		while resp.status == 302:
+			login_server = resp.getheader('Location')
+			login_host = login_server.split('/')[2]
+			debug('SSL Redirect to %s' % login_server)
+			ls = httplib.HTTPSConnection(login_host)
+			headers = { 'Authorization': ahead }
+			ls.request('GET', login_server, '', headers)
+			resp = ls.getresponse()
+			debug('SSL Response %d' % resp.status)
+		
+		# now we have a definitive answer, if it's not 200 (success)
+		# just raise AuthError
+		if resp.status != 200:
+			# for now we raise 911, which means authentication
+			# failed; but maybe we can get more detailed
+			# information
+			raise 'AuthError', [911, 'SSL Auth failed']
+
+		# and parse the headers to get the passport id
+		try:
+			ainfo = resp.getheader('Authentication-Info')
+		except:
+			ainfo = resp.getheader('WWW-Authenticate')
+		
+		d = {}
+		for i in ainfo.split(','):
+			key, val = i.split('=', 1)
+			d[key] = val
+			
+		passportid = d['from-PP']
+		passportid = passportid[1:-1]		# remove the "'"
+		return passportid
+
+
+	def read(self, nd = None):
+		"""Reads from the specified nd and run the callback. The nd
+		can be either a msnd or a sbd (that's why it's called 'nd'
+		from 'network descriptor').
+		"""
+		if not nd:
+			nd = self
+		
+		# handle different stages of switchboard initialization
+		if nd in self.sb_fds:
+			# connect pending
+			if nd.state == 'cp':
+				# see if the connect went well
+				r = nd.fd.getsockopt(socket.SOL_SOCKET,
+					socket.SO_ERROR)
+				if r != 0:
+					raise 'SocketError', 'ConnectFailed'
+				nd.fd.setblocking(1)
+				nd.block = 1
+				nd.state = 're'
+				
+			# need to send the answer to the remote invitation
+			if nd.type == 'answer' and nd.state == 're':
+				params = self.email + ' ' + nd.hash + ' ' + \
+					nd.session_id
+				self._send('ANS', params, nd)
+				nd.state = 'an'
+				return
+			if nd.type == 'invite' and nd.state == 're':
+				params = self.email + ' ' + nd.hash
+				self._send('USR', params, nd)
+				nd.state = 'us'
+				return
+		
+		
+		
+		r = self._recv(nd.fd)
+		type = r[0]
+		tid = r[1]
+		params = string.strip(r[2])
+
+		if   type == 'CHL': self.cb.chl(self, type, tid, params)
+		elif type == 'QRY': self.cb.qry(self, type, tid, params)
+		elif type == 'ILN': self.cb.iln(self, type, tid, params)
+		elif type == 'CHG': self.cb.chg(self, type, tid, params)
+		elif type == 'OUT': self.cb.out(self, type, tid, params)
+		elif type == 'FLN': self.cb.fln(self, type, tid, params)
+		elif type == 'NLN': self.cb.nln(self, type, tid, params)
+		elif type == 'BLP': self.cb.blp(self, type, tid, params)
+		elif type == 'LST': self.cb.lst(self, type, tid, params)
+		elif type == 'GTC': self.cb.gtc(self, type, tid, params)
+		elif type == 'SYN': self.cb.syn(self, type, tid, params)
+		elif type == 'PRP': self.cb.prp(self, type, tid, params)
+		elif type == 'LSG': self.cb.lsg(self, type, tid, params)
+		elif type == 'BPR': self.cb.bpr(self, type, tid, params)
+		elif type == 'ADD': self.cb.add(self, type, tid, params)
+		elif type == 'REA': self.cb.rea(self, type, tid, params)
+		elif type == 'REM': self.cb.rem(self, type, tid, params)
+		elif type == 'ADG': self.cb.adg(self, type, tid, params)
+		elif type == 'RMG': self.cb.rmg(self, type, tid, params)
+		elif type == 'REG': self.cb.reg(self, type, tid, params)
+		elif type == 'RNG': self.cb.rng(self, type, tid, params)
+		
+		elif type == 'IRO': self.cb.iro(self, type, tid, params, nd)
+		elif type == 'ANS': self.cb.ans(self, type, tid, params, nd)
+		elif type == 'XFR': self.cb.xfr(self, type, tid, params)
+		elif type == 'USR': self.cb.usr(self, type, tid, params, nd)
+		elif type == 'CAL': self.cb.cal(self, type, tid, params, nd)
+		elif type == 'JOI': self.cb.joi(self, type, tid, params, nd)
+
+		elif type == 'ACK': self.cb.ack(self, type, tid, params, nd)
+		elif type == 'NAK': self.cb.nak(self, type, tid, params, nd)
+		elif type == 'BYE': self.cb.bye(self, type, tid, params, nd)
+
+		
+		elif type == 'MSG':
+			params = tid + ' ' + params
+			mlen = int(r[2].split()[-1])
+			msg = self._recvmsg(mlen, nd.fd)
+			self.cb.msg(self, type, params, msg, nd)
+		
+		else:
+			# catch server errors - always numeric type
+			try:
+				errno = int(type)
+			except:
+				errno = None
+			
+			if errno:
+				self.cb.err(self, errno, \
+					str(tid) + ' ' + str(params))
+			else:
+				# if we got this far, we have no idea
+				self.cb.unk(self, type, tid, params)
+		return
+
+
+	def sendmsg(self, email, msg = '', sb = None):
+		"""Sends a message to the user identified by 'email', either
+		the one specified or flush the queue.
+		Returns:
+		1	message queued for delivery
+		2	queue flushed
+		-2	the message is too big
+
+		To verify the message delivery, use the ack callbacks.
+		Message sending order is guaranteed within a sbd; but not the
+		acknowledge; that's what the ACK/NAK callbacks are for.
+		"""
+
+		if email and email not in self.users.keys():
+			self.users[email] = user(email)
+		
+		if len(msg) > 1500:
+			return -2
+		
+		if not sb:
+			sb = self.users[email].sbd
+		
+		# we don't have a connection
+		if not sb:
+			sb = sbd()
+			sb.state = 'xf'
+			sb.type = 'invite'
+			sb.emails.append(email)
+			sb.msgqueue.append(msg)
+
+			self.submit_sbd(sb)	# no need to connect it yet
+			# we set the orig_tid of the sbd to the next tid (that
+			# is, the tid the XFR is going to have), in order to
+			# be able to identify it later, in cb.cb_xfr()
+			sb.orig_tid = str(self.tid)
+			self._send('XFR', 'SB')
+			return 1
+		
+		# it's not ready yet
+		elif sb.state != 'es':
+			sb.msgqueue.append(msg)
+			return 1
+		
+		# no more excuses, send it
+		else:
+			# we make a list with all the messages to send
+			pend = sb.msgqueue
+			if msg:
+				pend.append(msg)
+			while len(pend):
+				m = pend[0]
+				header = "MIME-Version: 1.0\r\n" + \
+					"Content-Type: text/plain; " + \
+					"charset=UTF-8\r\n\r\n"
+				m = header + m
+				msize = len(self.encode(m))
+				params = 'A ' + str(msize) + '\r\n' + m
+				self._send('MSG', params, sb, raw = 1)
+				del(pend[0])
+		
+			return 2
+		
+		
+
diff --git a/msnrc.sample b/msnrc.sample
new file mode 100644
index 0000000..ccb32ca
--- /dev/null
+++ b/msnrc.sample
@@ -0,0 +1,65 @@
+
+# This is the msn client configuration file.
+# It should be placed in ~/.msn/msnrc, with permissions 0600 (but this are not
+# checked by the client, so you have to take care of it)
+# The format is: variable = value
+# Anything that begins with a '#' or doesn't have a '=' is a comment.
+# There are some variables that _must_ be set, and others that can be omitted,
+# in which case the default value will be used. Here they are:
+
+# email, mandatory
+email = myself@mydomain.whatever
+
+
+# all the following are optional parameters and can be omited, in which case
+# the default will be used
+
+# password, if you avoid it, the client will ask you for it at runtime
+password = XXXX
+
+# number of message history to keep in memory
+# defaults to 10
+history size = 10
+
+# number of user input lines to keep in memory
+# defaults to 10
+input history size = 10
+
+# defines if you want to log your incoming/outgoing messages to the disk, must
+# be yes or no.
+# the default is yes
+log history = yes
+
+# directory where to store your log history (if it's used, of course).
+# defaults to $HOME/.msn/history
+history directory = /home/myself/.msn/history
+
+# number of seconds after, if no command was received, we set automatically
+# away. defaults to 0, which disables it.
+auto away = 0
+
+# initial status when we first connect
+# defaults to online and must be valid, that is, one of online, away, busy,
+# and so on)
+initial status = online
+
+# local encoding
+# it's the local character encoding, used to display and send the messages'
+# text in an standard way. don't worry about it if you never had problems when
+# sending non-english characters. it defaults to the environment variables
+# LC_ALL, or LANG, and if none is available to iso-8859-1.
+encoding = iso-8859-1
+
+# debug variable, can be yes or no; note that you can enable and disable it at
+# runtime using the 'debug' command, so this is mostly used when you want to
+# debug the whole session init.
+# the default is no
+debug = no
+
+# color theme
+# this configures which color theme to use; but you can still change it at
+# runtime with the 'color' command (use it to see a list of available themes
+# too)
+color theme = default
+
+
diff --git a/msnsetup b/msnsetup
new file mode 100644
index 0000000..d722e70
--- /dev/null
+++ b/msnsetup
@@ -0,0 +1,87 @@
+#!/bin/bash
+
+function intro() {
+	echo
+	echo "This is a script for creating an initial configuration for the msn client."
+	echo "Any problems or questions regarding it, email albertogli@telpin.com.ar."
+	echo
+	echo "Now you will have to answer a few questions. If you are in doubt, press ENTER and the harmless default will be used."
+	echo "If you want to abort any time, just press CTRL+C"
+	echo
+}
+
+function get_email() {
+	echo "* Email"
+	echo "This is the email address you use with your msn account, usually (but not necesarily) a hotmail.com account."
+	while [ -z "$EMAIL" ]; do
+		read -p "Please insert your email address: " EMAIL
+		echo
+	done
+	echo
+	export EMAIL
+}
+
+function get_pass() {
+	echo "* Password"
+	echo "Your email address' password. Note that the characters won't be displayed for security issues."
+	echo "If you press ENTER, it won't be written, and you'll be asked for it at login time."
+	read -s -p "Please insert your password: " PASS
+	echo
+	export PASS
+}
+
+function create_dirs() {
+	mkdir "$HOME/.msn" 2> /dev/null
+	mkdir "$HOME/.msn/history" 2> /dev/null
+	chmod -R og-rwx "$HOME/.msn"
+}
+
+function create_rc() {
+	# first parameter is the rc file to create
+	if [ -s "$1" ]; then
+		echo "Error: file $1 already exists!"
+		exit
+	fi
+	touch "$1"
+	chmod -R 0600 "$1"
+	echo "# msn client configuration file" >> "$1"
+	echo "# created automatically by the msnsetup script" >> "$1"
+	echo >> "$1"
+	echo "email = $EMAIL" >> "$1"
+	if [ -z "$PASS" ]; then
+		echo "# password not configured" >> "$1"
+	else
+		echo "password = $PASS" >> "$1"
+	fi
+	echo >> "$1"
+}	
+
+
+# main
+
+# we take only one optional parameter, the profile to create the rc for
+
+intro
+get_email
+get_pass
+
+echo "Creating the directory hierachy ($HOME/.msn)"
+create_dirs
+
+RC="$HOME/.msn/msnrc"
+# if we have the profile, use it
+if [ "$1" ]; then
+	echo "Configuring for profile $1"
+	RC="$HOME/.msn/msnrc-$1"
+fi
+echo "Creating the configuration file ($RC)"
+create_rc "$RC"
+
+RUN="msn"
+if [ "$1" ]; then
+	RUN="msn $1"
+fi
+echo "Done! run '$RUN' to start the client"
+
+
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..1b192e1
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,12 @@
+
+from distutils.core import setup
+
+setup(name="msnlib",
+	version="3.4",
+	description="MSN Messenger Library and Client",
+	author="Alberto Bertogli",
+	author_email="albertogli@telpin.com.ar",
+	url="http://users.auriga.wearlab.de/~alb/msnlib",
+	py_modules=['msnlib', 'msncb'],
+)
+									 
diff --git a/utils/clean b/utils/clean
new file mode 100644
index 0000000..168a843
--- /dev/null
+++ b/utils/clean
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+rm -vrf *.pyc *.pyo *~
+rm -vrf build
+
diff --git a/utils/hmerge b/utils/hmerge
new file mode 100644
index 0000000..2843d68
--- /dev/null
+++ b/utils/hmerge
@@ -0,0 +1,110 @@
+#!/usr/bin/env python
+
+"""
+Merger for msnlib logfiles.
+
+It takes two logfiles as arguments, and prints out the merge between them,
+sorting using the time.
+
+Quite useful when you have used msn in two different places and want to unify
+the logs.
+
+
+Note that this will not do absolute time sorting (as it's usual for time to go
+backwards, as we all know =), but record-by-record time compares.
+
+Alberto Bertogli (albertogli@telpin.com.ar), 02/Jun/2003
+Please send any reports to msnlib-devel@auriga.wearlab.de.
+"""
+
+
+import sys
+import time
+
+
+def get_records(fd):
+	records = []
+	l = fd.readline()
+	rec = l
+	l = fd.readline()
+	while rec:
+		# if the line begins with \t, then it's a multi-line record
+		if l and l[0] == '\t':
+			rec += l
+			l = fd.readline()
+			continue
+		
+		# process the actual record
+		ls = rec.split(' ', 2)
+		raw_date = ls[0] + ' ' + ls[1]
+		date = time.strptime(raw_date, '%d/%b/%Y %H:%M:%S ')
+		date = time.mktime(date)
+		records.append((date, rec))
+		
+		# save the current line
+		rec = l
+		l = fd.readline()
+	return records
+
+def panic(s):
+	print s
+	sys.exit(1)
+
+try:
+	fd1 = open(sys.argv[1])
+	fd2 = open(sys.argv[2])
+except:
+	panic("Use: hmerge file1 file2")
+
+# this is the invalid record to mark the end of the record list
+eor_record = (0, '')
+
+
+rec1 = get_records(fd1)
+rec2 = get_records(fd2)
+
+if not rec1: panic("Error: file 1 doesn't have any records")
+if not rec2: panic("Error: file 1 doesn't have any records")
+
+# append the eor_record to both lists
+rec1.append(eor_record)
+rec2.append(eor_record)
+
+len1 = len(rec1)
+len2 = len(rec2)
+
+point1 = 0
+point2 = 0
+
+while 1:
+		
+	r1 = rec1[point1]
+	r2 = rec2[point2]
+	
+	# if we have any at the end, print it or exit
+	if r1[0] == 0 or r2[0] == 0:
+		# if we reach the end of both lists, we exit
+		if r1[0] == 0 and r2[0] == 0:
+			break
+		if r1[0] == 0:
+			print r2[1],
+			point2 += 1
+		elif r2[0] == 0:
+			print r1[1],
+			point1 += 1
+	
+	# otherwise, compare and print the earlier
+	else:
+		if r1[0] < r2[0]:
+			print r1[1],
+			point1 += 1
+		elif r1[0] > r2[0]:
+			print r2[1],
+			point2 += 1
+		else:
+			print r1[1],
+			print r2[1],
+			point1 += 1
+			point2 += 1
+		
+
diff --git a/utils/msnbot b/utils/msnbot
new file mode 100644
index 0000000..001d2d1
--- /dev/null
+++ b/utils/msnbot
@@ -0,0 +1,127 @@
+#!/usr/bin/env python
+
+"""
+This is a very simple bot to show how automation using msnlib could be done.
+It's not quite useful as-is, but provides a good example.
+
+If you play with it, please let me know.
+"""
+
+
+# sys, for getting the parameters
+import sys
+
+# time, for sleeping
+import time
+
+# select to wait for events
+import select
+
+# socket, to catch errors
+import socket
+
+# thread, for creating the worker thread
+import thread
+
+# and, of course, msnlib
+import msnlib
+import msncb
+
+
+m = msnlib.msnd()
+m.cb = msncb.cb()
+
+
+def do_work():
+	"""
+	Here you do your stuff and send messages using m.sendmsg()
+	This is the only place your code lives
+	"""
+	
+	# wait a bit for everything to settle down (sync taking efect
+	# basically)
+	time.sleep(15)
+	
+	print '-' * 20 + 'SEND 1'
+	print m.sendmsg("xx@me.com", "Message One")
+
+	print '-' * 20 + 'SEND 2'
+	print m.sendmsg("xx@me.com", "Message Two")
+
+	# give time to send the messages
+	time.sleep(30)
+
+	# and then quit
+	quit()
+	
+
+# you shouldn't need to touch anything past here
+
+
+# get the login email and password from the parameters
+try:
+	m.email = sys.argv[1]
+	m.pwd = sys.argv[2]
+except:
+	print "Use: msnbot email password"
+	sys.exit(1)
+
+
+print "Logging In"
+m.login()
+
+print "Sync"
+# this makes the server send you the contact list, and it's recommended that
+# you do it because you can get in trouble when getting certain events from
+# people that are not on your list; and it's not that expensive anyway
+m.sync()
+
+print "Changing Status"
+# any non-offline status will do, otherwise we'll get an error from msn when
+# sending a message
+m.change_status("away")
+
+def quit():
+	try:
+		m.disconnect()
+	except:
+		pass
+	print "Exit"
+	sys.exit(0)
+
+# we start a thread to do the work. it's a thread because we want to share
+# everything, and fork cow semantics cause problems here
+thread.start_new_thread(do_work, ())
+
+
+# we loop over the network socket to get events
+print "Loop"
+while 1:
+	# we get pollable fds
+	t = m.pollable()
+	infd = t[0]
+	outfd = t[1]
+
+	# we select, waiting for events
+	try:
+		fds = select.select(infd, outfd, [], 0)
+	except:
+		quit()
+	
+	for i in fds[0] + fds[1]:       # see msnlib.msnd.pollable.__doc__
+		try:
+			m.read(i)
+		except ('SocketError', socket.error), err:
+			if i != m:
+				# user closed a connection
+				# note that messages can be lost here
+				m.close(i)
+			else:
+				# main socket closed
+				quit()
+
+	# sleep a bit so we don't take over the cpu
+	time.sleep(0.01)
+
+
+
diff --git a/utils/msncd b/utils/msncd
new file mode 100644
index 0000000..82a7e7e
--- /dev/null
+++ b/utils/msncd
@@ -0,0 +1,417 @@
+#!/usr/bin/env python 
+
+
+import sys
+import os
+import socket
+import select
+import string
+
+import msnlib
+import msncb
+
+
+"""
+MSN Client Daemon
+
+This is a MSN client that reads commands from a named pipe, using
+a little text-only protocol. It's main use is to serve as a 'glue'
+to implement clients in other languages.
+
+This is yet experimental because lack of testing, please let me know if you
+try it out.
+"""
+
+
+def null(s):
+	"Null function, useful to void debug ones"
+	pass
+		
+
+#
+# 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]
+	equeue.append('STCH %s %s\n' % (email, 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]
+	equeue.append('STCH %s %s\n' % (email, status))
+	msncb.cb_nln(md, type, tid, params)
+m.cb.nln = cb_nln
+
+def cb_fln(md, type, tid, params):
+	email = tid
+	u = m.users[email]
+	discarded = 0
+	if u.sbd and u.sbd.msgqueue:
+		discarded = len(u.sbd.msgqueue)
+	equeue.append('STCH %s offline %d\n' % (email, discarded))
+	msncb.cb_fln(md, type, tid, params)
+m.cb.fln = cb_fln
+
+# server disconnect
+def cb_out(md, type, tid, params):
+	equeue.append('ERR SERV_DISC Server sent disconnect\n')
+	msncb.cb_out(md, type, tid, params)
+m.cb.out = cb_out
+
+
+# message
+def cb_msg(md, type, tid, params, sbd):
+	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 = 1
+	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
+	
+	if headers.has_key('Content-Type') and headers['Content-Type'] == 'text/x-msmsgscontrol':
+		# the typing notices
+		equeue.append('TYPING %s\n' % email)
+	else:
+		# messages
+		equeue.append('MSG %d %d %s\n%s\n' % \
+			(len(lines), eoh, email, string.join(lines, '\n')) )
+	
+	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:
+		equeue.append('MFLUSH %s\n' % email)
+	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]
+	equeue.append('ERR %s %s\n' % (errno, 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]
+	if type == 'RL':
+		equeue.append('UADD %s\n' % email)
+	elif type == 'FL':
+		equeue.append('ADDFL %s\n' % email)
+	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':
+		equeue.append('UDEL %s\n' % email)
+	elif type == 'FL':
+		equeue.append('DELFL %s\n' % email)
+	msncb.cb_rem(md, type, tid, params)
+m.cb.rem = cb_rem
+
+
+def login(email, password):
+	# 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:
+		perror('Network error: ' + str(info) + '\n')
+		quit(1)
+	except:
+		pexc('Exception logging in\n')
+		quit(1)
+
+
+#
+# the pipe read
+#
+
+# first, a small send wrapper to avoid repeating 'addr' all over the place
+# note that as they are implemented using udp, if more than one client
+# connects it can get quite quite messy
+addr = ()
+def psend(pipe, s):
+	print '-->', s,
+	return pipe.sendto(s, addr)
+
+# read from the pipe, c being the pipe socket passed from the caller
+def pipe_read(c):
+	global m
+	global addr
+	global equeue	
+	
+	# we don't worry about lines too much in this implementation because
+	# we use datagrams. however, when using stream sockets you should
+	s, addr = c.recvfrom(4 * 1024)	# input buffer, should be enough
+	print '<--', s,
+	try:
+		s = s.split(' ', 1)
+		if len(s) == 2:
+			cmd, params = s
+		else:
+			cmd = s[0]
+			params = ''
+
+		cmd = cmd.strip()
+		if params:
+			params = params.strip()
+			params = params.split(' ')
+	except:
+		psend(c, 'ERR EINVAL\n')
+		return
+	
+	if cmd == 'LOGIN':
+		if len(params) != 2:
+			psend(c, 'ERR PARAMS\n')
+			return
+		try:
+			email, pwd = params
+			m.email = email
+			m.pwd = pwd
+			m.login()
+			m.sync()
+		except 'AuthError', info:
+			errno = int(info[0])
+			if not msncb.error_table.has_key(errno):
+				desc = 'Unknown'
+			else:
+				desc = msncb.error_table[errno]
+			psend(c, 'ERR MSN %d %s\n' % (errno, desc))
+			return
+		except ('SocketError', socket.error), info:
+			psend(c, 'ERR SOCK %s\n' % str(info))
+			return
+		psend(c, 'OK\n')
+		return
+		
+	elif cmd == 'LOGOFF':
+		m.disconnect()
+		psend(c, 'OK\n')
+		return
+		
+	# if we are not connected, the following commands are not available
+	if not m.fd:
+		psend(c, 'ERR ENOTCONN\n')
+		return
+	
+	
+	if cmd == 'STATUS':
+		status = string.join(params, ' ')
+		if not m.change_status(status):
+			psend(c, 'ERR UNK STATUS\n')
+		else:
+			psend(c, 'OK\n')
+		return
+		
+	if cmd == 'POLL':
+		equeue.append('POLLEND\n')
+		for evt in equeue:
+			psend(c, evt)
+		equeue = []
+		return
+	
+	if cmd == 'GETCL':
+		psend(c, 'CL %d\n' % len(m.users.keys()) )
+		for email in m.users.keys():
+			u = m.users[email]
+			status = msnlib.reverse_status[u.status]
+			psend(c, '%s %s %s\n' % (status, email, u.nick))
+		return
+	
+	if cmd == 'GETRCL':
+		psend(c, 'CL %d\n' % len(m.reverse.keys()) )
+		for email in m.reverse.keys():
+			u = m.reverse[email]
+			status = msnlib.reverse_status[u.status]
+			psend(c, '%s %s %s\n' % (status, email, u.nick))
+		return
+	
+	if cmd == 'INFO':
+		if len(params) != 1:
+			psend(c, 'ERR PARAMS\n')
+			return
+		if not m.users.has_key(email):
+			psend(c, 'ERR UNK USER\n')
+		u = m.users[email]
+		psend(c, 'email = %s\n' % email)
+		psend(c, 'nick = %s\n' % u.nick)
+		psend(c, 'homep = %s\n' % u.homep)
+		psend(c, 'workp = %s\n' % u.workp)
+		psend(c, 'mobilep = %s\n' % u.mobilep)
+		psend(c, '\n')
+		return
+	
+	if cmd == 'ADD':
+		if len(params) != 2:
+			psend(c, 'ERR PARAMS\n')
+			return
+		nick, email = params
+		m.useradd(email, nick)
+		psend(c, 'OK\n')
+		return
+	
+	if cmd == 'DEL':
+		if len(params) != 1:
+			psend(c, 'ERR PARAMS\n')
+			return
+		m.userdel(params)
+		psend(c, 'OK\n')
+		return
+	
+	if cmd == 'NICK':
+		if len(params) != 1:
+			psend(c, 'ERR PARAMS\n')
+			return
+		m.change_nick(params)
+		psend(c, 'OK\n')
+		return
+	
+	if cmd == 'PRIV':
+		if len(params) != 2:
+			psend(c, 'ERR PARAMS\n')
+			return
+		try:
+			public = int(p[0])
+			auth = int(p[1])
+			if public not in (0, 1) or auth not in (0, 1):
+				raise
+		except:
+			psend(c, 'ERR EINVAL\n')
+			return
+		m.privacy(public, auth)
+		psend(c, 'OK\n')
+		return
+	
+	if cmd == 'SENDMSG':
+		params = string.join(params, ' ')
+		params = string.split(params, '\n', 2)
+		params, msg = params
+		params = string.split(params, ' ')
+		if len(params) < 2:
+			psend(c, 'ERR PARAMS\n')
+			return
+		lines = params[0]
+		email = params[1]
+		msg = msg
+		m.sendmsg(email, msg)
+		psend(c, 'OK\n')
+		return
+		
+	# if we got here is because the command is unknown		
+	psend(c, 'ERR UNK\n')
+	return
+	
+		
+
+#
+# now the real thing
+#
+
+# void the debug
+msnlib.debug = null
+msncb.debug = null
+
+# POLL event queue
+# We implement it in a very, very efficient way: text =)
+# Yes, it's actually a list, but just because .append() is readable
+# and allow us to keep track of the number of pending events
+equeue = []
+
+# open the socket for local communication
+# we use datagram sockets to avoid complex reads and writes for now, but the
+# protocol is line-oriented and perfectly capable of working over a stream
+# socket.
+pipe = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)
+pipe.bind(('127.0.0.1', 3030))
+
+
+# loop, waiting for connections
+while 1:
+	infd = outfd = []
+	# if we are connected, poll from msn
+	if m.fd != None:
+		t = m.pollable()
+		infd = t[0]
+		outfd = t[1]
+	infd.append(pipe)
+
+	fds = select.select(infd, outfd, [], 0)
+	
+	for i in fds[0] + fds[1]:	# see msnlib.msnd.pollable.__doc__
+		if i == pipe:
+			# read from the pipe
+			pipe_read(pipe)
+		else:
+			try:
+				m.read(i)
+			except ('SocketError', socket.error), err:
+				if i != m:
+					# user closed a connection
+					# note that messages can be
+					# lost here
+					equeue.append('SCLOSE USER %s %d\n' % (i.emails[0], len(i.msgqueue)) )
+					m.close(i)
+				else:
+					# main socket closed
+					# report
+					equeue.append('SCLOSE MAIN\n')
+					quit(1)
+
+
diff --git a/utils/msnlog.vim b/utils/msnlog.vim
new file mode 100644
index 0000000..800ec75
--- /dev/null
+++ b/utils/msnlog.vim
@@ -0,0 +1,30 @@
+
+" Vim syntax file for msnlib logfiles
+" Alberto (albertogli@telpin.com.ar) 28/Sep/2003
+
+" Use it to read your msnlib log files with color, makes it much easier.
+" Install it by copying to ~/.vim/syntax/msnlog.vim and then run (from vim)
+" :set syntax=msnlog to apply it.
+
+syntax clear
+hi clear
+syntax case ignore
+
+
+syntax match	mlogMultiStr	"^\t.*$"
+syntax match	mlogIMsg	"<<< .*$"
+syntax match	mlogOMsg	">>> .*$"
+syntax match	mlogStatus	".*\*\*\* .*$"
+syntax match	mlogMchat	"+++ .*$"
+syntax match	mlogDate	"^../.../.... ..:..:.."
+
+
+hi mlogDate		ctermfg=blue
+hi mlogIMsg		ctermfg=green
+hi mlogOMsg		ctermfg=cyan
+hi mlogStatus		ctermfg=yellow
+hi mlogMchat		ctermfg=yellow
+hi mlogMultiStr		ctermfg=magenta
+
+
+
diff --git a/utils/msntk b/utils/msntk
new file mode 100644
index 0000000..a4d3376
--- /dev/null
+++ b/utils/msntk
@@ -0,0 +1,468 @@
+#!/usr/bin/env python
+
+import sys
+import time
+import string
+import socket
+import select
+from Tkinter import *
+import tkMessageBox
+import tkSimpleDialog
+
+import msnlib
+import msncb
+
+"""
+MSN Tk Client
+
+This is a beta msn client based on msnlib. As you see, it's GUI based on the
+Tk bindings, which provide an abstraction to create graphical interfaces; it
+works both under linux, windows and probably others too.
+
+For further information refer to the documentation or the source (which is
+always preferred).
+Please direct any comments to the msnlib mailing list,
+msnlib-devel@auriga.wearlab.de.
+You can find more information, and the package itself, at
+http://users.auriga.wearlab.de/~alb/msnlib
+"""
+
+
+# main msnlib classes
+m = msnlib.msnd()
+m.cb = msncb.cb()
+
+# void debug output
+#def void(s): pass
+#msnlib.debug = msncb.debug = void
+
+
+
+#
+# useful functions
+#
+
+#sys.setdefaultencoding("iso-8859-15")
+encoding = 'iso-8859-1'
+
+def encode(s):
+	try:
+		return s.decode(encoding).encode('utf-8')
+	except:
+		return s
+
+def decode(s):
+	try:
+		return s.decode('utf-8').encode(encoding)
+	except:
+		return s
+
+def nick2email(nick):
+	"Returns an email according to the given nick, or None if noone matches"
+	for email in m.users.keys():
+		if str(m.users[email].nick) == str(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 now():
+	"Returns the current time in format HH:MM:SSTT"
+	return time.strftime('%I:%M:%S%p', time.localtime(time.time()) )
+
+def quit():
+	"Cleans up and quits everything"
+	try:
+		m.disconnect()
+	except:
+		pass
+	root.quit()
+	sys.exit(0)
+
+
+
+#
+# GUI classes
+#
+
+class userlist(Frame):
+	"The user list"
+	def __init__(self, master):
+		Frame.__init__(self, master)
+		self.scrollbar = Scrollbar(self, orient = VERTICAL)
+		self.list = Listbox(self, 
+				yscrollcommand = self.scrollbar.set)
+		self.list.config(font = "Courier")
+		self.scrollbar.config(command = self.list.yview)
+		self.scrollbar.pack(side = RIGHT, fill = Y)
+		self.list.pack(side = LEFT, fill = BOTH, expand = 1)
+		
+		self.list.bind("<Double-Button-1>", self.create_chat)
+			
+	def create_chat(self, evt = None):
+		"Creates a chat window"
+		if m.status == 'HDN':
+			tkMessageBox.showwarning("Warning", 
+				"You can't open chats when you're invisible")
+			return
+		nick = self.list.get(self.list.curselection())[4:]
+		email = nick2email(nick)
+		if email in emwin.keys():
+			emwin[email].lift()
+		elif m.users[email].status == 'FLN':
+			tkMessageBox.showwarning("Warning",
+				"The user is offline")
+		else:
+			emwin[email] = chatwindow(root, email)
+	
+
+class mainmenu(Menu):
+	"Main menu used in the main window"
+	def __init__(self, master):
+		Menu.__init__(self, master)
+		self.status_menu = Menu(self, tearoff = 0)
+		self.add_cascade(label = "Status", menu = self.status_menu)
+		self.status_menu.add_command(label = "Online",
+			command = self.chst_online)
+		self.status_menu.add_command(label = "Away",
+			command = self.chst_away)
+		self.status_menu.add_command(label = "Busy",
+			command = self.chst_busy)
+		self.status_menu.add_command(label = "Be Right Back",
+			command = self.chst_brb)
+		self.status_menu.add_command(label = "Lunch",
+			command = self.chst_lunch)
+		self.status_menu.add_command(label = "Phone", 
+			command = self.chst_phone)
+		self.status_menu.add_command(label = "Invisible", 
+			command = self.chst_invisible)
+			
+		self.add_command(label = 'Info', command = self.show_info)
+	
+	def show_info(self, evt = None):
+		csel = mainlist.list.curselection()
+		if not csel:
+			return
+		nick = mainlist.list.get(csel)[4:]
+		email = nick2email(nick)
+		infowindow(root, email)
+
+	# status change callbacks
+	def clear_heads(self):
+		for i in emwin.keys():
+			emwin[i].head.config(text = '')
+	
+	def chst_online(self):
+		self.clear_heads()
+		m.change_status('online')
+	def chst_away(self):
+		self.clear_heads()
+		m.change_status('away')
+	def chst_busy(self):
+		self.clear_heads()
+		m.change_status('busy')
+	def chst_brb(self):
+		self.clear_heads()
+		m.change_status('brb')
+	def chst_lunch(self):
+		self.clear_heads()
+		m.change_status('lunch')
+	def chst_phone(self):
+		self.clear_heads()
+		m.change_status('phone')
+	def chst_invisible(self):
+		warn = "Warning: as you are invisible, it is possible that\n"
+		warn += "the messages you type here never get to the user."
+		for i in emwin.keys():
+			emwin[i].head.config(text = warn)
+		m.change_status('invisible')
+
+
+class chatwindow(Toplevel):
+	"Represents a chat window"
+	def __init__(self, master, email):
+		Toplevel.__init__(self, master)
+		self.email = email
+		self.protocol("WM_DELETE_WINDOW", self.destroy_window)
+		nick = email2nick(email)
+		# FIXME: update the title with status change
+		status = msnlib.reverse_status[m.users[email].status]
+		if nick:
+			self.wm_title(nick + ' (' + status + ')')
+		else:
+			self.wm_title(email + ' (' + status + ')')
+
+		# head label
+		self.head = Label(self)
+		self.head.pack(side = TOP, fill = X, expand = 0)
+		self.head.config(justify = LEFT)
+		self.head.config(text = "")
+
+		# text box (with scrollbar), where the message goes
+		self.frame = Frame(self)
+		self.scrollbar = Scrollbar(self.frame, orient = VERTICAL)
+		self.text = Text(self.frame, 
+				yscrollcommand = self.scrollbar.set)
+		self.scrollbar.config(command = self.text.yview)
+		self.scrollbar.pack(side = RIGHT, fill = Y)
+		self.text.pack(side = TOP, fill = BOTH, expand = 1)
+		self.frame.pack(side = TOP, fill = BOTH, expand = 1)
+		
+		self.text.config(state = DISABLED)
+		self.text.tag_config('from', foreground = 'blue')
+		self.text.tag_config('to', foreground = 'red')
+		self.text.tag_config('typing', foreground = 'lightblue')
+		
+		# entry, where the user types
+		self.entry = Entry(self)
+		self.entry.pack(side = BOTTOM, fill = X, expand = 0)
+		self.entry.bind('<Return>', self.send_line)
+	
+	def append(self, s, direction, scroll = 1):
+		"Adds text to the window's text box"
+		self.text.config(state = NORMAL)
+		self.text.insert(END, s, direction)
+		self.text.yview(SCROLL, scroll, UNITS)
+		self.text.config(state = DISABLED)
+	
+	def send_line(self, evt = None):
+		"Sends the current entry as a message"
+		msg = self.entry.get()
+		lines = msg.split('\n')
+		if len(lines) == 1:
+			s = now() + ' >>> ' + msg + '\n'
+		else:
+			s = now() + ' >>>\n\t'
+			s += string.join(lines, '\n\t')
+			s = s[:-1]
+		self.append(s, 'to', scroll = len(lines))
+		
+		# we need to encode it before sending because msg is already
+		# an unicode string; so use utf-8
+		msg = msg.encode('utf-8')
+
+		m.sendmsg(self.email, msg)
+		self.entry.delete(0, END)
+	
+	def destroy_window(self, evt = None):
+		"Clean up when the window is closed"
+		del(emwin[self.email])
+		self.destroy()
+
+
+class infowindow(Toplevel):
+	"Represents a window with user information"
+	def __init__(self, master, email):
+		Toplevel.__init__(self, master)
+		self.email = email
+		self.wm_title('Info on ' + email)
+		u = m.users[email]
+		out = ''
+		out += 'Information for user ' + email + '\n\n'
+		out += 'Nick: ' + u.nick + '\n'
+		out += 'Status: ' + msnlib.reverse_status[u.status] + '\n'
+		if 'B' in u.lists:
+			out += 'Mode: ' + 'blocked' + '\n'
+		if u.gid != None:
+			out += 'Group: ' + m.groups[u.gid] + '\n'
+		if u.realnick:
+			out += 'Real Nick: ' + u.realnick + '\n'
+		if u.homep:
+			out += 'Home phone: ' + u.homep + '\n'
+		if u.workp:
+			out += 'Work phone: ' + u.workp + '\n'
+		if u.mobilep:
+			out += 'Mobile phone: ' + u.mobilep + '\n'
+
+		self.label = Label(self)
+		self.label.pack(side = TOP, fill = BOTH, expand = 1)
+		self.label.config(justify = LEFT)
+		self.label.config(text = out)
+
+
+def redraw_main():
+	"Redraws the main screen"
+	# sync the user list - FIXME: instead of redrawing, use the callbacks
+	# for status change notifications
+	nicks = []
+	for i in m.users.keys():
+		if m.users[i].status == 'FLN':
+			s = '[X] '
+		elif m.users[i].status in ('NLN', 'IDL'):
+			s = '[ ] '
+		else:
+			s = '[-] '
+		if 'B' in m.users[i].lists:
+			s = '[!] '
+		
+		s += m.users[i].nick
+		nicks.append(s)
+	nicks.sort()
+	mainlist.list.delete(0, END)
+	for i in nicks:
+		mainlist.list.insert(END, i)
+	
+	# update status
+	s = msnlib.reverse_status[m.status]
+	status.config(text = s)
+
+
+
+#
+# callbacks
+#
+
+def cb_msg(md, type, tid, params, sbd):
+	"Gets a message"
+	t = tid.split(' ')
+	email = t[0]
+
+	# parse
+	lines = params.split('\n')
+	headers = {} 
+	eoh = 0
+	for i in lines:
+		# end of headers
+		if i == '\r':
+			break
+		tv = i.split(':', 1)
+		type = tv[0]
+		value = tv[1].strip()
+		headers[type] = value
+		eoh += 1
+	eoh +=1
+
+	# ignore hotmail messages
+	if email == 'Hotmail':
+		return
+	
+	if email not in emwin.keys():
+		emwin[email] = chatwindow(root, email)
+		
+	# typing notifications
+	if (headers.has_key('Content-Type') and 
+			headers['Content-Type'] == 'text/x-msmsgscontrol'):
+		if not m.users[email].priv.has_key('typing'):
+			m.users[email].priv['typing'] = 1
+			msg = now() + ' --- is typing\n'
+			emwin[email].append(msg, 'typing')
+			
+	# normal message
+	else:
+		if len(lines[eoh:]) > 1:
+			msg = now() + ' <<<\n\t'
+			msg += string.join(lines[eoh:], '\n\t')
+			msg = msg.replace('\r', '')
+		else:
+			msg = now() + ' <<< ' + lines[eoh] + '\n'
+			
+		if m.users[email].priv.has_key('typing'):
+			del(m.users[email].priv['typing'])
+			
+		emwin[email].append(msg, 'from')
+		root.bell()
+
+	msncb.cb_msg(md, type, tid, params, sbd)
+m.cb.msg = cb_msg
+
+
+
+#
+# main
+#
+
+# email - chatwindow dictionary
+emwin = {}
+
+# gui init
+root = Tk()
+root.wm_title('msnlib')
+
+mainlist = userlist(root)
+mainlist.pack(side = TOP, fill = BOTH, expand = 1)
+
+status = Label(root, text = "logging in...", bd=1, relief = SUNKEN, anchor = W)
+status.pack(side = BOTTOM, fill = X, expand = 0)
+
+menu = mainmenu(root)
+root.config(menu = menu)
+
+# initial update, to display at least something while we log in
+root.update()
+
+# ask for username and password if not given in the command line
+if len(sys.argv) < 3:
+	m.email = tkSimpleDialog.askstring("Username",
+		"Please insert your email")
+	if not m.email:
+		quit()
+	
+	m.pwd = tkSimpleDialog.askstring("Password",
+		"Please insert your password")
+	if not m.pwd:
+		quit()
+else:
+	m.email = sys.argv[1]
+	m.pwd = sys.argv[2]
+
+m.email = m.email.strip()
+m.pwd = m.pwd.strip()
+
+# the encoding is utf-8 because the text class uses unicode directly
+m.encoding = 'utf-8'
+
+root.update()
+
+# login
+try:
+	m.login()
+	m.sync()
+except 'AuthError':
+	tkMessageBox.showerror("Login", "Error logging in: wrong password")
+	quit()
+
+# start as invisible
+m.change_status('invisible')
+
+
+# main loop
+while 1:
+	fds = m.pollable()
+	infd = fds[0]
+	outfd = fds[1]
+	
+	try:
+		# both network and gui checks
+		fds = select.select(infd, outfd, [], 0)
+		root.update()
+	except KeyboardInterrupt:
+		quit()
+	except TclError:
+		quit()
+
+	for i in fds[0] + fds[1]:
+		try:
+			m.read(i)
+		except ('SocketError', socket.error), err:
+			if i != m:
+				m.close(i)
+			else:
+				tkMessageBox.showwarning("Warning",
+					"Server disconnected us - you " +
+					"probably logged in somewhere else")
+				quit()
+		
+		# always redraw after a network event
+		redraw_main()
+	
+	# sleep a bit so we don't take over the cpu
+	time.sleep(0.05)
+
+
diff --git a/utils/vicl b/utils/vicl
new file mode 100644
index 0000000..2a87f61
--- /dev/null
+++ b/utils/vicl
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+# yeah i work as root =)
+DIR=/root/devel/msn/msnlib/cur
+
+cd $DIR
+cd doc
+cp Changelog Changelog~
+
+DATE=`date '+%d %h %y %H.%M.%S'`
+echo "$DATE - Alberto <albertogli@telpin.com.ar>"  > Changelog
+echo " * " >> Changelog
+echo >> Changelog
+
+cat Changelog~ >> Changelog
+
+vi Changelog
+
+rm Changelog~