Search code examples
javascriptphpjqueryphpmailer

Different behaviors on success/fail on PHPMailer


I have a PHPMailer form and I am using .ajax using this SO answer jquery-ajax-post-example-with-php

It is successfully sending or not sending, so I know the Mailer and captcha are working. Now, if I could get different behaviors for success and fail, that would be great, but I am not getting this right somehow.

PREFERRED BEHAVIORS

SUCCESS --> reload screen and show bootstrap modal with success msg

FAIL --> reload screen and show bootstrap modal with fail msg

I have a lot of code so, Think of the modals as vanilla as possible, and the PHPMailer is, in fact, sending as well. The problem I am having should be in the code below, but if you have to have something else, just ask. Trying to keep the question as decluttered as possible

  <script type="text/javascript">
        $(document).ready(function() {
            var request;

            $("#contactForm").submit(function(event){
                event.preventDefault();

                // Abort any pending request
                if (request) {
                    request.abort();
                }

                // setup some local variables
                var $form = $(this);

                // Let's select and cache all the fields
                var $inputs = $form.find("input, select, button, textarea");

                // Serialize the data in the form
                var serializedData = $form.serialize();

                // Let's disable the inputs for the duration of the Ajax request.
                // Note: we disable elements AFTER the form data has been serialized.
                // Disabled form elements will not be serialized.
                $inputs.prop("disabled", true);

                request = $.ajax({
                    url: "processLogin.php",
                    type: "post",
                    data: serializedData
                });

                // Callback handler that will be called on success
                request.done(function (response, textStatus, jqXHR){
                    if (response == true ) {
                        top.location.reload();
                        // top.location.href="/";
                        // $('#successEmail').modal('show');
                    } else {
                        // top.location.reload();
                        $('#failEmail').modal('show');
                    }
                });

                // Callback handler that will be called on failure
                request.fail(function (jqXHR, textStatus, errorThrown){
                    // Log the error to the console
                    // console.error(
                    //     "The following error occurred: "+
                    //     textStatus, errorThrown
                    // );
                    // top.location.reload();
                });

                // Callback handler that will be called regardless
                // if the request failed or succeeded
                request.always(function () {
                    // Reenable the inputs
                    $inputs.prop("disabled", false);
                });

            });
        });
    </script>

PHPMailer Form

<?php
    session_start();
?>
<?php
    /**
    * This example shows how to handle a simple contact form.
    */
    $msg = '';


    use PHPMailer\PHPMailer\PHPMailer;

    require './mail/PHPMailer.php';
    require './mail/Exception.php';
    require './mail/SMTP.php';
    require './mail/PHPMailerAutoload.php';

    include_once $_SERVER['DOCUMENT_ROOT'] . '/securimage/securimage.php';

    $securimage = new Securimage();

    //Don't run this unless we're handling a form submission
    if (array_key_exists('email', $_POST)) {
        date_default_timezone_set('Etc/UTC');

        //Create a new PHPMailer instance
        $mail = new PHPMailer;

        $mail->SMTPDebug = 0;                                 
        $mail->isSMTP();    
        $mail->Host = 'smtp.live.com';                 
        $mail->SMTPAuth = true;     
        $mail->Username = '[email protected]';       
        $mail->Password = 'password';                    
        $mail->SMTPSecure = 'tls';                   
        $mail->Port = 587;        

        $mail->setFrom('[email protected]', 'Mailer');
        $mail->addAddress('[email protected]', 'First Last');


        $email = isset($_POST['email']) ? $_POST['email'] : null;
        $name = isset($_POST['name']) ? $_POST['name'] : null;
        $phone = isset($_POST['phone']) ? $_POST['phone'] : null;
        $message = isset($_POST['message']) ? $_POST['message'] : null;


        if ($mail->addReplyTo($_POST['email'], $_POST['name'])) {
            $mail->Subject = 'Contact request';
            $mail->isHTML(true);
            $mail->Body = <<<EOT
    <div style="width:100%">
    <div><label style="color: #044F69; font-weight:bold">Email:</label> <span>{$_POST['email']}</span></div>
    <div><label style="color: #044F69; font-weight:bold">Name:</label> <span>{$_POST['name']}</span></div>
    <div><label style="color: #044F69; font-weight:bold">Phone:</label> <span>{$_POST['phone']}</span></div>
    <div><label style="color: #044F69; font-weight:bold">Message:</label> <span>{$_POST['message']}</span></div>
    </div>
    EOT;

            if ($securimage->check($_POST['captcha_code']) == false) {
                // echo "<meta http-equiv='refresh' content='0'>";
                exit;
            }

            //Send the message, check for errors
            if (!$mail->send()) {
                $msg = 'Sorry, something went wrong. Please try again later.';
            } else {
                header("Location: /");
                exit;
            }
        } else {
            $msg = 'Invalid email address, message ignored.';
        }
    }
    ?>

P.S. The code as I have it (See above), shows the fail modal and no reload regardless of whether it passes or fail.

Thanks in advance!


