/** * Send an email to a specified user * * @param stdClass $user A {@link $USER} object * @param stdClass $from A {@link $USER} object * @param string $subject plain text subject line of the email * @param string $messagetext plain text version of the message * @param string $messagehtml complete html version of the message (optional) * @param string $attachment a file on the filesystem, either relative to $CFG->dataroot or a full path to a file in $CFG->tempdir * @param string $attachname the name of the file (extension indicates MIME) * @param bool $usetrueaddress determines whether $from email address should * be sent out. Will be overruled by user profile setting for maildisplay * @param string $replyto Email address to reply to * @param string $replytoname Name of reply to recipient * @param int $wordwrapwidth custom word wrap width, default 79 * @return bool Returns true if mail was sent OK and false if there was an error. */ function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '', $attachment = '', $attachname = '', $usetrueaddress = true, $replyto = '', $replytoname = '', $wordwrapwidth = 79) { global $CFG, $PAGE, $SITE; if (empty($user) or empty($user->id)) { debugging('Can not send email to null user', DEBUG_DEVELOPER); return false; } if (empty($user->email)) { debugging('Can not send email to user without email: ' . $user->id, DEBUG_DEVELOPER); return false; } if (!empty($user->deleted)) { debugging('Can not send email to deleted user: '******'BEHAT_SITE_RUNNING')) { // Fake email sending in behat. return true; } if (!empty($CFG->noemailever)) { // Hidden setting for development sites, set in config.php if needed. debugging('Not sending email due to $CFG->noemailever config setting', DEBUG_NORMAL); return true; } if (email_should_be_diverted($user->email)) { $subject = "[DIVERTED {$user->email}] {$subject}"; $user = clone $user; $user->email = $CFG->divertallemailsto; } // Skip mail to suspended users. if (isset($user->auth) && $user->auth == 'nologin' or isset($user->suspended) && $user->suspended) { return true; } if (!validate_email($user->email)) { // We can not send emails to invalid addresses - it might create security issue or confuse the mailer. debugging("email_to_user: User {$user->id} (" . fullname($user) . ") email ({$user->email}) is invalid! Not sending."); return false; } if (over_bounce_threshold($user)) { debugging("email_to_user: User {$user->id} (" . fullname($user) . ") is over bounce threshold! Not sending."); return false; } // TLD .invalid is specifically reserved for invalid domain names. // For More information, see {@link http://tools.ietf.org/html/rfc2606#section-2}. if (substr($user->email, -8) == '.invalid') { debugging("email_to_user: User {$user->id} (" . fullname($user) . ") email domain ({$user->email}) is invalid! Not sending."); return true; // This is not an error. } // If the user is a remote mnet user, parse the email text for URL to the // wwwroot and modify the url to direct the user's browser to login at their // home site (identity provider - idp) before hitting the link itself. if (is_mnet_remote_user($user)) { require_once $CFG->dirroot . '/mnet/lib.php'; $jumpurl = mnet_get_idp_jump_url($user); $callback = partial('mnet_sso_apply_indirection', $jumpurl); $messagetext = preg_replace_callback("%({$CFG->wwwroot}[^[:space:]]*)%", $callback, $messagetext); $messagehtml = preg_replace_callback("%href=[\"'`]({$CFG->wwwroot}[\\w_:\\?=#&@/;.~-]*)[\"'`]%", $callback, $messagehtml); } $mail = get_mailer(); if (!empty($mail->SMTPDebug)) { echo '<pre>' . "\n"; } $temprecipients = array(); $tempreplyto = array(); // Make sure that we fall back onto some reasonable no-reply address. $noreplyaddress = empty($CFG->noreplyaddress) ? 'noreply@' . get_host_from_url($CFG->wwwroot) : $CFG->noreplyaddress; // 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 = $noreplyaddress; } $alloweddomains = null; if (!empty($CFG->allowedemaildomains)) { $alloweddomains = explode(PHP_EOL, $CFG->allowedemaildomains); } // Email will be sent using no reply address. if (empty($alloweddomains)) { $usetrueaddress = false; } if (is_string($from)) { // So we can pass whatever we want if there is need. $mail->From = $noreplyaddress; $mail->FromName = $from; // Check if using the true address is true, and the email is in the list of allowed domains for sending email, // and that the senders email setting is either displayed to everyone, or display to only other users that are enrolled // in a course with the sender. } else { if ($usetrueaddress && can_send_from_real_email_address($from, $user, $alloweddomains)) { $mail->From = $from->email; $fromdetails = new stdClass(); $fromdetails->name = fullname($from); $fromdetails->url = $CFG->wwwroot; $fromstring = $fromdetails->name; if ($CFG->emailfromvia == EMAIL_VIA_ALWAYS) { $fromstring = get_string('emailvia', 'core', $fromdetails); } $mail->FromName = $fromstring; if (empty($replyto)) { $tempreplyto[] = array($from->email, fullname($from)); } } else { $mail->From = $noreplyaddress; $fromdetails = new stdClass(); $fromdetails->name = fullname($from); $fromdetails->url = $CFG->wwwroot; $fromstring = $fromdetails->name; if ($CFG->emailfromvia != EMAIL_VIA_NEVER) { $fromstring = get_string('emailvia', 'core', $fromdetails); } $mail->FromName = $fromstring; if (empty($replyto)) { $tempreplyto[] = array($noreplyaddress, get_string('noreplyname')); } } } if (!empty($replyto)) { $tempreplyto[] = array($replyto, $replytoname); } $temprecipients[] = array($user->email, fullname($user)); // Set word wrap. $mail->WordWrap = $wordwrapwidth; if (!empty($from->customheaders)) { // Add custom headers. if (is_array($from->customheaders)) { foreach ($from->customheaders as $customheader) { $mail->addCustomHeader($customheader); } } else { $mail->addCustomHeader($from->customheaders); } } // If the X-PHP-Originating-Script email header is on then also add an additional // header with details of where exactly in moodle the email was triggered from, // either a call to message_send() or to email_to_user(). if (ini_get('mail.add_x_header')) { $stack = debug_backtrace(false); $origin = $stack[0]; foreach ($stack as $depth => $call) { if ($call['function'] == 'message_send') { $origin = $call; } } $originheader = $CFG->wwwroot . ' => ' . gethostname() . ':' . str_replace($CFG->dirroot . '/', '', $origin['file']) . ':' . $origin['line']; $mail->addCustomHeader('X-Moodle-Originating-Script: ' . $originheader); } if (!empty($from->priority)) { $mail->Priority = $from->priority; } $renderer = $PAGE->get_renderer('core'); $context = array('sitefullname' => $SITE->fullname, 'siteshortname' => $SITE->shortname, 'sitewwwroot' => $CFG->wwwroot, 'subject' => $subject, 'to' => $user->email, 'toname' => fullname($user), 'from' => $mail->From, 'fromname' => $mail->FromName); if (!empty($tempreplyto[0])) { $context['replyto'] = $tempreplyto[0][0]; $context['replytoname'] = $tempreplyto[0][1]; } if ($user->id > 0) { $context['touserid'] = $user->id; $context['tousername'] = $user->username; } if (!empty($user->mailformat) && $user->mailformat == 1) { // Only process html templates if the user preferences allow html email. if ($messagehtml) { // If html has been given then pass it through the template. $context['body'] = $messagehtml; $messagehtml = $renderer->render_from_template('core/email_html', $context); } else { // If no html has been given, BUT there is an html wrapping template then // auto convert the text to html and then wrap it. $autohtml = trim(text_to_html($messagetext)); $context['body'] = $autohtml; $temphtml = $renderer->render_from_template('core/email_html', $context); if ($autohtml != $temphtml) { $messagehtml = $temphtml; } } } $context['body'] = $messagetext; $mail->Subject = $renderer->render_from_template('core/email_subject', $context); $mail->FromName = $renderer->render_from_template('core/email_fromname', $context); $messagetext = $renderer->render_from_template('core/email_text', $context); // Autogenerate a MessageID if it's missing. if (empty($mail->MessageID)) { $mail->MessageID = generate_email_messageid(); } if ($messagehtml && !empty($user->mailformat) && $user->mailformat == 1) { // Don't ever send HTML to users who don't want it. $mail->isHTML(true); $mail->Encoding = 'quoted-printable'; $mail->Body = $messagehtml; $mail->AltBody = "\n{$messagetext}\n"; } else { $mail->IsHTML(false); $mail->Body = "\n{$messagetext}\n"; } if ($attachment && $attachname) { if (preg_match("~\\.\\.~", $attachment)) { // Security check for ".." in dir path. $temprecipients[] = array($supportuser->email, fullname($supportuser, true)); $mail->addStringAttachment('Error in attachment. User attempted to attach a filename with a unsafe name.', 'error.txt', '8bit', 'text/plain'); } else { require_once $CFG->libdir . '/filelib.php'; $mimetype = mimeinfo('type', $attachname); $attachmentpath = $attachment; // Before doing the comparison, make sure that the paths are correct (Windows uses slashes in the other direction). $attachpath = str_replace('\\', '/', $attachmentpath); // Make sure both variables are normalised before comparing. $temppath = str_replace('\\', '/', realpath($CFG->tempdir)); // If the attachment is a full path to a file in the tempdir, use it as is, // otherwise assume it is a relative path from the dataroot (for backwards compatibility reasons). if (strpos($attachpath, $temppath) !== 0) { $attachmentpath = $CFG->dataroot . '/' . $attachmentpath; } $mail->addAttachment($attachmentpath, $attachname, 'base64', $mimetype); } } // Check if the email should be sent in an other charset then the default UTF-8. if (!empty($CFG->sitemailcharset) || !empty($CFG->allowusermailcharset)) { // Use the defined site mail charset or eventually the one preferred by the recipient. $charset = $CFG->sitemailcharset; if (!empty($CFG->allowusermailcharset)) { if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id)) { $charset = $useremailcharset; } } // Convert all the necessary strings if the charset is supported. $charsets = get_list_of_charsets(); unset($charsets['UTF-8']); if (in_array($charset, $charsets)) { $mail->CharSet = $charset; $mail->FromName = core_text::convert($mail->FromName, 'utf-8', strtolower($charset)); $mail->Subject = core_text::convert($mail->Subject, 'utf-8', strtolower($charset)); $mail->Body = core_text::convert($mail->Body, 'utf-8', strtolower($charset)); $mail->AltBody = core_text::convert($mail->AltBody, 'utf-8', strtolower($charset)); foreach ($temprecipients as $key => $values) { $temprecipients[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset)); } foreach ($tempreplyto as $key => $values) { $tempreplyto[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset)); } } } foreach ($temprecipients as $values) { $mail->addAddress($values[0], $values[1]); } foreach ($tempreplyto as $values) { $mail->addReplyTo($values[0], $values[1]); } if ($mail->send()) { set_send_count($user); if (!empty($mail->SMTPDebug)) { echo '</pre>'; } return true; } else { // Trigger event for failing to send email. $event = \core\event\email_failed::create(array('context' => context_system::instance(), 'userid' => $from->id, 'relateduserid' => $user->id, 'other' => array('subject' => $subject, 'message' => $messagetext, 'errorinfo' => $mail->ErrorInfo))); $event->trigger(); if (CLI_SCRIPT) { mtrace('Error: lib/moodlelib.php email_to_user(): ' . $mail->ErrorInfo); } if (!empty($mail->SMTPDebug)) { echo '</pre>'; } return false; } }
/** * Function to be run periodically according to the scheduled task. * * Finds all posts that have yet to be mailed out, and mails them * out to all subscribers as well as other maintance tasks. * * NOTE: Since 2.7.2 this function is run by scheduled task rather * than standard cron. * * @todo MDL-44734 The function will be split up into seperate tasks. */ function forum_cron() { global $CFG, $USER, $DB, $PAGE; $site = get_site(); // The main renderers. $htmlout = $PAGE->get_renderer('mod_forum', 'email', 'htmlemail'); $textout = $PAGE->get_renderer('mod_forum', 'email', 'textemail'); $htmldigestfullout = $PAGE->get_renderer('mod_forum', 'emaildigestfull', 'htmlemail'); $textdigestfullout = $PAGE->get_renderer('mod_forum', 'emaildigestfull', 'textemail'); $htmldigestbasicout = $PAGE->get_renderer('mod_forum', 'emaildigestbasic', 'htmlemail'); $textdigestbasicout = $PAGE->get_renderer('mod_forum', 'emaildigestbasic', 'textemail'); // All users that are subscribed to any post that needs sending, // please increase $CFG->extramemorylimit on large sites that // send notifications to a large number of users. $users = array(); $userscount = 0; // Cached user counter - count($users) in PHP is horribly slow!!! // Status arrays. $mailcount = array(); $errorcount = array(); // caches $discussions = array(); $forums = array(); $courses = array(); $coursemodules = array(); $subscribedusers = array(); $messageinboundhandlers = array(); // Posts older than 2 days will not be mailed. This is to avoid the problem where // cron has not been running for a long time, and then suddenly people are flooded // with mail from the past few weeks or months $timenow = time(); $endtime = $timenow - $CFG->maxeditingtime; $starttime = $endtime - 48 * 3600; // Two days earlier // Get the list of forum subscriptions for per-user per-forum maildigest settings. $digestsset = $DB->get_recordset('forum_digests', null, '', 'id, userid, forum, maildigest'); $digests = array(); foreach ($digestsset as $thisrow) { if (!isset($digests[$thisrow->forum])) { $digests[$thisrow->forum] = array(); } $digests[$thisrow->forum][$thisrow->userid] = $thisrow->maildigest; } $digestsset->close(); // Create the generic messageinboundgenerator. $messageinboundgenerator = new \core\message\inbound\address_manager(); $messageinboundgenerator->set_handler('\\mod_forum\\message\\inbound\\reply_handler'); if ($posts = forum_get_unmailed_posts($starttime, $endtime, $timenow)) { // Mark them all now as being mailed. It's unlikely but possible there // might be an error later so that a post is NOT actually mailed out, // but since mail isn't crucial, we can accept this risk. Doing it now // prevents the risk of duplicated mails, which is a worse problem. if (!forum_mark_old_posts_as_mailed($endtime)) { mtrace('Errors occurred while trying to mark some posts as being mailed.'); return false; // Don't continue trying to mail them, in case we are in a cron loop } // checking post validity, and adding users to loop through later foreach ($posts as $pid => $post) { $discussionid = $post->discussion; if (!isset($discussions[$discussionid])) { if ($discussion = $DB->get_record('forum_discussions', array('id' => $post->discussion))) { $discussions[$discussionid] = $discussion; \mod_forum\subscriptions::fill_subscription_cache($discussion->forum); \mod_forum\subscriptions::fill_discussion_subscription_cache($discussion->forum); } else { mtrace('Could not find discussion ' . $discussionid); unset($posts[$pid]); continue; } } $forumid = $discussions[$discussionid]->forum; if (!isset($forums[$forumid])) { if ($forum = $DB->get_record('forum', array('id' => $forumid))) { $forums[$forumid] = $forum; } else { mtrace('Could not find forum ' . $forumid); unset($posts[$pid]); continue; } } $courseid = $forums[$forumid]->course; if (!isset($courses[$courseid])) { if ($course = $DB->get_record('course', array('id' => $courseid))) { $courses[$courseid] = $course; } else { mtrace('Could not find course ' . $courseid); unset($posts[$pid]); continue; } } if (!isset($coursemodules[$forumid])) { if ($cm = get_coursemodule_from_instance('forum', $forumid, $courseid)) { $coursemodules[$forumid] = $cm; } else { mtrace('Could not find course module for forum ' . $forumid); unset($posts[$pid]); continue; } } // Save the Inbound Message datakey here to reduce DB queries later. $messageinboundgenerator->set_data($pid); $messageinboundhandlers[$pid] = $messageinboundgenerator->fetch_data_key(); // Caching subscribed users of each forum. if (!isset($subscribedusers[$forumid])) { $modcontext = context_module::instance($coursemodules[$forumid]->id); if ($subusers = \mod_forum\subscriptions::fetch_subscribed_users($forums[$forumid], 0, $modcontext, 'u.*', true)) { foreach ($subusers as $postuser) { // this user is subscribed to this forum $subscribedusers[$forumid][$postuser->id] = $postuser->id; $userscount++; if ($userscount > FORUM_CRON_USER_CACHE) { // Store minimal user info. $minuser = new stdClass(); $minuser->id = $postuser->id; $users[$postuser->id] = $minuser; } else { // Cache full user record. forum_cron_minimise_user_record($postuser); $users[$postuser->id] = $postuser; } } // Release memory. unset($subusers); unset($postuser); } } $mailcount[$pid] = 0; $errorcount[$pid] = 0; } } if ($users && $posts) { foreach ($users as $userto) { // Terminate if processing of any account takes longer than 2 minutes. core_php_time_limit::raise(120); mtrace('Processing user ' . $userto->id); // Init user caches - we keep the cache for one cycle only, otherwise it could consume too much memory. if (isset($userto->username)) { $userto = clone $userto; } else { $userto = $DB->get_record('user', array('id' => $userto->id)); forum_cron_minimise_user_record($userto); } $userto->viewfullnames = array(); $userto->canpost = array(); $userto->markposts = array(); // Setup this user so that the capabilities are cached, and environment matches receiving user. cron_setup_user($userto); // Reset the caches. foreach ($coursemodules as $forumid => $unused) { $coursemodules[$forumid]->cache = new stdClass(); $coursemodules[$forumid]->cache->caps = array(); unset($coursemodules[$forumid]->uservisible); } foreach ($posts as $pid => $post) { $discussion = $discussions[$post->discussion]; $forum = $forums[$discussion->forum]; $course = $courses[$forum->course]; $cm =& $coursemodules[$forum->id]; // Do some checks to see if we can bail out now. // Only active enrolled users are in the list of subscribers. // This does not necessarily mean that the user is subscribed to the forum or to the discussion though. if (!isset($subscribedusers[$forum->id][$userto->id])) { // The user does not subscribe to this forum. continue; } if (!\mod_forum\subscriptions::is_subscribed($userto->id, $forum, $post->discussion, $coursemodules[$forum->id])) { // The user does not subscribe to this forum, or to this specific discussion. continue; } if ($subscriptiontime = \mod_forum\subscriptions::fetch_discussion_subscription($forum->id, $userto->id)) { // Skip posts if the user subscribed to the discussion after it was created. if (isset($subscriptiontime[$post->discussion]) && $subscriptiontime[$post->discussion] > $post->created) { continue; } } // Don't send email if the forum is Q&A and the user has not posted. // Initial topics are still mailed. if ($forum->type == 'qanda' && !forum_get_user_posted_time($discussion->id, $userto->id) && $pid != $discussion->firstpost) { mtrace('Did not email ' . $userto->id . ' because user has not posted in discussion'); continue; } // Get info about the sending user. if (array_key_exists($post->userid, $users)) { // We might know the user already. $userfrom = $users[$post->userid]; if (!isset($userfrom->idnumber)) { // Minimalised user info, fetch full record. $userfrom = $DB->get_record('user', array('id' => $userfrom->id)); forum_cron_minimise_user_record($userfrom); } } else { if ($userfrom = $DB->get_record('user', array('id' => $post->userid))) { forum_cron_minimise_user_record($userfrom); // Fetch only once if possible, we can add it to user list, it will be skipped anyway. if ($userscount <= FORUM_CRON_USER_CACHE) { $userscount++; $users[$userfrom->id] = $userfrom; } } else { mtrace('Could not find user ' . $post->userid . ', author of post ' . $post->id . '. Unable to send message.'); continue; } } // Note: If we want to check that userto and userfrom are not the same person this is probably the spot to do it. // Setup global $COURSE properly - needed for roles and languages. cron_setup_user($userto, $course); // Fill caches. if (!isset($userto->viewfullnames[$forum->id])) { $modcontext = context_module::instance($cm->id); $userto->viewfullnames[$forum->id] = has_capability('moodle/site:viewfullnames', $modcontext); } if (!isset($userto->canpost[$discussion->id])) { $modcontext = context_module::instance($cm->id); $userto->canpost[$discussion->id] = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext); } if (!isset($userfrom->groups[$forum->id])) { if (!isset($userfrom->groups)) { $userfrom->groups = array(); if (isset($users[$userfrom->id])) { $users[$userfrom->id]->groups = array(); } } $userfrom->groups[$forum->id] = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid); if (isset($users[$userfrom->id])) { $users[$userfrom->id]->groups[$forum->id] = $userfrom->groups[$forum->id]; } } // Make sure groups allow this user to see this email. if ($discussion->groupid > 0 and $groupmode = groups_get_activity_groupmode($cm, $course)) { // Groups are being used. if (!groups_group_exists($discussion->groupid)) { // Can't find group - be safe and don't this message. continue; } if (!groups_is_member($discussion->groupid) and !has_capability('moodle/site:accessallgroups', $modcontext)) { // Do not send posts from other groups when in SEPARATEGROUPS or VISIBLEGROUPS. continue; } } // Make sure we're allowed to see the post. if (!forum_user_can_see_post($forum, $discussion, $post, null, $cm)) { mtrace('User ' . $userto->id . ' can not see ' . $post->id . '. Not sending message.'); continue; } // OK so we need to send the email. // Does the user want this post in a digest? If so postpone it for now. $maildigest = forum_get_user_maildigest_bulk($digests, $userto, $forum->id); if ($maildigest > 0) { // This user wants the mails to be in digest form. $queue = new stdClass(); $queue->userid = $userto->id; $queue->discussionid = $discussion->id; $queue->postid = $post->id; $queue->timemodified = $post->created; $DB->insert_record('forum_queue', $queue); continue; } // Prepare to actually send the post now, and build up the content. $cleanforumname = str_replace('"', "'", strip_tags(format_string($forum->name))); $userfrom->customheaders = array('List-Id: "' . $cleanforumname . '" ' . generate_email_messageid('moodleforum' . $forum->id), 'List-Help: ' . $CFG->wwwroot . '/mod/forum/view.php?f=' . $forum->id, 'Message-ID: ' . forum_get_email_message_id($post->id, $userto->id), 'X-Course-Id: ' . $course->id, 'X-Course-Name: ' . format_string($course->fullname, true), 'Precedence: Bulk', 'X-Auto-Response-Suppress: All', 'Auto-Submitted: auto-generated'); $shortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id))); // Generate a reply-to address from using the Inbound Message handler. $replyaddress = null; if ($userto->canpost[$discussion->id] && array_key_exists($post->id, $messageinboundhandlers)) { $messageinboundgenerator->set_data($post->id, $messageinboundhandlers[$post->id]); $replyaddress = $messageinboundgenerator->generate($userto->id); } if (!isset($userto->canpost[$discussion->id])) { $canreply = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext); } else { $canreply = $userto->canpost[$discussion->id]; } $data = new \mod_forum\output\forum_post_email($course, $cm, $forum, $discussion, $post, $userfrom, $userto, $canreply); $userfrom->customheaders[] = sprintf('List-Unsubscribe: <%s>', $data->get_unsubscribediscussionlink()); if (!isset($userto->viewfullnames[$forum->id])) { $data->viewfullnames = has_capability('moodle/site:viewfullnames', $modcontext, $userto->id); } else { $data->viewfullnames = $userto->viewfullnames[$forum->id]; } // Not all of these variables are used in the default language // string but are made available to support custom subjects. $a = new stdClass(); $a->subject = $data->get_subject(); $a->forumname = $cleanforumname; $a->sitefullname = format_string($site->fullname); $a->siteshortname = format_string($site->shortname); $a->courseidnumber = $data->get_courseidnumber(); $a->coursefullname = $data->get_coursefullname(); $a->courseshortname = $data->get_coursename(); $postsubject = html_to_text(get_string('postmailsubject', 'forum', $a), 0); $rootid = forum_get_email_message_id($discussion->firstpost, $userto->id); if ($post->parent) { // This post is a reply, so add reply header (RFC 2822). $parentid = forum_get_email_message_id($post->parent, $userto->id); $userfrom->customheaders[] = "In-Reply-To: {$parentid}"; // If the post is deeply nested we also reference the parent message id and // the root message id (if different) to aid threading when parts of the email // conversation have been deleted (RFC1036). if ($post->parent != $discussion->firstpost) { $userfrom->customheaders[] = "References: {$rootid} {$parentid}"; } else { $userfrom->customheaders[] = "References: {$parentid}"; } } // MS Outlook / Office uses poorly documented and non standard headers, including // Thread-Topic which overrides the Subject and shouldn't contain Re: or Fwd: etc. $a->subject = $discussion->name; $threadtopic = html_to_text(get_string('postmailsubject', 'forum', $a), 0); $userfrom->customheaders[] = "Thread-Topic: {$threadtopic}"; $userfrom->customheaders[] = "Thread-Index: " . substr($rootid, 1, 28); // Send the post now! mtrace('Sending ', ''); $eventdata = new \core\message\message(); $eventdata->courseid = $course->id; $eventdata->component = 'mod_forum'; $eventdata->name = 'posts'; $eventdata->userfrom = $userfrom; $eventdata->userto = $userto; $eventdata->subject = $postsubject; $eventdata->fullmessage = $textout->render($data); $eventdata->fullmessageformat = FORMAT_PLAIN; $eventdata->fullmessagehtml = $htmlout->render($data); $eventdata->notification = 1; $eventdata->replyto = $replyaddress; if (!empty($replyaddress)) { // Add extra text to email messages if they can reply back. $textfooter = "\n\n" . get_string('replytopostbyemail', 'mod_forum'); $htmlfooter = html_writer::tag('p', get_string('replytopostbyemail', 'mod_forum')); $additionalcontent = array('fullmessage' => array('footer' => $textfooter), 'fullmessagehtml' => array('footer' => $htmlfooter)); $eventdata->set_additional_content('email', $additionalcontent); } $smallmessagestrings = new stdClass(); $smallmessagestrings->user = fullname($userfrom); $smallmessagestrings->forumname = "{$shortname}: " . format_string($forum->name, true) . ": " . $discussion->name; $smallmessagestrings->message = $post->message; // Make sure strings are in message recipients language. $eventdata->smallmessage = get_string_manager()->get_string('smallmessage', 'forum', $smallmessagestrings, $userto->lang); $contexturl = new moodle_url('/mod/forum/discuss.php', array('d' => $discussion->id), 'p' . $post->id); $eventdata->contexturl = $contexturl->out(); $eventdata->contexturlname = $discussion->name; $mailresult = message_send($eventdata); if (!$mailresult) { mtrace("Error: mod/forum/lib.php forum_cron(): Could not send out mail for id {$post->id} to user {$userto->id}" . " ({$userto->email}) .. not trying again."); $errorcount[$post->id]++; } else { $mailcount[$post->id]++; // Mark post as read if forum_usermarksread is set off. if (!$CFG->forum_usermarksread) { $userto->markposts[$post->id] = $post->id; } } mtrace('post ' . $post->id . ': ' . $post->subject); } // Mark processed posts as read. if (get_user_preferences('forum_markasreadonnotification', 1, $userto->id) == 1) { forum_tp_mark_posts_read($userto, $userto->markposts); } unset($userto); } } if ($posts) { foreach ($posts as $post) { mtrace($mailcount[$post->id] . " users were sent post {$post->id}, '{$post->subject}'"); if ($errorcount[$post->id]) { $DB->set_field('forum_posts', 'mailed', FORUM_MAILED_ERROR, array('id' => $post->id)); } } } // release some memory unset($subscribedusers); unset($mailcount); unset($errorcount); cron_setup_user(); $sitetimezone = core_date::get_server_timezone(); // Now see if there are any digest mails waiting to be sent, and if we should send them mtrace('Starting digest processing...'); core_php_time_limit::raise(300); // terminate if not able to fetch all digests in 5 minutes if (!isset($CFG->digestmailtimelast)) { // To catch the first time set_config('digestmailtimelast', 0); } $timenow = time(); $digesttime = usergetmidnight($timenow, $sitetimezone) + $CFG->digestmailtime * 3600; // Delete any really old ones (normally there shouldn't be any) $weekago = $timenow - 7 * 24 * 3600; $DB->delete_records_select('forum_queue', "timemodified < ?", array($weekago)); mtrace('Cleaned old digest records'); if ($CFG->digestmailtimelast < $digesttime and $timenow > $digesttime) { mtrace('Sending forum digests: ' . userdate($timenow, '', $sitetimezone)); $digestposts_rs = $DB->get_recordset_select('forum_queue', "timemodified < ?", array($digesttime)); if ($digestposts_rs->valid()) { // We have work to do $usermailcount = 0; //caches - reuse the those filled before too $discussionposts = array(); $userdiscussions = array(); foreach ($digestposts_rs as $digestpost) { if (!isset($posts[$digestpost->postid])) { if ($post = $DB->get_record('forum_posts', array('id' => $digestpost->postid))) { $posts[$digestpost->postid] = $post; } else { continue; } } $discussionid = $digestpost->discussionid; if (!isset($discussions[$discussionid])) { if ($discussion = $DB->get_record('forum_discussions', array('id' => $discussionid))) { $discussions[$discussionid] = $discussion; } else { continue; } } $forumid = $discussions[$discussionid]->forum; if (!isset($forums[$forumid])) { if ($forum = $DB->get_record('forum', array('id' => $forumid))) { $forums[$forumid] = $forum; } else { continue; } } $courseid = $forums[$forumid]->course; if (!isset($courses[$courseid])) { if ($course = $DB->get_record('course', array('id' => $courseid))) { $courses[$courseid] = $course; } else { continue; } } if (!isset($coursemodules[$forumid])) { if ($cm = get_coursemodule_from_instance('forum', $forumid, $courseid)) { $coursemodules[$forumid] = $cm; } else { continue; } } $userdiscussions[$digestpost->userid][$digestpost->discussionid] = $digestpost->discussionid; $discussionposts[$digestpost->discussionid][$digestpost->postid] = $digestpost->postid; } $digestposts_rs->close(); /// Finished iteration, let's close the resultset // Data collected, start sending out emails to each user foreach ($userdiscussions as $userid => $thesediscussions) { core_php_time_limit::raise(120); // terminate if processing of any account takes longer than 2 minutes cron_setup_user(); mtrace(get_string('processingdigest', 'forum', $userid), '... '); // First of all delete all the queue entries for this user $DB->delete_records_select('forum_queue', "userid = ? AND timemodified < ?", array($userid, $digesttime)); // Init user caches - we keep the cache for one cycle only, // otherwise it would unnecessarily consume memory. if (array_key_exists($userid, $users) and isset($users[$userid]->username)) { $userto = clone $users[$userid]; } else { $userto = $DB->get_record('user', array('id' => $userid)); forum_cron_minimise_user_record($userto); } $userto->viewfullnames = array(); $userto->canpost = array(); $userto->markposts = array(); // Override the language and timezone of the "current" user, so that // mail is customised for the receiver. cron_setup_user($userto); $postsubject = get_string('digestmailsubject', 'forum', format_string($site->shortname, true)); $headerdata = new stdClass(); $headerdata->sitename = format_string($site->fullname, true); $headerdata->userprefs = $CFG->wwwroot . '/user/forum.php?id=' . $userid . '&course=' . $site->id; $posttext = get_string('digestmailheader', 'forum', $headerdata) . "\n\n"; $headerdata->userprefs = '<a target="_blank" href="' . $headerdata->userprefs . '">' . get_string('digestmailprefs', 'forum') . '</a>'; $posthtml = '<p>' . get_string('digestmailheader', 'forum', $headerdata) . '</p>' . '<br /><hr size="1" noshade="noshade" />'; foreach ($thesediscussions as $discussionid) { core_php_time_limit::raise(120); // to be reset for each post $discussion = $discussions[$discussionid]; $forum = $forums[$discussion->forum]; $course = $courses[$forum->course]; $cm = $coursemodules[$forum->id]; //override language cron_setup_user($userto, $course); // Fill caches if (!isset($userto->viewfullnames[$forum->id])) { $modcontext = context_module::instance($cm->id); $userto->viewfullnames[$forum->id] = has_capability('moodle/site:viewfullnames', $modcontext); } if (!isset($userto->canpost[$discussion->id])) { $modcontext = context_module::instance($cm->id); $userto->canpost[$discussion->id] = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext); } $strforums = get_string('forums', 'forum'); $canunsubscribe = !\mod_forum\subscriptions::is_forcesubscribed($forum); $canreply = $userto->canpost[$discussion->id]; $shortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id))); $posttext .= "\n \n"; $posttext .= '====================================================================='; $posttext .= "\n \n"; $posttext .= "{$shortname} -> {$strforums} -> " . format_string($forum->name, true); if ($discussion->name != $forum->name) { $posttext .= " -> " . format_string($discussion->name, true); } $posttext .= "\n"; $posttext .= $CFG->wwwroot . '/mod/forum/discuss.php?d=' . $discussion->id; $posttext .= "\n"; $posthtml .= "<p><font face=\"sans-serif\">" . "<a target=\"_blank\" href=\"{$CFG->wwwroot}/course/view.php?id={$course->id}\">{$shortname}</a> -> " . "<a target=\"_blank\" href=\"{$CFG->wwwroot}/mod/forum/index.php?id={$course->id}\">{$strforums}</a> -> " . "<a target=\"_blank\" href=\"{$CFG->wwwroot}/mod/forum/view.php?f={$forum->id}\">" . format_string($forum->name, true) . "</a>"; if ($discussion->name == $forum->name) { $posthtml .= "</font></p>"; } else { $posthtml .= " -> <a target=\"_blank\" href=\"{$CFG->wwwroot}/mod/forum/discuss.php?d={$discussion->id}\">" . format_string($discussion->name, true) . "</a></font></p>"; } $posthtml .= '<p>'; $postsarray = $discussionposts[$discussionid]; sort($postsarray); $sentcount = 0; foreach ($postsarray as $postid) { $post = $posts[$postid]; if (array_key_exists($post->userid, $users)) { // we might know him/her already $userfrom = $users[$post->userid]; if (!isset($userfrom->idnumber)) { $userfrom = $DB->get_record('user', array('id' => $userfrom->id)); forum_cron_minimise_user_record($userfrom); } } else { if ($userfrom = $DB->get_record('user', array('id' => $post->userid))) { forum_cron_minimise_user_record($userfrom); if ($userscount <= FORUM_CRON_USER_CACHE) { $userscount++; $users[$userfrom->id] = $userfrom; } } else { mtrace('Could not find user ' . $post->userid); continue; } } if (!isset($userfrom->groups[$forum->id])) { if (!isset($userfrom->groups)) { $userfrom->groups = array(); if (isset($users[$userfrom->id])) { $users[$userfrom->id]->groups = array(); } } $userfrom->groups[$forum->id] = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid); if (isset($users[$userfrom->id])) { $users[$userfrom->id]->groups[$forum->id] = $userfrom->groups[$forum->id]; } } // Headers to help prevent auto-responders. $userfrom->customheaders = array("Precedence: Bulk", 'X-Auto-Response-Suppress: All', 'Auto-Submitted: auto-generated'); $maildigest = forum_get_user_maildigest_bulk($digests, $userto, $forum->id); if (!isset($userto->canpost[$discussion->id])) { $canreply = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext); } else { $canreply = $userto->canpost[$discussion->id]; } $data = new \mod_forum\output\forum_post_email($course, $cm, $forum, $discussion, $post, $userfrom, $userto, $canreply); if (!isset($userto->viewfullnames[$forum->id])) { $data->viewfullnames = has_capability('moodle/site:viewfullnames', $modcontext, $userto->id); } else { $data->viewfullnames = $userto->viewfullnames[$forum->id]; } if ($maildigest == 2) { // Subjects and link only. $posttext .= $textdigestbasicout->render($data); $posthtml .= $htmldigestbasicout->render($data); } else { // The full treatment. $posttext .= $textdigestfullout->render($data); $posthtml .= $htmldigestfullout->render($data); // Create an array of postid's for this user to mark as read. if (!$CFG->forum_usermarksread) { $userto->markposts[$post->id] = $post->id; } } $sentcount++; } $footerlinks = array(); if ($canunsubscribe) { $footerlinks[] = "<a href=\"{$CFG->wwwroot}/mod/forum/subscribe.php?id={$forum->id}\">" . get_string("unsubscribe", "forum") . "</a>"; } else { $footerlinks[] = get_string("everyoneissubscribed", "forum"); } $footerlinks[] = "<a href='{$CFG->wwwroot}/mod/forum/index.php?id={$forum->course}'>" . get_string("digestmailpost", "forum") . '</a>'; $posthtml .= "\n<div class='mdl-right'><font size=\"1\">" . implode(' ', $footerlinks) . '</font></div>'; $posthtml .= '<hr size="1" noshade="noshade" /></p>'; } if (empty($userto->mailformat) || $userto->mailformat != 1) { // This user DOESN'T want to receive HTML $posthtml = ''; } $eventdata = new \core\message\message(); $eventdata->courseid = SITEID; $eventdata->component = 'mod_forum'; $eventdata->name = 'digests'; $eventdata->userfrom = core_user::get_noreply_user(); $eventdata->userto = $userto; $eventdata->subject = $postsubject; $eventdata->fullmessage = $posttext; $eventdata->fullmessageformat = FORMAT_PLAIN; $eventdata->fullmessagehtml = $posthtml; $eventdata->notification = 1; $eventdata->smallmessage = get_string('smallmessagedigest', 'forum', $sentcount); $mailresult = message_send($eventdata); if (!$mailresult) { mtrace("ERROR: mod/forum/cron.php: Could not send out digest mail to user {$userto->id} " . "({$userto->email})... not trying again."); } else { mtrace("success."); $usermailcount++; // Mark post as read if forum_usermarksread is set off if (get_user_preferences('forum_markasreadonnotification', 1, $userto->id) == 1) { forum_tp_mark_posts_read($userto, $userto->markposts); } } } } /// We have finishied all digest emails, update $CFG->digestmailtimelast set_config('digestmailtimelast', $timenow); } cron_setup_user(); if (!empty($usermailcount)) { mtrace(get_string('digestsentusers', 'forum', $usermailcount)); } if (!empty($CFG->forum_lastreadclean)) { $timenow = time(); if ($CFG->forum_lastreadclean + 24 * 3600 < $timenow) { set_config('forum_lastreadclean', $timenow); mtrace('Removing old forum read tracking info...'); forum_tp_clean_read_records(); } } else { set_config('forum_lastreadclean', time()); } return true; }
/** * Test email message id generation * * @dataProvider generate_email_messageid_provider * * @param string $wwwroot The wwwroot * @param array $msgids An array of msgid local parts and the final result */ public function test_generate_email_messageid($wwwroot, $msgids) { global $CFG; $this->resetAfterTest(); $CFG->wwwroot = $wwwroot; foreach ($msgids as $local => $final) { $this->assertEquals($final, generate_email_messageid($local)); } }