]> git.mjollnir.org Git - moodle.git/commitdiff
First cut of email to module (or core) processing.
authormjollnir_ <mjollnir_>
Tue, 8 Feb 2005 02:57:14 +0000 (02:57 +0000)
committermjollnir_ <mjollnir_>
Tue, 8 Feb 2005 02:57:14 +0000 (02:57 +0000)
This patch contains:

* email_to_user will set the envelope sender to a special bounce processing address (based on $CFG settings)
* email_to_user will accept (and set) a reply-to header, to be generated by the module calling the function.

* new functions:

* generate_email_processing_address - ALWAYS use this to generate the reply-to header. reply-to header will look like this:

(LIMIT: 64 chars total)
prefix - EXACTLY four chars
encodeded, packed, moduleid (0 for core) (2 chars)
up to 42 chars for the modules to put anything they want it (can contain userid (or, eg for forum, postids to reply to), or anything really. 42 chars is ABSOLUTE LIMIT)
16 char hash (half an md5) of the first part of the address, together with a site "secret"

* moodle_process_email - any non-module email processing goes here (currently used for processing bounces)

* bounce handling:

* config settings for bounce threshold and ratio (and whether to handle bounces at all)
* if too many bounces occur against any given user, user_not_fully_set_up will force an email address change
* associated functions (over_bounce_threshold, set_send_count, set_bounce_count)

* handling emails to noreply address (see below)

* new script - admin/process_email.php

This script needs to be called from your mail program for anything starting with the 4 char prefix described above (and optionally, the noreply address)
It will bounce emails to the noreplyaddress, with a friendly "this is not a real email address" message

It will break down and unencode the email address into moduleid and validate the half md5 hash, and call $modname_process_email (if it exists). Arguments to these functions are: $modargs (any part of the email address that isn't the prefix, modid or the hash) and the contents of the email (read from STDIN).

* associated string changes/additions

* changes in config-dist.php to give clues as to how to set this up.

MODULE WRITERS!

take a look at new functions moodle_process_email and generate_email_processing_address  in moodlelib.php for ideas about how to

* encode and unencode the arguments your module needs to do the processing
* how to deal with multiple "actions" for any given module.

Martin Langhoff <martin@catalyst.net.nz> will be writing up some PROPER documentation, containing amongst other things config settings for different mail servers (this was developed against Postfix).  Feel free to email me with any feedback on the code or design, penny@catalyst.net.nz.  Or post on the developer fourm.

admin/process_email.php [new file with mode: 0755]
config-dist.php
lang/en/moodle.php
lib/moodlelib.php
user/edit.php

diff --git a/admin/process_email.php b/admin/process_email.php
new file mode 100755 (executable)
index 0000000..7604bb0
--- /dev/null
@@ -0,0 +1,79 @@
+#!/usr/bin/php -f
+<?
+define('FULLME','cron'); // prevent warnings
+//error_reporting(0);
+//ini_set('display_errors',0);
+require_once(dirname(dirname(__FILE__)).'/config.php');
+$tmp = explode('@',$_ENV['RECIPIENT']);
+$address = $tmp[0];
+
+// BOUNCE EMAILS TO NOREPLY
+if ($_ENV['RECIPIENT'] == $CFG->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
index ccdefda0e6e5817a8d2de23e6932f4150d77848c..68bb3b5df91e8090f9f94b9e0adabe39a7d75cef 100644 (file)
@@ -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
index 23fe5db34dde2636a74624c23d9eacf8c207a870..c73934a24c8d047e4ac8d60b1684a4d2d636ce8c 100644 (file)
@@ -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 <b>must</b> change it to continue.';
 $string['toomanytoshow'] = 'There are too many users to show.';
 $string['top'] = 'Top';
 $string['topic'] = 'Topic';
index 36c00174415c4fd6a7674012f417fe05e84d78ad..70f69a857193a381e893501801641079a55b6728 100644 (file)
@@ -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 .'&amp;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,7 +2695,36 @@ 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);
index 2305f463a7d48e721a7184f9e42b20fbd9b896f6..0f42ec34757316de1d8f1232a2564c3d2d2b69d1 100644 (file)
                     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) {
     $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");