Bug 1482040 - Add UID property to address book interfaces; r=mkmelin
authorGeoff Lankow <geoff@darktrojan.net>
Thu, 27 Sep 2018 12:00:15 +1200
changeset 24825 2bab51e3b5c282ca391161b0bd6ef50d9dcfd735
parent 24824 60dfc2e2e18c5de53cd2ba530d817707a70c5ba6
child 24826 60d7adb0d3bd2503f9de5ffe6627c713d27b54e6
push id14935
push usergeoff@darktrojan.net
push dateThu, 27 Sep 2018 00:02:24 +0000
treeherdercomm-central@4dbfa038c936 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmkmelin
bugs1482040
Bug 1482040 - Add UID property to address book interfaces; r=mkmelin
mailnews/addrbook/public/nsIAbCard.idl
mailnews/addrbook/public/nsIAbDirectory.idl
mailnews/addrbook/public/nsIAddrDatabase.idl
mailnews/addrbook/src/nsAbCardProperty.cpp
mailnews/addrbook/src/nsAbDirProperty.cpp
mailnews/addrbook/src/nsAbDirProperty.h
mailnews/addrbook/src/nsAddrDatabase.cpp
mailnews/addrbook/src/nsAddrDatabase.h
mailnews/addrbook/test/unit/data/existing.mab
mailnews/addrbook/test/unit/test_uid.js
mailnews/addrbook/test/unit/xpcshell.ini
--- a/mailnews/addrbook/public/nsIAbCard.idl
+++ b/mailnews/addrbook/public/nsIAbCard.idl
@@ -105,16 +105,22 @@ interface nsIAbCard : nsIAbItem {
    * card.
    *
    * Similar to directoryId, consumers of cards outside of directory
    * implementations SHOULD NOT, in general, modify this property.
    */
   attribute AUTF8String localId;
 
   /**
+   * A 128-bit unique identifier for this card.
+   */
+  readonly attribute AUTF8String UID;
+  [noscript] void setUID(in AUTF8String aUID);
+
+  /**
    * A list of all the properties that this card has as an enumerator, whose
    * members are all nsIProperty objects.
    */
   readonly attribute nsISimpleEnumerator properties;
 
   /**
    * Returns a property for the given name.
    *
@@ -282,16 +288,17 @@ interface nsIAbCard : nsIAbItem {
    * will contain the URI of the associated
    * mail list
    */
   attribute string mailListURI;
 };
 
 %{C++
 // A nice list of properties for the benefit of C++ clients
+#define kUIDProperty                "UID"
 #define kFirstNameProperty          "FirstName"
 #define kLastNameProperty           "LastName"
 #define kDisplayNameProperty        "DisplayName"
 #define kNicknameProperty           "NickName"
 #define kPriEmailProperty           "PrimaryEmail"
 #define kPreferMailFormatProperty   "PreferMailFormat"
 #define kLastModifiedDateProperty   "LastModifiedDate"
 #define kPopularityIndexProperty    "PopularityIndex"
--- a/mailnews/addrbook/public/nsIAbDirectory.idl
+++ b/mailnews/addrbook/public/nsIAbDirectory.idl
@@ -65,16 +65,22 @@ interface nsIAbDirectory : nsIAbCollecti
   // XXX This should really be replaced by a QI or something better
   readonly attribute long dirType;
 
   // eliminated a bit more.
 
   // The filename for address books within this directory.
   readonly attribute ACString fileName;
 
+  /**
+   * A 128-bit unique identifier for this directory.
+   */
+  readonly attribute AUTF8String UID;
+  [noscript] void setUID(in AUTF8String aUID);
+
   // The URI of the address book
   readonly attribute ACString URI;
 
   // The position of the directory on the display.
   readonly attribute long position;
 
   // will be used for LDAP replication
   attribute unsigned long lastModifiedDate;
--- a/mailnews/addrbook/public/nsIAddrDatabase.idl
+++ b/mailnews/addrbook/public/nsIAddrDatabase.idl
@@ -203,16 +203,17 @@ interface nsIAddrDatabase : nsIAddrDBAnn
   boolean findMailListbyUnicodeName(in wstring listName);
 
   void getCardCount(out uint32_t count);
 
   [noscript] readonly attribute nsIMdbRow newRow;
   [noscript] readonly attribute nsIMdbRow newListRow;
   [noscript] void addCardRowToDB(in nsIMdbRow newRow);
   [noscript] void addLdifListMember(in nsIMdbRow row, in string value);
+  [noscript] void addUID(in nsIMdbRow row, in string value);
   [noscript] void addFirstName(in nsIMdbRow row, in string value);
   [noscript] void addLastName(in nsIMdbRow row, in string value);
   [noscript] void addPhoneticFirstName(in nsIMdbRow row, in string value);
   [noscript] void addPhoneticLastName(in nsIMdbRow row, in string value);
   [noscript] void addDisplayName(in nsIMdbRow row, in string value);
   [noscript] void addNickName(in nsIMdbRow row, in string value);
   [noscript] void addPrimaryEmail(in nsIMdbRow row, in string value);
   [noscript] void add2ndEmail(in nsIMdbRow row, in string value);
--- a/mailnews/addrbook/src/nsAbCardProperty.cpp
+++ b/mailnews/addrbook/src/nsAbCardProperty.cpp
@@ -15,16 +15,17 @@
 #include "nsComponentManagerUtils.h"
 #include "nsServiceManagerUtils.h"
 #include "nsMemory.h"
 #include "nsVCardObj.h"
 #include "nsIMutableArray.h"
 #include "nsArrayUtils.h"
 #include "mozITXTToHTMLConv.h"
 #include "nsIAbManager.h"
+#include "nsIUUIDGenerator.h"
 
 #include "nsVariant.h"
 #include "nsIProperty.h"
 #include "nsCOMArray.h"
 #include "nsArrayEnumerator.h"
 #include "prmem.h"
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/Services.h"
@@ -338,16 +339,44 @@ NS_IMETHODIMP nsAbCardProperty::SetPrope
 }
 
 NS_IMETHODIMP nsAbCardProperty::DeleteProperty(const nsACString &name)
 {
   m_properties.Remove(name);
   return NS_OK;
 }
 
