You are here

subscriptions_mail.module in Subscriptions 5.2

Same filename and directory in other branches
  1. 6 subscriptions_mail.module
  2. 7 subscriptions_mail.module

Subscriptions module mail gateway.

File

subscriptions_mail.module
View source
<?php

/**
 * @file
 * Subscriptions module mail gateway.
 */

/**
 * Implementation of hook_cron().
 *
 * Takes items from {subscriptions_queue} and generates notification emails.
 */
function subscriptions_mail_cron() {
  global $user, $locale;
  include_once drupal_get_path('module', 'subscriptions_mail') . '/subscriptions_mail.templates.inc';
  $mails_allowed = variable_get('subscriptions_number_of_mails', 0);
  $from = _subscriptions_mail_site_mail();
  $old_uid = 0;
  $single_count = 0;
  $digest_count = 0;
  $loaded_objects = array();
  $users = array();
  $fields = array();
  $mails = array();

  // Strategy for cron:
  // Use 50% of the remaining time to process queue items, send single messages, and assemble digest messages;
  // send the digest messages in the other half and hopefully leave some time for other modules.
  $total_seconds = ini_get('max_execution_time');
  $lost_seconds = timer_read('page') / 1000;
  $available_seconds = $total_seconds - $lost_seconds;

  //TEST: watchdog('cron', "Subscriptions has $available_seconds of $total_seconds seconds available.");
  while (($mails_allowed <= 0 || $single_count + count($mails) < $mails_allowed) && $total_seconds - timer_read('page') / 1000 > $available_seconds * variable_get('subscriptions_cron_percent', 50) / 100) {
    subscriptions_mail_mail_edit_variables($null = NULL);

    // clear cache
    $result = db_query_range('SELECT * FROM {subscriptions_queue} WHERE last_sent + send_interval < %d ORDER BY sqid', time(), 0, 1);
    if (!($s = db_fetch_array($result))) {
      break;

      // No more subscriptions, terminate loop.
    }
    if (!isset($users[$s['uid']])) {
      $users[$s['uid']] = user_load(array(
        'uid' => $s['uid'],
      ));
    }
    $saved_user = $user;
    session_save_session(FALSE);
    $user = $users[$s['uid']];
    $locale = locale_initialize();
    do {

      // once and repeat while adding to a digest
      if ($user->status && $user->access) {
        $cids = array();
        $load_function = $s['load_function'];
        $index = $load_args = $s['load_args'];
        if (!isset($loaded_objects[$user->uid][$load_function][$load_args])) {
          if (is_numeric($load_args)) {
            $object = $load_function($load_args, $s['sqid'], $s['is_new']);
          }
          else {
            $load_args = unserialize($load_args);
            $load_args[] = $s['is_new'];
            $object = call_user_func_array($load_function, $load_args);
          }
          if (!empty($object)) {
            $access = module_invoke_all('subscriptions', 'access', $load_function, $load_args, $object);

            // One FALSE vote is enough to deny. Also, we need a non-empty array.
            $allow = !empty($access) && array_search(FALSE, $access) === FALSE;
            $loaded_objects[$user->uid][$load_function][$index] = $allow ? $object : FALSE;
          }
        }
        if ($object = $loaded_objects[$user->uid][$load_function][$index]) {
          if (!isset($users[$object->uid])) {
            $users[$object->uid] = user_load(array(
              'uid' => $object->uid,
            ));
          }
          $sender = $users[$object->uid];
          $module = $s['module'];
          $ori_field = $field = $s['field'];
          $ori_value = $value = $s['value'];
          if (!isset($fields[$locale][$module])) {
            $fields[$locale][$module] = module_invoke_all('subscriptions', 'fields', $module);
          }
          if ($module == 'node' && $field == 'nid' && (!empty($object->_subscriptions_is_updated) || !empty($object->_subscriptions_is_new)) && user_access('subscribe to content types', $user)) {
            $unlisteds = variable_get('subscriptions_unlisted_content_types', array());
            if (isset($object->type) && !in_array($object->type, $unlisteds)) {
              $field = 'type';
              $value = $object->type;
            }
          }
          $mailvars_function = $fields[$locale][$module][$field]['mailvars_function'];
          $mailkey = 'subscriptions-' . $module . '-' . $field;
          if (!is_numeric($value)) {
            $mailkey .= '-' . $value;
          }
          $digest = $s['digest'] > 0 || $s['digest'] == -1 && _subscriptions_get_setting('digest', 0) > 0;
          if ($digest) {
            if (!($body_template = db_result(db_query("SELECT item_body FROM {subscriptions_mail_edit} WHERE mailkey = '%s'", SUBSCRIPTIONS_DIGEST_MAILKEY . '-item')))) {
              $body_template = subscriptions_mail_template('DITEM');
            }
          }
          else {
            $body_template = variable_get('subscriptions_email_body', subscriptions_mail_template('BODY'));
            $subject_template = variable_get('subscriptions_email_subject', subscriptions_mail_template('SUBJ'));
          }
          init_theme();
          $show_node_info = isset($object->type) ? theme_get_setting('toggle_node_info_' . $object->type) : TRUE;
          $base = 'user/' . $s['uid'] . '/';
          $mailvars = array(
            '!site' => variable_get('site_name', 'drupal'),
            '!sender_name' => $show_node_info ? $sender->uid ? $sender->name : variable_get('anonymous', '!sender_name') : '!sender_name',
            '!sender_page' => $show_node_info && $sender->uid ? url("user/{$sender->uid}", NULL, NULL, TRUE) : '!sender_page',
            '!sender_contact_page' => $show_node_info ? empty($sender->contact) ? t('(disabled)') : url("user/{$sender->uid}/contact", NULL, NULL, TRUE) : '!sender_contact_page',
            '!sender_has_contact_page' => $show_node_info ? empty($sender->contact) ? 0 : 1 : 0,
            '!manage_url' => url($base . 'subscriptions', NULL, NULL, TRUE),
            '!name' => $s['name'],
            '!subs_type' => $fields[$locale][$module][$field]['!subs_type'],
            '!unsubscribe_url' => url("s/del/{$module}/{$ori_field}/{$ori_value}/" . $s['author_uid'] . '/' . $s['uid'] . '/' . md5(drupal_get_private_key() . $module . $ori_field . $ori_value . $s['author_uid'] . $s['uid']), NULL, NULL, TRUE),
          );
          $mailvars_function($mailvars, $object, $field, $s);
          $mailvars += module_invoke_all('subscriptions_get_mailvars', $object);
          if ($digest && !empty($object->_subscriptions_comments) && module_exists('subscriptions_content')) {
            static $digest_comment_template;
            if (!$digest_comment_template) {
              $digest_comment_template = db_result(db_query("SELECT item_body FROM {subscriptions_mail_edit} WHERE mailkey = '%s'", SUBSCRIPTIONS_DIGEST_MAILKEY . '-item-comment'));
              $digest_comment_template = $digest_comment_template ? $digest_comment_template : subscriptions_mail_template('DITEMCMT');
            }
            $mailvars['!comments'] = _subscriptions_content_format_comments($object, $digest_comment_template, '');
          }
          $body = strtr(subscriptions_mail_template_preprocess($body_template, $mailvars), $mailvars);
          $subject = strtr(subscriptions_mail_template_preprocess($subject_template, $mailvars), $mailvars);
          if ($digest) {
            $mails[$s['uid']]['bodies'][] = $body;
            $mails[$s['uid']]['send'] = array(
              'name' => $s['name'],
              'mail' => $s['mail'],
              'from' => $from,
              '!name' => $mailvars['!name'],
              '!manage_url' => $mailvars['!manage_url'],
            );
          }
          else {
            subscriptions_mail_mail_edit_variables($mailvars);
            _subscriptions_mail_send($mailkey, $s['name'], $s['mail'], $subject, $body, $from, $s['uid']);
            ++$single_count;
          }
        }
      }
      db_query("DELETE FROM {subscriptions_queue} WHERE load_function = '%s' AND load_args = '%s' AND uid = %d", $s['load_function'], $s['load_args'], $s['uid']);
      if ($digest) {

        // TODO: Get the next queue item for this user and finish off this user's digest
        // before moving on to the next user. All messages in one digest together count
        // as one mail, and if the number of mails is limited (per cron run), we must
        // not let this cause a split up of the digest.
        // Issue: We must know which notifications to send according to their send_interval.
      }
    } while (FALSE);

    // TODO: while adding to a digest
    $user = $saved_user;
    $locale = locale_initialize();
    session_save_session(TRUE);
  }
  if ($mails) {
    static $separator;
    if (!isset($separator)) {
      $separator = db_result(db_query("SELECT item_body FROM {subscriptions_mail_edit} WHERE mailkey = '%s'", SUBSCRIPTIONS_DIGEST_MAILKEY . '-separator'));
      $separator = $separator ? $separator : subscriptions_mail_template('SEP');
    }
    session_save_session(FALSE);
    foreach ($mails as $uid => $user_mails) {
      $user = $users[$uid];
      $locale = locale_initialize();
      $s = $user_mails['send'];
      if ($templates = db_fetch_object(db_query("SELECT * FROM {mail_edit} WHERE mailkey = '%s'", SUBSCRIPTIONS_DIGEST_MAILKEY))) {
        $subject_template = $templates->subject;
        $body_template = $templates->body;
      }
      else {
        $subject_template = subscriptions_mail_template('DSUBJ');
        $body_template = subscriptions_mail_template('DBODY');
      }
      $mailvars['!bodies'] = implode($separator, $user_mails['bodies']);
      $mailvars['!name'] = $s['!name'];
      $mailvars['!manage_url'] = $s['!manage_url'];
      $subject = strtr(subscriptions_mail_template_preprocess($subject_template, $mailvars), $mailvars);
      $body = strtr(subscriptions_mail_template_preprocess($body_template, $mailvars), $mailvars);
      subscriptions_mail_mail_edit_variables($mailvars);
      _subscriptions_mail_send(SUBSCRIPTIONS_DIGEST_MAILKEY, $s['name'], $s['mail'], $subject, $body, $s['from'], $uid);
      ++$digest_count;
    }
    $user = $saved_user;
    $locale = locale_initialize();
    session_save_session(TRUE);
  }
  if ($single_count + $digest_count > 0) {
    $watchdog = 'watchdog';

    // keep potx from translating 'cron'
    $watchdog('cron', t("!module sent !single_count single and !digest_count digest notifications in !used_seconds of !available_seconds available seconds; !remaining_items queue items left.", array(
      '!module' => 'Subscriptions',
      '!single_count' => $single_count,
      '!digest_count' => $digest_count,
      '!used_seconds' => (int) (timer_read('page') / 1000 - $lost_seconds),
      '!available_seconds' => (int) $available_seconds . ($lost_seconds > 5 * $total_seconds / 100 ? " ({$total_seconds})" : ''),
      '!remaining_items' => db_result(db_query("SELECT COUNT(*) FROM {subscriptions_queue} WHERE last_sent + send_interval < %d", time())),
    )));
  }
}

