├── .editorconfig ├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json └── src └── SMTP.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.php] 15 | indent_size = 4 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Vendor (e.g. Composer) 2 | vendor 3 | 4 | # Visual Studio 5 | .vscode 6 | 7 | # macOS 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Brandon Nifong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WP SMTP 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/log1x/wp-smtp/v/stable)](https://packagist.org/packages/log1x/wp-smtp) [![Total Downloads](https://poser.pugx.org/log1x/wp-smtp/downloads)](https://packagist.org/packages/log1x/wp-smtp) 4 | 5 | WP SMTP is a simple Composer package for handling WordPress SMTP with `.env`. No admin menus or [other bloat](https://blog.sucuri.net/2019/03/0day-vulnerability-in-easy-wp-smtp-affects-thousands-of-sites.html). Just a simple admin notice to verify your connection when needed and the ability to do a simple task WordPress should probably be handling natively. 6 | 7 | ## Getting Started 8 | 9 | ### Requirements 10 | 11 | - [Sage](https://github.com/roots/sage) >= 9.0 12 | - [Bedrock](https://github.com/roots/bedrock) 13 | - [PHP](https://secure.php.net/manual/en/install.php) >= 7.1.3 14 | - [Composer](https://getcomposer.org/download/) 15 | 16 | ### Installation 17 | 18 | Install via Composer: 19 | 20 | ```sh 21 | composer require log1x/wp-smtp 22 | ``` 23 | 24 | ## Usage 25 | 26 | ### Configuration 27 | 28 | All configuration goes into `.env`. 29 | 30 | #### Required 31 | 32 | ```conf 33 | WP_SMTP_HOST=mail.example.com # Host 34 | WP_SMTP_USERNAME=example # Username 35 | WP_SMTP_PASSWORD=secure123 # Password 36 | ``` 37 | 38 | #### Optional 39 | 40 | ```conf 41 | WP_SMTP_PORT=587 # Port 42 | WP_SMTP_PROTOCOL=tls # Protocol 43 | WP_SMTP_TIMEOUT=10 # Timeout 44 | WP_SMTP_FORCEFROM=example@example.com # Force From Email 45 | WP_SMTP_FORCEFROMNAME=Example # Force From Name 46 | ``` 47 | 48 | #### Mailhog 49 | 50 | ```conf 51 | WP_SMTP_HOST=localhost 52 | WP_SMTP_PORT=1025 53 | WP_SMTP_PROTOCOL=false 54 | ``` 55 | 56 | ## Debugging 57 | 58 | > SMTP connect() failed. 59 | 60 | This error means the initial connection to your host/port failed. 61 | 62 | ## Contributing 63 | 64 | Contributing whether it be through PRs, reporting an issue, or suggesting an idea is encouraged and appreciated. When contributing code, please follow the existing code style. 65 | 66 | If you're feeling generous, I also take contributions in the form of [coffee & energy drinks](https://www.buymeacoffee.com/log1x). 67 | 68 | ## License 69 | 70 | WP SMTP is provided under the [MIT License](https://github.com/log1x/wp-smtp/blob/master/LICENSE.md). 71 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "log1x/wp-smtp", 3 | "description": "Simple package for handling WordPress SMTP with .env when using the Roots stack.", 4 | "type": "package", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Brandon Nifong", 9 | "email": "brandon@tendency.me" 10 | } 11 | ], 12 | "autoload": { 13 | "files": [ 14 | "src/SMTP.php" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/SMTP.php: -------------------------------------------------------------------------------- 1 | true, 16 | 'ssl' => [ 17 | 'verify_peer' => false, 18 | 'verify_peer_name' => false, 19 | 'allow_self_signed' => true 20 | ], 21 | 'timeout' => 10, 22 | 'host' => '', 23 | 'port' => 587, 24 | 'protocol' => 'tls', 25 | 'username' => '', 26 | 'password' => '', 27 | 'forceFrom' => '', 28 | 'forceFromName' => '', 29 | ]; 30 | 31 | /** 32 | * Constructor 33 | */ 34 | public function __construct() 35 | { 36 | $this->admin(); 37 | $this->mail(); 38 | } 39 | 40 | /** 41 | * Config 42 | * 43 | * @return object 44 | */ 45 | protected function config() 46 | { 47 | $config = collect([ 48 | 'host' => env('WP_SMTP_HOST'), 49 | 'port' => env('WP_SMTP_PORT'), 50 | 'protocol' => env('WP_SMTP_PROTOCOL'), 51 | 'username' => env('WP_SMTP_USERNAME'), 52 | 'password' => env('WP_SMTP_PASSWORD'), 53 | 'timeout' => env('WP_SMTP_TIMEOUT'), 54 | 'forceFrom' => env('WP_SMTP_FORCEFROM'), 55 | 'forceFromName' => env('WP_SMTP_FORCEFROMNAME') 56 | ])->filter(); 57 | 58 | return (object) collect($this->config) 59 | ->merge($config) 60 | ->all(); 61 | } 62 | 63 | /** 64 | * Validate Hash 65 | * 66 | * @return boolean 67 | */ 68 | protected function validate() 69 | { 70 | return $this->hash() === get_option('wp_mail_verify'); 71 | } 72 | 73 | /** 74 | * Hash 75 | * 76 | * @return string 77 | */ 78 | protected function hash() 79 | { 80 | return hash('crc32', serialize($this->config())); 81 | } 82 | 83 | /** 84 | * Notice 85 | * 86 | * @param string $message 87 | * @param string $type 88 | * @param boolean $dismissible 89 | * @return void 90 | */ 91 | protected function notice($message, $type = 'info', $dismissible = false) 92 | { 93 | add_action('admin_notices', function () use ($message, $type, $dismissible) { 94 | printf( 95 | '

