Bug 664726 - Add hooks to make address book more extend-able; r=jcranmer,standard8
authorMike Conley <mconley@mozilla.com>
Tue, 05 Jul 2011 16:02:13 +0100
changeset 8047 2a1da01c4f764df9b733da7134797aa7b8ce4f82
parent 8046 79ea291ad0c0261dc49b8507c33a15e8ef04bfab
child 8048 2bfa709f04ce425daf07825f288140b27dfb74b3
push idunknown
push userunknown
push dateunknown
reviewersjcranmer, standard8
bugs664726
Bug 664726 - Add hooks to make address book more extend-able; r=jcranmer,standard8
mail/components/addrbook/content/abCardOverlay.js
mail/components/addrbook/content/abCardOverlay.xul
mail/components/addrbook/content/abCardViewOverlay.js
mail/components/addrbook/content/abCommon.js
mail/components/addrbook/content/abEditListDialog.xul
mail/components/addrbook/content/abMailListDialog.xul
mailnews/addrbook/content/abMailListDialog.js
mailnews/addrbook/public/nsIAbDirectory.idl
mailnews/addrbook/public/nsIAbManager.idl
mailnews/addrbook/src/nsAbDirProperty.cpp
mailnews/addrbook/src/nsAbMDBDirectory.cpp
mailnews/addrbook/src/nsAbMDBDirectory.h
mailnews/addrbook/src/nsAbManager.cpp
mailnews/addrbook/src/nsAbOutlookDirectory.cpp
mailnews/addrbook/src/nsAbOutlookDirectory.h
--- a/mail/components/addrbook/content/abCardOverlay.js
+++ b/mail/components/addrbook/content/abCardOverlay.js
@@ -86,19 +86,20 @@ const kVcardFields =
          ["Custom3", "Custom3"],
          ["Custom4", "Custom4"],
           // Other > Notes
          ["Notes", "Notes"]];
 
 const kDefaultYear = 2000;
 var gEditCard;
 var gOnSaveListeners = new Array();
+var gOnLoadListeners = new Array();
 var gOkCallback = null;
 var gHideABPicker = false;
-var gOriginalPhotoURI = "";
+var gPhotoHandlers = {};
 
 function OnLoadNewCard()
 {
   InitEditCard();
 
   gEditCard.card =
     (("arguments" in window) && (window.arguments.length > 0) &&
      (window.arguments[0] instanceof Components.interfaces.nsIAbCard))
@@ -223,17 +224,17 @@ function EditCardOKButton()
 
   directory.modifyCard(gEditCard.card);
   
   for (i=0; i < foundDirectoriesCount; i++) {
     // Update the addressLists item for this card
     foundDirectories[i].directory.addressLists
                        .replaceElementAt(gEditCard.card, foundDirectories[i].index, false);
   }
-                                        
+
   NotifySaveListeners(directory);
 
   // callback to allow caller to update
   if (gOkCallback)
     gOkCallback();
 
   return true;  // close the window
 }
@@ -312,32 +313,68 @@ function OnLoadEditCard()
       
       // hide  remote content in HTML field for remote directories
       if (directory.isRemote)
         document.getElementById('allowRemoteContent').hidden = true;
     }
   }
 }
 
-// this is used by people who extend the ab card dialog
-// like Netscape does for screenname
+/* Registers functions that are called when loading the card
+ * values into the contact editor dialog.  This is useful if
+ * extensions have added extra fields to the nsIAbCard, and
+ * need to display them in the contact editor.
+ */
+function RegisterLoadListener(aFunc)
+{
+  gOnLoadListeners[gOnLoadListeners.length] = aFunc;
+}
+
+function UnregisterLoadListener(aFunc)
+{
+  var fIndex = gOnLoadListeners.indexOf(aFunc);
+  if (fIndex != -1)
+    gOnLoadListeners.splice(fIndex, 1);
+}
+
+// Notifies load listeners that an nsIAbCard is being loaded.
+function NotifyLoadListeners(aCard, aDoc)
+{
+  if (!gOnLoadListeners.length)
+    return;
+
+  for (var i = 0; i < gOnLoadListeners.length; i++)
+    gOnLoadListeners[i](aCard, aDoc);
+}
+
+/* Registers functions that are called when saving the card
+ * values.  This is useful if extensions have added extra
+ * fields to the user interface, and need to set those values
+ * in their nsIAbCard.
+ */
 function RegisterSaveListener(func)
 {
   gOnSaveListeners[gOnSaveListeners.length] = func;
 }
 
-// this is used by people who extend the ab card dialog
-// like Netscape does for screenname
+function UnregisterSaveListener(aFunc)
+{
+  var fIndex = gOnSaveListeners.indexOf(aFunc);
+  if (fIndex != -1)
+    gOnSaveListeners.splice(fIndex, 1);
+}
+
+// Notifies save listeners that an nsIAbCard is being saved.
 function NotifySaveListeners(directory)
 {
   if (!gOnSaveListeners.length)
     return;
 
-  for ( var i = 0; i < gOnSaveListeners.length; i++ )
-    gOnSaveListeners[i]();
+  for (var i = 0; i < gOnSaveListeners.length; i++)
+    gOnSaveListeners[i](gEditCard.card, document);
 
   // the save listeners might have tweaked the card
   // in which case we need to commit it.
   directory.modifyCard(gEditCard.card);
 }
 
 function InitPhoneticFields()
 {
@@ -404,18 +441,19 @@ function NewCardOKButton()
     if (gEditCard.card)
     {
       if (!CheckAndSetCardValues(gEditCard.card, document, true))
         return false;  // don't close window
 
       // replace gEditCard.card with the card we added
       // so that save listeners can get / set attributes on
       // the card that got created.
-      gEditCard.card = GetDirectoryFromURI(uri).addCard(gEditCard.card);
-      NotifySaveListeners();
+      var directory = GetDirectoryFromURI(uri);
+      gEditCard.card = directory.addCard(gEditCard.card);
+      NotifySaveListeners(directory);
       if ("arguments" in window && window.arguments[0] &&
           "allowRemoteContent" in window.arguments[0]) {
         // getProperty may return a "1" or "0" string, we want a boolean
         window.arguments[0].allowRemoteContent =
           gEditCard.card.getProperty("AllowRemoteContent", false) != false;
       }
     }
   }
