From e0a58638f14d57effc0397a459c239c11a302568 Mon Sep 17 00:00:00 2001 From: Oleksandr Mykhailenko Date: Sat, 17 Aug 2024 11:18:19 +0300 Subject: [PATCH] Email fallback. In Progress (#189) * Email fallback. In Progress * Prepare version 2.1.1 --- CHANGELOG.md | 3 + includes/wp-mail-api.php | 281 ++++++++++++++++++++++++++++++++++---- includes/wp-mail-smtp.php | 4 +- mailgun.php | 2 +- readme.md | 5 +- readme.txt | 4 +- 6 files changed, 271 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db3d6b6..a8fcf3e 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ Changelog ========= +2.1.1 (2024-08-17) +- Added fallback to regular mail in case or error during sending email vua API + 2.1.0 (2024-07-27) - Added ability to suppress Track Clicks when we send Reset Password email (it was an issue with domain url in the email) - Added field to setup Reply-to(header) email for the emails. diff --git a/includes/wp-mail-api.php b/includes/wp-mail-api.php index c9011c7..cd280c1 100644 --- a/includes/wp-mail-api.php +++ b/includes/wp-mail-api.php @@ -449,39 +449,272 @@ function wp_mail($to, $subject, $message, $headers = '', $attachments = []) ]; $endpoint = mg_api_get_region($region); - $endpoint = ($endpoint) ? $endpoint : 'https://api.mailgun.net/v3/'; + $endpoint = ($endpoint) ?: 'https://api.mailgun.net/v3/'; $url = $endpoint . "{$domain}/messages"; - // TODO: Mailgun only supports 1000 recipients per request, since we are - // overriding this function, let's add looping here to handle that - $response = wp_remote_post($url, $data); - if (is_wp_error($response)) { - // Store WP error in last error. - mg_api_last_error($response->get_error_message()); + $isFallbackNeeded = false; + try { + $response = wp_remote_post($url, $data); + if (is_wp_error($response)) { + // Store WP error in last error. + mg_api_last_error($response->get_error_message()); - return false; - } + $isFallbackNeeded = true; + } - $response_code = wp_remote_retrieve_response_code($response); - $response_body = json_decode(wp_remote_retrieve_body($response)); + $response_code = wp_remote_retrieve_response_code($response); + $response_body = json_decode(wp_remote_retrieve_body($response)); - // Mailgun API should *always* return a `message` field, even when - // $response_code != 200, so a lack of `message` indicates something - // is broken. - if ((int)$response_code != 200 || !isset($response_body->message)) { - // Store response code and HTTP response message in last error. - $response_message = wp_remote_retrieve_response_message($response); - $errmsg = "$response_code - $response_message"; - mg_api_last_error($errmsg); + if ((int)$response_code !== 200 || !isset($response_body->message)) { + // Store response code and HTTP response message in last error. + $response_message = wp_remote_retrieve_response_message($response); + $errmsg = "$response_code - $response_message"; + mg_api_last_error($errmsg); - return false; + $isFallbackNeeded = true; + } + if ($response_body->message !== 'Queued. Thank you.') { + mg_api_last_error($response_body->message); + + $isFallbackNeeded = true; + } + } catch (Throwable $throwable) { + $isFallbackNeeded = true; } - // Not sure there is any additional checking that needs to be done here, but why not? - if ($response_body->message !== 'Queued. Thank you.') { - mg_api_last_error($response_body->message); + //Email Fallback - return false; + if ($isFallbackNeeded) { + global $phpmailer; + + // (Re)create it, if it's gone missing. + if (!($phpmailer instanceof PHPMailer\PHPMailer\PHPMailer)) { + require_once ABSPATH . WPINC . '/PHPMailer/PHPMailer.php'; + require_once ABSPATH . WPINC . '/PHPMailer/SMTP.php'; + require_once ABSPATH . WPINC . '/PHPMailer/Exception.php'; + $phpmailer = new PHPMailer\PHPMailer\PHPMailer(true); + + $phpmailer::$validator = static function ($email) { + return (bool)is_email($email); + }; + } + + // Empty out the values that may be set. + $phpmailer->clearAllRecipients(); + $phpmailer->clearAttachments(); + $phpmailer->clearCustomHeaders(); + $phpmailer->clearReplyTos(); + $phpmailer->Body = ''; + $phpmailer->AltBody = ''; + + // Set "From" name and email. + + // If we don't have a name from the input headers. + if (!isset($from_name)) { + $from_name = 'WordPress'; + } + + /* + * If we don't have an email from the input headers, default to wordpress@$sitename + * Some hosts will block outgoing mail from this address if it doesn't exist, + * but there's no easy alternative. Defaulting to admin_email might appear to be + * another option, but some hosts may refuse to relay mail from an unknown domain. + * See https://core.trac.wordpress.org/ticket/5007. + */ + if (!isset($from_email)) { + // Get the site domain and get rid of www. + $sitename = wp_parse_url(network_home_url(), PHP_URL_HOST); + $from_email = 'wordpress@'; + + if (null !== $sitename) { + if (str_starts_with($sitename, 'www.')) { + $sitename = substr($sitename, 4); + } + + $from_email .= $sitename; + } + } + + /** + * Filters the email address to send from. + * @param string $from_email Email address to send from. + * @since 2.2.0 + */ + $from_email = apply_filters('wp_mail_from', $from_email); + + /** + * Filters the name to associate with the "from" email address. + * @param string $from_name Name associated with the "from" email address. + * @since 2.3.0 + */ + $from_name = apply_filters('wp_mail_from_name', $from_name); + + try { + $phpmailer->setFrom($from_email, $from_name, false); + } catch (PHPMailer\PHPMailer\Exception $e) { + $mail_error_data = compact('to', 'subject', 'message', 'headers', 'attachments'); + $mail_error_data['phpmailer_exception_code'] = $e->getCode(); + + /** This filter is documented in wp-includes/pluggable.php */ + do_action('wp_mail_failed', new WP_Error('wp_mail_failed', $e->getMessage(), $mail_error_data)); + + return false; + } + + // Set mail's subject and body. + $phpmailer->Subject = $subject; + $phpmailer->Body = $message; + + // Set destination addresses, using appropriate methods for handling addresses. + $address_headers = compact('to', 'cc', 'bcc', 'replyTo'); + + foreach ($address_headers as $address_header => $addresses) { + if (empty($addresses)) { + continue; + } + + foreach ((array)$addresses as $address) { + try { + // Break $recipient into name and address parts if in the format "Foo ". + $recipient_name = ''; + + if (preg_match('/(.*)<(.+)>/', $address, $matches)) { + if (count($matches) === 3) { + $recipient_name = $matches[1]; + $address = $matches[2]; + } + } + + switch ($address_header) { + case 'to': + $phpmailer->addAddress($address, $recipient_name); + break; + case 'cc': + $phpmailer->addCc($address, $recipient_name); + break; + case 'bcc': + $phpmailer->addBcc($address, $recipient_name); + break; + case 'reply_to': + $phpmailer->addReplyTo($address, $recipient_name); + break; + } + } catch (PHPMailer\PHPMailer\Exception $e) { + continue; + } + } + } + + // Set to use PHP's mail(). + $phpmailer->isMail(); + + // Set Content-Type and charset. + + // If we don't have a Content-Type from the input headers. + if (!isset($content_type)) { + $content_type = 'text/plain'; + } + + /** + * Filters the wp_mail() content type. + * @param string $content_type Default wp_mail() content type. + * @since 2.3.0 + */ + $content_type = apply_filters('wp_mail_content_type', $content_type); + + $phpmailer->ContentType = $content_type; + + // Set whether it's plaintext, depending on $content_type. + if ('text/html' === $content_type) { + $phpmailer->isHTML(true); + } + + // If we don't have a charset from the input headers. + if (!isset($charset)) { + $charset = get_bloginfo('charset'); + } + + /** + * Filters the default wp_mail() charset. + * @param string $charset Default email charset. + * @since 2.3.0 + */ + $phpmailer->CharSet = apply_filters('wp_mail_charset', $charset); + + // Set custom headers. + if (!empty($headers)) { + foreach ((array)$headers as $name => $content) { + // Only add custom headers not added automatically by PHPMailer. + if (!in_array($name, ['MIME-Version', 'X-Mailer'], true)) { + try { + $phpmailer->addCustomHeader(sprintf('%1$s: %2$s', $name, $content)); + } catch (PHPMailer\PHPMailer\Exception $e) { + continue; + } + } + } + + if (false !== stripos($content_type, 'multipart') && !empty($boundary)) { + $phpmailer->addCustomHeader(sprintf('Content-Type: %s; boundary="%s"', $content_type, $boundary)); + } + } + + if (!empty($attachments)) { + foreach ($attachments as $filename => $attachment) { + $filename = is_string($filename) ? $filename : ''; + + try { + $phpmailer->addAttachment($attachment, $filename); + } catch (PHPMailer\PHPMailer\Exception $e) { + continue; + } + } + } + + /** + * Fires after PHPMailer is initialized. + * @param PHPMailer $phpmailer The PHPMailer instance (passed by reference). + * @since 2.2.0 + */ + do_action_ref_array('phpmailer_init', [&$phpmailer]); + + $mail_data = compact('to', 'subject', 'message', 'headers', 'attachments'); + + // Send! + try { + $send = $phpmailer->send(); + + /** + * Fires after PHPMailer has successfully sent an email. + * The firing of this action does not necessarily mean that the recipient(s) received the + * email successfully. It only means that the `send` method above was able to + * process the request without any errors. + * @param array $mail_data { + * An array containing the email recipient(s), subject, message, headers, and attachments. + * @type string[] $to Email addresses to send message. + * @type string $subject Email subject. + * @type string $message Message contents. + * @type string[] $headers Additional headers. + * @type string[] $attachments Paths to files to attach. + * } + * @since 5.9.0 + */ + do_action('wp_mail_succeeded', $mail_data); + + return $send; + } catch (PHPMailer\PHPMailer\Exception $e) { + $mail_data['phpmailer_exception_code'] = $e->getCode(); + + /** + * Fires after a PHPMailer\PHPMailer\Exception is caught. + * @param WP_Error $error A WP_Error object with the PHPMailer\PHPMailer\Exception message, and an array + * containing the mail recipient, subject, message, headers, and attachments. + * @since 4.4.0 + */ + do_action('wp_mail_failed', new WP_Error('wp_mail_failed', $e->getMessage(), $mail_data)); + + return false; + } } return true; diff --git a/includes/wp-mail-smtp.php b/includes/wp-mail-smtp.php index c1a9514..83e5cba 100644 --- a/includes/wp-mail-smtp.php +++ b/includes/wp-mail-smtp.php @@ -80,7 +80,9 @@ function wp_mail_failed($error) if (is_wp_error($error)) { mg_smtp_last_error($error->get_error_message()); } else { - mg_smtp_last_error($error->__toString()); + if (method_exists($error, '__toString')) { + mg_smtp_last_error($error->__toString()); + } } } diff --git a/mailgun.php b/mailgun.php index d719f6e..ab59d5a 100755 --- a/mailgun.php +++ b/mailgun.php @@ -3,7 +3,7 @@ * Plugin Name: Mailgun * Plugin URI: http://wordpress.org/extend/plugins/mailgun/ * Description: Mailgun integration for WordPress - * Version: 2.1.0 + * Version: 2.1.1 * Requires PHP: 7.4 * Requires at least: 4.4 * Author: Mailgun diff --git a/readme.md b/readme.md index e41ec25..5f7856c 100755 --- a/readme.md +++ b/readme.md @@ -4,7 +4,7 @@ Mailgun for WordPress Contributors: mailgun, sivel, lookahead.io, m35dev, alanfuller Tags: mailgun, smtp, http, api, mail, email Tested up to: 6.6.1 -Stable tag: 2.1.0 +Stable tag: 2.1.1 License: GPLv2 or later Easily send email from your WordPress site through Mailgun using the HTTP API or SMTP. @@ -132,6 +132,9 @@ MAILGUN_REPLY_TO_ADDRESS Type: string == Changelog == += 2.1.1 (2024-08-17): = +- Added fallback to regular mail in case or error during sending email vua API + = 2.1.0 (2024-07-27): = - Added ability to suppress Track Clicks when we send Reset Password email (it was an issue with domain url in the email) - Added field to setup Reply-to(header) email for the emails. diff --git a/readme.txt b/readme.txt index a238232..cadcbc6 100755 --- a/readme.txt +++ b/readme.txt @@ -126,9 +126,11 @@ MAILGUN_TRACK_OPENS Type: string Choices: 'yes' or 'no' 5. Using a Subscription Code 6. Subscription Form Seen By Site Visitors - == Changelog == += 2.1.1 (2024-08-17): = +- Added fallback to regular mail in case or error during sending email vua API + = 2.1.0 (2024-07-27): = - Added ability to suppress Track Clicks when we send Reset Password email (it was an issue with domain url in the email) - Added field to setup Reply-to(header) email for the emails.