killbill-uncached

account: add plumbing to add and remove email addresses These

6/13/2012 8:26:14 PM

Changes

Details

diff --git a/account/src/main/java/com/ning/billing/account/api/user/DefaultAccountUserApi.java b/account/src/main/java/com/ning/billing/account/api/user/DefaultAccountUserApi.java
index d7d8049..057aaf6 100644
--- a/account/src/main/java/com/ning/billing/account/api/user/DefaultAccountUserApi.java
+++ b/account/src/main/java/com/ning/billing/account/api/user/DefaultAccountUserApi.java
@@ -145,4 +145,14 @@ public class DefaultAccountUserApi implements AccountUserApi {
     public void saveEmails(final UUID accountId, final List<AccountEmail> newEmails, final CallContext context) {
         accountEmailDao.saveEmails(accountId, newEmails, context);
     }
+
+    @Override
+    public void addEmail(final UUID accountId, final AccountEmail email, final CallContext context) {
+        accountEmailDao.addEmail(accountId, email, context);
+    }
+
+    @Override
+    public void removeEmail(final UUID accountId, final AccountEmail email, final CallContext context) {
+        accountEmailDao.removeEmail(accountId, email, context);
+    }
 }
diff --git a/account/src/main/java/com/ning/billing/account/dao/AccountEmailDao.java b/account/src/main/java/com/ning/billing/account/dao/AccountEmailDao.java
index 2c470d3..09f2cda 100644
--- a/account/src/main/java/com/ning/billing/account/dao/AccountEmailDao.java
+++ b/account/src/main/java/com/ning/billing/account/dao/AccountEmailDao.java
@@ -34,5 +34,9 @@ public interface AccountEmailDao {
      */
     public void saveEmails(UUID accountId, List<AccountEmail> emails, CallContext context);
 
+    public void addEmail(UUID accountId, AccountEmail email, CallContext context);
+
+    public void removeEmail(UUID accountId, AccountEmail email, CallContext context);
+
     public void test();
 }
diff --git a/account/src/main/java/com/ning/billing/account/dao/AccountEmailHistoryBinder.java b/account/src/main/java/com/ning/billing/account/dao/AccountEmailHistoryBinder.java
index 01344b3..ed9ebae 100644
--- a/account/src/main/java/com/ning/billing/account/dao/AccountEmailHistoryBinder.java
+++ b/account/src/main/java/com/ning/billing/account/dao/AccountEmailHistoryBinder.java
@@ -16,33 +16,34 @@
 
 package com.ning.billing.account.dao;
 
