wp_mail function timing out

I have the following function. This is part of a membership-based site. When a post of the type “message” is moved to Publish status, it should send an email to all users who belong (custom user meta) to the district(s) that are checked (taxonomy) when the post is drafted.

If I send to a small group, it seems to work. If I send to all the districts, it hangs and no messages appear to go out. I tried limiting it into chunks (25) to ease the pain, but does not help. Any thoughts besides telling the client to only send to one group at a time?

add_action('publish_messages', 'messages_notify', 10, 3);
function messages_notify() {
    global $post;
    // if post already published, abort
    if($post->post_status == 'publish') {
        return;
    }
    // get districts ticked, else abort
    $districts = get_the_terms($post->ID, 'message_district');
    if(empty($districts)) return;

    // query members who belong to one of the districts ticked
    $the_districts = array();
    $district_args = array('relation' => 'OR' );
    foreach($districts as $district){
        $the_districts[] = $district->name;
        if ($district->name == 'Northeast') {
            $district_args[] = array('key' => 'whprms_district_ne', 'value' => 1);
        }
        if ($district->name == 'North Central') {
            $district_args[] = array('key' => 'whprms_district_nc', 'value' => 1);
        }
        if ($district->name == 'Southeast') {
            $district_args[] = array('key' => 'whprms_district_se', 'value' => 1);
        }
        if ($district->name == 'Southwest') {
            $district_args[] = array('key' => 'whprms_district_sw', 'value' => 1);
        }
        if ($district->name == 'West') {
            $district_args[] = array('key' => 'whprms_district_w', 'value' => 1);
        }
    }
    $district_members = get_users( 
            array(
            'meta_query' => $district_args,
            )
    );

    $attachment_args = array('post_type' => 'attachment', 'orderby' => 'date', 'order' => 'ASC', 'numberposts' => -1, 'post_status' => NULL, 'post_parent' =>$post->ID);
    $attachments = get_posts($attachment_args);
    if ($attachments) {
        $att_array = array();
        foreach ($attachments as $attachment) {
            $att_array[] = get_the_title($attachment->ID);
        }
    }

    // message only users with desired role(s)
    $bcc = array();
    foreach($district_members as $district_member) {
        if(user_can($district_member->ID, 'board_members') || user_can($district_member->ID, 'members')) {
            $bcc[] = $district_member->user_email;
        }
    }

    $subject="WHPRMS Message: ".$post->post_title;
    $message="New message posted by ";
    if(user_can($post->post_author, 'administrator')) {
        $message .= get_the_author_meta('user_nicename', $post->post_author)."\n\r";
    }
    else {
        $message .= get_the_author_meta('first_name', $post->post_author).' '.get_the_author_meta('last_name', $post->post_author)."\n\r"; 
    }

    $message .= 'Excerpt: '.stripslashes(substr($post->post_content, 0, 240)).'...'."\n\r";
    $message .= 'Link: '.get_permalink($post->ID)."\n\r";

    $message .= 'District(s): '.implode(', ', $the_districts)."\n\r";
    if (!empty($att_array)) {
        $message .= 'Attachment(s): '.implode(', ', $att_array)."\n\r";
    }

    $message .= 'You are receiving this message because you belong to one of the districts mentioned above.';

    $chunked_bcc = array_chunk($bcc, 25);

    foreach($chunked_bcc as $bcc_chunk){
        $headers = array();
        $headers['Bcc'] = 'BCC: '.implode(', ', $bcc_chunk);
        wp_mail('[email protected]', $subject, $message, $headers);
    }

}

2 Answers
2

Well, this isn’t a full solution per se, but I would start by isolating whether the problem is the SQL queries or the wp_mail() call. If you comment out the wp_mail() line, you can get an idea of how long the function takes to run the queries and build the messages without trying to send them.

If you find that suddenly the function is lightning fast, then it’s a fair bet that the mail connections are what’s slowing you down, but unfortunately that opens another can of worms since email configurations can vary wildly from web host to web host.

If on the other hand the function is just as slow, then the queries are the likely culprit, and then it would be very helpful to see the actual SQL that WordPress is generating in order to try to identify the performance leak.

EDIT:

Sounds like it is the queries doing many many JOINs after all. In that case, the goal is to avoid that. We can take advantage of the fact that we’re looking for different cases of the same wp_usermeta.meta_key values to build a query that doesn’t need all the JOINs:

