# Module of TWiki Enterprise Collaboration Platform, http://TWiki.org/ # # Copyright (C) 2004 Wind River Systems Inc. # Copyright (C) 1999-2006 TWiki Contributors. # All Rights Reserved. TWiki Contributors # are listed in the AUTHORS file in the root of this distribution. # NOTE: Please extend that file, not this notice. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. For # more details read LICENSE in the root of this distribution. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # # As per the GPL, removal of this notice is prohibited. use strict; use TWiki; use TWiki::Contrib::MailerContrib::WebNotify; use TWiki::Contrib::MailerContrib::Change; use TWiki::Contrib::MailerContrib::UpData; =pod ---+ package TWiki::Contrib::Mailer Package of support for extended WebNotify notification, supporting per-topic notification and notification of changes to children. Also supported is a simple API that can be used to change the WebNotify topic from other code. =cut package TWiki::Contrib::Mailer; use URI; use vars qw ( $VERSION $RELEASE $verbose ); # This should always be $Rev: 11662 $ so that TWiki can determine the checked-in # status of the plugin. It is used by the build automation tools, so # you should leave it alone. $VERSION = '$Rev: 11662 $'; # This is a free-form string you can use to "name" your own plugin version. # It is *not* used by the build automation tools, but is reported as part # of the version number in PLUGINDESCRIPTIONS. $RELEASE = 'Dakar'; =pod ---++ StaticMethod mailNotify($webs, $session, $verbose) * =$webs= - filter list of names webs to process. Wildcards (*) may be used. * =$session= - optional session object. If not given, will use a local object. * =$verbose= - true to get verbose (debug) output Main entry point. Process the WebNotify topics in each web and generate and issue notification mails. Designed to be invoked from the command line; should only be called by =mailnotify= scripts. =cut sub mailNotify { my( $webs, $twiki, $noisy ) = @_; $verbose = $noisy; my $webstr; if ( defined( $webs )) { $webstr = join( '|', @$webs ); } $webstr = '*' unless ( $webstr ); $webstr =~ s/\*/\.\*/g; if (!defined $twiki) { $twiki = new TWiki(); } $twiki->enterContext( 'command_line'); # absolute URL context for email generation $twiki->enterContext( 'absolute_urls' ); my $report = ''; foreach my $web ( grep( /$webstr/, $twiki->{store}->getListOfWebs( 'user ') )) { $report .= _processWeb( $twiki, $web ); } $twiki->leaveContext( 'absolute_urls' ); return $report; } # PRIVATE: Read the webnotify, and notify changes sub _processWeb { my( $twiki, $web) = @_; if( ! $twiki->{store}->webExists( $web ) ) { print STDERR "**** ERROR mailnotifier cannot find web $web\n"; return ''; } print "Processing $web\n" if $verbose; my $report = ''; # Read the webnotify and load subscriptions my $wn = new TWiki::Contrib::MailerContrib::WebNotify( $twiki, $web, $TWiki::cfg{NotifyTopicName} ); if ( $wn->isEmpty() ) { print "\t$web has no subscribers\n" if $verbose; } else { # create a DB object for parent pointers print $wn->stringify() if $verbose; my $db = new TWiki::Contrib::MailerContrib::UpData( $twiki, $web ); $report .= _processSubscriptions( $twiki, $web, $wn, $db ); } return $report; } sub _processSubscriptions { my ( $twiki, $web, $notify, $db ) = @_; my $timeOfLastNotify = $twiki->{store}->readMetaData( $web, 'mailnotify' ) || 0; my $timeOfLastChange = ''; if ( $verbose ) { print "\tLast notification was at " . TWiki::Time::formatTime( $timeOfLastNotify ). "\n"; } # Hash indexed on email address, each entry contains a hash # of topics already processed in the change set for this email. # Each subhash maps the topic name to the index of the change # record for this topic in the array of Change objects for this # email in %changeset. my %seenset; # Hash indexed on email address, each entry contains an array # indexed by the index stored in %seenSet. Each entry in the array # is a ref to a Change object. my %changeset; # Hash indexed on topic name, mapping to email address, used to # record simple newsletter subscriptions. my %allSet; my $changes = $twiki->{store}->readMetaData( $web, 'changes' ); unless ( $changes ) { print "No changes\n" if ( $verbose ); return ''; } foreach my $line ( reverse split( /\n/, $changes ) ) { # Parse lines from .changes: # # WebHome FredBloggs 1014591347 21 next if $line =~ /minor$/; my ($topicName, $userName, $changeTime, $revision) = split( /\t/, $line); next unless $twiki->{store}->topicExists( $web, $topicName ); $timeOfLastChange = $changeTime unless( $timeOfLastChange ); # found last interesting change? last if( $changeTime <= $timeOfLastNotify ); print "\tFound change to $topicName\n" if ( $verbose ); # Formulate a change record, irrespective of # whether any subscriber is interested my $change = new TWiki::Contrib::MailerContrib::Change ( $twiki, $web, $topicName, $userName, $changeTime, $revision ); # Now, find subscribers to this change and extend the change set $notify->processChange( $change, $db, \%changeset, \%seenset, \%allSet ); } # For each topic, see if there's a compulsory subscription independent # of the time since last notify foreach my $topic ($twiki->{store}->getTopicNames($web)) { $notify->processCompulsory( $topic, $db, \%allSet ); } # Now generate emails for each recipient my $report = _sendChangesMails( $twiki, $web, \%changeset, TWiki::Time::formatTime($timeOfLastNotify) ); $report .= _sendNewsletterMails( $twiki, $web, \%allSet); $twiki->{store}->saveMetaData( $web, 'mailnotify', $timeOfLastChange ); return $report; } # PRIVATE generate and send an email for each user sub _sendChangesMails { my ( $twiki, $web, $changeset, $lastTime ) = @_; my $report = ''; my $skin = $twiki->getSkin(); my $template = $twiki->{templates}->readTemplate( 'mailnotify', $skin ); my $homeTopic = $TWiki::cfg{HomeTopicName}; my $before_html = $twiki->{templates}->expandTemplate( 'HTML:before' ); my $middle_html = $twiki->{templates}->expandTemplate( 'HTML:middle' ); my $after_html = $twiki->{templates}->expandTemplate( 'HTML:after' ); my $before_plain = $twiki->{templates}->expandTemplate( 'PLAIN:before' ); my $middle_plain = $twiki->{templates}->expandTemplate( 'PLAIN:middle' ); my $after_plain = $twiki->{templates}->expandTemplate( 'PLAIN:after' ); my $mailtmpl = $twiki->{templates}->expandTemplate( 'MailNotifyBody' ); $mailtmpl = $twiki->handleCommonTags( $mailtmpl, $web, $homeTopic ); if( $TWiki::cfg{RemoveImgInMailnotify} ) { # change images to [alt] text if there, else remove image $mailtmpl =~ s/]*\balt=\"([^\"]+)[^>]*>/[$1]/goi; $mailtmpl =~ s/]>//goi; } my $sentMails = 0; foreach my $email ( keys %{$changeset} ) { my $html = ''; my $plain = ''; foreach my $change (sort { $a->{TIME} cmp $b->{TIME} } @{$changeset->{$email}} ) { $html .= $change->expandHTML( $middle_html ); $plain .= $change->expandPlain( $middle_plain ); } $plain =~ s/\($TWiki::cfg{UsersWebName}\./\(/go; my $mail = $mailtmpl; $mail =~ s/%EMAILTO%/$email/go; $mail =~ s/%HTML_TEXT%/$before_html$html$after_html/go; $mail =~ s/%PLAIN_TEXT%/$before_plain$plain$after_plain/go; $mail =~ s/%LASTDATE%/$lastTime/geo; $mail = $twiki->handleCommonTags( $mail, $web, $homeTopic ); my $base = $TWiki::cfg{DefaultUrlHost} . $TWiki::cfg{ScriptUrlPath}; $mail =~ s/(href=\")([^"]+)/$1.relativeURL($base,$2)/goei; $mail =~ s/(action=\")([^"]+)/$1.relativeURL($base,$2)/goei; # remove and tags $mail =~ s/( ?) *<\/?(nop|noautolink)\/?>\n?/$1/gois; my $error = $twiki->{net}->sendEmail( $mail, 5 ); if ($error) { print STDERR "Error sending mail: $error\n"; $report .= $error."\n"; } else { $sentMails++; } } $report .= "\t$sentMails change notifications\n"; return $report; } sub relativeURL { my( $base, $link ) = @_; return URI->new_abs( $link, URI->new($base) )->as_string; } sub _sendNewsletterMails { my ($twiki, $web, $allSet) = @_; my $report = ''; foreach my $topic (keys %$allSet) { $report .= _sendNewsletterMail( $twiki, $web, $topic, $allSet->{$topic}); } return $report; } sub _sendNewsletterMail { my ($twiki, $web, $topic, $emails) = @_; my $wikiName = $twiki->{user}->wikiName(); # SMELL: this code is almost identical to PublishContrib # Read topic data. my ($meta, $text) = TWiki::Func::readTopic( $web, $topic ); # tell the session what topic we are currently rendering so the # contexts are correct $twiki->{topicName} = $topic; $twiki->{webName} = $web; # SMELL: need a new prefs object for each topic $twiki->{prefs} = new TWiki::Prefs($twiki); my $prefs = $twiki->{prefs}->pushPreferences( $TWiki::cfg{SystemWebName}, $TWiki::cfg{SitePrefsTopicName}, 'DEFAULT' ); # Then local site prefs if( $TWiki::cfg{LocalSitePreferences} ) { my( $lweb, $ltopic ) = $twiki->normalizeWebTopicName( undef, $TWiki::cfg{LocalSitePreferences} ); $twiki->{prefs}->pushPreferences( $lweb, $ltopic, 'SITE' ); } # Get individual user preferences $twiki->{prefs}->pushPreferences( $TWiki::cfg{UsersWebName}, $wikiName, 'USER '.$wikiName); # and web preferences $twiki->{prefs}->pushWebPreferences($web); $twiki->{prefs}->pushPreferences($web, $topic, 'TOPIC'); $twiki->{prefs}->pushPreferenceValues( 'SESSION', $twiki->{client}->getSessionValues()) if $twiki->{client}; $twiki->enterContext( 'can_render_meta', $meta ); # Get the skin for this topic my $skin = $twiki->getSkin(); $twiki->{templates}->readTemplate( 'newsletter', $skin ); my $header = $twiki->{templates}->expandTemplate( 'NEWS:header' ); my $body = $twiki->{templates}->expandTemplate( 'NEWS:body' ); my $footer = $twiki->{templates}->expandTemplate( 'NEWS:footer' ); my ($revdate, $revuser, $maxrev); ($revdate, $revuser, $maxrev) = $meta->getRevisionInfo(); $revuser = $revuser->wikiName(); # Handle standard formatting. $body =~ s/%TEXT%/$text/g; # Don't render the header, it is preformatted $header = TWiki::Func::expandCommonVariables($header, $topic, $web); my $tmpl = "$body\n$footer"; $tmpl = TWiki::Func::expandCommonVariables($tmpl, $topic, $web); $tmpl = TWiki::Func::renderText($tmpl, "", $meta); $tmpl = "$header$tmpl"; # REFACTOR OPPORTUNITY: stop factor me into getTWikiRendering() # SMELL: this code is identical to PublishContrib! # New tags my $newTmpl = ''; my $tagSeen = 0; my $publish = 1; foreach my $s ( split( /(%STARTPUBLISH%|%STOPPUBLISH%)/, $tmpl )) { if( $s eq '%STARTPUBLISH%' ) { $publish = 1; $newTmpl = '' unless( $tagSeen ); $tagSeen = 1; } elsif( $s eq '%STOPPUBLISH%' ) { $publish = 0; $tagSeen = 1; } elsif( $publish ) { $newTmpl .= $s; } } $tmpl = $newTmpl; $tmpl =~ s/.*?<\/nopublish>//gs; $tmpl =~ s/%MAXREV%/$maxrev/g; $tmpl =~ s/%CURRREV%/$maxrev/g; $tmpl =~ s/%REVTITLE%//g; $tmpl =~ s|( ?) *\n?|$1|gois; # Remove tag $tmpl =~ s/]+\/>//; # Remove ... tag $tmpl =~ s/]+>.*?<\/base>//; # Rewrite absolute URLs my $base = $TWiki::cfg{DefaultUrlHost} . $TWiki::cfg{ScriptUrlPath}; $tmpl =~ s/(href=\")([^"]+)/$1.relativeURL($base,$2)/goei; $tmpl =~ s/(action=\")([^"]+)/$1.relativeURL($base,$2)/goei; my $report = ''; my $sentMails = 0; my %targets = map { $_ => 1 } @$emails; foreach my $email ( keys %targets ) { my $mail = $tmpl; $mail =~ s/%EMAILTO%/$email/go; my $base = $TWiki::cfg{DefaultUrlHost} . $TWiki::cfg{ScriptUrlPath}; $mail =~ s/(href=\")([^"]+)/$1.relativeURL($base,$2)/goei; $mail =~ s/(action=\")([^"]+)/$1.relativeURL($base,$2)/goei; # remove and tags $mail =~ s/( ?) *<\/?(nop|noautolink)\/?>\n?/$1/gois; my $error = $twiki->{net}->sendEmail( $mail, 5 ); if ($error) { print STDERR "Error sending mail: $error\n"; $report .= $error."\n"; } else { $sentMails++; } } $report .= "\t$sentMails newsletters\n"; return $report; } 1;