diff --git a/includes/simplenews.admin.inc b/includes/simplenews.admin.inc
index 009f466..5b47cd1 100644
--- a/includes/simplenews.admin.inc
+++ b/includes/simplenews.admin.inc
@@ -1442,7 +1442,7 @@ function simplenews_admin_settings_mail($form, &$form_state) {
     '#title' => t('Cache'),
     '#description' => t('Chosing a different cache implementation allows for a different behavior during sending mails.') . '<br /><br />' . $sources_descriptions,
     '#options' => $sources_labels,
-    '#default_value' => variable_get('simplenews_source_cache', 'SimplenewsSourceNodeCached'),
+    '#default_value' => variable_get('simplenews_source_cache', 'SimplenewsSourceCacheBuild'),
   );
 
   $throttle = drupal_map_assoc(array(1, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000));
diff --git a/includes/simplenews.mail.inc b/includes/simplenews.mail.inc
index 2d85307..0dea44d 100644
--- a/includes/simplenews.mail.inc
+++ b/includes/simplenews.mail.inc
@@ -13,18 +13,8 @@
  * @param mixed $node
  *   The newsletter node to be sent.  If an integer, the node nid; if an object,
  *   the node object.
- * @param array $accounts
- *   An array of recipient accounts for this newsletter.  Each account may
- *   have the following properties, but either snid, mail, or uid must be
- *   defined:
- *   - snid: The subscription id, or 0 if no subscription record exists.
- *   - tid: The newsletter category id.
- *   - uid: The subscriber's uid, or 0 if the subscriber has no user account.
- *   - mail: The subscriber's email address.
- *   - name: Emtpy; added for compatibility with user account object.
- *   - language: The language object of the subscriber's preferred language.
  */
