..., 'body' => ... ) ). PAYGRAB Pro adds an editor for them. * * @package PAYGRAB */ defined( 'ABSPATH' ) || exit; /** * Compose and send PAYGRAB emails. */ class Paygrab_Mailer { /** * Hooks. * * @return void */ public static function init() { add_action( 'paygrab_order_placed', array( __CLASS__, 'on_placed' ), 10, 2 ); add_action( 'paygrab_order_status_changed', array( __CLASS__, 'on_status' ), 10, 4 ); } /** * Site name used in subjects and signatures. * * @return string */ private static function site() { $name = (string) Paygrab_Settings::get( 'mail_from_name' ); return '' !== $name ? $name : wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ); } /** * Address that receives notices for the seller. * * @return string */ public static function admin_address() { $to = (string) Paygrab_Settings::get( 'mail_admin' ); return is_email( $to ) ? $to : (string) get_option( 'admin_email' ); } /** * Send one mail. * * @param string $type Mail type (used in the filter name). * @param string $to Recipient. * @param string $subject Subject. * @param string $body Body. * @param Paygrab_Order|null $order Related order (for the history). * @param string $html HTML version; the text version is sent with it for text-only mail apps. * @param array $extra More values for $context['values'] (e.g. customer, link, valid_until). * @return bool */ public static function send( $type, $to, $subject, $body, $order = null, $html = '', $extra = array() ) { /** * Filters a PAYGRAB email before it is sent. * * Return an empty array to not send it. PAYGRAB Pro's mail editor replaces the subject and * the text here, filling in the values from $context['values']. * * @param array $mail subject, body (text), html (optional). * @param Paygrab_Order|null $order Related order. * @param array $context type, to, values (name => value, see placeholders()). */ $mail = apply_filters( 'paygrab_mail_' . $type, array( 'subject' => $subject, 'body' => $body, 'html' => $html, ), $order, array( 'type' => $type, 'to' => $to, 'values' => array_merge( self::placeholders( $order ), $extra ), ) ); if ( empty( $mail ) || empty( $to ) ) { return false; } $from = (string) Paygrab_Settings::get( 'mail_from' ); $from_name = self::site(); $set_from = static fn() => $from; $set_name = static fn() => $from_name; if ( is_email( $from ) ) { add_filter( 'wp_mail_from', $set_from, 99 ); } add_filter( 'wp_mail_from_name', $set_name, 99 ); if ( ! empty( $mail['html'] ) ) { // One email with both versions: HTML for most mail apps, text for the rest. $text = $mail['body']; $set_text = static function ( $phpmailer ) use ( $text ) { $phpmailer->AltBody = $text; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase }; add_action( 'phpmailer_init', $set_text ); $sent = wp_mail( $to, $mail['subject'], $mail['html'], array( 'Content-Type: text/html; charset=UTF-8' ) ); remove_action( 'phpmailer_init', $set_text ); } else { $sent = wp_mail( $to, $mail['subject'], $mail['body'], array( 'Content-Type: text/plain; charset=UTF-8' ) ); } remove_filter( 'wp_mail_from', $set_from, 99 ); remove_filter( 'wp_mail_from_name', $set_name, 99 ); if ( $order instanceof Paygrab_Order ) { Paygrab_Orders::add_event( $order->id, 'mail_' . $type, $sent /* translators: %s: email subject */ ? sprintf( __( 'Sent “%s”.', 'paygrab' ), $mail['subject'] ) /* translators: %s: email subject */ : sprintf( __( 'Could not send “%s”.', 'paygrab' ), $mail['subject'] ) ); } return (bool) $sent; } /** * Values an email can fill in (for PAYGRAB Pro's mail editor). * * @param Paygrab_Order|null $order Related order. * @return array name => value. Names: site, site_url, signature, and with an order: * customer, product, amount, order_number, deadline, transfer_to, * transfer_code, transfer_name, mypage_url. */ public static function placeholders( $order = null ) { $values = array( 'site' => self::site(), 'site_url' => home_url( '/' ), 'signature' => ltrim( self::footer(), "\n" ), ); if ( $order instanceof Paygrab_Order ) { $items = $order->items(); $values = array_merge( $values, array( 'customer' => $order->billing_name, 'product' => implode( paygrab_comma(), wp_list_pluck( $items, 'name' ) ), 'amount' => paygrab_price( $order->total ), 'order_number' => $order->order_number, 'deadline' => $order->pay_deadline ? paygrab_deadline( $order->pay_deadline ) : '', 'transfer_to' => 'bank_manual' === $order->gateway ? Paygrab_Gateway_Bank_Manual::account_text() : '', 'transfer_code' => (string) $order->transfer_code, 'transfer_name' => $order->transfer_name_example(), 'mypage_url' => paygrab_mypage_url( $order->order_number ), ) ); } /** * Filters the values an email can fill in. * * @param array $values name => value. * @param Paygrab_Order|null $order Related order. */ return (array) apply_filters( 'paygrab_mail_placeholders', $values, $order ); } /** * Subject with the site name in front. * * @param string $text Subject text. * @return string */ private static function subject( $text ) { return '【' . self::site() . '】' . $text; } /** * Common footer of buyer emails. * * @return string */ private static function footer() { $lines = array( '', '──────────', self::site(), home_url( '/' ) ); $law = paygrab_page_url( 'tokushoho' ); if ( $law ) { $lines[] = __( 'Legal notice:', 'paygrab' ) . ' ' . $law; } return implode( "\n", $lines ); } /** * Order lines (number, items, amount). * * @param Paygrab_Order $order Order. * @param bool $tax Show the tax. * @return string */ private static function order_lines( Paygrab_Order $order, $tax = false ) { $lines = array( '■' . __( 'Your order', 'paygrab' ) ); $lines[] = __( 'Order number', 'paygrab' ) . paygrab_colon() . $order->order_number; foreach ( $order->items() as $item ) { $lines[] = __( 'Product', 'paygrab' ) . paygrab_colon() . $item->name; } $amount = paygrab_price( $order->total ) . __( ' (tax included)', 'paygrab' ); if ( $tax ) { $parts = array(); foreach ( $order->tax_breakdown() as $rate => $row ) { /* translators: 1: tax rate, 2: tax amount */ $parts[] = sprintf( __( 'tax %1$d%%: %2$s', 'paygrab' ), $rate, paygrab_price( $row['tax'] ) ); } $amount = paygrab_price( $order->total ) . paygrab_open() . implode( paygrab_comma(), $parts ) . paygrab_close(); } $lines[] = __( 'Amount', 'paygrab' ) . paygrab_colon() . $amount; return implode( "\n", $lines ); } /** * Mails after an order was placed. * * @param Paygrab_Order $order Order. * @param Paygrab_Gateway $gateway Gateway. * @return void */ public static function on_placed( Paygrab_Order $order, Paygrab_Gateway $gateway ) { if ( ! $order->is_pending() ) { return; // Paid at once (Pro: card) – the "paid" mail covers it. } $instructions = $gateway->instructions_text( $order ); /* translators: %s: buyer name */ $body = sprintf( __( 'Dear %s,', 'paygrab' ), $order->billing_name ) . "\n\n" . __( 'Thank you for your order.', 'paygrab' ) . "\n" . ( $order->pay_deadline /* translators: %s: payment deadline */ ? sprintf( __( 'Please pay by %s.', 'paygrab' ), paygrab_deadline( $order->pay_deadline ) ) . "\n" : '' ) . "\n" . self::order_lines( $order ) . "\n\n" . ( $instructions ? $instructions . "\n\n" : '' ) . __( 'We will email you when we have confirmed your payment.', 'paygrab' ) . "\n" . __( 'My page:', 'paygrab' ) . ' ' . paygrab_mypage_url( $order->order_number ) . "\n\n" . __( 'If you do not pay by the deadline, the order is cancelled automatically.', 'paygrab' ) . self::footer(); self::send( 'order_received', $order->billing_email, self::subject( __( 'Thank you for your order (how to pay)', 'paygrab' ) ), $body, $order ); /* translators: %s: order number */ $admin_subject = sprintf( __( '[PAYGRAB] New order (awaiting payment) %s', 'paygrab' ), $order->order_number ); $admin_body = __( 'A new order came in.', 'paygrab' ) . "\n\n" . __( 'Order number', 'paygrab' ) . paygrab_colon() . $order->order_number . "\n" . __( 'Customer', 'paygrab' ) . paygrab_colon() . $order->billing_name . paygrab_open() . $order->billing_email . paygrab_close() . "\n" . __( 'Product', 'paygrab' ) . paygrab_colon() . $order->summary() . "\n" . __( 'Amount', 'paygrab' ) . paygrab_colon() . paygrab_price( $order->total ) . "\n" . ( 'bank_manual' === $order->gateway ? __( 'Transfer number', 'paygrab' ) . paygrab_colon() . $order->transfer_code . "\n" : '' ) . ( $order->pay_deadline ? __( 'Deadline', 'paygrab' ) . paygrab_colon() . paygrab_deadline( $order->pay_deadline ) . "\n" : '' ) . "\n" . ( 'bank_manual' === $order->gateway ? __( 'When you have confirmed the payment, press “Confirm payment” in the order list.', 'paygrab' ) . "\n" : '' ) . paygrab_admin_order_url( $order->id ); self::send( 'admin_new_order', self::admin_address(), $admin_subject, $admin_body, $order ); } /** * Mails after a status change. * * @param Paygrab_Order $order Order. * @param string $from Previous status. * @param string $to New status. * @param array $args notify, by. * @return void */ public static function on_status( Paygrab_Order $order, $from, $to, $args ) { if ( empty( $args['notify'] ) ) { return; } /* translators: %s: buyer name */ $hello = sprintf( __( 'Dear %s,', 'paygrab' ), $order->billing_name ) . "\n\n"; if ( 'paid' === $to ) { $lines = array(); foreach ( Paygrab_Downloads::for_order( $order->id ) as $row ) { $remaining = Paygrab_Downloads::remaining( $row ); $lines[] = null === $remaining ? __( 'Downloads: unlimited', 'paygrab' ) : sprintf( /* translators: %d: count */ __( 'Downloads: up to %d times', 'paygrab' ), $remaining ); $lines[] = $row->expires_at ? sprintf( /* translators: %s: date */ __( 'Available until: %s', 'paygrab' ), paygrab_date( $row->expires_at ) ) : __( 'Available: no time limit', 'paygrab' ); break; } $body = $hello . __( 'We have confirmed your payment. You can download the product from My page.', 'paygrab' ) . "\n\n" . '■' . __( 'Download', 'paygrab' ) . "\n" . paygrab_mypage_url( $order->order_number ) . "\n" . __( '(Log in to download.)', 'paygrab' ) . "\n" . implode( "\n", $lines ) . "\n\n" . self::order_lines( $order, true ) . "\n" . __( 'Payment method', 'paygrab' ) . paygrab_colon() . $order->payment_label() . "\n" . __( 'Payment date', 'paygrab' ) . paygrab_colon() . paygrab_date( $order->paid_at ) . "\n" . __( 'Seller', 'paygrab' ) . paygrab_colon() . ( Paygrab_Settings::get( 'law_seller' ) ? Paygrab_Settings::get( 'law_seller' ) : self::site() ) . self::footer(); self::send( 'order_paid', $order->billing_email, self::subject( __( 'We have confirmed your payment (download)', 'paygrab' ) ), $body, $order ); } elseif ( 'cancelled' === $to ) { $by_buyer = 'buyer' === ( $args['by'] ?? '' ); $reason = $by_buyer ? __( 'Your order was cancelled as you requested.', 'paygrab' ) : ( 'expired' === ( $args['by'] ?? '' ) ? __( 'We cancelled your order because we did not receive the payment by the deadline.', 'paygrab' ) : __( 'Your order was cancelled.', 'paygrab' ) ); $body = $hello . $reason . "\n\n" . self::order_lines( $order ) . "\n\n" . __( 'If you still want the product, please order again from the product page.', 'paygrab' ) . self::footer(); self::send( 'order_cancelled', $order->billing_email, self::subject( __( 'Your order was cancelled', 'paygrab' ) ), $body, $order ); if ( $by_buyer ) { /* translators: %s: order number */ $subject = sprintf( __( '[PAYGRAB] Order cancelled by the customer %s', 'paygrab' ), $order->order_number ); self::send( 'admin_cancelled', self::admin_address(), $subject, self::order_lines( $order ) . "\n" . __( 'Customer', 'paygrab' ) . paygrab_colon() . $order->billing_name, $order ); } } elseif ( 'refunded' === $to ) { $body = $hello . __( 'Your order was refunded. The download is no longer available.', 'paygrab' ) . "\n\n" . self::order_lines( $order ) . self::footer(); self::send( 'order_refunded', $order->billing_email, self::subject( __( 'Your order was refunded', 'paygrab' ) ), $body, $order ); } } /** * Reminder the day before the payment deadline. * * @param Paygrab_Order $order Order. * @return bool */ public static function reminder( Paygrab_Order $order ) { $gateway = Paygrab_Gateways::get( $order->gateway ); /* translators: %s: buyer name */ $body = sprintf( __( 'Dear %s,', 'paygrab' ), $order->billing_name ) . "\n\n" /* translators: %s: payment deadline */ . sprintf( __( 'The payment deadline for your order is %s. If you have not paid yet, please pay by then.', 'paygrab' ), paygrab_deadline( $order->pay_deadline ) ) . "\n" . __( 'If you have already paid, please ignore this email.', 'paygrab' ) . "\n\n" . self::order_lines( $order ) . "\n\n" . ( $gateway ? $gateway->instructions_text( $order ) . "\n\n" : '' ) . __( 'My page:', 'paygrab' ) . ' ' . paygrab_mypage_url( $order->order_number ) . self::footer(); return self::send( 'payment_reminder', $order->billing_email, self::subject( __( 'Your payment deadline is tomorrow', 'paygrab' ) ), $body, $order ); } /** * Email with a button (HTML) and the same content for text-only mail apps (M4). * * @param string $type Mail type. * @param string $to Recipient. * @param string $subject Subject (without the site name). * @param string $name Recipient's name. * @param array $text html (lead for the HTML version, "press the button"), plain (lead for the text version, * "open the URL"), button, ignore_html, ignore_plain, period (e.g. "24 hours"). * @param string $url Link. * @param int $expires Unix time when the link stops working. * @return bool */ private static function send_link( $type, $to, $subject, $name, array $text, $url, $expires ) { $site = self::site(); /* translators: %s: name */ $dear = sprintf( __( 'Dear %s,', 'paygrab' ), $name ); /* translators: date and time format for link expiry, see https://www.php.net/manual/datetime.format.php */ $when = wp_date( __( 'F j H:i', 'paygrab' ), $expires ); $values = array_merge( self::placeholders(), array( 'customer' => $name, 'link' => $url, 'valid_until' => $when, ) ); /** * Filters the wording of an email with a button (PAYGRAB Pro's mail editor changes it here). * The link, the button and the expiry line are always added. * * @param array $text html, plain, button, ignore_html, ignore_plain, period. * @param string $type verify_email | password_reset | email_change * @param array $values Values to fill in (site, customer, link, valid_until, …). */ $text = (array) apply_filters( 'paygrab_mail_link_text', $text, $type, $values ) + $text; /* translators: 1: date and time, 2: period such as "24 hours" */ $until = sprintf( __( 'Valid until: %1$s (%2$s)', 'paygrab' ), $when, $text['period'] ); $plain = $dear . "\n\n" . $text['plain'] . "\n\n" . $url . "\n\n" . $until . "\n\n" . $text['ignore_plain'] . self::footer(); $p = static fn( $t, $small = false ) => '

' . $t . '

'; $law = paygrab_page_url( 'tokushoho' ); $html = '' . '
' . '
' . esc_html( $site ) . '
' . $p( esc_html( $dear ) ) . $p( nl2br( esc_html( $text['html'] ) ) ) . '

' . esc_html( $text['button'] ) . '

' . $p( esc_html__( 'If the button does not work, paste this URL into your browser.', 'paygrab' ) . '
' . esc_html( $url ) . '', true ) /* translators: 1: date and time, 2: period such as "24 hours" */ . $p( wp_kses( sprintf( __( 'This link is valid until %1$s (%2$s).', 'paygrab' ), esc_html( $when ), esc_html( $text['period'] ) ), array( 'strong' => array() ) ), true ) . '
' . $p( nl2br( esc_html( $text['ignore_html'] ) ), true ) . '

' . esc_html( $site ) . ' ' . esc_html( home_url( '/' ) ) . ( $law ? '
' . esc_html__( 'Legal notice:', 'paygrab' ) . esc_html( $law ) : '' ) . '

' . '
'; return self::send( $type, $to, self::subject( $subject ), $plain, null, $html, $values ); } /** * Sign-up verification email (M4). * * @param string $email Address. * @param string $name Name. * @param string $url Verification link. * @param int $expires Unix time when the link stops working. * @return bool */ public static function verify_email( $email, $name, $url, $expires = 0 ) { return self::send_link( 'verify_email', $email, __( 'Confirm your email address', 'paygrab' ), $name, array( 'html' => __( 'Thank you for signing up. Press the button below to finish your registration.', 'paygrab' ), 'plain' => __( 'Thank you for signing up. Open the URL below to finish your registration.', 'paygrab' ), 'button' => __( 'Finish registration', 'paygrab' ), 'ignore_html' => __( 'If you did not sign up, please delete this email. Nobody becomes a member unless the button is pressed.', 'paygrab' ), 'ignore_plain' => __( 'If you did not sign up, please delete this email. Nobody becomes a member unless the URL is opened.', 'paygrab' ), 'period' => __( '24 hours', 'paygrab' ), ), $url, $expires ? $expires : time() + DAY_IN_SECONDS ); } /** * "Set a new password" email. * * @param WP_User $user User. * @param string $url Link. * @return bool */ public static function password_reset( WP_User $user, $url ) { return self::send_link( 'password_reset', $user->user_email, __( 'Set a new password', 'paygrab' ), $user->display_name, array( 'html' => __( 'We received a request to set a new password. Press the button below and choose a new password.', 'paygrab' ), 'plain' => __( 'We received a request to set a new password. Open the URL below and choose a new password.', 'paygrab' ), 'button' => __( 'Set a new password', 'paygrab' ), 'ignore_html' => __( 'If you did not ask for this, please delete this email. Your password stays the same.', 'paygrab' ), 'ignore_plain' => __( 'If you did not ask for this, please delete this email. Your password stays the same.', 'paygrab' ), 'period' => __( '1 hour', 'paygrab' ), ), $url, time() + HOUR_IN_SECONDS ); } /** * Confirm a new email address (sent to the new address). * * @param WP_User $user User. * @param string $email New address. * @param string $url Link. * @return bool */ public static function email_change( WP_User $user, $email, $url ) { return self::send_link( 'email_change', $email, __( 'Confirm your new email address', 'paygrab' ), $user->display_name, array( 'html' => __( 'Press the button below to change the email address of your account to this address.', 'paygrab' ), 'plain' => __( 'Open the URL below to change the email address of your account to this address.', 'paygrab' ), 'button' => __( 'Change my email address', 'paygrab' ), 'ignore_html' => __( 'If you did not ask for this, please delete this email. Nothing changes unless the button is pressed.', 'paygrab' ), 'ignore_plain' => __( 'If you did not ask for this, please delete this email. Nothing changes unless the URL is opened.', 'paygrab' ), 'period' => __( '24 hours', 'paygrab' ), ), $url, time() + DAY_IN_SECONDS ); } /** * Test mail to the seller. * * @return bool */ public static function test() { $body = __( 'This is a test email from PAYGRAB. If you can read this, emails are being delivered.', 'paygrab' ) . self::footer(); return self::send( 'test', self::admin_address(), self::subject( __( 'Test email', 'paygrab' ) ), $body ); } }