/**
 * Send the notification by mail.
 */
function _subscriptions_mail_send($mailkey, $name, $to, $subject, $body, $from, $uid) {
  global $base_url;
  $url = parse_url($base_url);
  $list_id = variable_get('site_name', '') . ' ' . t('Subscriptions') . ' <subscriptions.' . $url['host'] . '>';
  $mail_success = drupal_mail($mailkey, $to, $subject, $body, $from, array(
    'List-Id' => $list_id,
  ));
  $watchdog_params = array(
    '@name' => $name,
    '@to' => "<{$to}>",
  );
  if ($mail_success) {
    if (variable_get('subscriptions_watchgood', 1)) {
      watchdog('subscriptions', t('notification for @name at @to', $watchdog_params));
    }
    db_query("UPDATE {subscriptions_user} SET last_sent = %d WHERE uid = %d", time(), $uid);
    if (!db_affected_rows()) {
      @db_query("INSERT INTO {subscriptions_user} (uid, last_sent) VALUES(%d, %d)", $uid, time());
    }
  }
  else {
    watchdog('subscriptions', t('error mailing notification for @name at @to', $watchdog_params), WATCHDOG_ERROR);
  }
}

/**
 * Return the 'From:' address to use for sending e-mail.
 */
function _subscriptions_mail_site_mail($address_only = FALSE) {
  $email = variable_get('subscriptions_site_mail', '');
  if (empty($email)) {
    $email = variable_get('site_mail', ini_get('sendmail_from'));
  }
  if (!$address_only && ($name = variable_get('subscriptions_site_mail_name', FALSE))) {
    $email = '"' . $name . '" <' . $email . '>';
  }
  return $email;
}