Solution

  • You've chosen a relatively difficult example to learn with. Reloading after success/failure means you need to introduce sessions or cookies or URL parameters to track state between requests. There are several moving parts you need to get right. You must have your reasons for doing that, but if you can get away without the reload it simplifies things and would probably be a better user experience. I've included info below for both options.

    First up, the PHP. You have several possible exit states, depending on whether the email was successfully sent etc, but they are handled inconsistently. In one case it does a redirect, in some cases it exits silently. All cases should be consistent; the request to that PHP page should generate a response, no matter what happens, and ideally pass that response back to your calling Javascript so it can act on whatever happened.

    It looks like you started setting that up, but didn't complete it - you prepare a $msg with info about what happened, but that is never displayed, and $msg is missing completely from a few exit cases (specifically the success case and what looks like a CAPTCHA failure).

    In addition to your $msg, I'd include a status flag so the JS can easily tell whether the request was successful or not; and since you're reading this response in JS, I'd make it JSON.

    Note that as you want to reload the page in both success/failure cases, strictly speaking a JSON response isn't really necessary, but I think it is good practice and will help with debugging while developing.

    To make sure each exit condition generates a standard response, I've had to nest the actual mail sending code inside the CAPTCHA test. This IMO is a bit messy, and personally I'd restructure things, returning early to make testing the various conditions simpler and so you don't end up with 3-4 nested tests. For example instead of nesting everything inside the array_key_exists('email', $_POST) test, test the inverse (if there is no email value) and exit immediately if it fails.

    // Don't run this unless we're handling a form submission
    if (!array_key_exists('email', $_POST)) {
        // Generate response and bail out!
        echo $some_response_or_whatever;
        die();
    }
    

    I'd also suggest perhaps rethinking the flow, eg if CAPTCHA fails, do you really want to have to reload just to show that error? You're sabotaging one of the niceties of using AJAX - getting responses without having to reload. Maybe if something fails, you can just show the error msg on the form where the user clicked submit? If it succeeds, you open your modal, with a reload if really necessary.

    Summary of the changes I made below:

    1. Nest $mail->send() inside CAPTCHA condition;

    2. Create a $response with appropriate message for CAPTCHA failure;

    3. Create a $response for success case;

    4. Add status flag to all responses;

    5. Add the $response to the session, so that it is available after page reload;

    6. Finally, echo the response as JSON so the calling AJAX can tell what happened.

    Here's the updated, abbreviated (I removed large chunks of unchanged code) code:

    <?php
    session_start();
    
    // ... your code
    
    //Don't run this unless we're handling a form submission
    if (array_key_exists('email', $_POST)) {
    
        // ... your code
    
        if ($mail->addReplyTo($_POST['email'], $_POST['name'])) {
    
            // ... your code
    
            if ($securimage->check($_POST['captcha_code']) == false) {
                // Generate a response in this failure case, including a message and a status flag
                $response = [
                    'status'=> 1,
                    'msg'   => 'CAPTCHA test failed!'
                ];
    
            } else {
                //Send the message, check for errors
                if (!$mail->send()) {
                    // Generate a response in this failure case, including a message and a status flag
                    $response = [
                        'status'=> 1,
                        'msg'   => 'Sorry, something went wrong. Please try again later.'
                    ];
                } else {
                    // Generate a response in the success case, including a message and a status flag
                    $response = [
                        'status'=> 0,
                        'msg'   => 'Success!'
                    ];
                }
            }
        } else {
            // Generate a response in this failure case, including a message and a status flag
            $response = [
                'status'=> 1,
                'msg'   => 'Invalid email address, message ignored.'
            ];
        }
    }
    
    // Add the response to the session, so that it will be available after reload
    $_SESSION['response'] = $response;
    
    // Finally display the response as JSON so calling JS can see what happened
    header('Content-Type: application/json');
    echo json_encode($response);
    ?>
    

    Next, your Javascript. Since you want the page to reload in both success and failure cases, you don't really need to test the response.

    request.done(function (response, textStatus, jqXHR){
        // You could test the response here, but since both success and failure 
        // should reload, there is no point.
        window.location.reload();
    
        // If you did want to test response and act on it, here's how to do that:
        /*
        if (response.status == 0) {
            // Success! Do something successful, like show success modal
            $('#successEmail').modal('show');
        } else {
            // Oh no, failure - notify the user
            $('.message').html(response.msg);
        }
        */
    });
    

    You did not mention in your description what should happen in the fail case. Note the fail callback is fired when the request fails, eg 404 or timeout or something like that, not when one of your failure cases like invalid email is triggered. I'd simply generate a msg on the front end in this case, without reloading:

    request.fail(function (jqXHR, textStatus, errorThrown){
        // Say you have a <div class="message"></div> somewhere, maybe under your form
        $('.message').html('Error: ' + textStatus + ', ' + errorThrown)
    });
    

    So, now your PHP has fired, set a session variable and returned, and your JS has reloaded the page. You need to test the content of your session variable, and fire some JS accordingly. On your front end page, inside your existing $(document).ready( ...:

    <script>
    $(document).ready(function() {
        var request;
    
        // ... your code 
    
        <?php 
        // New PHP/JS to handle what happens when the page reloads
        if (isset($_SESSION['response'])) { 
            if ($_SESSION['response']['status'] == 0) { ?>
                // Success!
                $('#successEmail').modal('show');
            <?php } else { ?>
                $('#failEmail').modal('show');
            <?php }
    
            // In both cases we need to clear the response so next reload does not
            // fire a modal again
            unset($_SESSION['response']);
        } ?>
    });
    </script>
    

    You can access the content of the success/failure message in your modals using $_SESSION['response']['msg'].

    Another suggestion - if your success and failure modals are similar, you could use just one, and update its content with Javascript.

    <script>
    <?php if (isset($_SESSION['response'])) { ?>
        // First add the message to the modal, assuming you have a 
        // <div class="message"></div> in your modal
        $('#modal .message').html('<?php echo $_SESSION['response']['msg']; ?>');
    
        // Now open the one modal to rule them all
        $('#modal').modal('show');
    
        <?php
        unset($_SESSION['response']);
    } ?>
    </script>
    

    Side note - I am not sure if your Abort any pending request is really doing what you want. request is declared on document ready, so it is always going to exist, AFAICT?