package CGI::Application::MailPage; use strict; use CGI::Application; use File::Spec; use HTML::Template; use HTML::TreeBuilder; use HTTP::Date; use MIME::Entity; use Mail::Header; use Mail::Internet; use Net::SMTP; use Text::Format; use vars qw($VERSION @ISA); $VERSION = '1.0'; @ISA = qw(CGI::Application); sub setup { my $self = shift; $self->start_mode('show_form'); $self->mode_param('rm'); $self->run_modes( show_form => \&show_form, send_mail => \&send_mail, ); # make sure we have required params die "You must set PARAMS => { document_root => '/path' } in your MailPage stub!" unless defined $self->param('document_root'); die "You must set PARAMS => { smtp_server => 'your.smtp.server' } in your MailPage stub!" unless defined $self->param('smtp_server'); } sub show_form { my $self = shift; my $alert = shift; my $query = $self->query; my $page = $query->param('page'); if (not defined $page) { unless($self->param('use_page_param')) { $page = $query->referer(); return "Sorry, I can't tell what page you want to send. You need to be using either Netscape 4 or Internet Explorer 4 (or newer) to use this feature. Please upgrade your browser and try again!" unless defined $page; } else { die "no value for page param!" unless defined $page; } } my $template; if ($self->param('form_template')) { $template = HTML::Template->new(filename => $self->param('form_template'), cache => 1, associate => $query); } else { $template = HTML::Template->new(filename => 'CGI/Application/MailPage/form.tmpl', path => [@INC], cache => 1, associate => $query); } $template->param(PAGE => $page); $template->param(SUBJECT => $query->param('subject') || $self->param('email_subject') || ''); $template->param(FORMAT_SELECTOR => $query->popup_menu(-name => 'format', '-values' => ['both_attachment', 'html','html_attachment', 'text', 'text_attachment', 'url'], -labels => { url => 'Just A Link', html => 'Full HTML', html_attachment => 'Full HTML as an Attachment', text => 'Plain Text', text_attachment => 'Plain Text as an Attachment', both_attachment => 'Both Text and Full HTML as Attachments', }, -default => 'both_attachment', )); $template->param(ALERT => $alert) if defined $alert; return $template->output(); } sub send_mail { my $self = shift; my $query = $self->query; # check parameters my $name = $query->param('name'); die "Missing parameter assignment for \$name!" unless defined($name); my $from_email = $query->param('from_email'); die "Missing parameter assignment for \$from_email!" unless defined($from_email); my $to_emails = $query->param('to_emails'); die "Missing parameter assignment for \$to_emails!" unless defined($to_emails); my $note = $query->param('note'); die "Missing parameter assignment for \$note!" unless defined($note); my $format = $query->param('format'); die "Missing parameter assignment for \$format!" unless defined($format); my $subject = $query->param('subject'); die "Missing parameter assignment for \$subject!" unless defined($subject); my $page = $query->param('page'); die "Missing parameter assignment for \$page!" unless defined($page); return $self->show_form("Please fill in your name in the form below.") unless length $name; return $self->show_form("Please fill in your email address in the form below.") unless length $from_email; return $self->show_form("Please fill in your friends' email addresses in the form below.") unless length $to_emails; return $self->show_form("Please enter a Subject for the email in the form below.") unless length $subject; # check from_email return $self->show_form("Your email address is invalid - it should look like name\@host.com.") unless $from_email =~ /^[-\w\.]+\@[-\w\.]+$/; # parse out to_emails my @to_emails; foreach (split(/[\s,]+/, $to_emails)) { next unless length $_; return $self->show_form("One of your friend's email addresses is invalid - \"$_\" - it should look like name\@host.com.") unless /^[-\w\.]+\@[-\w\.]+$/; push(@to_emails, $_); } return $self->show_form("Please fill in your friends' email addresses in the form below.") unless @to_emails; # find the HTML file to open my $filename = $self->_find_html_file($page); die "Unable to find file $filename for page $page (might be empty or unreadable): $!" unless -e $filename and -r _ and -s _; my ($vol, $dir, $file) = File::Spec->splitpath($filename); my $base_url = $page; $base_url =~ s/\Q$file\E//; # if file is empty, assume index.html if (not defined $file or not length $file) { $file = 'index.html'; $filename .= '/index.html'; } my ($base, $ext) = $file =~ /(.*)\.([^\.]+)$/; # open the email template my $template; if ($self->param('email_template')) { $template = HTML::Template->new(filename => $self->param('email_template'), associate => $query, cache => 1); } else { $template = HTML::Template->new(filename => 'CGI/Application/MailPage/email.tmpl', associate => $query, path => \@INC, cache => 1); } # $msg will end up with either a Mail::Internet or MIME::Entity # object. my $msg; # are we doing attachments? if (index($format, '_attachment') != -1) { # open up a MIME::Entity for our msg $msg = MIME::Entity->build( Type => "multipart/mixed", From => "$name <$from_email>", 'Reply-To' => "$name <$from_email>", To => \@to_emails, Subject => $subject, Date => HTTP::Date::time2str(time()), ); $msg->attach(Data => $template->output); # attach the straight HTML if requested if ($format =~ /^(both|html)/) { my $buffer = ""; if ($self->param('read_file_callback')) { my $callback = $self->param('read_file_callback'); $buffer = $callback->($filename); } else { open(HTML, $filename) or die "Can't open $filename : $!"; while(read(HTML, $buffer, 10240, length($buffer))) {} close(HTML); } # add tag in $buffer =~ s/(<\s*[Hh][Ee][Aa][Dd].*?>)/$1\n\n/; $msg->attach(Data => $buffer, Type => 'text/html', Filename => $base . '.html', ); } # attach text translation if ($format =~ /^(both|text)/) { $msg->attach(Data => $self->_html2text($filename), Type => 'text/plain', Filename => $base . '.txt', ); } } else { # non attachment mail my $header = Mail::Header->new(); $header->add(From => "$name <$from_email>"); $header->add('Reply-To' => "$name <$from_email>"); $header->add(To => join(', ', @to_emails)); $header->add(Subject => $subject); $header->add(Date => HTTP::Date::time2str(time())); my @lines; push(@lines, $template->output()); if ($format =~ /^(both|text)/) { push(@lines, "\n---\n\n"); push(@lines, $self->_html2text($filename)); } if ($format =~ /^(both|html)/) { push(@lines, "\n---\n\n"); if ($self->param('read_file_callback')) { my $callback = $self->param('read_file_callback'); my $buffer = $callback->($filename); push(@lines, split("\n", $buffer)); } else { open(HTML, $filename) or die "Can't open $filename : $!"; push(@lines, ); close(HTML); } } if ($format =~ /url/) { push(@lines, "\n$page"); } $msg = Mail::Internet->new([], Header => $header, Body => \@lines); die "Unable to create Mail::Internet object!" unless defined $msg; } # send the message using SMTP - other methods can be added later unless($self->param('dump_mail')) { my $smtp = Net::SMTP->new($self->param('smtp_server')); die "Unable to connect to SMTP server ".$self->param('smtp_server')." : $!" unless defined $smtp and UNIVERSAL::isa($smtp,'Net::SMTP'); $smtp->debug(1) if $self->param('smtp_debug'); $smtp->mail("$name <$from_email>"); foreach (@to_emails) { $smtp->to($_); } $smtp->data(); $smtp->datasend($msg->as_string()); $smtp->dataend(); $smtp->quit(); } else { # debuging hook for test.pl my $mailref = $self->param('dump_mail'); $$mailref = $msg->as_string(); die "Mail Dumped"; } # all done return $self->show_thanks; } sub show_thanks { my $self = shift; my $query = $self->query; my $page = $query->param('page'); my $template; if ($self->param('thanks_template')) { $template = HTML::Template->new(filename => $self->param('thanks_template'), cache => 1); } else { $template = HTML::Template->new(filename => 'CGI/Application/MailPage/thanks.tmpl', path => [@INC], cache => 1); } $template->param(PAGE => $page); return $template->output(); } sub _find_html_file { my $self = shift; my $url = shift; # if it doesn't start with http, its invalid die "Invalid page url: $url" unless $url =~ m!^https?://([-\w\.]+)/(.*)!; my $host = $1; my $path = $2; # if the path starts with a ~user thing, remove it $path =~ s!~[^/]+/!!; # append it to document_root and return it return File::Spec->join($self->param('document_root'), $path); } # takes an html file and returns text. This code was taken and # modified from html2text.pl by Ave Wrigley. I don't really # understand most of it, but it seems to work well. #-------------------------------------------------------------------------- # # prefixes to convert tags into - some are converted bachk to Text::Format # formatting later # #-------------------------------------------------------------------------- my %prefix = ( 'li' => '* ', 'dt' => '+ ', 'dd' => '- ', ); my %underline = ( 'h1' => '=', 'h2' => '-', 'h3' => '-', 'h4' => '-', 'h5' => '-', 'h6' => '-', ); my @heading_number = ( 0, 0, 0, 0, 0, 0 ); sub _html2text { my $self = shift; my $filename = shift; my $html_tree = new HTML::TreeBuilder; my $text_formatter = new Text::Format; $text_formatter->firstIndent( 0 ); my $result = ""; #---------------------------------------------------------------------- # # get_text - get all the text under a node # #---------------------------------------------------------------------- sub get_text { my $this = shift; my $text = ''; # iterate though my children ... return unless defined $this->content; for my $child ( @{ $this->content } ) { # if the child is also non-text ... if ( ref( $child ) ) { # traverse it ... $child->traverse( # traveral callback sub { my( $node, $startflag, $depth ) = @_; # only visit once return 0 unless $startflag; # if it is non-text ... if ( ref( $node ) ) { # recurse get_text $text .= get_text( $node ); } # if it is text else { # add it to $text $text .= $node if $node =~ /\S/; } return 0; }, 0 ); } # if it is text else { # add it to $text $text .= $child if $child =~ /\S/; } } return $text; } #-------------------------------------------------------------------------- # # get_paragraphs - routine for generating an array of paras from a given node # #-------------------------------------------------------------------------- sub get_paragraphs { my $this = shift; # array to save paragraphs in my @paras = (); # avoid -w warning for .= operation on undefined $paras[ 0 ] = ''; # iterate though my children ... for my $child ( @{ $this->content } ) { # if the child is also non-text ... if ( ref( $child ) ) { # traverse it ... $child->traverse( # traveral callback sub { my( $node, $startflag, $depth ) = @_; # only visit once return 0 unless $startflag; # if it is non-text ... if ( ref( $node ) ) { # if it is a list element ... if ( $node->tag =~ /^(?:li|dd|dt)$/ ) { # recurse get_paragraphs my @new_paras = get_paragraphs( $node ); # pre-pend appropriate prefix for list $new_paras[ 0 ] = $prefix{ $node->tag } . $new_paras[ 0 ] ; # and update the @paras array @paras = ( @paras, @new_paras ); # and traverse no more return 0; } else { # any other element, just traverse return 1; } } else { # add text to the current paragraph ... $paras[ $#paras ] = join( ' ', $paras[ $#paras ], $node ) if $node =~ /\S/ ; # and recurse no more return 0; } }, 0 ); } else { # add test to current paragraph ... $paras[ $#paras ] = join( ' ', $paras[ $#paras ], $child ) if $child =~ /\S/ ; } } return @paras; } #-------------------------------------------------------------------------- # # Main # #-------------------------------------------------------------------------- # parse the HTML file if ($self->param('read_file_callback')) { my $callback = $self->param('read_file_callback'); $html_tree->parse( $callback->($filename) ); } else { open(HTML, $filename) or die "Can't open $filename : $!"; $html_tree->parse( join( '', ) ); close(HTML); } # main tree traversal routine $html_tree->traverse( sub { my( $node, $startflag, $depth ) = @_; # ignore what's in the return 0 if ref( $node ) and $node->tag eq 'head'; # only visit nodes once return 0 unless $startflag; # if this node is non-text ... if ( ref $node ) { # if this is a para ... if ( $node->tag eq 'p' ) { # iterate sub-paragraphs (including lists) ... for ( get_paragraphs( $node ) ) { # if it is a
  • ... if ( /^\* / ) { # indent first line by 4, rest by 6 $text_formatter->firstIndent( 4 ); $text_formatter->bodyIndent( 6 ); } # if it is a
    ... elsif ( s/^\+ // ) { # set left margin to 4 $text_formatter->leftMargin( 4 ); } # if it is a
    ... elsif ( s/^- // ) { # set left margin to 8 $text_formatter->leftMargin( 8 ); } # print formatted paragraphs ... $result .= $text_formatter->paragraphs( $_ ); # and reset formatter defaults $text_formatter->leftMargin( 0 ); $text_formatter->firstIndent( 0 ); $text_formatter->bodyIndent( 0 ); } $result .= "\n"; return 0; } # if this is a heading ... elsif ( $node->tag =~ /^h(\d)/ ) { # get the heading level ... my $level = $1; # increment the number for this level ... $heading_number[ $level ]++; # reset lower level heading numbers ... for ( $level+1 .. $#heading_number ) { $heading_number[ $_ ] = 0; } # create heading number string my $heading_number = join( '.', @heading_number[ 1 .. $level ] ); # generate heading from number string and heading text ... # my $text = "$heading_number " . get_text( $node ); my $text = get_text( $node ); # underline it with the appropriate underline character ... $text =~ s{ (.*) } { "$1\n" . $underline{ $node->tag } x length( $1 ) }gex ; $result .= $text; return 0; } else { return 1; } } # if it is text ... else { return 0 unless $node =~ /\S/; $result .= $text_formatter->format( $node ); return 0; } }, 0 ); # filter out comments $result =~ s///gs; return $result; } 1; __END__ =head1 NAME CGI::Application::MailPage - module to allow users to send HTML pages to friends. =head1 SYNOPSIS use CGI::Application::MailPage; my $mailpage = CGI::Application::MailPage->new( PARAMS => { document_root => '/home/httpd', smtp_server => 'smtp.foo.org' }); $mailpage->run(); =head1 DESCRIPTION CGI::Application::MailPage is a CGI::Application module that allows users to send HTML pages to their friends. This module provides the functionality behind a typical "Mail This Page To A Friend" link. To use this module you need to create a simple "stub" script. It should look like: #!/usr/bin/perl use CGI::Application::MailPage; my $mailpage = CGI::Application::MailPage->new( PARAMS => { document_root => '/home/httpd', smtp_server => 'smtp.foo.org', }, ); $mailpage->run(); You'll need to replace the "/home/httpd" with the real path to your document root - the place where the HTML files are kept for your site. You'll also need to change "smtp.foo.org" to your SMTP server. Put this somewhere where CGIs can run and name it something like C. Now, add a link in the pages you want people to be able to send to their friends that looks like: mail this page to a friend This gets you the default behavior and look. To get something more to your specifications you can use the options described below. =head1 OPTIONS CGI::Application modules accept options using the PARAMS arguement to C. To give options for this module you change the C call in the "stub" shown above: my $mailpage = CGI::Application::MailPage->new( PARAMS => { document_root => '/home/httpd', smtp_server => 'smtp.foo.org', use_page_param => 1, } ); The C option tells MailPage not to use the REFERER header to determine the page to mail. See below for more information about C and other options. =over 4 =item * document_root (required) This parameter is used to specify the document root for your server - this is the place where the HTML files are kept. MailPage needs to know this so that it can find the HTML files to email. =item * smtp_server (required) This must be set to an SMTP server that MailPage can use to send mail. Future versions of MailPage may support other methods of sending mail, but for now you'll need a working SMTP server. =item * use_page_param By default MailPage uses the REFERER header to determine the page that the user wants to mail to their friends. This doesn't always work right, particularily on very old browsers. If you don't want to use REFERER then you can set this option and write your links to the application as: mail page You'll have to replace http://host/page.html with the url for each page you put the link in. You could cook up some Javascript to do this for you, but if the browser has working Javascript then it probably has a working REFERER! =item * email_subject The default subject of the email sent from the program. Defaults to empty, requiring the user to enter a subject. =item * form_template This application uses HTML::Template to generate its HTML pages. If you would like to customize the HTML you can copy the default form template and edit it to suite your needs. The default form template is called 'form.tmpl' and you can get it from the distribution or from wherever this module ended up in your C<@INC>. Pass in the path to your custom template as the value of this parameter. See L for more information about the template syntax. =item * thanks_template The default "Thanks" page template is called 'thanks.tmpl' and you can get it from the distribution or from wherever this module ended up in your C<@INC>. Pass in the path to your custom template as the value of this parameter. See L for more information about the template syntax. =item * email_template The default email template is called 'email.tmpl' and you can get it from the distribution or from wherever this module ended up in your C<@INC>. Pass in the path to your custom template as the value of this parameter. See L for more information about the template syntax. =item * read_file_callback You can provide a subroutine reference that will be called when MailPage needs to open an HTML file on your site. This can used to resolve complex aliasing problems or to perform any desired manipulation of the HTML text. The called subroutine recieves one arguement, the name of the file to be opened. It should return the text of the file. Here's an example that changes all 'p's to 'q's in the text of the files: #!/usr/bin/perl -w use CGI::Application::MailPage; sub p_to_q { my $filename = shift; open(FILE, $filename) or die; my $buffer; while() { s/p/q/g; $buffer .= $_; } return $buffer; } my $mailpage = CGI::Application::MailPage->new( PARAMS => { document_root => '/home/httpd', smtp_server => 'smtp.foo.org', read_file_callback => \&p_to_q, }, ); $mailpage->run(); =head1 AUTHOR Copyright 2002, Sam Tregar (sam@tregar.com). Questions, bug reports and suggestions can be sent to the CGI::Application mailing list. You can subscribe by sending a blank message to cgiapp-subscribe@lists.vm.com. See you there! =head1 LICENSE This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =head1 SEE ALSO L, L =cut