@@ -424,16 +462,20 @@ function NewCardOKButton()
 }
 
 // Move the data from the cardproperty to the dialog
 function GetCardValues(cardproperty, doc)
 {
   if (!cardproperty)
     return;
 
+  // Pass the nsIAbCard and the Document through the listeners
+  // to give extensions a chance to populate custom fields.
+  NotifyLoadListeners(cardproperty, doc);
+
   for (var i = kVcardFields.length; i-- > 0; ) {
     doc.getElementById(kVcardFields[i][0]).value =
       cardproperty.getProperty(kVcardFields[i][1], "");
   }
 
   var birthday = doc.getElementById("Birthday");
   modifyDatepicker(birthday);
 
@@ -488,42 +530,20 @@ function GetCardValues(cardproperty, doc
     doc.getElementById("PhoneticFirstName").value = cardproperty.getProperty("PhoneticFirstName", "");
     doc.getElementById("PhoneticLastName").value = cardproperty.getProperty("PhoneticLastName", "");
   }
   catch (ex) {}
 
   // Store the original photo URI and update the photo
   // Select the type if there is a valid value stored for that type, otherwise
   // select the generic photo
-  gOriginalPhotoURI = cardproperty.getProperty("PhotoURI", "");
-  switch (cardproperty.getProperty("PhotoType", "")) {
-    case "file":
-      try {
-        var file = Components.classes["@mozilla.org/network/io-service;1"]
-                             .getService(Components.interfaces.nsIIOService)
-                             .newURI(gOriginalPhotoURI, null, null)
-                             .QueryInterface(Components.interfaces.nsIFileURL)
-                             .file;
-      } catch (e) {}
-      if (file) {
-        document.getElementById("PhotoFile").file = file;
-        updatePhoto("file");
-      }
-      else
-        updatePhoto("generic");
-      break;
-    case "web":
-      document.getElementById("PhotoURI").value = gOriginalPhotoURI;
-      updatePhoto("web");
-      break;
-    default:
-      if (gOriginalPhotoURI)
-        document.getElementById("GenericPhotoList").value = gOriginalPhotoURI;
-      updatePhoto("generic");
-  }
+  var photoType = cardproperty.getProperty("PhotoType", "");
+  document.getElementById("PhotoType").value = photoType;
+  loadPhoto(cardproperty);
+  setCardEditorPhoto(photoType, cardproperty);
 }
 
 // when the ab card dialog is being loaded to show a vCard,
 // hide the fields which aren't supported
 // by vCard so the user does not try to edit them.
 function HideNonVcardFields()
 {
   document.getElementById("homeTabButton").hidden = true;
@@ -576,47 +596,18 @@ function CheckAndSetCardValues(cardprope
 
   // set phonetic fields if exist
   try {
     cardproperty.setProperty("PhoneticFirstName", doc.getElementById("PhoneticFirstName").value);
     cardproperty.setProperty("PhoneticLastName", doc.getElementById("PhoneticLastName").value);
   }
   catch (ex) {}
 
-  var type = document.getElementById("PhotoType").value;
-  var photoURI = gOriginalPhotoURI;
-  if (type == "file" && document.getElementById("PhotoFile").file)
-    photoURI = Components.classes["@mozilla.org/network/io-service;1"]
-                         .getService(Components.interfaces.nsIIOService)
-                         .newFileURI(document.getElementById("PhotoFile").file)
-                         .spec;
-  else if (type == "web" && document.getElementById("PhotoURI").value)
-    photoURI = document.getElementById("PhotoURI").value;
-  else {
-    type = "generic";
-    photoURI = document.getElementById("GenericPhotoList").value;
-  }
-  cardproperty.setProperty("PhotoType", type);
-  if (photoURI != gOriginalPhotoURI) {
-    // Store the original URI
-    cardproperty.setProperty("PhotoURI", photoURI);
-    // Save the photo if it isn't one of the generic photos
-    if (type == "generic") {
-      // Remove the original, if any
-      removePhoto(cardproperty.getProperty("PhotoName", null));
-    } else {
-      // Save the new file and store its URI as PhotoName 
-      var file = savePhoto(photoURI);
-      if (file) {
-        // Remove the original, if any
-        removePhoto(cardproperty.getProperty("PhotoName", null));
-        cardproperty.setProperty("PhotoName", file.leafName);
-      }
-    }
-  }
+  savePhoto(cardproperty);
+
   return true;
 }
 
 function CleanUpWebPage(webPage)
 {
   // no :// yet so we should add something
   if ( webPage.length && webPage.search("://") == -1 )
   {
@@ -906,46 +897,81 @@ function modifyDatepicker(aDatepicker) {
     }
     // make the field's value null if aValue is null and the field's value isn't
     if (aValue == null && aField.value != null)
       aField.value = null;
   }
 }
 
 /**
- * Updates the photo by setting the src attribute of the photo element.
+ * Updates the photo displayed in the contact editor based on the
+ * type of photo selected.  If the type is not recognized, the
+ * photo will automatically switch to the generic photo.
  *
- * @param aType Optional. The type of photo (web, file, or generic).
- *              If supplied the corresponding radio button will be selected.
- *              If not supplied the type will be determined by the currently
- *              selected type.
+ * @param aType The type of photo (web, file, or generic available
+ *              by default).
+ * @param aCard The nsIAbCard being edited
+ *
  */
-function updatePhoto(aType) {
-  if (aType)
-    // Select the type's radio button
-    document.getElementById("PhotoType").value = aType;
-  else
-    aType = document.getElementById("PhotoType").value;
+function setCardEditorPhoto(aType, aCard)
+{
+  if (!gPhotoHandlers[aType] ||
+      !gPhotoHandlers[aType].onShow(aCard, document, "photo"))
+    gPhotoHandlers["generic"].onShow(aCard, document, "photo");
+}
+
+/**
+ * Extract the photo information from an nsIAbCard, and populate
+ * the appropriate input fields in the contact editor.  If the
+ * nsIAbCard returns an unrecognized PhotoType, the generic
+ * display photo is switched to.
+ *
+ * @param aCard The nsIAbCard to extract the information from.
+ *
+ */
+function loadPhoto(aCard)
+{
+  var type = aCard.getProperty("PhotoType", "")
+  if (!gPhotoHandlers[type] ||
+      !gPhotoHandlers[type].onLoad(aCard, document))
+    gPhotoHandlers["generic"].onLoad(aCard, document);
+}
 
-  var value;
-  switch (aType) {
-    case "file":
-      var file = document.getElementById("PhotoFile").file;
-      value = file ? Components.classes["@mozilla.org/network/io-service;1"]
-                               .getService(Components.interfaces.nsIIOService)
-                               .newFileURI(file)
-                               .spec : "";
-      break;
-    case "web":
-      value = document.getElementById("PhotoURI").value;
-      break;
-    default:
-      value = document.getElementById("GenericPhotoList").value;
-  }
-  document.getElementById("photo").setAttribute("src", value || defaultPhotoURI);
+/**
+ * Given the fields in the current contact editor, commit
+ * the photo to an nsIAbCard.  If the photo cannot be saved,
+ * the generic contact photo is saved instead.
+ *
+ * @param aType
+ *
+ */
+function savePhoto(aCard)
+{
+  var type = document.getElementById("PhotoType").value;
+  if (!gPhotoHandlers[type] ||
+      !gPhotoHandlers[type].onSave(aCard, document))
+    gPhotoHandlers["generic"].onSave(aCard, document);
+}
+
+/**
+ * Event handler for when the user switches the type of
+ * photo for the nsIAbCard being edited.  Called from
+ * abCardOverlay.xul.
+ */
+function onSwitchPhotoType(photoType)
+{
+  if (!gEditCard)
+    return;
+
+  if (photoType)
+    document.getElementById("PhotoType").value = photoType;
+  else
+    photoType = document.getElementById("PhotoType").value;
+
+  setCardEditorPhoto(photoType, gEditCard.card);
 }
 
 /**
  * Removes the photo file at the given path, if present.
  *
  * @param aName The name of the photo to remove from the Photos directory.
  *
  * @return true if the file was deleted.
@@ -979,13 +1005,178 @@ function browsePhoto() {
   fp.init(window, gAddressBookBundle.getString("browsePhoto"), nsIFilePicker.modeOpen);
   
   // Add All Files & Image Files filters and select the latter
   fp.appendFilters(nsIFilePicker.filterImages);
   fp.appendFilters(nsIFilePicker.filterAll);
 
   if (fp.show() == nsIFilePicker.returnOK) {
     document.getElementById("PhotoFile").file = fp.file;
-    updatePhoto("file");
+    let photoType = document.getElementById("FilePhotoType").value;
+    onSwitchPhotoType(photoType);
     return true;
   }
   return false;
 }
+
+/* A photo handler defines the behaviour of the contact editor
+ * for a particular photo type. Each photo handler must implement
+ * the following interface:
+ *
+ * onLoad: function(aCard, aDocument):
+ *   Called when the editor wants to populate the contact editor
+ *   input fields with information about aCard's photo.  Note that
+ *   this does NOT make aCard's photo appear in the contact editor -
+ *   this is left to the onShow function.  Returns true on success.
+ *   If the function returns false, the generic photo handler onLoad
+ *   function will be called.
+ *
+ * onShow: function(aCard, aDocument, aTargetID):
+ *   Called when the editor wants to show this photo type.
+ *   The onShow method should take the input fields in the document,
+ *   and render the requested photo in the IMG tag with id
+ *   aTargetID.  Note that onShow does NOT save the photo for aCard -
+ *   this job is left to the onSave function.  Returns true on success.
+ *   If the function returns false, the generic photo handler onShow
+ *   function will be called.
+ *
+ * onSave: function(aCard, aDocument)
+ *   Called when the editor wants to save this photo type.  The
+ *   onSave method is responsible for analyzing the photo of this
+ *   type requested by the user, and storing it, as well as the
+ *   other fields required by onLoad/onShow to retrieve and display
+ *   the photo again.  Returns true on success.  If the function
+ *   returns false, the generic photo handler onSave function will
+ *   be called.
+ */
+
+var genericPhotoHandler = {
+
+  onLoad: function(aCard, aDocument) {
+    return true;
+  },
+
+  onShow: function(aCard, aDocument, aTargetID) {
+    aDocument.getElementById(aTargetID)
+             .setAttribute("src", defaultPhotoURI);
+    return true;
+  },
+
+  onSave: function(aCard, aDocument) {
+    // If we had the photo saved locally, clear it.
+    removePhoto(aCard.getProperty("PhotoName", null));
+    aCard.setProperty("PhotoName", null);
+    aCard.setProperty("PhotoType", "generic");
+    return true;
+  }
+}
+
+var filePhotoHandler = {
+
+  onLoad: function(aCard, aDocument) {
+    var photoURI = aCard.getProperty("PhotoURI", "");
+    try {
+      var file = Components.classes["@mozilla.org/network/io-service;1"]
+                           .getService(Components.interfaces.nsIIOService)
+                           .newURI(photoURI, null, null)
+                           .QueryInterface(Components.interfaces.nsIFileURL)
+                           .file;
+    } catch (e) {}
+
+    if (!file)
+      return false;
+
+    aDocument.getElementById("PhotoFile").file = file;
+    return true;
+  },
+
+  onShow: function(aCard, aDocument, aTargetID) {
+    var file = aDocument.getElementById("PhotoFile").file;
+    try {
+      var value = Components.classes["@mozilla.org/network/io-service;1"]
+                            .getService(Components.interfaces.nsIIOService)
+                            .newFileURI(file)
+                            .spec;
+    } catch (e) {}
+
+    if (!value)
+      return false;
+
+    aDocument.getElementById(aTargetID).setAttribute("src", value);
+    return true;
+  },
+
+  onSave: function(aCard, aDocument) {
+    var file = aDocument.getElementById("PhotoFile").file;
+    if (!file)
+      return false;
+
+    var photoURI = Components.classes["@mozilla.org/network/io-service;1"]
+                             .getService(Components.interfaces.nsIIOService)
+                             .newFileURI(file)
+                             .spec;
+
+    var file = storePhoto(photoURI);
+
+    if (!file)
+      return false;
+
+    // Remove the original, if any
+    removePhoto(aCard.getProperty("PhotoName", null));
+    aCard.setProperty("PhotoName", file.leafName);
+    aCard.setProperty("PhotoType", "file");
+    aCard.setProperty("PhotoURI", photoURI);
+    return true;
+  }
+}
+
+var webPhotoHandler = {
+
+  onLoad: function(aCard, aDocument) {
+    var photoURI = aCard.getProperty("PhotoURI", null);
+
+    if (!photoURI)
+      return false;
+
+    aDocument.getElementById("PhotoURI").value = photoURI;
+    return true;
+  },
+
+  onShow: function(aCard, aDocument, aTargetID) {
+    var photoURI = aDocument.getElementById("PhotoURI").value;
+
+    if (!photoURI)
+      return false;
+
+    aDocument.getElementById(aTargetID).setAttribute("src", photoURI);
+    return true;
+  },
+
+  onSave: function(aCard, aDocument) {
+    var photoURI = aDocument.getElementById("PhotoURI").value;
+
+    var file = storePhoto(photoURI);
+    if (!file)
+      return false;
+
+    // Remove the original, if any
+    removePhoto(aCard.getProperty("PhotoName", null));
+    aCard.setProperty("PhotoName", file.leafName);
+    aCard.setProperty("PhotoURI", photoURI);
+    aCard.setProperty("PhotoType", "web");
+    return true;
+  }
+}
+
+/* In order for other photo handlers to be recognized for
+ * a particular type, they must be registered through this
+ * function.
+ * @param aType the type of photo to handle
+ * @param aPhotoHandler the photo handler to register
+ */
+function registerPhotoHandler(aType, aPhotoHandler)
+{
+  gPhotoHandlers[aType] = aPhotoHandler;
+}
+
+registerPhotoHandler("generic", genericPhotoHandler);
+registerPhotoHandler("web", webPhotoHandler);
+registerPhotoHandler("file", filePhotoHandler);
--- a/mail/components/addrbook/content/abCardOverlay.xul
+++ b/mail/components/addrbook/content/abCardOverlay.xul
@@ -66,25 +66,25 @@
     <tabpanels id="abTabPanels" flex="1">
       <!-- ** Name Tab ** -->
       <!-- The following vbox contains two hboxes
            top: Name/Email/Phonenumber bottom: Email prefs. -->
       <vbox id="abNameTab" >
         <!-- This hbox contains two vboxes
              left: Name/Email, right: Phonenumbers -->
         <hbox>
-          <vbox> <!-- This box contains the Names and Emailnames -->
+          <vbox id="namesAndEmailAddresses"> <!-- This box contains the Names and Emailnames -->
 
             <!-- LOCALIZATION NOTE:
                  NameField1, NameField2, PhoneticField1, PhoneticField2
                  those fields are either LN or FN depends on the target country.
                  They are configurable in the .dtd file.
               -->
 
-            <hbox align="center">
+            <hbox id="NameField1Container" align="center">
               <spacer flex="1"/>
               <label control="&NameField1.id;" value="&NameField1.label;"
                      accesskey="&NameField1.accesskey;"/>
               <hbox class="CardEditWidth" align="center">
                 <textbox id="&NameField1.id;" flex="1"
                          oninput="GenerateDisplayName()"/>
 
                 <!-- LOCALIZATION NOTE:
@@ -93,17 +93,17 @@
                   -->
 
                 <spacer id="PhoneticSpacer1" flex="1" hidden="true"/>
                 <label id="PhoneticLabel1" control="&PhoneticField1.id;"
                        value="&PhoneticField1.label;" hidden="true"/>
                 <textbox id="&PhoneticField1.id;" flex="1" hidden="true"/>
               </hbox>
             </hbox>
-            <hbox align="center">
+            <hbox id="NameField2Container" align="center">
               <spacer flex="1"/>
               <label control="&NameField2.id;" value="&NameField2.label;"
                      accesskey="&NameField2.accesskey;"/>
               <hbox class="CardEditWidth" align="center">
                 <textbox id="&NameField2.id;" flex="1"
                          oninput="GenerateDisplayName()"/>
 
                 <!-- LOCALIZATION NOTE:
@@ -112,94 +112,94 @@
                   -->
 
                 <spacer id="PhoneticSpacer2" flex="1" hidden="true"/>
                 <label id="PhoneticLabel2" control="&PhoneticField2.id;"
                        value="&PhoneticField2.label;" hidden="true"/>
                 <textbox id="&PhoneticField2.id;" flex="1" hidden="true"/>
               </hbox>
             </hbox>
-            <hbox align="center">
+            <hbox id="DisplayNameContainer" align="center">
               <spacer flex="1"/>
               <label control="DisplayName" value="&DisplayName.label;"
                      accesskey="&DisplayName.accesskey;" />
               <hbox class="CardEditWidth">
                 <textbox id="DisplayName" flex="1"
                          oninput="DisplayNameChanged()"/>
               </hbox>
             </hbox>
-            <hbox align="center">
+            <hbox id="PreferDisplayNameContainer" align="center">
               <spacer flex="1"/>
               <hbox class="CardEditWidth">
                 <checkbox id="preferDisplayName"
                           label="&preferDisplayName.label;"
                           accesskey="&preferDisplayName.accesskey;"/>
               </hbox>
             </hbox>
 
-            <hbox id="nickNameContainer" align="center">
+            <hbox id="NickNameContainer" align="center">
               <spacer flex="1"/>
               <label control="NickName" value="&NickName.label;"
                      accesskey="&NickName.accesskey;"/>
               <hbox class="CardEditWidth">
                 <textbox id="NickName" flex="1"/>
               </hbox>
             </hbox>
-            <hbox align="center">
+            <hbox id="PrimaryEmailContainer" align="center">
               <spacer flex="1"/>
               <label control="PrimaryEmail" value="&PrimaryEmail.label;"
                      accesskey="&PrimaryEmail.accesskey;"/>
               <hbox class="CardEditWidth">
                 <textbox id="PrimaryEmail" flex="1" class="uri-element"/>
               </hbox>
             </hbox>
-            <hbox id="secondaryEmailContainer" align="center">
+            <hbox id="SecondaryEmailContainer" align="center">
               <spacer flex="1"/>
               <label control="SecondEmail" value="&SecondEmail.label;"
                      accesskey="&SecondEmail.accesskey;"/>
               <hbox class="CardEditWidth">
                 <textbox id="SecondEmail" flex="1" class="uri-element"/>
               </hbox>
             </hbox>
-            <hbox id="screenNameContainer" align="center">
+            <hbox id="ScreenNameContainer" align="center">
               <spacer flex="1"/>
               <label control="ScreenName" value="&ScreenName.label;"
                      accesskey="&ScreenName.accesskey;"/>
               <hbox class="CardEditWidth">
                 <textbox id="ScreenName" flex="1"/>
               </hbox>
             </hbox>
           </vbox> <!-- End of Name/Email -->
           <!-- Phone Number section -->
-          <vbox>
-            <hbox align="center">
+          <vbox id="PhoneNumbers">
+            <hbox id="WorkPhoneContainer" align="center">
               <spacer flex="1"/>
               <label control="WorkPhone" value="&WorkPhone.label;"
                      accesskey="&WorkPhone.accesskey;" />
               <textbox id="WorkPhone" class="PhoneEditWidth"/>
             </hbox>
-            <hbox align="center">
+            <hbox id="HomePhoneContainer" align="center">
               <spacer flex="1"/>
               <label control="HomePhone" value="&HomePhone.label;"
                      accesskey="&HomePhone.accesskey;"/>
               <textbox id="HomePhone" class="PhoneEditWidth"/>
             </hbox>
-            <hbox align="center">
+            <hbox id="FaxNumberContainer" align="center">
               <spacer flex="1"/>
               <label control="FaxNumber" value="&FaxNumber.label;"
                      accesskey="&FaxNumber.accesskey;"/>
               <textbox id="FaxNumber" class="PhoneEditWidth"/>
             </hbox>
-            <hbox align="center">
+            <hbox id="PagerNumberContainer" align="center">
               <spacer flex="1"/>
               <label control="PagerNumber" value="&PagerNumber.label;"
                      accesskey="&PagerNumber.accesskey;"/>
               <textbox id="PagerNumber" class="PhoneEditWidth"/>
             </hbox>
-            <hbox align="center">
+            <hbox id="CellularNumberContainer" align="center">
               <spacer flex="1"/>
               <label control="CellularNumber" value="&CellularNumber.label;"
                      accesskey="&CellularNumber.accesskey;"/>
               <textbox id="CellularNumber" class="PhoneEditWidth"/>
             </hbox>
           </vbox> <!-- End of Phonenumbers -->
         </hbox> <!-- End of Name/Email/Phonenumbers -->
         <!-- Email Preferences -->
@@ -236,21 +236,21 @@
         <hbox align="center">
           <spacer flex="1"/>
           <label control="HomeAddress2" value="&HomeAddress2.label;"
                  accesskey="&HomeAddress2.accesskey;"/>
           <hbox class="AddressCardEditWidth">
             <textbox id="HomeAddress2" flex="1"/>
           </hbox>
         </hbox>
-        <hbox align="center">
+        <hbox id="HomeCityContainer" align="center">
           <spacer flex="1"/>
           <label control="HomeCity" value="&HomeCity.label;"
                  accesskey="&HomeCity.accesskey;"/>
-          <hbox class="AddressCardEditWidth">
+          <hbox id="HomeCityFieldContainer" align="center" class="AddressCardEditWidth">
             <textbox id="HomeCity" flex="1"/>
           </hbox>
         </hbox>
         <hbox align="center">
           <spacer flex="1"/>
           <label control="HomeState" value="&HomeState.label;"
                  accesskey="&HomeState.accesskey;"/>
           <hbox align="center" class="AddressCardEditWidth">
@@ -264,17 +264,17 @@
         <hbox align="center">
           <spacer flex="1"/>
           <label control="HomeCountry" value="&HomeCountry.label;"
                  accesskey="&HomeCountry.accesskey;"/>
           <hbox class="AddressCardEditWidth">
             <textbox id="HomeCountry" flex="1"/>
           </hbox>
         </hbox>
-        <hbox align="center">
+        <hbox id="WebPage2Container" align="center">
           <spacer flex="1"/>
           <label control="WebPage2" value="&HomeWebPage.label;"
                  accesskey="&HomeWebPage.accesskey;"/>
           <hbox class="AddressCardEditWidth">
             <textbox id="WebPage2" flex="1" class="uri-element"/>
           </hbox>
         </hbox>
         <hbox id="birthdayField" align="center">
@@ -294,80 +294,80 @@
             <label value="&YearsOld.label;"/>
             <spacer flex="1"/>
           </hbox>
         </hbox>
       </vbox>
 
       <!-- ** Business Address Tab ** -->
       <vbox id="abBusinessTab" >
-        <hbox align="center">
+        <hbox id="JobTitleDepartmentContainer" align="center">
           <spacer flex="1"/>
           <label control="JobTitle" value="&JobTitle.label;"
                  accesskey="&JobTitle.accesskey;"/>
           <hbox class="AddressCardEditWidth" align="center">
             <textbox id="JobTitle" flex="1"/>
             <label control="Department" value="&Department.label;"
                    accesskey="&Department.accesskey;"/>
             <textbox id="Department" flex="1"/>
           </hbox>
         </hbox>
-        <hbox align="center">
+        <hbox id="CompanyContainer" align="center">
           <spacer flex="1"/>
           <label control="Company" value="&Company.label;"
                  accesskey="&Company.accesskey;"/>
           <hbox class="AddressCardEditWidth">
             <textbox id="Company" flex="1"/>
           </hbox>
         </hbox>
-        <hbox align="center">
+        <hbox id="WorkAddressContainer" align="center">
           <spacer flex="1"/>
           <label control="WorkAddress" value="&WorkAddress.label;"
                  accesskey="&WorkAddress.accesskey;"/>
           <hbox class="AddressCardEditWidth">
             <textbox id="WorkAddress" flex="1"/>
           </hbox>
         </hbox>
-        <hbox align="center">
+        <hbox id="WorkAddress2Container" align="center">
           <spacer flex="1"/>
           <label control="WorkAddress2" value="&WorkAddress2.label;"
                  accesskey="&WorkAddress2.accesskey;"/>
           <hbox class="AddressCardEditWidth">
             <textbox id="WorkAddress2" flex="1"/>
           </hbox>
         </hbox>
-        <hbox align="center">
+        <hbox id="WorkCityContainer" align="center">
           <spacer flex="1"/>
           <label control="WorkCity" value="&WorkCity.label;"
                  accesskey="&WorkCity.accesskey;"/>
-          <hbox class="AddressCardEditWidth">
+          <hbox id="WorkCityFieldContainer" class="AddressCardEditWidth" align="center">
             <textbox id="WorkCity" flex="1"/>
           </hbox>
         </hbox>
-        <hbox align="center">
+        <hbox id="WorkStateZipContainer" align="center">
           <spacer flex="1"/>
           <label control="WorkState" value="&WorkState.label;"
                  accesskey="&WorkState.accesskey;"/>
           <hbox class="AddressCardEditWidth" align="center">
             <textbox id="WorkState" flex="1"/>
             <spacer class="stateZipSpacer"/>
             <label control="WorkZipCode" value="&WorkZipCode.label;"
                    accesskey="&WorkZipCode.accesskey;"/>
             <textbox id="WorkZipCode" class="ZipWidth"/>
           </hbox>
         </hbox>
-        <hbox align="center">
+        <hbox id="WorkCountryContainer" align="center">
           <spacer flex="1"/>
           <label control="WorkCountry" value="&WorkCountry.label;"
                  accesskey="&WorkCountry.accesskey;"/>
           <hbox class="AddressCardEditWidth">
             <textbox id="WorkCountry" flex="1"/>
           </hbox>
         </hbox>
-        <hbox align="center">
+        <hbox id="WebPage1Container" align="center">
           <spacer flex="1"/>
           <label control="WebPage1" value="&WorkWebPage.label;"
                  accesskey="&WorkWebPage.accesskey;"/>
           <hbox class="AddressCardEditWidth">
             <textbox id="WebPage1" flex="1" class="uri-element"/>
           </hbox>
         </hbox>
       </vbox>
@@ -403,43 +403,51 @@
 
       <!-- ** Photo Tab ** -->
       <hbox id="abPhotoTab" align="center">
         <description style="min-width: 25ch; max-width: 25ch; text-align: center">
           <html:img id="photo" style="max-width: 25ch; max-height: 25ch;"/>
         </description>
         <groupbox flex="1">
           <caption label="&PhotoDesc.label;"/>
-          <radiogroup id="PhotoType" onselect="updatePhoto();">
-            <radio value="generic" label="&GenericPhoto.label;"
-                   accesskey="&GenericPhoto.accesskey;"/>
-            <menulist id="GenericPhotoList" class="indent" flex="1"
-                      oncommand="updatePhoto('generic');">
-              <menupopup>
-                <menuitem label="&DefaultPhoto.label;" selected="true"
-                          value="chrome://messenger/skin/addressbook/icons/contact-generic.png"
-                          image="chrome://messenger/skin/addressbook/icons/contact-generic-tiny.png"/>
-              </menupopup>
-            </menulist>
-            <radio value="file" label="&PhotoFile.label;"
-                   accesskey="&PhotoFile.accesskey;"/>
-            <hbox class="indent">
-              <filefield id="PhotoFile" maxlength="255" flex="1"/>
-              <button oncommand="browsePhoto();" id="BrowsePhoto"
-                      label="&BrowsePhoto.label;"
-                      accesskey="&BrowsePhoto.accesskey;"/>
-            </hbox>
-            <radio value="web" label="&PhotoURL.label;"
-                   accesskey="&PhotoURL.accesskey;"/>
-            <hbox class="indent">
-              <textbox id="PhotoURI" maxlength="255" flex="1"
-                       placeholder="&PhotoURL.placeholder;"/>
-              <button oncommand="updatePhoto('web');" id="UpdatePhoto"
-                      label="&UpdatePhoto.label;"
-                      accesskey="&UpdatePhoto.accesskey;"/>
-            </hbox>
+          <radiogroup id="PhotoType" onselect="onSwitchPhotoType();">
+              <vbox id="GenericPhotoContainer">
+                <radio id="GenericPhotoType" value="generic" label="&GenericPhoto.label;"
+                       accesskey="&GenericPhoto.accesskey;"/>
+                <menulist id="GenericPhotoList" class="indent" flex="1"
+                          oncommand="onSwitchPhotoType('generic');">
+                  <menupopup>
+                    <menuitem label="&DefaultPhoto.label;" selected="true"
+                              value="chrome://messenger/skin/addressbook/icons/contact-generic.png"
+                              image="chrome://messenger/skin/addressbook/icons/contact-generic-tiny.png"/>
+                  </menupopup>
+                </menulist>
+              </vbox>
+
+              <vbox id="FilePhotoContainer">
+                <radio id="FilePhotoType" value="file" label="&PhotoFile.label;"
+                       accesskey="&PhotoFile.accesskey;"/>
+                <hbox class="indent">
+                  <filefield id="PhotoFile" maxlength="255" flex="1"/>
+                   <button oncommand="browsePhoto();" id="BrowsePhoto"
+                          label="&BrowsePhoto.label;"
+                          accesskey="&BrowsePhoto.accesskey;"/>
+                </hbox>
+              </vbox>
+
+              <vbox id="WebPhotoContainer">
+                <radio id="WebPhotoType" value="web" label="&PhotoURL.label;"
+                       accesskey="&PhotoURL.accesskey;"/>
+                <hbox class="indent">
+                  <textbox id="PhotoURI" maxlength="255" flex="1"
+                           placeholder="&PhotoURL.placeholder;"/>
+                  <button oncommand="onSwitchPhotoType('web');" id="UpdatePhoto"
+                          label="&UpdatePhoto.label;"
+                          accesskey="&UpdatePhoto.accesskey;"/>
+                </hbox>
+              </vbox>
           </radiogroup>
         </groupbox>
       </hbox>
     </tabpanels>
   </tabbox>
 </vbox>
 </overlay>
--- a/mail/components/addrbook/content/abCardViewOverlay.js
+++ b/mail/components/addrbook/content/abCardViewOverlay.js
@@ -47,16 +47,17 @@ gPrefs = gPrefs.QueryInterface(Component
 
 var gProfileDirURL;
 
 var gMapItURLFormat = gPrefs.getComplexValue("mail.addr_book.mapit_url.format",
                                               Components.interfaces.nsIPrefLocalizedString).data;
 
 var gIOService = Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
 var gFileHandler = gIOService.getProtocolHandler("file").QueryInterface(Components.interfaces.nsIFileProtocolHandler);
+var gPhotoDisplayHandlers = {};
 
 var zListName;
 var zPrimaryEmail;
 var zSecondaryEmail;
 var zScreenName;
 var zNickname;
 var zDisplayName;
 var zWork;
@@ -204,17 +205,17 @@ function DisplayCardViewPane(realCard)
                isMailList : realCard.isMailList,
                mailListURI : realCard.mailListURI
   };
 
   var data = top.cvData;
   var visible;
 
   // Contact photo
-  cvData.cvPhoto.setAttribute("src", getPhotoURI(card.getProperty("PhotoName")));
+  displayPhoto(card, cvData.cvPhoto);
 
   var titleString;
   if (generatedName == "")
     titleString = card.primaryEmail;  // if no generatedName, use email
   else
     titleString = generatedName;
 
   // set fields in card view pane
@@ -589,8 +590,64 @@ function openLink(id)
     messenger = messenger.QueryInterface(Components.interfaces.nsIMessenger);
     messenger.launchExternalURL(document.getElementById(id).getAttribute("href"));
   } catch (ex) {}
 
   // return false, so we don't load the href in the addressbook window
   return false;
 }
 
