diff --git a/admin/process_email.php b/admin/process_email.php new file mode 100755 index 00000000000..7604bb06e55 --- /dev/null +++ b/admin/process_email.php @@ -0,0 +1,79 @@ +#!/usr/bin/php -f +noreplyaddress) { + $user->email = $_ENV['SENDER']; + + if (!validate_email($user->email)) { + die(); + } + + $site = get_site(); + $subject = get_string('noreplybouncesubject','moodle',$site->fullname); + $body = get_string('noreplybouncemessage','moodle',$site->fullname)."\n\n"; + + $fd = fopen('php://stdin','r'); + if ($fd) { + while(!feof($fd)) { + $body .= fgets($fd); + } + fclose($fd); + } + + $user->id = 0; // to prevent anything annoying happening + + $from->firstname = null; + $from->lastname = null; + $from->email = '<>'; + $from->maildisplay = true; + + email_to_user($user,$from,$subject,$body); + die (); +} +/// ALL OTHER PROCESSING +// we need to split up the address +$prefix = substr($address,0,4); +$mod = substr($address,4,2); +$modargs = substr($address,6,-16); +$hash = substr($address,-16); + +if (substr(md5($prefix.$mod.$modargs.$CFG->sitesecret),0,16) != $hash) { + die("HASH DIDN'T MATCH!\n"); +} +list(,$modid) = unpack('C',base64_decode($mod.'==')); + +if ($modid == '0') { // special + $modname = 'moodle'; +} +else { + $modname = get_field("modules","name","id",$modid); + require_once('mod/'.$modname.'/lib.php'); +} +$function = $modname.'_process_email'; + +if (!function_exists($function)) { + die(); +} +$fd = fopen('php://stdin','r'); +if (!$fd) { + exit(); +} + +while(!feof($fd)) { + $body .= fgets($fd); +} + +$function($modargs,$body); + +fclose($handle); + + + +?> \ No newline at end of file diff --git a/config-dist.php b/config-dist.php index ccdefda0e6e..68bb3b5df91 100644 --- a/config-dist.php +++ b/config-dist.php @@ -195,6 +195,17 @@ $CFG->defaultblocks = 'participants,activity_modules,search_forums,admin,course_ // may break things for users coming using proxies that change all the time, // like AOL. // $CFG->tracksessionip = true; +// +// +// The following lines are for handling email bounces. +// $CFG->handlebounces = true; +// $CFG->minbounces = 10; +// $CFG->bounceratio = .20; +// The next lines are needed both for bounce handling and any other email to module processing. +// mailprefix must be EXACTLY four characters. +// Uncomment and customise this block for Postfix +//$CFG->mailprefix = 'mdl+'; // + is postfix default separator. +//$CFG->maildomain = 'youremaildomain.com'; //========================================================================= // ALL DONE! To continue installation, visit your main page with a browser diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 23fe5db34dd..c73934a24c8 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -810,6 +810,9 @@ $string['nopotentialadmins'] = 'No potential admins'; $string['nopotentialcreators'] = 'No potential course creators'; $string['nopotentialstudents'] = 'No potential students'; $string['nopotentialteachers'] = 'No potential teachers'; +$string['noreplyname'] = 'Do not reply to this email'; +$string['noreplybouncemessage'] = 'You have replied to a no-reply email address. If you were atttempting to reply to a forum post, please instead reply using the $a forums. '."\n\n".'Following is the content of your email:'."\n\n"; +$string['noreplybouncesubject'] = '$a - bounced email.'; $string['noresults'] = 'No results'; $string['normal'] = 'Normal'; $string['normalfilter'] = 'Normal search'; @@ -1042,6 +1045,7 @@ $string['timezone'] = 'Timezone'; $string['to'] = 'To'; $string['today'] = 'Today'; $string['todaylogs'] = 'Today\'s logs'; +$string['toomanybounces'] = 'That email address has had too many bounces. You must change it to continue.'; $string['toomanytoshow'] = 'There are too many users to show.'; $string['top'] = 'Top'; $string['topic'] = 'Topic'; diff --git a/lib/moodlelib.php b/lib/moodlelib.php index 36c00174415..70f69a85719 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -954,7 +954,6 @@ function require_login($courseid=0, $autologinguest=true) { Please contact your Moodle Administrator.'); } } - // Check that the user account is properly set up if (user_not_fully_set_up($USER)) { redirect($CFG->wwwroot .'/user/edit.php?id='. $USER->id .'&course='. SITEID); @@ -1085,7 +1084,68 @@ function update_user_login_times() { * @return boolean */ function user_not_fully_set_up($user) { - return ($user->username != 'guest' and (empty($user->firstname) or empty($user->lastname) or empty($user->email))); + return ($user->username != 'guest' and (empty($user->firstname) or empty($user->lastname) or empty($user->email) or over_bounce_threshold($user))); +} + +function over_bounce_threshold($user) { + + global $CFG; + + if (empty($CFG->handlebounces)) { + return false; + } + // set sensible defaults + if (empty($CFG->minbounces)) { + $CFG->minbounces = 10; + } + if (empty($CFG->bounceratio)) { + $CFG->bounceratio = .20; + } + $bouncecount = 0; + $sendcount = 0; + if ($bounce = get_record('user_preferences','userid',$user->id,'name','email_bounce_count')) { + $bouncecount = $bounce->value; + } + if ($send = get_record('user_preferences','userid',$user->id,'name','email_send_count')) { + $sendcount = $send->value; + } + return ($bouncecount >= $CFG->minbounces && $bouncecount/$sendcount >= $CFG->bounceratio); +} + +/** + * @param $user - object containing an id + * @param $reset - will reset the count to 0 + */ +function set_send_count($user,$reset=false) { + if ($pref = get_record('user_preferences','userid',$user->id,'name','email_send_count')) { + $pref->value = (!empty($reset)) ? 0 : $pref->value+1; + update_record('user_preferences',$pref); + } + else if (!empty($reset)) { // if it's not there and we're resetting, don't bother. + // make a new one + $pref->name = 'email_send_count'; + $pref->value = 1; + $pref->userid = $user->id; + insert_record('user_preferences',$pref); + } +} + +/** +* @param $user - object containing an id + * @param $reset - will reset the count to 0 + */ +function set_bounce_count($user,$reset=false) { + if ($pref = get_record('user_preferences','userid',$user->id,'name','email_bounce_count')) { + $pref->value = (!empty($reset)) ? 0 : $pref->value+1; + update_record('user_preferences',$pref); + } + else if (!empty($reset)) { // if it's not there and we're resetting, don't bother. + // make a new one + $pref->name = 'email_bounce_count'; + $pref->value = 1; + $pref->userid = $user->id; + insert_record('user_preferences',$pref); + } } /** @@ -2635,8 +2695,37 @@ function setup_and_print_groups($course, $groupmode, $urlroot) { return $currentgroup; } +function generate_email_processing_address($modid,$modargs) { + global $CFG; + + if (empty($CFG->sitesecret)) { + set_config('sitesecret',random_string(10)); + } + + $header = $CFG->mailprefix . substr(base64_encode(pack('C',$modid)),0,2).$modargs; + return $header . substr(md5($header.$CFG->sitesecret),0,16).'@'.$CFG->maildomain; +} +function moodle_process_email($modargs,$body) { + // the first char should be an unencoded letter. We'll take this as an action + switch ($modargs{0}) { + case 'B': { // bounce + list(,$userid) = unpack('V',base64_decode(substr($modargs,1,8))); + if ($user = get_record_select("user","id=$userid","id,email")) { + // check the half md5 of their email + $md5check = substr(md5($user->email),0,16); + if ($md5check = substr($modargs, -16)) { + set_bounce_count($user); + } + // else maybe they've already changed it? + } + } + break; + // maybe more later? + } +} + /// CORRESPONDENCE //////////////////////////////////////////////// /** @@ -2657,7 +2746,7 @@ function setup_and_print_groups($course, $groupmode, $urlroot) { * @return boolean|string Returns "true" if mail was sent OK, "emailstop" if email * was blocked by user and "false" if there was another sort of error. */ -function email_to_user($user, $from, $subject, $messagetext, $messagehtml='', $attachment='', $attachname='', $usetrueaddress=true) { +function email_to_user($user, $from, $subject, $messagetext, $messagehtml='', $attachment='', $attachname='', $usetrueaddress=true, $repyto='', $replytoname='') { global $CFG, $FULLME; @@ -2675,6 +2764,11 @@ function email_to_user($user, $from, $subject, $messagetext, $messagehtml='', $a if (!empty($user->emailstop)) { return 'emailstop'; } + + if (over_bounce_threshold($user)) { + error_log("User $user->id (".fullname($user).") is over bounce threshold! Not sending."); + return false; + } $mail = new phpmailer; @@ -2709,7 +2803,14 @@ function email_to_user($user, $from, $subject, $messagetext, $messagehtml='', $a $adminuser = get_admin(); - $mail->Sender = $adminuser->email; + // make up an email address for handling bounces + if (!empty($CFG->handlebounces)) { + $modargs = 'B'.base64_encode(pack('V',$user->id)).substr(md5($user->email),0,16); + $mail->Sender = generate_email_processing_address(0,$modargs); + } + else { + $mail->Sender = $adminuser->email; + } if (is_string($from)) { // So we can pass whatever we want if there is need $mail->From = $CFG->noreplyaddress; @@ -2720,7 +2821,15 @@ function email_to_user($user, $from, $subject, $messagetext, $messagehtml='', $a } else { $mail->From = $CFG->noreplyaddress; $mail->FromName = fullname($from); + if (empty($replyto)) { + $mail->AddReplyTo($CFG->noreplyaddress,get_string('noreplyname')); + } } + + if (!empty($replyto)) { + $mail->AddReplyTo($replyto,$replytoname); + } + $mail->Subject = stripslashes($subject); $mail->AddAddress($user->email, fullname($user) ); @@ -2759,6 +2868,7 @@ function email_to_user($user, $from, $subject, $messagetext, $messagehtml='', $a } if ($mail->Send()) { + set_send_count($user); return true; } else { mtrace('ERROR: '. $mail->ErrorInfo); diff --git a/user/edit.php b/user/edit.php index 2305f463a7d..0f42ec34757 100644 --- a/user/edit.php +++ b/user/edit.php @@ -165,6 +165,11 @@ auth_user_update($userold, $usernew); }; + if ($userold->email != $usernew->email) { + set_bounce_count($usernew,true); + set_send_count($usernew,true); + } + add_to_log($course->id, "user", "update", "view.php?id=$user->id&course=$course->id", ""); if ($user->id == $USER->id) { @@ -193,6 +198,10 @@ $strparticipants = get_string("participants"); $strnewuser = get_string("newuser"); + if (over_bounce_threshold($user) && empty($err['email'])) { + $err['email'] = get_string('toomanybounces'); + } + if (($user->firstname and $user->lastname) or $newaccount) { if ($newaccount) { $userfullname = $strnewuser; @@ -307,6 +316,9 @@ function find_form_errors(&$user, &$usernew, &$err, &$um) { if (empty($usernew->email)) $err["email"] = get_string("missingemail"); + if (over_bounce_threshold($user) && $user->email == $usernew->email) + $err['email'] = get_string('toomanybounces'); + if (empty($usernew->description) and !isadmin()) $err["description"] = get_string("missingdescription");