]*>(\s| ||,|:)*#i', '', $text ); $text = preg_replace( '/[ \t]*,[ \t]*(?=,|\n|$)/', '', $text ); $text = preg_replace( "/\n{3,}/", "\n\n", $text ); return trim( $text ); } /** @var string|null While a test is sent: the only recipient. */ private static $redirect = null; /** @var bool Result of the last send(). */ private static $last_sent = false; /** * Settings "Send Test Email": the real 3.0 email, built from a sample * application (never a borrower's data) and sent only to $to. * * @param string $to Address typed by the admin. * @param string $type client | admin | reminder. * @return bool Sent. */ public static function send_test( $to, $type ) { $id = self::sample_application(); if ( ! $id ) { return false; } self::$redirect = $to; self::$last_sent = false; // The sample is deleted right after, so its links just open the form. $plain = function ( $url, $app_id, $token, $base ) { return $base; }; add_filter( 'mapp_resume_url', $plain, 10, 4 ); try { if ( 'client' === $type ) { self::borrower_receipt( $id ); } elseif ( 'reminder' === $type ) { self::reminder( $id, \Mapp\Rest\Controller::resume_url( $id, 'sample' ) ); } else { $subject = self::option( 'mortgage_application_mail_subject', 'mortgage_application_use_network_settings' ); self::lo_alert( $id, 'submitted', $subject ? $subject : __( 'New application: {first_name} {last_name}', '1003-mortgage-application' ), '{all_fields}' ); } } finally { remove_filter( 'mapp_resume_url', $plain, 10 ); self::$redirect = null; \Mapp\Integrations\ConnectorGuard::quietly( function () use ( $id ) { wp_delete_post( $id, true ); } ); } return self::$last_sent; } /** * A submitted sample application (Jane Sample, purchase in Austin) with * the site's first loan officer. Saved through the normal code so every * derived value is real; deleted right after the test. * * @return int Application ID, or 0. */ private static function sample_application() { $id = Repository::create( array( 'lang' => 'en' ) ); if ( ! $id ) { return 0; } $los = LoanOfficers::published(); if ( $los ) { update_post_meta( $id, 'mapp_lo_id', (int) key( $los ) ); } Repository::save_answers( $id, array( 'purpose' => 'Home Purchase', 'zip_code' => array( 'formatted' => '123 Main St, Austin, TX 78701', 'street' => '123 Main St', 'city' => 'Austin', 'state' => 'TX', 'zip' => '78701' ), 'home_description' => 'Single Family', 'property_use' => 'Primary Residence', 'loan_purpose_purchase' => 'Within 30 Days', 'first_time_buyer' => 'Yes', 'purchase_price' => '350000', 'down_payment' => '35000', 'credit_rating' => 'Good', 'military' => 'No', 'employment_status' => 'Employed', 'monthly_income' => '8500', 'first_name' => 'Jane', 'last_name' => 'Sample', 'email' => 'jane.sample@example.com', 'phone_number' => '512 555 0199', 'mailing_address' => '9 Elm St, Austin, TX 78702', 'bankruptcy' => 'No', 'foreclosure' => 'No', ) ); Repository::refresh_derived( $id ); Repository::advance( $id, Repository::STAGE_CONTACT ); Repository::advance( $id, Repository::STAGE_SUBMITTED ); return $id; } /** * @param int $id Application ID. * @param string $milestone Milestone. */ public static function on_milestone( $id, $milestone ) { switch ( $milestone ) { case 'contact': if ( 'no' !== get_option( 'mapp_alert_partial', 'yes' ) ) { self::lo_alert( $id, $milestone, __( 'New lead in progress: {first_name} {last_name}', '1003-mortgage-application' ), __( '{first_name} saved their contact details and is still answering questions. You will get another email when they submit.', '1003-mortgage-application' ) ); } // The contact screen promises the borrower their results. if ( 'no' !== get_option( 'mapp_borrower_numbers', 'yes' ) && Repository::STAGE_CONTACT === Repository::stage( $id ) ) { self::borrower_numbers( $id ); } break; case 'submitted': $subject = self::option( 'mortgage_application_mail_subject', 'mortgage_application_use_network_settings' ); self::lo_alert( $id, $milestone, $subject ? $subject : __( 'New application: {first_name} {last_name}', '1003-mortgage-application' ), '{all_fields}' ); if ( self::receipt_on() ) { self::borrower_receipt( $id ); } break; case 'full_submitted': self::lo_alert( $id, $milestone, __( 'Full 1003 submitted: {first_name} {last_name}', '1003-mortgage-application' ), __( '{first_name} completed the full application.', '1003-mortgage-application' ) . "\n\n{all_fields}" ); break; case 'documents': self::lo_alert( $id, $milestone, __( 'Documents uploaded: {first_name} {last_name}', '1003-mortgage-application' ), __( '{first_name} uploaded documents.', '1003-mortgage-application' ) . "\n\n{documents}" ); break; } } /** * @param string $option Option name. * @param string $network Network-settings toggle. * @param string $mode text | html. * @return string */ private static function option( $option, $network, $mode = 'text' ) { $value = function_exists( 'mapp_get_option_with_shortcode' ) ? (string) mapp_get_option_with_shortcode( $option, $network, $mode ) : (string) get_option( $option, '' ); // An empty box means the default, not a blank email. if ( '' === trim( wp_strip_all_tags( $value ) ) && function_exists( 'mapp_default_option' ) ) { $value = mapp_default_option( $option ); } return $value; } /** * LO-side recipients for an application. * * @param int $id Application ID. * @return string[] */ public static function recipients( $id ) { $out = array(); $lo = LoanOfficers::profile( (int) get_post_meta( $id, 'mapp_lo_id', true ) ); if ( $lo ) { $out = $lo['notify']; } else { $route = (string) get_post_meta( $id, 'dynamic_email', true ); if ( is_email( $route ) ) { $out[] = $route; } } if ( empty( $out ) || 'yes' === get_option( 'mapp_notify_site_too', 'no' ) ) { $site = self::option( 'mortgage_application_email_recipients', 'mortgage_application_use_network_settings' ); $out = array_merge( $out, array_filter( array_map( 'trim', explode( ',', $site ) ), 'is_email' ) ); } return array_values( array_unique( array_map( 'strtolower', $out ) ) ); } /** * @param int $id Application ID. * @param string $milestone Milestone. * @param string $subject Subject template. * @param string $message Message template. */ private static function lo_alert( $id, $milestone, $subject, $message ) { $to = self::recipients( $id ); if ( empty( $to ) && null === self::$redirect ) { return; } $headers = self::reply_to( trim( get_post_meta( $id, 'first_name', true ) . ' ' . get_post_meta( $id, 'last_name', true ) ), (string) get_post_meta( $id, 'email', true ) ); $body = self::render( $id, $message, 'lo', $milestone ); self::send( implode( ', ', $to ), self::fill_text( $id, $subject ), $body, $headers ); } /** * The 2.6 "Client Email Send" switch. 3.0 sends the receipt unless that * switch was saved off (in 2.6 it was off until someone turned it on). * * @return bool */ public static function receipt_on() { $value = function_exists( 'get_front_mortgage_application_option' ) ? get_front_mortgage_application_option( 'mortgage_application_client_email_recipients', 'mortgage_application_use_network_settings' ) : get_option( 'mortgage_application_client_email_recipients', false ); return (bool) apply_filters( 'mapp_send_receipt', false === $value || null === $value || 'on' === $value ); } /** * Borrower receipt after the short app: a confirmation, their numbers, * what happens next (with the documents their answers call for), a link * back to upload and finish, then every answer. * * The 2.6 receipt settings still give the subject and the opening (in * English); {all_fields} there is ignored, the answers always go last. * * @param int $id Application ID. */ private static function borrower_receipt( $id ) { $to = sanitize_email( (string) get_post_meta( $id, 'email', true ) ); if ( ! $to ) { return; } $es = 'es' === get_post_meta( $id, 'mapp_lang', true ); if ( $es ) { $subject = 'Recibimos su solicitud'; $intro = "Hola {first_name},\n\nGracias por su solicitud. Esto es lo que sigue."; } else { $subject = self::option( 'mortgage_application_user_mail_subject', 'mortgage_application_use_network_settings' ); $intro = self::option( 'mortgage_application_user_mail_message', 'mortgage_application_use_network_settings', 'html' ); $subject = $subject ? $subject : __( 'We received your application', '1003-mortgage-application' ); $intro = trim( self::clean_template( (string) $intro ) ); $intro = '' !== $intro ? $intro : "Hi {first_name},\n\nThank you for your application. Here is what happens next."; } $lo = LoanOfficers::profile( (int) get_post_meta( $id, 'mapp_lo_id', true ) ); $headers = $lo ? self::reply_to( $lo['name'], $lo['email'] ) : array(); self::send( $to, self::fill_text( $id, $subject ), self::render( $id, $intro . "\n\n{receipt}", 'borrower', 'submitted', array( '{receipt}' => self::receipt_body( $id, $es, $lo ) ) ), $headers ); } /** * @param int $id Application ID. * @param bool $es Spanish. * @param array|null $lo LO profile. * @return string HTML. */ private static function receipt_body( $id, $es, $lo ) { $t = function ( $en, $sp ) use ( $es ) { return $es ? $sp : $en; }; $h = function ( $text ) { return '

