Bug 955678 - message.servername not set when hostname does not have . or : in it (e.g. localhost) [str is undefined in irc.js:839]. r=aleth
authorPatrick Cloke <clokep@gmail.com>
Wed, 22 Oct 2014 07:10:13 -0400 (2014-10-22)
changeset 16975 0ac236b0be380235fa0092bee0255d08c1e88ad3
parent 16974 223659495f31bce6ee77868a3e8c613935fd4209
child 16976 332650fa4e8d2d0eb5ddf77c58a345ac9791fad9
push id10542
push userclokep@gmail.com
push dateWed, 29 Oct 2014 02:55:46 +0000 (2014-10-29)
treeherdercomm-central@7d0449dffea8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersaleth
bugs955678
Bug 955678 - message.servername not set when hostname does not have . or : in it (e.g. localhost) [str is undefined in irc.js:839]. r=aleth
chat/locales/en-US/irc.properties
chat/protocols/irc/irc.js
chat/protocols/irc/ircBase.jsm
chat/protocols/irc/ircCTCP.jsm
chat/protocols/irc/ircNonStandard.jsm
chat/protocols/irc/ircServices.jsm
chat/protocols/irc/ircUtils.jsm
chat/protocols/irc/test/test_ircMessage.js
im/test/xpcshell.ini
--- a/chat/locales/en-US/irc.properties
+++ b/chat/locales/en-US/irc.properties
@@ -8,16 +8,17 @@
 #  configuring an IRC account.
 irc.usernameHint=nick
 
 # LOCALIZATION NOTE (connection.error.*):
 #   These will show in the account manager if the account is
 #   disconnected because of an error.
 connection.error.lost=Lost connection with server
 connection.error.timeOut=Connection timed out
+connection.error.invalidUsername=%S is not an allowed username
 connection.error.invalidPassword=Invalid server password
 connection.error.passwordRequired=Password required
 
 # LOCALIZATION NOTE (joinChat.*):
 #   These show up on the join chat menu. An underscore is for the access key.
 joinChat.channel=_Channel
 joinChat.password=_Password
 
--- a/chat/protocols/irc/irc.js
+++ b/chat/protocols/irc/irc.js
@@ -20,30 +20,36 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
 /*
  * Parses a raw IRC message into an object (see section 2.3 of RFC 2812). This
  * returns an object with the following fields:
  *   rawMessage The initial message string received without any processing.
  *   command    A string that is the command or response code.
  *   params     An array of strings for the parameters. The last parameter is
  *              stripped of its : prefix.
- * If the message is from a user:
- *   nickname   The user's nickname.
+ *   origin     The user's nickname or the server who sent the message. Can be
+ *              a host (e.g. irc.mozilla.org) or an IPv4 address (e.g. 1.2.3.4)
+ *              or an IPv6 address (e.g. 3ffe:1900:4545:3:200:f8ff:fe21:67cf).
  *   user       The user's username, note that this can be undefined.
  *   host       The user's hostname, note that this can be undefined.
  *   source     A "nicely" formatted combination of user & host, which is
  *              <user>@<host> or <user> if host is undefined.
- * Otherwise if it's from a server:
- *   servername This is the address of the server as a host (e.g.
- *              irc.mozilla.org) or an IPv4 address (e.g. 1.2.3.4) or IPv6
- *              address (e.g. 3ffe:1900:4545:3:200:f8ff:fe21:67cf).
+ *
+ * There are cases (e.g. localhost) where it cannot be easily determined if a
+ * message is from a server or from a user, thus the usage of a generic "origin"
+ * instead of "nickname" or "servername".
+ *
+ * Inputs:
+ *  aData       The raw string to parse, it should already have the \r\n
+ *              stripped from the end.
+ *  aOrigin     The default origin to use for unprefixed messages.
  */