+NS_IMETHODIMP nsAbCardProperty::GetUID(nsACString &uid)
+{
+  nsAutoString aString;
+  nsresult rv = GetPropertyAsAString(kUIDProperty, aString);
+  if (NS_SUCCEEDED(rv)) {
+    uid = NS_ConvertUTF16toUTF8(aString);
+    return rv;
+  }
+
+  nsCOMPtr<nsIUUIDGenerator> uuidgen = mozilla::services::GetUUIDGenerator();
+  NS_ENSURE_TRUE(uuidgen, NS_ERROR_FAILURE);
+
+  nsID id;
+  rv = uuidgen->GenerateUUIDInPlace(&id);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  char idString[NSID_LENGTH];
+  id.ToProvidedString(idString);
+
+  uid.AppendASCII(idString + 1, NSID_LENGTH - 3);
+  return SetUID(uid);
+}
+
+NS_IMETHODIMP nsAbCardProperty::SetUID(const nsACString &aUID)
+{
+  return SetPropertyAsAString(kUIDProperty, NS_ConvertUTF8toUTF16(aUID));
+}
+
 NS_IMETHODIMP nsAbCardProperty::GetFirstName(nsAString &aString)
 {
   nsresult rv = GetPropertyAsAString(kFirstNameProperty, aString);
   if (rv == NS_ERROR_NOT_AVAILABLE)
   {
     aString.Truncate();
     return NS_OK;
   }
--- a/mailnews/addrbook/src/nsAbDirProperty.cpp
+++ b/mailnews/addrbook/src/nsAbDirProperty.cpp
@@ -9,26 +9,30 @@
 #include "nsDirPrefs.h"
 #include "nsIPrefService.h"
 #include "nsIPrefLocalizedString.h"
 #include "nsServiceManagerUtils.h"
 #include "nsComponentManagerUtils.h"
 #include "prmem.h"
 #include "nsIAbManager.h"
 #include "nsArrayUtils.h"
+#include "nsIUUIDGenerator.h"
+#include "mozilla/Services.h"
+using namespace mozilla;
 
 // From nsDirPrefs
 #define kDefaultPosition 1
 
 nsAbDirProperty::nsAbDirProperty(void)
   : m_LastModifiedDate(0),
     mIsValidURI(false),
     mIsQueryURI(false)
 {
   m_IsMailList = false;
+  mUID = EmptyCString();
 }
 
 nsAbDirProperty::~nsAbDirProperty(void)
 {
 #if 0
   // this code causes a regression #138647
   // don't turn it on until you figure it out
   if (m_AddressList) {
@@ -142,16 +146,53 @@ NS_IMETHODIMP nsAbDirProperty::GetDirTyp
   return GetIntValue("dirType", LDAPDirectory, aDirType);
 }
 
 NS_IMETHODIMP nsAbDirProperty::GetFileName(nsACString &aFileName)
 {
   return GetStringValue("filename", EmptyCString(), aFileName);
 }
 
+NS_IMETHODIMP nsAbDirProperty::GetUID(nsACString &aUID)
+{
+  nsresult rv = NS_OK;
+  if (!mUID.IsEmpty()) {
+    aUID = mUID;
+    return rv;
+  }
+  if (!m_IsMailList) {
+    rv = GetStringValue("uid", EmptyCString(), aUID);
+    if (!aUID.IsEmpty()) {
+      return rv;
+    }
+  }
+
+  nsCOMPtr<nsIUUIDGenerator> uuidgen = mozilla::services::GetUUIDGenerator();
+  NS_ENSURE_TRUE(uuidgen, NS_ERROR_FAILURE);
+
+  nsID id;
+  rv = uuidgen->GenerateUUIDInPlace(&id);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  char idString[NSID_LENGTH];
+  id.ToProvidedString(idString);
+
+  aUID.AppendASCII(idString + 1, NSID_LENGTH - 3);
+  return SetUID(aUID);
+}
+
+NS_IMETHODIMP nsAbDirProperty::SetUID(const nsACString &aUID)
+{
+  mUID = aUID;
+  if (m_IsMailList) {
+    return NS_OK;
+  }
+  return SetStringValue("uid", aUID);
+}
+
 NS_IMETHODIMP nsAbDirProperty::GetURI(nsACString &aURI)
 {
   // XXX Should we complete this for Mailing Lists?
   return NS_ERROR_NOT_IMPLEMENTED;
 }
 
 NS_IMETHODIMP nsAbDirProperty::GetPosition(int32_t *aPosition)
 {
--- a/mailnews/addrbook/src/nsAbDirProperty.h
+++ b/mailnews/addrbook/src/nsAbDirProperty.h
@@ -47,16 +47,17 @@ protected:
 
   nsString m_ListDirName;
   nsString m_ListName;
   nsString m_ListNickName;
   nsString m_Description;
   bool     m_IsMailList;
 
   nsCString mURI;
+  nsCString mUID;
   nsCString mQueryString;
   nsCString mURINoQuery;
   bool mIsValidURI;
   bool mIsQueryURI;
 
 
   /*
    * Note that any derived implementations should ensure that this item
--- a/mailnews/addrbook/src/nsAddrDatabase.cpp
+++ b/mailnews/addrbook/src/nsAddrDatabase.cpp
@@ -68,16 +68,17 @@ nsAddrDatabase::nsAddrDatabase()
     : m_mdbEnv(nullptr), m_mdbStore(nullptr),
       m_mdbPabTable(nullptr),
       m_mdbTokensInitialized(false),
       m_mdbDeletedCardsTableRemoved(false),
       m_PabTableKind(0),
       m_MailListTableKind(0),
       m_DeletedCardsTableKind(0),
       m_CardRowScopeToken(0),
+      m_UIDColumnToken(0),
       m_FirstNameColumnToken(0),
       m_LastNameColumnToken(0),
       m_PhoneticFirstNameColumnToken(0),
       m_PhoneticLastNameColumnToken(0),
       m_DisplayNameColumnToken(0),
       m_NickNameColumnToken(0),
       m_PriEmailColumnToken(0),
       m_2ndEmailColumnToken(0),
@@ -936,16 +937,17 @@ nsresult nsAddrDatabase::InitMDBInfo()
     m_mdbTokensInitialized = true;
     err = m_mdbStore->StringToToken(m_mdbEnv, kCardRowScope, &m_CardRowScopeToken);
     err = m_mdbStore->StringToToken(m_mdbEnv, kListRowScope, &m_ListRowScopeToken);
     err = m_mdbStore->StringToToken(m_mdbEnv, kDataRowScope, &m_DataRowScopeToken);
     gAddressBookTableOID.mOid_Scope = m_CardRowScopeToken;
     gAddressBookTableOID.mOid_Id = ID_PAB_TABLE;
     if (NS_SUCCEEDED(err))
     {
+      m_mdbStore->StringToToken(m_mdbEnv,  kUIDProperty, &m_UIDColumnToken);
       m_mdbStore->StringToToken(m_mdbEnv,  kFirstNameProperty, &m_FirstNameColumnToken);
       m_mdbStore->StringToToken(m_mdbEnv,  kLastNameProperty, &m_LastNameColumnToken);
       m_mdbStore->StringToToken(m_mdbEnv,  kPhoneticFirstNameProperty, &m_PhoneticFirstNameColumnToken);
       m_mdbStore->StringToToken(m_mdbEnv,  kPhoneticLastNameProperty, &m_PhoneticLastNameColumnToken);
       m_mdbStore->StringToToken(m_mdbEnv,  kDisplayNameProperty, &m_DisplayNameColumnToken);
       m_mdbStore->StringToToken(m_mdbEnv,  kNicknameProperty, &m_NickNameColumnToken);
       m_mdbStore->StringToToken(m_mdbEnv,  kPriEmailProperty, &m_PriEmailColumnToken);
       m_mdbStore->StringToToken(m_mdbEnv,  kLowerPriEmailColumn, &m_LowerPriEmailColumnToken);
@@ -1086,16 +1088,20 @@ nsresult nsAddrDatabase::AddAttributeCol
 
 NS_IMETHODIMP nsAddrDatabase::CreateNewCardAndAddToDB(nsIAbCard *aNewCard, bool aNotify /* = FALSE */, nsIAbDirectory *aParent)
 {
   nsCOMPtr <nsIMdbRow> cardRow;
 
   if (!aNewCard || !m_mdbPabTable || !m_mdbEnv || !m_mdbStore)
     return NS_ERROR_NULL_POINTER;
 
+  // Ensure the new card has a UID.
+  nsAutoCString uid;
+  aNewCard->GetUID(uid);
+
   // Per the UUID requirements, we want to try to reuse the local id if at all
   // possible. nsACString::ToInteger probably won't fail if the local id looks
   // like "23bozo" (returning 23 instead), but it's okay since we aren't going
   // to overwrite anything with 23 if it already exists and the id for the row
   // doesn't matter otherwise.
   nsresult rv;
 
   nsAutoCString id;
@@ -1326,16 +1332,20 @@ nsresult nsAddrDatabase::AddListAttribut
 
   nsCOMPtr<nsIAbMDBDirectory> dblist(do_QueryInterface(list,&err));
   if (NS_SUCCEEDED(err))
     dblist->SetDbRowID(rowOid.mOid_Id);
 
   // add the row to the singleton table.
   if (NS_SUCCEEDED(err) && listRow)
   {
+    nsAutoCString acStr;
+    list->GetUID(acStr);
+    AddUID(listRow, acStr.get());
+
     nsString unicodeStr;
 
     list->GetDirName(unicodeStr);
     if (!unicodeStr.IsEmpty())
         AddUnicodeToColumn(listRow, m_ListNameColumnToken, m_LowerListNameColumnToken, unicodeStr.get());
 
     list->GetListNickName(unicodeStr);
     AddListNickName(listRow, NS_ConvertUTF16toUTF8(unicodeStr).get());
@@ -2204,27 +2214,35 @@ NS_IMETHODIMP nsAddrDatabase::InitCardFr
     }
   } while (true);
 
   uint32_t key = 0;
   rv = GetIntColumn(cardRow, m_RecordKeyColumnToken, &key, 0);
   if (NS_SUCCEEDED(rv))
     newCard->SetPropertyAsUint32(kRecordKeyColumn, key);
 
-  return NS_OK;
+  nsAutoCString uid;
+  newCard->GetUID(uid);
+  rv = SetCardValue(newCard, kUIDProperty, NS_ConvertUTF8toUTF16(uid).get(), false);
+  return Commit(nsAddrDBCommitType::kSessionCommit);
 }
 
 nsresult nsAddrDatabase::GetListCardFromDB(nsIAbCard *listCard, nsIMdbRow* listRow)
 {
   nsresult    err = NS_OK;
   if (!listCard || !listRow)
-      return NS_ERROR_NULL_POINTER;
+    return NS_ERROR_NULL_POINTER;
 
   nsAutoString tempString;
 
+  err = GetStringColumn(listRow, m_UIDColumnToken, tempString);
+  if (NS_SUCCEEDED(err) && !tempString.IsEmpty())
+  {
+    listCard->SetPropertyAsAString(kUIDProperty, tempString);
+  }
   err = GetStringColumn(listRow, m_ListNameColumnToken, tempString);
   if (NS_SUCCEEDED(err) && !tempString.IsEmpty())
   {
     listCard->SetDisplayName(tempString);
     listCard->SetLastName(tempString);
   }
   err = GetStringColumn(listRow, m_ListNickNameColumnToken, tempString);
   if (NS_SUCCEEDED(err) && !tempString.IsEmpty())
@@ -2246,16 +2264,21 @@ nsresult nsAddrDatabase::GetListCardFrom
 nsresult nsAddrDatabase::GetListFromDB(nsIAbDirectory *newList, nsIMdbRow* listRow)
 {
   nsresult    err = NS_OK;
   if (!newList || !listRow || !m_mdbStore || !m_mdbEnv)
     return NS_ERROR_NULL_POINTER;
 
   nsAutoString tempString;
 
+  err = GetStringColumn(listRow, m_UIDColumnToken, tempString);
+  if (NS_SUCCEEDED(err) && !tempString.IsEmpty())
+  {
+    newList->SetUID(NS_ConvertUTF16toUTF8(tempString));
+  }
   err = GetStringColumn(listRow, m_ListNameColumnToken, tempString);
   if (NS_SUCCEEDED(err) && !tempString.IsEmpty())
   {
     newList->SetDirName(tempString);
   }
   err = GetStringColumn(listRow, m_ListNickNameColumnToken, tempString);
   if (NS_SUCCEEDED(err) && !tempString.IsEmpty())
   {
--- a/mailnews/addrbook/src/nsAddrDatabase.h
+++ b/mailnews/addrbook/src/nsAddrDatabase.h
@@ -62,16 +62,19 @@ public:
                                    const nsACString & uUTF8Value,
                                    bool aCaseInsensitive,
                                    nsISimpleEnumerator **cards) override;
   NS_IMETHOD GetNewRow(nsIMdbRow * *newRow) override;
   NS_IMETHOD GetNewListRow(nsIMdbRow * *newRow) override;
   NS_IMETHOD AddCardRowToDB(nsIMdbRow *newRow) override;
   NS_IMETHOD AddLdifListMember(nsIMdbRow* row, const char * value) override;
 
+  NS_IMETHOD AddUID(nsIMdbRow * row, const char * value) override
+  { return AddCharStringColumn(row, m_UIDColumnToken, value); }
+
   NS_IMETHOD AddFirstName(nsIMdbRow * row, const char * value) override
   { return AddCharStringColumn(row, m_FirstNameColumnToken, value); }
 
   NS_IMETHOD AddLastName(nsIMdbRow * row, const char * value) override
   { return AddCharStringColumn(row, m_LastNameColumnToken, value); }
 
   NS_IMETHOD AddPhoneticFirstName(nsIMdbRow * row, const char * value) override
   { return AddCharStringColumn(row, m_PhoneticFirstNameColumnToken, value); }
@@ -338,16 +341,17 @@ protected:
   mdb_kind      m_PabTableKind;
   mdb_kind      m_MailListTableKind;
   mdb_kind      m_DeletedCardsTableKind;
 
   mdb_scope      m_CardRowScopeToken;
   mdb_scope      m_ListRowScopeToken;
   mdb_scope      m_DataRowScopeToken;
 
+  mdb_token      m_UIDColumnToken;
   mdb_token      m_FirstNameColumnToken;
   mdb_token      m_LastNameColumnToken;
   mdb_token      m_PhoneticFirstNameColumnToken;
   mdb_token      m_PhoneticLastNameColumnToken;
   mdb_token      m_DisplayNameColumnToken;
   mdb_token      m_NickNameColumnToken;
   mdb_token      m_PriEmailColumnToken;
   mdb_token      m_2ndEmailColumnToken;
new file mode 100644
--- /dev/null
+++ b/mailnews/addrbook/test/unit/data/existing.mab
@@ -0,0 +1,37 @@
+// <!-- <mdb:mork:z v="1.4"/> -->
+< <(a=c)> // (f=iso-8859-1)
+  (B8=LastModifiedDate)(B9=RecordKey)(BA=AddrCharSet)(BB=LastRecordKey)
+  (BC=ns:addrbk:db:table:kind:pab)(BD=ListName)(BE=ListNickName)
+  (BF=ListDescription)(C0=ListTotalAddresses)(C1=LowercaseListName)
+  (C2=ns:addrbk:db:table:kind:deleted)(C3=_Yahoo)(C4=_MSN)(C5=DbRowID)
+  (C6=PhotoType)(C7=_ICQ)(C8=_IRC)(C9=_JabberId)(CA=_Skype)
+  (CB=PreferDisplayName)(CC=PhotoName)(CD=_QQ)(CE=PhotoURI)
+  (CF=_GoogleTalk)(80=ns:addrbk:db:row:scope:card:all)
+  (81=ns:addrbk:db:row:scope:list:all)
+  (82=ns:addrbk:db:row:scope:data:all)(83=FirstName)(84=LastName)
+  (85=PhoneticFirstName)(86=PhoneticLastName)(87=DisplayName)
+  (88=NickName)(89=PrimaryEmail)(8A=LowercasePrimaryEmail)
+  (8B=SecondEmail)(8C=LowercaseSecondEmail)(8D=PreferMailFormat)
+  (8E=PopularityIndex)(8F=WorkPhone)(90=HomePhone)(91=FaxNumber)
+  (92=PagerNumber)(93=CellularNumber)(94=WorkPhoneType)(95=HomePhoneType)
+  (96=FaxNumberType)(97=PagerNumberType)(98=CellularNumberType)
+  (99=HomeAddress)(9A=HomeAddress2)(9B=HomeCity)(9C=HomeState)
+  (9D=HomeZipCode)(9E=HomeCountry)(9F=WorkAddress)(A0=WorkAddress2)
+  (A1=WorkCity)(A2=WorkState)(A3=WorkZipCode)(A4=WorkCountry)
+  (A5=JobTitle)(A6=Department)(A7=Company)(A8=_AimScreenName)
+  (A9=AnniversaryYear)(AA=AnniversaryMonth)(AB=AnniversaryDay)
+  (AC=SpouseName)(AD=FamilyName)(AE=WebPage1)(AF=WebPage2)(B0=BirthYear)
+  (B1=BirthMonth)(B2=BirthDay)(B3=Custom1)(B4=Custom2)(B5=Custom3)
+  (B6=Custom4)(B7=Notes)>
+
+<(8A=2)(81=First Last)(82=)(83=1)(80=0)(84=generic)(85=First)(86=Last)
+  (87=first@last)(88=List)(89=list)>
+{1:^80 {(k^BC:c)(s=9)} 
+  [1:^82(^BB=2)]
+  [1(^87^81)(^86=)(^85=)(^C3=)(^B7=)(^A1=)(^AE=)(^C4=)(^C5=1)(^8D=0)
+    (^B2=)(^A7=)(^9A=)(^B3=)(^A6=)(^A4=)(^92=)(^A5=)(^A3=)(^9B=)(^C6^84)
+    (^9E=)(^B0=)(^8E=0)(^C7=)(^93=)(^C8=)(^A0=)(^C9=)(^CA=)(^99=)(^83^85)
+    (^A8=)(^CB=1)(^84^86)(^9C=)(^B6=)(^B8=0)(^89^87)(^AF=)(^CC=)(^90=)
+    (^B1=)(^CD=)(^8F=)(^A2=)(^91=)(^88=)(^9F=)(^B4=)(^9D=)(^8B=)(^CE=)
+    (^CF=)(^B5=)(^8A^87)(^B9=1)]
+  [1:^81(^BD^88)(^C1^89)(^BE=)(^BF=)(^C0=0)(^B9=2)]}
new file mode 100644
--- /dev/null
+++ b/mailnews/addrbook/test/unit/test_uid.js
@@ -0,0 +1,83 @@
+/*
+ * Test to check that pre-existing cards are given a UID,
+ * and that the UID remains the same after a shutdown.
+ */
+Cu.importGlobalProperties(["fetch"]);
+
+var profD = do_get_profile();
+
+// Installs an address book with some existing objects.
+function run_test() {
+  let testAB = do_get_file("data/existing.mab");
+  testAB.copyTo(profD, kPABData.fileName);
+
+  run_next_test();
+}
+
+// Tests that directories have UIDs.
+add_test(function directoryUID() {
+  for (let book of MailServices.ab.directories) {
+    equal(36, book.UID.length, "Existing directory has a UID");
+  }
+
+  let dirName = MailServices.ab.newAddressBook("test", "", kPABData.dirType);
+  let directory = MailServices.ab.getDirectoryFromId(dirName);
+  equal(36, directory.UID.length, "New directory has a UID");
+
+  run_next_test();
+});
+
+// Tests that an existing contact has a UID generated, and that that UID is
+// saved to the database so that the same UID is used next time.
+add_task(async function existingContactUID() {
+  let book = MailServices.ab.getDirectory(kPABData.URI);
+  let bookCards = [...book.childCards];
+  equal(2, bookCards.length, "Loaded test address book");
+
+  let card = bookCards[0];
+  if (card.isMailList) {
+    card = bookCards[1];
+  }
+  equal(36, card.UID.length, "Existing contact has a UID");
+
+  let abFile = profD.clone();
+  abFile.append(kPABData.fileName);
+  let response = await fetch(Services.io.newFileURI(abFile).spec);
+  let text = await response.text();
+
+  ok(text.includes(card.UID), "UID has been saved to file");
+});
+
+// Tests that new contacts have UIDs. Do this test last so we don't muck up
+// the others by adding new things to the address book.
+add_test(function newContactUID() {
+  let book = MailServices.ab.getDirectory(kPABData.URI);
+  let contact = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(Ci.nsIAbCard);
+  let newContact = book.addCard(contact);
+  equal(36, newContact.UID.length, "New contact has a UID");
+
+  run_next_test();
+});
+
+// Tests that new lists have UIDs.
+add_test(function listUID() {
+  let book = MailServices.ab.getDirectory(kPABData.URI);
+  let lists = book.addressLists;
+  equal(1, lists.length);
+
+  let directory = lists.GetElementAt(0);
+  directory.QueryInterface(Ci.nsIAbDirectory);
+  equal(36, directory.UID.length, "Existing list's directory has a UID");
+
+  let list = Cc["@mozilla.org/addressbook/directoryproperty;1"].createInstance();
+  list.QueryInterface(Ci.nsIAbDirectory);
+  list.isMailList = true;
+  book.addMailList(list);
+  equal(2, lists.length);
+
+  let newDirectory = lists.GetElementAt(1);
+  newDirectory.QueryInterface(Ci.nsIAbDirectory);
+  equal(36, newDirectory.UID.length, "New list's directory has a UID");
+
+  run_next_test();
+});
--- a/mailnews/addrbook/test/unit/xpcshell.ini
+++ b/mailnews/addrbook/test/unit/xpcshell.ini
@@ -24,9 +24,10 @@ support-files = data/*
 [test_nsAbAutoCompleteSearch4.js]
 [test_nsAbAutoCompleteSearch5.js]
 [test_nsAbAutoCompleteSearch6.js]
 [test_nsAbAutoCompleteSearch7.js]
 [test_nsAbManager1.js]
 [test_nsAbManager2.js]
 [test_nsAbManager3.js]
 [test_nsIAbCard.js]
+[test_uid.js]
 [test_uuid.js]