diff -Naur msnlib-1.0/doc/COPYING msnlib-2.0/doc/COPYING
--- msnlib-1.0/doc/COPYING	2003-01-20 14:26:15.000000000 -0300
+++ msnlib-2.0/doc/COPYING	1969-12-31 21:00:00.000000000 -0300
@@ -1,369 +0,0 @@
-
-
-This license is based (read cp'ied) from the linux kernel source tree.
-
-The following comments are based on Linus' but have been slightly modified to
-fit the library; anyway the same ideas apply.
-
-I don't like bothering with this things, and that's why i've chosen this
-license.
-
-
-NOTE! This copyright does *not* cover programs that only call/use/import the
-library and/or callbacks without modifying them - this is merely considered
-normal use of the library, and does *not* fall under the heading of "derived
-work".
-
-Also note that the GPL below is copyrighted by the Free Software Foundation,
-but the instance of code that it refers to (the msnlib) is copyrighted by me.
-This software has _nothing_ to do with the GNU movement or the FSF.
-And it's _not_ part of the GNU suite or toolset or however they call it.
-
-Finally, the only valid version of the GPL as far as the library is concerned
-is _this_ particular version of the license (ie v2, not v2.2 or v3.x or
-whatever).
-
-		Alberto Bertogli
-
-----------------------------------------
-
-		    GNU GENERAL PUBLIC LICENSE
-		       Version 2, June 1991
-
- Copyright (C) 1989, 1991 Free Software Foundation, Inc.
-                       59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
-			    Preamble
-
-  The licenses for most software are designed to take away your
-freedom to share and change it.  By contrast, the GNU General Public
-License is intended to guarantee your freedom to share and change free
-software--to make sure the software is free for all its users.  This
-General Public License applies to most of the Free Software
-Foundation's software and to any other program whose authors commit to
-using it.  (Some other Free Software Foundation software is covered by
-the GNU Library General Public License instead.)  You can apply it to
-your programs, too.
-
-  When we speak of free software, we are referring to freedom, not
-price.  Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-this service if you wish), that you receive source code or can get it
-if you want it, that you can change the software or use pieces of it
-in new free programs; and that you know you can do these things.
-
-  To protect your rights, we need to make restrictions that forbid
-anyone to deny you these rights or to ask you to surrender the rights.
-These restrictions translate to certain responsibilities for you if you
-distribute copies of the software, or if you modify it.
-
-  For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must give the recipients all the rights that
-you have.  You must make sure that they, too, receive or can get the
-source code.  And you must show them these terms so they know their
-rights.
-
-  We protect your rights with two steps: (1) copyright the software, and
-(2) offer you this license which gives you legal permission to copy,
-distribute and/or modify the software.
-
-  Also, for each author's protection and ours, we want to make certain
-that everyone understands that there is no warranty for this free
-software.  If the software is modified by someone else and passed on, we
-want its recipients to know that what they have is not the original, so
-that any problems introduced by others will not reflect on the original
-authors' reputations.
-
-  Finally, any free program is threatened constantly by software
-patents.  We wish to avoid the danger that redistributors of a free
-program will individually obtain patent licenses, in effect making the
-program proprietary.  To prevent this, we have made it clear that any
-patent must be licensed for everyone's free use or not licensed at all.
-
-  The precise terms and conditions for copying, distribution and
-modification follow.
-
-		    GNU GENERAL PUBLIC LICENSE
-   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
-
-  0. This License applies to any program or other work which contains
-a notice placed by the copyright holder saying it may be distributed
-under the terms of this General Public License.  The "Program", below,
-refers to any such program or work, and a "work based on the Program"
-means either the Program or any derivative work under copyright law:
-that is to say, a work containing the Program or a portion of it,
-either verbatim or with modifications and/or translated into another
-language.  (Hereinafter, translation is included without limitation in
-the term "modification".)  Each licensee is addressed as "you".
-
-Activities other than copying, distribution and modification are not
-covered by this License; they are outside its scope.  The act of
-running the Program is not restricted, and the output from the Program
-is covered only if its contents constitute a work based on the
-Program (independent of having been made by running the Program).
-Whether that is true depends on what the Program does.
-
-  1. You may copy and distribute verbatim copies of the Program's
-source code as you receive it, in any medium, provided that you
-conspicuously and appropriately publish on each copy an appropriate
-copyright notice and disclaimer of warranty; keep intact all the
-notices that refer to this License and to the absence of any warranty;
-and give any other recipients of the Program a copy of this License
-along with the Program.
-
-You may charge a fee for the physical act of transferring a copy, and
-you may at your option offer warranty protection in exchange for a fee.
-
-  2. You may modify your copy or copies of the Program or any portion
-of it, thus forming a work based on the Program, and copy and
-distribute such modifications or work under the terms of Section 1
-above, provided that you also meet all of these conditions:
-
-    a) You must cause the modified files to carry prominent notices
-    stating that you changed the files and the date of any change.
-
-    b) You must cause any work that you distribute or publish, that in
-    whole or in part contains or is derived from the Program or any
-    part thereof, to be licensed as a whole at no charge to all third
-    parties under the terms of this License.
-
-    c) If the modified program normally reads commands interactively
-    when run, you must cause it, when started running for such
-    interactive use in the most ordinary way, to print or display an
-    announcement including an appropriate copyright notice and a
-    notice that there is no warranty (or else, saying that you provide
-    a warranty) and that users may redistribute the program under
-    these conditions, and telling the user how to view a copy of this
-    License.  (Exception: if the Program itself is interactive but
-    does not normally print such an announcement, your work based on
-    the Program is not required to print an announcement.)
-
-These requirements apply to the modified work as a whole.  If
-identifiable sections of that work are not derived from the Program,
-and can be reasonably considered independent and separate works in
-themselves, then this License, and its terms, do not apply to those
-sections when you distribute them as separate works.  But when you
-distribute the same sections as part of a whole which is a work based
-on the Program, the distribution of the whole must be on the terms of
-this License, whose permissions for other licensees extend to the
-entire whole, and thus to each and every part regardless of who wrote it.
-
-Thus, it is not the intent of this section to claim rights or contest
-your rights to work written entirely by you; rather, the intent is to
-exercise the right to control the distribution of derivative or
-collective works based on the Program.
-
-In addition, mere aggregation of another work not based on the Program
-with the Program (or with a work based on the Program) on a volume of
-a storage or distribution medium does not bring the other work under
-the scope of this License.
-
-  3. You may copy and distribute the Program (or a work based on it,
-under Section 2) in object code or executable form under the terms of
-Sections 1 and 2 above provided that you also do one of the following:
-
-    a) Accompany it with the complete corresponding machine-readable
-    source code, which must be distributed under the terms of Sections
-    1 and 2 above on a medium customarily used for software interchange; or,
-
-    b) Accompany it with a written offer, valid for at least three
-    years, to give any third party, for a charge no more than your
-    cost of physically performing source distribution, a complete
-    machine-readable copy of the corresponding source code, to be
-    distributed under the terms of Sections 1 and 2 above on a medium
-    customarily used for software interchange; or,
-
-    c) Accompany it with the information you received as to the offer
-    to distribute corresponding source code.  (This alternative is
-    allowed only for noncommercial distribution and only if you
-    received the program in object code or executable form with such
-    an offer, in accord with Subsection b above.)
-
-The source code for a work means the preferred form of the work for
-making modifications to it.  For an executable work, complete source
-code means all the source code for all modules it contains, plus any
-associated interface definition files, plus the scripts used to
-control compilation and installation of the executable.  However, as a
-special exception, the source code distributed need not include
-anything that is normally distributed (in either source or binary
-form) with the major components (compiler, kernel, and so on) of the
-operating system on which the executable runs, unless that component
-itself accompanies the executable.
-
-If distribution of executable or object code is made by offering
-access to copy from a designated place, then offering equivalent
-access to copy the source code from the same place counts as
-distribution of the source code, even though third parties are not
-compelled to copy the source along with the object code.
-
-  4. You may not copy, modify, sublicense, or distribute the Program
-except as expressly provided under this License.  Any attempt
-otherwise to copy, modify, sublicense or distribute the Program is
-void, and will automatically terminate your rights under this License.
-However, parties who have received copies, or rights, from you under
-this License will not have their licenses terminated so long as such
-parties remain in full compliance.
-
-  5. You are not required to accept this License, since you have not
-signed it.  However, nothing else grants you permission to modify or
-distribute the Program or its derivative works.  These actions are
-prohibited by law if you do not accept this License.  Therefore, by
-modifying or distributing the Program (or any work based on the
-Program), you indicate your acceptance of this License to do so, and
-all its terms and conditions for copying, distributing or modifying
-the Program or works based on it.
-
-  6. Each time you redistribute the Program (or any work based on the
-Program), the recipient automatically receives a license from the
-original licensor to copy, distribute or modify the Program subject to
-these terms and conditions.  You may not impose any further
-restrictions on the recipients' exercise of the rights granted herein.
-You are not responsible for enforcing compliance by third parties to
-this License.
-
-  7. If, as a consequence of a court judgment or allegation of patent
-infringement or for any other reason (not limited to patent issues),
-conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License.  If you cannot
-distribute so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you
-may not distribute the Program at all.  For example, if a patent
-license would not permit royalty-free redistribution of the Program by
-all those who receive copies directly or indirectly through you, then
-the only way you could satisfy both it and this License would be to
-refrain entirely from distribution of the Program.
-
-If any portion of this section is held invalid or unenforceable under
-any particular circumstance, the balance of the section is intended to
-apply and the section as a whole is intended to apply in other
-circumstances.
-
-It is not the purpose of this section to induce you to infringe any
-patents or other property right claims or to contest validity of any
-such claims; this section has the sole purpose of protecting the
-integrity of the free software distribution system, which is
-implemented by public license practices.  Many people have made
-generous contributions to the wide range of software distributed
-through that system in reliance on consistent application of that
-system; it is up to the author/donor to decide if he or she is willing
-to distribute software through any other system and a licensee cannot
-impose that choice.
-
-This section is intended to make thoroughly clear what is believed to
-be a consequence of the rest of this License.
-
-  8. If the distribution and/or use of the Program is restricted in
-certain countries either by patents or by copyrighted interfaces, the
-original copyright holder who places the Program under this License
-may add an explicit geographical distribution limitation excluding
-those countries, so that distribution is permitted only in or among
-countries not thus excluded.  In such case, this License incorporates
-the limitation as if written in the body of this License.
-
-  9. The Free Software Foundation may publish revised and/or new versions
-of the General Public License from time to time.  Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
-Each version is given a distinguishing version number.  If the Program
-specifies a version number of this License which applies to it and "any
-later version", you have the option of following the terms and conditions
-either of that version or of any later version published by the Free
-Software Foundation.  If the Program does not specify a version number of
-this License, you may choose any version ever published by the Free Software
-Foundation.
-
-  10. If you wish to incorporate parts of the Program into other free
-programs whose distribution conditions are different, write to the author
-to ask for permission.  For software which is copyrighted by the Free
-Software Foundation, write to the Free Software Foundation; we sometimes
-make exceptions for this.  Our decision will be guided by the two goals
-of preserving the free status of all derivatives of our free software and
-of promoting the sharing and reuse of software generally.
-
-			    NO WARRANTY
-
-  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
-FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
-OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
-PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
-OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
-MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
-TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
-PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
-REPAIR OR CORRECTION.
-
-  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
-REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
-INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
-OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
-TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
-YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
-PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGES.
-
-		     END OF TERMS AND CONDITIONS
-
-	    How to Apply These Terms to Your New Programs
-
-  If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
-  To do so, attach the following notices to the program.  It is safest
-to attach them to the start of each source file to most effectively
-convey the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-    <one line to give the program's name and a brief idea of what it does.>
-    Copyright (C) <year>  <name of author>
-
-    This program is free software; you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation; either version 2 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License
-    along with this program; if not, write to the Free Software
-    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
-
-
-Also add information on how to contact you by electronic and paper mail.
-
-If the program is interactive, make it output a short notice like this
-when it starts in an interactive mode:
-
-    Gnomovision version 69, Copyright (C) year name of author
-    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
-    This is free software, and you are welcome to redistribute it
-    under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License.  Of course, the commands you use may
-be called something other than `show w' and `show c'; they could even be
-mouse-clicks or menu items--whatever suits your program.
-
-You should also get your employer (if you work as a programmer) or your
-school, if any, to sign a "copyright disclaimer" for the program, if
-necessary.  Here is a sample; alter the names:
-
-  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
-  `Gnomovision' (which makes passes at compilers) written by James Hacker.
-
-  <signature of Ty Coon>, 1 April 1989
-  Ty Coon, President of Vice
-
-This General Public License does not permit incorporating your program into
-proprietary programs.  If your program is a subroutine library, you may
-consider it more useful to permit linking proprietary applications with the
-library.  If this is what you want to do, use the GNU Library General
-Public License instead of this License.
diff -Naur msnlib-1.0/doc/Changelog msnlib-2.0/doc/Changelog
--- msnlib-1.0/doc/Changelog	2003-05-01 19:20:50.000000000 -0300
+++ msnlib-2.0/doc/Changelog	2003-06-08 19:11:31.000000000 -0300
@@ -1,3 +1,61 @@
+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
 
diff -Naur msnlib-1.0/doc/LICENSE msnlib-2.0/doc/LICENSE
--- msnlib-1.0/doc/LICENSE	1969-12-31 21:00:00.000000000 -0300
+++ msnlib-2.0/doc/LICENSE	2003-05-20 18:51:13.000000000 -0300
@@ -0,0 +1,190 @@
+
+This project, 'msnlib', is copyrighted by Alberto Bertogli and licensed under
+the "Open Software License v1.1" 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
+
+
+[ This copy of the license is the flat-text version of original,
+  available in its full glory at
+
+	 http://www.opensource.org/licenses/osl.php
+
+  please refer to there for the authorative and slightly more
+  pretty-printed version ]
+
+------
+
+			The Open Software License
+			v. 1.1
+
+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 1.1
+
+
+1) Grant of Copyright License. Licensor hereby grants You a world-wide, 
+royalty-free, non-exclusive, perpetual, non-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, non-sublicenseable license, under 
+patent claims owned or controlled by the Licensor that are embodied in the 
+Original Work as furnished by the Licensor ("Licensed Claims") to make, use, 
+sell and offer for sale the Original Work. Licensor hereby grants You a 
+world-wide, royalty-free, non-exclusive, perpetual, non-sublicenseable license 
+under the Licensed Claims to make, use, sell and offer for sale 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. 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 and Disclaimer of Warranty. Licensor warrants that the copyright 
+in and to the Original Work is owned by the Licensor or that the Original Work 
+is distributed by Licensor under a valid current license from the copyright 
+owner. 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 and volitional 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 Sections 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 Sections 
+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) Mutual 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 if You file a lawsuit in any court alleging that any OSI 
+Certified open source software that is licensed under any license containing 
+this "Mutual Termination for Patent Action" clause infringes any patent claims 
+that are essential to use that software.
+
+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) 2002 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 -Naur msnlib-1.0/doc/TODO msnlib-2.0/doc/TODO
--- msnlib-1.0/doc/TODO	2003-01-20 14:26:15.000000000 -0300
+++ msnlib-2.0/doc/TODO	2003-06-07 15:40:55.000000000 -0300
@@ -28,31 +28,13 @@
 	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.
-* create a detached client with a server/client model
-	this can be either a one-pc-only detached mode or maybe some kind of
-	web interface; after all it should have very much in common, and with
-	a couple of cookie handling and a browser which supports auto-refresh,
-	a piece of cake (and java-free)
-* make colors configurable
-	either as colors or as random strings, so you can, for instance,
-	output in html. this is here just because it's really low priority
 
 
 msn lib 
-* higher level callbacks
-	I'm not that sure about this one.. it would be really nice to have ALL
-	the protocol encapsulated and then call upper-level callbacks if the
-	user just don't want to care about, for instance, message parsing.
-	But, OTOH, maybe it's not worthy.
 * make login async (maybe using something like continuations?)
 	continuations would have worked nicely, but are only available in
 	python >= 2.2 (which many people don't have), so it will have to wait
 	for a bit
-* group support (part is in place (eg. gid))
-	do we really want this? If not, drop partial support
-* multi-user chat handling 
- 	part in place, the ugly sb.emails[0]
-	is this useful? If not, drop partial support
 * 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
diff -Naur msnlib-1.0/doc/icons msnlib-2.0/doc/icons
--- msnlib-1.0/doc/icons	1969-12-31 21:00:00.000000000 -0300
+++ msnlib-2.0/doc/icons	2003-06-05 17:38:25.000000000 -0300
@@ -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 -Naur msnlib-1.0/msn msnlib-2.0/msn
--- msnlib-1.0/msn	2003-05-01 19:18:47.000000000 -0300
+++ msnlib-2.0/msn	2003-06-08 19:13:09.000000000 -0300
@@ -20,11 +20,57 @@
 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). 
+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"
+lignore [nick]	Locally ignores the user, or display the locally ignored users
+lunignore nick	Removes a user from the locally ignored users list
+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
 #
@@ -103,7 +149,7 @@
 	safe_write('\n')
 	safe_flush()
 
-def print_list(md, only_online = None, userlist = None, include_emails = 0):
+def print_list(md, only_online = 0, userlist = None, include_emails = 0):
 	"Prints the user list"
 	if not userlist:
 		userlist = md.users
@@ -112,15 +158,48 @@
 	for email in ul:
 		u = userlist[email]
 		if u.status != 'FLN': 
-			safe_write(c.bold)
+			hl = 1
 		else:
-			if only_online:		# this is not nice, but it's simple enough
-				continue
+			if only_online:	continue
+			hl = 0
 		status = msnlib.reverse_status[u.status]
-		print '%7.7s :: %s' % (status, u.nick),
-		if include_emails: print '(%s)' % (email),
-		print c.normal
-
+		printl('%7.7s :: %s ' % (status, u.nick), bold = hl)
+		if include_emails: printl('(%s) ' % (email), bold = hl)
+		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)
+			printl('\n')
+			
 def print_user_info(email):
 	"Prints the user information, and pending messages"
 	u = m.users[email]
@@ -128,6 +207,8 @@
 	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'
+	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'
@@ -244,6 +325,13 @@
 	else:
 		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:
@@ -538,7 +626,6 @@
 		safe_write('\r' + (screenwidth - 1) * ' ' + '\r')
 
 
-
 #
 # stdin command parser
 #
@@ -571,7 +658,7 @@
 		if not params:
 			return 'Your current status is %s' % msnlib.reverse_status[m.status]
 		if not m.change_status(params):
-			return 'Status must be one of: online, away, busy, brb, phone, lunch, invisible, idle or offline'
+			return 'Status must be one of: online, away, busy, brb, phone, lunch, invisible or idle'
 		return 'Status changed to: %s' % params
 		
 	elif cmd == 'q':		# quit
@@ -582,27 +669,33 @@
 		m.cb = msncb.cb()
 	
 	elif cmd == 'w':		# list
-		print_list(m)
+		print_grouped_list(m)
 	
 	elif cmd == 'ww':		# list, include emails
-		print_list(m, include_emails = 1)
+		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_list(m, only_online = 1, include_emails = 1)
+		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:
-			c = params[0:3]
-			p = params[4:]
+			cmd = params[0:3]
+			pars = params[4:]
 		except:
 			return 'Error parsing command'
-		m._send(c, p)
+		m._send(cmd, pars)
 	
 	elif cmd == 'debug':		# enable/disable debugging
 		p = params.split()
@@ -698,10 +791,20 @@
 			return 'Error parsing command'
 		elif len(p) == 1: 
 			email = nick = p[0]
-		else: 
+			gid = '0'
+		elif len(p) == 2: 
+			email = p[0]
+			nick = p[1]
+			gid = '0'
+		else:
 			email = p[0]
 			nick = p[1]
-		m.useradd(email, nick)
+			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()
@@ -711,6 +814,53 @@
 			return 'Unknown nick (%s)' % p[0]
 		m.userdel(email)
 	
+	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
 		p = params.split()
 		if len(p) != 1: return 'Error parsing command'
@@ -775,8 +925,11 @@
 			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':
+		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:
@@ -790,34 +943,7 @@
 			return 'Error %d sending message' % r
 		
 	elif cmd == 'help' or cmd == '?':
-		r = ''
-		r += 'Command list:\n'
-		r += 'status [mode]\t Shows the current status, or changes it to "mode", which can be one of:\n'
-		r += '\t\t\t online, away, busy, brb, phone, lunch, invisible, idle or offline\n'
-		r += 'q\t\t Quits the program\n'
-		r += 'w\t\t Prints your entire contact list\n'
-		r += 'ww\t\t Prints your entire contact list, including email addresses\n'
-		r += 'e\t\t Prints your online contacts\n'
-		r += 'ee\t\t Prints your online contacts, including email addresses\n'
-		r += 'wr\t\t Prints your reverse contact list\n'
-		r += 'h\t\t Shows your incoming message history\n'
-		r += 'add email nick\t Adds the user "email" with the nick "nick"\n'
-		r += 'del nick\t Deletes the user with nick "nick"\n'
-		r += 'info [nick]\t Shows the user information and pending messages (if any), or our personal info\n'
-		r += 'lignore [nick]\t Locally ignores the user, or display the locally ignored users list\n'
-		r += 'lunignore nick\t Removes a user from the locally ignored users list\n'
-		r += 'color [theme]\t Shows or set the color theme to "theme"\n'
-		r += 'close nick\t Closes the switchboard connection with "nick"\n'
-		r += 'config\t\t Shows the configuration\n'
-		r += 'nick newnick\t Changes your nick to "newnick"\n'
-		r += 'privacy p a\t Sets whether accept messages from people not on your list (p) and require authorization (a)\n'
-		r += 'm nick text\t Sends a message to "nick" with the "text"\n'
-		r += 'a text\t\t Sends a message with "text" to the last person you sent a message to\n'
-		r += 'r text\t\t Sends a message with "text" to the last person that sent you a message\n'
-		r += '\n'
-		r += 'In most cases, where you are asked for a nick, you can alternatively enter the email.\n'
-		r += 'This makes it easier to handle people with weird or long nicks.\n'
-		return r
+		return help
 	else:
 		return 'Unknown command, type "help" for help'
 	
@@ -836,7 +962,7 @@
 
 # status change
 def cb_iln(md, type, tid, params):
-	t = params.split()
+	t = params.split(' ')
 	status = msnlib.reverse_status[t[0]]
 	email = t[1]
 	nick = md.users[email].nick
@@ -851,7 +977,7 @@
 
 def cb_nln(md, type, tid, params):
 	status = msnlib.reverse_status[tid]
-	t = string.split(params)
+	t = params.split(' ')
 	email = t[0]
 	nick = md.users[email].nick
 	ctime = time.strftime('%I:%M:%S%p', now())
@@ -885,11 +1011,22 @@
 	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)
+	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 = string.split(tid)
+	t = tid.split(' ')
 	email = t[0]
 	
 	# messages from hotmail are only when we connect, and send things
@@ -901,14 +1038,14 @@
 		return
 
 	# parse
-	lines = string.split(params, '\n')
+	lines = params.split('\n')
 	headers = {}
 	eoh = 0
 	for i in lines:
 		# end of headers
 		if i == '\r':
 			break
-		tv = string.split(i, ':')
+		tv = i.split(':')
 		type = tv[0]
 		value = string.join(tv[1:], ':')
 		value = string.strip(value)
@@ -949,9 +1086,14 @@
 # join a conversation and send pending messages
 def cb_joi(md, type, tid, params, sbd):
 	email = tid
-	if len(sbd.msgqueue) > 0:
-		nick = email2nick(email)
-		if not nick: nick = email
+	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)
+	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)
@@ -960,6 +1102,28 @@
 	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)
+	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):
@@ -973,7 +1137,7 @@
 	
 # users add, delete and modify
 def cb_add(md, type, tid, params):
-	t = params.split()
+	t = params.split(' ')
 	type = t[0]
 	if type == 'RL' or type == 'FL':
 		email = t[2]
@@ -989,7 +1153,7 @@
 m.cb.add = cb_add
 
 def cb_rem(md, type, tid, params):
-	t = params.split()
+	t = params.split(' ')
 	type = t[0]
 	if type == 'RL' or type == 'FL':
 		email = t[2]
@@ -1003,12 +1167,49 @@
 	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 (1.0) *\n', c.yellow, 1)
+printl('* MSN Client (2.0) *\n', c.yellow, 1)
 
 # first, the configuration
 printl('Loading config... ', c.green, 1)
diff -Naur msnlib-1.0/msncb.py msnlib-2.0/msncb.py
--- msnlib-1.0/msncb.py	2003-03-05 13:57:03.000000000 -0300
+++ msnlib-2.0/msncb.py	2003-06-07 16:16:22.000000000 -0300
@@ -33,10 +33,9 @@
 		Alberto (albertogli@telpin.com.ar)
 """
 
-import sys
-def debug(s):
-	sys.stderr.write('\r' + str(s) + '\n')
-	sys.stderr.flush()
+
+# use the debug function from msnlib
+debug = msnlib.debug
 
 
 class cb:
@@ -57,12 +56,15 @@
 		self.gtc = cb_ign	# add notification
 		self.syn = cb_ign	# list sync confirmation
 		self.prp = cb_prp	# private info
-		self.lsg = cb_ign	# group support, TODO
+		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_ign	# multi-user chat, TODO
+		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
@@ -156,13 +158,14 @@
 
 def cb_iln(md, type, tid, params):
 	"Handles a friend status change"
-	t = params.split()
+	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))
 
 
@@ -171,25 +174,24 @@
 	email = tid
 	debug('FRIEND %s disconnected (%s)' % (email, type))
 	md.users[email].status = type
-	if md.users[email].sbd:
-		md.close(md.users[email].sbd)
 
 
 def cb_nln(md, type, tid, params):
 	"Handles a friend status change"
 	status = tid
-	t = string.split(params)
+	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"
-	t = params.split()
+	t = params.split(' ')
 	email = t[0]
 	type = t[1]
 	if len(t) > 2: param = urllib.unquote(t[2])
@@ -204,7 +206,7 @@
 
 
 def cb_lst(md, type, tid, params):
-	p = params.split()
+	p = params.split(' ')
 	reqtype = p[0]
 
 	# forward list
@@ -215,6 +217,7 @@
 			
 		lver, uid, ucount, email, nick, gid = p[1:]
 		nick = urllib.unquote(nick)
+		gid = gid.split(',')[0]
 		if email not in md.users.keys():
 			md.users[email] = msnlib.user(email, nick, gid) 
 		else:
@@ -235,9 +238,20 @@
 		pass
 
 
+def cb_lsg(md, type, tid, params):
+	"Handles group list"
+	p = params.split(' ')
+	lver, gnum, gcount, gid, 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()
+	t = params.split(' ')
 	type = t[0]
 	if len(t) > 1: param = urllib.unquote(t[1])
 	else: param = ''
@@ -251,7 +265,7 @@
 def cb_add(md, type, tid, params):
 	"""Handles a user add. 
 	Only make something in the case of a user adding you"""
-	t = params.split()
+	t = params.split(' ')
 	type = t[0]
 	if type == 'RL':
 		email = t[2]
@@ -260,7 +274,8 @@
 	elif type == 'FL':
 		email = t[2]
 		nick = urllib.unquote(t[3])
-		md.users[email] = msnlib.user(email, nick)
+		gid = t[4]
+		md.users[email] = msnlib.user(email, nick, gid)
 		debug('ADD: adding %s (%s)' % (email, nick))
 	else:
 		pass
@@ -269,7 +284,7 @@
 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()
+	t = params.split(' ')
 	type = t[0]
 	if type == 'RL':
 		email = t[2]
@@ -281,10 +296,34 @@
 	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:
+			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()
+	t = params.split(' ')
 	email = t[1]
 	nick = urllib.unquote(t[2])
 	if email != md.email:
@@ -296,7 +335,7 @@
 
 def cb_rng(md, type, tid, params):
 	"Handles switchboard invitations."
-	t = params.split()
+	t = params.split(' ')
 	sid = tid
 	ip, port = t[0].split(':')
 	port = int(port)
@@ -307,14 +346,8 @@
 	# 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.
-	# the connect() is surrounded by a try/except because it throws an
-	# exception
 	fd.setblocking(0)
-	try:
-		fd.connect((ip, port))
-	except socket.error:
-		pass
-	
+	fd.connect_ex((ip, port))
 	
 	sbd = msnlib.sbd()
 	sbd.fd = fd
@@ -330,26 +363,24 @@
 
 def cb_xfr(md, type, tid, params):
 	"Handles switchboard requests"
-	t = params.split()
+	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
-	try:
-		fd.connect((ip, port))
-	except socket.error:
-		pass
+	fd.connect_ex((ip, port))
 	
-	# ugly, but we have to do this
+	# look for the sbd, matching the tid
 	sbd = None
-	for sbd in md.sb_fds:
-		if sbd.state == 'xf':
+	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
+		raise 'XFRError'
 	
 	sbd.fd = fd
 	sbd.block = 0
@@ -357,6 +388,19 @@
 	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"
@@ -368,10 +412,19 @@
 def cb_joi(md, type, tid, params, sbd):
 	"Handles a switchboard join, and sends the pending messages"
 	email = tid
-	sbd.state = 'es'
-	debug('CALL: user %s replied your chat request; flushing' % email)
-	md.sendmsg(email)
-	debug('CALL: message queue for %s flushed' % email)
+	# 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):
@@ -397,6 +450,12 @@
 
 def cb_bye(md, type, tid, params, sbd):
 	"Handles a user sb disconnect"
-	debug('BYE: closing %s' % str(sbd))
-	md.close(sbd)
+	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 -Naur msnlib-1.0/msnlib.py msnlib-2.0/msnlib.py
--- msnlib-1.0/msnlib.py	2003-05-01 19:07:28.000000000 -0300
+++ msnlib-2.0/msnlib.py	2003-06-08 19:13:17.000000000 -0300
@@ -14,7 +14,7 @@
 """
 
 # constants
-VERSION = 0x100
+VERSION = 0x0200
 LOGIN_HOST = 'messenger.hotmail.com'
 LOGIN_PORT = 1863
 
@@ -50,9 +50,10 @@
 
 class user:
 	"""User class, used to store your 'friends'"""
-	def __init__(self, email = None, nick = None, gid = None):
+	def __init__(self, email = '', nick = '', gid = None):
 		self.email = email
 		self.nick = nick
+		self.realnick = ''
 		self.status = 'FLN'
 		self.online = 0
 		self.gid = gid
@@ -99,10 +100,11 @@
 		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: email:%s state:%s block:%d endpoint:%s>' % \
-			(self.emails[0], self.state, self.block, self.endpoint)
+		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()
@@ -174,6 +176,7 @@
 
 		self.users = {}			# forward user list
 		self.reverse = {}		# reverse user list
+		self.groups = {}		# group list
 		
 	
 	def __repr__(self):
@@ -266,8 +269,9 @@
 		
 		if c == '':
 			raise 'SocketError'
-
-		pbuf = string.split(buf)
+		
+		buf = buf.strip()
+		pbuf = buf.split(' ')
 		
 		cmd = pbuf[0]
 		
@@ -275,7 +279,7 @@
 		# the most common) so we cover our backs
 		if len(pbuf) >= 3:
 			tid = pbuf[1]
-			params = string.join(self.decode(pbuf[2:]))
+			params = self.decode(string.join(pbuf[2:]))
 		elif len(pbuf) == 2:
 			tid = pbuf[1]
 			params = ''
@@ -310,10 +314,12 @@
 		be able to do operations on users that are not in our server
 		list."""
 		
-		email = sbd.emails[0]
 		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:
+			self.close(self.users[email].sbd)
 		self.users[email].sbd = sbd
 		return
 	
@@ -354,12 +360,12 @@
 		return 1
 	
 
-	def useradd(self, email, nick = None):
+	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 + ' 0')
+		self._send('ADD', 'FL ' + email + ' ' + nick + ' ' + gid)
 		return 1
 	
 
@@ -369,6 +375,21 @@
 		self._send('REM', 'FL ' + email)
 		return 1
 	
+	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"
@@ -381,12 +402,17 @@
 		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"
 
@@ -466,7 +492,10 @@
 		if nd in self.sb_fds:
 			# connect pending
 			if nd.state == 'cp':
-				nd.fd.connect(nd.endpoint)
+				try:
+					nd.fd.connect(nd.endpoint)
+				except:
+					raise 'SocketError'
 				nd.fd.setblocking(1)
 				nd.block = 1
 				nd.state = 're'
@@ -508,9 +537,12 @@
 		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)
 		
+		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)
@@ -573,6 +605,10 @@
 			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
 		
diff -Naur msnlib-1.0/setup.py msnlib-2.0/setup.py
--- msnlib-1.0/setup.py	2003-05-01 19:07:41.000000000 -0300
+++ msnlib-2.0/setup.py	2003-06-08 19:13:40.000000000 -0300
@@ -2,11 +2,11 @@
 from distutils.core import setup
 
 setup(name="msnlib",
-	version="1.0",
+	version="2.0",
 	description="MSN Messenger Library and Client",
 	author="Alberto Bertogli",
 	author_email="albertogli@telpin.com.ar",
-	url="http://users.auriga.wearlab.de/~alb/",
+	url="http://users.auriga.wearlab.de/~alb/msnlib",
 	py_modules=['msnlib', 'msncb'],
 )
 									 
diff -Naur msnlib-1.0/utils/hmerge msnlib-2.0/utils/hmerge
--- msnlib-1.0/utils/hmerge	1969-12-31 21:00:00.000000000 -0300
+++ msnlib-2.0/utils/hmerge	2003-06-02 17:08:22.000000000 -0300
@@ -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
+		
+
