├── layout ├── footer.php └── header.php ├── .github └── FUNDING.yml ├── style └── main.css ├── logout.php ├── db.sql ├── README.md ├── activate.php ├── memberpage.php ├── LICENSE ├── includes └── config.php ├── classes ├── phpmailer │ ├── mail.php │ ├── Exception.php │ ├── OAuth.php │ ├── POP3.php │ └── SMTP.php └── user.php ├── login.php ├── reset.php ├── resetPassword.php └── index.php /layout/footer.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [dcblogdev] 4 | -------------------------------------------------------------------------------- /style/main.css: -------------------------------------------------------------------------------- 1 | .bg-danger, .bg-success { 2 | padding: 0 5px; 3 | } 4 | 5 | a { 6 | color: #EF1F2F; 7 | text-decoration: none; 8 | } -------------------------------------------------------------------------------- /logout.php: -------------------------------------------------------------------------------- 1 | logout(); 5 | 6 | //logged in return to index page 7 | header('Location: index.php'); 8 | exit; 9 | ?> -------------------------------------------------------------------------------- /db.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `members` ( 2 | `memberID` int(11) NOT NULL AUTO_INCREMENT, 3 | `username` varchar(255) NOT NULL, 4 | `password` varchar(255) NOT NULL, 5 | `email` varchar(255) NOT NULL, 6 | `active` varchar(255) NOT NULL, 7 | `resetToken` varchar(255) DEFAULT NULL, 8 | `resetComplete` varchar(3) DEFAULT 'No', 9 | PRIMARY KEY (`memberID`) 10 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; 11 | -------------------------------------------------------------------------------- /layout/header.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <?=isset($title) ? $title : null;?> 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Login Register 2 | ============= 3 | 4 | These files acompany the tutorial: [Login and Registration system with PHP](https://dcblog.dev/login-and-registration-system-with-php) 5 | 6 | ## send emails locally 7 | 8 | For anyone who is struggling with emails locally I highly recommend using https://mailtrap.io this is a great service to catch emails when working locally. 9 | 10 | To set it up open classes/phpmailer/mail.php and enter the SMTP details: 11 | 12 | Ensure you enter the username and password provided from mailtrap. 13 | 14 | 15 | ```php 16 | public $Host = 'smtp.mailtrap.io'; 17 | public $Mailer = 'smtp'; 18 | public $SMTPAuth = true; 19 | public $Username = ''; 20 | public $Password = ''; 21 | //public $SMTPSecure = 'tls'; 22 | ``` 23 | -------------------------------------------------------------------------------- /activate.php: -------------------------------------------------------------------------------- 1 | prepare("UPDATE members SET active = 'Yes' WHERE memberID = :memberID AND active = :active"); 13 | $stmt->execute(array( 14 | ':memberID' => $memberID, 15 | ':active' => $active 16 | )); 17 | 18 | //if the row was updated redirect the user 19 | if ($stmt->rowCount() == 1){ 20 | //redirect to login page 21 | header('Location: login.php?action=active'); 22 | exit; 23 | 24 | } else { 25 | echo "Your account could not be activated."; 26 | } 27 | 28 | } 29 | ?> -------------------------------------------------------------------------------- /memberpage.php: -------------------------------------------------------------------------------- 1 | is_logged_in()){ 5 | header('Location: login.php'); 6 | exit(); 7 | } 8 | 9 | //define page title 10 | $title = 'Member Page'; 11 | 12 | //include header template 13 | require('layout/header.php'); 14 | ?> 15 | 16 |
17 |

18 |

Logout

19 | 20 | 21 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Nobis veritatis nemo ad recusandae labore nihil iure qui eum consequatur, officiis facere quis sunt tempora impedit ullam reprehenderit facilis ex amet! 22 | 23 | 24 |
25 | 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 David Carr 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /includes/config.php: -------------------------------------------------------------------------------- 1 | setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);//Suggested to uncomment on production websites 23 | $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);//Suggested to comment on production websites 24 | $db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); 25 | 26 | } catch(PDOException $e) { 27 | //show error 28 | echo '

'.$e->getMessage().'

'; 29 | exit; 30 | } 31 | 32 | //include the user class, pass in the database connection 33 | include('classes/user.php'); 34 | include('classes/phpmailer/mail.php'); 35 | $user = new User($db); 36 | ?> 37 | -------------------------------------------------------------------------------- /classes/phpmailer/mail.php: -------------------------------------------------------------------------------- 1 | Subject = $subject; 25 | } 26 | 27 | public function body($body) 28 | { 29 | $this->Body = $body; 30 | } 31 | 32 | public function send() 33 | { 34 | $this->AltBody = strip_tags(stripslashes($this->Body))."\n\n"; 35 | $this->AltBody = str_replace(" ", "\n\n", $this->AltBody); 36 | return parent::send(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /classes/phpmailer/Exception.php: -------------------------------------------------------------------------------- 1 | 10 | * @author Jim Jagielski (jimjag) 11 | * @author Andy Prevost (codeworxtech) 12 | * @author Brent R. Matzelle (original founder) 13 | * @copyright 2012 - 2020 Marcus Bointon 14 | * @copyright 2010 - 2012 Jim Jagielski 15 | * @copyright 2004 - 2009 Andy Prevost 16 | * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License 17 | * @note This program is distributed in the hope that it will be useful - WITHOUT 18 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | * FITNESS FOR A PARTICULAR PURPOSE. 20 | */ 21 | 22 | namespace PHPMailer\PHPMailer; 23 | 24 | /** 25 | * PHPMailer exception handler. 26 | * 27 | * @author Marcus Bointon 28 | */ 29 | class Exception extends \Exception 30 | { 31 | /** 32 | * Prettify error message output. 33 | * 34 | * @return string 35 | */ 36 | public function errorMessage() 37 | { 38 | return '' . htmlspecialchars($this->getMessage()) . "
\n"; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /classes/user.php: -------------------------------------------------------------------------------- 1 | _db = $db; 11 | $this->_ignoreCase = false; 12 | } 13 | 14 | public function setIgnoreCase($sensitive) { 15 | $this->_ignoreCase = $sensitive; 16 | } 17 | 18 | public function getIgnoreCase() { 19 | return $this->_ignoreCase; 20 | } 21 | 22 | private function get_user_hash($username) 23 | { 24 | try { 25 | if ($this->_ignoreCase) { 26 | $stmt = $this->_db->prepare('SELECT password, username, memberID FROM members WHERE LOWER(username) = LOWER(:username) AND active="Yes" '); 27 | } else { 28 | $stmt = $this->_db->prepare('SELECT password, username, memberID FROM members WHERE username = :username AND active="Yes" '); 29 | } 30 | $stmt->execute(array('username' => $username)); 31 | 32 | return $stmt->fetch(); 33 | 34 | } catch(PDOException $e) { 35 | echo '

'.$e->getMessage().'

'; 36 | } 37 | } 38 | 39 | public function isValidUsername($username) 40 | { 41 | if (strlen($username) < 3) { 42 | return false; 43 | } 44 | 45 | if (strlen($username) > 17) { 46 | return false; 47 | } 48 | 49 | if (! ctype_alnum($username)) { 50 | return false; 51 | } 52 | 53 | return true; 54 | } 55 | 56 | public function login($username, $password) 57 | { 58 | if (! $this->isValidUsername($username)) { 59 | return false; 60 | } 61 | 62 | if (strlen($password) < 3) { 63 | return false; 64 | } 65 | 66 | $row = $this->get_user_hash($username); 67 | 68 | if (password_verify($password, $row['password'])) { 69 | 70 | $_SESSION['loggedin'] = true; 71 | $_SESSION['username'] = $row['username']; 72 | $_SESSION['memberID'] = $row['memberID']; 73 | 74 | return true; 75 | } 76 | return false; 77 | } 78 | 79 | public function logout() 80 | { 81 | session_destroy(); 82 | } 83 | 84 | public function is_logged_in() 85 | { 86 | if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] == true){ 87 | return true; 88 | } 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /login.php: -------------------------------------------------------------------------------- 1 | is_logged_in() ){ header('Location: index.php'); exit(); } 7 | 8 | //process login form if submitted 9 | if(isset($_POST['submit'])){ 10 | 11 | if (! isset($_POST['username'])) { 12 | $error[] = "Please fill out all fields"; 13 | } 14 | 15 | if (! isset($_POST['password'])) { 16 | $error[] = "Please fill out all fields"; 17 | } 18 | 19 | $username = $_POST['username']; 20 | if ($user->isValidUsername($username)){ 21 | if (! isset($_POST['password'])){ 22 | $error[] = 'A password must be entered'; 23 | } 24 | 25 | $password = $_POST['password']; 26 | 27 | if ($user->login($username, $password)){ 28 | $_SESSION['username'] = $username; 29 | header('Location: memberpage.php'); 30 | exit; 31 | 32 | } else { 33 | $error[] = 'Wrong username or password or your account has not been activated.'; 34 | } 35 | }else{ 36 | $error[] = 'Usernames are required to be Alphanumeric, and between 3-16 characters long'; 37 | } 38 | 39 | }//end if submit 40 | 41 | //define page title 42 | $title = 'Login'; 43 | 44 | //include header template 45 | require('layout/header.php'); 46 | ?> 47 | 48 | 49 |
50 | 51 |
52 | 53 |
54 |
55 |

Please Login

56 |

Back to home page

57 |
58 | 59 | '.$error.'

'; 64 | } 65 | } 66 | 67 | if (isset($_GET['action'])){ 68 | 69 | //check the action 70 | switch ($_GET['action']) { 71 | case 'active': 72 | echo "

Your account is now active you may now log in.

"; 73 | break; 74 | case 'reset': 75 | echo "

Please check your inbox for a reset link.

"; 76 | break; 77 | case 'resetAccount': 78 | echo "

Password changed, you may now login.

"; 79 | break; 80 | } 81 | 82 | } 83 | 84 | 85 | ?> 86 | 87 |
88 | 89 |
90 | 91 |
92 | 93 |
94 | 95 |
96 | 99 |
100 | 101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | 109 | 110 | 111 |
112 | 113 | 114 | 118 | -------------------------------------------------------------------------------- /reset.php: -------------------------------------------------------------------------------- 1 | is_logged_in()){ 5 | header('Location: memberpage.php'); 6 | exit(); 7 | } 8 | 9 | //if form has been submitted process it 10 | if (isset($_POST['submit'])){ 11 | 12 | //Make sure all POSTS are declared 13 | if (! isset($_POST['email'])) { 14 | $error[] = "Please fill out all fields"; 15 | } 16 | 17 | 18 | //email validation 19 | if (! filter_var($_POST['email'], FILTER_VALIDATE_EMAIL)){ 20 | $error[] = 'Please enter a valid email address'; 21 | } else { 22 | $stmt = $db->prepare('SELECT email FROM members WHERE email = :email'); 23 | $stmt->execute(array(':email' => $_POST['email'])); 24 | $row = $stmt->fetch(PDO::FETCH_ASSOC); 25 | 26 | if (empty($row['email'])){ 27 | $error[] = 'Email provided is not recognised.'; 28 | } 29 | 30 | } 31 | 32 | //if no errors have been created carry on 33 | if (! isset($error)){ 34 | 35 | //create the activation code 36 | $token = md5(uniqid(rand(),true)); 37 | 38 | try { 39 | 40 | $stmt = $db->prepare("UPDATE members SET resetToken = :token, resetComplete='No' WHERE email = :email"); 41 | $stmt->execute(array( 42 | ':email' => $row['email'], 43 | ':token' => $token 44 | )); 45 | 46 | //send email 47 | $to = $row['email']; 48 | $subject = "Password Reset"; 49 | $body = "