+/* Display the contact photo from the nsIAbCard in the IMG element.
+ * If the photo cannot be displayed, show the generic contact
+ * photo.
+ */
+function displayPhoto(aCard, aImg)
+{
+  var type = aCard.getProperty("PhotoType", "");
+  if (!gPhotoDisplayHandlers[type] ||
+      !gPhotoDisplayHandlers[type](aCard, aImg))
+    gPhotoDisplayHandlers["generic"](aCard, aImg);
+}
+
+/* In order to display the contact photos in the card view, there
+ * must be a registered photo display handler for the card photo
+ * type.  The generic, file, and web photo types are handled
+ * by default.
+ *
+ * A photo display handler is a function that behaves as follows:
+ *
+ * function(aCard, aImg):
+ *    The function is responsible for determining how to retrieve
+ *    the photo from nsIAbCard aCard, and for displaying it in img
+ *    img element aImg.  Returns true if successful.  If it returns
+ *    false, the generic photo display handler will be called.
+ *
+ * The following display handlers are for the generic, file and
+ * web photo types.
+ */
+
+var genericPhotoDisplayHandler = function(aCard, aImg)
+{
+  aImg.setAttribute("src", defaultPhotoURI);
+  return true;
+}
+
+var photoNameDisplayHandler = function(aCard, aImg)
+{
+  var photoSrc = getPhotoURI(aCard.getProperty("PhotoName"));
+  aImg.setAttribute("src", photoSrc);
+  return true;
+}
+
+/* In order for a photo display handler to be registered for
+ * a particular photo type, it must be registered here.
+ */
+function registerPhotoDisplayHandler(aType, aPhotoDisplayHandler)
+{
+  if (!gPhotoDisplayHandlers[aType])
+    gPhotoDisplayHandlers[aType] = aPhotoDisplayHandler;
+}
+
+registerPhotoDisplayHandler("generic", genericPhotoDisplayHandler);
+// File and Web are treated the same, and therefore use the
+// same handler.
+registerPhotoDisplayHandler("file", photoNameDisplayHandler);
+registerPhotoDisplayHandler("web", photoNameDisplayHandler);
--- a/mail/components/addrbook/content/abCommon.js
+++ b/mail/components/addrbook/content/abCommon.js
@@ -352,19 +352,31 @@ function AbNewMessage()
   if (params)
   {
     params.type = msgComposeType.New;
     params.format = msgComposFormat.Default;
     var composeFields = Components.classes["@mozilla.org/messengercompose/composefields;1"].createInstance(Components.interfaces.nsIMsgCompFields);
     if (composeFields)
     {
       if (DirPaneHasFocus())
-        composeFields.to = GetSelectedAddressesFromDirTree();
+      {
+        var directory = gDirectoryTreeView.getDirectoryAtIndex(gDirTree.currentIndex);
+        if (directory && directory.isMailList &&
+            directory.getBoolValue("HidesRecipients", false))
+          // Bug 669301 (https://bugzilla.mozilla.org/show_bug.cgi?id=669301)
+          // We're using BCC right now to hide recipients from one another.
+          // We should probably use group syntax, but that's broken
+          // right now, so this will have to do.
+          composeFields.bcc = GetSelectedAddressesFromDirTree();
+        else
+          composeFields.to = GetSelectedAddressesFromDirTree();
+      }
       else
         composeFields.to = GetSelectedAddresses();
+
       params.composeFields = composeFields;
       msgComposeService.OpenComposeWindowWithParams(null, params);
     }
   }
 }
 
 // XXX todo
 // could this be moved into utilityOverlay.js?
