]*>(\s| |
|,|:)*\1>#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 .= ' |
' . 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 .= '' . ( (int) $n + 1 ) . ' | ' . $step . ' |
' . 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 .= '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| |
)*
' . 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 . '' . 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; } }