subscriptions_mail.module

Tracking 5.x-2.x branch
  1. drupal
    1. 5 contributions/subscriptions/subscriptions_mail.module
    2. 6 contributions/subscriptions/subscriptions_mail.module
    3. 7 contributions/subscriptions/subscriptions_mail.module

Subscriptions module mail gateway.

Functions & methods

NameDescription
subscriptions_mail_cronImplementation of hook_cron().
subscriptions_mail_form_alterImplementation of hook_form_alter().
subscriptions_mail_mail_alterImplementation of hook_mail_alter().
subscriptions_mail_mail_edit_form_submitMail Editor page submit handler.
subscriptions_mail_mail_edit_variablesAssemble mail variables.
subscriptions_mail_template_preprocessPreprocess a mail template (subject or body), detecting conditional clauses that conform to a prescribed syntax
subscriptions_template_preprocess
_subscriptions_mail_sendSend the notification by mail.
_subscriptions_mail_site_mailReturn the 'From:' address to use for sending e-mail.

File

View source
  1. <?php
  2. /**
  3. * @file
  4. * Subscriptions module mail gateway.
  5. */
  6. /**
  7. * Implementation of hook_cron().
  8. *
  9. * Takes items from {subscriptions_queue} and generates notification emails.
  10. */
  11. function subscriptions_mail_cron() {
  12. global $user, $locale;
  13. include_once drupal_get_path('module', 'subscriptions_mail') .'/subscriptions_mail.templates.inc';
  14. $mails_allowed = variable_get('subscriptions_number_of_mails', 0);
  15. $from = _subscriptions_mail_site_mail();
  16. $old_uid = 0;
  17. $single_count = 0;
  18. $digest_count = 0;
  19. $loaded_objects = array();
  20. $users = array();
  21. $fields = array();
  22. $mails = array();
  23. // Strategy for cron:
  24. // Use 50% of the remaining time to process queue items, send single messages, and assemble digest messages;
  25. // send the digest messages in the other half and hopefully leave some time for other modules.
  26. $total_seconds = ini_get('max_execution_time');
  27. $lost_seconds = timer_read('page')/1000;
  28. $available_seconds = $total_seconds - $lost_seconds;
  29. //TEST: watchdog('cron', "Subscriptions has $available_seconds of $total_seconds seconds available.");
  30. while (($mails_allowed <= 0 || $single_count + count($mails) < $mails_allowed)
  31. && $total_seconds - timer_read('page')/1000 > $available_seconds*variable_get('subscriptions_cron_percent', 50)/100) {
  32. subscriptions_mail_mail_edit_variables($null = NULL); // clear cache
  33. $result = db_query_range('SELECT * FROM {subscriptions_queue} WHERE last_sent + send_interval < %d ORDER BY sqid', time(), 0, 1);
  34. if (!($s = db_fetch_array($result))) {
  35. break; // No more subscriptions, terminate loop.
  36. }
  37. if (!isset($users[$s['uid']])) {
  38. $users[$s['uid']] = user_load(array('uid' => $s['uid']));
  39. }
  40. $saved_user = $user;
  41. session_save_session(FALSE);
  42. $user = $users[$s['uid']];
  43. $locale = locale_initialize();
  44. do { // once and repeat while adding to a digest
  45. if ($user->status && $user->access) {
  46. $cids = array();
  47. $load_function = $s['load_function'];
  48. $index = $load_args = $s['load_args'];
  49. if (!isset($loaded_objects[$user->uid][$load_function][$load_args])) {
  50. if (is_numeric($load_args)) {
  51. $object = $load_function($load_args, $s['sqid'], $s['is_new']);
  52. }
  53. else {
  54. $load_args = unserialize($load_args);
  55. $load_args[] = $s['is_new'];
  56. $object = call_user_func_array($load_function, $load_args);
  57. }
  58. if (!empty($object)) {
  59. $access = module_invoke_all('subscriptions', 'access', $load_function, $load_args, $object);
  60. // One FALSE vote is enough to deny. Also, we need a non-empty array.
  61. $allow = !empty($access) && array_search(FALSE, $access) === FALSE;
  62. $loaded_objects[$user->uid][$load_function][$index] = $allow ? $object : FALSE;
  63. }
  64. }
  65. if ($object = $loaded_objects[$user->uid][$load_function][$index]) {
  66. if (!isset($users[$object->uid])) {
  67. $users[$object->uid] = user_load(array('uid' => $object->uid));
  68. }
  69. $sender = $users[$object->uid];
  70. $module = $s['module'];
  71. $ori_field = $field = $s['field'];
  72. $ori_value = $value = $s['value'];
  73. if (!isset($fields[$locale][$module])) {
  74. $fields[$locale][$module] = module_invoke_all('subscriptions', 'fields', $module);
  75. }
  76. if ($module == 'node' && $field == 'nid' && (!empty($object->_subscriptions_is_updated) || !empty($object->_subscriptions_is_new)) && user_access('subscribe to content types', $user)) {
  77. $unlisteds = variable_get('subscriptions_unlisted_content_types', array());
  78. if (isset($object->type) && !in_array($object->type, $unlisteds)) {
  79. $field = 'type';
  80. $value = $object->type;
  81. }
  82. }
  83. $mailvars_function = $fields[$locale][$module][$field]['mailvars_function'];
  84. $mailkey = 'subscriptions-'. $module .'-'. $field;
  85. if (!is_numeric($value)) {
  86. $mailkey .= '-'. $value;
  87. }
  88. $digest = $s['digest'] > 0 || $s['digest'] == -1 && _subscriptions_get_setting('digest', 0) > 0;
  89. if ($digest) {
  90. if (!$body_template = db_result(db_query("SELECT item_body FROM {subscriptions_mail_edit} WHERE mailkey = '%s'", SUBSCRIPTIONS_DIGEST_MAILKEY .'-item'))) {
  91. $body_template = subscriptions_mail_template('DITEM');
  92. }
  93. }
  94. else {
  95. $body_template = variable_get('subscriptions_email_body', subscriptions_mail_template('BODY'));
  96. $subject_template = variable_get('subscriptions_email_subject', subscriptions_mail_template('SUBJ'));
  97. }
  98. init_theme();
  99. $show_node_info = (isset($object->type) ? theme_get_setting('toggle_node_info_'. $object->type) : TRUE);
  100. $base = 'user/'. $s['uid'] .'/';
  101. $mailvars = array(
  102. '!site' => variable_get('site_name', 'drupal'),
  103. '!sender_name' => ($show_node_info ? ($sender->uid ? $sender->name : variable_get('anonymous', '!sender_name')) : '!sender_name'),
  104. '!sender_page' => ($show_node_info && $sender->uid ? url("user/$sender->uid", NULL, NULL, TRUE) : '!sender_page'),
  105. '!sender_contact_page' => ($show_node_info ? (empty($sender->contact) ? t('(disabled)') : url("user/$sender->uid/contact", NULL, NULL, TRUE)) : '!sender_contact_page'),
  106. '!sender_has_contact_page' => ($show_node_info ? (empty($sender->contact) ? 0 : 1) : 0),
  107. '!manage_url' => url($base .'subscriptions', NULL, NULL, TRUE),
  108. '!name' => $s['name'],
  109. '!subs_type' => $fields[$locale][$module][$field]['!subs_type'],
  110. '!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),
  111. );
  112. $mailvars_function($mailvars, $object, $field, $s);
  113. $mailvars += module_invoke_all('subscriptions_get_mailvars', $object);
  114. if ($digest && !empty($object->_subscriptions_comments) && module_exists('subscriptions_content')) {
  115. static $digest_comment_template;
  116. if (!$digest_comment_template) {
  117. $digest_comment_template = db_result(db_query("SELECT item_body FROM {subscriptions_mail_edit} WHERE mailkey = '%s'", SUBSCRIPTIONS_DIGEST_MAILKEY .'-item-comment'));
  118. $digest_comment_template = ($digest_comment_template ? $digest_comment_template : subscriptions_mail_template('DITEMCMT'));
  119. }
  120. $mailvars['!comments'] = _subscriptions_content_format_comments($object, $digest_comment_template, '');
  121. }
  122. $body = strtr(subscriptions_mail_template_preprocess($body_template, $mailvars), $mailvars);
  123. $subject = strtr(subscriptions_mail_template_preprocess($subject_template, $mailvars), $mailvars);
  124. if ($digest) {
  125. $mails[$s['uid']]['bodies'][] = $body;
  126. $mails[$s['uid']]['send'] = array(
  127. 'name' => $s['name'],
  128. 'mail' => $s['mail'],
  129. 'from' => $from,
  130. '!name' => $mailvars['!name'],
  131. '!manage_url' => $mailvars['!manage_url'],
  132. );
  133. }
  134. else {
  135. subscriptions_mail_mail_edit_variables($mailvars);
  136. _subscriptions_mail_send($mailkey, $s['name'], $s['mail'], $subject, $body, $from, $s['uid']);
  137. ++$single_count;
  138. }
  139. }
  140. }
  141. 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']);
  142. if ($digest) {
  143. // TODO: Get the next queue item for this user and finish off this user's digest
  144. // before moving on to the next user. All messages in one digest together count
  145. // as one mail, and if the number of mails is limited (per cron run), we must
  146. // not let this cause a split up of the digest.
  147. // Issue: We must know which notifications to send according to their send_interval.
  148. }
  149. } while ( FALSE ); // TODO: while adding to a digest
  150. $user = $saved_user;
  151. $locale = locale_initialize();
  152. session_save_session(TRUE);
  153. }
  154. if ($mails) {
  155. static $separator;
  156. if (!isset($separator)) {
  157. $separator = db_result(db_query("SELECT item_body FROM {subscriptions_mail_edit} WHERE mailkey = '%s'", SUBSCRIPTIONS_DIGEST_MAILKEY .'-separator'));
  158. $separator = ($separator ? $separator : subscriptions_mail_template('SEP'));
  159. }
  160. session_save_session(FALSE);
  161. foreach ($mails as $uid => $user_mails) {
  162. $user = $users[$uid];
  163. $locale = locale_initialize();
  164. $s = $user_mails['send'];
  165. if ($templates = db_fetch_object(db_query("SELECT * FROM {mail_edit} WHERE mailkey = '%s'", SUBSCRIPTIONS_DIGEST_MAILKEY))) {
  166. $subject_template = $templates->subject;
  167. $body_template = $templates->body;
  168. }
  169. else {
  170. $subject_template = subscriptions_mail_template('DSUBJ');
  171. $body_template = subscriptions_mail_template('DBODY');
  172. }
  173. $mailvars['!bodies'] = implode($separator, $user_mails['bodies']);
  174. $mailvars['!name'] = $s['!name'];
  175. $mailvars['!manage_url'] = $s['!manage_url'];
  176. $subject = strtr(subscriptions_mail_template_preprocess($subject_template, $mailvars), $mailvars);
  177. $body = strtr(subscriptions_mail_template_preprocess($body_template, $mailvars), $mailvars);
  178. subscriptions_mail_mail_edit_variables($mailvars);
  179. _subscriptions_mail_send(SUBSCRIPTIONS_DIGEST_MAILKEY, $s['name'], $s['mail'], $subject, $body, $s['from'], $uid);
  180. ++$digest_count;
  181. }
  182. $user = $saved_user;
  183. $locale = locale_initialize();
  184. session_save_session(TRUE);
  185. }
  186. if ($single_count + $digest_count > 0) {
  187. $watchdog = 'watchdog'; // keep potx from translating 'cron'
  188. $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(
  189. '!module' => 'Subscriptions',
  190. '!single_count' => $single_count,
  191. '!digest_count' => $digest_count,
  192. '!used_seconds' => (integer) (timer_read('page')/1000 - $lost_seconds),
  193. '!available_seconds' => (integer) $available_seconds . ($lost_seconds > 5*$total_seconds/100 ? " ($total_seconds)" : ''),
  194. '!remaining_items' => db_result(db_query("SELECT COUNT(*) FROM {subscriptions_queue} WHERE last_sent + send_interval < %d", time())),
  195. )));
  196. }
  197. }
  198. /**
  199. * Send the notification by mail.
  200. */
  201. function _subscriptions_mail_send($mailkey, $name, $to, $subject, $body, $from, $uid) {
  202. global $base_url;
  203. $url = parse_url($base_url);
  204. $list_id = variable_get('site_name', '') .' '. t('Subscriptions') .' <subscriptions.'. $url['host'] .'>';
  205. $mail_success = drupal_mail($mailkey, $to, $subject, $body, $from, array('List-Id' => $list_id));
  206. $watchdog_params = array('@name' => $name, '@to' => "<$to>");
  207. if ($mail_success) {
  208. if (variable_get('subscriptions_watchgood', 1)) {
  209. watchdog('subscriptions', t('notification for @name at @to', $watchdog_params));
  210. }
  211. db_query("UPDATE {subscriptions_user} SET last_sent = %d WHERE uid = %d", time(), $uid);
  212. if (!db_affected_rows()) {
  213. @db_query("INSERT INTO {subscriptions_user} (uid, last_sent) VALUES(%d, %d)", $uid, time());
  214. }
  215. }
  216. else {
  217. watchdog('subscriptions', t('error mailing notification for @name at @to', $watchdog_params), WATCHDOG_ERROR);
  218. }
  219. }
  220. /**
  221. * Return the 'From:' address to use for sending e-mail.
  222. */
  223. function _subscriptions_mail_site_mail($address_only = FALSE) {
  224. $email = variable_get('subscriptions_site_mail', '');
  225. if (empty($email)) {
  226. $email = variable_get('site_mail', ini_get('sendmail_from'));
  227. }
  228. if (!$address_only && ($name = variable_get('subscriptions_site_mail_name', FALSE))) {
  229. $email = '"'. $name .'" <'. $email .'>';
  230. }
  231. return $email;
  232. }
  233. /**
  234. * Implementation of hook_form_alter().
  235. *
  236. * Adds to the General Settings part at admin/settings/subscriptions and
  237. * possibly a warning and [Remove legacy template] button at
  238. * admin/build/mail_edit/subscriptions-....
  239. */
  240. function subscriptions_mail_form_alter($form_id, &$form) {
  241. global $user;
  242. $tr = 't';
  243. if ($form_id == 'subscriptions_settings_form') {
  244. // check the $base_url (#199039, #226335)
  245. $url = url("", NULL, NULL, TRUE);
  246. if (empty($_POST) && preg_match('!//($|/|localhost/|([0-9]{1,3}\.){3}[0-9]{1,3}/)!', $url)) {
  247. 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');
  248. }
  249. $form['mail_settings'] = array(
  250. '#type' => 'fieldset',
  251. '#title' => t('Mail settings'),
  252. '#weight' => -3,
  253. );
  254. $form['mail_settings']['subscriptions_site_mail'] = array(
  255. '#type' => 'textfield',
  256. '#title' => t('E-mail address'),
  257. '#default_value' => _subscriptions_mail_site_mail(TRUE),
  258. '#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.'),
  259. );
  260. $form['mail_settings']['subscriptions_site_mail_name'] = array(
  261. '#type' => 'textfield',
  262. '#title' => t('E-mail name'),
  263. '#default_value' => variable_get('subscriptions_site_mail_name', ''),
  264. '#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.'),
  265. );
  266. $form['mail_settings']['subscriptions_number_of_mails'] = array(
  267. '#type' => 'textfield',
  268. '#title' => t('Maximum number of notifications to send per cron job'),
  269. '#default_value' => variable_get('subscriptions_number_of_mails', 0),
  270. '#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')),
  271. );
  272. $form['mail_settings']['subscriptions_watchgood'] = array(
  273. '#type' => 'checkbox',
  274. '#title' => t('Display watchdog entries for successful mailings'),
  275. '#default_value' => variable_get('subscriptions_watchgood', 1),
  276. '#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.'),
  277. );
  278. $form['mail_settings']['subscriptions_watchstats'] = array(
  279. '#type' => 'checkbox',
  280. '#title' => t('Display summary watchdog entries per cron job'),
  281. '#default_value' => variable_get('subscriptions_watchstats', 1),
  282. '#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.'),
  283. );
  284. }
  285. elseif ($form_id == 'mail_edit_form' && substr($form['mailkey']['#value'], 0, 14) == 'subscriptions-') {
  286. if ($form['mailkey']['#value'] != SUBSCRIPTIONS_DIGEST_MAILKEY &&
  287. (variable_get('subscriptions_email_body', '') || variable_get('subscriptions_email_subject', ''))) {
  288. $form['legacy'] = array(
  289. '#type' => 'fieldset',
  290. '#title' => t('Legacy template'),
  291. '#attributes' => array('class' => 'error'),
  292. '#weight' => -101,
  293. );
  294. $form['legacy']['explain'] = array(
  295. '#type' => 'item',
  296. '#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!'),
  297. );
  298. $form['legacy']['remove_legacy'] = array(
  299. '#type' => 'submit',
  300. '#value' => t('Remove legacy template'),
  301. );
  302. $form['#submit'] = array_reverse($form['#submit']); // we want to go first!
  303. $form['#submit']['subscriptions_mail_mail_edit_form_submit'] = array();
  304. $form['#submit'] = array_reverse($form['#submit']);
  305. }
  306. foreach (array('!sender_name', '!sender_page') as $key) {
  307. $expl = $form['help']['#variables'][$key];
  308. if ($expl[strlen($expl)-1] == '.') {
  309. $add_dot = TRUE;
  310. $expl = substr($expl, 0, strlen($expl) - 1);
  311. }
  312. $expl .= ' '. t('(if the sender is visible)') . (!empty($add_dot) ? '.' : '');
  313. $form['help']['#variables'][$key] = $expl;
  314. }
  315. }
  316. }
  317. /**
  318. * Mail Editor page submit handler.
  319. *
  320. * @ingroup form
  321. */
  322. function subscriptions_mail_mail_edit_form_submit($form_id, $form_values) {
  323. if ($form_values['op'] == $form_values['remove_legacy']) {
  324. variable_del('subscriptions_email_body');
  325. variable_del('subscriptions_email_subject');
  326. drupal_goto($_GET['q']); // no further processing!
  327. }
  328. }
  329. /**
  330. * Assemble mail variables.
  331. */
  332. function subscriptions_mail_mail_edit_variables(&$variables, $mailkey = NULL) {
  333. static $stored_variables;
  334. if (!isset($variables)) {
  335. unset($stored_variables);
  336. }
  337. elseif (isset($mailkey)) {
  338. if (substr($mailkey, 0, 13) == 'subscriptions' && !empty($stored_variables)) {
  339. $variables = $stored_variables + $variables;
  340. }
  341. }
  342. else {
  343. $stored_variables = $variables;
  344. }
  345. }
  346. // TODO: mail_edit.module is hard-coded to call this function,
  347. // and we need it, because only mail_edit.module replaces
  348. // variables such as !recipient_name.
  349. // We ought to be able to get rid of this tight coupling in mail_edit,
  350. // but I (hs) don't see yet how this is supposed to work...
  351. function subscriptions_template_preprocess($template, $mailvars) {
  352. return subscriptions_mail_template_preprocess($template, $mailvars);
  353. }
  354. /**
  355. * Preprocess a mail template (subject or body), detecting conditional clauses
  356. * that conform to a prescribed syntax
  357. *
  358. * @param string $template
  359. * the template for preprocessing
  360. * @param array $mailvars
  361. * an associatvie array of currently existing variables that are to be
  362. * interpolated into the template later , and which can be used by this
  363. * function for preprocessing
  364. *
  365. * This function allows the administrator to specify ternary-type conditions
  366. * to determine what text is used in a mail in a particular situation, using
  367. * the variables that are currently available for that mail for reference.
  368. * The syntax is standard PHP/C-style ternary syntax, but only allows the
  369. * "==" and "!=":
  370. * {{!variable_name==sometext?text for true condition:text for false condition}}
  371. *
  372. * sometext must not contain a question mark, and the true text no colon.
  373. */
  374. function subscriptions_mail_template_preprocess($template, $mailvars) {
  375. preg_match_all('/{{(?P<condition>[^?]+?)\?(?P<true>[^:]*?):(?P<false>[^\]]*?)}}/', $template, $conditions);
  376. // locate the actual operators/operand for each
  377. $replacement = '';
  378. foreach ($conditions[0] as $k => $v) {
  379. preg_match('/(?P<operand_1>!.+)\s*(?P<operator>==|!=)\s*(?P<operand_2>.+)/', $conditions['condition'][$k], $matches);
  380. $operand1 = (isset($mailvars[$matches['operand_1']]) ? $mailvars[$matches['operand_1']] : $matches['operand_1']);
  381. if ($matches['operator'] == '==') {
  382. $replacement = ($operand1 == $matches['operand_2']) ? $conditions['true'][$k] : $conditions['false'][$k];
  383. }
  384. elseif ($matches['operator'] == '!=') {
  385. $replacement = ($operand1 != $matches['operand_2']) ? $conditions['true'][$k] : $conditions['false'][$k];
  386. }
  387. else {
  388. continue;
  389. }
  390. // replace the condition with the result of its evalutation
  391. $template = str_replace($v, $replacement, $template);
  392. }
  393. return $template;
  394. }
  395. /**
  396. * Implementation of hook_mail_alter().
  397. *
  398. * Remove any trailing spaces (must run after mail_edit_mail_alter()!).
  399. */
  400. function subscriptions_mail_mail_alter(&$mailkey, &$to, &$subject, &$body, &$from, &$headers) {
  401. $body = preg_replace('/ +(\r?\n)/', '\\1', $body);
  402. }