@@ -968,17 +980,18 @@ function saveStreamToFile(aIStream, aFil
  * The URI is used to obtain a channel which is then opened synchronously and
  * this stream is written to the new file to store an offline, local copy of the
  * photo.
  *
  * @param aUri The URI of the photo.
  *
  * @return An nsIFile representation of the photo.
  */
-function savePhoto(aUri) {
+function storePhoto(aUri)
+{
   if (!aUri)
     return false;
 
   // Get the photos directory and check that it exists
   var file = getPhotosDir();
 
   // Create a channel from the URI and open it as an input stream
   var ios = Components.classes["@mozilla.org/network/io-service;1"]
@@ -1030,8 +1043,10 @@ function makePhotoFile(aDir, aExtension)
   // Find a random filename for the photo that doesn't exist yet
   do {
     filename = new String(Math.random()).replace("0.", "") + "." + aExtension;
     newFile = aDir.clone();
     newFile.append(filename);
   } while (newFile.exists());
   return newFile;
 }
+
+
--- a/mail/components/addrbook/content/abEditListDialog.xul
+++ b/mail/components/addrbook/content/abEditListDialog.xul
@@ -54,31 +54,31 @@
   </stringbundleset>
 
   <!-- move needed functions into a single js file -->
   <script type="application/javascript" src="chrome://messenger/content/messengercompose/addressingWidgetOverlay.js"/>
   <script type="application/javascript" src="chrome://messenger/content/addressbook/abCommon.js"/>
   <script type="application/javascript" src="chrome://messenger/content/addressbook/abMailListDialog.js"/>
 
   <vbox id="editlist">
-    <hbox>
+    <hbox id="ListNameContainer" align="center">
       <spacer flex="1"/>
       <label control="ListName" value="&ListName.label;" accesskey="&ListName.accesskey;" class="CardEditLabel"/>
       <hbox class="CardEditWidth">
         <textbox id="ListName" flex="1"/>
       </hbox>
     </hbox>
-    <hbox>
+    <hbox id="ListNickNameContainer" align="center">
       <spacer flex="1"/>
       <label control="ListNickName" value="&ListNickName.label;" accesskey="&ListNickName.accesskey;" class="CardEditLabel"/>
       <hbox class="CardEditWidth">
         <textbox id="ListNickName" flex="1"/>
       </hbox>
     </hbox>
-    <hbox>
+    <hbox id="ListDescriptionContainer" align="center">
       <spacer flex="1"/>
       <label control="ListDescription" value="&ListDescription.label;" accesskey="&ListDescription.accesskey;" class="CardEditLabel"/>
       <hbox class="CardEditWidth">
         <textbox id="ListDescription" flex="1"/>
       </hbox>
     </hbox>
 
     <spacer style="height:1em"/>
--- a/mail/components/addrbook/content/abMailListDialog.xul
+++ b/mail/components/addrbook/content/abMailListDialog.xul
@@ -64,31 +64,31 @@
       <menupopup id="abPopup-menupopup" class="addrbooksPopup" writable="true"
                  supportsmaillists="true"/>
     </menulist>
   </hbox>
 
   <spacer style="height:1em"/>
 
   <vbox id="editlist">
-    <hbox>
+    <hbox id="ListNameContainer" align="center">
       <spacer flex="1"/>
       <label control="ListName" value="&ListName.label;" accesskey="&ListName.accesskey;" class="CardEditLabel"/>
       <hbox class="CardEditWidth">
         <textbox id="ListName" flex="1"/>
       </hbox>
     </hbox>
-    <hbox>
+    <hbox id="ListNickNameContainer" align="center">
       <spacer flex="1"/>
       <label control="ListNickName" value="&ListNickName.label;" accesskey="&ListNickName.accesskey;" class="CardEditLabel"/>
       <hbox class="CardEditWidth">
         <textbox id="ListNickName" flex="1"/>
       </hbox>
     </hbox>
-    <hbox>
+    <hbox id="ListDescriptionContainer" align="center">
       <spacer flex="1"/>
       <label control="ListDescription" value="&ListDescription.label;" accesskey="&ListDescription.accesskey;" class="CardEditLabel"/>
       <hbox class="CardEditWidth">
         <textbox id="ListDescription" flex="1"/>
       </hbox>
     </hbox>
 
     <spacer style="height:1em"/>
--- a/mailnews/addrbook/content/abMailListDialog.js
+++ b/mailnews/addrbook/content/abMailListDialog.js
@@ -38,16 +38,18 @@
 top.MAX_RECIPIENTS = 1;
 var inputElementType = "";
 
 var gListCard;
 var gEditList;
 var oldListName = "";
 var gPromptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].getService(Components.interfaces.nsIPromptService);
 var gHeaderParser = Components.classes["@mozilla.org/messenger/headerparser;1"].getService(Components.interfaces.nsIMsgHeaderParser);
+var gLoadListeners = [];
+var gSaveListeners = [];
 
 try
 {
   var gDragService = Components.classes["@mozilla.org/widget/dragservice;1"]
                                .getService(Components.interfaces.nsIDragService);
 }
 catch (e)
 {
@@ -186,19 +188,21 @@ function MailListOKButton()
     if ( !uri )
       return false;  // don't close window
     // -----
 
     //Add mailing list to database
     var mailList = Components.classes["@mozilla.org/addressbook/directoryproperty;1"].createInstance();
     mailList = mailList.QueryInterface(Components.interfaces.nsIAbDirectory);
 
-    if (GetListValue(mailList, true)) {
-       var parentDirectory = GetDirectoryFromURI(uri);
-       parentDirectory.addMailList(mailList);
+    if (GetListValue(mailList, true))
+    {
+      var parentDirectory = GetDirectoryFromURI(uri);
+      mailList = parentDirectory.addMailList(mailList);
+      NotifySaveListeners(mailList);
     }
     else
       return false;
   }
 
   return true;  // close the window
 }
 
@@ -239,31 +243,34 @@ function OnLoadNewMailList()
   awFitDummyRows(1);
 
   document.addEventListener("keypress", awDocumentKeyPress, true);
 
   // focus on first name
   var listName = document.getElementById('ListName');
   if ( listName )
     setTimeout( function(firstTextBox) { firstTextBox.focus(); }, 0, listName );
+
+  NotifyLoadListeners(directory);
 }
 
 function EditListOKButton()
 {
   //edit mailing list in database
   if (GetListValue(gEditList, false))
   {
     if (gListCard) {
       // modify the list card (for the results pane) from the mailing list 
       gListCard.displayName = gEditList.dirName;
       gListCard.lastName = gEditList.dirName;
       gListCard.setProperty("NickName", gEditList.listNickName);
       gListCard.setProperty("Notes", gEditList.description);
     }
 
+    NotifySaveListeners(gEditList);
     gEditList.editMailListToDatabase(gListCard);
 
     return true;  // close the window
   }
 
   return false;
 }
 