/**
 * Implementation of hook_form_alter().
 *
 * Adds to the General Settings part at admin/settings/subscriptions and
 * possibly a warning and [Remove legacy template] button at
 * admin/build/mail_edit/subscriptions-....
 */
function subscriptions_mail_form_alter($form_id, &$form) {
  global $user;
  $tr = 't';
  if ($form_id == 'subscriptions_settings_form') {

    // check the $base_url (#199039, #226335)
    $url = url("", NULL, NULL, TRUE);
    if (empty($_POST) && preg_match('!//($|/|localhost/|([0-9]{1,3}\\.){3}[0-9]{1,3}/)!', $url)) {
      drupal_set_message(t('Your installation returns %url as the base URL of the site. This is probably not what you want, and it can usually be fixed by setting the %variable variable in your %file file.', array(
        '%url' => $url,
        '%variable' => '$base_url',
        '%file' => 'settings.php',
      )), 'error');
    }
    $form['mail_settings'] = array(
      '#type' => 'fieldset',
      '#title' => t('Mail settings'),
      '#weight' => -3,
    );
    $form['mail_settings']['subscriptions_site_mail'] = array(
      '#type' => 'textfield',
      '#title' => t('E-mail address'),
      '#default_value' => _subscriptions_mail_site_mail(TRUE),
      '#description' => t('A valid e-mail address to be used as the "From" address by the auto-mailer for !module notifications.  To lessen the likelihood of e-mail being marked as spam, this e-mail address should use the same domain as the website.', array(
        '!module' => 'Subscriptions',
      )) . '<br />' . t('Clear this field to use the default site e-mail address.'),
    );
    $form['mail_settings']['subscriptions_site_mail_name'] = array(
      '#type' => 'textfield',
      '#title' => t('E-mail name'),
      '#default_value' => variable_get('subscriptions_site_mail_name', ''),
      '#description' => t('An optional name to go with the e-mail address above, no "double-quotes".') . '<br />' . t('Clear this field to use the e-mail address only &mdash; some e-mail clients will display only the portion of the address to the left of the @ sign.'),
    );
    $form['mail_settings']['subscriptions_number_of_mails'] = array(
      '#type' => 'textfield',
      '#title' => t('Maximum number of notifications to send per cron job'),
      '#default_value' => variable_get('subscriptions_number_of_mails', 0),
      '#description' => t("!module tries to use a good part of the remaining time during each cron run. If it's using too much time or you need to limit the number of outgoing e-mails for some other reason, then set the number here. The default is 0, which means unlimited.", array(
        '!module' => 'Subscriptions',
      )),
    );
    $form['mail_settings']['subscriptions_watchgood'] = array(
      '#type' => 'checkbox',
      '#title' => t('Display watchdog entries for successful mailings'),
      '#default_value' => variable_get('subscriptions_watchgood', 1),
      '#description' => t('Logs successful mailings to the watchdog log.  Default is ON, but with many subscribers this will generate a huge number of log entries.'),
    );
    $form['mail_settings']['subscriptions_watchstats'] = array(
      '#type' => 'checkbox',
      '#title' => t('Display summary watchdog entries per cron job'),
      '#default_value' => variable_get('subscriptions_watchstats', 1),
      '#description' => t('Logs the mailing counts, time spent, and size of the remaining queue to the watchdog log.  This is valuable information for estimating the load on the cron job and on your mail server.  Default is ON.'),
    );
  }
  elseif ($form_id == 'mail_edit_form' && substr($form['mailkey']['#value'], 0, 14) == 'subscriptions-') {
    if ($form['mailkey']['#value'] != SUBSCRIPTIONS_DIGEST_MAILKEY && (variable_get('subscriptions_email_body', '') || variable_get('subscriptions_email_subject', ''))) {
      $form['legacy'] = array(
        '#type' => 'fieldset',
        '#title' => t('Legacy template'),
        '#attributes' => array(
          'class' => 'error',
        ),
        '#weight' => -101,
      );
      $form['legacy']['explain'] = array(
        '#type' => 'item',
        '#value' => t('You have Subscriptions 5.x-1.x-dev template variables defined in your database.<br />As long as these are in place, the template values below will be ignored!'),
      );
      $form['legacy']['remove_legacy'] = array(
        '#type' => 'submit',
        '#value' => t('Remove legacy template'),
      );
      $form['#submit'] = array_reverse($form['#submit']);

      // we want to go first!
      $form['#submit']['subscriptions_mail_mail_edit_form_submit'] = array();
      $form['#submit'] = array_reverse($form['#submit']);
    }
    foreach (array(
      '!sender_name',
      '!sender_page',
    ) as $key) {
      $expl = $form['help']['#variables'][$key];
      if ($expl[strlen($expl) - 1] == '.') {
        $add_dot = TRUE;
        $expl = substr($expl, 0, strlen($expl) - 1);
      }
      $expl .= ' ' . t('(if the sender is visible)') . (!empty($add_dot) ? '.' : '');
      $form['help']['#variables'][$key] = $expl;
    }
  }
}