%2$s

', 96 | esc_attr("notice notice-{$type}" . ($dismissible ? ' is-dismissible' : '')), 97 | __($message, 'wp-smtp') 98 | ); 99 | }); 100 | } 101 | 102 | /** 103 | * Admin 104 | * 105 | * @return void 106 | */ 107 | protected function admin() 108 | { 109 | if (! empty($_GET['verify-smtp']) && $_GET['verify-smtp'] === 'true' && wp_verify_nonce($_GET['_wpnonce'], 'verify-smtp')) { 110 | $this->verify(); 111 | } 112 | 113 | if (! $this->config()->username || ! $this->config()->password || ! $this->config()->host) { 114 | return $this->notice( 115 | 'WP SMTP failed to find your SMTP credentials. Please define them in .env and click here to test your configuration.', 116 | 'error' 117 | ); 118 | } 119 | 120 | if (! get_option('wp_mail_verify')) { 121 | return $this->notice( 122 | 'WP SMTP credentials found. Please click here to test your configuration.' 123 | ); 124 | } 125 | 126 | if (! $this->validate()) { 127 | return $this->notice( 128 | 'WP SMTP has detected a change in your credentials. Please click here to test your configuration.' 129 | ); 130 | } 131 | } 132 | 133 | /** 134 | * Mail 135 | * 136 | * @return void 137 | */ 138 | protected function mail() 139 | { 140 | if ($this->config()->username && $this->config()->password && $this->config()->host) { 141 | add_action('phpmailer_init', function ($mail) { 142 | $mail->isSMTP(); 143 | $mail->SMTPAuth = $this->config()->auth; 144 | $mail->SMTPSecure = $this->config()->protocol; 145 | $mail->SMTPOptions = ['ssl' => $this->config()->ssl]; 146 | $mail->Timeout = $this->config()->timeout; 147 | 148 | $mail->Host = $this->config()->host; 149 | $mail->Port = $this->config()->port; 150 | $mail->Username = $this->config()->username; 151 | $mail->Password = $this->config()->password; 152 | 153 | $mail->setFrom($mail->From, $mail->FromName); 154 | 155 | if ($this->config()->forceFrom && $this->config()->forceFromName) { 156 | $mail->setFrom($this->config()->forceFrom, $this->config()->forceFromName); 157 | } 158 | }, PHP_INT_MAX); 159 | } 160 | } 161 | 162 | /** 163 | * Verify SMTP Credentials 164 | * 165 | * @return void 166 | */ 167 | protected function verify() 168 | { 169 | require_once(ABSPATH . WPINC . '/PHPMailer/Exception.php'); 170 | require_once(ABSPATH . WPINC . '/PHPMailer/PHPMailer.php'); 171 | require_once(ABSPATH . WPINC . '/PHPMailer/SMTP.php'); 172 | 173 | $mail = new \PHPMailer\PHPMailer\PHPMailer(true); 174 | 175 | try { 176 | $mail->isSMTP(); 177 | $mail->SMTPAuth = $this->config()->auth; 178 | $mail->SMTPOptions = ['ssl' => $this->config()->ssl]; 179 | $mail->SMTPSecure = $this->config()->protocol; 180 | $mail->SMTPAutoTLS = false; 181 | 182 | $mail->Host = $this->config()->host; 183 | $mail->Username = $this->config()->username; 184 | $mail->Password = $this->config()->password; 185 | $mail->Port = $this->config()->port; 186 | $mail->Timeout = $this->config()->timeout; 187 | 188 | $mail->setFrom('no-reply@'.preg_replace('/https?:\/\/(www\.)?/', '', get_bloginfo('url')), get_bloginfo('name')); 189 | $mail->AddAddress(wp_get_current_user()->user_email); 190 | 191 | if ($this->config()->forceFrom && $this->config()->forceFromName) { 192 | $mail->setFrom($this->config()->forceFrom, $this->config()->forceFromName); 193 | } 194 | 195 | $mail->CharSet = get_bloginfo('charset'); 196 | $mail->Subject = 'WP SMTP Validation'; 197 | $mail->Body = 'Success.'; 198 | 199 | $mail->Send(); 200 | $mail->ClearAddresses(); 201 | $mail->ClearAllRecipients(); 202 | } catch (\PHPMailer\PHPMailer\Exception $error) { 203 | return $this->notice( 204 | $error->errorMessage(), 205 | 'error', 206 | true 207 | ); 208 | } catch (Exception $error) { 209 | return $this->notice( 210 | $error->getMessage(), 211 | 'error', 212 | true 213 | ); 214 | } 215 | 216 | if (update_option('wp_mail_verify', $this->hash())) { 217 | return $this->notice( 218 | 'WP SMTP connection successful!', 219 | 'success', 220 | true 221 | ); 222 | } 223 | } 224 | } 225 | 226 | if (function_exists('add_action')) { 227 | return new SMTP; 228 | } 229 | --------------------------------------------------------------------------------