Bug 1208087 - Add tests for proxy via SOCKS for all protocols, r=rkent.
authorJoshua Cranmer <Pidgeot18@gmail.com>
Tue, 19 Jan 2016 22:10:42 -0600
changeset 18869 a1c04274c6e9b402dca39b094e2024100d6194ab
parent 18868 5f1dfe01ec6767d7a1531c327498f1bd30cb509d
child 18870 b5723697d212da88c9d358d46f1814e39800fd0f
push id11563
push userPidgeot18@gmail.com
push dateWed, 20 Jan 2016 04:12:19 +0000
treeherdercomm-central@a1c04274c6e9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrkent
bugs1208087, 791645
Bug 1208087 - Add tests for proxy via SOCKS for all protocols, r=rkent. Ideally, the test framework would be using PAC to set up the proxy (it's much simpler), but bug 791645 appears to keep this from working properly.
mailnews/compose/test/unit/head_compose.js
mailnews/compose/test/unit/test_smtpProxy.js
mailnews/compose/test/unit/xpcshell.ini
mailnews/imap/test/unit/head_server.js
mailnews/imap/test/unit/test_imapProxy.js
mailnews/imap/test/unit/xpcshell.ini
mailnews/local/test/unit/head_maillocal.js
mailnews/local/test/unit/test_pop3Proxy.js
mailnews/local/test/unit/xpcshell.ini
mailnews/moz.build
mailnews/news/test/unit/head_server_setup.js
mailnews/news/test/unit/test_nntpProxy.js
mailnews/news/test/unit/xpcshell.ini
mailnews/test/resources/NetworkTestUtils.jsm
mailnews/test/resources/localAccountUtils.js
--- a/mailnews/compose/test/unit/head_compose.js
+++ b/mailnews/compose/test/unit/head_compose.js
@@ -35,18 +35,19 @@ var gDraftFolder;
 // Setup the daemon and server
 function setupServerDaemon(handler) {
   if (!handler)
     handler = function (d) { return new SMTP_RFC2821_handler(d); };
   var server = new nsMailServer(handler, new smtpDaemon());
   return server;
 }
 
-function getBasicSmtpServer(port=1) {
-  let server = localAccountUtils.create_outgoing_server(port, "user", "password");
+function getBasicSmtpServer(port=1, hostname="localhost") {
+  let server = localAccountUtils.create_outgoing_server(port, "user",
+    "password", hostname);
 
   // Override the default greeting so we get something predicitable
   // in the ELHO message
   Services.prefs.setCharPref("mail.smtpserver.default.hello_argument", "test");
 
   return server;
 }
 
new file mode 100644
--- /dev/null
+++ b/mailnews/compose/test/unit/test_smtpProxy.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+// Tests that SMTP over a SOCKS proxy works.
+
+Components.utils.import("resource://testing-common/mailnews/NetworkTestUtils.jsm");
+Components.utils.import("resource://testing-common/mailnews/PromiseTestUtils.jsm");
+
+const PORT = 25;
+var daemon, localserver, server;
+
+add_task(function* setup() {
+  server = setupServerDaemon();
+  daemon = server._daemon;
+  server.start();
+  NetworkTestUtils.configureProxy("smtp.tinderbox.invalid", PORT, server.port);
+  localserver = getBasicSmtpServer(PORT, "smtp.tinderbox.invalid");
+});
+
+let CompFields = CC("@mozilla.org/messengercompose/composefields;1",
+                    Ci.nsIMsgCompFields);
+
+add_task(function* sendMessage() {
+  equal(daemon.post, undefined);
+  let identity = getSmtpIdentity("test@tinderbox.invalid", localserver);
+  var testFile = do_get_file("data/message1.eml");
+  var urlListener = new PromiseTestUtils.PromiseUrlListener();
+  MailServices.smtp.sendMailMessage(testFile, "somebody@example.org", identity,
+                                    null, urlListener, null, null,
+                                    false, {}, {});
+  yield urlListener.promise;
+  notEqual(daemon.post, "");
+});
+
+add_task(function* cleanUp() {
+  NetworkTestUtils.shutdownServers();
+});
+
+function run_test() {
+  localAccountUtils.loadLocalMailAccount();
+  run_next_test();
+}
+
--- a/mailnews/compose/test/unit/xpcshell.ini
+++ b/mailnews/compose/test/unit/xpcshell.ini
@@ -31,13 +31,14 @@ skip-if = os == 'mac'
 [test_smtp8bitMime.js]
 [test_smtpAuthMethods.js]
 [test_smtpPassword.js]
 [test_smtpPassword2.js]
 [test_smtpPasswordFailure1.js]
 [test_smtpPasswordFailure2.js]
 [test_smtpPasswordFailure3.js]
 [test_smtpProtocols.js]
+[test_smtpProxy.js]
 [test_smtpURL.js]
 [test_splitRecipients.js]
 [test_staleTemporaryFileCleanup.js]
 [test_temporaryFilesRemoved.js]
 [test_longLines.js]
--- a/mailnews/imap/test/unit/head_server.js
+++ b/mailnews/imap/test/unit/head_server.js
@@ -48,19 +48,19 @@ function makeServer(daemon, infoString, 
     }
     return handler;
   }
   var server = new nsMailServer(createHandler, daemon);
   server.start();
   return server;
 }
 