@@ -323,16 +330,17 @@ function OnLoadEditList()
 
   document.addEventListener("keypress", awDocumentKeyPress, true);
 
   // workaround for bug 118337 - for mailing lists that have more rows than fits inside
   // the display, the value of the textbox inside the new row isn't inherited into the input -
   // the first row then appears to be duplicated at the end although it is actually empty.
   // see awAppendNewRow which copies first row and clears it
   setTimeout(AppendLastRow, 0);
+  NotifyLoadListeners(gEditList);
 }
 
 function AppendLastRow()
 { 
   AppendNewRowAndSetFocus();
   awFitDummyRows(1);
 
   // focus on first name
@@ -561,8 +569,65 @@ function DropListAddress(target, address
 {
     awClickEmptySpace(target, true);    //that will automatically set the focus on a new available row, and make sure is visible
     if (top.MAX_RECIPIENTS == 0)
     top.MAX_RECIPIENTS = 1;
   var lastInput = awGetInputElement(top.MAX_RECIPIENTS);
     lastInput.value = address;
     awAppendNewRow(true);
 }
+
+/* Allows extensions to register a listener function for
+ * when a mailing list is loaded.  The listener function
+ * should take two parameters - the first being the
+ * mailing list being loaded, the second one being the
+ * current window document.
+ */
+function RegisterLoadListener(aListener)
+{
+  gLoadListeners.push(aListener);
+}
+
+/* Allows extensions to unload a load listener function.
+ */
+function UnregisterLoadListener(aListener)
+{
+  var fIndex = gLoadListeners.indexOf(aListener);
+  if (fIndex != -1)
+    gLoadListeners.splice(fIndex, 1);
+}
+
+/* Allows extensions to register a listener function for
+ * when a mailing list is saved.  Like a load listener,
+ * the save listener should take two parameters: the first
+ * being a copy of the mailing list that is being saved,
+ * and the second being the current window document.
+ */
+function RegisterSaveListener(aListener)
+{
+  gSaveListeners.push(aListener);
+}
+
+/* Allows extensions to unload a save listener function.
+ */
+function UnregisterSaveListener(aListener)
+{
+  var fIndex = gSaveListeners.indexOf(aListener);
+  if (fIndex != -1)
+    gSaveListeners.splice(fIndex, 1);
+}
+
+/* Notifies all load listeners.
+ */
+function NotifyLoadListeners(aMailingList)
+{
+  for (let i = 0; i < gLoadListeners.length; i++)
+    gLoadListeners[i](aMailingList, document);
+}
+
+/* Notifies all save listeners.
+ */
+function NotifySaveListeners(aMailingList)
+{
+  for (let i = 0; i < gSaveListeners.length; i++)
+    gSaveListeners[i](aMailingList, document);
+}
+
--- a/mailnews/addrbook/public/nsIAbDirectory.idl
+++ b/mailnews/addrbook/public/nsIAbDirectory.idl
@@ -74,17 +74,17 @@ interface nsIMutableArray;
  * address book, the scheme is "moz-abmdbdirectory", so the contract ID for
  * the Mork-based address book type is:
  *
  * @mozilla.org/addressbook/directory;1?type=moz-abmdbdirectory
  *
  * The UUID of an nsIAbDirectory is its preference ID and its name, concatenated
  * together.
  */
-[scriptable, uuid(81927b85-11a2-4967-8c90-19ca600cedf0)]
+[scriptable, uuid(72dc868b-db5b-4daa-b6c6-071be4a05d02)]
 interface nsIAbDirectory : nsIAbCollection {
 
   /**
    * The chrome URI to use for bringing up a dialog to edit this directory.
    * When opening the dialog, use a JS argument of
    * {selectedDirectory: thisdir} where thisdir is this directory that you just
    * got the chrome URI from.
    */
@@ -213,18 +213,19 @@ interface nsIAbDirectory : nsIAbCollecti
 
   // Specific to a directory which stores mail lists
 
   /**
    * Creates a new mailing list in the directory. Currently only supported 
    * for top-level directories.
    *
    * @param  list  The new mailing list to add.
+   * @return The mailing list directory added, which may have been modified.
    */
-  void addMailList(in nsIAbDirectory list);
+  nsIAbDirectory addMailList(in nsIAbDirectory list);
 
   /**
    * Nick Name of the mailing list. This attribute is only really used when
    * the nsIAbDirectory represents a mailing list.
    */
   attribute AString listNickName;
 
   /**
@@ -291,16 +292,25 @@ interface nsIAbDirectory : nsIAbCollecti
   //@{
   long getIntValue(in string aName, in long aDefaultValue);
   boolean getBoolValue(in string aName, in boolean aDefaultValue);
   ACString getStringValue(in string aName, in ACString aDefaultValue);
   AUTF8String getLocalizedStringValue(in string aName, in AUTF8String aDefaultValue);
   //@}
 
   /**
+   * The following attributes are read from an nsIAbDirectory via the above methods:
+   *
+   * HidesRecipients (Boolean)
+   *    If true, and this nsIAbDirectory is a mailing list, then when sending mail to
+   *    this list, recipients addresses will be hidden from one another by sending
+   *    via BCC.
+   */
+
+  /**
    * @name  setXXXValue
    *
    * Helper functions to set different types of pref values.
    *
    * @param aName         The name of the pref within the branch dirPrefId to
    *                      get a value from.
    *
    * @param aValue        The value to set the pref to.
@@ -309,9 +319,10 @@ interface nsIAbDirectory : nsIAbCollecti
    *                      be obtained (e.g. dirPrefId isn't set).
    */
   //@{
   void setIntValue(in string aName, in long aValue);
   void setBoolValue(in string aName, in boolean aValue);
   void setStringValue(in string aName, in ACString aValue);
   void setLocalizedStringValue(in string aName, in AUTF8String aValue);
   //@}
+
 };
--- a/mailnews/addrbook/public/nsIAbManager.idl
+++ b/mailnews/addrbook/public/nsIAbManager.idl
@@ -39,25 +39,26 @@
 #include "nsIAbListener.idl"
 
 interface nsIDOMWindow;
 interface nsIAbDirectory;
 interface nsIAbCard;
 interface nsIAbDirectoryProperties;
 interface nsILocalFile;
 interface nsISimpleEnumerator;
+interface nsIAbBooleanExpression;
 
 /**
  * nsIAbManager is an interface to the main address book mananger
  * via the contract id "@mozilla.org/abmanager;1"
  *
  * It contains the main functions to create and delete address books as well
  * as some helper functions.
  */
-[scriptable, uuid(549bf1f6-ada4-40c5-9f59-4ea9eda4a935)]
+[scriptable, uuid(479919a2-c5f9-4d84-af87-f99c4ecb7f5e)]
 interface nsIAbManager : nsISupports 
 {
   /**
    * Returns an enumerator containing all the top-level directories
    * (non-recursive)
    */
   readonly attribute nsISimpleEnumerator directories;
 
@@ -195,9 +196,19 @@ interface nsIAbManager : nsISupports
    * Use of this method is preferred in such cases, since it is designed to work
    * with other methods of this interface.
    *
    * @param directoryId The directory ID.
    * @param localId     The per-directory ID.
    * @return            A string to use for the UUID.
    */
   AUTF8String generateUUID(in AUTF8String directoryId, in AUTF8String localId);
+
+
+  /**
+   * A utility function that converts an nsIAbDirectory query string to an
+   * nsIAbBooleanExpression.
+   *
+   * @param aQueryString The nsIAbDirectory query string
+   * @return an nsIAbBooleanExpression for the query string
+   */
+  nsIAbBooleanExpression convertQueryStringToExpression(in string aQueryString);
 };
--- a/mailnews/addrbook/src/nsAbDirProperty.cpp
+++ b/mailnews/addrbook/src/nsAbDirProperty.cpp
@@ -337,17 +337,17 @@ nsAbDirProperty::CreateNewDirectory(cons
                                     nsACString &aResult)
 { return NS_ERROR_NOT_IMPLEMENTED; }
 
 NS_IMETHODIMP
 nsAbDirProperty::CreateDirectoryByURI(const nsAString &aDisplayName,
                                       const nsACString &aURI)
 { return NS_ERROR_NOT_IMPLEMENTED; }
 
-NS_IMETHODIMP nsAbDirProperty::AddMailList(nsIAbDirectory *list)
+NS_IMETHODIMP nsAbDirProperty::AddMailList(nsIAbDirectory *list, nsIAbDirectory **addedList)
 { return NS_ERROR_NOT_IMPLEMENTED; }
 
 NS_IMETHODIMP nsAbDirProperty::EditMailListToDatabase(nsIAbCard *listCard)
 { return NS_ERROR_NOT_IMPLEMENTED; }
 
 NS_IMETHODIMP nsAbDirProperty::AddCard(nsIAbCard *childCard, nsIAbCard **addedCard)
 { return NS_ERROR_NOT_IMPLEMENTED; }
 
--- a/mailnews/addrbook/src/nsAbMDBDirectory.cpp
+++ b/mailnews/addrbook/src/nsAbMDBDirectory.cpp
@@ -621,18 +621,20 @@ NS_IMETHODIMP nsAbMDBDirectory::HasDirec
 
     if (NS_SUCCEEDED(rv))
       rv = database->ContainsMailList(dir, hasDir);
   }
 
   return rv;
 }
 