-import com.ning.billing.account.api.AccountEmail;
-import com.ning.billing.util.dao.EntityHistory;
-import org.skife.jdbi.v2.SQLStatement;
-import org.skife.jdbi.v2.sqlobject.Binder;
-import org.skife.jdbi.v2.sqlobject.BinderFactory;
-import org.skife.jdbi.v2.sqlobject.BindingAnnotation;
-
 import java.lang.annotation.Annotation;
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
+import org.skife.jdbi.v2.SQLStatement;
+import org.skife.jdbi.v2.sqlobject.Binder;
+import org.skife.jdbi.v2.sqlobject.BinderFactory;
+import org.skife.jdbi.v2.sqlobject.BindingAnnotation;
+
+import com.ning.billing.account.api.AccountEmail;
+import com.ning.billing.util.dao.EntityHistory;
+
 @BindingAnnotation(AccountEmailHistoryBinder.AccountEmailHistoryBinderFactory.class)
 @Retention(RetentionPolicy.RUNTIME)
 @Target({ElementType.PARAMETER})
 public @interface AccountEmailHistoryBinder {
     public static class AccountEmailHistoryBinderFactory implements BinderFactory {
         @Override
-        public Binder<AccountEmailHistoryBinder, EntityHistory<AccountEmail>> build(Annotation annotation) {
+        public Binder<AccountEmailHistoryBinder, EntityHistory<AccountEmail>> build(final Annotation annotation) {
             return new Binder<AccountEmailHistoryBinder, EntityHistory<AccountEmail>>() {
                 @Override
-                public void bind(SQLStatement q, AccountEmailHistoryBinder bind, EntityHistory<AccountEmail> history) {
+                public void bind(SQLStatement q, final AccountEmailHistoryBinder bind, final EntityHistory<AccountEmail> history) {
                     q.bind("recordId", history.getValue());
                     q.bind("changeType", history.getChangeType().toString());
 
-                    AccountEmail accountEmail = history.getEntity();
+                    final AccountEmail accountEmail = history.getEntity();
                     q.bind("id", accountEmail.getId().toString());
                     q.bind("accountId", accountEmail.getAccountId().toString());
                     q.bind("email", accountEmail.getEmail());
diff --git a/account/src/main/java/com/ning/billing/account/dao/AuditedAccountEmailDao.java b/account/src/main/java/com/ning/billing/account/dao/AuditedAccountEmailDao.java
index d8fb806..f5f50ed 100644
--- a/account/src/main/java/com/ning/billing/account/dao/AuditedAccountEmailDao.java
+++ b/account/src/main/java/com/ning/billing/account/dao/AuditedAccountEmailDao.java
@@ -17,12 +17,17 @@
 package com.ning.billing.account.dao;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.UUID;
 
 import org.skife.jdbi.v2.IDBI;
+import org.skife.jdbi.v2.Transaction;
+import org.skife.jdbi.v2.TransactionStatus;
 import org.skife.jdbi.v2.sqlobject.mixins.Transmogrifier;
 
+import com.google.common.collect.ImmutableList;
 import com.google.inject.Inject;
 import com.ning.billing.account.api.AccountEmail;
 import com.ning.billing.util.callcontext.CallContext;
@@ -55,6 +60,50 @@ public class AuditedAccountEmailDao extends AuditedCollectionDaoBase<AccountEmai
     }
 
     @Override
+    public void addEmail(final UUID accountId, final AccountEmail email, final CallContext context) {
+        accountEmailSqlDao.inTransaction(new Transaction<Object, AccountEmailSqlDao>() {
+            @Override
+            public Object inTransaction(final AccountEmailSqlDao transactional, final TransactionStatus status) throws Exception {
+                // Compute the final list of emails by looking up the current ones and adding the new one
+                // We can use a simple set here as the supplied email may not have its id field populated
+                final List<AccountEmail> currentEmails = accountEmailSqlDao.load(accountId.toString(), ObjectType.ACCOUNT_EMAIL);
+                final Map<String, AccountEmail> newEmails = new HashMap<String, AccountEmail>();
+                for (final AccountEmail currentEmail : currentEmails) {
+                    newEmails.put(currentEmail.getEmail(), currentEmail);
+                }
+                newEmails.put(email.getEmail(), email);
+
+                saveEntitiesFromTransaction(getSqlDao(), accountId, ObjectType.ACCOUNT_EMAIL,
+                                            ImmutableList.<AccountEmail>copyOf(newEmails.values()), context);
+
+                return null;
+            }
+        });
+    }
+
+    @Override
+    public void removeEmail(final UUID accountId, final AccountEmail email, final CallContext context) {
+        accountEmailSqlDao.inTransaction(new Transaction<Object, AccountEmailSqlDao>() {
+            @Override
+            public Object inTransaction(final AccountEmailSqlDao transactional, final TransactionStatus status) throws Exception {
+                // Compute the final list of emails by looking up the current ones and removing the new one
+                // We can use a simple set here as the supplied email may not have its id field populated
+                final List<AccountEmail> currentEmails = accountEmailSqlDao.load(accountId.toString(), ObjectType.ACCOUNT_EMAIL);
+                final Map<String, AccountEmail> newEmails = new HashMap<String, AccountEmail>();
+                for (final AccountEmail currentEmail : currentEmails) {
+                    newEmails.put(currentEmail.getEmail(), currentEmail);
+                }
+                newEmails.remove(email.getEmail());
+
+                saveEntitiesFromTransaction(getSqlDao(), accountId, ObjectType.ACCOUNT_EMAIL,
+                                            ImmutableList.<AccountEmail>copyOf(newEmails.values()), context);
+
+                return null;
+            }
+        });
+    }
+
+    @Override
     public String getKey(final AccountEmail entity) {
         return entity.getEmail();
     }
@@ -70,7 +119,7 @@ public class AuditedAccountEmailDao extends AuditedCollectionDaoBase<AccountEmai
     }
 
     @Override
-    protected UpdatableEntityCollectionSqlDao<AccountEmail> transmogrifyDao(Transmogrifier transactionalDao) {
+    protected UpdatableEntityCollectionSqlDao<AccountEmail> transmogrifyDao(final Transmogrifier transactionalDao) {
         return transactionalDao.become(AccountEmailSqlDao.class);
     }
 
diff --git a/account/src/test/java/com/ning/billing/account/api/MockAccountUserApi.java b/account/src/test/java/com/ning/billing/account/api/MockAccountUserApi.java
index 7aeac80..21a78fe 100644
--- a/account/src/test/java/com/ning/billing/account/api/MockAccountUserApi.java
+++ b/account/src/test/java/com/ning/billing/account/api/MockAccountUserApi.java
@@ -109,6 +109,16 @@ public class MockAccountUserApi implements AccountUserApi {
     }
 
     @Override
+    public void addEmail(final UUID accountId, final AccountEmail email, final CallContext context) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void removeEmail(final UUID accountId, final AccountEmail email, final CallContext context) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
     public void updateAccount(final Account account, final CallContext context) {
         throw new UnsupportedOperationException();
     }
diff --git a/account/src/test/java/com/ning/billing/account/api/user/TestDefaultAccountUserApi.java b/account/src/test/java/com/ning/billing/account/api/user/TestDefaultAccountUserApi.java
index e3e2f84..27134fa 100644
--- a/account/src/test/java/com/ning/billing/account/api/user/TestDefaultAccountUserApi.java
+++ b/account/src/test/java/com/ning/billing/account/api/user/TestDefaultAccountUserApi.java
@@ -27,9 +27,11 @@ import org.testng.annotations.Test;
 import com.ning.billing.account.api.Account;
 import com.ning.billing.account.api.AccountData;
 import com.ning.billing.account.api.DefaultAccount;
+import com.ning.billing.account.api.DefaultAccountEmail;
 import com.ning.billing.account.dao.AccountDao;
 import com.ning.billing.account.dao.AccountEmailDao;
 import com.ning.billing.account.dao.MockAccountDao;
+import com.ning.billing.account.dao.MockAccountEmailDao;
 import com.ning.billing.catalog.api.Currency;
 import com.ning.billing.util.bus.Bus;
 import com.ning.billing.util.callcontext.CallContext;
@@ -38,14 +40,15 @@ import com.ning.billing.util.callcontext.CallContextFactory;
 public class TestDefaultAccountUserApi {
     private final CallContextFactory factory = Mockito.mock(CallContextFactory.class);
     private final CallContext callContext = Mockito.mock(CallContext.class);
-    private final AccountEmailDao accountEmailDao = Mockito.mock(AccountEmailDao.class);
 
     private AccountDao accountDao;
+    private AccountEmailDao accountEmailDao;
     private DefaultAccountUserApi accountUserApi;
 
     @BeforeMethod(groups = "fast")
     public void setUp() throws Exception {
         accountDao = new MockAccountDao(Mockito.mock(Bus.class));
+        accountEmailDao = new MockAccountEmailDao();
         accountUserApi = new DefaultAccountUserApi(factory, accountDao, accountEmailDao);
     }
 
@@ -98,4 +101,26 @@ public class TestDefaultAccountUserApi {
         Assert.assertEquals(account.isMigrated(), isMigrated);
         Assert.assertEquals(account.isNotifiedForInvoices(), isNotifiedForInvoices);
     }
+
+    @Test(groups = "fast")
+    public void testAddEmail() throws Exception {
+        final UUID accountId = UUID.randomUUID();
+
+        // Verify the initial state
+        Assert.assertEquals(accountEmailDao.getEmails(accountId).size(), 0);
+
+        // Add the first email
+        final String email1 = UUID.randomUUID().toString();
+        accountUserApi.addEmail(accountId, new DefaultAccountEmail(accountId, email1), callContext);
+        Assert.assertEquals(accountEmailDao.getEmails(accountId).size(), 1);
+
+        // Add a second one
+        final String email2 = UUID.randomUUID().toString();
+        accountUserApi.addEmail(accountId, new DefaultAccountEmail(accountId, email2), callContext);
+        Assert.assertEquals(accountEmailDao.getEmails(accountId).size(), 2);
+
+        // Remove the first second one
+        accountUserApi.removeEmail(accountId, new DefaultAccountEmail(accountId, email1), callContext);
+        Assert.assertEquals(accountEmailDao.getEmails(accountId).size(), 1);
+    }
 }
diff --git a/account/src/test/java/com/ning/billing/account/dao/MockAccountEmailDao.java b/account/src/test/java/com/ning/billing/account/dao/MockAccountEmailDao.java
new file mode 100644
index 0000000..933ca54
--- /dev/null
+++ b/account/src/test/java/com/ning/billing/account/dao/MockAccountEmailDao.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.account.dao;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+import com.google.common.collect.ImmutableList;
+import com.ning.billing.account.api.AccountEmail;
+import com.ning.billing.util.callcontext.CallContext;
+
+public class MockAccountEmailDao implements AccountEmailDao {
+    private final Map<UUID, Set<AccountEmail>> emails = new ConcurrentHashMap<UUID, Set<AccountEmail>>();
+
+    @Override
+    public List<AccountEmail> getEmails(final UUID accountId) {
+        final Set<AccountEmail> accountEmails = emails.get(accountId);
+        if (accountEmails == null) {
+            return ImmutableList.<AccountEmail>of();
+        } else {
+            return ImmutableList.<AccountEmail>copyOf(accountEmails.iterator());
+        }
+    }
+
+    @Override
+    public void saveEmails(final UUID accountId, final List<AccountEmail> newEmails, final CallContext context) {
+        if (emails.get(accountId) == null) {
+            emails.put(accountId, new HashSet<AccountEmail>());
+        }
+
+        emails.get(accountId).addAll(newEmails);
+    }
+
+    @Override
+    public void addEmail(final UUID accountId, final AccountEmail email, final CallContext context) {
+        if (emails.get(accountId) == null) {
+            emails.put(accountId, new HashSet<AccountEmail>());
+        }
+
+        emails.get(accountId).add(email);
+    }
+
+    @Override
+    public void removeEmail(final UUID accountId, final AccountEmail email, final CallContext context) {
+        if (emails.get(accountId) == null) {
+            emails.put(accountId, new HashSet<AccountEmail>());
+        }
+
+        emails.get(accountId).remove(email);
+    }
+
+    @Override
+    public void test() {
+    }
+}
diff --git a/account/src/test/java/com/ning/billing/account/dao/TestAccountDao.java b/account/src/test/java/com/ning/billing/account/dao/TestAccountDao.java
index 5e51ecd..4c9205c 100644
--- a/account/src/test/java/com/ning/billing/account/dao/TestAccountDao.java
+++ b/account/src/test/java/com/ning/billing/account/dao/TestAccountDao.java
@@ -16,27 +16,11 @@
 
 package com.ning.billing.account.dao;
 
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertNotNull;
-import static org.testng.Assert.assertTrue;
-import static org.testng.Assert.fail;
-
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
-import com.ning.billing.account.api.AccountEmail;
-import com.ning.billing.account.api.DefaultAccountEmail;
-import com.ning.billing.util.api.TagApiException;
-import com.ning.billing.util.customfield.CustomField;
-import com.ning.billing.util.customfield.StringCustomField;
-import com.ning.billing.util.customfield.dao.AuditedCustomFieldDao;
-import com.ning.billing.util.customfield.dao.CustomFieldDao;
-import com.ning.billing.util.dao.ObjectType;
-import com.ning.billing.util.entity.EntityPersistenceException;
-import com.ning.billing.util.tag.dao.AuditedTagDao;
-import com.ning.billing.util.tag.dao.TagDao;
 import org.joda.time.DateTimeZone;
 import org.skife.jdbi.v2.Handle;
 import org.testng.annotations.Test;
@@ -44,40 +28,56 @@ import org.testng.annotations.Test;
 import com.ning.billing.account.api.Account;
 import com.ning.billing.account.api.AccountApiException;
 import com.ning.billing.account.api.AccountData;
+import com.ning.billing.account.api.AccountEmail;
 import com.ning.billing.account.api.DefaultAccount;
+import com.ning.billing.account.api.DefaultAccountEmail;
 import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.util.api.TagApiException;
+import com.ning.billing.util.customfield.CustomField;
+import com.ning.billing.util.customfield.StringCustomField;
+import com.ning.billing.util.customfield.dao.AuditedCustomFieldDao;
+import com.ning.billing.util.customfield.dao.CustomFieldDao;
+import com.ning.billing.util.dao.ObjectType;
+import com.ning.billing.util.entity.EntityPersistenceException;
 import com.ning.billing.util.tag.DefaultTagDefinition;
 import com.ning.billing.util.tag.Tag;
 import com.ning.billing.util.tag.TagDefinition;
+import com.ning.billing.util.tag.dao.AuditedTagDao;
+import com.ning.billing.util.tag.dao.TagDao;
 import com.ning.billing.util.tag.dao.TagDefinitionSqlDao;
 
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
 @Test(groups = {"slow", "account-dao"})
 public class TestAccountDao extends AccountDaoTestBase {
-    private Account createTestAccount(int billCycleDay) {
+    private Account createTestAccount(final int billCycleDay) {
         return createTestAccount(billCycleDay, "123-456-7890");
     }
 
-    private Account createTestAccount(int billCycleDay, String phone) {
-        String thisKey = "test" + UUID.randomUUID().toString();
-        String lastName = UUID.randomUUID().toString();
-        String thisEmail = "me@me.com" + " " + UUID.randomUUID();
-        String firstName = "Bob";
-        String name = firstName + " " + lastName;
-        String locale = "EN-US";
-        DateTimeZone timeZone = DateTimeZone.forID("America/Los_Angeles");
-        int firstNameLength = firstName.length();
+    private Account createTestAccount(final int billCycleDay, final String phone) {
+        final String thisKey = "test" + UUID.randomUUID().toString();
+        final String lastName = UUID.randomUUID().toString();
+        final String thisEmail = "me@me.com" + " " + UUID.randomUUID();
+        final String firstName = "Bob";
+        final String name = firstName + " " + lastName;
+        final String locale = "EN-US";
+        final DateTimeZone timeZone = DateTimeZone.forID("America/Los_Angeles");
+        final int firstNameLength = firstName.length();
 
         return new DefaultAccount(UUID.randomUUID(), thisKey, thisEmail, name, firstNameLength, Currency.USD,
-                billCycleDay, UUID.randomUUID(), timeZone, locale,
-                null, null, null, null, null, null, null, // add null address fields
-                phone, false, false);
+                                  billCycleDay, UUID.randomUUID(), timeZone, locale,
+                                  null, null, null, null, null, null, null, // add null address fields
+                                  phone, false, false);
     }
 
     @Test
     public void testBasic() throws EntityPersistenceException {
-        Account a = createTestAccount(5);
+        final Account a = createTestAccount(5);
         accountDao.create(a, context);
-        String key = a.getExternalKey();
+        final String key = a.getExternalKey();
 
         Account r = accountDao.getAccountByKey(key);
         assertNotNull(r);
@@ -87,7 +87,7 @@ public class TestAccountDao extends AccountDaoTestBase {
         assertNotNull(r);
         assertEquals(r.getExternalKey(), a.getExternalKey());
 
-        List<Account> all = accountDao.get();
+        final List<Account> all = accountDao.get();
         assertNotNull(all);
         assertTrue(all.size() >= 1);
     }
@@ -95,27 +95,27 @@ public class TestAccountDao extends AccountDaoTestBase {
     // simple test to ensure long phone numbers can be stored
     @Test
     public void testLongPhoneNumber() throws EntityPersistenceException {
-        Account account = createTestAccount(1, "123456789012345678901234");
+        final Account account = createTestAccount(1, "123456789012345678901234");
         accountDao.create(account, context);
 
-        Account saved = accountDao.getAccountByKey(account.getExternalKey());
+        final Account saved = accountDao.getAccountByKey(account.getExternalKey());
         assertNotNull(saved);
     }
 
     // simple test to ensure excessively long phone numbers cannot be stored
     @Test(expectedExceptions = {EntityPersistenceException.class})
     public void testOverlyLongPhoneNumber() throws EntityPersistenceException {
-        Account account = createTestAccount(1, "12345678901234567890123456");
+        final Account account = createTestAccount(1, "12345678901234567890123456");
         accountDao.create(account, context);
     }
 
     @Test
     public void testGetById() throws EntityPersistenceException {
         Account account = createTestAccount(1);
-        UUID id = account.getId();
-        String key = account.getExternalKey();
-        String name = account.getName();
-        int firstNameLength = account.getFirstNameLength();
+        final UUID id = account.getId();
+        final String key = account.getExternalKey();
+        final String name = account.getName();
+        final int firstNameLength = account.getFirstNameLength();
 
         accountDao.create(account, context);
 
@@ -130,45 +130,45 @@ public class TestAccountDao extends AccountDaoTestBase {
 
     @Test
     public void testCustomFields() throws EntityPersistenceException {
-        String fieldName = "testField1";
-        String fieldValue = "testField1_value";
+        final String fieldName = "testField1";
+        final String fieldValue = "testField1_value";
 
-        UUID accountId = UUID.randomUUID();
-        List<CustomField> customFields = new ArrayList<CustomField>();
+        final UUID accountId = UUID.randomUUID();
+        final List<CustomField> customFields = new ArrayList<CustomField>();
         customFields.add(new StringCustomField(fieldName, fieldValue));
-        CustomFieldDao customFieldDao = new AuditedCustomFieldDao(dbi);
+        final CustomFieldDao customFieldDao = new AuditedCustomFieldDao(dbi);
         customFieldDao.saveEntities(accountId, ObjectType.ACCOUNT, customFields, context);
 
-        Map<String, CustomField> customFieldMap = customFieldDao.loadEntities(accountId, ObjectType.ACCOUNT);
+        final Map<String, CustomField> customFieldMap = customFieldDao.loadEntities(accountId, ObjectType.ACCOUNT);
         assertEquals(customFieldMap.size(), 1);
-        CustomField customField = customFieldMap.get(fieldName);
+        final CustomField customField = customFieldMap.get(fieldName);
         assertEquals(customField.getName(), fieldName);
         assertEquals(customField.getValue(), fieldValue);
     }
 
     @Test
     public void testTags() throws EntityPersistenceException, TagApiException {
-        Account account = createTestAccount(1);
-        TagDefinition definition = new DefaultTagDefinition("Test Tag", "For testing only", false);
-        TagDefinitionSqlDao tagDescriptionDao = dbi.onDemand(TagDefinitionSqlDao.class);
+        final Account account = createTestAccount(1);
+        final TagDefinition definition = new DefaultTagDefinition("Test Tag", "For testing only", false);
+        final TagDefinitionSqlDao tagDescriptionDao = dbi.onDemand(TagDefinitionSqlDao.class);
         tagDescriptionDao.create(definition, context);
 
-        TagDao tagDao = new AuditedTagDao(dbi, tagEventBuilder, bus);
+        final TagDao tagDao = new AuditedTagDao(dbi, tagEventBuilder, bus);
         tagDao.insertTag(account.getId(), ObjectType.ACCOUNT, definition, context);
 
-        Map<String, Tag> tagMap = tagDao.loadEntities(account.getId(), ObjectType.ACCOUNT);
+        final Map<String, Tag> tagMap = tagDao.loadEntities(account.getId(), ObjectType.ACCOUNT);
         assertEquals(tagMap.size(), 1);
-        Tag tag = tagMap.get(definition.getName());
+        final Tag tag = tagMap.get(definition.getName());
         assertEquals(tag.getTagDefinitionName(), definition.getName());
     }
 
     @Test
     public void testGetIdFromKey() throws EntityPersistenceException {
-        Account account = createTestAccount(1);
+        final Account account = createTestAccount(1);
         accountDao.create(account, context);
 
         try {
-            UUID accountId = accountDao.getIdFromKey(account.getExternalKey());
+            final UUID accountId = accountDao.getIdFromKey(account.getExternalKey());
             assertEquals(accountId, account.getId());
         } catch (AccountApiException a) {
             fail("Retrieving account failed.");
@@ -177,7 +177,7 @@ public class TestAccountDao extends AccountDaoTestBase {
 
     @Test(expectedExceptions = AccountApiException.class)
     public void testGetIdFromKeyForNullKey() throws AccountApiException {
-        String key = null;
+        final String key = null;
         accountDao.getIdFromKey(key);
     }
 
@@ -186,7 +186,7 @@ public class TestAccountDao extends AccountDaoTestBase {
         final Account account = createTestAccount(1);
         accountDao.create(account, context);
 
-        AccountData accountData = new AccountData() {
+        final AccountData accountData = new AccountData() {
             @Override
             public String getExternalKey() {
                 return account.getExternalKey();
@@ -236,6 +236,7 @@ public class TestAccountDao extends AccountDaoTestBase {
             public UUID getPaymentMethodId() {
                 return account.getPaymentMethodId();
             }
+
             @Override
             public DateTimeZone getTimeZone() {
                 return DateTimeZone.forID("Australia/Darwin");
@@ -245,6 +246,7 @@ public class TestAccountDao extends AccountDaoTestBase {
             public String getLocale() {
                 return "FR-CA";
             }
+
             @Override
             public String getAddress1() {
                 return null;
@@ -281,10 +283,10 @@ public class TestAccountDao extends AccountDaoTestBase {
             }
         };
 
-        Account updatedAccount = new DefaultAccount(account.getId(), accountData);
+        final Account updatedAccount = new DefaultAccount(account.getId(), accountData);
         accountDao.update(updatedAccount, context);
 
-        Account savedAccount = accountDao.getAccountByKey(account.getExternalKey());
+        final Account savedAccount = accountDao.getAccountByKey(account.getExternalKey());
 
         assertNotNull(savedAccount);
         assertEquals(savedAccount.getName(), updatedAccount.getName());
@@ -305,31 +307,31 @@ public class TestAccountDao extends AccountDaoTestBase {
 
     @Test
     public void testAddingContactInformation() throws Exception {
-        UUID accountId = UUID.randomUUID();
-        DefaultAccount account = new DefaultAccount(accountId, "extKey123456", "myemail123456@glam.com",
-                                                    "John Smith", 4, Currency.USD, 15, null,
-                                                    DateTimeZone.forID("America/Cambridge_Bay"), "EN-CA",
-                                                    null, null, null, null, null, null, null, null, false, false);
+        final UUID accountId = UUID.randomUUID();
+        final DefaultAccount account = new DefaultAccount(accountId, "extKey123456", "myemail123456@glam.com",
+                                                          "John Smith", 4, Currency.USD, 15, null,
+                                                          DateTimeZone.forID("America/Cambridge_Bay"), "EN-CA",
+                                                          null, null, null, null, null, null, null, null, false, false);
         accountDao.create(account, context);
 
-        String address1 = "123 address 1";
-        String address2 = "456 address 2";
-        String companyName = "Some Company";
-        String city = "Cambridge Bay";
-        String stateOrProvince = "Nunavut";
-        String country = "Canada";
-        String postalCode = "X0B 0C0";
-        String phone = "18001112222";
-
-        DefaultAccount updatedAccount = new DefaultAccount(accountId, "extKey123456", "myemail123456@glam.com",
-                                                    "John Smith", 4, Currency.USD, 15, null,
-                                                    DateTimeZone.forID("America/Cambridge_Bay"), "EN-CA",
-                                                    address1, address2, companyName, city, stateOrProvince, country,
-                                                    postalCode, phone, false, false);
+        final String address1 = "123 address 1";
+        final String address2 = "456 address 2";
+        final String companyName = "Some Company";
+        final String city = "Cambridge Bay";
+        final String stateOrProvince = "Nunavut";
+        final String country = "Canada";
+        final String postalCode = "X0B 0C0";
+        final String phone = "18001112222";
+
+        final DefaultAccount updatedAccount = new DefaultAccount(accountId, "extKey123456", "myemail123456@glam.com",
+                                                                 "John Smith", 4, Currency.USD, 15, null,
+                                                                 DateTimeZone.forID("America/Cambridge_Bay"), "EN-CA",
+                                                                 address1, address2, companyName, city, stateOrProvince, country,
+                                                                 postalCode, phone, false, false);
 
         accountDao.update(updatedAccount, context);
 
-        Account savedAccount = accountDao.getById(accountId);
+        final Account savedAccount = accountDao.getById(accountId);
 
         assertNotNull(savedAccount);
         assertEquals(savedAccount.getId(), accountId);
@@ -345,25 +347,25 @@ public class TestAccountDao extends AccountDaoTestBase {
 
     @Test
     public void testRemovingContactInformation() throws Exception {
-        UUID accountId = UUID.randomUUID();
-
-        DefaultAccount account = new DefaultAccount(accountId, "extKey654321", "myemail654321@glam.com",
-                                                    "John Smith", 4, Currency.USD, 15, null,
-                                                    DateTimeZone.forID("America/Cambridge_Bay"), "EN-CA",
-                                                    "123 address 1", "456 address 2", null, "Cambridge Bay",
-                                                    "Nunavut", "Canada", "X0B 0C0", "18001112222",
-                                                    false, false);
+        final UUID accountId = UUID.randomUUID();
+
+        final DefaultAccount account = new DefaultAccount(accountId, "extKey654321", "myemail654321@glam.com",
+                                                          "John Smith", 4, Currency.USD, 15, null,
+                                                          DateTimeZone.forID("America/Cambridge_Bay"), "EN-CA",
+                                                          "123 address 1", "456 address 2", null, "Cambridge Bay",
+                                                          "Nunavut", "Canada", "X0B 0C0", "18001112222",
+                                                          false, false);
         accountDao.create(account, context);
 
-        DefaultAccount updatedAccount = new DefaultAccount(accountId, "extKey654321", "myemail654321@glam.com",
-                                                    "John Smith", 4, Currency.USD, 15, null,
-                                                    DateTimeZone.forID("America/Cambridge_Bay"), "EN-CA",
-                                                    null, null, null, null, null, null, null, null,
-                                                    false, false);
+        final DefaultAccount updatedAccount = new DefaultAccount(accountId, "extKey654321", "myemail654321@glam.com",
+                                                                 "John Smith", 4, Currency.USD, 15, null,
+                                                                 DateTimeZone.forID("America/Cambridge_Bay"), "EN-CA",
+                                                                 null, null, null, null, null, null, null, null,
+                                                                 false, false);
 
         accountDao.update(updatedAccount, context);
 
-        Account savedAccount = accountDao.getById(accountId);
+        final Account savedAccount = accountDao.getById(accountId);
 
         assertNotNull(savedAccount);
         assertEquals(savedAccount.getId(), accountId);
@@ -379,19 +381,19 @@ public class TestAccountDao extends AccountDaoTestBase {
 
     @Test(expectedExceptions = EntityPersistenceException.class)
     public void testExternalKeyCannotBeUpdated() throws Exception {
-        UUID accountId = UUID.randomUUID();
-        String originalExternalKey = "extKey1337";
+        final UUID accountId = UUID.randomUUID();
+        final String originalExternalKey = "extKey1337";
 
-        DefaultAccount account = new DefaultAccount(accountId, originalExternalKey, "myemail1337@glam.com",
-                                                    "John Smith", 4, Currency.USD, 15, null,
-                                                    null, null, null, null, null, null, null, null, null, null,
-                                                    false, false);
+        final DefaultAccount account = new DefaultAccount(accountId, originalExternalKey, "myemail1337@glam.com",
+                                                          "John Smith", 4, Currency.USD, 15, null,
+                                                          null, null, null, null, null, null, null, null, null, null,
+                                                          false, false);
         accountDao.create(account, context);
 
-        DefaultAccount updatedAccount = new DefaultAccount(accountId, "extKey1338", "myemail1337@glam.com",
-                                                    "John Smith", 4, Currency.USD, 15, null,
-                                                    null, null, null, null, null, null, null, null, null, null,
-                                                    false, false);
+        final DefaultAccount updatedAccount = new DefaultAccount(accountId, "extKey1338", "myemail1337@glam.com",
+                                                                 "John Smith", 4, Currency.USD, 15, null,
+                                                                 null, null, null, null, null, null, null, null, null, null,
+                                                                 false, false);
         accountDao.update(updatedAccount, context);
     }
 
@@ -400,7 +402,7 @@ public class TestAccountDao extends AccountDaoTestBase {
         List<AccountEmail> emails = new ArrayList<AccountEmail>();
 
         // generate random account id
-        UUID accountId = UUID.randomUUID();
+        final UUID accountId = UUID.randomUUID();
 
         // add a new e-mail
         final AccountEmail email = new DefaultAccountEmail(accountId, "test@gmail.com");
@@ -413,7 +415,7 @@ public class TestAccountDao extends AccountDaoTestBase {
         verifyAccountEmailAuditAndHistoryCount(accountId, 1);
 
         // update e-mail
-        AccountEmail updatedEmail = new DefaultAccountEmail(email, "test2@gmail.com");
+        final AccountEmail updatedEmail = new DefaultAccountEmail(email, "test2@gmail.com");
         emails.clear();
         emails.add(updatedEmail);
         accountEmailDao.saveEmails(accountId, emails, context);
@@ -433,8 +435,46 @@ public class TestAccountDao extends AccountDaoTestBase {
         verifyAccountEmailAuditAndHistoryCount(accountId, 4);
     }
 
-    private void verifyAccountEmailAuditAndHistoryCount(UUID accountId, int expectedCount) {
-        Handle handle = dbi.open();
+    @Test
+    public void testAddAndRemoveAccountEmail() {
+        final UUID accountId = UUID.randomUUID();
+        final String email1 = UUID.randomUUID().toString();
+        final String email2 = UUID.randomUUID().toString();
+
+        // Verify the original state
+        assertEquals(accountEmailDao.getEmails(accountId).size(), 0);
+
+        // Add a new e-mail
+        final AccountEmail accountEmail1 = new DefaultAccountEmail(accountId, email1);
+        accountEmailDao.addEmail(accountId, accountEmail1, context);
+        final List<AccountEmail> firstEmails = accountEmailDao.getEmails(accountId);
+        assertEquals(firstEmails.size(), 1);
+        assertEquals(firstEmails.get(0).getAccountId(), accountId);
+        assertEquals(firstEmails.get(0).getEmail(), email1);
+
+        // Add a second e-mail
+        final AccountEmail accountEmail2 = new DefaultAccountEmail(accountId, email2);
+        accountEmailDao.addEmail(accountId, accountEmail2, context);
+        final List<AccountEmail> secondEmails = accountEmailDao.getEmails(accountId);
+        assertEquals(secondEmails.size(), 2);
+        assertTrue(secondEmails.get(0).getAccountId().equals(accountId));
+        assertTrue(secondEmails.get(1).getAccountId().equals(accountId));
+        assertTrue(secondEmails.get(0).getEmail().equals(email1) || secondEmails.get(0).getEmail().equals(email2));
+        assertTrue(secondEmails.get(1).getEmail().equals(email1) || secondEmails.get(1).getEmail().equals(email2));
+
+        // Delete the first e-mail
+        accountEmailDao.removeEmail(accountId, accountEmail1, context);
+        final List<AccountEmail> thirdEmails = accountEmailDao.getEmails(accountId);
+        assertEquals(thirdEmails.size(), 1);
+        assertEquals(thirdEmails.get(0).getAccountId(), accountId);
+        assertEquals(thirdEmails.get(0).getEmail(), email2);
+
+        // Verify that history and audit contain three entries (2 inserts and one delete)
+        verifyAccountEmailAuditAndHistoryCount(accountId, 3);
+    }
+
+    private void verifyAccountEmailAuditAndHistoryCount(final UUID accountId, final int expectedCount) {
+        final Handle handle = dbi.open();
 
         // verify audit
         StringBuilder sb = new StringBuilder();
diff --git a/analytics/src/test/java/com/ning/billing/analytics/MockAccountUserApi.java b/analytics/src/test/java/com/ning/billing/analytics/MockAccountUserApi.java
index a758681..51f2ae8 100644
--- a/analytics/src/test/java/com/ning/billing/analytics/MockAccountUserApi.java
+++ b/analytics/src/test/java/com/ning/billing/analytics/MockAccountUserApi.java
@@ -30,8 +30,6 @@ import com.ning.billing.account.api.DefaultAccount;
 import com.ning.billing.account.api.MigrationAccountData;
 import com.ning.billing.catalog.api.Currency;
 import com.ning.billing.util.callcontext.CallContext;
-import com.ning.billing.util.customfield.CustomField;
-import com.ning.billing.util.tag.TagDefinition;
 
 public class MockAccountUserApi implements AccountUserApi {
     private final AccountData account;
@@ -83,6 +81,16 @@ public class MockAccountUserApi implements AccountUserApi {
     }
 
     @Override
+    public void addEmail(final UUID accountId, final AccountEmail email, final CallContext context) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void removeEmail(final UUID accountId, final AccountEmail email, final CallContext context) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
     public Account migrateAccount(MigrationAccountData data, final CallContext context)
             throws AccountApiException {
         throw new UnsupportedOperationException();
diff --git a/api/src/main/java/com/ning/billing/account/api/AccountUserApi.java b/api/src/main/java/com/ning/billing/account/api/AccountUserApi.java
index 54d176f..85e03cf 100644
--- a/api/src/main/java/com/ning/billing/account/api/AccountUserApi.java
+++ b/api/src/main/java/com/ning/billing/account/api/AccountUserApi.java
@@ -50,4 +50,8 @@ public interface AccountUserApi {
     public List<AccountEmail> getEmails(UUID accountId);
 
     public void saveEmails(UUID accountId, List<AccountEmail> emails, CallContext context);
+
+    public void addEmail(UUID accountId, AccountEmail email, CallContext context);
+
+    public void removeEmail(UUID accountId, AccountEmail email, CallContext context);
 }
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/AccountEmailJson.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/AccountEmailJson.java
new file mode 100644
index 0000000..9a0d074
--- /dev/null
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/AccountEmailJson.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.jaxrs.json;
+
+import java.util.UUID;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.ning.billing.account.api.AccountEmail;
+
+public class AccountEmailJson {
+    private final String accountId;
+    private final String email;
+
+    @JsonCreator
+    public AccountEmailJson(@JsonProperty("accountId") final String accountId, @JsonProperty("email") final String email) {
+        this.accountId = accountId;
+        this.email = email;
+    }
+
+    public String getAccountId() {
+        return accountId;
+    }
+
+    public String getEmail() {
+        return email;
+    }
+
+    public AccountEmail toAccountEmail() {
+        final UUID accountEmailId = UUID.randomUUID();
+
+        return new AccountEmail() {
+            @Override
+            public UUID getAccountId() {
+                return UUID.fromString(accountId);
+            }
+
+            @Override
+            public String getEmail() {
+                return email;
+            }
+
+            @Override
+            public UUID getId() {
+                return accountEmailId;
+            }
+        };
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("AccountEmailJson");
+        sb.append("{accountId='").append(accountId).append('\'');
+        sb.append(", email='").append(email).append('\'');
+        sb.append('}');
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final AccountEmailJson that = (AccountEmailJson) o;
+
+        if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
+            return false;
+        }
+        if (email != null ? !email.equals(that.email) : that.email != null) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = accountId != null ? accountId.hashCode() : 0;
+        result = 31 * result + (email != null ? email.hashCode() : 0);
+        return result;
+    }
+}
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/AccountResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/AccountResource.java
index eb633cf..2249eb6 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/AccountResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/AccountResource.java
@@ -46,6 +46,7 @@ import com.ning.billing.ErrorCode;
 import com.ning.billing.account.api.Account;
 import com.ning.billing.account.api.AccountApiException;
 import com.ning.billing.account.api.AccountData;
+import com.ning.billing.account.api.AccountEmail;
 import com.ning.billing.account.api.AccountUserApi;
 import com.ning.billing.entitlement.api.timeline.BundleTimeline;
 import com.ning.billing.entitlement.api.timeline.EntitlementRepairException;
@@ -54,6 +55,7 @@ import com.ning.billing.entitlement.api.user.EntitlementUserApi;
 import com.ning.billing.entitlement.api.user.SubscriptionBundle;
 import com.ning.billing.invoice.api.Invoice;
 import com.ning.billing.invoice.api.InvoiceUserApi;
+import com.ning.billing.jaxrs.json.AccountEmailJson;
 import com.ning.billing.jaxrs.json.AccountJson;
 import com.ning.billing.jaxrs.json.AccountTimelineJson;
 import com.ning.billing.jaxrs.json.BundleJsonNoSubscriptions;
@@ -80,6 +82,7 @@ public class AccountResource extends JaxRsResourceBase {
     private static final String ID_PARAM_NAME = "accountId";
     private static final String CUSTOM_FIELD_URI = JaxrsResource.CUSTOM_FIELDS + "/{" + ID_PARAM_NAME + ":" + UUID_PATTERN + "}";
     private static final String TAG_URI = JaxrsResource.TAGS + "/{" + ID_PARAM_NAME + ":" + UUID_PATTERN + "}";
+    private static final String EMAIL_URI = JaxrsResource.EMAILS + "/{" + ID_PARAM_NAME + ":" + UUID_PATTERN + "}";
 
     private final AccountUserApi accountApi;
     private final EntitlementUserApi entitlementApi;
@@ -259,8 +262,7 @@ public class AccountResource extends JaxRsResourceBase {
         }
     }
 
-
-    /**
+    /*
      * **************************  PAYMENTS ********************************
      */
 
@@ -313,7 +315,6 @@ public class AccountResource extends JaxRsResourceBase {
         }
     }
 
-
     @GET
     @Path("/{accountId:\\w+-\\w+-\\w+-\\w+-\\w+}/" + PAYMENT_METHODS)
     @Produces(APPLICATION_JSON)
@@ -332,9 +333,10 @@ public class AccountResource extends JaxRsResourceBase {
         }
     }
 
-    /**
-     * *************************      TAGS     *****************************
+    /*
+     * *************************      CUSTOM FIELDS     *****************************
      */
+
     @GET
     @Path(CUSTOM_FIELD_URI)
     @Produces(APPLICATION_JSON)
@@ -368,6 +370,10 @@ public class AccountResource extends JaxRsResourceBase {
                                         context.createContext(createdBy, reason, comment));
     }
 
+    /*
+     * *************************      TAGS     *****************************
+     */
+
     @GET
     @Path(TAG_URI)
     @Produces(APPLICATION_JSON)
@@ -402,6 +408,71 @@ public class AccountResource extends JaxRsResourceBase {
                                 context.createContext(createdBy, reason, comment));
     }
 
+    /*
+     * *************************      EMAILS     *****************************
+     */
+
+    @GET
+    @Path(EMAIL_URI)
+    @Produces(APPLICATION_JSON)
+    public Response getEmails(@PathParam(ID_PARAM_NAME) final String id) {
+        final UUID accountId = UUID.fromString(id);
+        final List<AccountEmail> emails = accountApi.getEmails(accountId);
+
+        final List<AccountEmailJson> emailsJson = new ArrayList<AccountEmailJson>();
+        for (final AccountEmail email : emails) {
+            emailsJson.add(new AccountEmailJson(email.getAccountId().toString(), email.getEmail()));
+        }
+        return Response.status(Response.Status.OK).entity(emailsJson).build();
+    }
+
+    @POST
+    @Path(EMAIL_URI)
+    @Consumes(APPLICATION_JSON)
+    @Produces(APPLICATION_JSON)
+    public Response addEmail(final AccountEmailJson json,
+                             @PathParam(ID_PARAM_NAME) final String id,
+                             @HeaderParam(HDR_CREATED_BY) final String createdBy,
+                             @HeaderParam(HDR_REASON) final String reason,
+                             @HeaderParam(HDR_COMMENT) final String comment) {
+        try {
+            final UUID accountId = UUID.fromString(id);
+
+            // Make sure the account exist or we will confuse the history and auditing code
+            if (accountApi.getAccountById(accountId) == null) {
+                return Response.status(Response.Status.BAD_REQUEST).entity("Account id " + accountId + " does not exist").build();
+            }
+
+            accountApi.addEmail(accountId, json.toAccountEmail(), context.createContext(createdBy, reason, comment));
+
+            return uriBuilder.buildResponse(AccountResource.class, "getEmails", json.getAccountId());
+        } catch (RuntimeException e) {
+            return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
+        } catch (AccountApiException e) {
+            return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
+        }
+    }
+
+    @DELETE
+    @Path(EMAIL_URI + "/{email}")
+    @Produces(APPLICATION_JSON)
+    public Response removeEmail(@PathParam(ID_PARAM_NAME) final String id,
+                                @PathParam("email") final String email,
+                                @HeaderParam(HDR_CREATED_BY) final String createdBy,
+                                @HeaderParam(HDR_REASON) final String reason,
+                                @HeaderParam(HDR_COMMENT) final String comment) {
+        try {
+            final UUID accountId = UUID.fromString(id);
+            final AccountEmailJson accountEmailJson = new AccountEmailJson(id, email);
+            final AccountEmail accountEmail = accountEmailJson.toAccountEmail();
+            accountApi.removeEmail(accountId, accountEmail, context.createContext(createdBy, reason, comment));
+
+            return Response.status(Response.Status.OK).build();
+        } catch (RuntimeException e) {
+            return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
+        }
+    }
+
     @Override
     protected ObjectType getObjectType() {
         return ObjectType.ACCOUNT;
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java
index 1d2e92a..8da72f2 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java
@@ -88,6 +88,7 @@ public interface JaxrsResource {
 
     public static final String TAGS = "tags";
     public static final String CUSTOM_FIELDS = "custom_fields";
+    public static final String EMAILS = "emails";
 
     public static final String CATALOG = "catalog";
     public static final String CATALOG_PATH = PREFIX + "/" + CATALOG;
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestAccountEmailJson.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestAccountEmailJson.java
new file mode 100644
index 0000000..c9e55f8
--- /dev/null
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestAccountEmailJson.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.jaxrs.json;
+
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.ning.billing.account.api.AccountEmail;
+
+public class TestAccountEmailJson {
+    private static final ObjectMapper mapper = new ObjectMapper();
+
+    @Test(groups = "fast")
+    public void testJson() throws Exception {
+        final String accountId = UUID.randomUUID().toString();
+        final String email = UUID.randomUUID().toString();
+
+        final AccountEmailJson accountEmailJson = new AccountEmailJson(accountId, email);
+        Assert.assertEquals(accountEmailJson.getAccountId(), accountId);
+        Assert.assertEquals(accountEmailJson.getEmail(), email);
+
+        final String asJson = mapper.writeValueAsString(accountEmailJson);
+        Assert.assertEquals(asJson, "{\"accountId\":\"" + accountId + "\"," +
+                "\"email\":\"" + email + "\"}");
+
+        final AccountEmailJson fromJson = mapper.readValue(asJson, AccountEmailJson.class);
+        Assert.assertEquals(fromJson, accountEmailJson);
+    }
+
+    @Test(groups = "fast")
+    public void testToAccountEmail() throws Exception {
+        final String accountId = UUID.randomUUID().toString();
+        final String email = UUID.randomUUID().toString();
+
+        final AccountEmailJson accountEmailJson = new AccountEmailJson(accountId, email);
+        Assert.assertEquals(accountEmailJson.getAccountId(), accountId);
+        Assert.assertEquals(accountEmailJson.getEmail(), email);
+
+        final AccountEmail accountEmail = accountEmailJson.toAccountEmail();
+        Assert.assertEquals(accountEmail.getAccountId().toString(), accountId);
+        Assert.assertEquals(accountEmail.getEmail(), email);
+    }
+}
diff --git a/junction/src/main/java/com/ning/billing/junction/plumbing/api/BlockingAccountUserApi.java b/junction/src/main/java/com/ning/billing/junction/plumbing/api/BlockingAccountUserApi.java
index 9463923..e568b61 100644
--- a/junction/src/main/java/com/ning/billing/junction/plumbing/api/BlockingAccountUserApi.java
+++ b/junction/src/main/java/com/ning/billing/junction/plumbing/api/BlockingAccountUserApi.java
@@ -95,4 +95,14 @@ public class BlockingAccountUserApi implements AccountUserApi {
     public void saveEmails(final UUID accountId, final List<AccountEmail> emails, final CallContext context) {
         userApi.saveEmails(accountId, emails, context);
     }
+
+    @Override
+    public void addEmail(final UUID accountId, final AccountEmail email, final CallContext context) {
+        userApi.addEmail(accountId, email, context);
+    }
+
+    @Override
+    public void removeEmail(final UUID accountId, final AccountEmail email, final CallContext context) {
+        userApi.removeEmail(accountId, email, context);
+    }
 }
diff --git a/server/src/test/java/com/ning/billing/jaxrs/TestAccountEmail.java b/server/src/test/java/com/ning/billing/jaxrs/TestAccountEmail.java
new file mode 100644
index 0000000..428a352
--- /dev/null
+++ b/server/src/test/java/com/ning/billing/jaxrs/TestAccountEmail.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.jaxrs;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.ning.billing.jaxrs.json.AccountEmailJson;
+import com.ning.billing.jaxrs.json.AccountJson;
+import com.ning.billing.jaxrs.resources.JaxrsResource;
+import com.ning.http.client.Response;
+
+import static org.testng.Assert.assertEquals;
+
+public class TestAccountEmail extends TestJaxrsBase {
+    @Test(groups = "slow", enabled = true)
+    public void testAddAndRemoveAccountEmail() throws Exception {
+        final AccountJson input = createAccount(UUID.randomUUID().toString(), UUID.randomUUID().toString(), UUID.randomUUID().toString());
+        final String accountId = input.getAccountId();
+
+        final String email1 = UUID.randomUUID().toString();
+        final String email2 = UUID.randomUUID().toString();
+        final AccountEmailJson accountEmailJson1 = new AccountEmailJson(accountId, email1);
+        final AccountEmailJson accountEmailJson2 = new AccountEmailJson(accountId, email2);
+
+        final String baseUri = JaxrsResource.ACCOUNTS_PATH + "/" + JaxrsResource.EMAILS + "/" + accountId;
+
+        // Verify the initial state
+        final Response firstResponse = doGet(baseUri, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
+        Assert.assertEquals(firstResponse.getStatusCode(), javax.ws.rs.core.Response.Status.OK.getStatusCode());
+        final List<AccountEmailJson> firstEmails = mapper.readValue(firstResponse.getResponseBody(), new TypeReference<List<AccountEmailJson>>() {});
+        Assert.assertEquals(firstEmails.size(), 0);
+
+        // Add an email
+        final String firstEmailString = mapper.writeValueAsString(accountEmailJson1);
+        final Response secondResponse = doPost(baseUri, firstEmailString, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
+        assertEquals(secondResponse.getStatusCode(), javax.ws.rs.core.Response.Status.CREATED.getStatusCode());
+
+        // Verify we can retrieve it
+        final Response thirdResponse = doGet(baseUri, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
+        Assert.assertEquals(thirdResponse.getStatusCode(), javax.ws.rs.core.Response.Status.OK.getStatusCode());
+        final List<AccountEmailJson> secondEmails = mapper.readValue(thirdResponse.getResponseBody(), new TypeReference<List<AccountEmailJson>>() {});
+        Assert.assertEquals(secondEmails.size(), 1);
+        Assert.assertEquals(secondEmails.get(0).getAccountId(), accountId);
+        Assert.assertEquals(secondEmails.get(0).getEmail(), email1);
+
+        // Add another email
+        final String secondEmailString = mapper.writeValueAsString(accountEmailJson2);
+        final Response thridResponse = doPost(baseUri, secondEmailString, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
+        assertEquals(thridResponse.getStatusCode(), javax.ws.rs.core.Response.Status.CREATED.getStatusCode());
+
+        // Verify we can retrieve both
+        final Response fourthResponse = doGet(baseUri, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
+        Assert.assertEquals(fourthResponse.getStatusCode(), javax.ws.rs.core.Response.Status.OK.getStatusCode());
+        final List<AccountEmailJson> thirdEmails = mapper.readValue(fourthResponse.getResponseBody(), new TypeReference<List<AccountEmailJson>>() {});
+        Assert.assertEquals(thirdEmails.size(), 2);
+        Assert.assertEquals(thirdEmails.get(0).getAccountId(), accountId);
+        Assert.assertEquals(thirdEmails.get(1).getAccountId(), accountId);
+        Assert.assertTrue(thirdEmails.get(0).getEmail().equals(email1) || thirdEmails.get(0).getEmail().equals(email2));
+        Assert.assertTrue(thirdEmails.get(1).getEmail().equals(email1) || thirdEmails.get(1).getEmail().equals(email2));
+
+        // Delete the first email
+        final Response fifthResponse = doDelete(baseUri + "/" + email1, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
+        assertEquals(fifthResponse.getStatusCode(), javax.ws.rs.core.Response.Status.OK.getStatusCode());
+
+        // Verify it has been deleted
+        final Response sixthResponse = doGet(baseUri, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
+        Assert.assertEquals(sixthResponse.getStatusCode(), javax.ws.rs.core.Response.Status.OK.getStatusCode());
+        final List<AccountEmailJson> fourthEmails = mapper.readValue(sixthResponse.getResponseBody(), new TypeReference<List<AccountEmailJson>>() {});
+        Assert.assertEquals(fourthEmails.size(), 1);
+        Assert.assertEquals(fourthEmails.get(0).getAccountId(), accountId);
+        Assert.assertEquals(fourthEmails.get(0).getEmail(), email2);
+    }
+}
diff --git a/server/src/test/java/com/ning/billing/jaxrs/TestJaxrsBase.java b/server/src/test/java/com/ning/billing/jaxrs/TestJaxrsBase.java
index 7df9c44..6c9ac05 100644
--- a/server/src/test/java/com/ning/billing/jaxrs/TestJaxrsBase.java
+++ b/server/src/test/java/com/ning/billing/jaxrs/TestJaxrsBase.java
@@ -527,6 +527,7 @@ public class TestJaxrsBase {
         String country = "France";
         String phone = "81 53 26 56";
 
+        // Note: the accountId payload is ignored on account creation
         AccountJson accountJson = new AccountJson(accountId, name, length, externalKey, email, billCycleDay, currency, null, timeZone, address1, address2, company, state, country, phone);
         return accountJson;
     }
diff --git a/util/src/main/java/com/ning/billing/util/dao/AuditedCollectionDaoBase.java b/util/src/main/java/com/ning/billing/util/dao/AuditedCollectionDaoBase.java
index 3cf6620..0b61692 100644
--- a/util/src/main/java/com/ning/billing/util/dao/AuditedCollectionDaoBase.java
+++ b/util/src/main/java/com/ning/billing/util/dao/AuditedCollectionDaoBase.java
@@ -47,74 +47,94 @@ public abstract class AuditedCollectionDaoBase<T extends Entity, V> implements A
      */
     protected abstract V getEquivalenceObjectFor(T obj);
 
+    /**
+     * Update all entities of given type objectType for the given objectId, e.g. if T is AccountEmail, objectId will
+     * represent an account id and objectType will be ACCOUNT_EMAIL.
+     * <p/>
+     * This will add and delete entities as needed.
+     *
+     * @param transactionalDao the current dao (in the transaction)
+     * @param objectId         the parent object id
+     * @param objectType       the entity object type
+     * @param newEntities      the final list of entities
+     * @param context          the current content
+     */
     @Override
     public void saveEntitiesFromTransaction(Transmogrifier transactionalDao, UUID objectId, ObjectType objectType, List<T> newEntities, CallContext context) {
         UpdatableEntityCollectionSqlDao<T> dao = transmogrifyDao(transactionalDao);
 
-        // get list of existing entities
-        List<T> currentEntities = dao.load(objectId.toString(), objectType);
+        // Get list of all existing entities for this parent object, e.g. find all email addresses for this account
+        final List<T> currentEntities = dao.load(objectId.toString(), objectType);
 
-        Map<V, T> currentObjs = new HashMap<V, T>(currentEntities.size());
-        Map<V, T> updatedObjs = new HashMap<V, T>(newEntities.size());
+        // Compute the list of objects to add, remove and/or update
+        final Map<V, T> currentObjs = new HashMap<V, T>(currentEntities.size());
+        final Map<V, T> updatedObjs = new HashMap<V, T>(newEntities.size());
 
-        for (T currentObj : currentEntities) {
+        for (final T currentObj : currentEntities) {
             currentObjs.put(getEquivalenceObjectFor(currentObj), currentObj);
         }
-        for (T updatedObj : newEntities) {
+        for (final T updatedObj : newEntities) {
             updatedObjs.put(getEquivalenceObjectFor(updatedObj), updatedObj);
         }
 
-        Set<V> equivToRemove = Sets.difference(currentObjs.keySet(), updatedObjs.keySet());
-        Set<V> equivToAdd = Sets.difference(updatedObjs.keySet(), currentObjs.keySet());
-        Set<V> equivToCheckForUpdate = Sets.intersection(updatedObjs.keySet(), currentObjs.keySet());
+        final Set<V> equivToRemove = Sets.difference(currentObjs.keySet(), updatedObjs.keySet());
+        final Set<V> equivToAdd = Sets.difference(updatedObjs.keySet(), currentObjs.keySet());
+        final Set<V> equivToCheckForUpdate = Sets.intersection(updatedObjs.keySet(), currentObjs.keySet());
 
-        List<T> objsToAdd = new ArrayList<T>(equivToAdd.size());
-        List<T> objsToRemove = new ArrayList<T>(equivToRemove.size());
-        List<T> objsToUpdate = new ArrayList<T>(equivToCheckForUpdate.size());
+        final List<T> objsToAdd = new ArrayList<T>(equivToAdd.size());
+        final List<T> objsToRemove = new ArrayList<T>(equivToRemove.size());
+        final List<T> objsToUpdate = new ArrayList<T>(equivToCheckForUpdate.size());
 
-        for (V equiv : equivToAdd) {
+        for (final V equiv : equivToAdd) {
             objsToAdd.add(updatedObjs.get(equiv));
         }
-        for (V equiv : equivToRemove) {
+        for (final V equiv : equivToRemove) {
             objsToRemove.add(currentObjs.get(equiv));
         }
-        for (V equiv : equivToCheckForUpdate) {
-            T currentObj = currentObjs.get(equiv);
-            T updatedObj = updatedObjs.get(equiv);
+        for (final V equiv : equivToCheckForUpdate) {
+            final T currentObj = currentObjs.get(equiv);
+            final T updatedObj = updatedObjs.get(equiv);
             if (!currentObj.equals(updatedObj)) {
                 objsToUpdate.add(updatedObj);
             }
         }
 
+        // Perform the inserts
         if (objsToAdd.size() != 0) {
             dao.insertFromTransaction(objectId.toString(), objectType, objsToAdd, context);
         }
 
+        // Perform the updates
         if (objsToUpdate.size() != 0) {
             dao.updateFromTransaction(objectId.toString(), objectType, objsToUpdate, context);
         }
 
-        // get all custom entities (including those that are about to be deleted) from the database in order to get the record ids
-        List<Mapper<UUID, Long>> recordIds = dao.getRecordIds(objectId.toString(), objectType);
-        Map<UUID, Long> recordIdMap = convertToHistoryMap(recordIds);
+        // Find all pairs <entity id, record id> (including those that are about to be deleted) for this parent object
+        final List<Mapper<UUID, Long>> recordIds = dao.getRecordIds(objectId.toString(), objectType);
+        // Flip the map to look up the record id associated with an entity id
+        final Map<UUID, Long> recordIdMap = convertToHistoryMap(recordIds);
 
+        // Perform the deletes
         if (objsToRemove.size() != 0) {
             dao.deleteFromTransaction(objectId.toString(), objectType, objsToRemove, context);
         }
 
-        List<EntityHistory<T>> entityHistories = new ArrayList<EntityHistory<T>>();
+        // Create the history objects
+        final List<EntityHistory<T>> entityHistories = new ArrayList<EntityHistory<T>>();
         entityHistories.addAll(convertToHistory(objsToAdd, recordIdMap, ChangeType.INSERT));
         entityHistories.addAll(convertToHistory(objsToUpdate, recordIdMap, ChangeType.UPDATE));
         entityHistories.addAll(convertToHistory(objsToRemove, recordIdMap, ChangeType.DELETE));
 
-        Long maxHistoryRecordId = dao.getMaxHistoryRecordId();
+        final Long maxHistoryRecordId = dao.getMaxHistoryRecordId();
+        // Save the records in the history table
         dao.addHistoryFromTransaction(objectId.toString(), objectType, entityHistories, context);
 
-        // have to fetch history record ids to update audit log
-        List<Mapper<Long, Long>> historyRecordIds = dao.getHistoryRecordIds(maxHistoryRecordId);
-        Map<Long, Long> historyRecordIdMap = convertToAuditMap(historyRecordIds);
-        List<EntityAudit> entityAudits = convertToAudits(entityHistories, historyRecordIdMap);
+        // We have to fetch history record ids to update audit log
+        final List<Mapper<Long, Long>> historyRecordIds = dao.getHistoryRecordIds(maxHistoryRecordId);
+        final Map<Long, Long> historyRecordIdMap = convertToAuditMap(historyRecordIds);
+        final List<EntityAudit> entityAudits = convertToAudits(entityHistories, historyRecordIdMap);
 
+        // Save an entry in the audit log
         dao.insertAuditFromTransaction(entityAudits, context);
     }
 
@@ -167,11 +187,12 @@ public abstract class AuditedCollectionDaoBase<T extends Entity, V> implements A
         return audits;
     }
 
-    protected Map<UUID, Long> convertToHistoryMap(List<Mapper<UUID, Long>> recordIds) {
-        Map<UUID, Long> recordIdMap = new HashMap<UUID, Long>();
-        for (Mapper<UUID, Long> recordId : recordIds) {
+    protected Map<UUID, Long> convertToHistoryMap(final List<Mapper<UUID, Long>> recordIds) {
+        final Map<UUID, Long> recordIdMap = new HashMap<UUID, Long>();
+        for (final Mapper<UUID, Long> recordId : recordIds) {
             recordIdMap.put(recordId.getKey(), recordId.getValue());
         }
+
         return recordIdMap;
     }
 
@@ -184,7 +205,10 @@ public abstract class AuditedCollectionDaoBase<T extends Entity, V> implements A
     }
 
     protected abstract TableName getTableName();
+
     protected abstract UpdatableEntityCollectionSqlDao<T> transmogrifyDao(Transmogrifier transactionalDao);
+
     protected abstract UpdatableEntityCollectionSqlDao<T> getSqlDao();
+
     protected abstract String getKey(T entity);
 }
diff --git a/util/src/main/java/com/ning/billing/util/dao/CollectionHistorySqlDao.java b/util/src/main/java/com/ning/billing/util/dao/CollectionHistorySqlDao.java
index d3a01d5..2ab74f6 100644
--- a/util/src/main/java/com/ning/billing/util/dao/CollectionHistorySqlDao.java
+++ b/util/src/main/java/com/ning/billing/util/dao/CollectionHistorySqlDao.java
@@ -16,19 +16,19 @@
 
 package com.ning.billing.util.dao;
 
-import com.ning.billing.util.callcontext.CallContext;
-import com.ning.billing.util.entity.Entity;
+import java.util.List;
+
 import org.skife.jdbi.v2.sqlobject.SqlBatch;
 import org.skife.jdbi.v2.sqlobject.SqlUpdate;
 
-import java.util.List;
-import java.util.UUID;
+import com.ning.billing.util.callcontext.CallContext;
+import com.ning.billing.util.entity.Entity;
 
 public interface CollectionHistorySqlDao<T extends Entity> {
-    @SqlBatch(transactional = false)
+    @SqlBatch
     public void addHistoryFromTransaction(String objectId, ObjectType objectType,
-                                           List<EntityHistory<T>> histories,
-                                           CallContext context);
+                                          List<EntityHistory<T>> histories,
+                                          CallContext context);
 
     @SqlUpdate
     public void addHistoryFromTransaction(String objectId, ObjectType objectType,
diff --git a/util/src/main/java/com/ning/billing/util/entity/collection/dao/EntityCollectionSqlDao.java b/util/src/main/java/com/ning/billing/util/entity/collection/dao/EntityCollectionSqlDao.java
index e74a64a..2decb16 100644
--- a/util/src/main/java/com/ning/billing/util/entity/collection/dao/EntityCollectionSqlDao.java
+++ b/util/src/main/java/com/ning/billing/util/entity/collection/dao/EntityCollectionSqlDao.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2010-2011 Ning, Inc.
+ * Copyright 2010-2012 Ning, Inc.
  *
  * Ning licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
@@ -38,16 +38,17 @@ import com.ning.billing.util.entity.Entity;
 /**
  * provides consistent semantics for entity collections
  * note: this is intended to be extended by an interface which provides @ExternalizedSqlViaStringTemplate3 and mappers
+ *
  * @param <T>
  */
 public interface EntityCollectionSqlDao<T extends Entity> {
-    @SqlBatch(transactional=false)
+    @SqlBatch
     public void insertFromTransaction(@Bind("objectId") final String objectId,
                                       @ObjectTypeBinder final ObjectType objectType,
                                       @BindBean final Collection<T> entities,
                                       @CallContextBinder final CallContext context);
 
-    @SqlBatch(transactional=false)
+    @SqlBatch
     public void deleteFromTransaction(@Bind("objectId") final String objectId,
                                       @ObjectTypeBinder final ObjectType objectType,
                                       @BindBean final Collection<T> entities,
diff --git a/util/src/main/java/com/ning/billing/util/entity/collection/dao/UpdatableEntityCollectionSqlDao.java b/util/src/main/java/com/ning/billing/util/entity/collection/dao/UpdatableEntityCollectionSqlDao.java
index 0079008..554120f 100644
--- a/util/src/main/java/com/ning/billing/util/entity/collection/dao/UpdatableEntityCollectionSqlDao.java
+++ b/util/src/main/java/com/ning/billing/util/entity/collection/dao/UpdatableEntityCollectionSqlDao.java
@@ -40,7 +40,7 @@ import com.ning.billing.util.entity.Entity;
 public interface UpdatableEntityCollectionSqlDao<T extends Entity> extends EntityCollectionSqlDao<T>,
         CollectionHistorySqlDao<T>,
         AuditSqlDao, CloseMe, Transmogrifier {
-    @SqlBatch(transactional=false)
+    @SqlBatch
     public void updateFromTransaction(@Bind("objectId") final String objectId,
                                       @ObjectTypeBinder final ObjectType objectType,
                                       @BindBean final Collection<T> entities,