2

I'm trying to send an email using wp_mail() through a REST custom route, but it's failing somewhere.

EDIT: I found out where, in "sendContactMail()", $request->get_json_params(); returns nothing, although $request contains all my form information. What's the right way to extract this information then?

// custom route callback function
function sendContactMail(WP_REST_Request $request) {
  $response = array(
    'status' => 304,
    'message' => 'There was an error sending the form.'
  );
  $parameters = $request->get_json_params();
  if (count($_POST) > 0) {
    $parameters = $_POST;
  }
  $siteName = wp_strip_all_tags(trim(get_option('blogname')));
  $contactName = wp_strip_all_tags(trim($parameters['contact_name']));
  $contactEmail = wp_strip_all_tags(trim($parameters['contact_email']));
  $contactMessage = wp_strip_all_tags(trim($parameters['contact_message']));

  if (!empty($contactName) && !empty($contactEmail) && !empty($contactMessage)) {
    $subject = "(New message sent from site $siteName) $contactName <$contactEmail>";
    $body = "<h3>$subject</h3><br/>";
    $body .= "<p><b>Name:</b> $contactName</p>";
    $body .= "<p><b>Email:</b> $contactEmail</p>";
    $body .= "<p><b>Message:</b> $contactMessage</p>";

    if (send_email($contactEmail, $contactName, $body)) {
      $response['status'] = 200;
      $response['message'] = 'Form sent successfully.';
    }
  }
  return new WP_REST_Response($response);
}

// custom route declaration
add_action('rest_api_init', function () {
  register_rest_route( 'contact/v1', 'send', array(
    'methods' => ['POST','PUT'],
    'callback' => 'sendContactMail'
  ));
});

Form-related content of $request:

["body":protected]=> string(382) "------WebKitFormBoundaryoh45oL7PWjTynEQK Content-Disposition: form-data; name="contact_name" Jack ------WebKitFormBoundaryoh45oL7PWjTynEQK Content-Disposition: form-data; name="contact_email" [email protected] ------WebKitFormBoundaryoh45oL7PWjTynEQK Content-Disposition: form-data; name="contact_message" This is my message. ------WebKitFormBoundaryoh45oL7PWjTynEQK-- "
3
  • What is send_email? There is no send_mail function in the WP function reference, and the closest I can find is mb_send_mail over on php.net. Normally wp_mail would be used instead. Also, why are you doing all the sanitising and validating in the endpoint? You're meant to add a sanitising and validating function in register_rest_route. And why do you use a $parameters variable? Instead of $request['contact_name']?
    – Tom J Nowell
    Commented Jan 16, 2021 at 17:35
  • Also, have you confirmed that your server is capable of sending emails? There may be nothing wrong with this code, and instead your emails are being rejected because you sent them from a shared host known for spamming on a domain without anti-spam records
    – Tom J Nowell
    Commented Jan 16, 2021 at 17:37
  • Also there is no permission callback defined, that rest route will generate PHP warnings in WP v5.6+
    – Tom J Nowell
    Commented Jan 16, 2021 at 17:38

2 Answers 2

4
+150

You didn't include the code for your send_email() function, but I looked at the original source (revision 1) of your question, and it seems to me that your send_email() function is good, but if this answer doesn't help solve the issue, then add the function code into your question body.

So I don't know why are you setting your (REST) API endpoint's (HTTP) methods to POST and PUT, i.e. 'methods' => ['POST','PUT'], because I think POST is enough (i.e. 'methods' => 'POST'), but it seems to me that you are not properly sending your form data to the API endpoint.

And note that, when you use FormData (in JavaScript) with the PUT method, both $request->get_json_params() and the superglobal $_POST are empty, so if you must use the PUT method, then you would want to send your form data as a JSON-encoded string.

Secondly, regardless the (HTTP) method you use, if you send the data as JSON, then make sure the Content-Type header is set with application/json being the value.

So here are two examples using JavaScript, one for each HTTP method, and these examples worked fine for me on WordPress 5.6.0:

Using fetch(), FormData and the POST method

const formData = new FormData();