function messages_notify() {
    global $post;
    // if post already published, abort
    if($post->post_status == 'publish') {
        return;
    }
    // get districts ticked, else abort
    $post_districts = get_the_terms($post->ID, 'message_district');
    if(empty($post_districts)) return;

    // Build a list of districts to get users from.
    $districts = array(
        'Northeast' => 'whprms_district_ne', 
        'North Central' => 'whprms_district_nc', 
        'Southeast' => 'whprms_district_se', 
        'Southwest' => 'whprms_district_sw', 
        'West' => 'whprms_district_w', 
        //'Another' => 'whprms_district_XX', 
    );

    $searchDistricts = array();
    $districtNames = array_keys($districts);
    foreach($post_districts as $d){
        if(in_array($d->name, $districtNames){
            $searchDistricts[] = $districts[$d->name];
        }
    }

    $districts = implode(' OR ', array_map(function($s){ return "wp_usermeta.meta_key = '{$s}'"; }, $searchDistricts));  // Squash it down into an sql-compatible WHERE subclause.

    $sql = "SELECT DISTINCT wp_users.* 
    FROM wp_users 
    INNER JOIN wp_usermeta ON (wp_users.ID = wp_usermeta.user_id) 
    WHERE 
    -- This part isolates to keys in the list of districts
    ({$districts}) 
    -- Where the value of the district is 1.
    AND CAST(wp_usermeta.meta_value AS CHAR) = '1'
    ORDER BY user_login ASC";

    $district_members = $wpdb->WHATEVER_THE_CUSTOM_QUERY_METHOD_IS_CALLED($sql);

    $attachment_args = array('post_type' => 'attachment', 'orderby' => 'date', 'order' => 'ASC', 'numberposts' => -1, 'post_status' => NULL, 'post_parent' =>$post->ID);
    $attachments = get_posts($attachment_args);
    if ($attachments) {
        $att_array = array();
        foreach ($attachments as $attachment) {
            $att_array[] = get_the_title($attachment->ID);
        }
    }

    // message only users with desired role(s)
    $bcc = array();
    foreach($district_members as $district_member) {
        // Don't use user_can()! It's a DB hit!
        //if(user_can($district_member->ID, 'board_members') || user_can($district_member->ID, 'members')) {  //@TODO:  Look at $district_member->caps
        if(in_array('board_members', $district_member->roles) || in_array('members', $district_member->roles)) {
            $bcc[] = $district_member->user_email;
        }
    }

    $subject="WHPRMS Message: ".$post->post_title;
    $message="New message posted by ";
    if(user_can($post->post_author, 'administrator')) {
        $message .= get_the_author_meta('user_nicename', $post->post_author)."\n\r";
    }
    else {
        $message .= get_the_author_meta('first_name', $post->post_author).' '.get_the_author_meta('last_name', $post->post_author)."\n\r"; 
    }

    $message .= 'Excerpt: '.stripslashes(substr($post->post_content, 0, 240)).'...'."\n\r";
    $message .= 'Link: '.get_permalink($post->ID)."\n\r";

    $message .= 'District(s): '.implode(', ', $the_districts)."\n\r";
    if (!empty($att_array)) {
        $message .= 'Attachment(s): '.implode(', ', $att_array)."\n\r";
    }

    $message .= 'You are receiving this message because you belong to one of the districts mentioned above.';

    $chunked_bcc = array_chunk($bcc, 25);

    foreach($chunked_bcc as $bcc_chunk){
        $headers = array();
        $headers['Bcc'] = 'BCC: '.implode(', ', $bcc_chunk);
        wp_mail('[email protected]', $subject, $message, $headers);
    }

}

A couple things this doesn’t take into account, since I don’t have this particular WP instance in front of me to test with:

  • WHATEVER_THE_CUSTOM_QUERY_METHOD_IS_CALLED. Replace this with whatever the right method is.
  • I also don’t know what format that will return results in. If you can get WP_user objects out of it, that’d be ideal, because the next section depends on $district_users looking the same as what get_users() provided.
  • Also pay attention to the change in foreach($district_members as $district_member). The user_can() method is 2 unnecessary DB hits for EVERY user you’re looping over. You have the user’s capabilities in the results returned from the query already– use that instead.

Leave a Comment