diff --git a/includes/simplenews.admin.inc b/includes/simplenews.admin.inc
index 1ed89b7..9350fc3 100644
--- a/includes/simplenews.admin.inc
+++ b/includes/simplenews.admin.inc
@@ -285,7 +285,7 @@ function simplenews_issue_send($nids) {
     }
 
     simplenews_update_sent_status($node);
-    simplenews_send_node($node);
+    simplenews_add_node_to_spool($node);
   }
 }
 
@@ -1445,6 +1445,23 @@ function simplenews_admin_settings_mail($form, &$form_state) {
     '#default_value' => variable_get('simplenews_use_cron', TRUE),
     '#description' => t('When checked cron will be used to send newsletters (recommended). Test newsletters and confirmation emails will be sent immediately. Leave unchecked for testing purposes.'),
   );
+
+  $sources = simplenews_get_source_caches();
+  $sources_labels = array();
+  $sources_descriptions = '';
+  foreach ($sources as $name => $source) {
+    $sources_labels[$name] = $source['label'];
+    $sources_descriptions .= t('<strong>@label</strong>: @description <br />', array('@label' => $source['label'], '@description' => $source['description']));
+  }
+
+  $form['simplenews_mail_backend']['simplenews_source_cache'] = array(
+    '#type' => 'select',
+    '#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'),
+  );
+
   $throttle = drupal_map_assoc(array(1, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000));
   $throttle[999999] = t('Unlimited');
   if (function_exists('getrusage')) {
diff --git a/includes/simplenews.mail.inc b/includes/simplenews.mail.inc
index 1506284..5e7749c 100644
--- a/includes/simplenews.mail.inc
+++ b/includes/simplenews.mail.inc
@@ -24,7 +24,7 @@
  *   - name: Emtpy; added for compatibility with user account object.
  *   - language: The language object of the subscriber's preferred language.
  */
-function simplenews_send_node($node, $accounts = array()) {
+function simplenews_add_node_to_spool($node, $accounts = array()) {
   if (is_numeric($node)) {
     $node = node_load($node);
   }
@@ -35,6 +35,7 @@ function simplenews_send_node($node, $accounts = array()) {
 
     // Get accounts subscribed to this newsletter.
     // Using hook_simplenews_recipients modules can add recipients.
+    // @todo: This the result of this hook is ignored, remove this.
     $recipients = simplenews_get_subscriptions_by_list($node->simplenews->tid);
     foreach (module_implements('simplenews_recipients_alter') as $module) {
       $function = $module . '_simplenews_recipients_alter';
@@ -83,7 +84,7 @@ function simplenews_send_node($node, $accounts = array()) {
     // in the spool. When cron is used newsletters are send to addresses in the
     // spool during the next (and following) cron run.
     if (variable_get('simplenews_use_cron', TRUE) == FALSE) {
-      simplenews_mail_spool($spool_data['nid'], 999999);
+      simplenews_mail_spool(99999);
       drupal_set_message(t('Newsletter sent.'));
       simplenews_clear_spool();
       simplenews_send_status_update();
@@ -99,152 +100,101 @@ function simplenews_send_node($node, $accounts = array()) {
  * Send test version of newsletter.
  *
  * @param mixed $node
- *   The newsletter node to be sent. If an integer, the node nid; if an object,
- *   the node object.
+ *   The newsletter node to be sent.
  */
 function simplenews_send_test($node, $test_addresses) {
-  if (is_numeric($node)) {
-    $node = node_load($node);
+  // Prevent session information from being saved while sending.
+  if ($original_session = drupal_save_session()) {
+    drupal_save_session(FALSE);
   }
-  if (is_object($node)) {
-    // Prevent session information from being saved while sending.
-    if ($original_session = drupal_save_session()) {
-      drupal_save_session(FALSE);
-    }
 
-    // Force the current user to anonymous to ensure consistent permissions.
-    $original_user = $GLOBALS['user'];
-    $GLOBALS['user'] = drupal_anonymous_user();
-
-    // Send the test newsletter to the test address(es) specified in the node.
-    // Build array of test email addresses
-
-    // Send newsletter to test addresses.
-    // Emails are send direct, not using the spool.
-    $recipients = array('anonymous' => array(), 'user' => array());
-    foreach ($test_addresses as $mail) {
-      $mail = trim($mail);
-      if (!empty($mail)) {
-        $message = new stdClass();
-        $message->nid = $node->nid;
-        $message->tid = $node->simplenews->tid;
-        $message->data = simplenews_get_subscription((object)array('mail' => $mail));
-        $account = _simplenews_user_load($mail);
-        $subscription = simplenews_get_subscription($account);
-        if ($account->uid) {
-          $recipients['user'][] = $account->name . ' <'.$mail.'>';
-        }
-        else {
-          $recipients['anonymous'][] = $mail;
-        }
-        $tmpres = simplenews_mail_mail($message, 'test');
+  // Force the current user to anonymous to ensure consistent permissions.
+  $original_user = $GLOBALS['user'];
+  $GLOBALS['user'] = drupal_anonymous_user();
+
+  // Send the test newsletter to the test address(es) specified in the node.
+  // Build array of test email addresses
+
+  // Send newsletter to test addresses.
+  // Emails are send direct, not using the spool.
+  $recipients = array('anonymous' => array(), 'user' => array());
+  foreach ($test_addresses as $mail) {
+    $mail = trim($mail);
+    if (!empty($mail)) {
+      $account = _simplenews_user_load($mail);
+      $subscriber = simplenews_get_subscription($account);
+      if ($account->uid) {
+        $recipients['user'][] = $account->name . ' <' . $mail . '>';
       }
+      else {
+        $recipients['anonymous'][] = $mail;
+      }
+      $source = new SimplenewsSourceNode($node, $subscriber);
+      $source->setKey('test');
+      $result = simplenews_send_source($source);
     }
-    if (count($recipients['user'])) {
-      $recipients_txt = implode(', ', $recipients['user']);
-      drupal_set_message(t('Test newsletter sent to user %recipient.', array('%recipient' => $recipients_txt)));
-    }
-    if (count($recipients['anonymous'])) {
-      $recipients_txt = implode(', ', $recipients['anonymous']);
-      drupal_set_message(t('Test newsletter sent to anonymous %recipient.', array('%recipient' => $recipients_txt)));
-    }
+  }
+  if (count($recipients['user'])) {
+    $recipients_txt = implode(', ', $recipients['user']);
+    drupal_set_message(t('Test newsletter sent to user %recipient.', array('%recipient' => $recipients_txt)));
+  }
+  if (count($recipients['anonymous'])) {
+    $recipients_txt = implode(', ', $recipients['anonymous']);
+    drupal_set_message(t('Test newsletter sent to anonymous %recipient.', array('%recipient' => $recipients_txt)));
+  }
 
-    $GLOBALS['user'] = $original_user;
-    if ($original_session) {
-      drupal_save_session(TRUE);
-    }
+  $GLOBALS['user'] = $original_user;
+  if ($original_session) {
+    drupal_save_session(TRUE);
   }
 }
 
 /**
  * Send a node to an email address.
  *
- * @param object $msgbase
- *   The mail message object as returned by simplenews_load_spool(), containing
- *   the following properties:
- *   - nid
- *   - tid
- *   - data
- * @param $key
- *   The email key, either 'node' or 'test'.
+ * @param $source
+ *   The source object.s
  *
  * @return boolean
  *   TRUE if the email was successfully delivered; otherwise FALSE.
  */
-function simplenews_mail_mail($msgbase, $key = 'node') {
+function simplenews_send_source(SimplenewsSourceInterface $source) {
+  $params['simplenews_source'] = $source;
 
-  $nid = $msgbase->nid;
-  $tid = $msgbase->tid;
-
-  $subscriber = $msgbase->data;
-  if (!$subscriber) {
-    $subscriber = simplenews_get_subscription((object)array('mail' => $msgbase->mail));
-    //$account = user_load($msgbase->uid));
-  }
-  $params['context']['account'] = $subscriber;
+  // Send mail.
+  $message = drupal_mail('simplenews', $source->getKey(), $source->getRecipient(), $source->getLanguage(), $params, $source->getFromFormatted());
 
-  // Get node data for the mail
-  $node = node_load($nid);
-
-  if (is_object($node)) {
-    $params['context']['node'] = $node;
-    $params['context']['newsletter'] = simplenews_newsletter_load($tid);
-    $params['context']['category'] = simplenews_category_load($tid);
-    $params['from'] = _simplenews_set_from($params['context']['category']);
-
-    // Optional params for Mime Mail.
-    $params['plain'] = $params['context']['category']->format == 'plain' ? TRUE : NULL;
-    // @todo Create the plaintext portion of the message, we don't have $message['body'] here.
-    // $params['plaintext'] = $params['plain'] ? $message['body'] : simplenews_html_to_text($message['body'], TRUE);
-    // @todo Get the attachments. Upload module no longer exists for Drupal 7.
-    // $params['attachments'] = isset($params['context']['node']->files) ? $params['context']['node']->files : array();;
-
-    // Send mail
-    $message = drupal_mail('simplenews', $key, $subscriber->mail, $subscriber->language, $params, $params['from']['formatted']);
-
-    // Log sent result in watchdog.
-    if (variable_get('simplenews_debug', FALSE)) {
-      $via_mimemail = '';
-      if (module_exists('mimemail')) {
-        $via_mimemail = t('Sent via Mime Mail');
-      }
-      // @todo Add line break before %mimemail.
-      if ($message['result']) {
-        watchdog('simplenews', 'Outgoing email. Message type: %type<br />Subject: %subject<br />Recipient: %to %mimemail', array('%type' => $key, '%to' => $message['to'], '%subject' => $message['subject'], '%mimemail' => $via_mimemail), WATCHDOG_DEBUG);
-      }
-      else {
-        watchdog('simplenews', 'Outgoing email failed. Message type: %type<br />Subject: %subject<br />Recipient: %to %mimemail', array('%type' => $key, '%to' => $message['to'], '%subject' => $message['subject'], '%mimemail' => $via_mimemail), WATCHDOG_ERROR);
-      }
+  // Log sent result in watchdog.
+  if (variable_get('simplenews_debug', FALSE)) {
+    $via_mimemail = '';
+    if (module_exists('mimemail')) {
+      $via_mimemail = t('Sent via Mime Mail');
     }
-
-    // Build array of sent results for spool table and reporting.
+    // @todo Add line break before %mimemail.
     if ($message['result']) {
-      $message['result'] = array(
-        'status' => SIMPLENEWS_SPOOL_DONE,
-        'error' => FALSE,
-      );
+      watchdog('simplenews', 'Outgoing email. Message type: %type<br />Subject: %subject<br />Recipient: %to %mimemail', array('%type' => $source->getKey(), '%to' => $message['to'], '%subject' => $message['subject'], '%mimemail' => $via_mimemail), WATCHDOG_DEBUG);
     }
     else {
-      // This error may be caused by faulty mailserver configuration or overload.
-      // Mark "pending" to keep trying.
-      $message['result'] = array(
-        'status' => SIMPLENEWS_SPOOL_PENDING,
-        'error' => TRUE,
-      );
+      watchdog('simplenews', 'Outgoing email failed. Message type: %type<br />Subject: %subject<br />Recipient: %to %mimemail', array('%type' => $source->getKey(), '%to' => $message['to'], '%subject' => $message['subject'], '%mimemail' => $via_mimemail), WATCHDOG_ERROR);
     }
+  }
 
+  // Build array of sent results for spool table and reporting.
+  if ($message['result']) {
+    $result = array(
+      'status' => SIMPLENEWS_SPOOL_DONE,
+      'error' => FALSE,
+    );
   }
   else {
-    // Node could not be loaded. The node is probably deleted while pending to be sent.
-    // This error is not recoverable, mark "done".
-    $message['result'] = array(
-      'status' => SIMPLENEWS_SPOOL_DONE,
+    // This error may be caused by faulty mailserver configuration or overload.
+    // Mark "pending" to keep trying.
+    $result = array(
+      'status' => SIMPLENEWS_SPOOL_PENDING,
       'error' => TRUE,
     );
-    watchdog('simplenews', 'Newsletter not send: newsletter issue does not exist (nid = @nid).', array('@nid' => $nid), WATCHDOG_ERROR);
   }
-
-  return isset($message['result']) ? $message['result'] : FALSE;
+  return $result;
 }
 
 /**
@@ -254,11 +204,10 @@ function simplenews_mail_mail($msgbase, $key = 'node') {
  * Sending is triggered by cron or immediately when the node is saved.
  * Mail data is retrieved from the spool, rendered and send one by one
  * If sending is successful the message is marked as send in the spool.
- * @todo Replace time(): http://drupal.org/node/224333#time
  *
  * TODO: Redesign API to allow language counter in multilingual sends.
  */
-function simplenews_mail_spool($nid = NULL, $limit = NULL) {
+function simplenews_mail_spool($limit = NULL) {
   $check_counter = 0;
 
   // Send pending messages from database cache
@@ -278,18 +227,23 @@ function simplenews_mail_spool($nid = NULL, $limit = NULL) {
 
     _simplenews_measure_usec(TRUE);
 
-    foreach ($spool_list as $msid => $spool_data) {
-      $result = simplenews_mail_mail($spool_data);
+    $spool = new SimplenewsSpool($spool_list);
+    while ($source = $spool->nextSource()) {
+      $source->setKey('node');
+      $result = simplenews_send_source($source);
 
       // Update spool status.
       // This is not optimal for performance but prevents duplicate emails
       // in case of PHP execution time overrun.
-      simplenews_update_spool(array($msid), $result);
-      if ($result['status'] == SIMPLENEWS_SPOOL_DONE) {
-        $count_success++;
-      }
-      if ($result['error']) {
-        $count_fail++;
+      foreach ($spool->getProcessed() as $msid => $row) {
+        $row_result = isset($row->result) ? $row->result : $result;
+        simplenews_update_spool(array($msid), $row_result);
+        if ($row_result['status'] == SIMPLENEWS_SPOOL_DONE) {
+          $count_success++;
+        }
+        if ($row_result['error']) {
+          $count_fail++;
+        }
       }
 
       // Check every n emails if we exceed the limit.
@@ -304,6 +258,19 @@ function simplenews_mail_spool($nid = NULL, $limit = NULL) {
           break;
         }
       }
+
+      // It is possible that all or at the end some results failed to get
+      // prepared, report them separately.
+      foreach ($spool->getProcessed() as $msid => $row) {
+        $row_result = isset($row->result) ? $row->result : $result;
+        simplenews_update_spool(array($msid), $row_result);
+        if ($row_result['status'] == SIMPLENEWS_SPOOL_DONE) {
+          $count_success++;
+        }
+        if ($row_result['error']) {
+          $count_fail++;
+        }
+      }
     }
 
     // Report sent result and elapsed time. On Windows systems getrusage() is
@@ -324,7 +291,30 @@ function simplenews_mail_spool($nid = NULL, $limit = NULL) {
       drupal_save_session(TRUE);
     }
   }
+}
+
+/**
+ * Returns the source object for the provided spool data.
+ *
+ * @param $spool_data
+ *   The mail message object as returned by simplenews_load_spool(), containing
+ *   the following properties:
+ *   - nid
+ *   - tid
+ *   - data
+ *
+ * @return SimplenewsSourceInterface
+ *   An impementation of the source interface.
+ */
+function simplenews_get_source_from_spool($spool_list) {
+  $source_class = variable_get('simplenews_source', 'SimplenewsSourceNode');
 
+  // Check if this is a valid source.
+  if (!class_exists($source_class) || !($source_class instanceof SimplenewsSpoolSourceInterface)) {
+    // If not, fall back to default.
+    $source_class = 'SimplenewsSourceNode';
+  }
+  return $source_class;
 }
 
 /**
@@ -600,83 +590,18 @@ function simplenews_send_status_update() {
 }
 
 /**
- * Build header array with priority and receipt confirmation settings.
- *
- * @param $node
- *   Newsletter category object.
- * @param $from
- *   Newsletter from email address
- *
- * @return Header array with priority and receipt confirmation info
- */
-function _simplenews_headers($category, $from) {
-  $headers = array();
-
-  // If receipt is requested, add headers.
-  if ($category->receipt) {
-    $headers['Disposition-Notification-To'] = $from;
-    $headers['X-Confirm-Reading-To'] = $from;
-  }
-
-  // Add priority if set.
-  switch ($category->priority) {
-    case SIMPLENEWS_PRIORITY_HIGHEST:
-      $headers['Priority'] = 'High';
-      $headers['X-Priority'] = '1';
-      $headers['X-MSMail-Priority'] = 'Highest';
-      break;
-    case SIMPLENEWS_PRIORITY_HIGH:
-      $headers['Priority'] = 'urgent';
-      $headers['X-Priority'] = '2';
-      $headers['X-MSMail-Priority'] = 'High';
-      break;
-    case SIMPLENEWS_PRIORITY_NORMAL:
-      $headers['Priority'] = 'normal';
-      $headers['X-Priority'] = '3';
-      $headers['X-MSMail-Priority'] = 'Normal';
-      break;
-    case SIMPLENEWS_PRIORITY_LOW:
-      $headers['Priority'] = 'non-urgent';
-      $headers['X-Priority'] = '4';
-      $headers['X-MSMail-Priority'] = 'Low';
-      break;
-    case SIMPLENEWS_PRIORITY_LOWEST:
-      $headers['Priority'] = 'non-urgent';
-      $headers['X-Priority'] = '5';
-      $headers['X-MSMail-Priority'] = 'Lowest';
-      break;
-  }
-
-  // Add general headers
-  $headers['Precedence'] = 'bulk';
-
-  return $headers;
-}
-
-/**
  * Build formatted from-name and email for a mail object.
  *
- * Each newsletter category can have a different from address.
- *
- * @param $category
- *   Newsletter category object.
- *
  * @return Associative array with (un)formatted from address
  *  'address'   => From address
  *  'formatted' => Formatted, mime encoded, from name and address
  */
-function _simplenews_set_from($category = NULL) {
+function _simplenews_set_from() {
   $address_default = variable_get('site_mail', ini_get('sendmail_from'));
   $name_default = variable_get('site_name', 'Drupal');
 
-  if ($category) {
-    $address = $category->from_address;
-    $name = $category->from_name;
-  }
-  else {
-    $address = variable_get('simplenews_from_address', $address_default);
-    $name = variable_get('simplenews_from_name', $name_default);
-  }
+  $address = variable_get('simplenews_from_address', $address_default);
+  $name = variable_get('simplenews_from_name', $name_default);
 
   // Windows based PHP systems don't accept formatted emails.
   $formatted_address = substr(PHP_OS, 0, 3) == 'WIN' ? $address : '"' . mime_header_encode($name) . '" <' . $address . '>';
@@ -803,3 +728,85 @@ function _simplenews_measure_usec($start = FALSE) {
   }
   return $now - $start_time;
 }
+
+
+/**
+ * Build subject and body of the test and normal newsletter email.
+ *
+ * @param array $message
+ *   Message array as used by hook_mail().
+ * @param array $source
+ *   The SimplenewsSource instance.
+ */
+function simplenews_build_newsletter_mail(&$message, SimplenewsSourceInterface $source) {
+  // 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']['footer'] = $source->getFooter();
+
+  // Optional params for HTML mails.
+  if ($source->getFormat() == 'html') {
+    $message['params']['plain'] = NULL;
+    $message['params']['plaintext'] = $source->getPlainBody();
+    $message['params']['attachments'] = $source->getAttachments();
+  }
+  else {
+    $message['params']['plain'] = TRUE;
+  }
+}
+
+/**
+ * Build subject and body of the subscribe confirmation email.
+ *
+ * @param array $message
+ *   Message array as used by hook_mail().
+ * @param array $params
+ *   Parameter array as used by hook_mail().
+ */
+function simplenews_build_subscribe_mail(&$message, $params) {
+  $context = $params['context'];
+  $langcode = $message['language'];
+
+  // Use formatted from address "name" <mail_address>
+  $message['headers']['From'] = $params['from']['formatted'];
+
+  $message['subject'] = simplenews_subscription_confirmation_text('subscribe_subject', $langcode);
+  $message['subject'] = token_replace($message['subject'], $context, array('sanitize' => FALSE));
+
+  if (simplenews_user_is_subscribed($context['account']->mail, $context['category']->tid)) {
+    $body = simplenews_subscription_confirmation_text('subscribe_subscribed', $langcode);
+  }
+  else {
+    $body = simplenews_subscription_confirmation_text('subscribe_unsubscribed', $langcode);
+  }
+    $message['body'][] = token_replace($body, $context, array('sanitize' => FALSE));
+}
+
+/**
+ * Build subject and body of the unsubscribe confirmation email.
+ *
+ * @param array $message
+ *   Message array as used by hook_mail().
+ * @param array $params
+ *   Parameter array as used by hook_mail().
+ */
+function simplenews_build_unsubscribe_mail(&$message, $params) {
+  $context = $params['context'];
+  $langcode = $message['language'];
+
+  // Use formatted from address "name" <mail_address>
+  $message['headers']['From'] = $params['from']['formatted'];
+
+  $message['subject'] = simplenews_subscription_confirmation_text('subscribe_subject', $langcode);
+  $message['subject'] = token_replace($message['subject'], $context, array('sanitize' => FALSE));
+
+  if (simplenews_user_is_subscribed($context['account']->mail, $context['category']->tid)) {
+    $body = simplenews_subscription_confirmation_text('unsubscribe_subscribed', $langcode);
+    $message['body'][] = token_replace($body, $context, array('sanitize' => FALSE));
+  }
+  else {
+    $body = simplenews_subscription_confirmation_text('unsubscribe_unsubscribed', $langcode);
+    $message['body'][] = token_replace($body, $context, array('sanitize' => FALSE));
+  }
+}
\ No newline at end of file
diff --git a/includes/simplenews.source.inc b/includes/simplenews.source.inc
new file mode 100644
index 0000000..19bfdd8
--- /dev/null
+++ b/includes/simplenews.source.inc
@@ -0,0 +1,838 @@
+<?php
+
+/**
+ * @file
+ * Contains SimplenewsSource interface and implementations.
+ */
+
+/**
+ * The source used to build a newsletter mail.
+ */
+interface SimplenewsSourceInterface {
+
+  /**
+   * Returns the mail headers.
+   *
+   * @param $headers
+   *   The default mail headers.
+   *
+   * @return
+   *   Mail headers as an array.
+   */
+  function getHeaders(array $headers);
+
+  /**
+   * Returns the mail subject.
+   */
+  function getSubject();
+
+  /**
+   * Returns the mail body.
+   */
+  function getBody();
+
+  /**
+   * Returns the plaintext body.
+   */
+  function getPlainBody();
+
+  /**
+   * Returns the mail footer.
+   */
+  function getFooter();
+
+  /**
+   * Returns the mail format.
+   *
+   * @return
+   *   The mail format as string, either 'plain' or 'html'.
+   */
+  function getFormat();
+
+  /**
+   * Returns the recipent of this newsletter mail.
+   *
+   * @return
+   *   The recipient mail address(es) of this newsletter as a string.
+   */
+  function getRecipient();
+
+  /**
+   * The language that should be used for this newsletter mail.
+   */
+  function getLanguage();
+
+  /**
+   * Returns an array of attachments for this newsletter mail.
+   *
+   * @return
+   *   An array of managed file objects with properties uri, filemime and so on.
+   */
+  function getAttachments();
+
+  /**
+   * Returns the token context to be used with token replacements.
+   *
+   * @return
+   *   An array of objects as required by token_replace().
+   */
+  function getTokenContext();
+
+  /**
+   * Returns the mail key to be used for drupal_mail().
+   *
+   * @return
+   *   The mail key, either test or node.
+   */
+  function getKey();
+
+  /**
+   * Returns the formatted from mail address.
+   */
+  function getFromFormatted();
+
+  /**
+   * Returns the plain mail address.
+   */
+  function getFromAddress();
+}
+
+/**
+ * Source interface based on a node.
+ *
+ * This is the interface that needs to be implemented to be compatible with
+ * the default simplenews spool implementation and therefore exposed in
+ * hook_simplenews_source_cache_info().
+ */
+interface SimplenewsSourceNodeInterface extends SimplenewsSourceInterface {
+
+  /**
+   * Create a source based on a node and subscriber.
+   */
+  function __construct($node, $subscriber);
+
+  /**
+   * Returns the actually used node of this source.
+   */
+  function getNode();
+}
+
+/**
+ * Interface for a simplenews source cache implementation.
+ *
+ * This is only compatible with the SimplenewsSpoolSourceInterface interface.
+ */
+interface SimplenewsSourceCacheInterface {
+
+  /**
+   * Create a new instance, allows to initialize based on the used
+   * source.
+   */
+  function __construct(SimplenewsSourceNodeInterface $source);
+
+  /**
+   * Return a cached element, if existing.
+   *
+   * Although group and key can be used to identify the requested cache, the
+   * implementations are responsible to create a unique cache key themself using
+   * the $source. For example based on the node id and the language.
+   *
+   * @param $group
+   *   Group of the cache key, which allows cache implementations to decide what
+   *   they want to cache. Currently used groups:
+   *     - data: Raw data, e.g. attachments.
+   *     - build: Built and themed content, before personalizations like tokens.
+   *     - final: The final returned data. Caching this means that newsletter
+   *       can not be personalized anymore.
+   * @param $key
+   *   Identifies the requested element, e.g. body, footer or attachments.
+   */
+  function get($group, $key);
+
+  /**
+   * Write an element to the cache.
+   *
+   * Although group and key can be used to identify the requested cache, the
+   * implementations are responsible to create a unique cache key themself using
+   * the $source. For example based on the node id and the language.
+   *
+   * @param $group
+   *   Group of the cache key, which allows cache implementations to decide what
+   *   they want to cache. Currently used groups:
+   *     - data: Raw data, e.g. attachments.
+   *     - build: Built and themed content, before personalizations like tokens.
+   *     - final: The final returned data. Caching this means that newsletter
+   *       can not be personalized anymore.
+   * @param $key
+   *   Identifies the requested element, e.g. body, footer or attachments.
+   * @param $data
+   *   The data to be saved in the cache.
+   */
+  function set($group, $key, $data);
+}
+
+/**
+ * A Simplenews spool implementation is a factory for Simplenews sources.
+ *
+ * Their main functionility is to return a number of sources based on the passed
+ * in array of mail spool rows. Additionally, it needs to return the processed
+ * mail rows after a source was sent.
+ *
+ * @todo: Move spool functions into this interface.
+ */
+interface SimplenewsSpoolInterface {
+
+  /**
+   * Initalizes the spool implementation.
+   *
+   * @param $spool_list
+   *   An array of rows from the {simplenews_mail_spool} table.
+   */
+  function __construct($pool_list);
+
+  /**
+   * Returns a Simplenews source to be sent.
+   *
+   * A single source may represent any number of mail spool rows, e.g. by
+   * addressing them as BCC.
+   */
+  function nextSource();
+
+  /**
+   * Returns the processed mail spool rows, keyed by the msid.
+   *
+   * Only rows that were processed while preparing the previously returned
+   * source must be returned.
+   *
+   * @return
+   *   An array of mail spool rows, keyed by the msid. Can optionally have set
+   *   the following additional properties.
+   *     - actual_nid: In case of content translation, the source node that was
+   *       used for this mail.
+   *     - error: FALSE if the prepration for this row failed. For example set
+   *       when the corresponding node failed to load.
+   *     - status: A simplenews spool status to indicate the status.
+   */
+  function getProcessed();
+}
+
+/**
+ * Abstract base class for simplenews sources.
+ */
+class SimplenewsSpool implements SimplenewsSpoolInterface {
+
+  /**
+   * Array with mail spool rows being processed.
+   *
+   * @var array
+   */
+  protected $spool_list;
+
+  /**
+   * Array of the processed mail spool rows.
+   */
+  protected $processed = array();
+
+  /**
+   * Implements SimplenewsSpoolInterface::_construct($spool_list);
+   */
+  public function __construct($spool_list) {
+    $this->spool_list = $spool_list;
+  }
+
+  /**
+   * Implements SimplenewsSpoolInterface::nextSource();
+   */
+  public function nextSource() {
+    // Get the current mail spool row and update the internal pointer to the
+    // next row.
+    $return = each($this->spool_list);
+    // If we're done, return false.
+    if (!$return) {
+      return FALSE;
+    }
+    $spool_data = $return['value'];
+
+    // Store this spool row as processed.
+    $this->processed[$spool_data->msid] = $spool_data;
+
+    $node = node_load($spool_data->nid);
+    if (!$node) {
+      // If node the load failed, set the processed status done and proceed with
+      // the next mail.
+      $this->processed[$spool_data->msid]->result = array(
+        'status' => SIMPLENEWS_SPOOL_DONE,
+        'error' => TRUE
+      );
+      return $this->prepareMail();
+    }
+
+    if ($spool_data->data) {
+      $subscriber = $spool_data->data;
+    }
+    else {
+      $subscriber = simplenews_get_subscription(_simplenews_user_load($spool_data->mail));
+    }
+
+    $source_class = $this->getSourceImplementation($spool_data);
+    $source = new $source_class($node, $subscriber);
+
+    // Set which node is actually used. In case of a translation set, this might
+    // not be the same node.
+    $this->processed[$spool_data->msid]->actual_nid = $source->getNode()->nid;
+    return $source;
+  }
+
+  /**
+   * Implements SimplenewsSpoolInterface::getProcessed();
+   */
+  function getProcessed() {
+    $processed = $this->processed;
+    $this->processed = array();
+    return $processed;
+  }
+
+  /**
+   * Return the Simplenews source implementation for the given mail spool row.
+   */
+  protected function getSourceImplementation($spool_data) {
+    return variable_get('simplenews_source', 'SimplenewsSourceNode');
+  }
+}
+
+/**
+ * Simplenews source implementation based on nodes for a single subscriber.
+ */
+class SimplenewsSourceNode implements SimplenewsSourceNodeInterface {
+
+  /**
+   * The node object.
+   */
+  protected $node;
+
+  /**
+   * The cached build render array.
+   */
+  protected $build;
+
+  /**
+   * The newsletter category.
+   */
+  protected $category;
+
+  /**
+   * The subscriber and therefore recipient of this mail.
+   */
+  protected $subscriber;
+
+  /**
+   * The mail key used for drupal_mail().
+   */
+  protected $key = 'test';
+
+  /**
+   * The simplenews newsletter.
+   */
+  protected $newsletter;
+
+  /**
+   * Cache implementation used for this source.
+   *
+   * @var SimplenewsSourceCacheInterface
+   */
+  protected $cache;
+
+  /**
+   * Implements SimplenewsSourceInterface::_construct();
+   */
+  public function __construct($node, $subscriber) {
+    $this->setSubscriber($subscriber);
+    $this->setNode($node);
+    $this->newsletter = simplenews_newsletter_load($node->nid);
+    $this->category = simplenews_category_load($this->newsletter->tid);
+    $this->initCache();
+  }
+
+  /**
+   * Set the node of this source.
+   *
+   * If the node is part of a translation set, switch to the node for the
+   * requested language, if existent.
+   */
+  public function setNode($node) {
+    $langcode = $this->getLanguage();
+    $nid = $node->nid;
+    if (module_exists('translation')) {
+      // If the node has translations and a translation is required
+      // the equivalent of the node in the required language is used
+      // or the base node (nid == tnid) is used.
+      if ($tnid = $node->tnid) {
+        if ($langcode != $node->language) {
+          $translations = translation_node_get_translations($tnid);
+          // A translation is available in the preferred language.
+          if ($translation = $translations[$langcode]) {
+            $nid = $translation->nid;
+            $langcode = $translation->language;
+          }
+          else {
+            // No translation found which matches the preferred language.
+            foreach ($translations as $translation) {
+              if ($translation->nid == $tnid) {
+                $nid = $tnid;
+                $langcode = $translation->language;
+                break;
+              }
+            }
+          }
+        }
+      }
+    }
+    // If a translation of the node is used, load this node.
+    if ($nid != $node->nid) {
+      $this->node = node_load($nid);
+    }
+    else {
+      $this->node = $node;
+    }
+  }
+
+  /**
+   * Initialize the cache implementation.
+   */
+  protected function initCache() {
+    $class = variable_get('simplenews_source_cache', 'SimplenewsSourceCacheBuild');
+    $this->cache = new $class($this);
+  }
+
+  /**
+   * Returns the corresponding category.
+   */
+  public function getCategory() {
+    return $this->category;
+  }
+
+  /**
+   * Set the active subscriber.
+   */
+  public function setSubscriber($subscriber) {
+    $this->subscriber = $subscriber;
+  }
+
+  /**
+   * Return the subscriber object.
+   */
+  public function getSubscriber() {
+    return $this->subscriber;
+  }
+
+  /**
+   * Implements SimplenewsSourceInterface::getHeaders().
+   */
+  public function getHeaders(array $headers) {
+
+    // If receipt is requested, add headers.
+    if ($this->category->receipt) {
+      $headers['Disposition-Notification-To'] = $from;
+      $headers['X-Confirm-Reading-To'] = $from;
+    }
+
+    // Add priority if set.
+    switch ($this->category->priority) {
+      case SIMPLENEWS_PRIORITY_HIGHEST:
+        $headers['Priority'] = 'High';
+        $headers['X-Priority'] = '1';
+        $headers['X-MSMail-Priority'] = 'Highest';
+        break;
+      case SIMPLENEWS_PRIORITY_HIGH:
+        $headers['Priority'] = 'urgent';
+        $headers['X-Priority'] = '2';
+        $headers['X-MSMail-Priority'] = 'High';
+        break;
+      case SIMPLENEWS_PRIORITY_NORMAL:
+        $headers['Priority'] = 'normal';
+        $headers['X-Priority'] = '3';
+        $headers['X-MSMail-Priority'] = 'Normal';
+        break;
+      case SIMPLENEWS_PRIORITY_LOW:
+        $headers['Priority'] = 'non-urgent';
+        $headers['X-Priority'] = '4';
+        $headers['X-MSMail-Priority'] = 'Low';
+        break;
+      case SIMPLENEWS_PRIORITY_LOWEST:
+        $headers['Priority'] = 'non-urgent';
+        $headers['X-Priority'] = '5';
+        $headers['X-MSMail-Priority'] = 'Lowest';
+        break;
+    }
+
+    // Add user specific header data.
+    $message['headers']['From'] = $this->getFromFormatted();
+    $message['headers']['List-Unsubscribe'] = '<' . token_replace('[simplenews-subscriber:unsubscribe-url]', $this->getTokenContext(), array('sanitize' => FALSE)) . '>';
+
+    // Add general headers
+    $headers['Precedence'] = 'bulk';
+    return $headers;
+  }
+
+  /**
+   * Implements SimplenewsSourceInterface::getTokenContext().
+   */
+  function getTokenContext() {
+    return array(
+      'category' => $this->getCategory(),
+      'account' => $this->getSubscriber(),
+      'node' => $this->getNode(),
+    );
+  }
+
+  /**
+   * Set the mail key.
+   */
+  function setKey($key) {
+    $this->key = $key;
+  }
+
+  /**
+   * Implements SimplenewsSourceInterface::getKey().
+   */
+  function getKey() {
+    return $this->key;
+  }
+
+  /**
+   * Implements SimplenewsSourceInterface::getFromFormatted().
+   */
+  function getFromFormatted() {
+    $name = $this->getCategory()->from_name;
+
+    // Windows based PHP systems don't accept formatted emails.
+    if (drupal_substr(PHP_OS, 0, 3) == 'WIN') {
+      return $this->getFromAddress();
+    }
+    else {
+      return '"' . mime_header_encode($name) . '" <' . $this->getFromAddress() . '>';
+    }
+
+    return $formatted_address;
+  }
+
+  /**
+   * Implements SimplenewsSourceInterface::getFromAddress().
+   */
+  function getFromAddress() {
+    return $this->getCategory()->from_address;
+  }
+
+  /**
+   * Implements SimplenewsSourceInterface::getRecipient().
+   */
+  function getRecipient() {
+    return $this->getSubscriber()->mail;
+  }
+
+  /**
+   * Implements SimplenewsSourceInterface::getFormat().
+   */
+  function getFormat() {
+    return $this->getCategory()->format;
+  }
+
+  /**
+   * Implements SimplenewsSourceInterface::getLanguage().
+   */
+  function getLanguage() {
+    return $this->getSubscriber()->language;
+  }
+
+  /**
+   * Implements SimplenewsSourceSpoolInterface::getNode().
+   */
+  function getNode() {
+    return $this->node;
+  }
+
+  /**
+   * Implements SimplenewsSourceInterface::getSubject().
+   */
+  function getSubject() {
+    // Build email subject and perform some sanitizing.
+    $subject = token_replace($this->getCategory()->email_subject, $this->getTokenContext(), array('sanitize' => FALSE));
+
+    // Line breaks are removed from the email subject to prevent injection of
+    // malicious data into the email header.
+    $subject = str_replace(array("\r", "\n"), '', $subject);
+    return $subject;
+  }
+
+  /**
+   * Overwrites the current content language for i18n_select.
+   */
+  protected function setContentLanguage($language) {
+    if (module_exists('i18n_select')) {
+      $this->original_language = $GLOBALS['language_content'];
+      $languages = language_list();
+      $GLOBALS['language_content'] = $languages[$language];
+    }
+  }
+
+  /**
+   * Reset the content language back to the previous value.
+   */
+  protected function resetContentLanguage() {
+    if (module_exists('i18n_select')) {
+      $GLOBALS['language_content'] = $this->original_language;
+    }
+  }
+
+  /**
+   * Build the node object.
+   *
+   * The resulting build array is cached as it is used in multiple places.
+   * @param $format
+   *   (Optional) Override the default format. Defaults to getFormat().
+   */
+  protected function build($format = NULL) {
+    if (empty($format)) {
+      $format = $this->getFormat();
+    }
+    if (!empty($this->build[$format])) {
+      return $this->build[$format];
+    }
+
+    // Set the active language to the node's language.
+    // This needs to be done as otherwise the language used to send the mail
+    // is the language of the user logged in.
+    $this->setContentLanguage($this->node->language);
+
+    // Build message body
+    // Supported view modes: 'email_plain', 'email_html', 'email_textalt'
+    $build = node_view($this->node, 'email_' . $format);
+    unset($build['#theme']);
+
+    foreach (field_info_instances('node', $this->node->type) as $field_name => $field) {
+      if (isset($build[$field_name])) {
+        $build[$field_name]['#theme'] = 'simplenews_field';
+      }
+    }
+
+    $this->resetContentLanguage();
+
+    $this->build = $build;
+    return $build;
+  }
+
+  /**
+   * Build the themed newsletter body.
+   *
+   * @param $format
+   *   (Optional) Override the default format. Defaults to getFormat().
+   */
+  protected function buildBody($format = NULL) {
+    if (empty($format)) {
+      $format = $this->getFormat();
+    }
+    if ($cache = $this->cache->get('build', 'body:' . $format)) {
+      return $cache;
+    }
+    $body = theme('simplenews_newsletter_body', array('build' => $this->build($format), 'category' => $this->getCategory(), 'language' => $this->getLanguage()));
+    $this->cache->set('build', 'body', $body);
+    return $body;
+  }
+
+  /**
+   * Implements SimplenewsSourceInterface::getBody().
+   */
+  public function getBody() {
+    $body = $this->buildBody();
+
+    // Build message body, replace tokens.
+    $body = token_replace($body, $this->getTokenContext(), array('sanitize' => FALSE));
+    return $body;
+  }
+
+  /**
+   * Implements SimplenewsSourceInterface::getBody().
+   */
+  public function getPlainBody() {
+    $body = $this->buildBody('plain');
+
+    // Build message body, replace tokens.
+    $body = token_replace($body, $this->getTokenContext(), array('sanitize' => FALSE));
+    $body = simplenews_html_to_text($body, $this->getCategory()->hyperlinks);
+    return $body;
+  }
+
+  /**
+   * Builds the themed footer.
+   */
+  protected function buildFooter() {
+    if ($cache = $this->cache->get('build', 'footer')) {
+      return $cache;
+    }
+
+    // Build and buffer message footer
+    $footer = theme('simplenews_newsletter_footer', array(
+      'build' => $this->build(),
+      'category' => $this->getCategory(),
+      'context' => $this->getTokenContext(),
+      'key' => $this->getKey(),
+      'language' => $this->getLanguage(),
+      )
+    );
+    $this->cache->set('build', 'footer', $footer);
+    return $footer;
+  }
+
+  /**
+   * Implements SimplenewsSourceInterface::getFooter().
+   */
+  function getFooter() {
+    if ($cache = $this->cache->get('final', 'footer')) {
+      return $cache;
+    }
+
+    if ($this->category->opt_inout != SIMPLENEWS_OPT_INOUT_HIDDEN) {
+      $final_footer = token_replace($this->buildFooter(), $this->getTokenContext(), array('sanitize' => FALSE));
+    }
+
+    $this->cache->set('build', 'footer', $final_footer);
+    return $final_footer;
+  }
+
+  /**
+   * Implements SimplenewsSourceInterface::getAttachments().
+   */
+  function getAttachments() {
+    if ($cache = $this->cache->get('data', 'attachments')) {
+      return $cache;
+    }
+
+    $attachments = array();
+    $build = $this->build();
+    $fids = array();
+    foreach (field_info_instances('node', $this->node->type) as $field_name => $field_instance) {
+      // @todo: Find a better way to support more field types.
+      // Only add fields of type file which are enabled for the current view
+      // mode as attachments.
+      $field = field_info_field($field_name);
+      if ($field['type'] == 'file' && isset($build[$field_name])) {
+
+        if ($items = field_get_items('node', $node, $field_name)) {
+          foreach ($items as $item) {
+            $fids[] = $item['fid'];
+          }
+        }
+      }
+    }
+    if (!empty($fids)) {
+      $attachments = file_load_multiple($fids);
+    }
+
+    $this->cache->set('data', 'attachments', $attachments);
+    return $attachments;
+  }
+}
+
+/**
+ * Abstract implementation of the source caching that does static caching.
+ *
+ * Subclasses need to implement the abstract function isCacheable() to decide
+ * what should be cached.
+ */
+abstract class SimplenewsSourceCacheStatic implements SimplenewsSourceCacheInterface {
+
+  /**
+   * The simplenews source for which this cache is used.
+   *
+   * @var SimplenewsSourceNodeInterface
+   */
+  protected $source;
+
+  /**
+   * The cache identifier for the given source.
+   */
+  protected $cid;
+
+  /**
+   * The static cache.
+   */
+  protected static $cache = array();
+
+  /**
+   * Implements SimplenewsSourceNodeInterface::__construct().
+   */
+  public function __construct(SimplenewsSourceNodeInterface $source) {
+    $this->source = $source;
+  }
+
+  /**
+   * Returns the cache identifier for the current source.
+   */
+  protected function getCid() {
+    if (empty($this->cid)) {
+      $this->cid = $this->source->getNode()->nid . ':' . $this->source->getLanguage();
+    }
+    return $this->cid;
+  }
+
+  /**
+   * Implements SimplenewsSourceNodeInterface::get().
+   */
+  public function get($group, $key) {
+    if (!$this->isCacheable($group, $key)) {
+      return;
+    }
+
+    if (isset(self::$cache[$this->getCid()][$group][$key])) {
+      return self::$cache[$this->getCid()][$group][$key];
+    }
+  }
+
+  /**
+   * Implements SimplenewsSourceNodeInterface::set().
+   */
+  public function set($group, $key, $data) {
+    if (!$this->isCacheable($group, $key)) {
+      return;
+    }
+
+    self::$cache[$this->getCid()][$group][$key] = $data;
+  }
+
+  /**
+   * Return if the requested element should be cached.
+   *
+   * @return
+   *   TRUE if it should be cached, FALSE otherwise.
+   */
+  abstract function isCacheable($group, $key);
+}
+
+/**
+ * Cache implementation that does not cache anything at all.
+ */
+class SimplenewsSourceCacheNone extends SimplenewsSourceCacheStatic {
+
+  /**
+   * Implements SimplenewsSourceCacheStatic::set().
+   */
+  public function isCacheable($group, $key) {
+    return FALSE;
+  }
+
+}
+
+/**
+ * Source cache implementation that caches build and data element.
+ */
+class SimplenewsSourceCacheBuild extends SimplenewsSourceCacheStatic {
+
+  /**
+   * Implements SimplenewsSourceCacheStatic::set().
+   */
+  function isCacheable($group, $key) {
+     // Only cache data and build information.
+    return in_array($group, array('data', 'build'));
+  }
+
+}
diff --git a/simplenews.info b/simplenews.info
index 31a1794..4ae68e3 100644
--- a/simplenews.info
+++ b/simplenews.info
@@ -8,6 +8,7 @@ dependencies[] = taxonomy
 test_dependencies[] = i18n_taxonomy
 
 files[] = tests/simplenews.test
+files[] = includes/simplenews.source.inc
 files[] = includes/views/handlers/simplenews_handler_field_newsletter_status.inc
 files[] = includes/views/handlers/simplenews_handler_field_newsletter_priority.inc
 files[] = includes/views/handlers/simplenews_handler_field_category_hyperlinks.inc
diff --git a/simplenews.module b/simplenews.module
index 30841f4..1fd723c 100644
--- a/simplenews.module
+++ b/simplenews.module
@@ -458,7 +458,7 @@ function simplenews_node_update($node) {
   if ($node->simplenews->status == SIMPLENEWS_STATUS_SEND_PUBLISH && $node->status == NODE_PUBLISHED) {
     module_load_include('inc', 'simplenews', 'includes/simplenews.mail');
     simplenews_update_sent_status($node);
-    simplenews_send_node($node);
+    simplenews_add_node_to_spool($node);
   }
   else {
     // simplenews_update_sent_status() already saves the node, only need to do
@@ -1837,12 +1837,12 @@ function simplenews_recent_newsletters($tid, $count = 5) {
  *     to user_mail_tokens().
  */
 function simplenews_mail($key, &$message, $params) {
-  $params['key'] = $key;
+  module_load_include('inc', 'simplenews', 'includes/simplenews.mail');
 
   switch ($key) {
     case 'node':
     case 'test':
-      simplenews_build_node_mail($message, $params);
+      simplenews_build_newsletter_mail($message, $params['simplenews_source']);
       break;
     case 'subscribe':
       simplenews_build_subscribe_mail($message, $params);
@@ -1860,226 +1860,6 @@ function simplenews_mail($key, &$message, $params) {
 }
 
 /**
- * Build subject and body of the test and normal newsletter email.
- *
- * @param array $message
- *   Message array as used by hook_mail().
- * @param array $params
- *   Parameter array as used by hook_mail().
- */
-function simplenews_build_node_mail(&$message, $params) {
-  $context = $params['context'];
-  $category = $context['category'];
-  $key = $params['key'];
-
-  // Message header, body and mail headers are buffered to increase
-  // performance when sending multiple mails. Buffered data only contains
-  // general data, no recipient specific content. Tokens are used
-  // for recipient data and will later be replaced.
-  // When mailing multiple newsletters in one page call or cron run,
-  // data is once stored and subsequently retrieved from the
-  // static $messages variable.
-  // $message buffer is node and language specific.
-  $messages = &drupal_static(__FUNCTION__, array());
-
-  // By default the the node is send which is supplied in the function call.
-  // When translation is used, the availability of translations is checked
-  // and when available the translation of the preferred language is selected.
-  $nid = $context['node']->nid;
-  // @todo Make language selection a separate function.
-  $langcode = $message['language'];
-  if (module_exists('translation')) {
-    // If the node has translations and a translation is required
-    // the equivalent of the node in the required language is used
-    // or the base node (nid == tnid) is used.
-    if ($tnid = $context['node']->tnid) {
-      if ($langcode != $context['node']->language) {
-        $translations = translation_node_get_translations($tnid);
-        // A translation is available in the preferred language.
-        if ($translation = $translations[$langcode]) {
-          $nid = $translation->nid;
-          $langcode = $translation->language;
-        }
-        else {
-          // No translation found which matches the preferred language.
-          foreach ($translations as $translation) {
-            if ($translation->nid == $tnid) {
-              $nid = $tnid;
-              $langcode = $translation->language;
-              break;
-            }
-          }
-        }
-      }
-    }
-    // If a translation of the node is used and this node is not available in
-    // the message buffer, then load this node.
-    if ($nid != $context['node']->nid && !isset($messages[$nid][$langcode])) {
-      $context['node'] = node_load($nid);
-    }
-  }
-
-  // Check if this node-language pair has been buffered.
-  // If not, build the message and store it for later use.
-  if (!isset($messages[$nid][$langcode])) {
-
-    // Use the default theme to render the email content.
-    // We temporary clear the $custom_theme to prevent the admin theme
-    // from being used when the newsletter is sent from the
-    // node add/edit form and the admin theme is other than the
-    // default theme. When no $custom_theme is set, the
-    // After theming the email $custom_theme is restored.
-    global $custom_theme;
-    $org_custom_theme = $custom_theme;
-    $custom_theme = '';
-
-    $node = clone $context['node'];
-
-    // Add simplenews specific header data
-    module_load_include('inc', 'simplenews', 'includes/simplenews.mail');
-    $headers = array_merge($message['headers'], _simplenews_headers($category, $params['from']['address']));
-    $headers['From'] = $params['from']['formatted'];
-    $message['headers'] = $messages[$nid][$langcode]['headers'] = $headers;
-
-    // Build email subject and perform some sanitizing.
-    // Line breaks are removed from the email subject to prevent injection of
-    // malicious data into the email header.
-    $subject = token_replace($category->email_subject, $context, array('sanitize' => FALSE));
-    $subject = str_replace(array("\r", "\n"), '', $subject);  // @todo Check if this is done by drupal_mail()
-    $message['subject'] = $messages[$nid][$langcode]['subject'] = $subject;
-
-    // Set the active language to the node's language.
-    // This needs to be done as otherwise the language used to send the mail
-    // is the language of the user logged in.
-    // @todo Rewrite this code for drupal 7.
-//    if (module_exists('i18n')) {
-//         i18n_selection_mode('node', $node->language);
-//    }
-
-    // Build message body
-// @todo move this code to a function. We need to do this twice for HTML and Plain text Alternative.
-    // @todo restore the format selection.
-    //$build = node_view($node, 'email_' . $category->format);
-    // Supported view modes: 'email_plain', 'email_html', 'email_textalt'
-    $build = node_view($node, 'email_plain');
-    // @todo Use simplenews_newsletter_body as #theme?
-    unset($build['#theme']);
-    foreach (element_children($build) as $child) {
-      $build[$child]['#theme'] = 'simplenews_field';
-    }
-// END move this code to a function.
-
-    $body = theme('simplenews_newsletter_body', array('build' => $build, 'category' => $category, 'language' => $message['language']));
-
-    // Buffer body text node and language specific
-    $messages[$nid][$langcode]['body'] = $body;
-
-    // @todo Move hidden footer into preprocess function.
-    //      We need to replace this concept. A footer is depending on the subscription list, not anymore on the newsletter category.
-    //      In the future this will also depend on whether the receiver is subscribed to a list or not a list member at all.
-    if ($category->opt_inout != SIMPLENEWS_OPT_INOUT_HIDDEN) {
-      // Build and buffer message footer
-      $footer = theme('simplenews_newsletter_footer',
-        array(
-          'build' => $build,
-          'category' => $category,
-          'context' => $context,
-          'key' => $key,
-          'language' => $message['language'],
-        )
-      );
-      $messages[$nid][$langcode]['footer'] = $footer;
-    }
-
-    // Reset the language to the original settings.
-    // @todo Rewrite this code for drupal 7.
-//    if (module_exists('i18n')) {
-//      i18n_selection_mode('reset');
-//    }
-
-    // Restore the custom theme.
-    $custom_theme = $org_custom_theme;
-  }
-  else {
-    // Get message data from buffer
-    $message['headers'] = $messages[$nid][$langcode]['headers'];
-    $message['subject'] = $messages[$nid][$langcode]['subject'];
-    $body               = $messages[$nid][$langcode]['body'];
-    $footer             = $messages[$nid][$langcode]['footer'];
-  }
-
-  // Build message body, replace tokens.
-  // Convert to plain text if required.
-  $message['body']['body'] = token_replace($body, $context, array('sanitize' => FALSE));
-  if ($category->format == 'plain') {
-    module_load_include('inc', 'simplenews', 'includes/simplenews.mail');
-    $message['body']['body'] = simplenews_html_to_text($message['body']['body'], $category->hyperlinks);
-  }
-
-  // Build message footer, replace tokens.
-  $message['body']['footer'] = token_replace($footer, $context, array('sanitize' => FALSE));
-
-  // Add user specific header data.
-  $message['headers']['List-Unsubscribe'] = '<' . token_replace('[simplenews-subscriber:unsubscribe-url]', $context, array('sanitize' => FALSE)) . '>';
-}
-
-/**
- * Build subject and body of the subscribe confirmation email.
- *
- * @param array $message
- *   Message array as used by hook_mail().
- * @param array $params
- *   Parameter array as used by hook_mail().
- */
-function simplenews_build_subscribe_mail(&$message, $params) {
-  $context = $params['context'];
-  $langcode = $message['language'];
-
-  // Use formatted from address "name" <mail_address>
-  $message['headers']['From'] = $params['from']['formatted'];
-
-  $message['subject'] = simplenews_subscription_confirmation_text('subscribe_subject', $langcode);
-  $message['subject'] = token_replace($message['subject'], $context, array('sanitize' => FALSE));
-
-  if (simplenews_user_is_subscribed($context['account']->mail, $context['category']->tid)) {
-    $body = simplenews_subscription_confirmation_text('subscribe_subscribed', $langcode);
-  }
-  else {
-    $body = simplenews_subscription_confirmation_text('subscribe_unsubscribed', $langcode);
-  }
-    $message['body'][] = token_replace($body, $context, array('sanitize' => FALSE));
-}
-
-/**
- * Build subject and body of the unsubscribe confirmation email.
- *
- * @param array $message
- *   Message array as used by hook_mail().
- * @param array $params
- *   Parameter array as used by hook_mail().
- */
-function simplenews_build_unsubscribe_mail(&$message, $params) {
-  $context = $params['context'];
-  $langcode = $message['language'];
-
-  // Use formatted from address "name" <mail_address>
-  $message['headers']['From'] = $params['from']['formatted'];
-
-  $message['subject'] = simplenews_subscription_confirmation_text('subscribe_subject', $langcode);
-  $message['subject'] = token_replace($message['subject'], $context, array('sanitize' => FALSE));
-
-  if (simplenews_user_is_subscribed($context['account']->mail, $context['category']->tid)) {
-    $body = simplenews_subscription_confirmation_text('unsubscribe_subscribed', $langcode);
-    $message['body'][] = token_replace($body, $context, array('sanitize' => FALSE));
-  }
-  else {
-    $body = simplenews_subscription_confirmation_text('unsubscribe_unsubscribed', $langcode);
-    $message['body'][] = token_replace($body, $context, array('sanitize' => FALSE));
-  }
-}
-
-
-/**
  * Implementation of hook_views_api().
  */
 function simplenews_views_api() {
@@ -3211,7 +2991,7 @@ function simplenews_node_tab_send_form_submit($form, &$form_state) {
   module_load_include('inc', 'simplenews', 'includes/simplenews.mail');
   if ($values['simplenews']['send'] == SIMPLENEWS_COMMAND_SEND_NOW) {
     simplenews_update_sent_status($node);
-    simplenews_send_node($node);
+    simplenews_add_node_to_spool($node);
   }
   elseif ($values['simplenews']['send'] == SIMPLENEWS_COMMAND_SEND_TEST) {
     simplenews_send_test($node, $form_state['test_addresses']);
@@ -3345,3 +3125,28 @@ function simplenews_require_double_opt_in($tid, $account) {
     return $category->opt_inout == 'double';
   }
 }
+
+/**
+ * Returns the available simplenews sources.
+ */
+function simplenews_get_source_caches() {
+  $sources = module_invoke_all('simplenews_source_cache_info');
+  drupal_alter('simplenews_source_cache_info', $sources);
+  return $sources;
+}
+
+/**
+ * Implements hook_simplenews_source_cache_info().
+ */
+function simplenews_simplenews_source_cache_info() {
+  return array(
+    'SimplenewsSourceCacheNone' => array(
+      'label' => t('No caching'),
+      'description' => t('This allows to theme each newsletter separately.'),
+    ),
+    'SimplenewsSourceNodeCached' => 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/simplenews_action/simplenews_action.module b/simplenews_action/simplenews_action.module
index 69339ca..1009741 100644
--- a/simplenews_action/simplenews_action.module
+++ b/simplenews_action/simplenews_action.module
@@ -88,7 +88,7 @@ function simplenews_action_send_newsletter(&$object, $context = array()) {
         simplenews_newsletter_save($newsletter);
       }
       module_load_include('inc', 'simplenews', 'includes/simplenews.mail');
-      simplenews_send_node($context['nid'], $accounts);
+      simplenews_add_node_to_spool($context['nid'], $accounts);
     }
     watchdog('action', 'Simplenews newsletter %title send.', array('%title' => $context['title']));
   }
diff --git a/tests/simplenews.test b/tests/simplenews.test
index 4be1dde..2aef89b 100644
--- a/tests/simplenews.test
+++ b/tests/simplenews.test
@@ -875,12 +875,12 @@ class SimpleNewsI18nTestCase extends SimplenewsTestCase {
       'name' => t('Simplenews I18n'),
       'description' => t('Translation of newsletter categories'),
       'group' => t('Simplenews'),
-      'dependencies' => array('i18n_taxonomy'),
+      'dependencies' => array('i18n_taxonomy', 'variable'),
     );
   }
 
   function setUp() {
-    parent::setUp(array('i18n_taxonomy'));
+    parent::setUp(array('locale', 'i18n', 'variable', 'i18n_string', 'i18n_translation', 'i18n_taxonomy'));
     $this->admin_user = $this->drupalCreateUser(array('bypass node access', 'administer nodes', 'administer languages', 'administer content types', 'administer blocks', 'access administration pages', 'administer filters', 'administer taxonomy', 'translate interface', 'subscribe to newsletters'));
     $this->drupalLogin($this->admin_user);
     $this->setUpLanguages();