' . esc_html( $text ) . '

'; }; $link = Layout::accent(); $when = (int) get_post_meta( $id, 'mapp_submitted_at', true ); $when = $when ? $when : time(); // Confirmation. $out = '
'; $out .= '

✓ ' . esc_html( $t( 'Application received', 'Solicitud recibida' ) ) . '

'; $out .= '

' . esc_html( $t( 'Submitted', 'Enviada el' ) . ' ' . wp_date( 'm-d-Y', $when ) . ' · ' . $t( 'Reference', 'Referencia' ) . ' #' . (int) $id ) . '

'; $out .= '
'; // Numbers. $rows = \Mapp\Application\Numbers::rows( $id, $es ? 'es' : 'en' ); if ( $rows ) { $out .= $h( $t( 'Your numbers', 'Sus números' ) ) . self::numbers_table( $rows ); $out .= '

' . esc_html( $t( 'Estimates from your answers, not a loan offer or a commitment to lend.', 'Estimados según sus respuestas; no son una oferta ni un compromiso de préstamo.' ) ) . '

'; } // Next steps. $steps = array(); $steps[] = $lo ? sprintf( $t( '%s reviews your application and reaches out.', '%s revisa su solicitud y se comunica con usted.' ), esc_html( $lo['name'] ) ) . ( '' !== $lo['phone'] ? ' ' . esc_html( $lo['phone'] ) . '' : '' ) : esc_html( $t( 'Your loan officer reviews your application and reaches out.', 'Su oficial de préstamos revisa su solicitud y se comunica con usted.' ) ); $docs = \Mapp\Files\Store::checklist( $id, $es ? 'es' : 'en' ); $have = wp_list_pluck( Store::files_for( $id ), 'doc_type' ); $list = ''; foreach ( $docs as $type => $name ) { $done = in_array( $type, $have, true ); $list .= '
  • ' . esc_html( $name ) . ( $done ? ' (' . esc_html( $t( 'received', 'recibido' ) ) . ')' : '' ) . '
  • '; } $steps[] = esc_html( $t( 'Send your documents. These speed things up:', 'Envíe sus documentos. Estos agilizan el proceso:' ) ) . ''; if ( ! in_array( Repository::stage( $id ), array( Repository::STAGE_FULL_SUBMITTED ), true ) ) { $steps[] = esc_html( $t( 'Finish the full application when you are ready. It takes about 15 minutes and saves as you go.', 'Termine la solicitud completa cuando quiera. Toma unos 15 minutos y se guarda sola.' ) ); } $out .= $h( $t( 'What happens next', 'Lo que sigue' ) ) . ''; foreach ( $steps as $n => $step ) { $out .= ''; } $out .= '
    ' . ( (int) $n + 1 ) . '
    ' . $step . '
    '; // One token, two places: the document checklist, or where they left off. $token = \Mapp\Application\Tokens::issue( $id, 'link' ); $buttons = Layout::button( \Mapp\Rest\Controller::resume_url( $id, $token, 'docs' ), $t( 'Upload your documents', 'Suba sus documentos' ) ); $more = array(); if ( Repository::STAGE_FULL_SUBMITTED !== Repository::stage( $id ) ) { $more[ $t( 'Continue the full application', 'Continuar la solicitud completa' ) ] = \Mapp\Rest\Controller::resume_url( $id, $token ); } if ( $lo && '' !== $lo['booking_url'] ) { /* translators: %s: LO first name */ $more[ sprintf( $t( 'Book a call with %s', 'Agende una llamada con %s' ), strtok( $lo['name'], ' ' ) ) ] = $lo['booking_url']; } $out .= $buttons . Layout::buttons( $more ); $out .= '

    ' . esc_html( sprintf( $t( 'The link opens your application and works for %d days. Keep this email.', 'El enlace abre su solicitud y funciona por %d días. Guarde este correo.' ), round( \Mapp\Application\Tokens::lifetime() / DAY_IN_SECONDS ) ) ) . '

    '; // Answers. $answers = self::answers_table( $id, 'borrower', $es ? 'es' : 'en' ); if ( '' !== $answers ) { $out .= $h( $t( 'Your answers', 'Sus respuestas' ) ) . $answers; } return $out; } /** * Numbers as a table, the first (the one that matters) large. * * @param array $rows [ label, value ] rows. * @return string */ private static function numbers_table( $rows ) { $out = ''; foreach ( array_values( $rows ) as $i => $row ) { if ( 0 === $i ) { $out .= '' . esc_html( $row[0] ) . '' . esc_html( $row[1] ) . ''; continue; } $out .= '' . esc_html( $row[0] ) . '' . esc_html( $row[1] ) . ''; } return '' . $out . '
    '; } /** * The borrower's results after the contact step, with a link to continue. * * @param int $id Application ID. */ public static function borrower_numbers( $id ) { $to = sanitize_email( (string) get_post_meta( $id, 'email', true ) ); $es = 'es' === get_post_meta( $id, 'mapp_lang', true ); $rows = \Mapp\Application\Numbers::rows( $id, $es ? 'es' : 'en' ); if ( ! $to || ! $rows ) { return; } $table = self::numbers_table( $rows ); $url = \Mapp\Rest\Controller::resume_url( $id, \Mapp\Application\Tokens::issue( $id, 'link' ) ); $message = $es ? "Hola {first_name},\n\nEstos son los números de su solicitud:\n\n{numbers}\n\nSolo son estimados, no una oferta ni un compromiso de préstamo.\n\nSu solicitud está guardada. Continúe cuando quiera:\n\n{resume_button}" : "Hi {first_name},\n\nHere are the numbers from your application:\n\n{numbers}\n\nThese are estimates, not a loan offer or a commitment to lend.\n\nYour application is saved. Pick up where you left off any time:\n\n{resume_button}"; $lo = LoanOfficers::profile( (int) get_post_meta( $id, 'mapp_lo_id', true ) ); self::send( $to, $es ? 'Sus números hipotecarios' : 'Your mortgage numbers', self::render( $id, $message, 'borrower', 'contact', array( '{numbers}' => $table, '{resume_button}' => self::button( $url, $es ? 'Continuar mi solicitud' : 'Continue my application' ), ) ), $lo ? self::reply_to( $lo['name'], $lo['email'] ) : array() ); } /** * Email the borrower a resume link. * * @param int $id Application ID. * @param string $url Resume URL with token. */ public static function resume_link( $id, $url ) { $to = sanitize_email( (string) get_post_meta( $id, 'email', true ) ); if ( ! $to ) { return; } $es = 'es' === get_post_meta( $id, 'mapp_lang', true ); $subject = $es ? 'Continúe su solicitud' : 'Pick up where you left off'; $message = $es ? "Hola {first_name},\n\nUse este enlace para continuar su solicitud donde la dejó:\n\n{resume_button}\n\nEl enlace vence en {resume_days} días. Si usted no lo pidió, puede ignorar este correo." : "Hi {first_name},\n\nUse this link to continue your application where you left off:\n\n{resume_button}\n\nThe link expires in {resume_days} days. If you did not ask for it, you can ignore this email."; self::send( $to, $subject, self::render( $id, $message, 'borrower', 'resume', array( '{resume_button}' => self::button( $url, $es ? 'Continuar mi solicitud' : 'Continue my application' ) ) ) ); } /** * Reminder for an application left at the contact stage. * * @param int $id Application ID. * @param string $url Resume URL with token. */ public static function reminder( $id, $url ) { $to = sanitize_email( (string) get_post_meta( $id, 'email', true ) ); if ( ! $to ) { return; } $es = 'es' === get_post_meta( $id, 'mapp_lang', true ); $subject = self::option( 'mortgage_application_reminder_mail_subject', 'mortgage_application_use_network_settings' ); $message = self::option( 'mortgage_application_reminder_mail_message', 'mortgage_application_use_network_settings', 'html' ); $message = self::clean_template( $message ); if ( $es || '' === trim( wp_strip_all_tags( $message ) ) ) { $subject = $es ? 'Su solicitud está casi lista' : ( $subject ? $subject : 'Your application is almost done' ); $message = $es ? "Hola {first_name},\n\nSu solicitud está guardada. Termínela en un par de minutos." : "Hi {first_name},\n\nYour application is saved. You can finish it in a couple of minutes."; } // The button always follows the text. $message .= "\n\n{edit_url}"; self::send( $to, self::fill_text( $id, $subject ), self::render( $id, $message, 'borrower', 'reminder', array( '{edit_url}' => self::button( $url, $es ? 'Terminar mi solicitud' : 'Finish my application' ) ) ), self::lo_reply_to( $id ) ); } /** * Reminder for a full 1003 that was started and left (built-in text). * * @param int $id Application ID. * @param string $url Resume URL with token. */ public static function full_reminder( $id, $url ) { $to = sanitize_email( (string) get_post_meta( $id, 'email', true ) ); if ( ! $to ) { return; } $es = 'es' === get_post_meta( $id, 'mapp_lang', true ); $subject = $es ? 'Termine su solicitud completa' : 'Finish your full application'; $message = $es ? "Hola {first_name},\n\nSu solicitud completa está guardada, con todas sus respuestas. Continúe donde la dejó:\n\n{edit_url}\n\nSi tiene preguntas, responda a este correo." : "Hi {first_name},\n\nYour full application is saved with everything you have answered so far. Pick up where you left off:\n\n{edit_url}\n\nQuestions? Just reply to this email."; self::send( $to, $subject, self::render( $id, $message, 'borrower', 'reminder', array( '{edit_url}' => self::button( $url, $es ? 'Continuar mi solicitud' : 'Finish my full application' ) ) ), self::lo_reply_to( $id ) ); } /** * Reply-To the application's loan officer, if it has one. * * @param int $id Application ID. * @return string[] */ private static function lo_reply_to( $id ) { $lo = LoanOfficers::profile( (int) get_post_meta( $id, 'mapp_lo_id', true ) ); return $lo ? self::reply_to( $lo['name'], $lo['email'] ) : array(); } /** * Fill placeholders and wrap in the email layout. * * Order matters: the template (admin-written or built in) gets paragraphs * and kses first; then placeholders are filled, borrower values escaped; * generated tables and buttons go in last, untouched. * * @param int $id Application ID. * @param string $message Template (HTML or text). * @param string $audience lo | borrower. * @param string $milestone Milestone. * @param array $extra More placeholder => HTML. * @return string HTML. */ private static function render( $id, $message, $audience, $milestone, $extra = array() ) { $lang = 'borrower' === $audience && 'es' === get_post_meta( $id, 'mapp_lang', true ) ? 'es' : 'en'; $lo = LoanOfficers::profile( (int) get_post_meta( $id, 'mapp_lo_id', true ) ); $message = trim( (string) $message ); if ( ! preg_match( '/<(p|div|table|br|ul|ol|h[1-6])\b/i', $message ) ) { $message = wpautop( $message ); } $message = wp_kses_post( $message ); $replace = array_merge( array( '{first_name}' => esc_html( (string) get_post_meta( $id, 'first_name', true ) ), '{last_name}' => esc_html( (string) get_post_meta( $id, 'last_name', true ) ), '{email}' => esc_html( (string) get_post_meta( $id, 'email', true ) ), '{all_fields}' => self::answers_table( $id, $audience, $lang ), '{documents}' => self::documents_list( $id, 'documents' === $milestone ), '{lo_name}' => $lo ? esc_html( $lo['name'] ) : '', '{lo_phone}' => $lo ? esc_html( $lo['phone'] ) : '', '{lo_email}' => $lo ? esc_html( $lo['email'] ) : '', '{lo_nmls}' => $lo ? esc_html( $lo['nmls'] ) : '', '{resume_days}' => (string) round( \Mapp\Application\Tokens::lifetime() / DAY_IN_SECONDS ), '{admin_url}' => esc_url( admin_url( 'post.php?post=' . (int) $id . '&action=edit' ) ), '{application_id}' => (string) (int) $id, ), $extra ); $message = strtr( $message, $replace ); // Export buttons (Pro license). Templates may place them; LO alerts get // the row anyway. Unlicensed: the placeholders are dropped. $exports = 'lo' === $audience ? self::export_links( $id ) : array(); $intro = '

    Download for your LOS (built from the latest answers when you click; sign in as an administrator):

    '; $tags = array( '{mismo_file_url}' => 'MISMO 3.4', '{fnm_file_url}' => 'FNM 3.2', '{csv_file_url}' => 'CSV' ); $first = null; $named = array(); foreach ( $tags as $tag => $label ) { $at = strpos( $message, $tag ); if ( false !== $at ) { $named[ $label ] = isset( $exports[ $label ] ) ? $exports[ $label ] : null; $first = null === $first ? $at : min( $first, $at ); } } $placed = (bool) $named; if ( $placed ) { // The template's export tags become one row, where the first one was. $row = array_filter( $named ); $message = substr_replace( $message, '{mapp_exports}', $first, 0 ); $message = str_replace( array_keys( $tags ), '', $message ); $message = str_replace( '{mapp_exports}', $row ? $intro . Layout::buttons( $row ) : '', $message ); $message = preg_replace( '#

    (\s| |)*

    #i', '', $message ); } $message = str_replace( '{edit_url}', '', $message ); if ( 'lo' === $audience ) { $message = self::summary( $id, $milestone ) . $message; if ( $exports && ! $placed && in_array( $milestone, array( 'submitted', 'full_submitted', 'documents' ), true ) ) { $message .= $intro . Layout::buttons( $exports ); } $message .= '
    ' . self::button( admin_url( 'post.php?post=' . (int) $id . '&action=edit' ), 'Open application' ) . '
    '; } return Layout::wrap( $message, $lo, $lang ); } /** * The LO's at-a-glance block: who, how to reach them, what they want. * * @param int $id Application ID. * @param string $milestone Milestone. * @return string */ private static function summary( $id, $milestone ) { $get = function ( $key ) use ( $id ) { return (string) get_post_meta( $id, $key, true ); }; $stages = array( 'contact' => 'In progress (contact saved)', 'submitted' => 'Short app submitted', 'full_submitted' => 'Full 1003 submitted', 'documents' => 'Documents uploaded', ); $link = Layout::accent(); $name = trim( $get( 'first_name' ) . ' ' . $get( 'last_name' ) ); $phone = $get( 'phone_number' ); $email = $get( 'email' ); $facts = array_filter( array( 'Home Purchase' === $get( 'purpose' ) ? 'Purchase' : ( 'Home Refinance' === $get( 'purpose' ) ? 'Refinance' : '' ), '' !== $get( 'loan_amount' ) ? '$' . number_format( (float) $get( 'loan_amount' ) ) . ' estimated loan' : '', Schema::display_value( 'credit_rating', $get( 'credit_rating' ) ), $get( 'property_city' ) ? $get( 'property_city' ) . ( $get( 'property_state' ) ? ', ' . $get( 'property_state' ) : '' ) : '', ) ); $source = $get( 'mapp_utm_source' ); $out = '

    ' . esc_html( isset( $stages[ $milestone ] ) ? $stages[ $milestone ] : '' ) . '

    '; $out .= '

    ' . esc_html( '' !== $name ? $name : 'New applicant' ) . '

    '; $lines = array(); if ( '' !== $phone ) { $lines[] = '' . esc_html( $phone ) . ''; } if ( '' !== $email ) { $lines[] = '' . esc_html( $email ) . ''; } if ( $lines ) { $out .= '

    ' . implode( ' · ', $lines ) . '

    '; } if ( $facts ) { $out .= '

    ' . esc_html( implode( ' · ', $facts ) ) . '

    '; } if ( 'yes' !== $get( 'consent_tcpa' ) ) { $out .= '

    No call/text consent given: email only, or call manually.

    '; } if ( '' !== $source ) { $out .= '

    Source: ' . esc_html( $source ) . '

    '; } return $out . '
    '; } /** * Subject placeholders (plain text). * * @param int $id Application ID. * @param string $subject Subject. * @return string */ private static function fill_text( $id, $subject ) { return trim( strtr( $subject, array( '{first_name}' => (string) get_post_meta( $id, 'first_name', true ), '{last_name}' => (string) get_post_meta( $id, 'last_name', true ), '{email}' => (string) get_post_meta( $id, 'email', true ), ) ) ); } /** * Every answer as a two-column table. SSNs are masked; borrowers see the * questions as they were asked, LOs see short labels. * * @param int $id Application ID. * @param string $audience lo | borrower. * @param string $lang Language. * @return string */ public static function answers_table( $id, $audience, $lang ) { $answers = Repository::answers( $id, 'lo' === $audience ); $rows = ''; foreach ( array( 'short', 'contact', 'final', 'full' ) as $stage ) { foreach ( Schema::questions_in( $stage ) as $key => $def ) { if ( ! isset( $answers[ $key ] ) || 'consent' === $def['type'] || ! Schema::is_visible( $key, $answers ) ) { continue; } $value = Schema::display_value( $key, $answers[ $key ], $lang ); if ( '' === $value ) { continue; } $label = 'lo' === $audience ? Schema::admin_label( $key ) : Schema::text( $def['label'], $lang ); $rows .= '' . esc_html( $label ) . '' . '' . nl2br( esc_html( $value ) ) . ''; } } $loan_amount = get_post_meta( $id, 'loan_amount', true ); if ( '' !== $loan_amount && 'lo' === $audience ) { $rows .= 'Estimated loan amount$' . esc_html( number_format( (float) $loan_amount ) ) . ''; } return $rows ? '' . $rows . '
    ' : ''; } /** * Signed download links for the application's documents. * * @param int $id Application ID. * @param bool $this_batch Only the batch just sent (the documents alert). * @return string */ private static function documents_list( $id, $this_batch = false ) { $batch = (int) get_post_meta( $id, 'mapp_documents_batch', true ); $items = ''; $earlier = 0; foreach ( Store::files_for( $id ) as $file_id => $file ) { if ( $this_batch && $batch && (int) ( isset( $file['batch'] ) ? $file['batch'] : 0 ) !== $batch ) { ++$earlier; continue; } $type = isset( $file['doc_type'] ) && '' !== $file['doc_type'] ? Store::doc_label( $file['doc_type'] ) : ''; $items .= '' . esc_html( $file['name'] ) . '' . ( '' !== $type ? '
    ' . esc_html( $type ) . '' : '' ) . '' . esc_html( size_format( $file['size'] ) ) . ''; } if ( '' === $items ) { return ''; } $out = '' . $items . '
    '; if ( $earlier ) { /* translators: %d: count */ $out .= '

    ' . esc_html( sprintf( _n( '%d earlier file is on the application.', '%d earlier files are on the application.', $earlier, '1003-mortgage-application' ), $earlier ) ) . '

    '; } /* translators: %d: days */ return $out . '

    ' . esc_html( sprintf( __( 'Links expire in %d days.', '1003-mortgage-application' ), Store::link_days() ) ) . '

    '; } /** * @param string $url URL. * @param string $label Label. * @return string */ public static function button( $url, $label ) { return Layout::button( $url, $label ); } /** * Export downloads for the LO alert (added by the Pro module). The links * run the export when clicked, so the file always has the latest answers; * the handler requires a signed-in administrator. * * @param int $id Application ID. * @return array Label => URL. */ public static function export_links( $id ) { if ( ! function_exists( 'mapp_generate_export_token' ) ) { return array(); } $base = add_query_arg( array( 'action' => 'mortgage_application_export_applications', 'post' => (int) $id, 'export_token' => mapp_generate_export_token( $id ), ), admin_url( 'admin-post.php' ) ); return (array) apply_filters( 'mapp_email_export_links', array(), $id, $base ); } /** * A Reply-To header with a quoted display name. * * @param string $name Name. * @param string $email Email. * @return string[] */ private static function reply_to( $name, $email ) { $email = sanitize_email( $email ); if ( ! is_email( $email ) ) { return array(); } $name = trim( str_replace( array( '"', "\r", "\n", '\\' ), '', (string) $name ) ); return array( 'Reply-To: ' . ( '' !== $name ? '"' . $name . '" ' : '' ) . '<' . $email . '>' ); } /** * Send with the 2.6 From / Reply-To settings. Not through 2.6's * mortgage_mail(): its wp_kses_post pass strips the (charset, * color-scheme) and Outlook markup. Template text is kses'd in render(). * * @param string $to Recipients. * @param string $subject Subject. * @param string $html Body. * @param array $headers Headers. * @return bool */ public static function send( $to, $subject, $html, $headers = array() ) { if ( null !== self::$redirect ) { $to = self::$redirect; $subject = '[TEST] ' . $subject; } $from = sanitize_email( self::option( 'mortgage_application_mail_from', 'mortgage_application_use_network_settings' ) ); $from = is_email( $from ) ? $from : (string) get_option( 'admin_email' ); $from_name = trim( str_replace( array( '"', "\r", "\n" ), '', sanitize_text_field( self::option( 'mortgage_application_mail_from_name', 'mortgage_application_use_network_settings' ) ) ) ); $from_name = '' !== $from_name ? $from_name : get_bloginfo( 'name' ); if ( ! preg_grep( '/^Reply-To:/i', $headers ) ) { $reply = sanitize_email( self::option( 'mortgage_application_mail_reply_to', 'mortgage_application_use_network_settings' ) ); if ( is_email( $reply ) ) { $headers[] = 'Reply-To: <' . $reply . '>'; } } $headers[] = 'From: "' . $from_name . '" <' . $from . '>'; $headers[] = 'Content-Type: text/html; charset=UTF-8'; $subject = str_replace( array( "\r", "\n" ), ' ', wp_strip_all_tags( $subject ) ); $sent = wp_mail( $to, $subject, $html, $headers ); self::$last_sent = (bool) $sent; if ( ! $sent && defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( '1003 Mortgage Application: wp_mail returned false for "' . $subject . '"' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions } return (bool) $sent; } }