Someone requested that the password be reset.

50 |

If this was a mistake, just ignore this email and nothing will happen.

51 |

To reset your password, visit the following address: ".DIR."resetPassword.php?key=$token

"; 52 | 53 | $mail = new Mail(); 54 | $mail->setFrom(SITEEMAIL); 55 | $mail->addAddress($to); 56 | $mail->subject($subject); 57 | $mail->body($body); 58 | $mail->send(); 59 | 60 | //redirect to index page 61 | header('Location: login.php?action=reset'); 62 | exit; 63 | 64 | //else catch the exception and show the error. 65 | } catch(PDOException $e) { 66 | $error[] = $e->getMessage(); 67 | } 68 | } 69 | } 70 | 71 | //define page title 72 | $title = 'Reset Account'; 73 | 74 | //include header template 75 | require('layout/header.php'); 76 | ?> 77 | 78 |
79 | 80 |
81 | 82 |
83 |
84 |

Reset Password

85 |

Back to login page

86 |
87 | 88 | '.$error.'

'; 93 | } 94 | } 95 | 96 | if (isset($_GET['action'])){ 97 | 98 | //check the action 99 | switch ($_GET['action']) { 100 | case 'active': 101 | echo "

Your account is now active you may now log in.

"; 102 | break; 103 | case 'reset': 104 | echo "

Please check your inbox for a reset link.

"; 105 | break; 106 | } 107 | } 108 | ?> 109 | 110 |
111 | 112 |
113 | 114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 | 122 | 123 |
124 | 125 | 129 | -------------------------------------------------------------------------------- /resetPassword.php: -------------------------------------------------------------------------------- 1 | is_logged_in() ){ 5 | header('Location: memberpage.php'); 6 | exit(); 7 | } 8 | 9 | $resetToken = $_GET['key']; 10 | 11 | $stmt = $db->prepare('SELECT resetToken, resetComplete FROM members WHERE resetToken = :token'); 12 | $stmt->execute(array(':token' => $resetToken)); 13 | $row = $stmt->fetch(PDO::FETCH_ASSOC); 14 | 15 | //if no token from db then kill the page 16 | if (empty($row['resetToken'])){ 17 | $stop = 'Invalid token provided, please use the link provided in the reset email.'; 18 | } elseif($row['resetComplete'] == 'Yes') { 19 | $stop = 'Your password has already been changed!'; 20 | } 21 | 22 | //if form has been submitted process it 23 | if (isset($_POST['submit'])){ 24 | 25 | if (! isset($_POST['password']) || ! isset($_POST['passwordConfirm'])) { 26 | $error[] = 'Both Password fields are required to be entered'; 27 | } 28 | 29 | //basic validation 30 | if (strlen($_POST['password']) < 3){ 31 | $error[] = 'Password is too short.'; 32 | } 33 | 34 | if (strlen($_POST['passwordConfirm']) < 3){ 35 | $error[] = 'Confirm password is too short.'; 36 | } 37 | 38 | if ($_POST['password'] != $_POST['passwordConfirm']){ 39 | $error[] = 'Passwords do not match.'; 40 | } 41 | 42 | //if no errors have been created carry on 43 | if (! isset($error)){ 44 | 45 | //hash the password 46 | $hashedpassword = password_hash($_POST['password'], PASSWORD_BCRYPT); 47 | 48 | try { 49 | 50 | $stmt = $db->prepare("UPDATE members SET password = :hashedpassword, resetComplete = 'Yes' WHERE resetToken = :token"); 51 | $stmt->execute(array( 52 | ':hashedpassword' => $hashedpassword, 53 | ':token' => $row['resetToken'] 54 | )); 55 | 56 | //redirect to index page 57 | header('Location: login.php?action=resetAccount'); 58 | exit; 59 | 60 | //else catch the exception and show the error. 61 | } catch(PDOException $e) { 62 | $error[] = $e->getMessage(); 63 | } 64 | } 65 | } 66 | 67 | //define page title 68 | $title = 'Reset Account'; 69 | 70 | //include header template 71 | require('layout/header.php'); 72 | ?> 73 | 74 |
75 | 76 |
77 | 78 |
79 | 80 | 81 | $stop

"; 84 | 85 | } else { ?> 86 | 87 |
88 |

Change Password

89 |
90 | 91 | '.$error.'

'; 96 | } 97 | } 98 | 99 | if (isset($_GET['action'])) { 100 | 101 | //check the action 102 | switch ($_GET['action']) { 103 | case 'active': 104 | echo "

Your account is now active you may now log in.

"; 105 | break; 106 | case 'reset': 107 | echo "

Please check your inbox for a reset link.

