simplenews.mail.inc 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926
  1. <?php
  2. /**
  3. * @file
  4. * Simplenews email send and spool handling
  5. *
  6. * @ingroup mail
  7. */
  8. /**
  9. * Add the newsletter node to the mail spool.
  10. *
  11. * @param $node
  12. * The newsletter node to be sent.
  13. *
  14. * @ingroup issue
  15. */
  16. function simplenews_add_node_to_spool($node) {
  17. // To send the newsletter, the node id and target email addresses
  18. // are stored in the spool.
  19. // Only subscribed recipients are stored in the spool (status = 1).
  20. $select = db_select('simplenews_subscriber', 's');
  21. $select->innerJoin('simplenews_subscription', 't', 's.snid = t.snid');
  22. $select->addField('s', 'mail');
  23. $select->addField('s', 'snid');
  24. $select->addField('t', 'tid');
  25. $select->addExpression($node->nid, 'nid');
  26. $select->addExpression(SIMPLENEWS_SUBSCRIPTION_STATUS_SUBSCRIBED, 'status');
  27. $select->addExpression(REQUEST_TIME, 'timestamp');
  28. $select->condition('t.tid', $node->simplenews->tid);
  29. $select->condition('t.status', SIMPLENEWS_SUBSCRIPTION_STATUS_SUBSCRIBED);
  30. $select->condition('s.activated', SIMPLENEWS_SUBSCRIPTION_ACTIVE);
  31. db_insert('simplenews_mail_spool')
  32. ->from($select)
  33. ->execute();
  34. // Update simplenews newsletter status to send pending.
  35. simplenews_newsletter_update_sent_status($node);
  36. // Notify other modules that a newsletter was just spooled.
  37. module_invoke_all('simplenews_spooled', $node);
  38. }
  39. /**
  40. * Send mail spool immediatly if cron should not be used.
  41. *
  42. * @param $conditions
  43. * (Optional) Array of spool conditions which are applied to the query.
  44. */
  45. function simplenews_mail_attempt_immediate_send(array $conditions = array(), $use_batch = TRUE) {
  46. if (variable_get('simplenews_use_cron', TRUE)) {
  47. return FALSE;
  48. }
  49. if ($use_batch) {
  50. // Set up as many send operations as necessary to send all mails with the
  51. // defined throttle amount.
  52. $throttle = variable_get('simplenews_throttle', 20);
  53. $spool_count = simplenews_count_spool($conditions);
  54. $num_operations = ceil($spool_count / $throttle);
  55. $operations = array();
  56. for ($i = 0; $i < $num_operations; $i++) {
  57. $operations[] = array('simplenews_mail_spool', array($throttle, $conditions));
  58. }
  59. // Add separate operations to clear the spool and updat the send status.
  60. $operations[] = array('simplenews_clear_spool', array());
  61. $operations[] = array('simplenews_send_status_update', array());
  62. $batch = array(
  63. 'operations' => $operations,
  64. 'title' => t('Sending mails'),
  65. 'file' => drupal_get_path('module', 'simplenews') . '/includes/simplenews.mail.inc',
  66. );
  67. batch_set($batch);
  68. }
  69. else {
  70. // Send everything that matches the conditions immediatly.
  71. simplenews_mail_spool(SIMPLENEWS_UNLIMITED, $conditions);
  72. simplenews_clear_spool();
  73. simplenews_send_status_update();
  74. }
  75. return TRUE;
  76. }
  77. /**
  78. * Send test version of newsletter.
  79. *
  80. * @param mixed $node
  81. * The newsletter node to be sent.
  82. *
  83. * @ingroup issue
  84. */
  85. function simplenews_send_test($node, $test_addresses) {
  86. // Prevent session information from being saved while sending.
  87. if ($original_session = drupal_save_session()) {
  88. drupal_save_session(FALSE);
  89. }
  90. // Force the current user to anonymous to ensure consistent permissions.
  91. $original_user = $GLOBALS['user'];
  92. $GLOBALS['user'] = drupal_anonymous_user();
  93. // Send the test newsletter to the test address(es) specified in the node.
  94. // Build array of test email addresses
  95. // Send newsletter to test addresses.
  96. // Emails are send direct, not using the spool.
  97. $recipients = array('anonymous' => array(), 'user' => array());
  98. foreach ($test_addresses as $mail) {
  99. $mail = trim($mail);
  100. if (!empty($mail)) {
  101. $subscriber = simplenews_subscriber_load_by_mail($mail);
  102. if (!$subscriber) {
  103. // The source expects a subscriber object with mail and language set.
  104. // @todo: Find a cleaner way to do this.
  105. $subscriber = new stdClass();
  106. $subscriber->uid = 0;
  107. $subscriber->mail = $mail;
  108. $subscriber->language = $GLOBALS['language']->language;
  109. }
  110. if (!empty($account->uid)) {
  111. $recipients['user'][] = $account->name . ' <' . $mail . '>';
  112. }
  113. else {
  114. $recipients['anonymous'][] = $mail;
  115. }
  116. $source = new SimplenewsSourceNode($node, $subscriber);
  117. $source->setKey('test');
  118. $result = simplenews_send_source($source);
  119. }
  120. }
  121. if (count($recipients['user'])) {
  122. $recipients_txt = implode(', ', $recipients['user']);
  123. drupal_set_message(t('Test newsletter sent to user %recipient.', array('%recipient' => $recipients_txt)));
  124. }
  125. if (count($recipients['anonymous'])) {
  126. $recipients_txt = implode(', ', $recipients['anonymous']);
  127. drupal_set_message(t('Test newsletter sent to anonymous %recipient.', array('%recipient' => $recipients_txt)));
  128. }
  129. $GLOBALS['user'] = $original_user;
  130. if ($original_session) {
  131. drupal_save_session(TRUE);
  132. }
  133. }
  134. /**
  135. * Send a node to an email address.
  136. *
  137. * @param $source
  138. * The source object.s
  139. *
  140. * @return boolean
  141. * TRUE if the email was successfully delivered; otherwise FALSE.
  142. *
  143. * @ingroup source
  144. */
  145. function simplenews_send_source(SimplenewsSourceInterface $source) {
  146. $params['simplenews_source'] = $source;
  147. // Send mail.
  148. $message = drupal_mail('simplenews', $source->getKey(), $source->getRecipient(), $source->getLanguage(), $params, $source->getFromFormatted());
  149. // Log sent result in watchdog.
  150. if (variable_get('simplenews_debug', FALSE)) {
  151. if ($message['result']) {
  152. watchdog('simplenews', 'Outgoing email. Message type: %type<br />Subject: %subject<br />Recipient: %to', array('%type' => $source->getKey(), '%to' => $message['to'], '%subject' => $message['subject']), WATCHDOG_DEBUG);
  153. }
  154. else {
  155. watchdog('simplenews', 'Outgoing email failed. Message type: %type<br />Subject: %subject<br />Recipient: %to', array('%type' => $source->getKey(), '%to' => $message['to'], '%subject' => $message['subject']), WATCHDOG_ERROR);
  156. }
  157. }
  158. // Build array of sent results for spool table and reporting.
  159. if ($message['result']) {
  160. $result = array(
  161. 'status' => SIMPLENEWS_SPOOL_DONE,
  162. 'error' => FALSE,
  163. );
  164. }
  165. else {
  166. // This error may be caused by faulty mailserver configuration or overload.
  167. // Mark "pending" to keep trying.
  168. $result = array(
  169. 'status' => SIMPLENEWS_SPOOL_PENDING,
  170. 'error' => TRUE,
  171. );
  172. }
  173. return $result;
  174. }
  175. /**
  176. * Send simplenews newsletters from the spool.
  177. *
  178. * Individual newsletter emails are stored in database spool.
  179. * Sending is triggered by cron or immediately when the node is saved.
  180. * Mail data is retrieved from the spool, rendered and send one by one
  181. * If sending is successful the message is marked as send in the spool.
  182. *
  183. * @todo: Redesign API to allow language counter in multilingual sends.
  184. *
  185. * @param $limit
  186. * (Optional) The maximum number of mails to send. Defaults to
  187. * unlimited.
  188. * @param $conditions
  189. * (Optional) Array of spool conditions which are applied to the query.
  190. *
  191. * @return
  192. * Returns the amount of sent mails.
  193. *
  194. * @ingroup spool
  195. */
  196. function simplenews_mail_spool($limit = SIMPLENEWS_UNLIMITED, array $conditions = array()) {
  197. $check_counter = 0;
  198. // Send pending messages from database cache.
  199. $spool_list = simplenews_get_spool($limit, $conditions);
  200. if ($spool_list) {
  201. // Switch to the anonymous user.
  202. simplenews_impersonate_user(drupal_anonymous_user());
  203. $count_fail = $count_success = 0;
  204. $sent = array();
  205. _simplenews_measure_usec(TRUE);
  206. $spool = new SimplenewsSpool($spool_list);
  207. while ($source = $spool->nextSource()) {
  208. $source->setKey('node');
  209. $result = simplenews_send_source($source);
  210. // Update spool status.
  211. // This is not optimal for performance but prevents duplicate emails
  212. // in case of PHP execution time overrun.
  213. foreach ($spool->getProcessed() as $msid => $row) {
  214. $row_result = isset($row->result) ? $row->result : $result;
  215. simplenews_update_spool(array($msid), $row_result);
  216. if ($row_result['status'] == SIMPLENEWS_SPOOL_DONE) {
  217. $count_success++;
  218. if (!isset($sent[$row->actual_nid])) {
  219. $sent[$row->actual_nid] = 1;
  220. }
  221. else {
  222. $sent[$row->actual_nid]++;
  223. }
  224. }
  225. if ($row_result['error']) {
  226. $count_fail++;
  227. }
  228. }
  229. // Check every n emails if we exceed the limit.
  230. // When PHP maximum execution time is almost elapsed we interrupt
  231. // sending. The remainder will be sent during the next cron run.
  232. if (++$check_counter >= SIMPLENEWS_SEND_CHECK_INTERVAL && ini_get('max_execution_time') > 0) {
  233. $check_counter = 0;
  234. // Break the sending if a percentage of max execution time was exceeded.
  235. $elapsed = _simplenews_measure_usec();
  236. if ($elapsed > SIMPLENEWS_SEND_TIME_LIMIT * ini_get('max_execution_time')) {
  237. watchdog('simplenews', 'Sending interrupted: PHP maximum execution time almost exceeded. Remaining newsletters will be sent during the next cron run. If this warning occurs regularly you should reduce the !cron_throttle_setting.', array('!cron_throttle_setting' => l(t('Cron throttle setting'), 'admin/config/simplenews/mail')), WATCHDOG_WARNING);
  238. break;
  239. }
  240. }
  241. }
  242. // It is possible that all or at the end some results failed to get
  243. // prepared, report them separately.
  244. foreach ($spool->getProcessed() as $msid => $row) {
  245. $row_result = $row->result;
  246. simplenews_update_spool(array($msid), $row_result);
  247. if ($row_result['status'] == SIMPLENEWS_SPOOL_DONE) {
  248. $count_success++;
  249. if (isset($row->actual_nid)) {
  250. if (!isset($sent[$row->actual_nid])) {
  251. $sent[$row->actual_nid] = 1;
  252. }
  253. else {
  254. $sent[$row->actual_nid]++;
  255. }
  256. }
  257. }
  258. if ($row_result['error']) {
  259. $count_fail++;
  260. }
  261. }
  262. // Update subscriber count.
  263. foreach ($sent as $nid => $count) {
  264. db_update('simplenews_newsletter')
  265. ->condition('nid', $nid)
  266. ->expression('sent_subscriber_count', 'sent_subscriber_count + :count', array(':count' => $count))
  267. ->execute();
  268. }
  269. // Report sent result and elapsed time. On Windows systems getrusage() is
  270. // not implemented and hence no elapsed time is available.
  271. if (function_exists('getrusage')) {
  272. watchdog('simplenews', '%success emails sent in %sec seconds, %fail failed sending.', array('%success' => $count_success, '%sec' => round(_simplenews_measure_usec(), 1), '%fail' => $count_fail));
  273. }
  274. else {
  275. watchdog('simplenews', '%success emails sent, %fail failed.', array('%success' => $count_success, '%fail' => $count_fail));
  276. }
  277. variable_set('simplenews_last_cron', REQUEST_TIME);
  278. variable_set('simplenews_last_sent', $count_success);
  279. simplenews_revert_user();
  280. return $count_success;
  281. }
  282. }
  283. /**
  284. * Save mail message in mail cache table.
  285. *
  286. * @param array $spool
  287. * The message to be stored in the spool table, as an array containing the
  288. * following keys:
  289. * - mail
  290. * - nid
  291. * - tid
  292. * - status: (optional) Defaults to SIMPLENEWS_SPOOL_PENDING
  293. * - time: (optional) Defaults to REQUEST_TIME.
  294. *
  295. * @ingroup spool
  296. */
  297. function simplenews_save_spool($spool) {
  298. $status = isset($spool['status']) ? $spool['status'] : SIMPLENEWS_SPOOL_PENDING;
  299. $time = isset($spool['time']) ? $spool['time'] : REQUEST_TIME;
  300. db_insert('simplenews_mail_spool')
  301. ->fields(array(
  302. 'mail' => $spool['mail'],
  303. 'nid' => $spool['nid'],
  304. 'tid' => $spool['tid'],
  305. 'snid' => $spool['snid'],
  306. 'status' => $status,
  307. 'timestamp' => $time,
  308. 'data' => serialize($spool['data']),
  309. ))
  310. ->execute();
  311. }
  312. /*
  313. * Returns the expiration time for IN_PROGRESS status.
  314. *
  315. * @return int
  316. * A unix timestamp. Any IN_PROGRESS messages with a timestamp older than
  317. * this will be re-allocated and re-sent.
  318. */
  319. function simplenews_get_expiration_time() {
  320. $timeout = variable_get('simplenews_spool_progress_expiration', 3600);
  321. $expiration_time = REQUEST_TIME - $timeout;
  322. return $expiration_time;
  323. }
  324. /**
  325. * This function allocates messages to be sent in current run.
  326. *
  327. * Drupal acquire_lock guarantees that no concurrency issue happened.
  328. * If the message status is SIMPLENEWS_SPOOL_IN_PROGRESS but the maximum send
  329. * time has expired, the message id will be returned as a message which is not
  330. * allocated to another process.
  331. *
  332. * @param $limit
  333. * (Optional) The maximum number of mails to load from the spool. Defaults to
  334. * unlimited.
  335. * @param $conditions
  336. * (Optional) Array of conditions which are applied to the query. If not set,
  337. * status defaults to SIMPLENEWS_SPOOL_PENDING, SIMPLENEWS_SPOOL_IN_PROGRESS.
  338. *
  339. * @return
  340. * An array of message ids to be sent in the current run.
  341. *
  342. * @ingroup spool
  343. */
  344. function simplenews_get_spool($limit = SIMPLENEWS_UNLIMITED, $conditions = array()) {
  345. $messages = array();
  346. // Add default status condition if not set.
  347. if (!isset($conditions['status'])) {
  348. $conditions['status'] = array(SIMPLENEWS_SPOOL_PENDING, SIMPLENEWS_SPOOL_IN_PROGRESS);
  349. }
  350. // Special case for the status condition, the in progress actually only
  351. // includes spool items whose locking time has expired. So this need to build
  352. // an OR condition for them.
  353. $status_or = db_or();
  354. $statuses = is_array($conditions['status']) ? $conditions['status'] : array($conditions['status']);
  355. foreach ($statuses as $status) {
  356. if ($status == SIMPLENEWS_SPOOL_IN_PROGRESS) {
  357. $status_or->condition(db_and()
  358. ->condition('status', $status)
  359. ->condition('s.timestamp', simplenews_get_expiration_time(), '<')
  360. );
  361. }
  362. else {
  363. $status_or->condition('status', $status);
  364. }
  365. }
  366. unset($conditions['status']);
  367. $query = db_select('simplenews_mail_spool', 's')
  368. ->fields('s')
  369. ->condition($status_or)
  370. ->orderBy('s.timestamp', 'ASC');
  371. // Add conditions.
  372. foreach ($conditions as $field => $value) {
  373. $query->condition($field, $value);
  374. }
  375. /* BEGIN CRITICAL SECTION */
  376. // The semaphore ensures that multiple processes get different message ID's,
  377. // so that duplicate messages are not sent.
  378. if (lock_acquire('simplenews_acquire_mail')) {
  379. // Get message id's
  380. // Allocate messages
  381. if ($limit > 0) {
  382. $query->range(0, $limit);
  383. }
  384. foreach ($query->execute() as $message) {
  385. if (drupal_strlen($message->data)) {
  386. $message->data = unserialize($message->data);
  387. }
  388. else {
  389. $message->data = simplenews_subscriber_load_by_mail($message->mail);
  390. }
  391. $messages[$message->msid] = $message;
  392. }
  393. if (count($messages) > 0) {
  394. // Set the state and the timestamp of the messages
  395. simplenews_update_spool(
  396. array_keys($messages),
  397. array('status' => SIMPLENEWS_SPOOL_IN_PROGRESS)
  398. );
  399. }
  400. lock_release('simplenews_acquire_mail');
  401. }
  402. /* END CRITICAL SECTION */
  403. return $messages;
  404. }
  405. /**
  406. * Update status of mail data in spool table.
  407. *
  408. * Time stamp is set to current time.
  409. *
  410. * @param array $msids
  411. * Array of Mail spool ids to be updated
  412. * @param array $data
  413. * Array containing email sent results, with the following keys:
  414. * - status: An integer indicating the updated status. Must be one of:
  415. * - 0: hold
  416. * - 1: pending
  417. * - 2: send
  418. * - 3: in progress
  419. * - error: (optional) The error id. Defaults to 0 (no error).
  420. *
  421. * @ingroup spool
  422. */
  423. function simplenews_update_spool($msids, $data) {
  424. db_update('simplenews_mail_spool')
  425. ->condition('msid', $msids)
  426. ->fields(array(
  427. 'status' => $data['status'],
  428. 'error' => isset($result['error']) ? (int)$data['error'] : 0,
  429. 'timestamp' => REQUEST_TIME,
  430. ))
  431. ->execute();
  432. }
  433. /**
  434. * Count data in mail spool table.
  435. *
  436. * @param $conditions
  437. * (Optional) Array of conditions which are applied to the query. Defaults
  438. *
  439. * @return
  440. * Count of mail spool elements of the passed in arguments.
  441. *
  442. * @ingroup spool
  443. */
  444. function simplenews_count_spool(array $conditions = array()) {
  445. // Add default status condition if not set.
  446. if (!isset($conditions['status'])) {
  447. $conditions['status'] = array(SIMPLENEWS_SPOOL_PENDING, SIMPLENEWS_SPOOL_IN_PROGRESS);
  448. }
  449. $query = db_select('simplenews_mail_spool');
  450. // Add conditions.
  451. foreach ($conditions as $field => $value) {
  452. if ($field == 'status') {
  453. if (!is_array($value)) {
  454. $value = array($value);
  455. }
  456. $status_or = db_or();
  457. foreach ($value as $status) {
  458. // Do not count pending entries unless they are expired.
  459. if ($status == SIMPLENEWS_SPOOL_IN_PROGRESS) {
  460. $status_or->condition(db_and()
  461. ->condition('status', $status)
  462. ->condition('timestamp', simplenews_get_expiration_time(), '<')
  463. );
  464. }
  465. else {
  466. $status_or->condition('status', $status);
  467. }
  468. }
  469. $query->condition($status_or);
  470. }
  471. else {
  472. $query->condition($field, $value);
  473. }
  474. }
  475. $query->addExpression('COUNT(*)', 'count');
  476. return (int)$query
  477. ->execute()
  478. ->fetchField();
  479. }
  480. /**
  481. * Remove old records from mail spool table.
  482. *
  483. * All records with status 'send' and time stamp before the expiration date
  484. * are removed from the spool.
  485. *
  486. * @return
  487. * Number of deleted spool rows.
  488. *
  489. * @ingroup spool
  490. */
  491. function simplenews_clear_spool() {
  492. $expiration_time = REQUEST_TIME - variable_get('simplenews_spool_expire', 0) * 86400;
  493. return db_delete('simplenews_mail_spool')
  494. ->condition('status', SIMPLENEWS_SPOOL_DONE)
  495. ->condition('timestamp', $expiration_time, '<=')
  496. ->execute();
  497. }
  498. /**
  499. * Remove records from mail spool table according to the conditions.
  500. *
  501. * @return Count deleted
  502. *
  503. * @ingroup spool
  504. */
  505. function simplenews_delete_spool(array $conditions) {
  506. $query = db_delete('simplenews_mail_spool');
  507. foreach ($conditions as $condition => $value) {
  508. $query->condition($condition, $value);
  509. }
  510. return $query->execute();
  511. }
  512. /**
  513. * Update newsletter sent status.
  514. *
  515. * Set newsletter sent status based on email sent status in spool table.
  516. * Translated and untranslated nodes get a different treatment.
  517. *
  518. * The spool table holds data for emails to be sent and (optionally)
  519. * already send emails. The simplenews_newsletter table contains the overall
  520. * sent status of each newsletter issue (node).
  521. * Newsletter issues get the status pending when sending is initiated. As
  522. * long as unsend emails exist in the spool, the status of the newsletter remains
  523. * unsend. When no pending emails are found the newsletter status is set 'send'.
  524. *
  525. * Translated newsletters are a group of nodes that share the same tnid ({node}.tnid).
  526. * Only one node of the group is found in the spool, but all nodes should share
  527. * the same state. Therefore they are checked for the combined number of emails
  528. * in the spool.
  529. *
  530. * @ingroup issue
  531. */
  532. function simplenews_send_status_update() {
  533. $counts = array(); // number pending of emails in the spool
  534. $sum = array(); // sum of emails in the spool per tnid (translation id)
  535. $send = array(); // nodes with the status 'send'
  536. // For each pending newsletter count the number of pending emails in the spool.
  537. $query = db_select('simplenews_newsletter', 's');
  538. $query->innerJoin('node', 'n', 's.nid = n.nid');
  539. $query->fields('s', array('nid', 'tid'))
  540. ->fields('n', array('tnid'))
  541. ->condition('s.status', SIMPLENEWS_STATUS_SEND_PENDING);
  542. foreach ($query->execute() as $newsletter) {
  543. $counts[$newsletter->tnid][$newsletter->nid] = simplenews_count_spool(array('nid' => $newsletter->nid));
  544. }
  545. // Determine which nodes are send per translation group and per individual node.
  546. foreach ($counts as $tnid => $node_count) {
  547. // The sum of emails per tnid is the combined status result for the group of translated nodes.
  548. // Untranslated nodes have tnid == 0 which will be ignored later.
  549. $sum[$tnid] = array_sum($node_count);
  550. foreach ($node_count as $nid => $count) {
  551. // Translated nodes (tnid != 0)
  552. if ($tnid != '0' && $sum[$tnid] == '0') {
  553. $send[] = $nid;
  554. }
  555. // Untranslated nodes (tnid == 0)
  556. elseif ($tnid == '0' && $count == '0') {
  557. $send[] = $nid;
  558. }
  559. }
  560. }
  561. // Update overall newsletter status
  562. if (!empty($send)) {
  563. foreach ($send as $nid) {
  564. db_update('simplenews_newsletter')
  565. ->condition('nid', $nid)
  566. ->fields(array('status' => SIMPLENEWS_STATUS_SEND_READY))
  567. ->execute();
  568. }
  569. }
  570. }
  571. /**
  572. * Build formatted from-name and email for a mail object.
  573. *
  574. * @return Associative array with (un)formatted from address
  575. * 'address' => From address
  576. * 'formatted' => Formatted, mime encoded, from name and address
  577. */
  578. function _simplenews_set_from() {
  579. $address_default = variable_get('site_mail', ini_get('sendmail_from'));
  580. $name_default = variable_get('site_name', 'Drupal');
  581. $address = variable_get('simplenews_from_address', $address_default);
  582. $name = variable_get('simplenews_from_name', $name_default);
  583. // Windows based PHP systems don't accept formatted email addresses.
  584. $formatted_address = (drupal_substr(PHP_OS, 0, 3) == 'WIN') ? $address : '"'. addslashes(mime_header_encode($name)) .'" <'. $address .'>';
  585. return array(
  586. 'address' => $address,
  587. 'formatted' => $formatted_address,
  588. );
  589. }
  590. /**
  591. * HTML to text conversion for HTML and special characters.
  592. *
  593. * Converts some special HTML characters in addition to drupal_html_to_text()
  594. *
  595. * @param string $text
  596. * The source text with HTML and special characters.
  597. * @param boolean $inline_hyperlinks
  598. * TRUE: URLs will be placed inline.
  599. * FALSE: URLs will be converted to numbered reference list.
  600. * @return string
  601. * The target text with HTML and special characters replaced.
  602. */
  603. function simplenews_html_to_text($text, $inline_hyperlinks = TRUE) {
  604. // By replacing <a> tag by only its URL the URLs will be placed inline
  605. // in the email body and are not converted to a numbered reference list
  606. // by drupal_html_to_text().
  607. // URL are converted to absolute URL as drupal_html_to_text() would have.
  608. if ($inline_hyperlinks) {
  609. $pattern = '@<a[^>]+?href="([^"]*)"[^>]*?>(.+?)</a>@is';
  610. $text = preg_replace_callback($pattern, '_simplenews_absolute_mail_urls', $text);
  611. }
  612. // Replace some special characters before performing the drupal standard conversion.
  613. $preg = _simplenews_html_replace();
  614. $text = preg_replace(array_keys($preg), array_values($preg), $text);
  615. // Perform standard drupal html to text conversion.
  616. return drupal_html_to_text($text);
  617. }
  618. /**
  619. * Helper function for simplenews_html_to_text().
  620. *
  621. * Replaces URLs with absolute URLs.
  622. */
  623. function _simplenews_absolute_mail_urls($match) {
  624. global $base_url, $base_path;
  625. $regexp = &drupal_static(__FUNCTION__);
  626. $url = $label = '';
  627. if ($match) {
  628. if (empty($regexp)) {
  629. $regexp = '@^' . preg_quote($base_path, '@') . '@';
  630. }
  631. list(, $url, $label) = $match;
  632. $url = strpos($url, '://') ? $url : preg_replace($regexp, $base_url . '/', $url);
  633. // If the link is formed by Drupal's URL filter, we only return the URL.
  634. // The URL filter generates a label out of the original URL.
  635. if (strpos($label, '...') === drupal_strlen($label) - 3) {
  636. // Remove ellipsis from end of label.
  637. $label = drupal_substr($label, 0, drupal_strlen($label) - 3);
  638. }
  639. if (strpos($url, $label) !== FALSE) {
  640. return $url;
  641. }
  642. return $label . ' ' . $url;
  643. }
  644. }
  645. /**
  646. * Helper function for simplenews_html_to_text().
  647. *
  648. * List of preg* regular expression patterns to search for and replace with
  649. */
  650. function _simplenews_html_replace() {
  651. return array(
  652. '/&quot;/i' => '"',
  653. '/&gt;/i' => '>',
  654. '/&lt;/i' => '<',
  655. '/&amp;/i' => '&',
  656. '/&copy;/i' => '(c)',
  657. '/&trade;/i' => '(tm)',
  658. '/&#8220;/' => '"',
  659. '/&#8221;/' => '"',
  660. '/&#8211;/' => '-',
  661. '/&#8217;/' => "'",
  662. '/&#38;/' => '&',
  663. '/&#169;/' => '(c)',
  664. '/&#8482;/' => '(tm)',
  665. '/&#151;/' => '--',
  666. '/&#147;/' => '"',
  667. '/&#148;/' => '"',
  668. '/&#149;/' => '*',
  669. '/&reg;/i' => '(R)',
  670. '/&bull;/i' => '*',
  671. '/&euro;/i' => 'Euro ',
  672. );
  673. }
  674. /**
  675. * Helper function to measure PHP execution time in microseconds.
  676. *
  677. * @param bool $start
  678. * If TRUE, reset the time and start counting.
  679. *
  680. * @return float
  681. * The elapsed PHP execution time since the last start.
  682. */
  683. function _simplenews_measure_usec($start = FALSE) {
  684. // Windows systems don't implement getrusage(). There is no alternative.
  685. if (!function_exists('getrusage')) {
  686. return;
  687. }
  688. $start_time = &drupal_static(__FUNCTION__);
  689. $usage = getrusage();
  690. $now = (float)($usage['ru_stime.tv_sec'] . '.' . $usage['ru_stime.tv_usec']) + (float)($usage['ru_utime.tv_sec'] . '.' . $usage['ru_utime.tv_usec']);
  691. if ($start) {
  692. $start_time = $now;
  693. return;
  694. }
  695. return $now - $start_time;
  696. }
  697. /**
  698. * Build subject and body of the test and normal newsletter email.
  699. *
  700. * @param array $message
  701. * Message array as used by hook_mail().
  702. * @param array $source
  703. * The SimplenewsSource instance.
  704. *
  705. * @ingroup source
  706. */
  707. function simplenews_build_newsletter_mail(&$message, SimplenewsSourceInterface $source) {
  708. // Get message data from source.
  709. $message['headers'] = $source->getHeaders($message['headers']);
  710. $message['subject'] = $source->getSubject();
  711. $message['body']['body'] = $source->getBody();
  712. $message['body']['footer'] = $source->getFooter();
  713. // Optional params for HTML mails.
  714. if ($source->getFormat() == 'html') {
  715. $message['params']['plain'] = NULL;
  716. $message['params']['plaintext'] = $source->getPlainBody() . "\n" . $source->getPlainFooter();
  717. $message['params']['attachments'] = $source->getAttachments();
  718. }
  719. else {
  720. $message['params']['plain'] = TRUE;
  721. }
  722. }
  723. /**
  724. * Build subject and body of the subscribe confirmation email.
  725. *
  726. * @param array $message
  727. * Message array as used by hook_mail().
  728. * @param array $params
  729. * Parameter array as used by hook_mail().
  730. */
  731. function simplenews_build_subscribe_mail(&$message, $params) {
  732. $context = $params['context'];
  733. $langcode = $message['language'];
  734. // Use formatted from address "name" <mail_address>
  735. $message['headers']['From'] = $params['from']['formatted'];
  736. $message['subject'] = simplenews_subscription_confirmation_text('subscribe_subject', $langcode);
  737. $message['subject'] = token_replace($message['subject'], $context, array('sanitize' => FALSE));
  738. if (simplenews_user_is_subscribed($context['simplenews_subscriber']->mail, $context['category']->tid)) {
  739. $body = simplenews_subscription_confirmation_text('subscribe_subscribed', $langcode);
  740. }
  741. else {
  742. $body = simplenews_subscription_confirmation_text('subscribe_unsubscribed', $langcode);
  743. }
  744. $message['body'][] = token_replace($body, $context, array('sanitize' => FALSE));
  745. }
  746. /**
  747. * Build subject and body of the subscribe confirmation email.
  748. *
  749. * @param array $message
  750. * Message array as used by hook_mail().
  751. * @param array $params
  752. * Parameter array as used by hook_mail().
  753. */
  754. function simplenews_build_combined_mail(&$message, $params) {
  755. $context = $params['context'];
  756. $changes = $context['changes'];
  757. $langcode = $message['language'];
  758. // Use formatted from address "name" <mail_address>
  759. $message['headers']['From'] = $params['from']['formatted'];
  760. $message['subject'] = simplenews_subscription_confirmation_text('combined_subject', $langcode);
  761. $message['subject'] = token_replace($message['subject'], $context, array('sanitize' => FALSE));
  762. $changes_list = '';
  763. $actual_changes = 0;
  764. foreach (simplenews_confirmation_get_changes_list($context['simplenews_subscriber'], $changes, $langcode) as $tid => $change) {
  765. $changes_list .= ' - ' . $change . "\n";
  766. // Count the actual changes.
  767. $subscribed = simplenews_user_is_subscribed($context['simplenews_subscriber']->mail, $tid);
  768. if ($changes[$tid] == 'subscribe' && !$subscribed || $changes[$tid] == 'unsubscribe' && $subscribed) {
  769. $actual_changes++;
  770. }
  771. }
  772. // If there are actual changes, use the combined_body key otherwise use the
  773. // one without a confirmation link.
  774. $body_key = $actual_changes ? 'combined_body' : 'combined_body_unchanged';
  775. $body = simplenews_subscription_confirmation_text($body_key, $langcode);
  776. // The changes list is not an actual token.
  777. $body = str_replace('[changes-list]', $changes_list, $body);
  778. $message['body'][] = token_replace($body, $context, array('sanitize' => FALSE));
  779. }
  780. /**
  781. * Build subject and body of the unsubscribe confirmation email.
  782. *
  783. * @param array $message
  784. * Message array as used by hook_mail().
  785. * @param array $params
  786. * Parameter array as used by hook_mail().
  787. */
  788. function simplenews_build_unsubscribe_mail(&$message, $params) {
  789. $context = $params['context'];
  790. $langcode = $message['language'];
  791. // Use formatted from address "name" <mail_address>
  792. $message['headers']['From'] = $params['from']['formatted'];
  793. $message['subject'] = simplenews_subscription_confirmation_text('subscribe_subject', $langcode);
  794. $message['subject'] = token_replace($message['subject'], $context, array('sanitize' => FALSE));
  795. if (simplenews_user_is_subscribed($context['simplenews_subscriber']->mail, $context['category']->tid)) {
  796. $body = simplenews_subscription_confirmation_text('unsubscribe_subscribed', $langcode);
  797. $message['body'][] = token_replace($body, $context, array('sanitize' => FALSE));
  798. }
  799. else {
  800. $body = simplenews_subscription_confirmation_text('unsubscribe_unsubscribed', $langcode);
  801. $message['body'][] = token_replace($body, $context, array('sanitize' => FALSE));
  802. }
  803. }
  804. /**
  805. * A mail sending implementation that captures sent messages to a variable.
  806. *
  807. * This class is for running tests or for development and does not convert HTML
  808. * to plaintext.
  809. */
  810. class SimplenewsHTMLTestingMailSystem implements MailSystemInterface {
  811. /**
  812. * Implements MailSystemInterface::format().
  813. */
  814. public function format(array $message) {
  815. // Join the body array into one string.
  816. $message['body'] = implode("\n\n", $message['body']);
  817. // Wrap the mail body for sending.
  818. $message['body'] = drupal_wrap_mail($message['body']);
  819. return $message;
  820. }
  821. /**
  822. * Implements MailSystemInterface::mail().
  823. */
  824. public function mail(array $message) {
  825. $captured_emails = variable_get('drupal_test_email_collector', array());
  826. $captured_emails[] = $message;
  827. // @todo: This is rather slow when sending 100 and more mails during tests.
  828. // Investigate in other methods like APC shared memory.
  829. variable_set('drupal_test_email_collector', $captured_emails);
  830. return TRUE;
  831. }
  832. }