-function simplenews_add_node_to_spool($node, $accounts = array()) {
+function simplenews_add_node_to_spool($node) {
   if (is_numeric($node)) {
     $node = node_load($node);
   }
@@ -695,13 +685,13 @@ function simplenews_build_newsletter_mail(&$message, SimplenewsSourceInterface $
   // Get message data from source.
   $message['headers'] = $source->getHeaders($message['headers']);
   $message['subject'] = $source->getSubject();
-  $message['body']['body'] = $source->getFormat() == 'html' ? $source->getBody() : $source->getPlainBody();
+  $message['body']['body'] = $source->getBody();
   $message['body']['footer'] = $source->getFooter();
 
   // Optional params for HTML mails.
   if ($source->getFormat() == 'html') {
     $message['params']['plain'] = NULL;
-    $message['params']['plaintext'] = $source->getPlainBody();
+    $message['params']['plaintext'] = $source->getPlainBody() . "\n" . $source->getPlainFooter();
     $message['params']['attachments'] = $source->getAttachments();
   }
   else {
diff --git a/includes/simplenews.source.inc b/includes/simplenews.source.inc
index fd7275d..a14f4f8 100644
--- a/includes/simplenews.source.inc
+++ b/includes/simplenews.source.inc
@@ -28,6 +28,8 @@ interface SimplenewsSourceInterface {
 
   /**
    * Returns the mail body.
+   *
+   * The body should either be plaintext or html, depending on the format.
    */
   function getBody();
 
@@ -38,10 +40,17 @@ interface SimplenewsSourceInterface {
 
   /**
    * Returns the mail footer.
+   *
+   * The footer should either be plaintext or html, depending on the format.
    */
   function getFooter();
 
   /**
+   * Returns the plain footer.
+   */
+  function getPlainFooter();
+
+  /**
    * Returns the mail format.
    *
    * @return
@@ -635,7 +644,7 @@ class SimplenewsSourceNode implements SimplenewsSourceNodeInterface {
       return $cache;
     }
     $body = theme('simplenews_newsletter_body', array('build' => $this->build($format), 'category' => $this->getCategory(), 'language' => $this->getLanguage()));
-    $this->cache->set('build', 'body', $body);
+    $this->cache->set('build', 'body:' . $format, $body);
     return $body;
   }
 
@@ -695,29 +704,37 @@ class SimplenewsSourceNode implements SimplenewsSourceNodeInterface {
 
   /**
    * Builds the themed footer.
+   *
+   * @param $format
+   *   (Optional) Set the format of this footer build, overrides the default
+   *   format.
    */
-  protected function buildFooter() {
-    if ($cache = $this->cache->get('build', 'footer')) {
+  protected function buildFooter($format = NULL) {
+    if (empty($format)) {
+      $format = $this->getFormat();
+    }
+
+    if ($cache = $this->cache->get('build', 'footer:' . $format)) {
       return $cache;
     }
 
     // Build and buffer message footer
     $footer = theme('simplenews_newsletter_footer', array(
-      'build' => $this->build(),
+      'build' => $this->build($format),
       'category' => $this->getCategory(),
       'context' => $this->getTokenContext(),
       'key' => $this->getKey(),
       'language' => $this->getLanguage(),
       )
     );
-    $this->cache->set('build', 'footer', $footer);
+    $this->cache->set('build', 'footer:' . $format, $footer);
     return $footer;
   }
 
   /**
    * Implements SimplenewsSourceInterface::getFooter().
    */
-  function getFooter() {
+  public function getFooter() {
     if ($cache = $this->cache->get('final', 'footer')) {
       return $cache;
     }
@@ -731,6 +748,22 @@ class SimplenewsSourceNode implements SimplenewsSourceNodeInterface {
   }
 
   /**
+   * Implements SimplenewsSourceInterface::getPlainFooter().
+   */
+  public function getPlainFooter() {
+    if ($cache = $this->cache->get('final', 'footer:plain')) {
+      return $cache;
+    }
+
+    if ($this->category->opt_inout != SIMPLENEWS_OPT_INOUT_HIDDEN) {
+      $final_footer = token_replace($this->buildFooter('plain'), $this->getTokenContext(), array('sanitize' => FALSE));
+    }
+
+    $this->cache->set('build', 'footer:plain', $final_footer);
+    return $final_footer;
+  }
+
+  /**
    * Implements SimplenewsSourceInterface::getAttachments().
    */
   function getAttachments() {
diff --git a/simplenews.module b/simplenews.module
index b6562b3..9acd135 100644
--- a/simplenews.module
+++ b/simplenews.module
@@ -357,6 +357,11 @@ function simplenews_node_view($node, $view_mode) {
     return;
   }
 
+  // Only do token replacements for view modes other than the our own email view
+  // modes. Token replacements for them will happen later on.
+  if (strpos($view_mode, 'email_') !== FALSE) {
+    return;
+  }
 
   $context = array(
     'node' => $node,
@@ -3156,7 +3161,7 @@ function simplenews_simplenews_source_cache_info() {
       'label' => t('No caching'),
       'description' => t('This allows to theme each newsletter separately.'),
     ),
-    'SimplenewsSourceNodeCached' => array(
+    'SimplenewsSourceCacheBuild' => array(
       'label' => t('Cached content source'),
       'description' => t('This caches the rendered content to be sent for multiple recipients. It is not possible to use subscriber specific theming but tokens can be used for personalization.'),
     ),
diff --git a/tests/simplenews.test b/tests/simplenews.test
index 2aef89b..780eefd 100644
--- a/tests/simplenews.test
+++ b/tests/simplenews.test
@@ -1547,3 +1547,248 @@ class SimplenewsSendTestCase extends SimplenewsTestCase {
 
   }
 }
+
+/**
+ * Test cases for creating and sending newsletters.
+ */
+class SimplenewsSourceTestCase extends SimplenewsTestCase {
+  /**
+   * Implementation of getInfo().
+   */
+  function getInfo() {
+    return array(
+      'name' => t('Source tests'),
+      'description' => t('Tests for the new source interfaces and concepts.'),
+      'group' => t('Simplenews'),
+    );
+  }
+
+  function setUp() {
+    parent::setUp();
+
+    $this->user = $this->drupalCreateUser(array('administer newsletters', 'send newsletter', 'administer nodes', 'administer simplenews subscriptions', 'create simplenews content', 'edit any simplenews content', 'view own unpublished content', 'delete any simplenews content', 'administer simplenews settings'));
+    $this->drupalLogin($this->user);
+  }
+
+  function subscribeUsers($count = 100) {
+    // Subscribe users.
+    $this->subscribers = array();
+    for ($i = 0; $i < $count; $i++) {
+      $mail = $this->randomEmail();
+      $this->subscribers[$mail] = $mail;
+    }
+
+    $this->drupalGet('admin/people/simplenews');
+    $this->clickLink(t('Mass subscribe'));
+    $edit = array(
+      'emails' => implode(',', $this->subscribers),
+      // @todo: Don't hardcode the default tid.
+      '1' => TRUE,
+    );
+    $this->drupalPost(NULL, $edit, t('Subscribe'));
+  }
+
+  /**
+   * Tests that sending a minimal implementation of the source interface works.
+   */
+  function testSendMinimalSourceImplementation() {
+    // Create a basic plaintext test source and send it.
+    $plain_source = new SimplenewsSourceTest('plain');
+    simplenews_send_source($plain_source);
+    $mails = $this->drupalGetMails();
+    $mail = $mails[0];
+
+    // Assert resulting mail.
+    $this->assertEqual('simplenews_node', $mail['id']);
+    $this->assertEqual('simplenews', $mail['module']);
+    $this->assertEqual('node', $mail['key']);
+    $this->assertEqual($plain_source->getRecipient(), $mail['to']);
+    $this->assertEqual($plain_source->getFromFormatted(), $mail['from']);
+    $this->assertEqual($plain_source->getLanguage(), $mail['language']);
+    $this->assertTrue($mail['params']['plain']);
+
+    $this->assertFalse(isset($mail['params']['plaintext']));
+    $this->assertFalse(isset($mail['params']['attachments']));
+
+    $this->assertEqual($plain_source->getSubject(), $mail['subject']);
+    $this->assertTrue(strpos($mail['body'], 'the plain body') !== FALSE);
+    $this->assertTrue(strpos($mail['body'], 'the plain footer') !== FALSE);
+
+    $html_source = new SimplenewsSourceTest('html');
+    simplenews_send_source($html_source);
+    $mails = $this->drupalGetMails();
+    $mail = $mails[1];
+
+    // Assert resulting mail.
+    $this->assertEqual('simplenews_node', $mail['id']);
+    $this->assertEqual('simplenews', $mail['module']);
+    $this->assertEqual('node', $mail['key']);
+    $this->assertEqual($plain_source->getRecipient(), $mail['to']);
+    $this->assertEqual($plain_source->getFromFormatted(), $mail['from']);
+    $this->assertEqual($plain_source->getLanguage(), $mail['language']);
+    $this->assertEqual(NULL, $mail['params']['plain']);
+
+    $this->assertTrue(isset($mail['params']['plaintext']));
+    $this->assertTrue(strpos($mail['params']['plaintext'], 'the plain body') !== FALSE);
+    $this->assertTrue(strpos($mail['params']['plaintext'], 'the plain footer') !== FALSE);
+    $this->assertTrue(isset($mail['params']['attachments']));
+    $this->assertEqual('example://test.png', $mail['params']['attachments'][0]['uri']);
+
+    $this->assertEqual($plain_source->getSubject(), $mail['subject']);
+    $this->assertTrue(strpos($mail['body'], 'the body') !== FALSE);
+    $this->assertTrue(strpos($mail['body'], 'the footer') !== FALSE);
+  }
+
+  /**
+   * Test sending a newsletter to 100 recipients with caching enabled.
+   */
+  function testSendCaching() {
+
+    $this->subscribeUsers(100);
+    // Enable build caching.
+    $edit = array(
+      'simplenews_source_cache' => 'SimplenewsSourceCacheBuild',
+    );
+    $this->drupalPost('admin/config/services/simplenews/settings/mail', $edit, t('Save configuration'));
+
+    $edit = array(
+      'title' => $this->randomName(),
+      'body[und][0][value]' => "Mail token: [simplenews-subscriber:mail]",
+    );
+    $this->drupalPost('node/add/simplenews', $edit, ('Save'));
+    $this->assertTrue(preg_match('|node/(\d+)$|', $this->getUrl(), $matches), 'Node created');
+    $node = node_load($matches[1]);
+
+    // Add node to spool.
+    simplenews_add_node_to_spool($node);
+
+    $before = microtime(TRUE);
+    simplenews_mail_spool();
+    $after = microtime(TRUE);
+
+    // Make sure that 100 mails have been sent.
+    $this->assertEqual(100, count($this->drupalGetMails()));
+
+    // Test that tokens are correctly replaced.
+    foreach (array_slice($this->drupalGetMails(), 0, 3) as $mail) {
+      // Make sure that the same mail was used in the body token as it has been
+      // sent to.
+      $this->assertTrue(strpos($mail['body'], $mail['to']) !== FALSE);
+    }
+
+    // Report time. @todo: Find a way to actually do some assertions here.
+    $this->error(t('Mails have been sent in @sec seconds with build caching enabled.', array('@sec' => round($after - $before, 3))));
+  }
+
+  /**
+   * Test with disabled caching.
+   */
+  function testSendNoCaching() {
+    $this->subscribeUsers(100);
+    // Disable caching.
+    $edit = array(
+      'simplenews_source_cache' => 'SimplenewsSourceCacheNone',
+    );
+    $this->drupalPost('admin/config/services/simplenews/settings/mail', $edit, t('Save configuration'));
+
+    $edit = array(
+      'title' => $this->randomName(),
+      'body[und][0][value]' => "Mail token: [simplenews-subscriber:mail]",
+    );
+    $this->drupalPost('node/add/simplenews', $edit, ('Save'));
+    $this->assertTrue(preg_match('|node/(\d+)$|', $this->getUrl(), $matches), 'Node created');
+    $node = node_load($matches[1]);
+
+    // Add node to spool.
+    simplenews_add_node_to_spool($node);
+
+    $before = microtime(TRUE);
+    simplenews_mail_spool();
+    $after = microtime(TRUE);
+
+    // Make sure that 100 mails have been sent.
+    $this->assertEqual(100, count($this->drupalGetMails()));
+
+    // Test that tokens are correctly replaced.
+    foreach (array_slice($this->drupalGetMails(), 0, 3) as $mail) {
+      // Make sure that the same mail was used in the body token as it has been
+      // sent to.
+      $this->assertTrue(strpos($mail['body'], $mail['to']) !== FALSE);
+    }
+
+    // Report time. @todo: Find a way to actually do some assertions here.
+    $this->error(t('Mails have been sent in @sec seconds with caching disabled.', array('@sec' => round($after - $before, 3))));
+  }
+}
+
+class SimplenewsSourceTest implements SimplenewsSourceInterface {
+
+  protected $format;
+
+  public function __construct($format) {
+    $this->format = $format;
+  }
+
+  public function getAttachments() {
+    return array(
+      array(
+        'uri' => 'example://test.png',
+        'filemime' => 'x-example',
+        'filename' => 'test.png',
+      ),
+    );
+  }
+
+  public function getBody() {
+    return $this->getFormat() == 'plain' ? $this->getPlainBody() : 'the body';
+  }
+
+  public function getFooter() {
+    return $this->getFormat() == 'plain' ? $this->getPlainFooter() : 'the footer';
+  }
+
+  public function getPlainFooter() {
+    return 'the plain footer';
+  }
+
+  public function getFormat() {
+    return $this->format;
+  }
+
+  public function getFromAddress() {
+    return 'test@example.org';
+  }
+
+  public function getFromFormatted() {
+    return 'Test <test@example.org>';
+  }
+
+  public function getHeaders(array $headers) {
+    $headers['X-Simplenews-Test'] = 'OK';
+    return $headers;
+  }
+
+  public function getKey() {
+    return 'node';
+  }
+
+  public function getLanguage() {
+    return 'en';
+  }
+
+  public function getPlainBody() {
+    return 'the plain body';
+  }
+
+  public function getRecipient() {
+    return 'recipient@example.org';
+  }
+
+  public function getSubject() {
+    return 'the subject';
+  }
+
+  public function getTokenContext() {
+    return array();
+  }
+}