-function ircMessage(aData) {
+function ircMessage(aData, aOrigin) {
   let message = {rawMessage: aData};
-  let temp, prefix;
+  let temp;
 
   // Splits the raw string into four parts (the second is required), the command
   // is required. A raw string looks like:
   //   [":" <prefix> " "] <command> [" " <parameter>]* [":" <last parameter>]
   //     <prefix>: :(<server name> | <nickname> [["!" <user>] "@" <host>])
   //     <command>: /[^ ]+/
   //     <parameter>: /[^ ]+/
   //     <last parameter>: /.+/
@@ -52,42 +58,42 @@ function ircMessage(aData) {
   // 2812 would allow. It allows for empty parameters (besides the last
   // parameter, which can always be empty), by allowing multiple spaces.
   // (This is for compatibility with Unreal's 432 response, which returns an
   // empty first parameter.) It also allows a trailing space after the
   // <parameter>s when no <last parameter> is present (also occurs with Unreal).
   if (!(temp = aData.match(/^(?::([^ ]+) )?([^ ]+)((?: +[^: ][^ ]*)*)? *(?::([\s\S]*))?$/)))
     throw "Couldn't parse message: \"" + aData + "\"";
 
-  // Assume message is from the server if not specified
-  prefix = temp[1];
   message.command = temp[2];
   // Space separated parameters. Since we expect a space as the first thing
   // here, we want to ignore the first value (which is empty).
   message.params = temp[3] ? temp[3].split(" ").slice(1) : [];
   // Last parameter can contain spaces or be an empty string.
   if (temp[4] != undefined)
     message.params.push(temp[4]);
 
-  // The source string can be split into multiple parts as:
-  //   :(server|nickname[[!user]@host])
-  // If the source contains a . or a :, assume it's a server name. See RFC
-  // 2812 Section 2.3 definition of servername vs. nickname.
-  if (prefix &&
-      (temp = prefix.match(/^([^ !@\.:]+)(?:!([^ @]+))?(?:@([^ ]+))?$/))) {
-    message.nickname = temp[1];
-    message.user = temp[2] || null; // Optional
-    message.host = temp[3] || null; // Optional
-    if (message.user)
-      message.source = message.user + "@" + message.host;
-    else
-      message.source = message.host; // Note: this can be null!
-  }
-  else if (prefix)
-    message.servername = prefix;
+  // Handle the prefix part of the message per RFC 2812 Section 2.3.
+
+  // If no prefix is given, assume the current server is the origin.
+  if (!temp[1])
+    temp[1] = aOrigin;
+
+  // Split the prefix into separate nickname, username and hostname fields as:
+  //   :(servername|(nickname[[!user]@host]))
+  [message.origin, message.user, message.host] = temp[1].split(/[!@]/);
+
+  // It is occasionally useful to have a "source" which is a combination of
+  // user@host.
+  if (message.user)
+    message.source = message.user + "@" + message.host;
+  else if (message.host)
+    message.source = message.host;
+  else
+    message.source = "";
 
   return message;
 }
 
 // This handles a mode change string for both channels and participants. A mode
 // change string is of the form:
 //   aAddNewMode is true if modes are being added, false otherwise.
 //   aNewModes is an array of mode characters.
@@ -678,17 +684,18 @@ ircSocket.prototype = {
     // Low level dequote: replace quote character \020 followed by 0, n, r or
     // \020 with a \0, \n, \r or \020, respectively. Any other character is
     // replaced with itself.
     const lowDequote = {"0": "\0", "n": "\n", "r": "\r", "\x10": "\x10"};
     let dequotedMessage = aRawMessage.replace(/\x10./g,
       function(aStr) lowDequote[aStr[1]] || aStr[1]);
 
     try {
-      let message = new ircMessage(dequotedMessage);
+      let message = new ircMessage(dequotedMessage,
+                                   this._account._currentServerName);
       this.DEBUG(JSON.stringify(message) + conversionWarning);
       if (!ircHandlers.handleMessage(this._account, message)) {
         // If the message was not handled, throw a warning containing
         // the original quoted message.
         this.WARN("Unhandled IRC message:\n" + aRawMessage);
       }
     } catch (e) {
       // Catch the error, display it and hope the connection can continue with
@@ -778,16 +785,20 @@ function ircAccount(aProtocol, aImAccoun
   this._init(aProtocol, aImAccount);
   this.buddies = new NormalizedMap(this.normalizeNick.bind(this));
   this.conversations = new NormalizedMap(this.normalize.bind(this));
 
   // Split the account name into usable parts.
   let splitter = this.name.lastIndexOf("@");
   this._accountNickname = this.name.slice(0, splitter);
   this._server = this.name.slice(splitter + 1);
+  // To avoid _currentServerName being null, initialize it to the server being
+  // connected to. This will also get overridden during the 001 response from
+  // the server.
+  this._currentServerName = this._server;
 
   this._nickname = this._accountNickname;
   this._requestedNickname = this._nickname;
 
   // For more information, see where these are defined in the prototype below.
   this.trackQueue = [];
   this.pendingIsOnQueue = [];
   this.whoisInformation = new NormalizedMap(this.normalizeNick.bind(this));
@@ -813,16 +824,27 @@ ircAccount.prototype = {
   // The nickname stored in the account name.
   _accountNickname: null,
   // The nickname that was last requested by the user.
   _requestedNickname: null,
   // The nickname that was last requested. This can differ from
   // _requestedNickname when a new nick is automatically generated (e.g. by
   // adding digits).
   _sentNickname: null,
+  get username() {
+    let username;
+    // Use a custom username in a hidden preference.
+    if (this.prefs.prefHasUserValue("username"))
+      username = this.getString("username");
+    // But fallback to brandShortName if no username is provided (or is empty).
+    if (!username)
+      username = Services.appinfo.name;
+
+    return username;
+  },
   // The prefix minus the nick (!user@host) as returned by the server, this is
   // necessary for guessing message lengths.
   prefix: null,
 
   // Parts of the specification give max lengths, keep track of them since a
   // server can overwrite them. The defaults given here are from RFC 2812.
   maxNicknameLength: 9, // 1.2.1 Users
   maxChannelLength: 50, // 1.3 Channels
@@ -1270,29 +1292,29 @@ ircAccount.prototype = {
   },
 
   handlePingReply: function(aSource, aPongTime) {
     // Received PING response, display to the user.
     let sentTime = new Date(parseInt(aPongTime, 10));
 
     // The received timestamp is invalid.
     if (isNaN(sentTime)) {
-      this.WARN(aMessage.servername +
+      this.WARN(aMessage.origin +
                 " returned an invalid timestamp from a PING: " + aPongTime);
       return false;
     }
 
     // Find the delay in milliseconds.
     let delay = Date.now() - sentTime;
 
     // If the delay is negative or greater than 1 minute, something is
     // feeding us a crazy value. Don't display this to the user.
     if (delay < 0 || 60 * 1000 < delay) {
-      this.WARN(aMessage.servername +
-                " returned an invalid delay from a PING: " + delay);
+      this.WARN(aMessage.origin + " returned an invalid delay from a PING: " +
+                delay);
       return false;
     }
 
     let msg = PluralForm.get(delay, _("message.ping", aSource))
                         .replace("#2", delay);
     this.getConversation(aSource).writeMessage(aSource, msg, {system: true});
     return true;
   },
@@ -1651,24 +1673,17 @@ ircAccount.prototype = {
       this.sendMessage("PASS", this.getString("serverPassword"),
                        "PASS <password not logged>");
     }
 
     // Send the nick message (section 3.1.2).
     this.changeNick(this._requestedNickname);
 
     // Send the user message (section 3.1.3).
-    let username;
-    // Use a custom username in a hidden preference.
-    if (this.prefs.prefHasUserValue("username"))
-      username = this.getString("username");
-    // But fallback to brandShortName if no username is provided (or is empty).
-    if (!username)
-      username = Services.appinfo.name;
-    this.sendMessage("USER", [username, this._mode.toString(), "*",
+    this.sendMessage("USER", [this.username, this._mode.toString(), "*",
                               this._realname || this._requestedNickname]);
   },
 
   _reportDisconnecting: function(aErrorReason, aErrorMessage) {
     this.reportDisconnecting(aErrorReason, aErrorMessage);
 
     // Mark all contacts on the account as having an unknown status.
     this.buddies.forEach(function(aBuddy)
--- a/chat/protocols/irc/ircBase.jsm
+++ b/chat/protocols/irc/ircBase.jsm
@@ -40,19 +40,18 @@ ircRoomInfo.prototype = {
     this._account.getChatRoomDefaultFieldValues(this.name)
 }
 
 function privmsg(aAccount, aMessage, aIsNotification) {
   let params = {incoming: true};
   if (aIsNotification)
     params.notification = true;
   aAccount.getConversation(aAccount.isMUCName(aMessage.params[0]) ?
-                           aMessage.params[0] : aMessage.nickname)
-          .writeMessage(aMessage.nickname || aMessage.servername,
-                        aMessage.params[1], params);
+                             aMessage.params[0] : aMessage.origin)
+          .writeMessage(aMessage.origin, aMessage.params[1], params);
   return true;
 }
 
 // Display the message and remove them from the rooms they're in.
 function leftRoom(aAccount, aNicks, aChannels, aSource, aReason, aKicked) {
   let msgId = "message." +  (aKicked ? "kicked" : "parted");
   // If a part message was included, include it.
   let reason = aReason ? _(msgId + ".reason", aReason) : "";
@@ -88,18 +87,18 @@ function leftRoom(aAccount, aNicks, aCha
     }
   }
   return true;
 }
 
 function writeMessage(aAccount, aMessage, aString, aType) {
   let type = {};
   type[aType] = true;
-  aAccount.getConversation(aMessage.servername)
-          .writeMessage(aMessage.servername, aString, type);
+  aAccount.getConversation(aMessage.origin)
+          .writeMessage(aMessage.origin, aString, type);
   return true;
 }
 
 // If aNoLastParam is true, the last parameter is not printed out.
 function serverMessage(aAccount, aMsg, aNoLastParam) {
   // If we don't want to show messages from the server, just mark it as handled.
   if (!aAccount._showServerTab)
     return true;
@@ -181,111 +180,110 @@ var ircBase = {
       if (Services.prefs.getIntPref("messenger.conversations.autoAcceptChatInvitations") == 1) {
         // Auto-accept the invite.
         this.joinChat(this.getChatRoomDefaultFieldValues(aMessage.params[1]));
         this.LOG("Received invite for " + aMessage.params[1] +
                  ", auto-accepting.");
       }
       // Otherwise, just notify the user.
       this.getConversation(aMessage.params[1])
-          .writeMessage(aMessage.nickname,
-                        _("message.inviteReceived", aMessage.nickname,
+          .writeMessage(aMessage.origin,
+                        _("message.inviteReceived", aMessage.origin,
                           aMessage.params[1]), {system: true});
       return true;
     },
     "JOIN": function(aMessage) {
       // JOIN ( <channel> *( "," <channel> ) [ <key> *( "," <key> ) ] ) / "0"
       // Add the buddy to each channel
       for each (let channelName in aMessage.params[0].split(",")) {
         let conversation = this.getConversation(channelName);
-        if (this.normalize(aMessage.nickname, this.userPrefixes) ==
+        if (this.normalize(aMessage.origin, this.userPrefixes) ==
             this.normalize(this._nickname)) {
           // If you join, clear the participants list to avoid errors with
           // repeated participants.
           conversation.removeAllParticipants();
           conversation.left = false;
           conversation.joining = false;
 
           // If the user parted from this room earlier, confirm the rejoin.
           // If conversation.chatRoomFields is present, the rejoin was due to
           // an automatic reconnection, for which we already notify the user.
           if (!conversation._firstJoin && !conversation.chatRoomFields) {
-            conversation.writeMessage(aMessage.nickname, _("message.rejoined"),
+            conversation.writeMessage(aMessage.origin, _("message.rejoined"),
                                       {system: true});
           }
           delete conversation._firstJoin;
 
           // Ensure chatRoomFields information is available for reconnection.
           if (!conversation.chatRoomFields) {
             this.WARN("Opening a MUC without storing its " +
                       "prplIChatRoomFieldValues first.");
             conversation.chatRoomFields =
               this.getChatRoomDefaultFieldValues(channelName);
           }
         }
         else {
           // Don't worry about adding ourself, RPL_NAMREPLY takes care of that
           // case.
-          conversation.getParticipant(aMessage.nickname, true);
-          let msg = _("message.join", aMessage.nickname, aMessage.source || "");
-          conversation.writeMessage(aMessage.nickname, msg, {system: true,
-                                                             noLinkification: true});
+          conversation.getParticipant(aMessage.origin, true);
+          let msg = _("message.join", aMessage.origin, aMessage.source);
+          conversation.writeMessage(aMessage.origin, msg, {system: true,
+                                                           noLinkification: true});
         }
       }
       // If the joiner is a buddy, mark as online.
-      let buddy = this.buddies.get(aMessage.nickname);
+      let buddy = this.buddies.get(aMessage.origin);
       if (buddy)
         buddy.setStatus(Ci.imIStatusInfo.STATUS_AVAILABLE, "");
       return true;
     },
     "KICK": function(aMessage) {
       // KICK <channel> *( "," <channel> ) <user> *( "," <user> ) [<comment>]
       let comment = aMessage.params.length == 3 ? aMessage.params[2] : null;
       // Some servers (moznet) send the kicker as the comment.
-      if (comment == aMessage.nickname)
+      if (comment == aMessage.origin)
         comment = null;
       return leftRoom(this, aMessage.params[1].split(","),
-                      aMessage.params[0].split(","), aMessage.nickname,
-                      comment, true);
+                      aMessage.params[0].split(","), aMessage.origin, comment,
+                      true);
     },
     "MODE": function(aMessage) {
       // MODE <nickname> *( ( "+" / "-") *( "i" / "w" / "o" / "O" / "r" ) )
       // MODE <channel> *( ( "-" / "+" ) *<modes> *<modeparams> )
       if (this.isMUCName(aMessage.params[0])) {
         // If the first parameter is a channel name, a channel/participant mode
         // was updated.
         this.getConversation(aMessage.params[0])
             .setMode(aMessage.params[1], aMessage.params.slice(2),
-                     aMessage.nickname || aMessage.servername);
+                     aMessage.origin);
 
         return true;
       }
 
       // Otherwise the user's own mode is being returned to them.
       return this.setUserMode(aMessage.params[0], aMessage.params[1],
-                              aMessage.nickname || aMessage.servername,
-                              !this._userModeReceived);
+                              aMessage.origin, !this._userModeReceived);
     },
     "NICK": function(aMessage) {
       // NICK <nickname>
-      this.changeBuddyNick(aMessage.nickname, aMessage.params[0]);
+      this.changeBuddyNick(aMessage.origin, aMessage.params[0]);
       return true;
     },
     "NOTICE": function(aMessage) {
       // NOTICE <msgtarget> <text>
       // If the message doesn't have a nickname, it's from the server, don't
       // show it unless the user wants to see it.
-      if (!aMessage.hasOwnProperty("nickname"))
+      if (!aMessage.hasOwnProperty("origin"))
         return serverMessage(this, aMessage);
       return privmsg(this, aMessage, true);
     },
     "PART": function(aMessage) {
       // PART <channel> *( "," <channel> ) [ <Part Message> ]
-      return leftRoom(this, [aMessage.nickname], aMessage.params[0].split(","),
-                      aMessage.source || "",
+      return leftRoom(this, [aMessage.origin], aMessage.params[0].split(","),
+                      aMessage.source,
                       aMessage.params.length == 2 ? aMessage.params[1] : null);
     },
     "PING": function(aMessage) {
       // PING <server1> [ <server2> ]
       // Keep the connection alive.
       this.sendMessage("PONG", aMessage.params[0]);
       return true;
     },
@@ -295,69 +293,71 @@ var ircBase = {
 
       // Ping to keep the connection alive.
       if (pongTime.startsWith("_")) {
         this._socket.cancelDisconnectTimer();
         return true;
       }
       // Otherwise, the ping was from a user command.
       else
-        return this.handlePingReply(aMessage.servername, pongTime);
+        return this.handlePingReply(aMessage.origin, pongTime);
     },
     "PRIVMSG": function(aMessage) {
       // PRIVMSG <msgtarget> <text to be sent>
       // Display message in conversation
       return privmsg(this, aMessage);
     },
     "QUIT": function(aMessage) {
       // QUIT [ < Quit Message> ]
       // Some IRC servers automatically prefix a "Quit: " string. Remove the
       // duplication and use a localized version.
       let quitMsg = aMessage.params[0] || "";
       if (quitMsg.startsWith("Quit: "))
         quitMsg = quitMsg.slice(6); // "Quit: ".length
       // If a quit message was included, show it.
-      let msg = _("message.quit", aMessage.nickname,
+      let msg = _("message.quit", aMessage.origin,
                   quitMsg.length ? _("message.quit2", quitMsg) : "");
       // Loop over every conversation with the user and display that they quit.
       this.conversations.forEach(conversation => {
         if (conversation.isChat &&
-            conversation._participants.has(aMessage.nickname)) {
-          conversation.writeMessage(aMessage.servername, msg, {system: true});
-          conversation.removeParticipant(aMessage.nickname);
+            conversation._participants.has(aMessage.origin)) {
+          conversation.writeMessage(aMessage.origin, msg, {system: true});
+          conversation.removeParticipant(aMessage.origin);
         }
       });
 
       // Remove from the whois table.
-      this.removeBuddyInfo(aMessage.nickname);
+      this.removeBuddyInfo(aMessage.origin);
 
       // If the leaver is a buddy, mark as offline.
-      let buddy = this.buddies.get(aMessage.nickname);
+      let buddy = this.buddies.get(aMessage.origin);
       if (buddy)
         buddy.setStatus(Ci.imIStatusInfo.STATUS_OFFLINE, "");
       return true;
     },
     "SQUIT": function(aMessage) {
       // <server> <comment>
       return true;
     },
     "TOPIC": function(aMessage) {
       // TOPIC <channel> [ <topic> ]
       // Show topic as a message.
-      let source = aMessage.nickname || aMessage.servername;
       let conversation = this.getConversation(aMessage.params[0]);
       let topic = aMessage.params[1];
       // Set the topic in the conversation and update the UI.
-      conversation.setTopic(topic ? ctcpFormatToText(topic) : "", source);
+      conversation.setTopic(topic ? ctcpFormatToText(topic) : "",
+                            aMessage.origin);
       return true;
     },
     "001": function(aMessage) { // RPL_WELCOME
       // Welcome to the Internet Relay Network <nick>!<user>@<host>
       this._socket.resetPingTimer();
-      this._currentServerName = aMessage.servername;
+      // This seems a little strange, but we don't differentiate between a
+      // nickname and the servername since it can be ambiguous.
+      this._currentServerName = aMessage.origin;
 
       // Clear user mode.
       this._modes = new Set();
       this._userModeReceived = false;
 
       // Check if our nick has changed.
       if (aMessage.params[0] != this._nickname)
         this.changeBuddyNick(this._nickname, aMessage.params[0]);
@@ -485,17 +485,17 @@ var ircBase = {
     "219": function(aMessage) { // RPL_ENDOFSTATS
       // <stats letter> :End of STATS report
       return serverMessage(this, aMessage);
     },
 
     "221": function(aMessage) { // RPL_UMODEIS
       // <user mode string>
       return this.setUserMode(aMessage.params[0], aMessage.params[1],
-                              aMessage.servername, true);
+                              aMessage.origin, true);
     },
 
     /*
      * Services
      */
     "231": function(aMessage) { // RPL_SERVICEINFO
       // Non-generic
       return serverMessage(this, aMessage);
@@ -814,17 +814,17 @@ var ircBase = {
 
     /*
      * Channel functions
      */
     "324": function(aMessage) { // RPL_CHANNELMODEIS
       // <channel> <mode> <mode params>
       this.getConversation(aMessage.params[1])
           .setMode(aMessage.params[2], aMessage.params.slice(3),
-                   aMessage.servername);
+                   aMessage.origin);
 
       return true;
     },
     "325": function(aMessage) { // RPL_UNIQOPIS
       // <channel> <nickname>
       // TODO parse this and have the UI respond accordingly.
       return false;
     },
@@ -851,17 +851,17 @@ var ircBase = {
     /*
      * Invitations
      */
     "341": function(aMessage) { // RPL_INVITING
       // <channel> <nick>
       // Note that servers reply with parameters in the reverse order from the
       // above (which is as specified by RFC 2812).
       this.getConversation(aMessage.params[2])
-          .writeMessage(aMessage.servername,
+          .writeMessage(aMessage.origin,
                         _("message.invited", aMessage.params[1],
                           aMessage.params[2]), {system: true});
       return true;
     },
     "342": function(aMessage) { // RPL_SUMMONING
       // <user> :Summoning user to IRC
       return writeMessage(this, aMessage,
                           _("message.summoned", aMessage.params[0]));
@@ -986,17 +986,17 @@ var ircBase = {
       let conv = this.getConversation(aMessage.params[1]);
       let msg;
       if (conv.banMasks.length) {
         msg = [_("message.banMasks", aMessage.params[1])]
                .concat(conv.banMasks).join("\n");
       }
       else
         msg = _("message.noBanMasks", aMessage.params[1]);
-      conv.writeMessage(aMessage.servername, msg, {system: true});
+      conv.writeMessage(aMessage.origin, msg, {system: true});
       return true;
     },
     "369": function(aMessage) { // RPL_ENDOFWHOWAS
       // <nick> :End of WHOWAS
       // We've received everything about WHOWAS, tell the tooltip that is waiting
       // for this information.
       this.notifyWhois(aMessage.params[1]);
       return true;
@@ -1247,17 +1247,17 @@ var ircBase = {
       // <channel> :You're not on that channel
       this.ERROR("A command affecting " + aMessage.params[1] +
                  " failed because you aren't in that channel.");
       return true;
     },
     "443": function(aMessage) { // ERR_USERONCHANNEL
       // <user> <channel> :is already on channel
       this.getConversation(aMessage.params[2])
-          .writeMessage(aMessage.servername,
+          .writeMessage(aMessage.origin,
                         _("message.alreadyInChannel", aMessage.params[1],
                           aMessage.params[2]), {system: true});
       return true;
     },
     "444": function(aMessage) { // ERR_NOLOGIN
       // <user> :User not logged in
       // TODO
       return false;
@@ -1279,17 +1279,25 @@ var ircBase = {
         this.LOG("Server doesn't support CAP.");
         return true;
       }
       // TODO
       return false;
     },
     "461": function(aMessage) { // ERR_NEEDMOREPARAMS
       // <command> :Not enough parameters
-      // TODO
+
+      if (!this.connected) {
+        // The account has been set up with an illegal username.
+        this.ERROR("Erroneous username: " + this.username);
+        this.gotDisconnected(Ci.prplIAccount.ERROR_INVALID_USERNAME,
+                             _("connection.error.invalidUsername", this.user));
+        return true;
+      }
+
       return false;
     },
     "462": function(aMessage) { // ERR_ALREADYREGISTERED
       // :Unauthorized command (already registered)
       // TODO
       return false;
     },
     "463": function(aMessage) { // ERR_NOPERMFORHOST
--- a/chat/protocols/irc/ircCTCP.jsm
+++ b/chat/protocols/irc/ircCTCP.jsm
@@ -94,18 +94,17 @@ function ctcpHandleMessage(aMessage) {
 
   // Loop over each raw CTCP message.
   for each (let message in ctcpMessages) {
     if (!ircHandlers.handleCTCPMessage(this, message)) {
       this.WARN("Unhandled CTCP message: " + message.ctcp.rawMessage +
                 "\nin IRC message: " + message.rawMessage);
       // For unhandled CTCP message, respond with a NOTICE ERRMSG that echoes
       // back the original command.
-      this.sendCTCPMessage(message.nickname || message.servername, true,
-                           "ERRMSG",
+      this.sendCTCPMessage(message.origin, true, "ERRMSG",
                            [message.ctcp.rawMessage, ":Unhandled CTCP command"]);
     }
   }
 
   // We have handled this message as much as we can.
   return true;
 }
 
@@ -117,26 +116,25 @@ var ctcpBase = {
   isEnabled: function() true,
 
   // These represent CTCP commands.
   commands: {
     "ACTION": function(aMessage) {
       // ACTION <text>
       // Display message in conversation
       this.getConversation(this.isMUCName(aMessage.params[0]) ?
-                             aMessage.params[0] : aMessage.nickname)
-          .writeMessage(aMessage.nickname || aMessage.servername,
-                        "/me " + aMessage.ctcp.param,
+                             aMessage.params[0] : aMessage.origin)
+          .writeMessage(aMessage.origin, "/me " + aMessage.ctcp.param,
                         {incoming: true});
       return true;
     },
 
     // Used when an error needs to be replied with.
     "ERRMSG": function(aMessage) {
-      this.WARN(aMessage.nickname + " failed to handle CTCP message: " +
+      this.WARN(aMessage.origin + " failed to handle CTCP message: " +
                 aMessage.ctcp.param);
       return true;
     },
 
     // This is commented out since CLIENTINFO automatically returns the
     // supported CTCP parameters and this is not supported.
 
     // Returns the user's full name, and idle time.
@@ -151,41 +149,41 @@ var ctcpBase = {
         for (let handler of ircHandlers._ctcpHandlers) {
           for (let command in handler.commands)
             info.add(command);
         }
 
         let supportedCtcp = [...info].join(" ");
         this.LOG("Reporting support for the following CTCP messages: " +
                  supportedCtcp);
-        this.sendCTCPMessage(aMessage.nickname, true, "CLIENTINFO",
+        this.sendCTCPMessage(aMessage.origin, true, "CLIENTINFO",
                              supportedCtcp);
       }
       else {
         // Received a CLIENTINFO response, store the information for future
         // use.
         let info = aMessage.ctcp.param.split(" ");
-        this.setWhois(aMessage.nickname, {clientInfo: info})
+        this.setWhois(aMessage.origin, {clientInfo: info})
       }
       return true;
     },
 
     // Used to measure the delay of the IRC network between clients.
     "PING": function(aMessage) {
       // PING timestamp
       if (aMessage.command == "PRIVMSG") {
         // Received PING request, send PING response.
-        this.LOG("Received PING request from " + aMessage.nickname +
+        this.LOG("Received PING request from " + aMessage.origin +
                  ". Sending PING response: \"" + aMessage.ctcp.param + "\".");
-        this.sendCTCPMessage(aMessage.nickname, true, "PING",
+        this.sendCTCPMessage(aMessage.origin, true, "PING",
                              aMessage.ctcp.param);
         return true;
       }
       else
-        return this.handlePingReply(aMessage.nickname, aMessage.ctcp.param);
+        return this.handlePingReply(aMessage.origin, aMessage.ctcp.param);
     },
 
     // These are commented out since CLIENTINFO automatically returns the
     // supported CTCP parameters and this is not supported.
 
     // An encryption protocol between clients without any known reference.
     //"SED": function(aMessage) false,
 
@@ -193,28 +191,28 @@ var ctcpBase = {
     //"SOURCE": function(aMessage) false,
 
     // Gets the local date and time from other clients.
     "TIME": function(aMessage) {
       if (aMessage.command == "PRIVMSG") {
         // TIME
         // Received a TIME request, send a human readable response.
         let now = (new Date()).toString();
-        this.LOG("Received TIME request from " + aMessage.nickname +
+        this.LOG("Received TIME request from " + aMessage.origin +
                  ". Sending TIME response: \"" + now + "\".");
-        this.sendCTCPMessage(aMessage.nickname, true, "TIME", ":" + now);
+        this.sendCTCPMessage(aMessage.origin, true, "TIME", ":" + now);
       }
       else {
         // TIME :<human-readable-time-string>
         // Received a TIME reply, display it.
         // Remove the : prefix, if it exists and display the result.
         let time = aMessage.ctcp.param.slice(aMessage.ctcp.param[0] == ":");
-        this.getConversation(aMessage.nickname)
-            .writeMessage(aMessage.nickname,
-                          _("ctcp.time", aMessage.nickname, time),
+        this.getConversation(aMessage.origin)
+            .writeMessage(aMessage.origin,
+                          _("ctcp.time", aMessage.origin, time),
                           {system: true});
       }
       return true;
     },
 
     // This is commented out since CLIENTINFO automatically returns the
     // supported CTCP parameters and this is not supported.
 
@@ -222,24 +220,24 @@ var ctcpBase = {
     //"USERINFO": function(aMessage) false,
 
     // The version and type of the client.
     "VERSION": function(aMessage) {
       if (aMessage.command == "PRIVMSG") {
         // VERSION
         // Received VERSION request, send VERSION response.
         let version = Services.appinfo.name + " " + Services.appinfo.version;
-        this.LOG("Received VERSION request from " + aMessage.nickname +
+        this.LOG("Received VERSION request from " + aMessage.origin +
                  ". Sending VERSION response: \"" + version + "\".");
-        this.sendCTCPMessage(aMessage.nickname, true, "VERSION", version);
+        this.sendCTCPMessage(aMessage.origin, true, "VERSION", version);
       }
       else if (aMessage.command == "NOTICE" && aMessage.ctcp.param.length) {
         // VERSION #:#:#
         // Received VERSION response, display to the user.
-        let response = _("ctcp.version", aMessage.nickname,
+        let response = _("ctcp.version", aMessage.origin,
                          aMessage.ctcp.param);
-        this.getConversation(aMessage.nickname)
-            .writeMessage(aMessage.nickname, response, {system: true});
+        this.getConversation(aMessage.origin)
+            .writeMessage(aMessage.origin, response, {system: true});
       }
       return true;
     }
   }
 };
--- a/chat/protocols/irc/ircNonStandard.jsm
+++ b/chat/protocols/irc/ircNonStandard.jsm
@@ -31,26 +31,26 @@ var ircNonStandard = {
       // Try to avoid the stupid case where the user's nick is AUTH. If this
       // happens, it is ambiguous if it is an AUTH message or a NOTICE to the
       // user. Generally AUTH messages start with ***, but this could pretty
       // easily be faked.
       // Freenode simply sends * for the target. Moznet sends Auth (used to send
       // AUTH); in this case, check if the user's nickname is not auth, or the
       // the message starts with ***.
       let target = aMessage.params[0].toLowerCase();
-      let nickname = aMessage.nickname ? aMessage.nickname.toLowerCase() : "";
+      let nickname = aMessage.origin ? aMessage.origin.toLowerCase() : "";
       let isAuth = target == "*" ||
         (target == "auth" && (nickname != "auth" ||
                               aMessage.params[1].startsWith("***")));
 
       // Some servers , e.g. irc.umich.edu, use NOTICE before connection to give
       // directions to users.
       if (!this.connected && !isAuth) {
-        this.getConversation(aMessage.servername)
-            .writeMessage(aMessage.servername, aMessage.params[1],
+        this.getConversation(aMessage.origin)
+            .writeMessage(aMessage.origin, aMessage.params[1],
                           {incoming: true});
         return true;
       }
 
       // If we receive a ZNC error message requesting a password, the
       // serverPassword preference was not set by the user. Attempt to log into
       // ZNC using the account password.
       if (!isAuth ||
@@ -154,31 +154,31 @@ var ircNonStandard = {
     },
 
     "464": function(aMessage) {
       // :Password required
       // If we receive a ZNC error message requesting a password, eat it since
       // a NOTICE AUTH will follow causing us to send the password. This numeric
       // is, unfortunately, also sent if you give a wrong password. The
       // parameter in that case is "Invalid Password".
-      return aMessage.servername == "irc.znc.in" &&
+      return aMessage.origin == "irc.znc.in" &&
              aMessage.params[1] == "Password required";
     },
 
     "499": function(aMessage) { // ERR_CHANOWNPRIVNEEDED (Unreal)
       // <channel> :You're not the channel owner (status +q is needed)
       return conversationErrorMessage(this, aMessage, "error.notChannelOwner");
     },
 
     "671": function(aMessage) { // RPL_WHOISSECURE (Unreal & Charybdis)
       // <nick> :is using a Secure connection
       return this.setWhois(aMessage.params[1], {secure: true});
     },
 
     "998": function(aMessage) {
       // irc.umich.edu shows an ASCII captcha that must be typed in by the user.
-      this.getConversation(aMessage.servername)
-          .writeMessage(aMessage.servername, aMessage.params[1],
+      this.getConversation(aMessage.origin)
+          .writeMessage(aMessage.origin, aMessage.params[1],
                         {incoming: true, noFormat: true});
       return true;
     }
   }
 };
--- a/chat/protocols/irc/ircServices.jsm
+++ b/chat/protocols/irc/ircServices.jsm
@@ -36,17 +36,17 @@ function ServiceMessage(aAccount, aMessa
   // map "bar": "NickServ"). Note that the keys of this map should be
   // normalized.
   let nicknameToServiceName = {
     "chanserv": "ChanServ",
     "infoserv": "InfoServ",
     "nickserv": "NickServ"
   }
 
-  let nickname = aAccount.normalize(aMessage.nickname);
+  let nickname = aAccount.normalize(aMessage.origin);
   if (nicknameToServiceName.hasOwnProperty(nickname))
     aMessage.serviceName = nicknameToServiceName[nickname];
 
   return aMessage;
 }
 
 var ircServices = {
   name: "IRC Services",
@@ -59,17 +59,17 @@ var ircServices = {
                            "IDENTIFY <password not logged>");
     }
   },
 
   commands: {
     // If we automatically reply to a NOTICE message this does not abide by RFC
     // 2812. Oh well.
     "NOTICE": function(aMessage) {
-      if (!ircHandlers.hasServicesHandlers || !aMessage.hasOwnProperty("nickname"))
+      if (!ircHandlers.hasServicesHandlers || !aMessage.hasOwnProperty("origin"))
         return false;
 
       let message = ServiceMessage(this, aMessage);
 
       // If no service was found, return early.
       if (!message.hasOwnProperty("serviceName"))
         return false;
 
@@ -141,34 +141,33 @@ var servicesBase = {
       // Otherwise, display the message in that conversation.
       let params = {incoming: true};
       if (aMessage.command == "NOTICE")
         params.notification = true;
 
       // The message starts after the channel name, plus [, ] and a space.
       let message = aMessage.params[1].slice(channel.length + 3);
       this.getConversation(channel)
-          .writeMessage(aMessage.nickname, message, params);
+          .writeMessage(aMessage.origin, message, params);
       return true;
     },
 
     "InfoServ": function(aMessage) {
       let text = aMessage.params[1];
 
       // Show the message of the day in the server tab.
       if (text == "*** \u0002Message(s) of the Day\u0002 ***") {
         this._infoServMotd = [text];
         return true;
       }
       else if (text == "*** \u0002End of Message(s) of the Day\u0002 ***") {
         if (this._showServerTab && this._infoServMotd) {
           this._infoServMotd.push(text);
-          this.getConversation(aMessage.servername || this._currentServerName)
-              .writeMessage(aMessage.servername || aMessage.nickname,
-                            this._infoServMotd.join("\n"),
+          this.getConversation(aMessage.origin)
+              .writeMessage(aMessage.origin, this._infoServMotd.join("\n"),
                             {incoming: true});
           delete this._infoServMotd;
         }
         return true;
       }
       else if (this.hasOwnProperty("_infoServMotd")) {
         this._infoServMotd.push(text);
         return true;
--- a/chat/protocols/irc/ircUtils.jsm
+++ b/chat/protocols/irc/ircUtils.jsm
@@ -216,17 +216,17 @@ function mIRCColoring(aStack, aInput) {
   return [stack, output, length];
 }
 
 // Print an error message into a conversation, optionally mark the conversation
 // as not joined and/or not rejoinable.
 function conversationErrorMessage(aAccount, aMessage, aError,
                                   aJoinFailed = false, aRejoinable = true) {
   let conv = aAccount.getConversation(aMessage.params[1]);
-  conv.writeMessage(aMessage.servername, _(aError, aMessage.params[1]),
+  conv.writeMessage(aMessage.origin, _(aError, aMessage.params[1]),
                     {error: true, system: true});
   delete conv._pendingMessage;
 
   // Channels have a couple extra things that can be done to them.
   if (aAccount.isMUCName(aMessage.params[1])) {
     // If a value for joining is explictly given, mark it.
     if (aJoinFailed)
       conv.joining = false;
--- a/chat/protocols/irc/test/test_ircMessage.js
+++ b/chat/protocols/irc/test/test_ircMessage.js
@@ -15,18 +15,17 @@ const testData = [
   "OPER foo bar",
   "MODE WiZ -w",
   "MODE Angel +i",
   "MODE WiZ -o",
   "SERVICE dict * *.fr 0 0 :French Dictionary",
   "QUIT :Gone to have lunch",
   ":syrk!kalt@millennium.stealth.net QUIT :Gone to have lunch",
   "SQUIT tolsun.oulu.fi :Bad Link ?",
-  // This fails! But do we really care? It wasn't designed to handle server messages.
-  //":Trillian SQUIT cm22.eng.umd.edu :Server out of control",
+  ":Trillian SQUIT cm22.eng.umd.edu :Server out of control",
   "JOIN #foobar",
   "JOIN &foo fubar",
   "JOIN #foo,&bar fubar",
   "JOIN #foo,#bar fubar,foobar",
   "JOIN #foo,#bar",
   "JOIN 0",
   ":WiZ!jto@tolsun.oulu.fi JOIN #Twilight_zone",
   "PART #twilight_zone",
@@ -115,41 +114,49 @@ const testData = [
   "PRIVMSG foo :", // Empty last parameter.
   "PRIVMSG foo :This is :a test." // A "second" last parameter.
 ];
 
 function run_test() {
   add_test(testRFC2812Messages);
   add_test(testBrokenUnrealMessages);
   add_test(testNewLinesInMessages);
+  add_test(testLocalhost);
 
   run_next_test();
 }
 
+/*
+ * Test round tripping parsing and then rebuilding the messages from RFC 2812.
+ */
 function testRFC2812Messages() {
   for each (let expectedStringMessage in testData) {
-    let message = irc.ircMessage(expectedStringMessage);
+    // Pass in an empty default origin in order to check this below.
+    let message = irc.ircMessage(expectedStringMessage, "");
 
     let stringMessage =
       irc.ircAccount.prototype.buildMessage(message.command, message.params);
 
     // Let's do a little dance here...we don't rebuild the "source" of the
     // message (the server does that), so when comparing our output message, we
     // need to avoid comparing to that part.
-    if (message.servername || message.source) {
+    if (message.origin) {
       expectedStringMessage =
         expectedStringMessage.slice(expectedStringMessage.indexOf(" ") + 1);
     }
 
     do_check_eq(stringMessage, expectedStringMessage);
   }
 
   run_next_test();
 }
 
+/*
+ * Test if two objects have the same fields (recursively).
+ */
 function isEqual(aObject1, aObject2) {
   let result = true;
   for (let fieldName in aObject1) {
     let field1 = aObject1[fieldName];
     let field2 = aObject2[fieldName];
     if (typeof field1 == "object")
       result &= isEqual(field1, field2);
     else if (Array.isArray(field1))
@@ -159,63 +166,88 @@ function isEqual(aObject1, aObject2) {
   }
   return result;
 }
 
 // Unreal sends a couple of broken messages, see ircMessage in irc.js for a
 // description of what's wrong.
 function testBrokenUnrealMessages() {
   let messages = {
+    // Two spaces after command.
     ":gravel.mozilla.org 432  #momo :Erroneous Nickname: Illegal characters": {
       rawMessage: ":gravel.mozilla.org 432  #momo :Erroneous Nickname: Illegal characters",
       command: "432",
       params: ["", "#momo", "Erroneous Nickname: Illegal characters"],
-      servername: "gravel.mozilla.org"
+      origin: "gravel.mozilla.org"
     },
+    // An extraneous space at the end.
     ":gravel.mozilla.org MODE #tckk +n ": {
       rawMessage: ":gravel.mozilla.org MODE #tckk +n ",
       command: "MODE",
       params: ["#tckk", "+n"],
-      servername: "gravel.mozilla.org"
+      origin: "gravel.mozilla.org"
     },
+    // Two extraneous spaces at the end.
     ":services.esper.net MODE #foo-bar +o foobar  ": {
       rawMessage: ":services.esper.net MODE #foo-bar +o foobar  ",
       command: "MODE",
       params: ["#foo-bar", "+o", "foobar"],
-      servername: "services.esper.net"
+      origin: "services.esper.net"
     }
   };
 
   for (let messageStr in messages)
-    do_check_true(isEqual(messages[messageStr], irc.ircMessage(messageStr)));
+    do_check_true(isEqual(messages[messageStr], irc.ircMessage(messageStr, "")));
 
   run_next_test();
 }
 
 // After unescaping we can end up with line breaks inside of IRC messages. Test
 // this edge case specifically.
 function testNewLinesInMessages() {
   let messages = {
     ":test!Instantbir@host PRIVMSG #instantbird :First line\nSecond line": {
       rawMessage: ":test!Instantbir@host PRIVMSG #instantbird :First line\nSecond line",
       command: "PRIVMSG",
       params: ["#instantbird", "First line\nSecond line"],
-      nickname: "test",
+      origin: "test",
       user: "Instantbir",
       host: "host",
       source: "Instantbir@host"
     },
     ":test!Instantbir@host PRIVMSG #instantbird :First line\r\nSecond line": {
       rawMessage: ":test!Instantbir@host PRIVMSG #instantbird :First line\r\nSecond line",
       command: "PRIVMSG",
       params: ["#instantbird", "First line\r\nSecond line"],
-      nickname: "test",
+      origin: "test",
       user: "Instantbir",
       host: "host",
       source: "Instantbir@host"
     }
   };
 
   for (let messageStr in messages)
     do_check_true(isEqual(messages[messageStr], irc.ircMessage(messageStr)));
 
   run_next_test();
 }
+
+// Sometimes it is a bit hard to tell whether a prefix is a nickname or a
+// servername. Generally this happens when connecting to localhost or a local
+// hostname and is likely seen with bouncers.
+function testLocalhost() {
+  let messages = {
+    ":localhost 001 clokep :Welcome to the BitlBee gateway, clokep": {
+      rawMessage: ":localhost 001 clokep :Welcome to the BitlBee gateway, clokep",
+      command: "001",
+      params: ["clokep", "Welcome to the BitlBee gateway, clokep"],
+      origin: "localhost",
+      user: undefined,
+      host: undefined,
+      source: ""
+    }
+  };
+
+  for (let messageStr in messages)
+    do_check_true(isEqual(messages[messageStr], irc.ircMessage(messageStr)));
+
+  run_next_test();
+}
--- a/im/test/xpcshell.ini
+++ b/im/test/xpcshell.ini
@@ -1,9 +1,9 @@
 ; This Source Code Form is subject to the terms of the Mozilla Public
 ; License, v. 2.0. If a copy of the MPL was not distributed with this
 ; file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 [include:chat/modules/test/xpcshell.ini]
 [include:chat/components/src/test/xpcshell.ini]
 [include:chat/protocols/irc/test/xpcshell.ini]
 [include:chat/protocols/yahoo/test/xpcshell.ini]
-[include:extensions/purple/purplexpcom/src/test/xpcshell.ini]
+#[include:extensions/purple/purplexpcom/src/test/xpcshell.ini]