/**
 * Mail Editor page submit handler.
 *
 * @ingroup form
 */
function subscriptions_mail_mail_edit_form_submit($form_id, $form_values) {
  if ($form_values['op'] == $form_values['remove_legacy']) {
    variable_del('subscriptions_email_body');
    variable_del('subscriptions_email_subject');
    drupal_goto($_GET['q']);

    // no further processing!
  }
}

/**
 * Assemble mail variables.
 */
function subscriptions_mail_mail_edit_variables(&$variables, $mailkey = NULL) {
  static $stored_variables;
  if (!isset($variables)) {
    unset($stored_variables);
  }
  elseif (isset($mailkey)) {
    if (substr($mailkey, 0, 13) == 'subscriptions' && !empty($stored_variables)) {
      $variables = $stored_variables + $variables;
    }
  }
  else {
    $stored_variables = $variables;
  }
}

// TODO: mail_edit.module is hard-coded to call this function,
//       and we need it, because only mail_edit.module replaces
//       variables such as !recipient_name.
//       We ought to be able to get rid of this tight coupling in mail_edit,
//       but I (hs) don't see yet how this is supposed to work...
function subscriptions_template_preprocess($template, $mailvars) {
  return subscriptions_mail_template_preprocess($template, $mailvars);
}

/**
 * Preprocess a mail template (subject or body), detecting conditional clauses
 * that conform to a prescribed syntax
 *
 * @param string $template
 *  the template for preprocessing
 * @param array $mailvars
 *  an associatvie array of currently existing variables that are to be
 *  interpolated into the template later , and which can be used by this
 *  function for preprocessing
 *
 * This function allows the administrator to specify ternary-type conditions
 * to determine what text is used in a mail in a particular situation, using
 * the variables that are currently available for that mail for reference.
 * The syntax is standard PHP/C-style ternary syntax, but only allows the
 * "==" and "!=":
 * {{!variable_name==sometext?text for true condition:text for false condition}}
 *
 * sometext must not contain a question mark, and the true text no colon.
 */