"; 108 | break; 109 | } 110 | } 111 | ?> 112 | 113 |
114 |
115 |
116 | 117 |
118 |
119 |
120 |
121 | 122 |
123 |
124 |
125 | 126 |
127 |
128 |
129 |
130 |
131 | 132 | 133 |
134 |
135 | 136 | 137 |
138 | 139 | 143 | -------------------------------------------------------------------------------- /classes/phpmailer/OAuth.php: -------------------------------------------------------------------------------- 1 | 10 | * @author Jim Jagielski (jimjag) 11 | * @author Andy Prevost (codeworxtech) 12 | * @author Brent R. Matzelle (original founder) 13 | * @copyright 2012 - 2020 Marcus Bointon 14 | * @copyright 2010 - 2012 Jim Jagielski 15 | * @copyright 2004 - 2009 Andy Prevost 16 | * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License 17 | * @note This program is distributed in the hope that it will be useful - WITHOUT 18 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | * FITNESS FOR A PARTICULAR PURPOSE. 20 | */ 21 | 22 | namespace PHPMailer\PHPMailer; 23 | 24 | use League\OAuth2\Client\Grant\RefreshToken; 25 | use League\OAuth2\Client\Provider\AbstractProvider; 26 | use League\OAuth2\Client\Token\AccessToken; 27 | 28 | /** 29 | * OAuth - OAuth2 authentication wrapper class. 30 | * Uses the oauth2-client package from the League of Extraordinary Packages. 31 | * 32 | * @see http://oauth2-client.thephpleague.com 33 | * 34 | * @author Marcus Bointon (Synchro/coolbru) 35 | */ 36 | class OAuth 37 | { 38 | /** 39 | * An instance of the League OAuth Client Provider. 40 | * 41 | * @var AbstractProvider 42 | */ 43 | protected $provider; 44 | 45 | /** 46 | * The current OAuth access token. 47 | * 48 | * @var AccessToken 49 | */ 50 | protected $oauthToken; 51 | 52 | /** 53 | * The user's email address, usually used as the login ID 54 | * and also the from address when sending email. 55 | * 56 | * @var string 57 | */ 58 | protected $oauthUserEmail = ''; 59 | 60 | /** 61 | * The client secret, generated in the app definition of the service you're connecting to. 62 | * 63 | * @var string 64 | */ 65 | protected $oauthClientSecret = ''; 66 | 67 | /** 68 | * The client ID, generated in the app definition of the service you're connecting to. 69 | * 70 | * @var string 71 | */ 72 | protected $oauthClientId = ''; 73 | 74 | /** 75 | * The refresh token, used to obtain new AccessTokens. 76 | * 77 | * @var string 78 | */ 79 | protected $oauthRefreshToken = ''; 80 | 81 | /** 82 | * OAuth constructor. 83 | * 84 | * @param array $options Associative array containing 85 | * `provider`, `userName`, `clientSecret`, `clientId` and `refreshToken` elements 86 | */ 87 | public function __construct($options) 88 | { 89 | $this->provider = $options['provider']; 90 | $this->oauthUserEmail = $options['userName']; 91 | $this->oauthClientSecret = $options['clientSecret']; 92 | $this->oauthClientId = $options['clientId']; 93 | $this->oauthRefreshToken = $options['refreshToken']; 94 | } 95 | 96 | /** 97 | * Get a new RefreshToken. 98 | * 99 | * @return RefreshToken 100 | */ 101 | protected function getGrant() 102 | { 103 | return new RefreshToken(); 104 | } 105 | 106 | /** 107 | * Get a new AccessToken. 108 | * 109 | * @return AccessToken 110 | */ 111 | protected function getToken() 112 | { 113 | return $this->provider->getAccessToken( 114 | $this->getGrant(), 115 | ['refresh_token' => $this->oauthRefreshToken] 116 | ); 117 | } 118 | 119 | /** 120 | * Generate a base64-encoded OAuth token. 121 | * 122 | * @return string 123 | */ 124 | public function getOauth64() 125 | { 126 | // Get a new token if it's not available or has expired 127 | if (null === $this->oauthToken || $this->oauthToken->hasExpired()) { 128 | $this->oauthToken = $this->getToken(); 129 | } 130 | 131 | return base64_encode( 132 | 'user=' . 133 | $this->oauthUserEmail . 134 | "\001auth=Bearer " . 135 | $this->oauthToken . 136 | "\001\001" 137 | ); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | is_logged_in() ){ 6 | header('Location: memberpage.php'); 7 | exit(); 8 | } 9 | 10 | //if form has been submitted process it 11 | if(isset($_POST['submit'])){ 12 | 13 | if (! isset($_POST['username'])) { 14 | $error[] = "Please fill out all fields"; 15 | } 16 | 17 | if (! isset($_POST['email'])) { 18 | $error[] = "Please fill out all fields"; 19 | } 20 | 21 | if (! isset($_POST['password'])) { 22 | $error[] = "Please fill out all fields"; 23 | } 24 | 25 | $username = $_POST['username']; 26 | 27 | //very basic validation 28 | if (! $user->isValidUsername($username)){ 29 | $error[] = 'Usernames must be at least 3 Alphanumeric characters'; 30 | } else { 31 | $stmt = $db->prepare('SELECT username FROM members WHERE username = :username'); 32 | $stmt->execute(array(':username' => $username)); 33 | $row = $stmt->fetch(PDO::FETCH_ASSOC); 34 | 35 | if (! empty($row['username'])){ 36 | $error[] = 'Username provided is already in use.'; 37 | } 38 | } 39 | 40 | if (strlen($_POST['password']) < 3){ 41 | $error[] = 'Password is too short.'; 42 | } 43 | 44 | if (strlen($_POST['passwordConfirm']) < 3){ 45 | $error[] = 'Confirm password is too short.'; 46 | } 47 | 48 | if ($_POST['password'] != $_POST['passwordConfirm']){ 49 | $error[] = 'Passwords do not match.'; 50 | } 51 | 52 | //email validation 53 | $email = htmlspecialchars_decode($_POST['email'], ENT_QUOTES); 54 | if (! filter_var($email, FILTER_VALIDATE_EMAIL)){ 55 | $error[] = 'Please enter a valid email address'; 56 | } else { 57 | $stmt = $db->prepare('SELECT email FROM members WHERE email = :email'); 58 | $stmt->execute(array(':email' => $email)); 59 | $row = $stmt->fetch(PDO::FETCH_ASSOC); 60 | 61 | if (! empty($row['email'])){ 62 | $error[] = 'Email provided is already in use.'; 63 | } 64 | } 65 | 66 | 67 | //if no errors have been created carry on 68 | if (!isset($error)){ 69 | 70 | //hash the password 71 | $hashedpassword = password_hash($_POST['password'], PASSWORD_BCRYPT); 72 | 73 | //create the activasion code 74 | $activasion = md5(uniqid(rand(),true)); 75 | 76 | try { 77 | 78 | //insert into database with a prepared statement 79 | $stmt = $db->prepare('INSERT INTO members (username,password,email,active) VALUES (:username, :password, :email, :active)'); 80 | $stmt->execute(array( 81 | ':username' => $username, 82 | ':password' => $hashedpassword, 83 | ':email' => $email, 84 | ':active' => $activasion 85 | )); 86 | $id = $db->lastInsertId('memberID'); 87 | 88 | //send email 89 | $to = $_POST['email']; 90 | $subject = "Registration Confirmation"; 91 | $body = "

Thank you for registering at demo site.

92 |

To activate your account, please click on this link: ".DIR."activate.php?x=$id&y=$activasion

93 |

Regards Site Admin

"; 94 | 95 | $mail = new Mail(); 96 | $mail->setFrom(SITEEMAIL); 97 | $mail->addAddress($to); 98 | $mail->subject($subject); 99 | $mail->body($body); 100 | $mail->send(); 101 | 102 | //redirect to index page 103 | header('Location: index.php?action=joined'); 104 | exit; 105 | 106 | //else catch the exception and show the error. 107 | } catch(PDOException $e) { 108 | $error[] = $e->getMessage(); 109 | } 110 | } 111 | } 112 | 113 | //define page title 114 | $title = 'Demo'; 115 | 116 | //include header template 117 | require('layout/header.php'); 118 | ?> 119 | 120 | 121 |
122 | 123 |
124 | 125 |
126 |
127 |

Please Sign Up

128 |

Already a member? Login

129 |
130 | 131 | '.$error.'

'; 136 | } 137 | } 138 | 139 | //if action is joined show sucess 140 | if(isset($_GET['action']) && $_GET['action'] == 'joined'){ 141 | echo "

Registration successful, please check your email to activate your account.

"; 142 | } 143 | ?> 144 | 145 |
146 | 147 |
148 |
149 | 150 |
151 |
152 |
153 |
154 | 155 |
156 |
157 |
158 |
159 | 160 |
161 |
162 |
163 | 164 |
165 |
166 |
167 |
168 |
169 |
170 | 171 |
172 | 173 | 177 | -------------------------------------------------------------------------------- /classes/phpmailer/POP3.php: -------------------------------------------------------------------------------- 1 | 10 | * @author Jim Jagielski (jimjag) 11 | * @author Andy Prevost (codeworxtech) 12 | * @author Brent R. Matzelle (original founder) 13 | * @copyright 2012 - 2020 Marcus Bointon 14 | * @copyright 2010 - 2012 Jim Jagielski 15 | * @copyright 2004 - 2009 Andy Prevost 16 | * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License 17 | * @note This program is distributed in the hope that it will be useful - WITHOUT 18 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | * FITNESS FOR A PARTICULAR PURPOSE. 20 | */ 21 | 22 | namespace PHPMailer\PHPMailer; 23 | 24 | /** 25 | * PHPMailer POP-Before-SMTP Authentication Class. 26 | * Specifically for PHPMailer to use for RFC1939 POP-before-SMTP authentication. 27 | * 1) This class does not support APOP authentication. 28 | * 2) Opening and closing lots of POP3 connections can be quite slow. If you need 29 | * to send a batch of emails then just perform the authentication once at the start, 30 | * and then loop through your mail sending script. Providing this process doesn't 31 | * take longer than the verification period lasts on your POP3 server, you should be fine. 32 | * 3) This is really ancient technology; you should only need to use it to talk to very old systems. 33 | * 4) This POP3 class is deliberately lightweight and incomplete, implementing just 34 | * enough to do authentication. 35 | * If you want a more complete class there are other POP3 classes for PHP available. 36 | * 37 | * @author Richard Davey (original author) 38 | * @author Marcus Bointon (Synchro/coolbru) 39 | * @author Jim Jagielski (jimjag) 40 | * @author Andy Prevost (codeworxtech) 41 | */ 42 | class POP3 43 | { 44 | /** 45 | * The POP3 PHPMailer Version number. 46 | * 47 | * @var string 48 | */ 49 | const VERSION = '6.2.0'; 50 | 51 | /** 52 | * Default POP3 port number. 53 | * 54 | * @var int 55 | */ 56 | const DEFAULT_PORT = 110; 57 | 58 | /** 59 | * Default timeout in seconds. 60 | * 61 | * @var int 62 | */ 63 | const DEFAULT_TIMEOUT = 30; 64 | 65 | /** 66 | * POP3 class debug output mode. 67 | * Debug output level. 68 | * Options: 69 | * @see POP3::DEBUG_OFF: No output 70 | * @see POP3::DEBUG_SERVER: Server messages, connection/server errors 71 | * @see POP3::DEBUG_CLIENT: Client and Server messages, connection/server errors 72 | * 73 | * @var int 74 | */ 75 | public $do_debug = self::DEBUG_OFF; 76 | 77 | /** 78 | * POP3 mail server hostname. 79 | * 80 | * @var string 81 | */ 82 | public $host; 83 | 84 | /** 85 | * POP3 port number. 86 | * 87 | * @var int 88 | */ 89 | public $port; 90 | 91 | /** 92 | * POP3 Timeout Value in seconds. 93 | * 94 | * @var int 95 | */ 96 | public $tval; 97 | 98 | /** 99 | * POP3 username. 100 | * 101 | * @var string 102 | */ 103 | public $username; 104 | 105 | /** 106 | * POP3 password. 107 | * 108 | * @var string 109 | */ 110 | public $password; 111 | 112 | /** 113 | * Resource handle for the POP3 connection socket. 114 | * 115 | * @var resource 116 | */ 117 | protected $pop_conn; 118 | 119 | /** 120 | * Are we connected? 121 | * 122 | * @var bool 123 | */ 124 | protected $connected = false; 125 | 126 | /** 127 | * Error container. 128 | * 129 | * @var array 130 | */ 131 | protected $errors = []; 132 | 133 | /** 134 | * Line break constant. 135 | */ 136 | const LE = "\r\n"; 137 | 138 | /** 139 | * Debug level for no output. 140 | * 141 | * @var int 142 | */ 143 | const DEBUG_OFF = 0; 144 | 145 | /** 146 | * Debug level to show server -> client messages 147 | * also shows clients connection errors or errors from server 148 | * 149 | * @var int 150 | */ 151 | const DEBUG_SERVER = 1; 152 | 153 | /** 154 | * Debug level to show client -> server and server -> client messages. 155 | * 156 | * @var int 157 | */ 158 | const DEBUG_CLIENT = 2; 159 | 160 | /** 161 | * Simple static wrapper for all-in-one POP before SMTP. 162 | * 163 | * @param string $host The hostname to connect to 164 | * @param int|bool $port The port number to connect to 165 | * @param int|bool $timeout The timeout value 166 | * @param string $username 167 | * @param string $password 168 | * @param int $debug_level 169 | * 170 | * @return bool 171 | */ 172 | public static function popBeforeSmtp( 173 | $host, 174 | $port = false, 175 | $timeout = false, 176 | $username = '', 177 | $password = '', 178 | $debug_level = 0 179 | ) { 180 | $pop = new self(); 181 | 182 | return $pop->authorise($host, $port, $timeout, $username, $password, $debug_level); 183 | } 184 | 185 | /** 186 | * Authenticate with a POP3 server. 187 | * A connect, login, disconnect sequence 188 | * appropriate for POP-before SMTP authorisation. 189 | * 190 | * @param string $host The hostname to connect to 191 | * @param int|bool $port The port number to connect to 192 | * @param int|bool $timeout The timeout value 193 | * @param string $username 194 | * @param string $password 195 | * @param int $debug_level 196 | * 197 | * @return bool 198 | */ 199 | public function authorise($host, $port = false, $timeout = false, $username = '', $password = '', $debug_level = 0) 200 | { 201 | $this->host = $host; 202 | // If no port value provided, use default 203 | if (false === $port) { 204 | $this->port = static::DEFAULT_PORT; 205 | } else { 206 | $this->port = (int) $port; 207 | } 208 | // If no timeout value provided, use default 209 | if (false === $timeout) { 210 | $this->tval = static::DEFAULT_TIMEOUT; 211 | } else { 212 | $this->tval = (int) $timeout; 213 | } 214 | $this->do_debug = $debug_level; 215 | $this->username = $username; 216 | $this->password = $password; 217 | // Reset the error log 218 | $this->errors = []; 219 | // connect 220 | $result = $this->connect($this->host, $this->port, $this->tval); 221 | if ($result) { 222 | $login_result = $this->login($this->username, $this->password); 223 | if ($login_result) { 224 | $this->disconnect(); 225 | 226 | return true; 227 | } 228 | } 229 | // We need to disconnect regardless of whether the login succeeded 230 | $this->disconnect(); 231 | 232 | return false; 233 | } 234 | 235 | /** 236 | * Connect to a POP3 server. 237 | * 238 | * @param string $host 239 | * @param int|bool $port 240 | * @param int $tval 241 | * 242 | * @return bool 243 | */ 244 | public function connect($host, $port = false, $tval = 30) 245 | { 246 | // Are we already connected? 247 | if ($this->connected) { 248 | return true; 249 | } 250 | 251 | //On Windows this will raise a PHP Warning error if the hostname doesn't exist. 252 | //Rather than suppress it with @fsockopen, capture it cleanly instead 253 | set_error_handler([$this, 'catchWarning']); 254 | 255 | if (false === $port) { 256 | $port = static::DEFAULT_PORT; 257 | } 258 | 259 | // connect to the POP3 server 260 | $errno = 0; 261 | $errstr = ''; 262 | $this->pop_conn = fsockopen( 263 | $host, // POP3 Host 264 | $port, // Port # 265 | $errno, // Error Number 266 | $errstr, // Error Message 267 | $tval 268 | ); // Timeout (seconds) 269 | // Restore the error handler 270 | restore_error_handler(); 271 | 272 | // Did we connect? 273 | if (false === $this->pop_conn) { 274 | // It would appear not... 275 | $this->setError( 276 | "Failed to connect to server $host on port $port. errno: $errno; errstr: $errstr" 277 | ); 278 | 279 | return false; 280 | } 281 | 282 | // Increase the stream time-out 283 | stream_set_timeout($this->pop_conn, $tval, 0); 284 | 285 | // Get the POP3 server response 286 | $pop3_response = $this->getResponse(); 287 | // Check for the +OK 288 | if ($this->checkResponse($pop3_response)) { 289 | // The connection is established and the POP3 server is talking 290 | $this->connected = true; 291 | 292 | return true; 293 | } 294 | 295 | return false; 296 | } 297 | 298 | /** 299 | * Log in to the POP3 server. 300 | * Does not support APOP (RFC 2828, 4949). 301 | * 302 | * @param string $username 303 | * @param string $password 304 | * 305 | * @return bool 306 | */ 307 | public function login($username = '', $password = '') 308 | { 309 | if (!$this->connected) { 310 | $this->setError('Not connected to POP3 server'); 311 | } 312 | if (empty($username)) { 313 | $username = $this->username; 314 | } 315 | if (empty($password)) { 316 | $password = $this->password; 317 | } 318 | 319 | // Send the Username 320 | $this->sendString("USER $username" . static::LE); 321 | $pop3_response = $this->getResponse(); 322 | if ($this->checkResponse($pop3_response)) { 323 | // Send the Password 324 | $this->sendString("PASS $password" . static::LE); 325 | $pop3_response = $this->getResponse(); 326 | if ($this->checkResponse($pop3_response)) { 327 | return true; 328 | } 329 | } 330 | 331 | return false; 332 | } 333 | 334 | /** 335 | * Disconnect from the POP3 server. 336 | */ 337 | public function disconnect() 338 | { 339 | $this->sendString('QUIT'); 340 | //The QUIT command may cause the daemon to exit, which will kill our connection 341 | //So ignore errors here 342 | try { 343 | @fclose($this->pop_conn); 344 | } catch (Exception $e) { 345 | //Do nothing 346 | } 347 | } 348 | 349 | /** 350 | * Get a response from the POP3 server. 351 | * 352 | * @param int $size The maximum number of bytes to retrieve 353 | * 354 | * @return string 355 | */ 356 | protected function getResponse($size = 128) 357 | { 358 | $response = fgets($this->pop_conn, $size); 359 | if ($this->do_debug >= self::DEBUG_SERVER) { 360 | echo 'Server -> Client: ', $response; 361 | } 362 | 363 | return $response; 364 | } 365 | 366 | /** 367 | * Send raw data to the POP3 server. 368 | * 369 | * @param string $string 370 | * 371 | * @return int 372 | */ 373 | protected function sendString($string) 374 | { 375 | if ($this->pop_conn) { 376 | if ($this->do_debug >= self::DEBUG_CLIENT) { //Show client messages when debug >= 2 377 | echo 'Client -> Server: ', $string; 378 | } 379 | 380 | return fwrite($this->pop_conn, $string, strlen($string)); 381 | } 382 | 383 | return 0; 384 | } 385 | 386 | /** 387 | * Checks the POP3 server response. 388 | * Looks for for +OK or -ERR. 389 | * 390 | * @param string $string 391 | * 392 | * @return bool 393 | */ 394 | protected function checkResponse($string) 395 | { 396 | if (strpos($string, '+OK') !== 0) { 397 | $this->setError("Server reported an error: $string"); 398 | 399 | return false; 400 | } 401 | 402 | return true; 403 | } 404 | 405 | /** 406 | * Add an error to the internal error store. 407 | * Also display debug output if it's enabled. 408 | * 409 | * @param string $error 410 | */ 411 | protected function setError($error) 412 | { 413 | $this->errors[] = $error; 414 | if ($this->do_debug >= self::DEBUG_SERVER) { 415 | echo '
';
416 |             foreach ($this->errors as $e) {
417 |                 print_r($e);
418 |             }
419 |             echo '
'; 420 | } 421 | } 422 | 423 | /** 424 | * Get an array of error messages, if any. 425 | * 426 | * @return array 427 | */ 428 | public function getErrors() 429 | { 430 | return $this->errors; 431 | } 432 | 433 | /** 434 | * POP3 connection error handler. 435 | * 436 | * @param int $errno 437 | * @param string $errstr 438 | * @param string $errfile 439 | * @param int $errline 440 | */ 441 | protected function catchWarning($errno, $errstr, $errfile, $errline) 442 | { 443 | $this->setError( 444 | 'Connecting to the POP3 server raised a PHP warning:' . 445 | "errno: $errno errstr: $errstr; errfile: $errfile; errline: $errline" 446 | ); 447 | } 448 | } 449 | -------------------------------------------------------------------------------- /classes/phpmailer/SMTP.php: -------------------------------------------------------------------------------- 1 | 10 | * @author Jim Jagielski (jimjag) 11 | * @author Andy Prevost (codeworxtech) 12 | * @author Brent R. Matzelle (original founder) 13 | * @copyright 2012 - 2020 Marcus Bointon 14 | * @copyright 2010 - 2012 Jim Jagielski 15 | * @copyright 2004 - 2009 Andy Prevost 16 | * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License 17 | * @note This program is distributed in the hope that it will be useful - WITHOUT 18 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | * FITNESS FOR A PARTICULAR PURPOSE. 20 | */ 21 | 22 | namespace PHPMailer\PHPMailer; 23 | 24 | /** 25 | * PHPMailer RFC821 SMTP email transport class. 26 | * Implements RFC 821 SMTP commands and provides some utility methods for sending mail to an SMTP server. 27 | * 28 | * @author Chris Ryan 29 | * @author Marcus Bointon 30 | */ 31 | class SMTP 32 | { 33 | /** 34 | * The PHPMailer SMTP version number. 35 | * 36 | * @var string 37 | */ 38 | const VERSION = '6.2.0'; 39 | 40 | /** 41 | * SMTP line break constant. 42 | * 43 | * @var string 44 | */ 45 | const LE = "\r\n"; 46 | 47 | /** 48 | * The SMTP port to use if one is not specified. 49 | * 50 | * @var int 51 | */ 52 | const DEFAULT_PORT = 25; 53 | 54 | /** 55 | * The maximum line length allowed by RFC 5321 section 4.5.3.1.6, 56 | * *excluding* a trailing CRLF break. 57 | * 58 | * @see https://tools.ietf.org/html/rfc5321#section-4.5.3.1.6 59 | * 60 | * @var int 61 | */ 62 | const MAX_LINE_LENGTH = 998; 63 | 64 | /** 65 | * The maximum line length allowed for replies in RFC 5321 section 4.5.3.1.5, 66 | * *including* a trailing CRLF line break. 67 | * 68 | * @see https://tools.ietf.org/html/rfc5321#section-4.5.3.1.5 69 | * 70 | * @var int 71 | */ 72 | const MAX_REPLY_LENGTH = 512; 73 | 74 | /** 75 | * Debug level for no output. 76 | * 77 | * @var int 78 | */ 79 | const DEBUG_OFF = 0; 80 | 81 | /** 82 | * Debug level to show client -> server messages. 83 | * 84 | * @var int 85 | */ 86 | const DEBUG_CLIENT = 1; 87 | 88 | /** 89 | * Debug level to show client -> server and server -> client messages. 90 | * 91 | * @var int 92 | */ 93 | const DEBUG_SERVER = 2; 94 | 95 | /** 96 | * Debug level to show connection status, client -> server and server -> client messages. 97 | * 98 | * @var int 99 | */ 100 | const DEBUG_CONNECTION = 3; 101 | 102 | /** 103 | * Debug level to show all messages. 104 | * 105 | * @var int 106 | */ 107 | const DEBUG_LOWLEVEL = 4; 108 | 109 | /** 110 | * Debug output level. 111 | * Options: 112 | * * self::DEBUG_OFF (`0`) No debug output, default 113 | * * self::DEBUG_CLIENT (`1`) Client commands 114 | * * self::DEBUG_SERVER (`2`) Client commands and server responses 115 | * * self::DEBUG_CONNECTION (`3`) As DEBUG_SERVER plus connection status 116 | * * self::DEBUG_LOWLEVEL (`4`) Low-level data output, all messages. 117 | * 118 | * @var int 119 | */ 120 | public $do_debug = self::DEBUG_OFF; 121 | 122 | /** 123 | * How to handle debug output. 124 | * Options: 125 | * * `echo` Output plain-text as-is, appropriate for CLI 126 | * * `html` Output escaped, line breaks converted to `
`, appropriate for browser output 127 | * * `error_log` Output to error log as configured in php.ini 128 | * Alternatively, you can provide a callable expecting two params: a message string and the debug level: 129 | * 130 | * ```php 131 | * $smtp->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";}; 132 | * ``` 133 | * 134 | * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug` 135 | * level output is used: 136 | * 137 | * ```php 138 | * $mail->Debugoutput = new myPsr3Logger; 139 | * ``` 140 | * 141 | * @var string|callable|\Psr\Log\LoggerInterface 142 | */ 143 | public $Debugoutput = 'echo'; 144 | 145 | /** 146 | * Whether to use VERP. 147 | * 148 | * @see http://en.wikipedia.org/wiki/Variable_envelope_return_path 149 | * @see http://www.postfix.org/VERP_README.html Info on VERP 150 | * 151 | * @var bool 152 | */ 153 | public $do_verp = false; 154 | 155 | /** 156 | * The timeout value for connection, in seconds. 157 | * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2. 158 | * This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure. 159 | * 160 | * @see http://tools.ietf.org/html/rfc2821#section-4.5.3.2 161 | * 162 | * @var int 163 | */ 164 | public $Timeout = 300; 165 | 166 | /** 167 | * How long to wait for commands to complete, in seconds. 168 | * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2. 169 | * 170 | * @var int 171 | */ 172 | public $Timelimit = 300; 173 | 174 | /** 175 | * Patterns to extract an SMTP transaction id from reply to a DATA command. 176 | * The first capture group in each regex will be used as the ID. 177 | * MS ESMTP returns the message ID, which may not be correct for internal tracking. 178 | * 179 | * @var string[] 180 | */ 181 | protected $smtp_transaction_id_patterns = [ 182 | 'exim' => '/[\d]{3} OK id=(.*)/', 183 | 'sendmail' => '/[\d]{3} 2.0.0 (.*) Message/', 184 | 'postfix' => '/[\d]{3} 2.0.0 Ok: queued as (.*)/', 185 | 'Microsoft_ESMTP' => '/[0-9]{3} 2.[\d].0 (.*)@(?:.*) Queued mail for delivery/', 186 | 'Amazon_SES' => '/[\d]{3} Ok (.*)/', 187 | 'SendGrid' => '/[\d]{3} Ok: queued as (.*)/', 188 | 'CampaignMonitor' => '/[\d]{3} 2.0.0 OK:([a-zA-Z\d]{48})/', 189 | ]; 190 | 191 | /** 192 | * The last transaction ID issued in response to a DATA command, 193 | * if one was detected. 194 | * 195 | * @var string|bool|null 196 | */ 197 | protected $last_smtp_transaction_id; 198 | 199 | /** 200 | * The socket for the server connection. 201 | * 202 | * @var ?resource 203 | */ 204 | protected $smtp_conn; 205 | 206 | /** 207 | * Error information, if any, for the last SMTP command. 208 | * 209 | * @var array 210 | */ 211 | protected $error = [ 212 | 'error' => '', 213 | 'detail' => '', 214 | 'smtp_code' => '', 215 | 'smtp_code_ex' => '', 216 | ]; 217 | 218 | /** 219 | * The reply the server sent to us for HELO. 220 | * If null, no HELO string has yet been received. 221 | * 222 | * @var string|null 223 | */ 224 | protected $helo_rply; 225 | 226 | /** 227 | * The set of SMTP extensions sent in reply to EHLO command. 228 | * Indexes of the array are extension names. 229 | * Value at index 'HELO' or 'EHLO' (according to command that was sent) 230 | * represents the server name. In case of HELO it is the only element of the array. 231 | * Other values can be boolean TRUE or an array containing extension options. 232 | * If null, no HELO/EHLO string has yet been received. 233 | * 234 | * @var array|null 235 | */ 236 | protected $server_caps; 237 | 238 | /** 239 | * The most recent reply received from the server. 240 | * 241 | * @var string 242 | */ 243 | protected $last_reply = ''; 244 | 245 | /** 246 | * Output debugging info via a user-selected method. 247 | * 248 | * @param string $str Debug string to output 249 | * @param int $level The debug level of this message; see DEBUG_* constants 250 | * 251 | * @see SMTP::$Debugoutput 252 | * @see SMTP::$do_debug 253 | */ 254 | protected function edebug($str, $level = 0) 255 | { 256 | if ($level > $this->do_debug) { 257 | return; 258 | } 259 | //Is this a PSR-3 logger? 260 | if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) { 261 | $this->Debugoutput->debug($str); 262 | 263 | return; 264 | } 265 | //Avoid clash with built-in function names 266 | if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) { 267 | call_user_func($this->Debugoutput, $str, $level); 268 | 269 | return; 270 | } 271 | switch ($this->Debugoutput) { 272 | case 'error_log': 273 | //Don't output, just log 274 | error_log($str); 275 | break; 276 | case 'html': 277 | //Cleans up output a bit for a better looking, HTML-safe output 278 | echo gmdate('Y-m-d H:i:s'), ' ', htmlentities( 279 | preg_replace('/[\r\n]+/', '', $str), 280 | ENT_QUOTES, 281 | 'UTF-8' 282 | ), "
\n"; 283 | break; 284 | case 'echo': 285 | default: 286 | //Normalize line breaks 287 | $str = preg_replace('/\r\n|\r/m', "\n", $str); 288 | echo gmdate('Y-m-d H:i:s'), 289 | "\t", 290 | //Trim trailing space 291 | trim( 292 | //Indent for readability, except for trailing break 293 | str_replace( 294 | "\n", 295 | "\n \t ", 296 | trim($str) 297 | ) 298 | ), 299 | "\n"; 300 | } 301 | } 302 | 303 | /** 304 | * Connect to an SMTP server. 305 | * 306 | * @param string $host SMTP server IP or host name 307 | * @param int $port The port number to connect to 308 | * @param int $timeout How long to wait for the connection to open 309 | * @param array $options An array of options for stream_context_create() 310 | * 311 | * @return bool 312 | */ 313 | public function connect($host, $port = null, $timeout = 30, $options = []) 314 | { 315 | // Clear errors to avoid confusion 316 | $this->setError(''); 317 | // Make sure we are __not__ connected 318 | if ($this->connected()) { 319 | // Already connected, generate error 320 | $this->setError('Already connected to a server'); 321 | 322 | return false; 323 | } 324 | if (empty($port)) { 325 | $port = self::DEFAULT_PORT; 326 | } 327 | // Connect to the SMTP server 328 | $this->edebug( 329 | "Connection: opening to $host:$port, timeout=$timeout, options=" . 330 | (count($options) > 0 ? var_export($options, true) : 'array()'), 331 | self::DEBUG_CONNECTION 332 | ); 333 | 334 | $this->smtp_conn = $this->getSMTPConnection($host, $port, $timeout, $options); 335 | 336 | if ($this->smtp_conn === false) { 337 | //Error info already set inside `getSMTPConnection()` 338 | return false; 339 | } 340 | 341 | $this->edebug('Connection: opened', self::DEBUG_CONNECTION); 342 | 343 | // Get any announcement 344 | $this->last_reply = $this->get_lines(); 345 | $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER); 346 | $responseCode = (int)substr($this->last_reply, 0, 3); 347 | if ($responseCode === 220) { 348 | return true; 349 | } 350 | //Anything other than a 220 response means something went wrong 351 | //RFC 5321 says the server will wait for us to send a QUIT in response to a 554 error 352 | //https://tools.ietf.org/html/rfc5321#section-3.1 353 | if ($responseCode === 554) { 354 | $this->quit(); 355 | } 356 | //This will handle 421 responses which may not wait for a QUIT (e.g. if the server is being shut down) 357 | $this->edebug('Connection: closing due to error', self::DEBUG_CONNECTION); 358 | $this->close(); 359 | return false; 360 | } 361 | 362 | /** 363 | * Create connection to the SMTP server. 364 | * 365 | * @param string $host SMTP server IP or host name 366 | * @param int $port The port number to connect to 367 | * @param int $timeout How long to wait for the connection to open 368 | * @param array $options An array of options for stream_context_create() 369 | * 370 | * @return false|resource 371 | */ 372 | protected function getSMTPConnection($host, $port = null, $timeout = 30, $options = []) 373 | { 374 | static $streamok; 375 | //This is enabled by default since 5.0.0 but some providers disable it 376 | //Check this once and cache the result 377 | if (null === $streamok) { 378 | $streamok = function_exists('stream_socket_client'); 379 | } 380 | 381 | $errno = 0; 382 | $errstr = ''; 383 | if ($streamok) { 384 | $socket_context = stream_context_create($options); 385 | set_error_handler([$this, 'errorHandler']); 386 | $connection = stream_socket_client( 387 | $host . ':' . $port, 388 | $errno, 389 | $errstr, 390 | $timeout, 391 | STREAM_CLIENT_CONNECT, 392 | $socket_context 393 | ); 394 | restore_error_handler(); 395 | } else { 396 | //Fall back to fsockopen which should work in more places, but is missing some features 397 | $this->edebug( 398 | 'Connection: stream_socket_client not available, falling back to fsockopen', 399 | self::DEBUG_CONNECTION 400 | ); 401 | set_error_handler([$this, 'errorHandler']); 402 | $connection = fsockopen( 403 | $host, 404 | $port, 405 | $errno, 406 | $errstr, 407 | $timeout 408 | ); 409 | restore_error_handler(); 410 | } 411 | 412 | // Verify we connected properly 413 | if (!is_resource($connection)) { 414 | $this->setError( 415 | 'Failed to connect to server', 416 | '', 417 | (string) $errno, 418 | $errstr 419 | ); 420 | $this->edebug( 421 | 'SMTP ERROR: ' . $this->error['error'] 422 | . ": $errstr ($errno)", 423 | self::DEBUG_CLIENT 424 | ); 425 | 426 | return false; 427 | } 428 | 429 | // SMTP server can take longer to respond, give longer timeout for first read 430 | // Windows does not have support for this timeout function 431 | if (strpos(PHP_OS, 'WIN') !== 0) { 432 | $max = (int)ini_get('max_execution_time'); 433 | // Don't bother if unlimited, or if set_time_limit is disabled 434 | if (0 !== $max && $timeout > $max && strpos(ini_get('disable_functions'), 'set_time_limit') === false) { 435 | @set_time_limit($timeout); 436 | } 437 | stream_set_timeout($connection, $timeout, 0); 438 | } 439 | 440 | return $connection; 441 | } 442 | 443 | /** 444 | * Initiate a TLS (encrypted) session. 445 | * 446 | * @return bool 447 | */ 448 | public function startTLS() 449 | { 450 | if (!$this->sendCommand('STARTTLS', 'STARTTLS', 220)) { 451 | return false; 452 | } 453 | 454 | //Allow the best TLS version(s) we can 455 | $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT; 456 | 457 | //PHP 5.6.7 dropped inclusion of TLS 1.1 and 1.2 in STREAM_CRYPTO_METHOD_TLS_CLIENT 458 | //so add them back in manually if we can 459 | if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) { 460 | $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; 461 | $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT; 462 | } 463 | 464 | // Begin encrypted connection 465 | set_error_handler([$this, 'errorHandler']); 466 | $crypto_ok = stream_socket_enable_crypto( 467 | $this->smtp_conn, 468 | true, 469 | $crypto_method 470 | ); 471 | restore_error_handler(); 472 | 473 | return (bool) $crypto_ok; 474 | } 475 | 476 | /** 477 | * Perform SMTP authentication. 478 | * Must be run after hello(). 479 | * 480 | * @see hello() 481 | * 482 | * @param string $username The user name 483 | * @param string $password The password 484 | * @param string $authtype The auth type (CRAM-MD5, PLAIN, LOGIN, XOAUTH2) 485 | * @param OAuth $OAuth An optional OAuth instance for XOAUTH2 authentication 486 | * 487 | * @return bool True if successfully authenticated 488 | */ 489 | public function authenticate( 490 | $username, 491 | $password, 492 | $authtype = null, 493 | $OAuth = null 494 | ) { 495 | if (!$this->server_caps) { 496 | $this->setError('Authentication is not allowed before HELO/EHLO'); 497 | 498 | return false; 499 | } 500 | 501 | if (array_key_exists('EHLO', $this->server_caps)) { 502 | // SMTP extensions are available; try to find a proper authentication method 503 | if (!array_key_exists('AUTH', $this->server_caps)) { 504 | $this->setError('Authentication is not allowed at this stage'); 505 | // 'at this stage' means that auth may be allowed after the stage changes 506 | // e.g. after STARTTLS 507 | 508 | return false; 509 | } 510 | 511 | $this->edebug('Auth method requested: ' . ($authtype ?: 'UNSPECIFIED'), self::DEBUG_LOWLEVEL); 512 | $this->edebug( 513 | 'Auth methods available on the server: ' . implode(',', $this->server_caps['AUTH']), 514 | self::DEBUG_LOWLEVEL 515 | ); 516 | 517 | //If we have requested a specific auth type, check the server supports it before trying others 518 | if (null !== $authtype && !in_array($authtype, $this->server_caps['AUTH'], true)) { 519 | $this->edebug('Requested auth method not available: ' . $authtype, self::DEBUG_LOWLEVEL); 520 | $authtype = null; 521 | } 522 | 523 | if (empty($authtype)) { 524 | //If no auth mechanism is specified, attempt to use these, in this order 525 | //Try CRAM-MD5 first as it's more secure than the others 526 | foreach (['CRAM-MD5', 'LOGIN', 'PLAIN', 'XOAUTH2'] as $method) { 527 | if (in_array($method, $this->server_caps['AUTH'], true)) { 528 | $authtype = $method; 529 | break; 530 | } 531 | } 532 | if (empty($authtype)) { 533 | $this->setError('No supported authentication methods found'); 534 | 535 | return false; 536 | } 537 | $this->edebug('Auth method selected: ' . $authtype, self::DEBUG_LOWLEVEL); 538 | } 539 | 540 | if (!in_array($authtype, $this->server_caps['AUTH'], true)) { 541 | $this->setError("The requested authentication method \"$authtype\" is not supported by the server"); 542 | 543 | return false; 544 | } 545 | } elseif (empty($authtype)) { 546 | $authtype = 'LOGIN'; 547 | } 548 | switch ($authtype) { 549 | case 'PLAIN': 550 | // Start authentication 551 | if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)) { 552 | return false; 553 | } 554 | // Send encoded username and password 555 | if ( 556 | !$this->sendCommand( 557 | 'User & Password', 558 | base64_encode("\0" . $username . "\0" . $password), 559 | 235 560 | ) 561 | ) { 562 | return false; 563 | } 564 | break; 565 | case 'LOGIN': 566 | // Start authentication 567 | if (!$this->sendCommand('AUTH', 'AUTH LOGIN', 334)) { 568 | return false; 569 | } 570 | if (!$this->sendCommand('Username', base64_encode($username), 334)) { 571 | return false; 572 | } 573 | if (!$this->sendCommand('Password', base64_encode($password), 235)) { 574 | return false; 575 | } 576 | break; 577 | case 'CRAM-MD5': 578 | // Start authentication 579 | if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) { 580 | return false; 581 | } 582 | // Get the challenge 583 | $challenge = base64_decode(substr($this->last_reply, 4)); 584 | 585 | // Build the response 586 | $response = $username . ' ' . $this->hmac($challenge, $password); 587 | 588 | // send encoded credentials 589 | return $this->sendCommand('Username', base64_encode($response), 235); 590 | case 'XOAUTH2': 591 | //The OAuth instance must be set up prior to requesting auth. 592 | if (null === $OAuth) { 593 | return false; 594 | } 595 | $oauth = $OAuth->getOauth64(); 596 | 597 | // Start authentication 598 | if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) { 599 | return false; 600 | } 601 | break; 602 | default: 603 | $this->setError("Authentication method \"$authtype\" is not supported"); 604 | 605 | return false; 606 | } 607 | 608 | return true; 609 | } 610 | 611 | /** 612 | * Calculate an MD5 HMAC hash. 613 | * Works like hash_hmac('md5', $data, $key) 614 | * in case that function is not available. 615 | * 616 | * @param string $data The data to hash 617 | * @param string $key The key to hash with 618 | * 619 | * @return string 620 | */ 621 | protected function hmac($data, $key) 622 | { 623 | if (function_exists('hash_hmac')) { 624 | return hash_hmac('md5', $data, $key); 625 | } 626 | 627 | // The following borrowed from 628 | // http://php.net/manual/en/function.mhash.php#27225 629 | 630 | // RFC 2104 HMAC implementation for php. 631 | // Creates an md5 HMAC. 632 | // Eliminates the need to install mhash to compute a HMAC 633 | // by Lance Rushing 634 | 635 | $bytelen = 64; // byte length for md5 636 | if (strlen($key) > $bytelen) { 637 | $key = pack('H*', md5($key)); 638 | } 639 | $key = str_pad($key, $bytelen, chr(0x00)); 640 | $ipad = str_pad('', $bytelen, chr(0x36)); 641 | $opad = str_pad('', $bytelen, chr(0x5c)); 642 | $k_ipad = $key ^ $ipad; 643 | $k_opad = $key ^ $opad; 644 | 645 | return md5($k_opad . pack('H*', md5($k_ipad . $data))); 646 | } 647 | 648 | /** 649 | * Check connection state. 650 | * 651 | * @return bool True if connected 652 | */ 653 | public function connected() 654 | { 655 | if (is_resource($this->smtp_conn)) { 656 | $sock_status = stream_get_meta_data($this->smtp_conn); 657 | if ($sock_status['eof']) { 658 | // The socket is valid but we are not connected 659 | $this->edebug( 660 | 'SMTP NOTICE: EOF caught while checking if connected', 661 | self::DEBUG_CLIENT 662 | ); 663 | $this->close(); 664 | 665 | return false; 666 | } 667 | 668 | return true; // everything looks good 669 | } 670 | 671 | return false; 672 | } 673 | 674 | /** 675 | * Close the socket and clean up the state of the class. 676 | * Don't use this function without first trying to use QUIT. 677 | * 678 | * @see quit() 679 | */ 680 | public function close() 681 | { 682 | $this->setError(''); 683 | $this->server_caps = null; 684 | $this->helo_rply = null; 685 | if (is_resource($this->smtp_conn)) { 686 | // close the connection and cleanup 687 | fclose($this->smtp_conn); 688 | $this->smtp_conn = null; //Makes for cleaner serialization 689 | $this->edebug('Connection: closed', self::DEBUG_CONNECTION); 690 | } 691 | } 692 | 693 | /** 694 | * Send an SMTP DATA command. 695 | * Issues a data command and sends the msg_data to the server, 696 | * finializing the mail transaction. $msg_data is the message 697 | * that is to be send with the headers. Each header needs to be 698 | * on a single line followed by a with the message headers 699 | * and the message body being separated by an additional . 700 | * Implements RFC 821: DATA . 701 | * 702 | * @param string $msg_data Message data to send 703 | * 704 | * @return bool 705 | */ 706 | public function data($msg_data) 707 | { 708 | //This will use the standard timelimit 709 | if (!$this->sendCommand('DATA', 'DATA', 354)) { 710 | return false; 711 | } 712 | 713 | /* The server is ready to accept data! 714 | * According to rfc821 we should not send more than 1000 characters on a single line (including the LE) 715 | * so we will break the data up into lines by \r and/or \n then if needed we will break each of those into 716 | * smaller lines to fit within the limit. 717 | * We will also look for lines that start with a '.' and prepend an additional '.'. 718 | * NOTE: this does not count towards line-length limit. 719 | */ 720 | 721 | // Normalize line breaks before exploding 722 | $lines = explode("\n", str_replace(["\r\n", "\r"], "\n", $msg_data)); 723 | 724 | /* To distinguish between a complete RFC822 message and a plain message body, we check if the first field 725 | * of the first line (':' separated) does not contain a space then it _should_ be a header and we will 726 | * process all lines before a blank line as headers. 727 | */ 728 | 729 | $field = substr($lines[0], 0, strpos($lines[0], ':')); 730 | $in_headers = false; 731 | if (!empty($field) && strpos($field, ' ') === false) { 732 | $in_headers = true; 733 | } 734 | 735 | foreach ($lines as $line) { 736 | $lines_out = []; 737 | if ($in_headers && $line === '') { 738 | $in_headers = false; 739 | } 740 | //Break this line up into several smaller lines if it's too long 741 | //Micro-optimisation: isset($str[$len]) is faster than (strlen($str) > $len), 742 | while (isset($line[self::MAX_LINE_LENGTH])) { 743 | //Working backwards, try to find a space within the last MAX_LINE_LENGTH chars of the line to break on 744 | //so as to avoid breaking in the middle of a word 745 | $pos = strrpos(substr($line, 0, self::MAX_LINE_LENGTH), ' '); 746 | //Deliberately matches both false and 0 747 | if (!$pos) { 748 | //No nice break found, add a hard break 749 | $pos = self::MAX_LINE_LENGTH - 1; 750 | $lines_out[] = substr($line, 0, $pos); 751 | $line = substr($line, $pos); 752 | } else { 753 | //Break at the found point 754 | $lines_out[] = substr($line, 0, $pos); 755 | //Move along by the amount we dealt with 756 | $line = substr($line, $pos + 1); 757 | } 758 | //If processing headers add a LWSP-char to the front of new line RFC822 section 3.1.1 759 | if ($in_headers) { 760 | $line = "\t" . $line; 761 | } 762 | } 763 | $lines_out[] = $line; 764 | 765 | //Send the lines to the server 766 | foreach ($lines_out as $line_out) { 767 | //RFC2821 section 4.5.2 768 | if (!empty($line_out) && $line_out[0] === '.') { 769 | $line_out = '.' . $line_out; 770 | } 771 | $this->client_send($line_out . static::LE, 'DATA'); 772 | } 773 | } 774 | 775 | //Message data has been sent, complete the command 776 | //Increase timelimit for end of DATA command 777 | $savetimelimit = $this->Timelimit; 778 | $this->Timelimit *= 2; 779 | $result = $this->sendCommand('DATA END', '.', 250); 780 | $this->recordLastTransactionID(); 781 | //Restore timelimit 782 | $this->Timelimit = $savetimelimit; 783 | 784 | return $result; 785 | } 786 | 787 | /** 788 | * Send an SMTP HELO or EHLO command. 789 | * Used to identify the sending server to the receiving server. 790 | * This makes sure that client and server are in a known state. 791 | * Implements RFC 821: HELO 792 | * and RFC 2821 EHLO. 793 | * 794 | * @param string $host The host name or IP to connect to 795 | * 796 | * @return bool 797 | */ 798 | public function hello($host = '') 799 | { 800 | //Try extended hello first (RFC 2821) 801 | if ($this->sendHello('EHLO', $host)) { 802 | return true; 803 | } 804 | 805 | //Some servers shut down the SMTP service here (RFC 5321) 806 | if (substr($this->helo_rply, 0, 3) == '421') { 807 | return false; 808 | } 809 | 810 | return $this->sendHello('HELO', $host); 811 | } 812 | 813 | /** 814 | * Send an SMTP HELO or EHLO command. 815 | * Low-level implementation used by hello(). 816 | * 817 | * @param string $hello The HELO string 818 | * @param string $host The hostname to say we are 819 | * 820 | * @return bool 821 | * 822 | * @see hello() 823 | */ 824 | protected function sendHello($hello, $host) 825 | { 826 | $noerror = $this->sendCommand($hello, $hello . ' ' . $host, 250); 827 | $this->helo_rply = $this->last_reply; 828 | if ($noerror) { 829 | $this->parseHelloFields($hello); 830 | } else { 831 | $this->server_caps = null; 832 | } 833 | 834 | return $noerror; 835 | } 836 | 837 | /** 838 | * Parse a reply to HELO/EHLO command to discover server extensions. 839 | * In case of HELO, the only parameter that can be discovered is a server name. 840 | * 841 | * @param string $type `HELO` or `EHLO` 842 | */ 843 | protected function parseHelloFields($type) 844 | { 845 | $this->server_caps = []; 846 | $lines = explode("\n", $this->helo_rply); 847 | 848 | foreach ($lines as $n => $s) { 849 | //First 4 chars contain response code followed by - or space 850 | $s = trim(substr($s, 4)); 851 | if (empty($s)) { 852 | continue; 853 | } 854 | $fields = explode(' ', $s); 855 | if (!empty($fields)) { 856 | if (!$n) { 857 | $name = $type; 858 | $fields = $fields[0]; 859 | } else { 860 | $name = array_shift($fields); 861 | switch ($name) { 862 | case 'SIZE': 863 | $fields = ($fields ? $fields[0] : 0); 864 | break; 865 | case 'AUTH': 866 | if (!is_array($fields)) { 867 | $fields = []; 868 | } 869 | break; 870 | default: 871 | $fields = true; 872 | } 873 | } 874 | $this->server_caps[$name] = $fields; 875 | } 876 | } 877 | } 878 | 879 | /** 880 | * Send an SMTP MAIL command. 881 | * Starts a mail transaction from the email address specified in 882 | * $from. Returns true if successful or false otherwise. If True 883 | * the mail transaction is started and then one or more recipient 884 | * commands may be called followed by a data command. 885 | * Implements RFC 821: MAIL FROM: . 886 | * 887 | * @param string $from Source address of this message 888 | * 889 | * @return bool 890 | */ 891 | public function mail($from) 892 | { 893 | $useVerp = ($this->do_verp ? ' XVERP' : ''); 894 | 895 | return $this->sendCommand( 896 | 'MAIL FROM', 897 | 'MAIL FROM:<' . $from . '>' . $useVerp, 898 | 250 899 | ); 900 | } 901 | 902 | /** 903 | * Send an SMTP QUIT command. 904 | * Closes the socket if there is no error or the $close_on_error argument is true. 905 | * Implements from RFC 821: QUIT . 906 | * 907 | * @param bool $close_on_error Should the connection close if an error occurs? 908 | * 909 | * @return bool 910 | */ 911 | public function quit($close_on_error = true) 912 | { 913 | $noerror = $this->sendCommand('QUIT', 'QUIT', 221); 914 | $err = $this->error; //Save any error 915 | if ($noerror || $close_on_error) { 916 | $this->close(); 917 | $this->error = $err; //Restore any error from the quit command 918 | } 919 | 920 | return $noerror; 921 | } 922 | 923 | /** 924 | * Send an SMTP RCPT command. 925 | * Sets the TO argument to $toaddr. 926 | * Returns true if the recipient was accepted false if it was rejected. 927 | * Implements from RFC 821: RCPT TO: . 928 | * 929 | * @param string $address The address the message is being sent to 930 | * @param string $dsn Comma separated list of DSN notifications. NEVER, SUCCESS, FAILURE 931 | * or DELAY. If you specify NEVER all other notifications are ignored. 932 | * 933 | * @return bool 934 | */ 935 | public function recipient($address, $dsn = '') 936 | { 937 | if (empty($dsn)) { 938 | $rcpt = 'RCPT TO:<' . $address . '>'; 939 | } else { 940 | $dsn = strtoupper($dsn); 941 | $notify = []; 942 | 943 | if (strpos($dsn, 'NEVER') !== false) { 944 | $notify[] = 'NEVER'; 945 | } else { 946 | foreach (['SUCCESS', 'FAILURE', 'DELAY'] as $value) { 947 | if (strpos($dsn, $value) !== false) { 948 | $notify[] = $value; 949 | } 950 | } 951 | } 952 | 953 | $rcpt = 'RCPT TO:<' . $address . '> NOTIFY=' . implode(',', $notify); 954 | } 955 | 956 | return $this->sendCommand( 957 | 'RCPT TO', 958 | $rcpt, 959 | [250, 251] 960 | ); 961 | } 962 | 963 | /** 964 | * Send an SMTP RSET command. 965 | * Abort any transaction that is currently in progress. 966 | * Implements RFC 821: RSET . 967 | * 968 | * @return bool True on success 969 | */ 970 | public function reset() 971 | { 972 | return $this->sendCommand('RSET', 'RSET', 250); 973 | } 974 | 975 | /** 976 | * Send a command to an SMTP server and check its return code. 977 | * 978 | * @param string $command The command name - not sent to the server 979 | * @param string $commandstring The actual command to send 980 | * @param int|array $expect One or more expected integer success codes 981 | * 982 | * @return bool True on success 983 | */ 984 | protected function sendCommand($command, $commandstring, $expect) 985 | { 986 | if (!$this->connected()) { 987 | $this->setError("Called $command without being connected"); 988 | 989 | return false; 990 | } 991 | //Reject line breaks in all commands 992 | if ((strpos($commandstring, "\n") !== false) || (strpos($commandstring, "\r") !== false)) { 993 | $this->setError("Command '$command' contained line breaks"); 994 | 995 | return false; 996 | } 997 | $this->client_send($commandstring . static::LE, $command); 998 | 999 | $this->last_reply = $this->get_lines(); 1000 | // Fetch SMTP code and possible error code explanation 1001 | $matches = []; 1002 | if (preg_match('/^([\d]{3})[ -](?:([\d]\\.[\d]\\.[\d]{1,2}) )?/', $this->last_reply, $matches)) { 1003 | $code = (int) $matches[1]; 1004 | $code_ex = (count($matches) > 2 ? $matches[2] : null); 1005 | // Cut off error code from each response line 1006 | $detail = preg_replace( 1007 | "/{$code}[ -]" . 1008 | ($code_ex ? str_replace('.', '\\.', $code_ex) . ' ' : '') . '/m', 1009 | '', 1010 | $this->last_reply 1011 | ); 1012 | } else { 1013 | // Fall back to simple parsing if regex fails 1014 | $code = (int) substr($this->last_reply, 0, 3); 1015 | $code_ex = null; 1016 | $detail = substr($this->last_reply, 4); 1017 | } 1018 | 1019 | $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER); 1020 | 1021 | if (!in_array($code, (array) $expect, true)) { 1022 | $this->setError( 1023 | "$command command failed", 1024 | $detail, 1025 | $code, 1026 | $code_ex 1027 | ); 1028 | $this->edebug( 1029 | 'SMTP ERROR: ' . $this->error['error'] . ': ' . $this->last_reply, 1030 | self::DEBUG_CLIENT 1031 | ); 1032 | 1033 | return false; 1034 | } 1035 | 1036 | $this->setError(''); 1037 | 1038 | return true; 1039 | } 1040 | 1041 | /** 1042 | * Send an SMTP SAML command. 1043 | * Starts a mail transaction from the email address specified in $from. 1044 | * Returns true if successful or false otherwise. If True 1045 | * the mail transaction is started and then one or more recipient 1046 | * commands may be called followed by a data command. This command 1047 | * will send the message to the users terminal if they are logged 1048 | * in and send them an email. 1049 | * Implements RFC 821: SAML FROM: . 1050 | * 1051 | * @param string $from The address the message is from 1052 | * 1053 | * @return bool 1054 | */ 1055 | public function sendAndMail($from) 1056 | { 1057 | return $this->sendCommand('SAML', "SAML FROM:$from", 250); 1058 | } 1059 | 1060 | /** 1061 | * Send an SMTP VRFY command. 1062 | * 1063 | * @param string $name The name to verify 1064 | * 1065 | * @return bool 1066 | */ 1067 | public function verify($name) 1068 | { 1069 | return $this->sendCommand('VRFY', "VRFY $name", [250, 251]); 1070 | } 1071 | 1072 | /** 1073 | * Send an SMTP NOOP command. 1074 | * Used to keep keep-alives alive, doesn't actually do anything. 1075 | * 1076 | * @return bool 1077 | */ 1078 | public function noop() 1079 | { 1080 | return $this->sendCommand('NOOP', 'NOOP', 250); 1081 | } 1082 | 1083 | /** 1084 | * Send an SMTP TURN command. 1085 | * This is an optional command for SMTP that this class does not support. 1086 | * This method is here to make the RFC821 Definition complete for this class 1087 | * and _may_ be implemented in future. 1088 | * Implements from RFC 821: TURN . 1089 | * 1090 | * @return bool 1091 | */ 1092 | public function turn() 1093 | { 1094 | $this->setError('The SMTP TURN command is not implemented'); 1095 | $this->edebug('SMTP NOTICE: ' . $this->error['error'], self::DEBUG_CLIENT); 1096 | 1097 | return false; 1098 | } 1099 | 1100 | /** 1101 | * Send raw data to the server. 1102 | * 1103 | * @param string $data The data to send 1104 | * @param string $command Optionally, the command this is part of, used only for controlling debug output 1105 | * 1106 | * @return int|bool The number of bytes sent to the server or false on error 1107 | */ 1108 | public function client_send($data, $command = '') 1109 | { 1110 | //If SMTP transcripts are left enabled, or debug output is posted online 1111 | //it can leak credentials, so hide credentials in all but lowest level 1112 | if ( 1113 | self::DEBUG_LOWLEVEL > $this->do_debug && 1114 | in_array($command, ['User & Password', 'Username', 'Password'], true) 1115 | ) { 1116 | $this->edebug('CLIENT -> SERVER: [credentials hidden]', self::DEBUG_CLIENT); 1117 | } else { 1118 | $this->edebug('CLIENT -> SERVER: ' . $data, self::DEBUG_CLIENT); 1119 | } 1120 | set_error_handler([$this, 'errorHandler']); 1121 | $result = fwrite($this->smtp_conn, $data); 1122 | restore_error_handler(); 1123 | 1124 | return $result; 1125 | } 1126 | 1127 | /** 1128 | * Get the latest error. 1129 | * 1130 | * @return array 1131 | */ 1132 | public function getError() 1133 | { 1134 | return $this->error; 1135 | } 1136 | 1137 | /** 1138 | * Get SMTP extensions available on the server. 1139 | * 1140 | * @return array|null 1141 | */ 1142 | public function getServerExtList() 1143 | { 1144 | return $this->server_caps; 1145 | } 1146 | 1147 | /** 1148 | * Get metadata about the SMTP server from its HELO/EHLO response. 1149 | * The method works in three ways, dependent on argument value and current state: 1150 | * 1. HELO/EHLO has not been sent - returns null and populates $this->error. 1151 | * 2. HELO has been sent - 1152 | * $name == 'HELO': returns server name 1153 | * $name == 'EHLO': returns boolean false 1154 | * $name == any other string: returns null and populates $this->error 1155 | * 3. EHLO has been sent - 1156 | * $name == 'HELO'|'EHLO': returns the server name 1157 | * $name == any other string: if extension $name exists, returns True 1158 | * or its options (e.g. AUTH mechanisms supported). Otherwise returns False. 1159 | * 1160 | * @param string $name Name of SMTP extension or 'HELO'|'EHLO' 1161 | * 1162 | * @return string|bool|null 1163 | */ 1164 | public function getServerExt($name) 1165 | { 1166 | if (!$this->server_caps) { 1167 | $this->setError('No HELO/EHLO was sent'); 1168 | 1169 | return; 1170 | } 1171 | 1172 | if (!array_key_exists($name, $this->server_caps)) { 1173 | if ('HELO' === $name) { 1174 | return $this->server_caps['EHLO']; 1175 | } 1176 | if ('EHLO' === $name || array_key_exists('EHLO', $this->server_caps)) { 1177 | return false; 1178 | } 1179 | $this->setError('HELO handshake was used; No information about server extensions available'); 1180 | 1181 | return; 1182 | } 1183 | 1184 | return $this->server_caps[$name]; 1185 | } 1186 | 1187 | /** 1188 | * Get the last reply from the server. 1189 | * 1190 | * @return string 1191 | */ 1192 | public function getLastReply() 1193 | { 1194 | return $this->last_reply; 1195 | } 1196 | 1197 | /** 1198 | * Read the SMTP server's response. 1199 | * Either before eof or socket timeout occurs on the operation. 1200 | * With SMTP we can tell if we have more lines to read if the 1201 | * 4th character is '-' symbol. If it is a space then we don't 1202 | * need to read anything else. 1203 | * 1204 | * @return string 1205 | */ 1206 | protected function get_lines() 1207 | { 1208 | // If the connection is bad, give up straight away 1209 | if (!is_resource($this->smtp_conn)) { 1210 | return ''; 1211 | } 1212 | $data = ''; 1213 | $endtime = 0; 1214 | stream_set_timeout($this->smtp_conn, $this->Timeout); 1215 | if ($this->Timelimit > 0) { 1216 | $endtime = time() + $this->Timelimit; 1217 | } 1218 | $selR = [$this->smtp_conn]; 1219 | $selW = null; 1220 | while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) { 1221 | //Must pass vars in here as params are by reference 1222 | //solution for signals inspired by https://github.com/symfony/symfony/pull/6540 1223 | set_error_handler([$this, 'errorHandler']); 1224 | $n = stream_select($selR, $selW, $selW, $this->Timelimit); 1225 | restore_error_handler(); 1226 | 1227 | if ($n === false) { 1228 | $message = $this->getError()['detail']; 1229 | 1230 | $this->edebug( 1231 | 'SMTP -> get_lines(): select failed (' . $message . ')', 1232 | self::DEBUG_LOWLEVEL 1233 | ); 1234 | 1235 | //stream_select returns false when the `select` system call is interrupted 1236 | //by an incoming signal, try the select again 1237 | if (stripos($message, 'interrupted system call') !== false) { 1238 | $this->edebug( 1239 | 'SMTP -> get_lines(): retrying stream_select', 1240 | self::DEBUG_LOWLEVEL 1241 | ); 1242 | $this->setError(''); 1243 | continue; 1244 | } 1245 | 1246 | break; 1247 | } 1248 | 1249 | if (!$n) { 1250 | $this->edebug( 1251 | 'SMTP -> get_lines(): select timed-out in (' . $this->Timelimit . ' sec)', 1252 | self::DEBUG_LOWLEVEL 1253 | ); 1254 | break; 1255 | } 1256 | 1257 | //Deliberate noise suppression - errors are handled afterwards 1258 | $str = @fgets($this->smtp_conn, self::MAX_REPLY_LENGTH); 1259 | $this->edebug('SMTP INBOUND: "' . trim($str) . '"', self::DEBUG_LOWLEVEL); 1260 | $data .= $str; 1261 | // If response is only 3 chars (not valid, but RFC5321 S4.2 says it must be handled), 1262 | // or 4th character is a space or a line break char, we are done reading, break the loop. 1263 | // String array access is a significant micro-optimisation over strlen 1264 | if (!isset($str[3]) || $str[3] === ' ' || $str[3] === "\r" || $str[3] === "\n") { 1265 | break; 1266 | } 1267 | // Timed-out? Log and break 1268 | $info = stream_get_meta_data($this->smtp_conn); 1269 | if ($info['timed_out']) { 1270 | $this->edebug( 1271 | 'SMTP -> get_lines(): stream timed-out (' . $this->Timeout . ' sec)', 1272 | self::DEBUG_LOWLEVEL 1273 | ); 1274 | break; 1275 | } 1276 | // Now check if reads took too long 1277 | if ($endtime && time() > $endtime) { 1278 | $this->edebug( 1279 | 'SMTP -> get_lines(): timelimit reached (' . 1280 | $this->Timelimit . ' sec)', 1281 | self::DEBUG_LOWLEVEL 1282 | ); 1283 | break; 1284 | } 1285 | } 1286 | 1287 | return $data; 1288 | } 1289 | 1290 | /** 1291 | * Enable or disable VERP address generation. 1292 | * 1293 | * @param bool $enabled 1294 | */ 1295 | public function setVerp($enabled = false) 1296 | { 1297 | $this->do_verp = $enabled; 1298 | } 1299 | 1300 | /** 1301 | * Get VERP address generation mode. 1302 | * 1303 | * @return bool 1304 | */ 1305 | public function getVerp() 1306 | { 1307 | return $this->do_verp; 1308 | } 1309 | 1310 | /** 1311 | * Set error messages and codes. 1312 | * 1313 | * @param string $message The error message 1314 | * @param string $detail Further detail on the error 1315 | * @param string $smtp_code An associated SMTP error code 1316 | * @param string $smtp_code_ex Extended SMTP code 1317 | */ 1318 | protected function setError($message, $detail = '', $smtp_code = '', $smtp_code_ex = '') 1319 | { 1320 | $this->error = [ 1321 | 'error' => $message, 1322 | 'detail' => $detail, 1323 | 'smtp_code' => $smtp_code, 1324 | 'smtp_code_ex' => $smtp_code_ex, 1325 | ]; 1326 | } 1327 | 1328 | /** 1329 | * Set debug output method. 1330 | * 1331 | * @param string|callable $method The name of the mechanism to use for debugging output, or a callable to handle it 1332 | */ 1333 | public function setDebugOutput($method = 'echo') 1334 | { 1335 | $this->Debugoutput = $method; 1336 | } 1337 | 1338 | /** 1339 | * Get debug output method. 1340 | * 1341 | * @return string 1342 | */ 1343 | public function getDebugOutput() 1344 | { 1345 | return $this->Debugoutput; 1346 | } 1347 | 1348 | /** 1349 | * Set debug output level. 1350 | * 1351 | * @param int $level 1352 | */ 1353 | public function setDebugLevel($level = 0) 1354 | { 1355 | $this->do_debug = $level; 1356 | } 1357 | 1358 | /** 1359 | * Get debug output level. 1360 | * 1361 | * @return int 1362 | */ 1363 | public function getDebugLevel() 1364 | { 1365 | return $this->do_debug; 1366 | } 1367 | 1368 | /** 1369 | * Set SMTP timeout. 1370 | * 1371 | * @param int $timeout The timeout duration in seconds 1372 | */ 1373 | public function setTimeout($timeout = 0) 1374 | { 1375 | $this->Timeout = $timeout; 1376 | } 1377 | 1378 | /** 1379 | * Get SMTP timeout. 1380 | * 1381 | * @return int 1382 | */ 1383 | public function getTimeout() 1384 | { 1385 | return $this->Timeout; 1386 | } 1387 | 1388 | /** 1389 | * Reports an error number and string. 1390 | * 1391 | * @param int $errno The error number returned by PHP 1392 | * @param string $errmsg The error message returned by PHP 1393 | * @param string $errfile The file the error occurred in 1394 | * @param int $errline The line number the error occurred on 1395 | */ 1396 | protected function errorHandler($errno, $errmsg, $errfile = '', $errline = 0) 1397 | { 1398 | $notice = 'Connection failed.'; 1399 | $this->setError( 1400 | $notice, 1401 | $errmsg, 1402 | (string) $errno 1403 | ); 1404 | $this->edebug( 1405 | "$notice Error #$errno: $errmsg [$errfile line $errline]", 1406 | self::DEBUG_CONNECTION 1407 | ); 1408 | } 1409 | 1410 | /** 1411 | * Extract and return the ID of the last SMTP transaction based on 1412 | * a list of patterns provided in SMTP::$smtp_transaction_id_patterns. 1413 | * Relies on the host providing the ID in response to a DATA command. 1414 | * If no reply has been received yet, it will return null. 1415 | * If no pattern was matched, it will return false. 1416 | * 1417 | * @return bool|string|null 1418 | */ 1419 | protected function recordLastTransactionID() 1420 | { 1421 | $reply = $this->getLastReply(); 1422 | 1423 | if (empty($reply)) { 1424 | $this->last_smtp_transaction_id = null; 1425 | } else { 1426 | $this->last_smtp_transaction_id = false; 1427 | foreach ($this->smtp_transaction_id_patterns as $smtp_transaction_id_pattern) { 1428 | $matches = []; 1429 | if (preg_match($smtp_transaction_id_pattern, $reply, $matches)) { 1430 | $this->last_smtp_transaction_id = trim($matches[1]); 1431 | break; 1432 | } 1433 | } 1434 | } 1435 | 1436 | return $this->last_smtp_transaction_id; 1437 | } 1438 | 1439 | /** 1440 | * Get the queue/transaction ID of the last SMTP transaction 1441 | * If no reply has been received yet, it will return null. 1442 | * If no pattern was matched, it will return false. 1443 | * 1444 | * @return bool|string|null 1445 | * 1446 | * @see recordLastTransactionID() 1447 | */ 1448 | public function getLastTransactionID() 1449 | { 1450 | return $this->last_smtp_transaction_id; 1451 | } 1452 | } 1453 | --------------------------------------------------------------------------------