Writing an Email Non-Delivery Report Processing Module is Easier in Drupal 7 than in Drupal 6
A year ago or so I wrote a post on how to construct a Drupal 6 module that processes email non-delivery reports. The object of such a thing - for me in any case - is to stop sending mail to dead addresses or in other circumstances where you have been asked to stop by a destination mail server, as ignoring non-delivery responses tends to make your site look like a spambot to the uncaring mail filters of the world. That will eventually hurt your ability to delivery mail to anywhere other than your users' junk folders.
As of the moment, I'm writing from scratch a Drupal 7 email non-delivery report processor, and finding it generally easier than in Drupal 6. Though, as always, writing the simpletest harness - and other test code that will let me run the whole module without a mail server being involved - is taking longer than writing the actual code to be tested. There is less of a need to play the game of butting heads with other modules over control of the mail interface in Drupal 7, however, which is pleasant. So the code for preventing mail going out to addresses that are blocked due to too many non-delivery reports looks something like this:
/** * Implements hook_mail_alter(). * * Here we do the following: * * 1) Set the return path header to the address specified in the admin settings form. * * 2) Add an identifier to the headers that should help match up non-delivery * reports with the email address that the originating mail was sent to. * * 3) Block outgoing mail to by removing addresses from $message['to'] or setting * $message['send'] = FALSE. */ function bounce_mail_alter(&$message) { $return_path = variable_get('bounce_mail_header_return_path', ''); if ($return_path) { $message['headers']['Return-Path'] = $return_path; } // Alter the "to" string to remove mails that are blocked. Remember that this // is RFC 2822, so could be a comma-delimited string of multiple addresses. $to = bounce_mail_remove_blocked_addresses($message['to']); if (empty($to)) { // no non-blocked addresses to send to, so don't send it. $message['send'] = FALSE; return; } else { // update and carry on. $message['to'] = $to; } // extract emails from the "to" string $emails = _bounce_unique_mails_from_text($message['to']); // If there is more than one email then they all get the same header. // That makes it more of a pain to figure out which one generated a // non-delivery report later on, but oh well. $header_id = _bounce_generate_uuid(); $message['headers'][BOUNCE_MAIL_HEADER_ID_KEY] = $header_id; $data = array( 'header_id' => $header_id, 'created' => time(), ); foreach($emails as $email) { $data['mail'] = $email; drupal_write_record('bounce_sent', $data); } }
Which is a considerable improvement over what needed to be hacked into place in Drupal 6 from my point of view. I don't have to worry all that much about other modules, and just do what needs to be done.
Now if you are blocking all outbound email based on addresses, there is probably a bare minimum of user notification that needs to happen. For example, what about registration and password recovery? Silently failing to send mail there is an outright terrible user experience. So at the very least, you'd want to do something along the lines of this:
/** * Implements hook_form_FORM_ID_alter(). * * Add a validation method to the password reset form. */ function bounce_form_user_register_form_alter(&$form, &$form_state) { $form['#validate'][] = 'bounce_user_register_form_validate'; } /** * Validation function for the registration form. Send back an error * if a user is registering with a blocked mail, and if the admin settings * say we should do that. */ function bounce_user_register_form_validate($form, &$form_state) { $notify = variable_get('bounce_error_on_registration', TRUE); $mail = preg_replace('/^\s+|\s+$/', '', $form_state['values']['mail']); if ($notify && bounce_is_blocked(mail)) { $message = variable_get('bounce_error_on_registration_message'); if($message) { form_set_error('mail', check_plain($message)); } } } /** * Implements hook_form_FORM_ID_alter(). * * Add a validation method to the password reset form. */ function bounce_form_user_pass_alter(&$form, &$form_state) { $form['#validate'][] = 'bounce_user_pass_validate'; } /** * Validation function for the reset password form. Send back an error * if a user enters a blocked mail, and if the admin settings * say we should do that. */ function bounce_user_pass_validate($form, &$form_state) { $user = user_load_by_mail($form_state['values']['name']); if (!$user) { $user = user_load_by_name($form_state['values']['name']); } if ($user && $user->bounce_mail_blocked) { $notify = variable_get('bounce_error_on_password_reset', TRUE); $message = variable_get('bounce_error_on_password_reset_message'); if($message && $notify) { form_set_error('mail', check_plain($message)); } } }
Another novelty in Drupal 7 is hook_hook_info() - which can be used to help organize a module that implements many hooks across a range of logically separate areas of functionality. It would be immensely more useful if you could specify arbitrary files and subdirectories rather than just a group string, or if worked for all hooks rather than just most hooks, but it's still enough as-is to break out a module into different files that are only loaded as necessary. The hook implementation looks like this:
/** * Implements hook_hook_info(). * * Tell Drupal which include files contain the various hook * implementations in this module. */ function bounce_hook_info() { $hooks = array(); // Bounce module defined hooks in bounce.api.inc $hooks['bounce_code_type'] = array( 'group' => 'api', ); $hooks['bounce_connector'] = array( 'group' => 'api', ); $hooks['bounce_connector_alter'] = array( 'group' => 'api', ); $hooks['bounce_analyst'] = array( 'group' => 'api', ); $hooks['bounce_analyst_alter'] = array( 'group' => 'api', ); $hooks['bounce_blocker'] = array( 'group' => 'api', ); $hooks['bounce_blocker_alter'] = array( 'group' => 'api', ); // cron hooks in bounce.cron.inc $hooks['cron'] = array( 'group' => 'cron', ); $hooks['cron_queue_info'] = array( 'group' => 'cron', ); // features hooks in bounce.features.inc $hook['features_api'] = array( 'group' => 'features', ); // user and user activity hooks in bounce.user.inc $hooks['form_user_pass_alter'] = array( 'group' => 'user', ); $hooks['form_user_register_form_alter'] = array( 'group' => 'user', ); $hooks['user_load'] = array( 'group' => 'user', ); $hooks['user_login'] = array( 'group' => 'user', ); return $hooks; }
That enables the PHP files in my module directory to look like this:
bounce.admin.inc bounce.analysis.inc bounce.api.inc bounce.blocker.inc bounce.connector.class.inc bounce.connector.inc bounce.connector.test.inc bounce.cron.inc bounce.features.inc bounce.install bounce.module bounce.test bounce.user.inc
Which is a whole lot more pleasant from the perspective of maintenance than having it all lumped into a single file, and (marginally) better for performance than including all of the supporting files on every page load just because the module is enabled.