Geen omschrijving

class-emails.php 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785
  1. <?php
  2. use WPForms\Tasks\Actions\EntryEmailsTask;
  3. /**
  4. * Emails.
  5. *
  6. * This class handles all (notification) emails sent by WPForms.
  7. *
  8. * Heavily influenced by the great AffiliateWP plugin by Pippin Williamson.
  9. * https://github.com/AffiliateWP/AffiliateWP/blob/master/includes/emails/class-affwp-emails.php
  10. *
  11. * @since 1.1.3
  12. */
  13. class WPForms_WP_Emails {
  14. /**
  15. * Store the from address.
  16. *
  17. * @since 1.1.3
  18. *
  19. * @var string
  20. */
  21. private $from_address;
  22. /**
  23. * Store the from name.
  24. *
  25. * @since 1.1.3
  26. *
  27. * @var string
  28. */
  29. private $from_name;
  30. /**
  31. * Store the reply-to address.
  32. *
  33. * @since 1.1.3
  34. *
  35. * @var string
  36. */
  37. private $reply_to = false;
  38. /**
  39. * Store the carbon copy addresses.
  40. *
  41. * @since 1.3.1
  42. *
  43. * @var string
  44. */
  45. private $cc = false;
  46. /**
  47. * Store the email content type.
  48. *
  49. * @since 1.1.3
  50. *
  51. * @var string
  52. */
  53. private $content_type;
  54. /**
  55. * Store the email headers.
  56. *
  57. * @since 1.1.3
  58. *
  59. * @var string
  60. */
  61. private $headers;
  62. /**
  63. * Whether to send email in HTML.
  64. *
  65. * @since 1.1.3
  66. *
  67. * @var bool
  68. */
  69. private $html = true;
  70. /**
  71. * The email template to use.
  72. *
  73. * @since 1.1.3
  74. *
  75. * @var string
  76. */
  77. private $template;
  78. /**
  79. * Form data and settings.
  80. *
  81. * @since 1.1.3
  82. *
  83. * @var array
  84. */
  85. public $form_data = array();
  86. /**
  87. * Fields, formatted, and sanitized.
  88. *
  89. * @since 1.1.3
  90. *
  91. * @var array
  92. */
  93. public $fields = array();
  94. /**
  95. * Entry ID.
  96. *
  97. * @since 1.2.3
  98. *
  99. * @var int
  100. */
  101. public $entry_id = '';
  102. /**
  103. * Notification ID that is currently being processed.
  104. *
  105. * @since 1.5.7
  106. *
  107. * @var int
  108. */
  109. public $notification_id = '';
  110. /**
  111. * Get things going.
  112. *
  113. * @since 1.1.3
  114. */
  115. public function __construct() {
  116. if ( 'none' === $this->get_template() ) {
  117. $this->html = false;
  118. }
  119. add_action( 'wpforms_email_send_before', array( $this, 'send_before' ) );
  120. add_action( 'wpforms_email_send_after', array( $this, 'send_after' ) );
  121. }
  122. /**
  123. * Set a property.
  124. *
  125. * @since 1.1.3
  126. *
  127. * @param string $key Object property key.
  128. * @param mixed $value Object property value.
  129. */
  130. public function __set( $key, $value ) {
  131. $this->$key = $value;
  132. }
  133. /**
  134. * Get the email from name.
  135. *
  136. * @since 1.1.3
  137. *
  138. * @return string The email from name
  139. */
  140. public function get_from_name() {
  141. if ( ! empty( $this->from_name ) ) {
  142. $this->from_name = $this->process_tag( $this->from_name );
  143. } else {
  144. $this->from_name = get_bloginfo( 'name' );
  145. }
  146. return apply_filters( 'wpforms_email_from_name', wpforms_decode_string( $this->from_name ), $this );
  147. }
  148. /**
  149. * Get the email from address.
  150. *
  151. * @since 1.1.3
  152. *
  153. * @return string The email from address.
  154. */
  155. public function get_from_address() {
  156. if ( ! empty( $this->from_address ) ) {
  157. $this->from_address = $this->process_tag( $this->from_address );
  158. } else {
  159. $this->from_address = get_option( 'admin_email' );
  160. }
  161. return apply_filters( 'wpforms_email_from_address', wpforms_decode_string( $this->from_address ), $this );
  162. }
  163. /**
  164. * Get the email reply-to.
  165. *
  166. * @since 1.1.3
  167. *
  168. * @return string The email reply-to address.
  169. */
  170. public function get_reply_to() {
  171. if ( ! empty( $this->reply_to ) ) {
  172. $this->reply_to = $this->process_tag( $this->reply_to );
  173. if ( ! is_email( $this->reply_to ) ) {
  174. $this->reply_to = false;
  175. }
  176. }
  177. return apply_filters( 'wpforms_email_reply_to', wpforms_decode_string( $this->reply_to ), $this );
  178. }
  179. /**
  180. * Get the email carbon copy addresses.
  181. *
  182. * @since 1.3.1
  183. *
  184. * @return string The email reply-to address.
  185. */
  186. public function get_cc() {
  187. if ( ! empty( $this->cc ) ) {
  188. $this->cc = $this->process_tag( $this->cc );
  189. $addresses = array_map( 'trim', explode( ',', $this->cc ) );
  190. foreach ( $addresses as $key => $address ) {
  191. if ( ! is_email( $address ) ) {
  192. unset( $addresses[ $key ] );
  193. }
  194. }
  195. $this->cc = implode( ',', $addresses );
  196. }
  197. return apply_filters( 'wpforms_email_cc', wpforms_decode_string( $this->cc ), $this );
  198. }
  199. /**
  200. * Get the email content type.
  201. *
  202. * @since 1.1.3
  203. *
  204. * @return string The email content type.
  205. */
  206. public function get_content_type() {
  207. if ( ! $this->content_type && $this->html ) {
  208. $this->content_type = apply_filters( 'wpforms_email_default_content_type', 'text/html', $this );
  209. } elseif ( ! $this->html ) {
  210. $this->content_type = 'text/plain';
  211. }
  212. return apply_filters( 'wpforms_email_content_type', $this->content_type, $this );
  213. }
  214. /**
  215. * Get the email headers.
  216. *
  217. * @since 1.1.3
  218. *
  219. * @return string The email headers.
  220. */
  221. public function get_headers() {
  222. if ( ! $this->headers ) {
  223. $this->headers = "From: {$this->get_from_name()} <{$this->get_from_address()}>\r\n";
  224. if ( $this->get_reply_to() ) {
  225. $this->headers .= "Reply-To: {$this->get_reply_to()}\r\n";
  226. }
  227. if ( $this->get_cc() ) {
  228. $this->headers .= "Cc: {$this->get_cc()}\r\n";
  229. }
  230. $this->headers .= "Content-Type: {$this->get_content_type()}; charset=utf-8\r\n";
  231. }
  232. return apply_filters( 'wpforms_email_headers', $this->headers, $this );
  233. }
  234. /**
  235. * Build the email.
  236. *
  237. * @since 1.1.3
  238. *
  239. * @param string $message The email message.
  240. *
  241. * @return string
  242. */
  243. public function build_email( $message ) {
  244. // Plain text email shortcut.
  245. if ( false === $this->html ) {
  246. $message = $this->process_tag( $message );
  247. $message = str_replace( '{all_fields}', $this->wpforms_html_field_value( false ), $message );
  248. return apply_filters( 'wpforms_email_message', wpforms_decode_string( $message ), $this );
  249. }
  250. /*
  251. * Generate an HTML email.
  252. */
  253. ob_start();
  254. $this->get_template_part( 'header', $this->get_template(), true );
  255. // Hooks into the email header.
  256. do_action( 'wpforms_email_header', $this );
  257. $this->get_template_part( 'body', $this->get_template(), true );
  258. // Hooks into the email body.
  259. do_action( 'wpforms_email_body', $this );
  260. $this->get_template_part( 'footer', $this->get_template(), true );
  261. // Hooks into the email footer.
  262. do_action( 'wpforms_email_footer', $this );
  263. $message = $this->process_tag( $message );
  264. $message = nl2br( $message );
  265. $body = ob_get_clean();
  266. $message = str_replace( '{email}', $message, $body );
  267. $message = str_replace( '{all_fields}', $this->wpforms_html_field_value( true ), $message );
  268. $message = make_clickable( $message );
  269. return apply_filters( 'wpforms_email_message', $message, $this );
  270. }
  271. /**
  272. * Send the email.
  273. *
  274. * @since 1.1.3
  275. *
  276. * @param string $to The To address.
  277. * @param string $subject The subject line of the email.
  278. * @param string $message The body of the email.
  279. * @param array $attachments Attachments to the email.
  280. *
  281. * @return bool
  282. */
  283. public function send( $to, $subject, $message, $attachments = array() ) {
  284. if ( ! did_action( 'init' ) && ! did_action( 'admin_init' ) ) {
  285. _doing_it_wrong( __FUNCTION__, esc_html__( 'You cannot send emails with WPForms_WP_Emails() until init/admin_init has been reached.', 'wpforms-lite' ), null );
  286. return false;
  287. }
  288. // Don't send anything if emails have been disabled.
  289. if ( $this->is_email_disabled() ) {
  290. return false;
  291. }
  292. // Don't send if email address is invalid.
  293. if ( ! is_email( $to ) ) {
  294. return false;
  295. }
  296. // Hooks before email is sent.
  297. do_action( 'wpforms_email_send_before', $this );
  298. // Deprecated filter for $attachments.
  299. $attachments = apply_filters_deprecated(
  300. 'wpforms_email_attachments',
  301. array( $attachments, $this ),
  302. '1.5.7 of the WPForms plugin',
  303. 'wpforms_emails_send_email_data'
  304. );
  305. /*
  306. * Allow to filter data on per-email basis,
  307. * useful for localizations based on recipient email address, form settings,
  308. * or for specific notifications - whatever available in WPForms_WP_Emails class.
  309. */
  310. $data = apply_filters(
  311. 'wpforms_emails_send_email_data',
  312. array(
  313. 'to' => $to,
  314. 'subject' => $subject,
  315. 'message' => $message,
  316. 'headers' => $this->get_headers(),
  317. 'attachments' => $attachments,
  318. ),
  319. $this
  320. );
  321. $send_same_process = apply_filters(
  322. 'wpforms_tasks_entry_emails_trigger_send_same_process',
  323. false,
  324. $this->fields,
  325. ! empty( wpforms()->entry ) ? wpforms()->entry->get( $this->entry_id ) : [],
  326. $this->form_data,
  327. $this->entry_id,
  328. 'entry'
  329. );
  330. if (
  331. $send_same_process ||
  332. ! empty( $this->form_data['settings']['disable_entries'] )
  333. ) {
  334. // Let's do this NOW.
  335. $result = wp_mail(
  336. $data['to'],
  337. $this->get_prepared_subject( $data['subject'] ),
  338. $this->build_email( $data['message'] ),
  339. $data['headers'],
  340. $data['attachments']
  341. );
  342. } else {
  343. // Schedule the email.
  344. $result = (bool) ( new EntryEmailsTask() )
  345. ->params(
  346. $data['to'],
  347. $this->get_prepared_subject( $data['subject'] ),
  348. $this->build_email( $data['message'] ),
  349. $data['headers'],
  350. $data['attachments']
  351. )
  352. ->register();
  353. }
  354. // Hooks after the email is sent.
  355. do_action( 'wpforms_email_send_after', $this );
  356. return $result;
  357. }
  358. /**
  359. * Add filters/actions before the email is sent.
  360. *
  361. * @since 1.1.3
  362. */
  363. public function send_before() {
  364. add_filter( 'wp_mail_from', array( $this, 'get_from_address' ) );
  365. add_filter( 'wp_mail_from_name', array( $this, 'get_from_name' ) );
  366. add_filter( 'wp_mail_content_type', array( $this, 'get_content_type' ) );
  367. }
  368. /**
  369. * Remove filters/actions after the email is sent.
  370. *
  371. * @since 1.1.3
  372. */
  373. public function send_after() {
  374. remove_filter( 'wp_mail_from', array( $this, 'get_from_address' ) );
  375. remove_filter( 'wp_mail_from_name', array( $this, 'get_from_name' ) );
  376. remove_filter( 'wp_mail_content_type', array( $this, 'get_content_type' ) );
  377. }
  378. /**
  379. * Convert text formatted HTML. This is primarily for turning line breaks
  380. * into <p> and <br/> tags.
  381. *
  382. * @since 1.1.3
  383. *
  384. * @param string $message Text to convert.
  385. *
  386. * @return string
  387. */
  388. public function text_to_html( $message ) {
  389. if ( 'text/html' === $this->content_type || true === $this->html ) {
  390. $message = wpautop( $message );
  391. }
  392. return $message;
  393. }
  394. /**
  395. * Process a smart tag.
  396. * Decodes entities and sanitized (keeping line breaks) by default.
  397. *
  398. * @uses wpforms_decode_string()
  399. *
  400. * @since 1.1.3
  401. * @since 1.6.0 Deprecated 2 params: $sanitize, $linebreaks.
  402. *
  403. * @param string $string String that may contain tags.
  404. *
  405. * @return string
  406. */
  407. public function process_tag( $string = '' ) {
  408. return wpforms_process_smart_tags( $string, $this->form_data, $this->fields, $this->entry_id );
  409. }
  410. /**
  411. * Process the all fields smart tag if present.
  412. *
  413. * @since 1.1.3
  414. *
  415. * @param bool $is_html_email Toggle to use HTML or plaintext.
  416. *
  417. * @return string
  418. */
  419. public function wpforms_html_field_value( $is_html_email = true ) { // phpcs:ignore
  420. if ( empty( $this->fields ) ) {
  421. return '';
  422. }
  423. if ( empty( $this->form_data['fields'] ) ) {
  424. $is_html_email = false;
  425. }
  426. $message = '';
  427. if ( $is_html_email ) {
  428. /*
  429. * HTML emails.
  430. */
  431. ob_start();
  432. // Hooks into the email field.
  433. do_action( 'wpforms_email_field', $this );
  434. $this->get_template_part( 'field', $this->get_template(), true );
  435. $field_template = ob_get_clean();
  436. // Check to see if user has added support for field type.
  437. $other_fields = apply_filters( 'wpforms_email_display_other_fields', array(), $this );
  438. $x = 1;
  439. foreach ( $this->form_data['fields'] as $field_id => $field ) {
  440. $field_name = '';
  441. $field_val = '';
  442. // If the field exists in the form_data but not in the final
  443. // field data, then it's a non-input based field, "other fields".
  444. if ( empty( $this->fields[ $field_id ] ) ) {
  445. if ( empty( $other_fields ) || ! in_array( $field['type'], $other_fields, true ) ) {
  446. continue;
  447. }
  448. if ( $field['type'] === 'divider' ) {
  449. $field_name = ! empty( $field['label'] ) ? str_repeat( '&mdash;', 3 ) . ' ' . $field['label'] . ' ' . str_repeat( '&mdash;', 3 ) : null;
  450. $field_val = ! empty( $field['description'] ) ? $field['description'] : '';
  451. } elseif ( $field['type'] === 'pagebreak' ) {
  452. if ( ! empty( $field['position'] ) && $field['position'] === 'bottom' ) {
  453. continue;
  454. }
  455. $title = ! empty( $field['title'] ) ? $field['title'] : esc_html__( 'Page Break', 'wpforms-lite' );
  456. $field_name = str_repeat( '&mdash;', 6 ) . ' ' . $title . ' ' . str_repeat( '&mdash;', 6 );
  457. } elseif ( $field['type'] === 'html' ) {
  458. // If CL is enabled and the field is conditionally hidden, hide it from message.
  459. if ( ! empty( $this->form_data['fields'][ $field['id'] ]['conditionals'] ) && ! wpforms_conditional_logic_fields()->field_is_visible( $this->form_data, $field['id'] ) ) {
  460. continue;
  461. }
  462. $field_name = ! empty( $field['name'] ) ? $field['name'] : esc_html__( 'HTML / Code Block', 'wpforms-lite' );
  463. $field_val = $field['code'];
  464. }
  465. } else {
  466. if (
  467. ! apply_filters( 'wpforms_email_display_empty_fields', false ) &&
  468. ( ! isset( $this->fields[ $field_id ]['value'] ) || (string) $this->fields[ $field_id ]['value'] === '' )
  469. ) {
  470. continue;
  471. }
  472. $field_name = $this->fields[ $field_id ]['name'];
  473. $field_val = empty( $this->fields[ $field_id ]['value'] ) && ! is_numeric( $this->fields[ $field_id ]['value'] ) ? '<em>' . esc_html__( '(empty)', 'wpforms-lite' ) . '</em>' : $this->fields[ $field_id ]['value'];
  474. }
  475. if ( empty( $field_name ) && null !== $field_name ) {
  476. $field_name = sprintf( /* translators: %d - field ID. */
  477. esc_html__( 'Field ID #%d', 'wpforms-lite' ),
  478. absint( $field['id'] )
  479. );
  480. }
  481. $field_item = $field_template;
  482. if ( 1 === $x ) {
  483. $field_item = str_replace( 'border-top:1px solid #dddddd;', '', $field_item );
  484. }
  485. $field_item = str_replace( '{field_name}', $field_name, $field_item );
  486. $field_item = str_replace(
  487. '{field_value}',
  488. apply_filters(
  489. 'wpforms_html_field_value',
  490. $field_val,
  491. isset( $this->fields[ $field_id ] ) ? $this->fields[ $field_id ] : $field,
  492. $this->form_data,
  493. 'email-html'
  494. ),
  495. $field_item
  496. );
  497. $message .= wpautop( $field_item );
  498. $x ++;
  499. }
  500. } else {
  501. /*
  502. * Plain Text emails.
  503. */
  504. foreach ( $this->fields as $field ) {
  505. if (
  506. ! apply_filters( 'wpforms_email_display_empty_fields', false ) &&
  507. ( ! isset( $field['value'] ) || (string) $field['value'] === '' )
  508. ) {
  509. continue;
  510. }
  511. $field_val = empty( $field['value'] ) && ! is_numeric( $field['value'] ) ? esc_html__( '(empty)', 'wpforms-lite' ) : $field['value'];
  512. $field_name = $field['name'];
  513. if ( empty( $field_name ) ) {
  514. $field_name = sprintf( /* translators: %d - field ID. */
  515. esc_html__( 'Field ID #%d', 'wpforms-lite' ),
  516. absint( $field['id'] )
  517. );
  518. }
  519. $message .= '--- ' . $field_name . " ---\r\n\r\n";
  520. $field_value = $field_val . "\r\n\r\n";
  521. $message .= apply_filters( 'wpforms_plaintext_field_value', $field_value, $field, $this->form_data );
  522. }
  523. }
  524. if ( empty( $message ) ) {
  525. $empty_message = esc_html__( 'An empty form was submitted.', 'wpforms-lite' );
  526. $message = $is_html_email ? wpautop( $empty_message ) : $empty_message;
  527. }
  528. return $message;
  529. }
  530. /**
  531. * Email kill switch if needed.
  532. *
  533. * @since 1.1.3
  534. *
  535. * @return bool
  536. */
  537. public function is_email_disabled() {
  538. return (bool) apply_filters( 'wpforms_disable_all_emails', false, $this );
  539. }
  540. /**
  541. * Get the enabled email template.
  542. *
  543. * @since 1.1.3
  544. *
  545. * @return string When filtering return 'none' to switch to text/plain email.
  546. */
  547. public function get_template() {
  548. if ( ! $this->template ) {
  549. $this->template = wpforms_setting( 'email-template', 'default' );
  550. }
  551. return apply_filters( 'wpforms_email_template', $this->template );
  552. }
  553. /**
  554. * Retrieve a template part. Taken from bbPress.
  555. *
  556. * @since 1.1.3
  557. *
  558. * @param string $slug Template file slug.
  559. * @param string $name Optional. Default null.
  560. * @param bool $load Maybe load.
  561. *
  562. * @return string
  563. */
  564. public function get_template_part( $slug, $name = null, $load = true ) {
  565. // Setup possible parts.
  566. $templates = array();
  567. if ( isset( $name ) ) {
  568. $templates[] = $slug . '-' . $name . '.php';
  569. }
  570. $templates[] = $slug . '.php';
  571. // Return the part that is found.
  572. return $this->locate_template( $templates, $load, false );
  573. }
  574. /**
  575. * Retrieve the name of the highest priority template file that exists.
  576. *
  577. * Search in the STYLESHEETPATH before TEMPLATEPATH so that themes which
  578. * inherit from a parent theme can just overload one file. If the template is
  579. * not found in either of those, it looks in the theme-compat folder last.
  580. *
  581. * Taken from bbPress.
  582. *
  583. * @since 1.1.3
  584. *
  585. * @param string|array $template_names Template file(s) to search for, in order.
  586. * @param bool $load If true the template file will be loaded if it is found.
  587. * @param bool $require_once Whether to require_once or require. Default true.
  588. * Has no effect if $load is false.
  589. *
  590. * @return string The template filename if one is located.
  591. */
  592. public function locate_template( $template_names, $load = false, $require_once = true ) {
  593. // No file found yet.
  594. $located = false;
  595. // Try to find a template file.
  596. foreach ( (array) $template_names as $template_name ) {
  597. // Continue if template is empty.
  598. if ( empty( $template_name ) ) {
  599. continue;
  600. }
  601. // Trim off any slashes from the template name.
  602. $template_name = ltrim( $template_name, '/' );
  603. // Try locating this template file by looping through the template paths.
  604. foreach ( $this->get_theme_template_paths() as $template_path ) {
  605. if ( file_exists( $template_path . $template_name ) ) {
  606. $located = $template_path . $template_name;
  607. break;
  608. }
  609. }
  610. }
  611. if ( ( true === $load ) && ! empty( $located ) ) {
  612. load_template( $located, $require_once );
  613. }
  614. return $located;
  615. }
  616. /**
  617. * Return a list of paths to check for template locations
  618. *
  619. * @since 1.1.3
  620. *
  621. * @return array
  622. */
  623. public function get_theme_template_paths() {
  624. $template_dir = 'wpforms-email';
  625. $file_paths = array(
  626. 1 => trailingslashit( get_stylesheet_directory() ) . $template_dir,
  627. 10 => trailingslashit( get_template_directory() ) . $template_dir,
  628. 100 => WPFORMS_PLUGIN_DIR . 'includes/emails/templates',
  629. );
  630. $file_paths = apply_filters( 'wpforms_email_template_paths', $file_paths );
  631. // Sort the file paths based on priority.
  632. ksort( $file_paths, SORT_NUMERIC );
  633. return array_map( 'trailingslashit', $file_paths );
  634. }
  635. /**
  636. * Perform email subject preparation: process tags, remove new lines, etc.
  637. *
  638. * @since 1.6.1
  639. *
  640. * @param string $subject Email subject to post-process.
  641. *
  642. * @return string
  643. */
  644. private function get_prepared_subject( $subject ) {
  645. $subject = $this->process_tag( $subject );
  646. $subject = trim( str_replace( [ "\r\n", "\r", "\n" ], ' ', $subject ) );
  647. return wpforms_decode_string( $subject );
  648. }
  649. }