formData.append( 'contact_name', 'John Doe' );
formData.append( 'contact_email', '[email protected]' );
formData.append( 'contact_message', 'Just testing' );

fetch( 'https://example.com/wp-json/contact/v1/send', {
    method: 'POST',
    body: formData
} )
    .then( res => res.json() )
    .then( data => console.log( data ) )
    .catch( error => console.log( error ) );

Using fetch(), JSON.stringify() and the PUT method

const formData = {
    contact_name: 'John Doe',
    contact_email: '[email protected]',
    contact_message: 'Just testing'
};

fetch( 'https://example.com/wp-json/contact/v1/send', {
    method: 'PUT',
    body: JSON.stringify( formData ),
    headers: {
        'Content-Type': 'application/json'
    }
} )
    .then( res => res.json() )
    .then( data => console.log( data ) )
    .catch( error => console.log( error ) );

So I hope that helps, and you don't have to use fetch() or even JavaScript, but the point in my answer is, make sure your request uses the proper body/content and content type header.

UPDATE

How to access the request parameters

Please check the Extending the REST API → Adding Custom Endpoints → Arguments section in the REST API handbook for more details, but here are two of the most common ways in accessing the parameters:

  • Use $request->get_params() to get all the parameters (URL-encoded POST body, JSON payload, URL query string, etc.). So in your case:

    // Instead of this:
    $parameters = $request->get_json_params();
    if (count($_POST) > 0) {
      $parameters = $_POST;
    }
    
    // You could simply do:
    $parameters = $request->get_params();
    
  • Use direct array access on the object passed as the first parameter to your endpoint callback, i.e. the $request variable above. E.g.

    $contactName    = $request['contact_name'];
    $contactEmail   = $request['contact_email'];
    $contactMessage = $request['contact_message'];
    

And in your case, you should probably just use the second option above.

Have you tested your send_email() function via direct function calls (i.e. not through the REST API)?

Although I don't know what's the code in your send_email() function, you should test the function by passing test data and see if the function actually works, e.g. does it return true, false, null, is there any errors/notices, etc. And if it's the function that doesn't work, then the problem is not with the REST API, i.e. even on normal requests like the homepage, that function would not work.

So test your email sending function, confirm that it receives all the necessary parameters and that it actually properly sends the email (e.g. use the correct headers like "From" and "Content-Type").

Always set permission_callback, and utilize validate_callback and sanitize_callback for validating/sanitizing the request parameters

So basically, except for the send_email() function, your code does work in that the endpoint gets registered and it returns a good response. But here are some things that you should bear in mind when adding custom endpoints:

  1. Make sure to always set a permission_callback for your endpoint, because if you don't set it, then the following would happen (but by default, only if debugging is enabled on your site):

    As of WordPress 5.5, if a permission_callback is not provided, the REST API will issue a _doing_it_wrong notice.

    Sample notice that would be issued:

    The REST API route definition for contact/v1/send is missing the required permission_callback argument. For REST API routes that are intended to be public, use __return_true as the permission callback.

  2. The third parameter for register_rest_route() accepts an argument named args which is an array of parameters for your endpoint, and for each parameter, you can set a validation callback (validate_callback) and a sanitization callback (sanitize_callback), so you should use that instead of doing the validation/sanitization in your endpoint's main callback. And here's one of the good reasons why would you want to use those callbacks:

    Using sanitize_callback and validate_callback allows the main callback to act only to process the request, and prepare data to be returned using the WP_REST_Response class. By using these two callbacks, you will be able to safely assume your inputs are valid and safe when processing.

    That was a (slightly reformatted) excerpt taken from the API handbook, BTW.

Try my code

Note: I originally tested your code with a dummy send_email() function, but here, I'm using wp_mail() directly in the main callback.

// The code is basically based on yours.