-NS_IMETHODIMP nsAbMDBDirectory::AddMailList(nsIAbDirectory *list)
+NS_IMETHODIMP nsAbMDBDirectory::AddMailList(nsIAbDirectory *list, nsIAbDirectory **addedList)
 {
+  NS_ENSURE_ARG_POINTER(addedList);
+
   if (mIsQueryURI)
     return NS_ERROR_NOT_IMPLEMENTED;
 
   nsresult rv = NS_OK;
   if (!mDatabase)
     rv = GetAbDatabase();
 
   if (NS_FAILED(rv) || !mDatabase)
@@ -672,16 +674,17 @@ NS_IMETHODIMP nsAbMDBDirectory::AddMailL
     nsCOMPtr<nsIAbMDBDirectory> dbnewList(do_QueryInterface(newList, &rv));
     NS_ENSURE_SUCCESS(rv, rv);
 
     dbnewList->CopyDBMailList(dblist);
     AddMailListToDirectory(newList);
     NotifyItemAdded(newList);
   }
 
+  NS_IF_ADDREF(*addedList = newList);
   return rv;
 }
 
 NS_IMETHODIMP nsAbMDBDirectory::AddCard(nsIAbCard* card, nsIAbCard **addedCard)
 {
   if (mIsQueryURI)
     return NS_ERROR_NOT_IMPLEMENTED;
 
--- a/mailnews/addrbook/src/nsAbMDBDirectory.h
+++ b/mailnews/addrbook/src/nsAbMDBDirectory.h
@@ -87,17 +87,17 @@ public:
   // nsIAbDirectory methods:
   NS_IMETHOD GetChildNodes(nsISimpleEnumerator* *result);
   NS_IMETHOD GetChildCards(nsISimpleEnumerator* *result);
   NS_IMETHOD GetIsQuery(PRBool *aResult);
   NS_IMETHOD DeleteDirectory(nsIAbDirectory *directory);
   NS_IMETHOD DeleteCards(nsIArray *cards);
   NS_IMETHOD HasCard(nsIAbCard *cards, PRBool *hasCard);
   NS_IMETHOD HasDirectory(nsIAbDirectory *dir, PRBool *hasDir);
-  NS_IMETHOD AddMailList(nsIAbDirectory *list);
+  NS_IMETHOD AddMailList(nsIAbDirectory *list, nsIAbDirectory **addedList);
   NS_IMETHOD AddCard(nsIAbCard *card, nsIAbCard **addedCard);
   NS_IMETHOD ModifyCard(nsIAbCard *aModifiedCard);
   NS_IMETHOD DropCard(nsIAbCard *card, PRBool needToCopyCard);
   NS_IMETHOD EditMailListToDatabase(nsIAbCard *listCard);
   NS_IMETHOD CardForEmailAddress(const nsACString &aEmailAddress,
                                  nsIAbCard ** aAbCard);
   NS_IMETHOD GetCardFromProperty(const char *aProperty,
                                  const nsACString &aValue,
--- a/mailnews/addrbook/src/nsAbManager.cpp
+++ b/mailnews/addrbook/src/nsAbManager.cpp
@@ -65,16 +65,17 @@
 #include "nsArrayUtils.h"
 #include "nsDirectoryServiceUtils.h"
 #include "nsIObserverService.h"
 #include "nsDirPrefs.h"
 #include "nsThreadUtils.h"
 #include "nsIAbDirFactory.h"
 #include "nsComponentManagerUtils.h"
 #include "nsIIOService.h"
+#include "nsAbQueryStringToExpression.h"
 
 struct ExportAttributesTableStruct
 {
   const char* abPropertyName;
   PRUint32 plainTextStringID;
 };
 
 // our schema is not fixed yet, but we still want some sort of objectclass
@@ -1274,8 +1275,16 @@ NS_IMETHODIMP
 nsAbManager::GenerateUUID(const nsACString &aDirectoryId,
                           const nsACString &aLocalId, nsACString &uuid)
 {
   uuid.Assign(aDirectoryId);
   uuid.Append('#');
   uuid.Append(aLocalId);
   return NS_OK;
 }
+
+NS_IMETHODIMP
+nsAbManager::ConvertQueryStringToExpression(const char *aQueryString,
+                                            nsIAbBooleanExpression **_retval)
+{
+  NS_ENSURE_ARG_POINTER(_retval);
+  return nsAbQueryStringToExpression::Convert(aQueryString, _retval);
+}
--- a/mailnews/addrbook/src/nsAbOutlookDirectory.cpp
+++ b/mailnews/addrbook/src/nsAbOutlookDirectory.cpp
@@ -422,21 +422,22 @@ NS_IMETHODIMP nsAbOutlookDirectory::AddC
 }
 
 NS_IMETHODIMP nsAbOutlookDirectory::DropCard(nsIAbCard *aData, PRBool needToCopyCard)
 {
     nsCOMPtr <nsIAbCard> addedCard;
     return AddCard(aData, getter_AddRefs(addedCard));
 }
 
-NS_IMETHODIMP nsAbOutlookDirectory::AddMailList(nsIAbDirectory *aMailList)
+NS_IMETHODIMP nsAbOutlookDirectory::AddMailList(nsIAbDirectory *aMailList, nsIAbDirectory **addedList)
 {
   if (mIsQueryURI)
     return NS_ERROR_NOT_IMPLEMENTED;
   NS_ENSURE_ARG_POINTER(aMailList);
+  NS_ENSURE_ARG_POINTER(addedList);
   if (m_IsMailList)
     return NS_OK;
   nsAbWinHelperGuard mapiAddBook (mAbWinType);
   nsCAutoString entryString;
   nsMapiEntry newEntry;
   PRBool didCopy = PR_FALSE;
 
   if (!mapiAddBook->IsOK())
@@ -481,16 +482,18 @@ NS_IMETHODIMP nsAbOutlookDirectory::AddM
 
   if (!m_AddressList)
   {
     m_AddressList = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
     NS_ENSURE_SUCCESS(rv, rv);
   }
   m_AddressList->AppendElement(newList, PR_FALSE);
   NotifyItemAddition(newList);
+  NS_IF_ADDREF(*addedList = newList);
+
   return rv;
 }
 
 NS_IMETHODIMP nsAbOutlookDirectory::EditMailListToDatabase(nsIAbCard *listCard)
 {
   if (mIsQueryURI)
     return NS_ERROR_NOT_IMPLEMENTED;
 
--- a/mailnews/addrbook/src/nsAbOutlookDirectory.h
+++ b/mailnews/addrbook/src/nsAbOutlookDirectory.h
@@ -72,17 +72,17 @@ public:
   NS_IMETHOD HasCard(nsIAbCard *aCard, PRBool *aHasCard);
   NS_IMETHOD HasDirectory(nsIAbDirectory *aDirectory, PRBool *aHasDirectory);
   NS_IMETHOD DeleteCards(nsIArray *aCardList);
   NS_IMETHOD DeleteDirectory(nsIAbDirectory *aDirectory);
   NS_IMETHOD UseForAutocomplete(const nsACString &aIdentityKey, PRBool *aResult);
   NS_IMETHOD AddCard(nsIAbCard *aData, nsIAbCard **addedCard);
   NS_IMETHOD ModifyCard(nsIAbCard *aModifiedCard);
   NS_IMETHOD DropCard(nsIAbCard *aData, PRBool needToCopyCard);
-  NS_IMETHOD AddMailList(nsIAbDirectory *aMailList);
+  NS_IMETHOD AddMailList(nsIAbDirectory *aMailList, nsIAbDirectory **addedList);
   NS_IMETHOD EditMailListToDatabase(nsIAbCard *listCard);
   
   // nsAbDirProperty method
   NS_IMETHOD Init(const char *aUri);
   // nsIAbDirectoryQuery methods
   NS_DECL_NSIABDIRECTORYQUERY
   // nsIAbDirectorySearch methods
   NS_DECL_NSIABDIRECTORYSEARCH