-function createLocalIMAPServer(port) {
+function createLocalIMAPServer(port, hostname="localhost") {
   let server = localAccountUtils.create_incoming_server("imap", port,
-							"user", "password");
+							"user", "password", hostname);
   server.QueryInterface(Ci.nsIImapIncomingServer);
   return server;
 }
 
 // <copied from="head_maillocal.js">
 /**
  * @param fromServer server.playTransaction
  * @param expected ["command", "command", ...]
new file mode 100644
--- /dev/null
+++ b/mailnews/imap/test/unit/test_imapProxy.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+// Test that IMAP over a SOCKS proxy works.
+
+Components.utils.import("resource://testing-common/mailnews/NetworkTestUtils.jsm");
+Components.utils.import("resource://testing-common/mailnews/PromiseTestUtils.jsm");
+load("../../../resources/messageGenerator.js");
+
+var server, daemon, incomingServer;
+
+const PORT = 143;
+
+add_task(function* setup() {
+  // Disable new mail notifications
+  Services.prefs.setBoolPref("mail.biff.play_sound", false);
+  Services.prefs.setBoolPref("mail.biff.show_alert", false);
+  Services.prefs.setBoolPref("mail.biff.show_tray_icon", false);
+  Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false);
+
+  let daemon = new imapDaemon();
+  server = makeServer(daemon, "");
+
+  let messages = [];
+  let messageGenerator = new MessageGenerator();
+  messages = messages.concat(messageGenerator.makeMessage());
+  let dataUri = Services.io.newURI("data:text/plain;base64," +
+                                   btoa(messages[0].toMessageString()),
+                                   null, null);
+  let imapMsg = new imapMessage(dataUri.spec, daemon.inbox.uidnext++, []);
+  daemon.inbox.addMessage(imapMsg);
+
+
+  NetworkTestUtils.configureProxy("imap.tinderbox.invalid", PORT, server.port);
+
+  // Set up the basic accounts and folders
+  incomingServer = createLocalIMAPServer(PORT, "imap.tinderbox.invalid");
+  let identity = MailServices.accounts.createIdentity();
+  let imapAccount = MailServices.accounts.createAccount();
+  imapAccount.addIdentity(identity);
+  imapAccount.defaultIdentity = identity;
+  imapAccount.incomingServer = incomingServer;
+});
+
+add_task(function* downloadEmail() {
+  let inboxFolder = incomingServer.rootFolder.getChildNamed("INBOX");
+
+  // Check that we haven't got any messages in the folder, if we have its a test
+  // setup issue.
+  Assert.equal(inboxFolder.getTotalMessages(false), 0);
+
+  // Now get the mail
+  let asyncUrlListener = new PromiseTestUtils.PromiseUrlListener();
+  inboxFolder.getNewMessages(null, asyncUrlListener);
+  yield asyncUrlListener.promise;
+
+  // We downloaded a message, so it works!
+  Assert.equal(inboxFolder.getTotalMessages(false), 1);
+});
+
+add_task(function* cleanUp() {
+  NetworkTestUtils.shutdownServers();
+  incomingServer.closeCachedConnections();
+  server.stop();
+});
+
+function run_test() {
+  run_next_test();
+}
--- a/mailnews/imap/test/unit/xpcshell.ini
+++ b/mailnews/imap/test/unit/xpcshell.ini
@@ -46,16 +46,17 @@ skip-if = true
 # Disabled until bug 870864 is resolved
 skip-if = true
 [test_imapID.js]
 [test_imapMove.js]
 [test_imapPasswordFailure.js]
 # Disabled until bug 870864 is resolved
 skip-if = true
 [test_imapProtocols.js]
+[test_imapProxy.js]
 [test_imapRename.js]
 [test_imapSearch.js]
 [test_imapStatusCloseDBs.js]
 [test_imapStoreMsgOffline.js]
 [test_imapUndo.js]
 [test_imapUrls.js]
 [test_largeOfflineStore.js]
 skip-if = os == 'mac'
--- a/mailnews/local/test/unit/head_maillocal.js
+++ b/mailnews/local/test/unit/head_maillocal.js
@@ -38,20 +38,20 @@ function setupServerDaemon(debugOption) 
     return handler;
   }
   var server = new nsMailServer(createHandler, daemon);
   if (debugOption)
     server.setDebugLevel(debugOption);
   return [daemon, server, extraProps];
 }
 
-function createPop3ServerAndLocalFolders(port) {
+function createPop3ServerAndLocalFolders(port, hostname="localhost") {
   localAccountUtils.loadLocalMailAccount();
   let server = localAccountUtils.create_incoming_server("pop3", port,
-							"fred", "wilma");
+    "fred", "wilma", hostname);
   return server;
 }
 
 var gCopyListener =
 {
   callbackFunction: null,
   copiedMessageHeaderKeys: [],
   OnStartCopy: function() {},
new file mode 100644
--- /dev/null
+++ b/mailnews/local/test/unit/test_pop3Proxy.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+// Test that POP3 over a proxy works.
+
+Components.utils.import("resource://testing-common/mailnews/NetworkTestUtils.jsm");
+Components.utils.import("resource://testing-common/mailnews/PromiseTestUtils.jsm");
+
+const PORT = 110;
+
+var server, daemon, incomingServer;
+
+add_task(function* setup() {
+  // Disable new mail notifications
+  Services.prefs.setBoolPref("mail.biff.play_sound", false);
+  Services.prefs.setBoolPref("mail.biff.show_alert", false);
+  Services.prefs.setBoolPref("mail.biff.show_tray_icon", false);
+  Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false);
+
+  [daemon, server] = setupServerDaemon();
+  server.start();
+  NetworkTestUtils.configureProxy("pop.tinderbox.invalid", PORT, server.port);
+
+  // Set up the basic accounts and folders
+  incomingServer = createPop3ServerAndLocalFolders(PORT, "pop.tinderbox.invalid");
+
+  // Add a message to download
+  daemon.setMessages(["message1.eml"]);
+});
+
+add_task(function* downloadEmail() {
+  // Check that we haven't got any messages in the folder, if we have its a test
+  // setup issue.
+  equal(localAccountUtils.inboxFolder.getTotalMessages(false), 0);
+
+  // Now get the mail
+  let urlListener = new PromiseTestUtils.PromiseUrlListener();
+  MailServices.pop3.GetNewMail(null, urlListener, localAccountUtils.inboxFolder,
+                               incomingServer);
+  yield urlListener.promise;
+
+  // We downloaded a message, so it works!
+  equal(localAccountUtils.inboxFolder.getTotalMessages(false), 1);
+});
+
+add_task(function* cleanUp() {
+  NetworkTestUtils.shutdownServers();
+  incomingServer.closeCachedConnections();
+  server.stop();
+});
+
+function run_test() {
+  run_next_test();
+}
--- a/mailnews/local/test/unit/xpcshell.ini
+++ b/mailnews/local/test/unit/xpcshell.ini
@@ -33,16 +33,17 @@ requesttimeoutfactor = 2
 [test_pop3MultiCopy.js]
 [test_pop3MultiCopy2.js]
 [test_pop3Password.js]
 [test_pop3Password2.js]
 [test_pop3Password3.js]
 [test_pop3PasswordFailure.js]
 [test_pop3PasswordFailure2.js]
 [test_pop3PasswordFailure3.js]
+[test_pop3Proxy.js]
 [test_pop3Pump.js]
 [test_pop3ServerBrokenCRAMDisconnect.js]
 [test_pop3ServerBrokenCRAMFail.js]
 [test_preview.js]
 [test_saveMessage.js]
 [test_streamHeaders.js]
 [test_undoDelete.js]
 [test_verifyLogon.js]
--- a/mailnews/moz.build
+++ b/mailnews/moz.build
@@ -69,16 +69,17 @@ TESTING_JS_MODULES.mailnews += [
     'test/fakeserver/maild.js',
     'test/fakeserver/nntpd.js',
     'test/fakeserver/pop3d.js',
     'test/fakeserver/smtpd.js',
     'test/resources/IMAPpump.js',
     'test/resources/localAccountUtils.js',
     'test/resources/mailTestUtils.js',
     'test/resources/MockFactory.js',
+    'test/resources/NetworkTestUtils.jsm',
     'test/resources/PromiseTestUtils.jsm',
 ]
 
 TEST_HARNESS_FILES.xpcshell.mailnews.data += [
     '/mailnews/test/data/**',
 ]
 
 TEST_HARNESS_FILES.xpcshell.mailnews.resources += [
--- a/mailnews/news/test/unit/head_server_setup.js
+++ b/mailnews/news/test/unit/head_server_setup.js
@@ -108,21 +108,22 @@ function subscribeServer(incomingServer)
       if (element[1])
         incomingServer.subscribeToNewsgroup(element[0]);
     });
   // Only allow one connection
   incomingServer.maximumConnectionsNumber = 1;
 }
 
 // Sets up the client-side portion of fakeserver
-function setupLocalServer(port) {
+function setupLocalServer(port, host="localhost") {
   if (_server != null)
     return _server;
   let serverAndAccount =
-    localAccountUtils.create_incoming_server_and_account("nntp", port, null, null);
+    localAccountUtils.create_incoming_server_and_account("nntp", port, null,
+      null, host);
   let server = serverAndAccount.server;
   subscribeServer(server);
 
   _server = server;
   _account = serverAndAccount.account;
 
   return server;
 }
new file mode 100644
--- /dev/null
+++ b/mailnews/news/test/unit/test_nntpProxy.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+// Tests that NNTP over a SOCKS proxy works.
+
+Components.utils.import("resource://testing-common/mailnews/NetworkTestUtils.jsm");
+Components.utils.import("resource://testing-common/mailnews/PromiseTestUtils.jsm");
+
+const PORT = 119;
+
+var daemon, localserver, server;
+
+add_task(function* setup() {
+  daemon = setupNNTPDaemon();
+  server = makeServer(NNTP_RFC2980_handler, daemon);
+  server.start();
+  NetworkTestUtils.configureProxy("news.tinderbox.invalid", PORT, server.port);
+  localserver = setupLocalServer(PORT, "news.tinderbox.invalid");
+});
+
+add_task(function* findMessages() {
+  // This is a trivial check that makes sure that we actually do some network
+  // traffic without caring about the exact network traffic.
+  let folder = localserver.rootFolder.getChildNamed("test.filter");
+  equal(folder.getTotalMessages(false), 0);
+  let asyncUrlListener = new PromiseTestUtils.PromiseUrlListener();
+  folder.getNewMessages(null, asyncUrlListener);
+  yield asyncUrlListener.promise;
+  equal(folder.getTotalMessages(false), 8);
+});
+
+add_task(function* cleanUp() {
+  NetworkTestUtils.shutdownServers();
+  localserver.closeCachedConnections();
+});
+
+function run_test() {
+  run_next_test();
+}
+
--- a/mailnews/news/test/unit/xpcshell.ini
+++ b/mailnews/news/test/unit/xpcshell.ini
@@ -18,12 +18,13 @@ skip-if = true
 [test_newsAutocomplete.js]
 [test_nntpGroupPassword.js]
 [test_nntpPassword.js]
 [test_nntpPassword2.js]
 [test_nntpPassword3.js]
 [test_nntpPasswordFailure.js]
 [test_nntpPost.js]
 [test_nntpProtocols.js]
+[test_nntpProxy.js]
 [test_nntpUrl.js]
 [test_server.js]
 run-sequentially = Uses fixed NNTP_PORT
 [test_uriParser.js]
new file mode 100644
--- /dev/null
+++ b/mailnews/test/resources/NetworkTestUtils.jsm
@@ -0,0 +1,268 @@
+/* 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/. */
+
+/**
+ * This file provides utilities useful in testing more advanced networking
+ * scenarios, such as proxies and SSL connections.
+ */
+
+this.EXPORTED_SYMBOLS = ['NetworkTestUtils'];
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cr = Components.results;
+var CC = Components.Constructor;
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/mailServices.js");
+
+const ServerSocket = CC("@mozilla.org/network/server-socket;1",
+                        "nsIServerSocket",
+                        "init");
+const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
+                             "nsIBinaryInputStream",
+                             "setInputStream");
+
+// The following code is adapted from network/test/unit/test_socks.js, in order
+// to provide a SOCKS proxy server for our testing code.
+//
+// For more details on how SOCKSv5 works, please read RFC 1928.
+var currentThread = Services.tm.currentThread;
+
+const STATE_WAIT_GREETING = 1;
+const STATE_WAIT_SOCKS5_REQUEST = 2;
+
+/**
+ * A client of a SOCKS connection.
+ *
+ * This doesn't implement all of SOCKSv5, just enough to get a simple proxy
+ * working for the test code.
+ *
+ * @param client_in The nsIInputStream of the socket.
+ * @param client_out The nsIOutputStream of the socket.
+ */
+function SocksClient(client_in, client_out) {
+  this.client_in = client_in;
+  this.client_out = client_out;
+  this.inbuf = [];
+  this.state = STATE_WAIT_GREETING;
+  this.waitRead(this.client_in);
+}
+SocksClient.prototype = {
+  // ... implement nsIInputStreamCallback ...
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIInputStreamCallback]),
+  onInputStreamReady(input) {
+    var len = input.available();
+    var bin = new BinaryInputStream(input);
+    var data = bin.readByteArray(len);
+    this.inbuf = this.inbuf.concat(data);
+
+    switch (this.state) {
+      case STATE_WAIT_GREETING:
+        this.handleGreeting();
+        break;
+      case STATE_WAIT_SOCKS5_REQUEST:
+        this.handleSocks5Request();
+        break;
+    }
+
+    if (!this.sub_transport)
+      this.waitRead(input);
+  },
+
+  /// Listen on the input for the next packet
+  waitRead(input) {
+    input.asyncWait(this, 0, 0, currentThread);
+  },
+
+  /// Simple handler to write out a binary string (because xpidl sucks here)
+  write(buf) {
+    this.client_out.write(buf, buf.length);
+  },
+
+  /// Handle the first SOCKSv5 client message
+  handleGreeting() {
+    if (this.inbuf.length == 0)
+      return;
+
+    if (this.inbuf[0] != 5) {
+      dump("Unknown protocol version: " + this.inbuf[0] + '\n');
+      this.close();
+      return;
+    }
+
+    // Some quality checks to make sure we've read the entire greeting.
+    if (this.inbuf.length < 2)
+      return;
+    var nmethods = this.inbuf[1];
+    if (this.inbuf.length < 2 + nmethods)
+      return;
+    var methods = this.inbuf.slice(2, 2 + nmethods);
+    this.inbuf = [];
+
+    // Tell them that we don't log into this SOCKS server.
+    this.state = STATE_WAIT_SOCKS5_REQUEST;
+    this.write('\x05\x00');
+  },
+
+  /// Handle the second SOCKSv5 message
+  handleSocks5Request() {
+    if (this.inbuf.length < 4)
+      return;
+
+    // Find the address:port requested.
+    var version = this.inbuf[0];
+    var cmd = this.inbuf[1];
+    var atype = this.inbuf[3];
+    if (atype == 0x01) { // IPv4 Address
+      var len = 4;
+      var addr = this.inbuf.slice(4, 8).join('.');
+    } else if (atype == 0x03) { // Domain name
+      var len = this.inbuf[4];
+      var addr = String.fromCharCode.apply(null,
+        this.inbuf.slice(5, 5 + len));
+      len = len + 1;
+    } else if (atype == 0x04) { // IPv6 address
+      var len = 16;
+      var addr = this.inbuf.slice(4, 20).map(i => i.toString(16)).join(':');
+    }
+    var port = this.inbuf[4 + len] << 8 | this.inbuf[5 + len];
+    dump("Requesting " + addr + ":" + port + '\n');
+
+    // Map that data to the port we report.
+    var foundPort = gPortMap.get(addr + ":" + port);
+    dump("This was mapped to " + foundPort + '\n');
+
+    if (foundPort !== undefined) {
+      this.write("\x05\x00\x00" + // Header for response
+          "\x04" + "\x00".repeat(15) + "\x01" + // IPv6 address ::1
+          String.fromCharCode(foundPort >> 8) +
+          String.fromCharCode(foundPort & 0xff) // Port number
+      );
+    } else {
+      this.write("\x05\x05\x00" + // Header for failed response
+          "\x04" + "\x00".repeat(15) + "\x01" + // IPv6 address ::1
+          "\x00\x00");
+      this.close();
+      return;
+    }
+
+    // At this point, we contact the local server on that port and then we feed
+    // the data back and forth. Easiest way to do that is to open the connection
+    // and use the async copy to do it in a background thread.
+    let sts = Cc["@mozilla.org/network/socket-transport-service;1"]
+                .getService(Ci.nsISocketTransportService);
+    let trans = sts.createTransport([], 0, "localhost", foundPort, null);
+    let tunnelInput = trans.openInputStream(0, 1024, 1024);
+    let tunnelOutput = trans.openOutputStream(0, 1024, 1024);
+    this.sub_transport = trans;
+    NetUtil.asyncCopy(tunnelInput, this.client_out);
+    NetUtil.asyncCopy(this.client_in, tunnelOutput);
+  },
+
+  close() {
+    this.client_in.close();
+    this.client_out.close();
+    if (this.sub_transport)
+      this.sub_transport.close(Cr.NS_OK);
+  }
+};
+
+/// A SOCKS server that runs on a random port.
+function SocksTestServer() {
+  this.listener = ServerSocket(-1, true, -1);
+  dump("Starting SOCKS server on " + this.listener.port + '\n');
+  this.port = this.listener.port;
+  this.listener.asyncListen(this);
+  this.client_connections = [];
+}
+SocksTestServer.prototype = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIServerSocketListener]),
+
+  onSocketAccepted(socket, trans) {
+    var input = trans.openInputStream(0, 0, 0);
+    var output = trans.openOutputStream(0, 0, 0);
+    var client = new SocksClient(input, output);
+    this.client_connections.push(client);
+  },
+
+  onStopListening(socket) { },
+
+  close() {
+    for (let client of this.client_connections)
+      client.close();
+    this.client_connections = [];
+    if (this.listener) {
+      this.listener.close();
+      this.listener = null;
+    }
+  }
+};
+
+var gSocksServer = null;
+/// hostname:port -> the port on localhost that the server really runs on.
+var gPortMap = new Map();
+
+var NetworkTestUtils = {
+  /**
+   * Set up a proxy entry such that requesting a connection to hostName:port
+   * will instead cause a connection to localRemappedPort. This will use a SOCKS
+   * proxy (because any other mechanism is too complicated). Since this is
+   * starting up a server, it does behoove you to call shutdownServers when you
+   * no longer need to use the proxy server.
+   *
+   * @param hostName          The DNS name to use for the client.
+   * @param hostPort          The port number to use for the client.
+   * @param localRemappedPort The port number on which the real server sits.
+   */
+  configureProxy(hostName, hostPort, localRemappedPort) {
+    if (gSocksServer == null) {
+      gSocksServer = new SocksTestServer();
+      // Using PAC makes much more sense here. However, it turns out that PAC
+      // appears to be broken with synchronous proxy resolve, so enabling the
+      // PAC mode requires bug 791645 to be fixed first.
+      /*
+      let pac = 'data:text/plain,function FindProxyForURL(url, host) {' +
+        "if (host == 'localhost' || host == '127.0.0.1') {" +
+          'return "DIRECT";' +
+        '}' +
+        'return "SOCKS5 127.0.0.1:' + gSocksServer.port + '";' +
+      '}';
+      dump(pac + '\n');
+      Services.prefs.setIntPref("network.proxy.type", 2);
+      Services.prefs.setCharPref("network.proxy.autoconfig_url", pac);
+      */
+
+      // Until then, we'll serve the actual proxy via a proxy filter.
+      let pps = Cc["@mozilla.org/network/protocol-proxy-service;1"]
+                  .getService(Ci.nsIProtocolProxyService);
+      let filter = {
+        QueryInterface: XPCOMUtils.generateQI([Ci.nsIProtocolProxyFilter]),
+        applyFilter(aProxyService, aURI, aProxyInfo) {
+          if (aURI.host != "localhost" && aURI.host != "127.0.0.1") {
+            return pps.newProxyInfo("socks", "localhost", gSocksServer.port,
+              Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST, 0, null);
+          }
+          return aProxyInfo;
+        },
+      };
+      pps.registerFilter(filter, 0);
+    }
+    dump("Requesting to map " + hostName + ":" + hostPort + "\n");
+    gPortMap.set(hostName + ":" + hostPort, localRemappedPort);
+  },
+
+  /**
+   * Turn off any servers started by this file (e.g., the SOCKS proxy server).
+   */
+  shutdownServers() {
+    if (gSocksServer)
+      gSocksServer.close();
+  },
+};
--- a/mailnews/test/resources/localAccountUtils.js
+++ b/mailnews/test/resources/localAccountUtils.js
@@ -72,37 +72,43 @@ var localAccountUtils = {
 
   /**
    * Create an nsIMsgIncomingServer and an nsIMsgAccount to go with it.
    *
    * @param aType The type of the server (pop3, imap etc).
    * @param aPort The port the server is on.
    * @param aUsername The username for the server.
    * @param aPassword The password for the server.
+   * @param aHostname The hostname for the server (defaults to localhost).
    * @return The newly-created nsIMsgIncomingServer.
    */
-  create_incoming_server: function(aType, aPort, aUsername, aPassword) {
+  create_incoming_server(aType, aPort, aUsername, aPassword,
+                         aHostname="localhost") {
     let serverAndAccount = localAccountUtils.
-      create_incoming_server_and_account(aType, aPort, aUsername, aPassword);
+      create_incoming_server_and_account(aType, aPort, aUsername, aPassword,
+                                         aHostname);
     return serverAndAccount.server;
   },
 
   /**
    * Create an nsIMsgIncomingServer and an nsIMsgAccount to go with it.
    *
    * @param aType The type of the server (pop3, imap etc).
    * @param aPort The port the server is on.
    * @param aUsername The username for the server.
    * @param aPassword The password for the server.
+   * @param aHostname The hostname for the server (defaults to localhost).
    * @return An object with the newly-created nsIMsgIncomingServer as the
              "server" property and the newly-created nsIMsgAccount as the
              "account" property.
    */
-  create_incoming_server_and_account: function (aType, aPort, aUsername, aPassword) {
-    let server = MailServices.accounts.createIncomingServer(aUsername, "localhost",
+  create_incoming_server_and_account(aType, aPort, aUsername, aPassword,
+                                     aHostname="localhost") {
+    let server = MailServices.accounts.createIncomingServer(aUsername,
+                                                            aHostname,
                                                             aType);
     server.port = aPort;
     if (aUsername != null)
       server.username = aUsername;
     if (aPassword != null)
       server.password = aPassword;
 
     server.valid = false;
@@ -122,21 +128,22 @@ var localAccountUtils = {
   },
 
   /**
    * Create an outgoing nsISmtpServer with the given parameters.
    *
    * @param aPort The port the server is on.
    * @param aUsername The username for the server
    * @param aPassword The password for the server
+   * @param aHostname The hostname for the server (defaults to localhost).
    * @return The newly-created nsISmtpServer.
    */
-  create_outgoing_server: function(aPort, aUsername, aPassword) {
+  create_outgoing_server(aPort, aUsername, aPassword, aHostname="localhost") {
     let server = MailServices.smtp.createServer();
-    server.hostname = "localhost";
+    server.hostname = aHostname;
     server.port = aPort;
     server.authMethod = Ci.nsMsgAuthMethod.none;
     return server;
   },
 
   /**
    * Associate the given outgoing server with the given incoming server's account.
    *