killbill-aplcache

Changes

pom.xml 17(+16 -1)

util/pom.xml 16(+16 -0)

Details

diff --git a/api/src/main/java/com/ning/billing/ErrorCode.java b/api/src/main/java/com/ning/billing/ErrorCode.java
index db73376..b65c80c 100644
--- a/api/src/main/java/com/ning/billing/ErrorCode.java
+++ b/api/src/main/java/com/ning/billing/ErrorCode.java
@@ -168,7 +168,15 @@ public enum ErrorCode {
     OVERDUE_OVERDUEABLE_NOT_SUPPORTED(5001, "The Overdueable type '%s' is not supported"), 
     OVERDUE_CAT_ERROR_ENCOUNTERED(5002,"Catalog error encountered on Overdueable: id='%s', type='%s'"),  
     
-    
+   /*
+    *
+    * Range 9000: Miscellaneous
+    *
+    */
+    EMAIL_SENDING_FAILED(9000, "Sending email failed"),
+    EMAIL_PROPERTIES_FILE_MISSING(9001, "The properties file for email configuration could not be found."),
+    MISSING_TRANSLATION_RESOURCE(9010, "The resources for %s translation could not be found."),
+    MISSING_DEFAULT_TRANSLATION_RESOURCE(9011, "The default resource for %s translation could not be found.")
     ;
 
     private int code;
diff --git a/api/src/main/java/com/ning/billing/invoice/api/InvoiceNotifier.java b/api/src/main/java/com/ning/billing/invoice/api/InvoiceNotifier.java
index 13026c7..7a7fbc4 100644
--- a/api/src/main/java/com/ning/billing/invoice/api/InvoiceNotifier.java
+++ b/api/src/main/java/com/ning/billing/invoice/api/InvoiceNotifier.java
@@ -16,8 +16,9 @@
 
 package com.ning.billing.invoice.api;
 
+import com.ning.billing.BillingExceptionBase;
 import com.ning.billing.account.api.Account;
 
 public interface InvoiceNotifier {
-    public void notify(Account account, Invoice invoice);
+    public void notify(Account account, Invoice invoice) throws BillingExceptionBase;
 }
diff --git a/invoice/src/main/java/com/ning/billing/invoice/glue/InvoiceModule.java b/invoice/src/main/java/com/ning/billing/invoice/glue/InvoiceModule.java
index ba63bf1..41b782d 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/glue/InvoiceModule.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/glue/InvoiceModule.java
@@ -16,6 +16,9 @@
 
 package com.ning.billing.invoice.glue;
 
+import com.ning.billing.invoice.api.InvoiceNotifier;
+import com.ning.billing.invoice.notification.EmailInvoiceNotifier;
+import com.ning.billing.util.email.EmailConfig;
 import org.skife.config.ConfigurationObjectFactory;
 
 import com.google.inject.AbstractModule;
@@ -39,7 +42,6 @@ import com.ning.billing.invoice.notification.NextBillingDateNotifier;
 import com.ning.billing.invoice.notification.NextBillingDatePoster;
 import com.ning.billing.util.glue.GlobalLockerModule;
 
-
 public class InvoiceModule extends AbstractModule {
     protected void installInvoiceDao() {
         bind(InvoiceDao.class).to(DefaultInvoiceDao.class).asEagerSingleton();
@@ -56,6 +58,9 @@ public class InvoiceModule extends AbstractModule {
     protected void installConfig() {
         final InvoiceConfig config = new ConfigurationObjectFactory(System.getProperties()).build(InvoiceConfig.class);
         bind(InvoiceConfig.class).toInstance(config);
+
+        final EmailConfig emailConfig = new ConfigurationObjectFactory(System.getProperties()).build(EmailConfig.class);
+        bind(EmailConfig.class).toInstance(emailConfig);
     }
 
     protected void installInvoiceService() {
@@ -66,9 +71,10 @@ public class InvoiceModule extends AbstractModule {
     	bind(InvoiceMigrationApi.class).to(DefaultInvoiceMigrationApi.class).asEagerSingleton();
 	}
 
-    protected void installNotifier() {
+    protected void installNotifiers() {
         bind(NextBillingDateNotifier.class).to(DefaultNextBillingDateNotifier.class).asEagerSingleton();
         bind(NextBillingDatePoster.class).to(DefaultNextBillingDatePoster.class).asEagerSingleton();
+        bind(InvoiceNotifier.class).to(EmailInvoiceNotifier.class).asEagerSingleton();
     }
 
     protected void installGlobalLocker() {
@@ -82,11 +88,11 @@ public class InvoiceModule extends AbstractModule {
     @Override
     protected void configure() {
         installInvoiceService();
-        installNotifier();
+        installConfig();
+        installNotifiers();
 
         installInvoiceListener();
         bind(InvoiceGenerator.class).to(DefaultInvoiceGenerator.class).asEagerSingleton();
-        installConfig();
         installInvoiceDao();
         installInvoiceUserApi();
         installInvoicePaymentApi();
diff --git a/invoice/src/main/java/com/ning/billing/invoice/InvoiceDispatcher.java b/invoice/src/main/java/com/ning/billing/invoice/InvoiceDispatcher.java
index 043c670..820c57d 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/InvoiceDispatcher.java
@@ -21,6 +21,7 @@ import java.util.List;
 import java.util.SortedSet;
 import java.util.UUID;
 
+import com.ning.billing.invoice.api.InvoiceNotifier;
 import com.ning.billing.util.bus.Bus;
 import com.ning.billing.util.bus.Bus.EventBusException;
 import com.ning.billing.util.bus.BusEvent;
@@ -59,6 +60,7 @@ public class InvoiceDispatcher {
     private final EntitlementBillingApi entitlementBillingApi;
     private final AccountUserApi accountUserApi;
     private final InvoiceDao invoiceDao;
+    private final InvoiceNotifier invoiceNotifier;
     private final GlobalLocker locker;
     private final Bus eventBus;
     private final Clock clock;
@@ -69,6 +71,7 @@ public class InvoiceDispatcher {
     public InvoiceDispatcher(final InvoiceGenerator generator, final AccountUserApi accountUserApi,
                              final EntitlementBillingApi entitlementBillingApi,
                              final InvoiceDao invoiceDao,
+                             final InvoiceNotifier invoiceNotifier,
                              final GlobalLocker locker,
                              final Bus eventBus,
                              final Clock clock) {
@@ -76,6 +79,7 @@ public class InvoiceDispatcher {
         this.entitlementBillingApi = entitlementBillingApi;
         this.accountUserApi = accountUserApi;
         this.invoiceDao = invoiceDao;
+        this.invoiceNotifier = invoiceNotifier;
         this.locker = locker;
         this.eventBus = eventBus;
         this.clock = clock;
@@ -175,6 +179,11 @@ public class InvoiceDispatcher {
                 invoiceDao.create(invoice, context);
             }
         }
+
+        if (account.isNotifiedForInvoices()) {
+
+        }
+
         return invoice;
     }
 
diff --git a/invoice/src/main/java/com/ning/billing/invoice/notification/EmailInvoiceNotifier.java b/invoice/src/main/java/com/ning/billing/invoice/notification/EmailInvoiceNotifier.java
new file mode 100644
index 0000000..aee0f66
--- /dev/null
+++ b/invoice/src/main/java/com/ning/billing/invoice/notification/EmailInvoiceNotifier.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2010-2011 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.invoice.notification;
+
+import com.google.inject.Inject;
+import com.ning.billing.ErrorCode;
+import com.ning.billing.account.api.Account;
+import com.ning.billing.account.api.AccountEmail;
+import com.ning.billing.account.api.AccountUserApi;
+import com.ning.billing.invoice.api.Invoice;
+import com.ning.billing.invoice.api.InvoiceNotifier;
+import com.ning.billing.util.email.DefaultEmailSender;
+import com.ning.billing.util.email.EmailApiException;
+import com.ning.billing.util.email.EmailConfig;
+import com.ning.billing.util.email.EmailSender;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class EmailInvoiceNotifier implements InvoiceNotifier {
+    private final AccountUserApi accountUserApi;
+    private final EmailConfig config;
+
+    @Inject
+    public EmailInvoiceNotifier(AccountUserApi accountUserApi, EmailConfig config) {
+        this.accountUserApi = accountUserApi;
+        this.config = config;
+    }
+
+    @Override
+    public void notify(Account account, Invoice invoice) throws EmailApiException {
+        List<String> to = new ArrayList<String>();
+        to.add(account.getEmail());
+
+        List<AccountEmail> accountEmailList = accountUserApi.getEmails(account.getId());
+        List<String> cc = new ArrayList<String>();
+        for (AccountEmail email : accountEmailList) {
+            cc.add(email.getEmail());
+        }
+
+        // TODO: get html body from template
+        String htmlBody = "";
+
+        // TODO: get subject
+        String subject = "";
+
+        EmailSender sender = new DefaultEmailSender(config);
+        try {
+            sender.sendSecureEmail(to, cc, subject, htmlBody);
+        } catch (IOException e) {
+            throw new EmailApiException(e, ErrorCode.EMAIL_SENDING_FAILED);
+        }
+    }
+}
diff --git a/invoice/src/main/java/com/ning/billing/invoice/notification/NullInvoiceNotifier.java b/invoice/src/main/java/com/ning/billing/invoice/notification/NullInvoiceNotifier.java
new file mode 100644
index 0000000..460e779
--- /dev/null
+++ b/invoice/src/main/java/com/ning/billing/invoice/notification/NullInvoiceNotifier.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2010-2011 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.invoice.notification;
+
+import com.ning.billing.account.api.Account;
+import com.ning.billing.invoice.api.Invoice;
+import com.ning.billing.invoice.api.InvoiceNotifier;
+
+public class NullInvoiceNotifier implements InvoiceNotifier {
+    @Override
+    public void notify(Account account, Invoice invoice) {
+        // deliberate no-op
+    }
+}
\ No newline at end of file
diff --git a/invoice/src/test/java/com/ning/billing/invoice/api/migration/MockModuleNoEntitlement.java b/invoice/src/test/java/com/ning/billing/invoice/api/migration/MockModuleNoEntitlement.java
index 300313c..e317734 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/api/migration/MockModuleNoEntitlement.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/api/migration/MockModuleNoEntitlement.java
@@ -19,11 +19,13 @@ package com.ning.billing.invoice.api.migration;
 import com.ning.billing.entitlement.api.billing.EntitlementBillingApi;
 import com.ning.billing.entitlement.engine.dao.EntitlementDao;
 import com.ning.billing.invoice.MockModule;
+import com.ning.billing.invoice.api.InvoiceNotifier;
 import com.ning.billing.invoice.glue.InvoiceModule;
 import com.ning.billing.invoice.notification.DefaultNextBillingDateNotifier;
 import com.ning.billing.invoice.notification.DefaultNextBillingDatePoster;
 import com.ning.billing.invoice.notification.NextBillingDateNotifier;
 import com.ning.billing.invoice.notification.NextBillingDatePoster;
+import com.ning.billing.invoice.notification.NullInvoiceNotifier;
 import com.ning.billing.mock.BrainDeadProxyFactory;
 import com.ning.billing.mock.BrainDeadProxyFactory.ZombieControl;
 
@@ -44,11 +46,12 @@ public class MockModuleNoEntitlement extends MockModule {
 		install(new InvoiceModule(){
 
 			@Override
-			protected void installNotifier() {
+			protected void installNotifiers() {
 				 bind(NextBillingDateNotifier.class).toInstance(BrainDeadProxyFactory.createBrainDeadProxyFor(NextBillingDateNotifier.class));
 				 NextBillingDatePoster poster = BrainDeadProxyFactory.createBrainDeadProxyFor(NextBillingDatePoster.class);
 				 ((ZombieControl)poster).addResult("insertNextBillingNotification",BrainDeadProxyFactory.ZOMBIE_VOID);
 			     bind(NextBillingDatePoster.class).toInstance(poster);
+                bind(InvoiceNotifier.class).to(NullInvoiceNotifier.class).asEagerSingleton();
 			}
 			
 			
diff --git a/invoice/src/test/java/com/ning/billing/invoice/api/migration/TestDefaultInvoiceMigrationApi.java b/invoice/src/test/java/com/ning/billing/invoice/api/migration/TestDefaultInvoiceMigrationApi.java
index 3aee83d..58d94dd 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/api/migration/TestDefaultInvoiceMigrationApi.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/api/migration/TestDefaultInvoiceMigrationApi.java
@@ -23,6 +23,8 @@ import java.util.SortedSet;
 import java.util.TreeSet;
 import java.util.UUID;
 
+import com.ning.billing.invoice.api.InvoiceNotifier;
+import com.ning.billing.invoice.notification.NullInvoiceNotifier;
 import com.ning.billing.util.callcontext.CallContext;
 import com.ning.billing.util.callcontext.CallOrigin;
 import com.ning.billing.util.callcontext.UserType;
@@ -169,6 +171,7 @@ public class TestDefaultInvoiceMigrationApi {
 		((ZombieControl)accountUserApi).addResult("getAccountById", account);
 		((ZombieControl)account).addResult("getCurrency", Currency.USD);
 		((ZombieControl)account).addResult("getId", accountId);
+        ((ZombieControl)account).addResult("isNotifiedForInvoices", true);
 
 		Subscription subscription =  BrainDeadProxyFactory.createBrainDeadProxyFor(Subscription.class);
         ((ZombieControl)subscription).addResult("getId", subscriptionId);
@@ -185,7 +188,10 @@ public class TestDefaultInvoiceMigrationApi {
 
 		EntitlementBillingApi entitlementBillingApi = BrainDeadProxyFactory.createBrainDeadProxyFor(EntitlementBillingApi.class);
         ((ZombieControl)entitlementBillingApi).addResult("getBillingEventsForAccountAndUpdateAccountBCD", events);
-		InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountUserApi, entitlementBillingApi, invoiceDao, locker, busService.getBus(), clock);
+
+        InvoiceNotifier invoiceNotifier = new NullInvoiceNotifier();
+		InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountUserApi, entitlementBillingApi,
+                                                             invoiceDao, invoiceNotifier, locker, busService.getBus(), clock);
 
         CallContext context = new DefaultCallContextFactory(clock).createCallContext("Migration test", CallOrigin.TEST, UserType.TEST);
 		Invoice invoice = dispatcher.processAccount(accountId, date_regular, true, context);
diff --git a/invoice/src/test/java/com/ning/billing/invoice/dao/InvoiceDaoTests.java b/invoice/src/test/java/com/ning/billing/invoice/dao/InvoiceDaoTests.java
index a3b8888..b1c038f 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/dao/InvoiceDaoTests.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/dao/InvoiceDaoTests.java
@@ -57,7 +57,7 @@ import static org.testng.Assert.assertNotNull;
 import static org.testng.Assert.assertNull;
 import static org.testng.Assert.assertTrue;
 
-@Test(groups = {"invoicing", "invoicing-invoiceDao"})
+@Test(groups = {"slow", "invoicing", "invoicing-invoiceDao"})
 public class InvoiceDaoTests extends InvoiceDaoTestBase {
     @Test
     public void testCreationAndRetrievalByAccount() {
@@ -406,8 +406,7 @@ public class InvoiceDaoTests extends InvoiceDaoTestBase {
         MockPlanPhase phase1 = new MockPlanPhase(recurringPrice, null, BillingPeriod.MONTHLY, PhaseType.TRIAL);
         MockPlan plan1 = new MockPlan(phase1);
 
-        Subscription subscription = BrainDeadProxyFactory.createBrainDeadProxyFor(Subscription.class);
-        ((ZombieControl) subscription).addResult("getId", UUID.randomUUID());
+        Subscription subscription = getZombieSubscription();
 
         DateTime effectiveDate1 = new DateTime(2011, 2, 1, 0, 0, 0, 0);
         BillingEvent event1 = new DefaultBillingEvent(null, subscription, effectiveDate1, plan1, phase1, null,
@@ -457,8 +456,7 @@ public class InvoiceDaoTests extends InvoiceDaoTestBase {
         MockPlanPhase phase = new MockPlanPhase(recurringPrice, null);
         MockPlan plan = new MockPlan(phase);
 
-        Subscription subscription = BrainDeadProxyFactory.createBrainDeadProxyFor(Subscription.class);
-        ((ZombieControl) subscription).addResult("getId", UUID.randomUUID());
+        Subscription subscription = getZombieSubscription();
         DateTime effectiveDate = buildDateTime(2011, 1, 1);
 
         BillingEvent event = new DefaultBillingEvent(null, subscription, effectiveDate, plan, phase, null,
@@ -475,6 +473,13 @@ public class InvoiceDaoTests extends InvoiceDaoTestBase {
         assertEquals(invoice.getTotalAmount().compareTo(ZERO), 0);
     }
 
+    private Subscription getZombieSubscription() {
+        Subscription subscription = BrainDeadProxyFactory.createBrainDeadProxyFor(Subscription.class);
+        ((ZombieControl) subscription).addResult("getId", UUID.randomUUID());
+        ((ZombieControl) subscription).addResult("getBundleId", UUID.randomUUID());
+        return subscription;
+    }
+
     @Test
     public void testInvoiceForFreeTrialWithRecurringDiscount() throws InvoiceApiException, CatalogApiException {
         Currency currency = Currency.USD;
@@ -490,8 +495,7 @@ public class InvoiceDaoTests extends InvoiceDaoTestBase {
 
         MockPlan plan = new MockPlan();
 
-        Subscription subscription = BrainDeadProxyFactory.createBrainDeadProxyFor(Subscription.class);
-        ((ZombieControl) subscription).addResult("getId", UUID.randomUUID());
+        Subscription subscription = getZombieSubscription();
         DateTime effectiveDate1 = buildDateTime(2011, 1, 1);
 
         BillingEvent event1 = new DefaultBillingEvent(null, subscription, effectiveDate1, plan, phase1, fixedPrice.getPrice(currency),
@@ -550,8 +554,7 @@ public class InvoiceDaoTests extends InvoiceDaoTestBase {
 
         MockPlan plan = new MockPlan();
 
-        Subscription subscription = BrainDeadProxyFactory.createBrainDeadProxyFor(Subscription.class);
-        ((ZombieControl) subscription).addResult("getId", UUID.randomUUID());
+        Subscription subscription = getZombieSubscription();
         DateTime effectiveDate1 = buildDateTime(2011, 1, 1);
 
         BillingEvent event1 = new DefaultBillingEvent(null, subscription, effectiveDate1, plan, phase1,
@@ -586,8 +589,7 @@ public class InvoiceDaoTests extends InvoiceDaoTestBase {
         DateTime targetDate1 = DateTime.now().plusMonths(1);
         DateTime targetDate2 = DateTime.now().plusMonths(2);
 
-        Subscription subscription = BrainDeadProxyFactory.createBrainDeadProxyFor(Subscription.class);
-        ((ZombieControl) subscription).addResult("getId", UUID.randomUUID());
+        Subscription subscription = getZombieSubscription();
 
         Plan plan = BrainDeadProxyFactory.createBrainDeadProxyFor(Plan.class);
         ((ZombieControl) plan).addResult("getName", "plan");
@@ -626,8 +628,7 @@ public class InvoiceDaoTests extends InvoiceDaoTestBase {
 
     @Test
     public void testAddingWrittenOffTag() throws InvoiceApiException {
-        Subscription subscription = BrainDeadProxyFactory.createBrainDeadProxyFor(Subscription.class);
-        ((ZombieControl) subscription).addResult("getId", UUID.randomUUID());
+        Subscription subscription = getZombieSubscription();
 
         Plan plan = BrainDeadProxyFactory.createBrainDeadProxyFor(Plan.class);
         ((ZombieControl) plan).addResult("getName", "plan");
@@ -657,8 +658,7 @@ public class InvoiceDaoTests extends InvoiceDaoTestBase {
 
     @Test
     public void testRemoveWrittenOffTag() throws InvoiceApiException {
-        Subscription subscription = BrainDeadProxyFactory.createBrainDeadProxyFor(Subscription.class);
-        ((ZombieControl) subscription).addResult("getId", UUID.randomUUID());
+        Subscription subscription = getZombieSubscription();
 
         Plan plan = BrainDeadProxyFactory.createBrainDeadProxyFor(Plan.class);
         ((ZombieControl) plan).addResult("getName", "plan");
diff --git a/invoice/src/test/java/com/ning/billing/invoice/glue/InvoiceModuleWithEmbeddedDb.java b/invoice/src/test/java/com/ning/billing/invoice/glue/InvoiceModuleWithEmbeddedDb.java
index 368b74f..cc5ad02 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/glue/InvoiceModuleWithEmbeddedDb.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/glue/InvoiceModuleWithEmbeddedDb.java
@@ -21,6 +21,8 @@ import static org.testng.Assert.assertNotNull;
 import java.io.IOException;
 import java.net.URL;
 
+import com.ning.billing.invoice.api.InvoiceNotifier;
+import com.ning.billing.invoice.notification.NullInvoiceNotifier;
 import org.skife.jdbi.v2.IDBI;
 
 import com.ning.billing.account.api.AccountUserApi;
@@ -80,9 +82,10 @@ public class InvoiceModuleWithEmbeddedDb extends InvoiceModule {
     }
 
     @Override
-    protected void installNotifier() {
+    protected void installNotifiers() {
         bind(NextBillingDateNotifier.class).to(MockNextBillingDateNotifier.class).asEagerSingleton();
         bind(NextBillingDatePoster.class).to(MockNextBillingDatePoster.class).asEagerSingleton();
+        bind(InvoiceNotifier.class).to(NullInvoiceNotifier.class).asEagerSingleton();
     }
 
     @Override
diff --git a/invoice/src/test/java/com/ning/billing/invoice/glue/InvoiceModuleWithMocks.java b/invoice/src/test/java/com/ning/billing/invoice/glue/InvoiceModuleWithMocks.java
index fe4feb4..0206bb2 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/glue/InvoiceModuleWithMocks.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/glue/InvoiceModuleWithMocks.java
@@ -16,8 +16,10 @@
 
 package com.ning.billing.invoice.glue;
 
+import com.ning.billing.invoice.api.InvoiceNotifier;
 import com.ning.billing.invoice.dao.InvoiceDao;
 import com.ning.billing.invoice.dao.MockInvoiceDao;
+import com.ning.billing.invoice.notification.NullInvoiceNotifier;
 import com.ning.billing.util.globallocker.GlobalLocker;
 import com.ning.billing.util.globallocker.MockGlobalLocker;
 import com.ning.billing.util.glue.CallContextModule;
@@ -45,8 +47,8 @@ public class InvoiceModuleWithMocks extends InvoiceModule {
     }
 
     @Override
-    protected void installNotifier() {
-
+    protected void installNotifiers() {
+        bind(InvoiceNotifier.class).to(NullInvoiceNotifier.class).asEagerSingleton();
     }
 
     @Override
diff --git a/invoice/src/test/java/com/ning/billing/invoice/notification/TestNextBillingDateNotifier.java b/invoice/src/test/java/com/ning/billing/invoice/notification/TestNextBillingDateNotifier.java
index e70626f..9c02629 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/notification/TestNextBillingDateNotifier.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/notification/TestNextBillingDateNotifier.java
@@ -24,6 +24,7 @@ import java.sql.SQLException;
 import java.util.UUID;
 import java.util.concurrent.Callable;
 
+import com.ning.billing.invoice.api.InvoiceNotifier;
 import org.apache.commons.io.IOUtils;
 import org.joda.time.DateTime;
 import org.skife.config.ConfigurationObjectFactory;
@@ -125,6 +126,7 @@ public class TestNextBillingDateNotifier {
                 bind(CallContextFactory.class).to(DefaultCallContextFactory.class).asEagerSingleton();
                 bind(Bus.class).to(InMemoryBus.class).asEagerSingleton();
                 bind(NotificationQueueService.class).to(DefaultNotificationQueueService.class).asEagerSingleton();
+                bind(InvoiceNotifier.class).to(NullInvoiceNotifier.class).asEagerSingleton();
                 final InvoiceConfig invoiceConfig = new ConfigurationObjectFactory(System.getProperties()).build(InvoiceConfig.class);
                 bind(InvoiceConfig.class).toInstance(invoiceConfig);
                 final CatalogConfig catalogConfig = new ConfigurationObjectFactory(System.getProperties()).build(CatalogConfig.class);
diff --git a/invoice/src/test/java/com/ning/billing/invoice/TestInvoiceDispatcher.java b/invoice/src/test/java/com/ning/billing/invoice/TestInvoiceDispatcher.java
index 45f1db4..6d8ec75 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/TestInvoiceDispatcher.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/TestInvoiceDispatcher.java
@@ -23,6 +23,8 @@ import java.util.SortedSet;
 import java.util.TreeSet;
 import java.util.UUID;
 
+import com.ning.billing.invoice.api.InvoiceNotifier;
+import com.ning.billing.invoice.notification.NullInvoiceNotifier;
 import org.apache.commons.io.IOUtils;
 import org.joda.time.DateTime;
 import org.slf4j.Logger;
@@ -135,6 +137,7 @@ public class TestInvoiceDispatcher {
 		((ZombieControl)accountUserApi).addResult("getAccountById", account);
 		((ZombieControl)account).addResult("getCurrency", Currency.USD);
 		((ZombieControl)account).addResult("getId", accountId);
+        ((ZombieControl)account).addResult(("isNotifiedForInvoices"), true);
 
 		Subscription subscription =  BrainDeadProxyFactory.createBrainDeadProxyFor(Subscription.class);
         ((ZombieControl)subscription).addResult("getId", subscriptionId);
@@ -154,7 +157,9 @@ public class TestInvoiceDispatcher {
 
 		DateTime target = new DateTime();
 
-		InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountUserApi, entitlementBillingApi, invoiceDao, locker, busService.getBus(), clock);
+        InvoiceNotifier invoiceNotifier = new NullInvoiceNotifier();
+		InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountUserApi, entitlementBillingApi, invoiceDao,
+                                                             invoiceNotifier, locker, busService.getBus(), clock);
 
 		Invoice invoice = dispatcher.processAccount(accountId, target, true, context);
 		Assert.assertNotNull(invoice);

pom.xml 17(+16 -1)

diff --git a/pom.xml b/pom.xml
index f7ab718..fef0a80 100644
--- a/pom.xml
+++ b/pom.xml
@@ -243,9 +243,19 @@
                 <scope>test</scope>
             </dependency>
             <dependency>
+                <groupId>org.apache.commons</groupId>
+                <artifactId>commons-email</artifactId>
+                <version>1.2</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.directory.studio</groupId>
+                <artifactId>org.apache.commons.io</artifactId>
+                <version>2.1</version>
+            </dependency>
+            <dependency>
                 <groupId>commons-io</groupId>
                 <artifactId>commons-io</artifactId>
-                <version>2.0.1</version>
+                <version>2.0</version>
             </dependency>
             <dependency>
                 <groupId>commons-lang</groupId>
@@ -317,6 +327,11 @@
                 <scope>test</scope>
             </dependency>
             <dependency>
+                <groupId>com.samskivert</groupId>
+                <artifactId>jmustache</artifactId>
+                <version>1.5</version>
+            </dependency>
+            <dependency>
                 <groupId>com.jayway.awaitility</groupId>
                 <artifactId>awaitility</artifactId>
                 <version>1.3.3</version>

util/pom.xml 16(+16 -0)

diff --git a/util/pom.xml b/util/pom.xml
index e0d256c..d9f7924 100644
--- a/util/pom.xml
+++ b/util/pom.xml
@@ -65,6 +65,14 @@
             <scope>test</scope>
         </dependency>
         <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-email</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.directory.studio</groupId>
+            <artifactId>org.apache.commons.io</artifactId>
+        </dependency>
+        <dependency>
             <groupId>commons-io</groupId>
             <artifactId>commons-io</artifactId>
             <scope>test</scope>
@@ -83,6 +91,14 @@
             <artifactId>awaitility</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>com.samskivert</groupId>
+            <artifactId>jmustache</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+        </dependency>
     </dependencies>
     <build>
         <plugins>
diff --git a/util/src/main/java/com/ning/billing/util/email/DefaultEmailSender.java b/util/src/main/java/com/ning/billing/util/email/DefaultEmailSender.java
new file mode 100644
index 0000000..103a87b
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/email/DefaultEmailSender.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2010-2011 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.util.email;
+
+import com.google.inject.Inject;
+import com.ning.billing.ErrorCode;
+import org.apache.commons.mail.EmailException;
+import org.apache.commons.mail.HtmlEmail;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+public class DefaultEmailSender implements EmailSender {
+    private final Logger log = LoggerFactory.getLogger(EmailSender.class);
+    private final EmailConfig config;
+
+    @Inject
+    public DefaultEmailSender(EmailConfig emailConfig) {
+        this.config = emailConfig;
+    }
+
+    @Override
+    public void sendSecureEmail(List<String> to, List<String> cc, String subject, String htmlBody) throws EmailApiException {
+        HtmlEmail email;
+        try {
+            email = new HtmlEmail();
+
+            email.setSmtpPort(config.getSmtpPort());
+            email.setAuthentication(config.getSmtpUserName(), config.getSmtpPassword());
+            email.setHostName(config.getSmtpServerName());
+            email.setFrom(config.getSmtpUserName());
+            email.setSubject(subject);
+            email.setHtmlMsg(htmlBody);
+
+            if (to != null) {
+                for (String recipient : to) {
+                    email.addTo(recipient);
+                }
+            }
+
+            if (cc != null) {
+                for (String recipient : cc) {
+                    email.addCc(recipient);
+                }
+            }
+
+            email.setSSL(true);
+            email.send();
+        } catch (EmailException ee)  {
+            log.warn("Failed to send e-mail", ee);
+        }
+    }
+}
diff --git a/util/src/main/java/com/ning/billing/util/email/EmailApiException.java b/util/src/main/java/com/ning/billing/util/email/EmailApiException.java
new file mode 100644
index 0000000..9b5d6ba
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/email/EmailApiException.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2010-2011 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.util.email;
+
+import com.ning.billing.BillingExceptionBase;
+import com.ning.billing.ErrorCode;
+
+public class EmailApiException extends BillingExceptionBase {
+    private static final long serialVersionUID = 1L;
+
+    public EmailApiException(Throwable cause, int code, final String msg) {
+        super(cause, code, msg);
+    }
+
+    public EmailApiException(Throwable cause, ErrorCode code, final Object... args) {
+        super(cause, code, args);
+    }
+
+    public EmailApiException(ErrorCode code, final Object... args) {
+        super(code, args);
+    }
+}
diff --git a/util/src/main/java/com/ning/billing/util/email/EmailConfig.java b/util/src/main/java/com/ning/billing/util/email/EmailConfig.java
new file mode 100644
index 0000000..427f9df
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/email/EmailConfig.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2010-2011 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.util.email;
+
+import com.ning.billing.config.KillbillConfig;
+import org.skife.config.Config;
+import org.skife.config.Default;
+
+import java.util.Locale;
+
+public interface EmailConfig extends KillbillConfig {
+    @Config("mail.smtp.host")
+    @Default("smtp.gmail.com")
+    public String getSmtpServerName();
+
+    @Config("mail.smtp.port")
+    @Default("465")
+    public int getSmtpPort();
+
+    @Config("mail.smtp.user")
+    @Default("killbill.ning@gmail.com")
+    public String getSmtpUserName();
+
+    @Config("mail.smtp.password")
+    @Default("killbill@ning!")
+    public String getSmtpPassword();
+
+    @Config("email.default.locale")
+    @Default("en_US")
+    public String getDefaultLocale();
+}
diff --git a/util/src/main/java/com/ning/billing/util/email/EmailSender.java b/util/src/main/java/com/ning/billing/util/email/EmailSender.java
new file mode 100644
index 0000000..198a2eb
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/email/EmailSender.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2010-2011 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.util.email;
+
+import java.io.IOException;
+import java.util.List;
+
+public interface EmailSender {
+    public void sendSecureEmail(List<String> to, List<String> cc, String subject, String htmlBody) throws IOException, EmailApiException;
+}
diff --git a/util/src/main/java/com/ning/billing/util/email/formatters/DefaultInvoiceFormatter.java b/util/src/main/java/com/ning/billing/util/email/formatters/DefaultInvoiceFormatter.java
new file mode 100644
index 0000000..80d11a3
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/email/formatters/DefaultInvoiceFormatter.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright 2010-2011 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.
+ */
+
+/*
+ * Copyright 2010-2011 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.util.email.formatters;
+
+import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.invoice.api.Invoice;
+import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.invoice.api.InvoicePayment;
+import com.ning.billing.util.callcontext.CallContext;
+import com.ning.billing.util.customfield.CustomField;
+import com.ning.billing.util.email.EmailConfig;
+import com.ning.billing.util.tag.Tag;
+import com.ning.billing.util.tag.TagDefinition;
+import org.joda.time.DateTime;
+import org.joda.time.format.DateTimeFormat;
+import org.joda.time.format.DateTimeFormatter;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.UUID;
+
+public class DefaultInvoiceFormatter implements InvoiceFormatter {
+    private final EmailConfig config;
+    private final Invoice invoice;
+    private final DateTimeFormatter dateFormatter;
+    private final Locale locale;
+
+    public DefaultInvoiceFormatter(EmailConfig config, Invoice invoice, Locale locale) {
+        this.config = config;
+        this.invoice = invoice;
+        dateFormatter = DateTimeFormat.mediumDate().withLocale(locale);
+        this.locale = locale;
+    }
+
+    @Override
+    public Integer getInvoiceNumber() {
+        return invoice.getInvoiceNumber();
+    }
+
+    @Override
+    public List<InvoiceItem> getInvoiceItems() {
+        List<InvoiceItem> formatters = new ArrayList<InvoiceItem>();
+        for (InvoiceItem item : invoice.getInvoiceItems()) {
+            formatters.add(new DefaultInvoiceItemFormatter(config, item, dateFormatter, locale));
+        }
+        return formatters;
+    }
+
+    @Override
+    public boolean addInvoiceItem(InvoiceItem item) {
+        return invoice.addInvoiceItem(item);
+    }
+
+    @Override
+    public boolean addInvoiceItems(List<InvoiceItem> items) {
+        return invoice.addInvoiceItems(items);
+    }
+
+    @Override
+    public <T extends InvoiceItem> List<InvoiceItem> getInvoiceItems(Class<T> clazz) {
+        return invoice.getInvoiceItems(clazz);
+    }
+
+    @Override
+    public int getNumberOfItems() {
+        return invoice.getNumberOfItems();
+    }
+
+    @Override
+    public boolean addPayment(InvoicePayment payment) {
+        return invoice.addPayment(payment);
+    }
+
+    @Override
+    public boolean addPayments(List<InvoicePayment> payments) {
+        return invoice.addPayments(payments);
+    }
+
+    @Override
+    public List<InvoicePayment> getPayments() {
+        return invoice.getPayments();
+    }
+
+    @Override
+    public int getNumberOfPayments() {
+        return invoice.getNumberOfPayments();
+    }
+
+    @Override
+    public UUID getAccountId() {
+        return invoice.getAccountId();
+    }
+
+    @Override
+    public BigDecimal getTotalAmount() {
+        return invoice.getTotalAmount();
+    }
+
+    @Override
+    public BigDecimal getBalance() {
+        return invoice.getBalance();
+    }
+
+    @Override
+    public boolean isDueForPayment(DateTime targetDate, int numberOfDays) {
+        return invoice.isDueForPayment(targetDate, numberOfDays);
+    }
+
+    @Override
+    public boolean isMigrationInvoice() {
+        return invoice.isMigrationInvoice();
+    }
+
+    @Override
+    public DateTime getInvoiceDate() {
+        return invoice.getInvoiceDate();
+    }
+
+    @Override
+    public DateTime getTargetDate() {
+        return invoice.getTargetDate();
+    }
+
+    @Override
+    public Currency getCurrency() {
+        return invoice.getCurrency();
+    }
+
+    @Override
+    public DateTime getLastPaymentAttempt() {
+        return invoice.getLastPaymentAttempt();
+    }
+
+    @Override
+    public BigDecimal getAmountPaid() {
+        return invoice.getAmountPaid();
+    }
+
+    @Override
+    public String getFormattedInvoiceDate() {
+        return invoice.getInvoiceDate().toString(dateFormatter);
+    }
+
+    @Override
+    public String getFieldValue(String fieldName) {
+        return invoice.getFieldValue(fieldName);
+    }
+
+    @Override
+    public void setFieldValue(String fieldName, String fieldValue) {
+        invoice.setFieldValue(fieldName, fieldValue);
+    }
+
+    @Override
+    public void saveFieldValue(String fieldName, String fieldValue, CallContext context) {
+        invoice.saveFieldValue(fieldName, fieldValue, context);
+    }
+
+    @Override
+    public List<CustomField> getFieldList() {
+        return invoice.getFieldList();
+    }
+
+    @Override
+    public void setFields(List<CustomField> fields) {
+        invoice.setFields(fields);
+    }
+
+    @Override
+    public void saveFields(List<CustomField> fields, CallContext context) {
+        invoice.saveFields(fields, context);
+    }
+
+    @Override
+    public void clearFields() {
+        invoice.clearFields();
+    }
+
+    @Override
+    public void clearPersistedFields(CallContext context) {
+        invoice.clearPersistedFields(context);
+    }
+
+    @Override
+    public String getObjectName() {
+        return invoice.getObjectName();
+    }
+
+    @Override
+    public UUID getId() {
+        return invoice.getId();
+    }
+
+    @Override
+    public String getCreatedBy() {
+        return invoice.getCreatedBy();
+    }
+
+    @Override
+    public DateTime getCreatedDate() {
+        return invoice.getCreatedDate();
+    }
+
+    @Override
+    public List<Tag> getTagList() {
+        return invoice.getTagList();
+    }
+
+    @Override
+    public boolean hasTag(String tagName) {
+        return invoice.hasTag(tagName);
+    }
+
+    @Override
+    public void addTag(TagDefinition definition) {
+        invoice.addTag(definition);
+    }
+
+    @Override
+    public void addTags(List<Tag> tags) {
+        invoice.addTags(tags);
+    }
+
+    @Override
+    public void clearTags() {
+        invoice.clearTags();
+    }
+
+    @Override
+    public void removeTag(TagDefinition definition) {
+        invoice.removeTag(definition);
+    }
+
+    @Override
+    public boolean generateInvoice() {
+        return invoice.generateInvoice();
+    }
+
+    @Override
+    public boolean processPayment() {
+        return invoice.processPayment();
+    }
+}
diff --git a/util/src/main/java/com/ning/billing/util/email/formatters/DefaultInvoiceItemFormatter.java b/util/src/main/java/com/ning/billing/util/email/formatters/DefaultInvoiceItemFormatter.java
new file mode 100644
index 0000000..1b24cf1
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/email/formatters/DefaultInvoiceItemFormatter.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2010-2011 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.util.email.formatters;
+
+import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.util.email.EmailConfig;
+import com.ning.billing.util.email.translation.CatalogTranslation;
+import com.ning.billing.util.email.translation.DefaultCatalogTranslation;
+import org.joda.time.DateTime;
+import org.joda.time.format.DateTimeFormatter;
+
+import java.math.BigDecimal;
+import java.util.Locale;
+import java.util.UUID;
+
+public class DefaultInvoiceItemFormatter implements InvoiceItemFormatter {
+    private final CatalogTranslation catalogTranslation;
+
+    private final InvoiceItem item;
+    private final DateTimeFormatter dateFormatter;
+    private final Locale locale;
+
+    public DefaultInvoiceItemFormatter(EmailConfig config, InvoiceItem item, DateTimeFormatter dateFormatter, Locale locale) {
+        this.item = item;
+        this.dateFormatter = dateFormatter;
+        this.locale = locale;
+
+        this.catalogTranslation = new DefaultCatalogTranslation(config);
+    }
+
+    @Override
+    public BigDecimal getAmount() {
+        return item.getAmount();
+    }
+
+    @Override
+    public Currency getCurrency() {
+        return item.getCurrency();
+    }
+
+    @Override
+    public InvoiceItem asCredit() {
+        return item.asCredit();
+    }
+
+    @Override
+    public String getDescription() {
+        return item.getDescription();
+    }
+
+    @Override
+    public DateTime getStartDate() {
+        return item.getStartDate();
+    }
+
+    @Override
+    public DateTime getEndDate() {
+        return item.getEndDate();
+    }
+
+    @Override
+    public String getFormattedStartDate() {
+        return item.getStartDate().toString(dateFormatter);
+    }
+
+    @Override
+    public String getFormattedEndDate() {
+        return item.getEndDate().toString(dateFormatter);
+    }
+
+    @Override
+    public UUID getInvoiceId() {
+        return item.getInvoiceId();
+    }
+
+    @Override
+    public UUID getAccountId() {
+        return item.getAccountId();
+    }
+
+    @Override
+    public UUID getBundleId() {
+        return item.getBundleId();
+    }
+
+    @Override
+    public UUID getSubscriptionId() {
+        return item.getSubscriptionId();
+    }
+
+    @Override
+    public String getPlanName() {
+        return catalogTranslation.getTranslation(locale, item.getPlanName());
+    }
+
+    @Override
+    public String getPhaseName() {
+        return catalogTranslation.getTranslation(locale, item.getPhaseName());
+    }
+
+    @Override
+    public int compareTo(InvoiceItem invoiceItem) {
+        return item.compareTo(invoiceItem);
+    }
+
+    @Override
+    public UUID getId() {
+        return item.getId();
+    }
+
+    @Override
+    public String getCreatedBy() {
+        return item.getCreatedBy();
+    }
+
+    @Override
+    public DateTime getCreatedDate() {
+        return item.getCreatedDate();
+    }
+}
\ No newline at end of file
diff --git a/util/src/main/java/com/ning/billing/util/email/formatters/InvoiceFormatter.java b/util/src/main/java/com/ning/billing/util/email/formatters/InvoiceFormatter.java
new file mode 100644
index 0000000..5107828
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/email/formatters/InvoiceFormatter.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2010-2011 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.util.email.formatters;
+
+import com.ning.billing.invoice.api.Invoice;
+
+public interface InvoiceFormatter extends Invoice {
+    public String getFormattedInvoiceDate();
+}
diff --git a/util/src/main/java/com/ning/billing/util/email/formatters/InvoiceItemFormatter.java b/util/src/main/java/com/ning/billing/util/email/formatters/InvoiceItemFormatter.java
new file mode 100644
index 0000000..f811b78
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/email/formatters/InvoiceItemFormatter.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2010-2011 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.util.email.formatters;
+
+import com.ning.billing.invoice.api.InvoiceItem;
+
+public interface InvoiceItemFormatter extends InvoiceItem {
+    public String getFormattedStartDate();
+    public String getFormattedEndDate();
+}
diff --git a/util/src/main/java/com/ning/billing/util/email/templates/HtmlInvoiceGenerator.java b/util/src/main/java/com/ning/billing/util/email/templates/HtmlInvoiceGenerator.java
new file mode 100644
index 0000000..f6e7710
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/email/templates/HtmlInvoiceGenerator.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2010-2011 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.util.email.templates;
+
+import com.google.inject.Inject;
+import com.ning.billing.account.api.Account;
+import com.ning.billing.invoice.api.Invoice;
+import com.ning.billing.util.email.EmailConfig;
+import com.ning.billing.util.email.formatters.DefaultInvoiceFormatter;
+import com.ning.billing.util.email.formatters.InvoiceFormatter;
+import com.ning.billing.util.email.translation.DefaultInvoiceTranslation;
+import com.samskivert.mustache.Mustache;
+import com.samskivert.mustache.Template;
+import org.apache.commons.io.IOUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringWriter;
+import java.lang.String;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+public class HtmlInvoiceGenerator {
+    private final EmailConfig config;
+
+    @Inject
+    public HtmlInvoiceGenerator(EmailConfig config) {
+        this.config = config;
+    }
+
+    public String generateInvoice(Account account, Invoice invoice, String templateName) throws IOException {
+        InputStream templateStream = this.getClass().getResourceAsStream(templateName + ".mustache");
+        StringWriter writer = new StringWriter();
+        IOUtils.copy(templateStream, writer, "UTF-8");
+        String templateText = writer.toString();
+
+        Template template = Mustache.compiler().compile(templateText);
+
+        Map<String, Object> data = new HashMap<String, Object>();
+
+        data.put("text", new DefaultInvoiceTranslation(config));
+        data.put("account", account);
+        Locale locale = new Locale(account.getLocale());
+
+        InvoiceFormatter formattedInvoice = new DefaultInvoiceFormatter(config, invoice, locale);
+        data.put("invoice", formattedInvoice);
+
+        return template.execute(data);
+    }
+}
\ No newline at end of file
diff --git a/util/src/main/java/com/ning/billing/util/email/templates/InvoiceStrings.java b/util/src/main/java/com/ning/billing/util/email/templates/InvoiceStrings.java
new file mode 100644
index 0000000..47c0857
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/email/templates/InvoiceStrings.java
@@ -0,0 +1,40 @@
+package com.ning.billing.util.email.templates;/*
+ * Copyright 2010-2011 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.
+ */
+
+public interface InvoiceStrings {
+    String getInvoiceTitle();
+    String getInvoiceDate();
+    String getInvoiceNumber();
+    String getAccountOwnerName();
+    String getAccountOwnerEmail();
+    String getAccountOwnerPhone();
+
+    // company name and address
+    String getCompanyName();
+    String getCompanyAddress();
+    String getCompanyCityProvincePostalCode();
+    String getCompanyCountry();
+    String getCompanyUrl();
+
+    String getInvoiceItemBundleName();
+    String getInvoiceItemDescription();
+    String getInvoiceItemServicePeriod();
+    String getInvoiceItemAmount();
+
+    String getInvoiceAmount();
+    String getInvoiceAmountPaid();
+    String getInvoiceBalance();
+}
diff --git a/util/src/main/java/com/ning/billing/util/email/translation/CatalogTranslation.java b/util/src/main/java/com/ning/billing/util/email/translation/CatalogTranslation.java
new file mode 100644
index 0000000..eac06a1
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/email/translation/CatalogTranslation.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2010-2011 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.util.email.translation;
+
+import java.util.Locale;
+
+public interface CatalogTranslation {
+    public String getTranslation(Locale locale, String originalText);
+}
diff --git a/util/src/main/java/com/ning/billing/util/email/translation/DefaultCatalogTranslation.java b/util/src/main/java/com/ning/billing/util/email/translation/DefaultCatalogTranslation.java
new file mode 100644
index 0000000..be01630
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/email/translation/DefaultCatalogTranslation.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2010-2011 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.util.email.translation;
+
+import com.google.inject.Inject;
+import com.ning.billing.util.email.EmailConfig;
+
+public class DefaultCatalogTranslation extends DefaultTranslationBase {
+    @Inject
+    public DefaultCatalogTranslation(EmailConfig config) {
+        super(config);
+    }
+
+    @Override
+    protected String getBundlePath() {
+        return "com/ning/billing/util/email/translation/CatalogTranslation";
+    }
+
+    @Override
+    protected String getTranslationType() {
+        return "catalog";
+    }
+}
diff --git a/util/src/main/java/com/ning/billing/util/email/translation/DefaultInvoiceTranslation.java b/util/src/main/java/com/ning/billing/util/email/translation/DefaultInvoiceTranslation.java
new file mode 100644
index 0000000..8ef9f16
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/email/translation/DefaultInvoiceTranslation.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2010-2011 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.util.email.translation;
+
+import com.google.inject.Inject;
+import com.ning.billing.util.email.EmailConfig;
+import com.ning.billing.util.email.templates.InvoiceStrings;
+
+import java.util.Locale;
+
+public class DefaultInvoiceTranslation extends DefaultTranslationBase implements InvoiceStrings {
+    private Locale locale;
+
+    @Inject
+    public DefaultInvoiceTranslation(EmailConfig config) {
+        super(config);
+    }
+
+    public void setLocale(Locale locale) {
+        this.locale = locale;
+    }
+
+    @Override
+    protected String getBundlePath() {
+        return "com/ning/billing/util/email/translation/InvoiceTranslation";
+    }
+
+    @Override
+    protected String getTranslationType() {
+        return "invoice";
+    }
+
+    @Override
+    public String getInvoiceTitle() {
+        return getTranslation(locale, "invoiceTitle");
+    }
+
+    @Override
+    public String getInvoiceDate() {
+        return getTranslation(locale, "invoiceDate");
+    }
+
+    @Override
+    public String getInvoiceNumber() {
+        return getTranslation(locale, "invoiceNumber");
+    }
+
+    @Override
+    public String getAccountOwnerName() {
+        return getTranslation(locale, "accountOwnerName");
+    }
+
+    @Override
+    public String getAccountOwnerEmail() {
+        return getTranslation(locale, "accountOwnerEmail");
+    }
+
+    @Override
+    public String getAccountOwnerPhone() {
+        return getTranslation(locale, "accountOwnerPhone");
+    }
+
+    @Override
+    public String getCompanyName() {
+        return getTranslation(locale, "companyName");
+    }
+
+    @Override
+    public String getCompanyAddress() {
+        return getTranslation(locale, "companyAddress");
+    }
+
+    @Override
+    public String getCompanyCityProvincePostalCode() {
+        return getTranslation(locale, "");
+    }
+
+    @Override
+    public String getCompanyCountry() {
+        return getTranslation(locale, "companyCountry");
+    }
+
+    @Override
+    public String getCompanyUrl() {
+        return getTranslation(locale, "companyUrl");
+    }
+
+    @Override
+    public String getInvoiceItemBundleName() {
+        return getTranslation(locale, "invoiceItemBundleName");
+    }
+
+    @Override
+    public String getInvoiceItemDescription() {
+        return getTranslation(locale, "invoiceItemDescription");
+    }
+
+    @Override
+    public String getInvoiceItemServicePeriod() {
+        return getTranslation(locale, "invoiceItemServicePeriod");
+    }
+
+    @Override
+    public String getInvoiceItemAmount() {
+        return getTranslation(locale, "invoiceItemAmount");
+    }
+
+    @Override
+    public String getInvoiceAmount() {
+        return getTranslation(locale, "invoiceAmount");
+    }
+
+    @Override
+    public String getInvoiceAmountPaid() {
+        return getTranslation(locale, "invoiceAmountPaid");
+    }
+
+    @Override
+    public String getInvoiceBalance() {
+        return getTranslation(locale, "invoiceBalance");
+    }
+}
diff --git a/util/src/main/java/com/ning/billing/util/email/translation/DefaultTranslationBase.java b/util/src/main/java/com/ning/billing/util/email/translation/DefaultTranslationBase.java
new file mode 100644
index 0000000..82a799a
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/email/translation/DefaultTranslationBase.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2010-2011 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.util.email.translation;
+
+import com.google.inject.Inject;
+import com.ning.billing.ErrorCode;
+import com.ning.billing.util.email.EmailConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Locale;
+import java.util.MissingResourceException;
+import java.util.ResourceBundle;
+
+public abstract class DefaultTranslationBase implements CatalogTranslation {
+    protected final EmailConfig config;
+    protected final Logger log = LoggerFactory.getLogger(DefaultTranslationBase.class);
+
+    @Inject
+    public DefaultTranslationBase(EmailConfig config) {
+        this.config = config;
+    }
+
+    protected abstract String getBundlePath();
+
+    /*
+     * string used for exception handling
+     */
+    protected abstract String getTranslationType();
+
+    @Override
+    public String getTranslation(Locale locale, String originalText) {
+        ResourceBundle bundle = null;
+        try {
+            bundle = ResourceBundle.getBundle(getBundlePath(), locale);
+        } catch (MissingResourceException mrex) {
+            log.warn(String.format(ErrorCode.MISSING_TRANSLATION_RESOURCE.toString(), getTranslationType()));
+        }
+
+        if ((bundle != null) && (bundle.containsKey(originalText))) {
+            return bundle.getString(originalText);
+        } else {
+            // TODO: make the default local a configuration item
+            try {
+                Locale defaultLocale = new Locale(config.getDefaultLocale());
+                bundle = ResourceBundle.getBundle(getBundlePath(), Locale.US);
+
+                if ((bundle != null) && (bundle.containsKey(originalText))) {
+                    return bundle.getString(originalText);
+                } else {
+                    return originalText;
+                }
+            } catch (MissingResourceException mrex) {
+                log.warn(String.format(ErrorCode.MISSING_TRANSLATION_RESOURCE.toString(), getTranslationType()));
+                return originalText;
+            }
+        }
+    }
+}
diff --git a/util/src/main/resources/com/ning/billing/util/email/templates/HtmlInvoiceTemplate.mustache b/util/src/main/resources/com/ning/billing/util/email/templates/HtmlInvoiceTemplate.mustache
new file mode 100644
index 0000000..73c70b3
--- /dev/null
+++ b/util/src/main/resources/com/ning/billing/util/email/templates/HtmlInvoiceTemplate.mustache
@@ -0,0 +1,96 @@
+<html>
+    <head>
+        <style type="text/css">
+            th {align=left; width=225px; border-bottom: solid 2px black;}
+        </style>
+    </head>
+    <body>
+        <h1>{{text.invoiceTitle}}</h1>
+        <table>
+            <tr>
+                <td rowspan=3 width=350px>Insert image here</td>
+                <td width=100px/>
+                <td width=225px/>
+                <td width=225px/>
+            </tr>
+            <tr>
+                <td />
+                <td align=right>{{text.invoiceDate}}</td>
+                <td>{{invoice.formattedInvoiceDate}}</td>
+            </tr>
+            <tr>
+                <td />
+                <td align=right>{{text.invoiceNumber}}</td>
+                <td>{{invoice.invoiceNumber}}</td>
+            </tr>
+            <tr>
+                <td>{{text.companyName}}</td>
+                <td></td>
+                <td align=right>{{text.accountOwnerName}}</td>
+                <td>{{account.name}}</td>
+            </tr>
+            <tr>
+                <td>{{text.companyAddress}}</td>
+                <td />
+                <td />
+                <td>{{account.email}}</td>
+            </tr>
+            <tr>
+                <td>{{text.companyCityProvincePostalCode}}</td>
+                <td />
+                <td />
+                <td>{{account.phone}}</td>
+            </tr>
+            <tr>
+                <td>{{text.companyCountry}}</td>
+                <td />
+                <td />
+                <td />
+            </tr>
+            <tr>
+                <td><{{text.companyUrl}}</td>
+                <td />
+                <td />
+                <td />
+            </tr>
+        </table>
+        <br />
+        <br />
+        <br />
+        <table>
+            <tr>
+                <th>{{text.invoiceItemBundleName}}</td>
+                <th>{{text.invoiceItemDescription}}</td>
+                <th>{{text.invoiceItemServicePeriod}}</td>
+                <th>{{text.invoiceItemAmount}}</td>
+            </tr>
+            {{#invoice.invoiceItems}}
+            <tr>
+                <td>{{description}}</td>
+                <td>{{planName}}</td>
+                <td>{{formattedStartDate}} - {{formattedEndDate}}</td>
+                <td>{{invoice.currency}} {{amount}}</td>
+            </tr>
+            {{/invoice.invoiceItems}}
+            <tr>
+                <td colspan=4 />
+            </tr>
+            <tr>
+                <td colspan=2 />
+                <td align=right><strong>{{text.invoiceAmount}}</strong></td>
+                <td align=right><strong>{{invoice.totalAmount}}</strong></td>
+            </tr>
+            <tr>
+                <td colspan=2 />
+                <td align=right><strong>{{text.invoiceAmountPaid}}</strong></td>
+                <td align=right><strong>{{invoice.amountPaid}}</strong></td>
+            </tr>
+            <tr>
+                <td colspan=2 />
+                <td align=right><strong>{{text.invoiceBalance}}</strong></td>
+                <td align=right><strong>{{invoice.balance}}</strong></td>
+            </tr>
+        </table>
+    </body>
+</html>
+
diff --git a/util/src/main/resources/com/ning/billing/util/email/translation/CatalogTranslation_EN_US.properties b/util/src/main/resources/com/ning/billing/util/email/translation/CatalogTranslation_EN_US.properties
new file mode 100644
index 0000000..b05a595
--- /dev/null
+++ b/util/src/main/resources/com/ning/billing/util/email/translation/CatalogTranslation_EN_US.properties
@@ -0,0 +1,2 @@
+ning-pro = Pro
+ning-plus = Plus
\ No newline at end of file
diff --git a/util/src/main/resources/com/ning/billing/util/email/translation/CatalogTranslation_FR_CA.properties b/util/src/main/resources/com/ning/billing/util/email/translation/CatalogTranslation_FR_CA.properties
new file mode 100644
index 0000000..e025a88
--- /dev/null
+++ b/util/src/main/resources/com/ning/billing/util/email/translation/CatalogTranslation_FR_CA.properties
@@ -0,0 +1 @@
+ning-plus = Plus en francais
\ No newline at end of file
diff --git a/util/src/main/resources/com/ning/billing/util/email/translation/InvoiceTranslation_EN_US.properties b/util/src/main/resources/com/ning/billing/util/email/translation/InvoiceTranslation_EN_US.properties
new file mode 100644
index 0000000..0162671
--- /dev/null
+++ b/util/src/main/resources/com/ning/billing/util/email/translation/InvoiceTranslation_EN_US.properties
@@ -0,0 +1,20 @@
+invoiceTitle=INVOICE
+invoiceDate=Date:
+invoiceNumber=Invoice #
+invoiceAmount=New Charges
+invoiceAmountPaid=Payment
+invoiceBalance=Balance
+
+accountOwnerName=Network Creator
+
+companyName=Ning, Inc.
+companyAddress=P.O. Box 1622
+companyCityProvincePostalCode=Palo Alto, CA 94302
+companyCountry=USA
+companyUrl=http://www.ning.com
+
+invoiceItemBundleName=NetworkName
+invoiceItemDescription=Description
+invoiceItemServicePeriod=Service Period
+invoiceItemAmount=Amount
+
diff --git a/util/src/test/java/com/ning/billing/util/email/DefaultCatalogTranslationTest.java b/util/src/test/java/com/ning/billing/util/email/DefaultCatalogTranslationTest.java
new file mode 100644
index 0000000..c704f18
--- /dev/null
+++ b/util/src/test/java/com/ning/billing/util/email/DefaultCatalogTranslationTest.java
@@ -0,0 +1,83 @@
+package com.ning.billing.util.email;/*
+ * Copyright 2010-2011 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.
+ */
+
+import com.ning.billing.util.email.translation.CatalogTranslation;
+import com.ning.billing.util.email.translation.DefaultCatalogTranslation;
+import org.skife.config.ConfigurationObjectFactory;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import java.util.Locale;
+
+import static org.testng.Assert.assertEquals;
+
+@Test(groups = {"fast", "email"})
+public class DefaultCatalogTranslationTest {
+    private CatalogTranslation translation;
+
+    @BeforeClass(groups={"fast", "email"})
+    public void setup() {
+        final EmailConfig config = new ConfigurationObjectFactory(System.getProperties()).build(EmailConfig.class);
+        translation = new DefaultCatalogTranslation(config);
+    }
+
+    @Test(groups = {"fast", "email"})
+    public void testInitialization() {
+        String ningPlusText = "ning-plus";
+        String ningProText = "ning-pro";
+        String badText = "Bad text";
+
+        assertEquals(translation.getTranslation(Locale.US, ningPlusText), "Plus");
+        assertEquals(translation.getTranslation(Locale.US, ningProText), "Pro");
+        assertEquals(translation.getTranslation(Locale.US, badText), badText);
+
+        assertEquals(translation.getTranslation(Locale.CANADA_FRENCH, ningPlusText), "Plus en francais");
+        assertEquals(translation.getTranslation(Locale.CANADA_FRENCH, ningProText), "Pro");
+        assertEquals(translation.getTranslation(Locale.CANADA_FRENCH, badText), badText);
+
+        assertEquals(translation.getTranslation(Locale.CHINA, ningPlusText), "Plus");
+        assertEquals(translation.getTranslation(Locale.CHINA, ningProText), "Pro");
+        assertEquals(translation.getTranslation(Locale.CHINA, badText), badText);
+    }
+
+    @Test
+    public void testExistingTranslation() {
+        // if the translation exists, return the translation
+        String originalText = "ning-plus";
+        assertEquals(translation.getTranslation(Locale.US,  originalText), "Plus");
+    }
+
+    @Test
+    public void testMissingTranslation() {
+        // if the translation is missing from the file, return the original text
+        String originalText = "missing translation";
+        assertEquals(translation.getTranslation(Locale.US, originalText), originalText);
+    }
+
+    @Test
+    public void testMissingTranslationFileWithEnglishText() {
+        // if the translation file doesn't exist, return the "English" translation
+        String originalText = "ning-plus";
+        assertEquals(translation.getTranslation(Locale.CHINA, originalText), "Plus");
+    }
+
+    @Test
+    public void testMissingFileAndText() {
+        // if the file is missing, and the "English" translation is missing, return the original text
+        String originalText = "missing translation";
+        assertEquals(translation.getTranslation(Locale.CHINA, originalText), originalText);
+    }
+}
diff --git a/util/src/test/java/com/ning/billing/util/email/EmailSenderTest.java b/util/src/test/java/com/ning/billing/util/email/EmailSenderTest.java
new file mode 100644
index 0000000..1685628
--- /dev/null
+++ b/util/src/test/java/com/ning/billing/util/email/EmailSenderTest.java
@@ -0,0 +1,42 @@
+package com.ning.billing.util.email;/*
+ * Copyright 2010-2011 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.
+ */
+
+import org.skife.config.ConfigurationObjectFactory;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Test(groups = {"slow", "email"})
+public class EmailSenderTest {
+    private EmailConfig config;
+
+    @BeforeClass
+    public void setup() {
+        config = new ConfigurationObjectFactory(System.getProperties()).build(EmailConfig.class);
+    }
+
+    @Test
+    public void testSendEmail() throws Exception {
+        String html = "<html><body><h1>Test E-mail</h1></body></html>";
+        List<String> recipients = new ArrayList<String>();
+        recipients.add("killbill.ning@gmail.com");
+
+        EmailSender sender = new DefaultEmailSender(config);
+        sender.sendSecureEmail(recipients, null, "Test message", html);
+    }
+}
diff --git a/util/src/test/java/com/ning/billing/util/email/HtmlInvoiceGeneratorTest.java b/util/src/test/java/com/ning/billing/util/email/HtmlInvoiceGeneratorTest.java
new file mode 100644
index 0000000..51ea400
--- /dev/null
+++ b/util/src/test/java/com/ning/billing/util/email/HtmlInvoiceGeneratorTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2010-2011 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.util.email;
+
+import com.ning.billing.account.api.Account;
+import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.invoice.api.Invoice;
+import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.mock.BrainDeadProxyFactory;
+import com.ning.billing.mock.BrainDeadProxyFactory.ZombieControl;
+import com.ning.billing.util.email.templates.HtmlInvoiceGenerator;
+import org.apache.commons.mail.Email;
+import org.joda.time.DateTime;
+import org.skife.config.ConfigurationObjectFactory;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import static org.testng.Assert.assertNotNull;
+
+@Test(groups = {"fast", "email"})
+public class HtmlInvoiceGeneratorTest {
+    private HtmlInvoiceGenerator g;
+    private final static String TEST_TEMPLATE_NAME = "HtmlInvoiceTemplate";
+
+    @BeforeClass
+    public void setup() {
+        EmailConfig config = new ConfigurationObjectFactory(System.getProperties()).build(EmailConfig.class);
+        g = new HtmlInvoiceGenerator(config);
+    }
+
+    @Test
+    public void testGenerateInvoice() throws Exception {
+        String output = g.generateInvoice(createAccount(), createInvoice(), TEST_TEMPLATE_NAME);
+        assertNotNull(output);
+        System.out.print(output);
+    }
+
+    private Account createAccount() {
+        Account account = BrainDeadProxyFactory.createBrainDeadProxyFor(Account.class);
+        ZombieControl zombieControl = (ZombieControl) account;
+        zombieControl.addResult("getExternalKey", "1234abcd");
+        zombieControl.addResult("getName", "Jim Smith");
+        zombieControl.addResult("getFirstNameLength", 3);
+        zombieControl.addResult("getEmail", "jim.smith@mail.com");
+        zombieControl.addResult("getLocale", Locale.US.toString());
+        zombieControl.addResult("getAddress1", "123 Some Street");
+        zombieControl.addResult("getAddress2", "Apt 456");
+        zombieControl.addResult("getCity", "Some City");
+        zombieControl.addResult("getStateOrProvince", "Some State");
+        zombieControl.addResult("getPostalCode", "12345-6789");
+        zombieControl.addResult("getCountry", "USA");
+        zombieControl.addResult("getPhone", "123-456-7890");
+
+        return account;
+    }
+
+    private Invoice createInvoice() {
+        DateTime startDate = new DateTime().minusMonths(1);
+        DateTime endDate = new DateTime();
+
+        BigDecimal price1 = new BigDecimal("29.95");
+        BigDecimal price2 = new BigDecimal("59.95");
+        Invoice dummyInvoice = BrainDeadProxyFactory.createBrainDeadProxyFor(Invoice.class);
+        ZombieControl zombie = (ZombieControl) dummyInvoice;
+        zombie.addResult("getInvoiceDate", startDate);
+        zombie.addResult("getInvoiceNumber", 42);
+        zombie.addResult("getCurrency", Currency.USD);
+        zombie.addResult("getTotalAmount", price1.add(price2));
+        zombie.addResult("getAmountPaid", BigDecimal.ZERO);
+        zombie.addResult("getBalance", price1.add(price2));
+
+        List<InvoiceItem> items = new ArrayList<InvoiceItem>();
+        items.add(createInvoiceItem(price1, "Domain 1", startDate, endDate, "ning-plus"));
+        items.add(createInvoiceItem(price2, "Domain 2", startDate, endDate, "ning-pro"));
+        zombie.addResult("getInvoiceItems", items);
+
+        return dummyInvoice;
+    }
+
+    private InvoiceItem createInvoiceItem(BigDecimal amount, String networkName, DateTime startDate, DateTime endDate, String planName) {
+        InvoiceItem item = BrainDeadProxyFactory.createBrainDeadProxyFor(InvoiceItem.class);
+        ZombieControl zombie = (ZombieControl) item;
+        zombie.addResult("getAmount", amount);
+        zombie.addResult("getStartDate", startDate);
+        zombie.addResult("getEndDate", endDate);
+        zombie.addResult("getPlanName", planName);
+        zombie.addResult("getDescription", networkName);
+
+
+        return item;
+    }
+}