function sendContactMail( WP_REST_Request $request ) {
    $response = array(
        'status'  => 304,
        'message' => 'There was an error sending the form.'
    );

    $siteName = wp_strip_all_tags( trim( get_option( 'blogname' ) ) );
    $contactName = $request['contact_name'];
    $contactEmail = $request['contact_email'];
    $contactMessage = $request['contact_message'];

    $subject = "[$siteName] (testing) New message from $contactName";
//  $body = "<h3>$subject</h3><br/>";
    $body = "<p><b>Name:</b> $contactName</p>";
    $body .= "<p><b>Email:</b> $contactEmail</p>";
    $body .= "<p><b>Message:</b> $contactMessage</p>";

    $to = get_option( 'admin_email' );
    $headers = array(
        'Content-Type: text/html; charset=UTF-8',
        "Reply-To: $contactName <$contactEmail>",
    );

    if ( wp_mail( $to, $subject, $body, $headers ) ) {
        $response['status'] = 200;
        $response['message'] = 'Form sent successfully.';
        $response['test'] = $body;
    }

    return new WP_REST_Response( $response );
}

add_action( 'rest_api_init', function () {
    register_rest_route( 'contact/v1', 'send', array(
        'methods'             => 'POST',
        'callback'            => 'sendContactMail',
        'permission_callback' => '__return_true',
        'args'                => array(
            'contact_name'    => array(
                'required'          => true,
                'validate_callback' => function ( $value ) {
                    return preg_match( '/[a-z0-9]{2,}/i', $value ) ? true :
                        new WP_Error( 'invalid_contact_name', 'Your custom error.' );
                },
                'sanitize_callback' => 'sanitize_text_field',
            ),
            'contact_email'   => array(
                'required'          => true,
                'validate_callback' => 'is_email',
                'sanitize_callback' => 'sanitize_email',
            ),
            'contact_message' => array(
                'required'          => true,
                'sanitize_callback' => 'sanitize_textarea_field',
            ),
        ),
    ) );
} );
8
  • thanks a lot! Your code works. One last thing though: wp_mail returns false although the content of $to, $subject, $body and $headers look OK. Any idea how I could debut this? Within a regular WP site I could use something like stackoverflow.com/questions/50715122/…. But from a non-WP site merely using WP as a headless CMS, I have no clue how to troubleshoot this particular problem. Any suggestion?
    – drake035
    Commented Jan 20, 2021 at 16:23
  • If you could add the REST API endpoint on that WP site, then why can't you do the debugging by hooking on wp_mail_failed? You could even do the debugging from within your endpoint/callback. And did wp_mail() only return false, but the email actually got sent? If no, then try with a direct call to the native mail() function in PHP - and I mean, something super simple like mail( '[email protected]', 'test', 'test' ) - does it work?
    – Sally CJ
    Commented Jan 20, 2021 at 23:49
  • And if I were you, I'd check the hosting support pages/docs to see if there's a special setup in sending emails from PHP on sites hosted by them. Also, you could try with plugins like WP Mail SMTP which comes with a feature to send test emails.
    – Sally CJ
    Commented Jan 20, 2021 at 23:50
  • Thanks! WP Mail SMTP is successfully sending a test email, yet mail('[email protected]', 'test', 'test') or mail('[email protected]', 'test', $headers) (using $headers as defined in your code) don't, can't figure out why..
    – drake035
    Commented Jan 22, 2021 at 11:24
  • 1
    Turns out it WAS working, only the hosting provider was having issues it seems. Thank you so much for your help! :)
    – drake035
    Commented Feb 5, 2021 at 18:45
0

I tried to simulate what you did in my test environment.

When I made the AJAX call like this, it worked.

$.ajax({
    type: "PUT", //HTTP VERB
    url: "${this.baseUrl}/wp-json/contact/v1/send", //URL
    dataType: 'json', //What type of response you expect back from the server
    contentType: 'text/plain', //What type of data you are trying to send
    data: fromData,
    success: function (response, request) {
        alert(response)
        this.success = true
        this.ApiResponse = respone
    },
    error: function (response, request) {
        alert(response)
        this.success = false
        this.ApiResponse = respone
    }

})

My callback function was very simple:

function sendContactMail(WP_REST_Request $request) {
  return 'OK';
}

Once you get the response in Javascript, you can start debug the callback function.

1
  • Thanks but that doesn't really help..
    – drake035
    Commented Jan 16, 2021 at 12:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.