function subscriptions_mail_template_preprocess($template, $mailvars) {
  preg_match_all('/{{(?P<condition>[^?]+?)\\?(?P<true>[^:]*?):(?P<false>[^\\]]*?)}}/', $template, $conditions);

  // locate the actual operators/operand for each
  $replacement = '';
  foreach ($conditions[0] as $k => $v) {
    preg_match('/(?P<operand_1>!.+)\\s*(?P<operator>==|!=)\\s*(?P<operand_2>.+)/', $conditions['condition'][$k], $matches);
    $operand1 = isset($mailvars[$matches['operand_1']]) ? $mailvars[$matches['operand_1']] : $matches['operand_1'];
    if ($matches['operator'] == '==') {
      $replacement = $operand1 == $matches['operand_2'] ? $conditions['true'][$k] : $conditions['false'][$k];
    }
    elseif ($matches['operator'] == '!=') {
      $replacement = $operand1 != $matches['operand_2'] ? $conditions['true'][$k] : $conditions['false'][$k];
    }
    else {
      continue;
    }

    // replace the condition with the result of its evalutation
    $template = str_replace($v, $replacement, $template);
  }
  return $template;
}

/**
 * Implementation of hook_mail_alter().
 *
 * Remove any trailing spaces (must run after mail_edit_mail_alter()!).
 */
function subscriptions_mail_mail_alter(&$mailkey, &$to, &$subject, &$body, &$from, &$headers) {
  $body = preg_replace('/ +(\\r?\\n)/', '\\1', $body);
}

Functions

Namesort descending Description
subscriptions_mail_cron Implementation of hook_cron().
subscriptions_mail_form_alter Implementation of hook_form_alter().
subscriptions_mail_mail_alter Implementation of hook_mail_alter().
subscriptions_mail_mail_edit_form_submit Mail Editor page submit handler.
subscriptions_mail_mail_edit_variables Assemble mail variables.
subscriptions_mail_template_preprocess Preprocess a mail template (subject or body), detecting conditional clauses that conform to a prescribed syntax
subscriptions_template_preprocess
_subscriptions_mail_send Send the notification by mail.
_subscriptions_mail_site_mail Return the 'From:' address to use for sending e-mail.