#-*- perl -*-
#
# Copyright (C) 2002,2003,2004 Ken'ichi Fukamachi
# All rights reserved. This program is free software; you can
# redistribute it and/or modify it under the same terms as Perl itself.
#
# $FML: ToHTML.pm,v 1.63 2004/02/15 04:38:37 fukachan Exp $
#
package Mail::Message::ToHTML;
use strict;
use vars qw(@ISA @EXPORT @EXPORT_OK $AUTOLOAD);
use Carp;
my $is_strict_warn = 0;
my $debug = 0;
my $URL =
"Mail::Message::ToHTML";
my $version = q$FML: ToHTML.pm,v 1.63 2004/02/15 04:38:37 fukachan Exp $;
my $versionid = 0;
if ($version =~ /,v\s+([\d\.]+)\s+/) {
$versionid = "$1";
$version = "$URL $versionid";
}
=head1 NAME
Mail::Message::ToHTML - convert text format mail to HTML format
=head1 SYNOPSIS
... lock by something ...
use Mail::Message::ToHTML;
my $obj = new Mail::Message::ToHTML {
charset => "euc-jp",
directory => "/var/www/htdocs/ml/elena",
};
$obj->htmlfy_rfc822_message({
id => 1,
src => "/var/spool/ml/elena/spool/1",
});
... unlock by something ...
This module itself provides no lock function.
please use flock() built in perl or CPAN lock modules for it.
=head1 DESCRIPTION
=head2 Message structure created as HTML
HTML-fied message has following structure.
something() below is method name.
for example
-------------------------------------------------------------------
html_begin()
...
mhl_preamble()
mhl_separator()
message header
From: ...
Subject: ...
mhl_separator()
message body
mhl_separator()
mhl_footer()
html_end()
=head1 METHODS
=head2 new($args)
$args = {
directory => $directory,
};
C<$directory> is top level directory where html-fied articles are
stored.
=cut
# Descriptions: constructor.
# Arguments: OBJ($self) HASH_REF($args)
# Side Effects: none
# Return Value: OBJ
sub new
{
my ($self, $args) = @_;
my ($type) = ref($self) || $self;
my $me = {};
$me->{ _charset } = $args->{ charset } || 'us-ascii';
$me->{ _html_base_directory } = $args->{ output_dir };
$me->{ _db_type } = $args->{ db_type };
$me->{ _db_name } = $args->{ db_name };
$me->{ _db_base_dir } = $args->{ db_base_dir };
$me->{ _is_attachment } = defined($args->{ attachment }) ? 1 : 0;
$me->{ _args } = $args;
$me->{ _num_attachment } = 0; # for child process
$me->{ _use_subdir } = 'yes';
$me->{ _subdir_style } = 'yyyymm';
$me->{ _html_id_order } = $args->{ index_order } || 'normal';
$me->{ _use_address_mask } = $args->{ use_address_mask } || 'yes';
$me->{ _address_mask_type } = $args->{ address_mask_type } || 'all';
use Mail::Message::Thread;
my $t = new Mail::Message::Thread $args;
$me->{ _thread_object } = $t;
return bless $me, $type;
}
# Descriptions: destructor.
# Arguments: OBJ($self)
# Side Effects: none
# Return Value: none
sub DESTROY
{
my ($self) = @_;
_PRINT_DEBUG("ToHTML::DESTROY");
1;
}
=head2 htmlfy_rfc822_message($args)
convert mail to html.
$args = {
id => $id,
path => $path,
};
where C<$path> is file path.
=cut
# Descriptions: top level entrance to convert mail to html
# Arguments: OBJ($self) HASH_REF($args)
# $args = { id => $id, path => $path };
# $id identifier (e.g. "1" (article id))
# $path file path (e.g. "/some/where/1");
# Side Effects: none
# Return Value: none
sub htmlfy_rfc822_message
{
my ($self, $args) = @_;
# prepare source
use Mail::Message;
use FileHandle;
my $rh = new FileHandle $args->{ src };
my $msg = Mail::Message->parse( { fd => $rh } );
my $hdr = $msg->whole_message_header;
my $body = $msg->whole_message_body;
$self->{ _current_msg } = $msg;
$self->{ _current_hdr } = $hdr;
$self->{ _current_body } = $body;
# initialize basic information
# $id = article id
# $src = source file
# $dst = destination file (target html)
my ($id, $src, $dst) = $self->_init_htmlfy_rfc822_message($args);
$self->{ _debug_id } = $id;
# target html exists already.
if (-f $dst) {
$self->{ _ignore_list }->{ $id } = 1; # ignore flag
warn("html file for $id already exists") if $debug;
return undef;
}
# save information for index.html and thread.html
$self->cache_message_info($msg, { id => $id,
src => $src,
dst => $dst,
} );
# prepare output channel
my $wh = $self->_set_output_channel( { dst => $dst } );
unless (defined $wh) {
croak("cannot open output file $dst\n");
}
# before main message
$self->html_begin($wh, { message => $msg });
$self->mhl_preamble($wh);
# analyze $msg, chain of Mail::Message objects.
# See Mail::Message class for more detail.
# XXX we use $m->{ next } here, but we should avoid this style and
# XXX prepare access method for it in Mail::Message class.
my ($m, $type, $attach);
CHAIN:
for ($m = $msg; defined($m) ; $m = $m->{ 'next' }) {
$type = $m->data_type;
last CHAIN if $type eq 'multipart.close-delimiter'; # last of multipart
next CHAIN if $type =~ /^multipart/; # multipart type is special.
unless ($type =~ /^\w+\/[-\w\d\.]+$/) {
warn("invalid type={$type}");
next CHAIN;
}
# header (Mail::Message object uses this special type)
if ($type eq 'text/rfc822-headers') {
$self->mhl_separator($wh);
my $charset = $self->{ _charset };
my $header = $self->_format_safe_header($msg);
_print_raw_str($wh, $header, $charset);
$self->mhl_separator($wh);
}
# message/rfc822 case (attached rfc822 message)
elsif ($type eq 'message/rfc822') {
$attach++;
my $tmpf = $self->_create_temporary_file_in_raw_mode($m);
if (defined $tmpf && -f $tmpf) {
# write attachement into a separete file
my $outf = _gen_attachment_filename($dst, $attach, 'html');
my $args = $self->{ _args };
$args->{ attachment } = 1; # clarify not top level content.
my $text = new Mail::Message::ToHTML $args;
$text->htmlfy_rfc822_message({
parent_id => $id,
src => $tmpf,
dst => $outf,
});
# show inline link,
# which appears in parent html ( == $wh channel ).
$self->_print_inline_object_link({
fh => $wh, # file descriptor
type => $type, # XXX derived from input message
num => $attach, # number
file => $outf, # temporary file name
});
unlink $tmpf;
}
}
# text/plain case.
# XXX inline expansion.
elsif ($type eq 'text/plain') {
if ($self->{ _use_address_mask } eq 'yes') {
$self->_text_plain_part_safe_print($wh, $m);
}
else { # original
$self->_text_safe_print({
fh => $wh, # parent html
data => $m->message_text(),
charset => $m->charset(),
encoding => $m->encoding_mechanism(),
});
}
}
# create a separete file for attachment
else {
$attach++;
# write attachement into a separete file
my $outf = _gen_attachment_filename($dst, $attach, $type);
my $enc = $m->encoding_mechanism;
my $msginfo = { message => $m };
# e.g. text/xxx case (e.g. text/html case)
if ($type =~ /^text/) {
# 1. firstly saved to temporary file $tmpf in "raw" mode
my $tmpf = $self->_create_temporary_filename();
$msginfo->{ file } = $tmpf;
# once create temporary file
_PRINT_DEBUG("attachment: type=$type attach=$attach enc=$enc");
if ($enc) {
$self->_binary_print($msginfo); # XXX raw mode
}
else {
$self->_text_raw_print($msginfo); # XXX raw mode
}
# 2. secondary convert $tmpf to real target $outf with
# some modification e.g. metachars escaping, ...
# disable html tag in file saved in raw mode.
if (-f $tmpf) {
$msginfo->{ description } = "(HTML TAGs are disabled)";
_disable_html_tag_in_file($tmpf, $outf);
unlink $tmpf;
}
}
# e.g. image/gif not text/* nor message/*
else {
$msginfo->{ file } = $outf;
$self->_binary_print($msginfo);
}
# show inline link appeared in parent html.
$self->_print_inline_object_link({
inline => 1,
fh => $wh,
type => $type,
num => $attach,
file => $outf,
info => $msginfo,
});
}
}
# show navigation bar et.al. after message itself
$self->mhl_separator($wh);
$self->mhl_footer($wh);
$self->html_end($wh);
}
# Descriptions: copy $inf file to $outf file with disabling HTML tag
# by _print_safe_buf().
# Arguments: STR($inf) STR($outf)
# Side Effects: create $outf file
# Return Value: none
sub _disable_html_tag_in_file
{
my ($inf, $outf) = @_;
my $mask = umask();
umask(022);
use FileHandle;
my $rh = new FileHandle $inf;
my $wh = new FileHandle "> $outf";
if (defined $rh) {
my $buf = '';
my $b;
while ($b = <$rh>) { $buf .= $b;}
_print_safe_buf($wh, $buf);
$wh->close;
$rh->close;
}
umask($mask);
}
# Descriptions: return HTML filename
# Arguments: OBJ($self) NUM($id)
# Side Effects: none
# Return Value: STR or UNDEF
sub html_filename
{
my ($self, $id) = @_;
my $use_subdir = $self->{ _use_subdir };
# relative path under html_base_dir
if (defined($id) && ($id > 0)) {
if ($use_subdir eq 'yes') {
return $self->_html_file_subdir_name($id);
}
else {
return "msg${id}.html";
}
}
else {
return undef;
}
}
# Descriptions: return HTML sub directory string
# Arguments: OBJ($self) NUM($id)
# Side Effects: none
# Return Value: STR
sub _html_file_subdir_name
{
my ($self, $id) = @_;
my $ndb = $self->ndb();
my $subdir = '';
my $html_base_dir = $self->{ _html_base_directory };
my $subdir_style = $self->{ _subdir_style };
my $dir_mode = $self->{ _dir_mode } || 0755;
if ($subdir_style eq 'yyyymm') {
my $hdr = $self->{ _current_hdr };
$subdir = $ndb->msg_time($hdr, 'yyyymm');
use File::Spec;
my $xsubdir = File::Spec->catfile($html_base_dir, $subdir);
unless (-d $xsubdir) {
my $mask = umask();
umask(022);
mkdir($xsubdir, $dir_mode);
umask($mask);
}
}
else {
croak("unknown \$subdir_style");
}
if ($subdir) {
use File::Spec;
return File::Spec->catfile($subdir, "msg$id.html");
}
else {
warn("not create msg$id.html");
return undef;
}
}
# Descriptions: return HTML file path
# Arguments: OBJ($self) NUM($id)
# Side Effects: none
# Return Value: STR
sub html_filepath
{
my ($self, $id) = @_;
my $html_base_dir = $self->{ _html_base_directory };
if (defined($id) && ($id > 0)) {
my $filename = $self->html_filename($id);
use File::Spec;
return File::Spec->catfile($html_base_dir, $filename);
}
else {
return undef;
}
}
# Descriptions: parse $args and return file id, name, path.
# Arguments: OBJ($self) HASH_REF($args)
# Side Effects: none
# Return Value: ARRAY(NUM, STR, STR)
sub _init_htmlfy_rfc822_message
{
my ($self, $args) = @_;
my ($id, $src, $dst);
if (defined $args->{ src }) {
$src = $args->{ src };
}
else {
croak("htmlfy_rfc822_message: \$src is mandatory\n");
}
if (defined $args->{ id }) {
my $html_base_dir = $self->{ _html_base_directory };
$id = $args->{ id };
$dst = $self->html_filepath($id);
}
# this object is an attachment if parent_id is specified.
elsif (defined $args->{ parent_id }) {
$self->{ _num_attachment }++;
$id = $args->{ parent_id } .'.'. $self->{ _num_attachment };
$dst = $args->{ dst };
}
# last resort: give unique identifier
elsif (defined $args->{ dst }) {
$id = time.".".$$;
$dst = $args->{ dst };
}
# oops ;) wrong call of this function
else {
croak("htmlfy_rfc822_message: specify \$id or \$dst\n");
}
return ($id, $src, $dst);
}
# Descriptions: show html header + file title in
# Arguments: OBJ($self) HANDLE($wh) HASH_REF($args)
# Side Effects: none
# Return Value: none
sub html_begin
{
my ($self, $wh, $args) = @_;
my ($msg, $hdr, $title);
if (defined $args->{ title }) {
$title = $args->{ title };
}
elsif (defined $args->{ message }) {
$msg = $args->{ message };
$hdr = $msg->whole_message_header;
$title = $self->_decode_mime_string( $hdr->get('article_subject') ||
$hdr->get('subject') );
}
print $wh "\n";
print $wh
q{};
print $wh "\n";
print $wh "\n";
print $wh "\n";
if (defined $self->{ _charset }) {
my $charset = $self->{ _charset };
print $wh "\n";
}
if (defined $self->{ _stylsheet }) {
my $css = $self->{ _stylsheet };
print $wh "\n";
}
if (defined $title) {
print $wh "";
_print_safe_str($wh, $title);
print $wh "\n";
}
print $wh "\n";
print $wh "\n";
print $wh "
";
_print_safe_str($wh, $title);
print $wh "
\n";
}
# Descriptions: show html closing
# Arguments: OBJ($self) HANDLE($wh)
# Side Effects: none
# Return Value: none
sub html_end
{
my ($self, $wh) = @_;
print $wh "";
print $wh "\n";
}
# Descriptions: show html separetor, we use now.
# Arguments: OBJ($self) HANDLE($wh)
# Side Effects: none
# Return Value: none
sub mhl_separator
{
my ($self, $wh) = @_;
print $wh "\n";
}
my $preamble_begin = "";
my $preamble_end = "";
my $footer_begin = "";
my $footer_end = "";
# Descriptions: prepare information area before main message appears.
# Later, this area is replaced with useful information
# e.g. thread link.
# Arguments: OBJ($self) HANDLE($wh)
# Side Effects: none
# Return Value: none
sub mhl_preamble
{
my ($self, $wh) = @_;
print $wh $preamble_begin, "\n";
print $wh $preamble_end, "\n";
}
# Descriptions: prepare information area after main message appears.
# Later, this area is replaced with useful information
# e.g. thread link.
# Arguments: OBJ($self) HANDLE($wh)
# Side Effects: none
# Return Value: none
sub mhl_footer
{
my ($self, $wh) = @_;
print $wh $footer_begin, "\n";
print $wh $footer_end, "\n";
}
# Descriptions: prepare write handle
# Arguments: OBJ($self) HASH_REF($args)
# Side Effects: create $args->{ dst } file if needed
# Return Value: HANDLE
sub _set_output_channel
{
my ($self, $args) = @_;
my $dst = $args->{ dst };
my $wh = undef;
my $mask = umask();
umask(022);
if (defined $dst) {
use FileHandle;
$wh = new FileHandle "> $dst";
}
else {
$wh = \*STDOUT;
}
umask($mask);
return $wh;
}
# Descriptions: return temporary file path.
# XXX temporary file is created under $db_dir not public space
# Arguments: OBJ($self)
# Side Effects: none
# Return Value: STR
sub _create_temporary_filename
{
my ($self) = @_;
my $html_base_dir = $self->{ _html_base_directory };
use File::Spec;
return File::Spec->catfile($html_base_dir, "tmp.$$");
}
# Descriptions: create a temporary file with the content $msg
# Arguments: OBJ($self) OBJ($msg)
# Side Effects: create $tmpf file
# Return Value: STR
sub _create_temporary_file_in_raw_mode
{
my ($self, $msg) = @_;
my $tmpf = $self->_create_temporary_filename();
my $mask = umask();
umask(022);
use FileHandle;
my $wh = new FileHandle "> $tmpf";
if (defined $wh) {
$wh->autoflush(1);
my $buf = $msg->message_text();
$wh->print($buf);
$wh->close;
umask($mask);
return ($tmpf);
}
umask($mask);
return undef;
}
# Descriptions: convert $file filepath to relative path
# XXX UNIX specific ???
# Arguments: OBJ($self) STR($file)
# Side Effects: none
# Todo: UNIX specific
# Return Value: STR
sub _relative_path
{
my ($self, $file) = @_;
my $html_base_dir = $self->{ _html_base_directory };
$file =~ s/$html_base_dir//;
$file =~ s@^/@@;
return $file;
}
# Descriptions: print inline link as html for attachments e.g.
# images, files et. al.
# Arguments: OBJ($self) HASH_REF($args)
# Side Effects: none
# Return Value: none
sub _print_inline_object_link
{
my ($self, $args) = @_;
my $wh = $args->{ fh };
my $type = $args->{ type };
my $num = $args->{ num };
my $file = $self->_relative_path($args->{ file });
my $desc = '';
my $inline = defined( $args->{ inline } ) ? 1 : 0;
if (defined $args->{ info }->{ description }) {
$desc = $args->{ info }->{ description };
}
if ($inline && $type =~ /image/) {
print $wh " $desc\n";
}
else {
my $t = $file;
print $wh " $type $num ";
print $wh "$desc \n";
}
}
# Descriptions: return attachment filename
# Arguments: STR($dst) STR($attach) STR($suffix)
# Side Effects: none
# Todo: UNIX specific
# Return Value: STR
sub _gen_attachment_filename
{
my ($dst, $attach, $suffix) = @_;
my $outf = $dst;
if ($suffix =~ m@/@) { $suffix =~ s@.*/@@;}
$outf =~s/\.html$//;
$outf = "$outf.$attach.$suffix";
return $outf;
}
# default header to show
my @header_field = qw(From To Cc Subject Date Message-Id X-Sequence);
# Descriptions: format header of $msg with escaping HTML metachars
# and disabling special HTML tags.
# See _sprintf_safe_str() for how to escape.
# Arguments: OBJ($self) OBJ($msg)
# Side Effects: none
# XXX $buf is printed out later in raw mode.
# Return Value: STR
sub _format_safe_header
{
my ($self, $msg) = @_;
my ($buf);
my $hdr = $msg->whole_message_header;
my $header_field = \@header_field;
# header
$buf .= "\n";
for my $field (@$header_field) {
if (defined($hdr->get($field))) {
$buf .= "\n";
$buf .= "${field}: ";
$buf .= "\n";
my $xbuf = $hdr->get($field);
# mask the raw address against address collector (e.g. spammer).
if ($self->{ _use_address_mask } eq 'yes') {
if ($self->_is_mask_address($field)) {
$xbuf = $self->_address_to_gecos($xbuf);
}
}
$xbuf = $self->_decode_mime_string($xbuf) if $xbuf =~ /=\?/i;
$buf .= "\n";
$buf .= _sprintf_safe_str($xbuf);
$buf .= "\n";
$buf .= " \n";
}
}
$buf .= "\n";
return($buf);
}
# Descriptions: show link to indexes as navigation
# Arguments: HASH_REF($args)
# Side Effects: none
# Return Value: none
sub _format_index_navigator
{
my ($args) = @_;
my $use_subdir = defined $args->{use_subdir} ? $args->{use_subdir} : 0;
my $prefix = $use_subdir ? '../' : '';
my $str = qq{
[ID Index][Thread Index][Monthly ID Index][Top Index]
};
return $str;
}
# Descriptions: print text/plain part by printing each paragraph.
# mask raw mail addresses in the signature if could.
# Arguments: OBJ($self) HANDLE($wh) OBJ($m)
# Side Effects: none
# Return Value: STR
sub _text_plain_part_safe_print
{
my ($self, $wh, $m) = @_;
my $i = 0;
my $n = $m->num_paragraph();
# print each paragraph.
for ($i = 1; $i <= $n ; $i++) {
my $buf = $m->nth_paragraph($i);
# try to hide domain since the last paragraph must be signature.
if ($self->{ _use_address_mask } eq 'yes') {
if ($i == $n) {
$buf =~ s/(\w+\@[\w\.]+)/$self->_address_to_gecos($1)/ge;
}
}
$self->_text_safe_print({
fh => $wh, # parent html
data => $buf,
charset => $m->charset(),
encoding => $m->encoding_mechanism(),
});
}
}
# Descriptions: print out text data with escaping by _print_safe_buf()
# Arguments: OBJ($self) HASH_REF($args)
# Side Effects: none
# Return Value: STR
sub _text_safe_print
{
my ($self, $args) = @_;
my $buf = $args->{ data };
my $fh = $args->{ fh } || \*STDOUT;
my $in_code = $args->{ charset } || undef;
my $encoding = $args->{ encoding } || '7bit';
if ($encoding eq 'base64') {
use Mail::Message::Encode;
my $encode = new Mail::Message::Encode;
$buf = $encode->decode_base64_string($buf);
}
elsif ($encoding eq 'quoted-printable') {
use Mail::Message::Encode;
my $encode = new Mail::Message::Encode;
$buf = $encode->decode_qp_string($buf);
}
# XXX-TODO: euc-jp is hard-coded.
if (defined $buf && $buf) {
$buf = $self->_convert($buf, 'euc');
}
_print_safe_buf($fh, $buf);
}
# Descriptions: print out message without escaping
# Arguments: OBJ($self) HASH_REF($args)
# Side Effects: create $outf
# Return Value: none
sub _text_raw_print
{
my ($self, $args) = @_;
my $msg = $args->{ message }; # Mail::Message object
my $type = $msg->data_type;
my $enc = $msg->encoding_mechanism;
my $buf = $msg->message_text();
if (defined( $args->{ file } )) {
my $mask = umask();
umask(022);
my $outf = $args->{ file };
use FileHandle;
my $fh = new FileHandle "> $outf";
# XXX-TODO: euc-jp is hard-coded.
if (defined $buf && $buf) {
$buf = $self->_convert($buf, 'euc');
}
print $fh $buf, "\n";
$fh->close();
umask($mask);
}
}
# Descriptions: print out binary with MIME encoding or
# text with escaping
# Arguments: OBJ($self) HASH_REF($args)
# Side Effects: create $outf file
# Return Value: none
sub _binary_print
{
my ($self, $args) = @_;
my $msg = $args->{ message }; # Mail::Message object
my $type = $msg->data_type;
my $enc = $msg->encoding_mechanism;
my $mask = umask();
umask(022);
if (defined( $args->{ file } )) {
my $outf = $args->{ file };
use FileHandle;
my $fh = new FileHandle "> $outf";
if (defined $fh) {
$fh->autoflush(1);
binmode($fh);
if ($enc eq 'base64') {
eval q{
use MIME::Base64;
print $fh decode_base64( $msg->message_text() );
};
}
elsif ($enc eq 'quoted-printable') {
eval q{
use MIME::QuotedPrint;
print $fh decode_qp( $msg->message_text() );
};
}
elsif ($enc eq '7bit') {
_print_safe_str($fh, $msg->message_text());
}
else {
my $r = "*** unknown MIME encoding enc='$enc' ***\n";
_print_safe_str($fh, $r);
_print_safe_str($fh, $msg->message_text());
}
$fh->close();
}
}
umask($mask);
}
=head2 is_ignore($id)
we should not process this C<$id>
=cut
# Descriptions: check whether article $id is ignored
# Arguments: OBJ($self) STR($id)
# Side Effects: none
# Return Value: 1 or 0
sub is_ignore
{
my ($self, $id) = @_;
return defined($self->{ _ignore_list }->{ $id }) ? 1 : 0;
}
=head1 METHODS for index and thread
=head2 cache_message_info($msg, $args)
save information into DB.
See section C for more detail.
=cut
# Descriptions: update database on message header, thread relation
# et. al.
# Arguments: OBJ($self) OBJ($msg) HASH_REF($args)
# Side Effects: update database
# Return Value: none
sub cache_message_info
{
my ($self, $msg, $args) = @_;
my $ndb = $self->ndb();
my $id = $args->{ id };
my $src = $args->{ src };
my $dst = $args->{ dst };
$ndb->set_key($id);
$ndb->set('html_filename', $id, $self->html_filename($id));
$ndb->set('html_filepath', $id, $dst);
unless ($ndb->get('message_id', $id)) {
# analyze $msg only if not yet analyzed.
print STDERR "debug: analyze $id.\n" if $debug;
$ndb->analyze($msg);
}
else {
print STDERR "debug: already analyzed!\n" if $debug;
}
}
# Descriptions: return Mail::Message::DB object.
# Arguments: OBJ($self)
# Side Effects: none
# Return Value: OBJ
sub ndb
{
my ($self) = @_;
my $t = $self->{ _thread_object };
return $t->db();
}
=head2 update_msg_html_links($id)
update link relation around C<$id>.
=cut
# Descriptions: top level dispatcher to update database.
# _msg_file_rewrite_links() has real function for updating.
# Arguments: OBJ($self) STR($id)
# Side Effects: update databse
# Return Value: none
sub update_msg_html_links
{
my ($self, $id) = @_;
my $info = $self->evaluate_links_relation($id);
my $list = $self->{ _affected_idlist } = [];
if ($self->is_ignore($id)) {
warn("not update relation around $id") if $debug;
return undef;
}
# sanity
return unless defined $id;
return unless $id;
# update target itself, of course
$self->_msg_file_rewrite_links($id);
push(@$list, $id);
# no rewriting for myself
my %uniq = ( $id => 1 );
KEY:
for my $_link (qw(prev_id next_id prev_thread_id next_thread_id)) {
if (defined $info->{ $_link }) {
my $_id = $info->{ $_link };
next KEY if $uniq{ $_id };
$uniq{ $_id } = 1;
_PRINT_DEBUG("try: rewrite $_link links in msg $_id");
if (defined $_id && $_id) {
$self->_msg_file_rewrite_links($_id);
push(@$list, $_id);
}
}
else {
_PRINT_DEBUG("error: fail to rewrite msg $_link");
}
}
# hint cached on memory, provided by _print_thread().
if (defined $self->{ _hint_ref_key_list }->{ $id }) {
my $thread_list = $self->{ _hint_ref_key_list }->{ $id } || [];
# update link relation for all articles in this thread.
KEY:
for my $id (@$thread_list) {
next KEY if $uniq{ $id};
$uniq{ $id } = 1;
$self->_msg_file_rewrite_links( $id );
push(@$list, $id);
}
}
}
# Descriptions: update link at preamble and footer of HTML-ified message.
# Arguments: OBJ($self) STR($id)
# Side Effects: rewrite index file
# Return Value: none
sub _msg_file_rewrite_links
{
my ($self, $id) = @_;
my $info = $self->evaluate_links_relation($id);
my $preamble = $self->evaluate_safe_preamble($info);
my $footer = $self->evaluate_safe_footer($info);
my $code = _charset_to_code($self->{ _charset });
my $pat_preamble_begin = quotemeta($preamble_begin);
my $pat_preamble_end = quotemeta($preamble_end);
my $pat_footer_begin = quotemeta($footer_begin);
my $pat_footer_end = quotemeta($footer_end);
my $mask = umask();
umask(022);
_PRINT_DEBUG("try _msg_file_rewrite_links($id)");
use FileHandle;
my $file = $info->{ filepath };
if (defined $file && $file && -f $file) {
my ($old, $new) = ($file, "$file.new.$$");
my $rh = new FileHandle $old;
my $wh = new FileHandle "> $new";
if (defined $rh && defined $wh) {
my $buf;
_PRINT_DEBUG("rewrite: open msg $id");
LINE:
while ($buf = <$rh>) {
if ($buf =~ /^$pat_preamble_begin/
..
$buf =~ /^$pat_preamble_end/) {
if ($buf =~ /^$pat_preamble_end/) {
_print_raw_str($wh, $preamble, $code);
}
next LINE;
}
if ($buf =~ /^$pat_footer_begin/
..
$buf =~ /^$pat_footer_end/) {
if ($buf =~ /^$pat_footer_end/) {
_print_raw_str($wh, $footer, $code);
}
next LINE;
}
# just copy (rewrite only $preamble and $footer not message)
_print_raw_str($wh, $buf, $code);
}
$rh->close;
$wh->close;
unless (rename($new, $old)) {
croak("rename($new, $old) fail (id=$id)\n");
}
else {
_PRINT_DEBUG("done: rewritten links in msg $id");
}
}
else {
unless (defined $file) {
$new = $old = '(null string)';
}
warn("cannot open $old (id=$id)\n") unless defined $rh;
warn("cannot create $new (id=$id)\n") unless defined $wh;
}
}
else {
warn("undefined file for $id\n") if $is_strict_warn;
}
umask($mask);
}
# Descriptions: return thread link relation info et.al. for $id
# Arguments: OBJ($self) NUM($id)
# Side Effects: none
# Return Value: HASH_REF
sub evaluate_links_relation
{
my ($self, $id) = @_;
my $ndb = $self->ndb();
return $ndb->tohtml_thread_summary($id);
}
# Descriptions: return preamble without metachars
# Arguments: OBJ($self) HASH_REF($args)
# Side Effects: none
# Return Value: STR
sub evaluate_safe_preamble
{
my ($self, $args) = @_;
my $link_prev_id = $args->{ link_prev_id };
my $link_next_id = $args->{ link_next_id };
my $link_prev_thread_id = $args->{ link_prev_thread_id };
my $link_next_thread_id = $args->{ link_next_thread_id };
my $use_subdir = $self->{ _use_subdir } eq 'yes' ? 1 : 0;
my $prefix = $use_subdir ? '../' : '';
my $preamble = $preamble_begin. "\n";
my $mask = umask();
umask(022);
# for debug
$preamble .= "\n";
if (defined($link_prev_id) && $link_prev_id) {
$preamble .= "[Prev by ID]\n";
}
else {
$preamble .= "[No Prev ID]\n";
}
if (defined($link_next_id) && $link_next_id) {
$preamble .= "[Next by ID]\n";
}
else {
$preamble .= "[No Next ID]\n";
}
if (defined $link_prev_thread_id && $link_prev_thread_id) {
$preamble .=
"[Prev by Thread]\n";
}
else {
if (defined $link_prev_id && $link_prev_id) {
$preamble .=
"[Prev by Thread]\n";
}
else {
$preamble .= "[No Prev Thread]\n";
}
}
if (defined $link_next_thread_id && $link_next_thread_id) {
$preamble .=
"[Next by Thread]\n";
}
else {
if (defined $link_next_id && $link_next_id) {
$preamble .=
"[Next by Thread]\n";
}
else {
$preamble .= "[No Next Thread]\n";
}
}
$preamble .= _format_index_navigator( { use_subdir => $use_subdir } );
$preamble .= $preamble_end. "\n";;
umask($mask);
return $preamble;
}
# Descriptions: return footer without metachars
# Arguments: OBJ($self) HASH_REF($args)
# Side Effects: none
# Return Value: STR
sub evaluate_safe_footer
{
my ($self, $args) = @_;
my $link_prev_id = $args->{ link_prev_id };
my $link_next_id = $args->{ link_next_id };
my $link_prev_thread_id = $args->{ link_prev_thread_id };
my $link_next_thread_id = $args->{ link_next_thread_id };
my $subject = $args->{ subject };
my $use_subdir = $self->{ _use_subdir } eq 'yes' ? 1 : 0;
my $prefix = $use_subdir ? '../' : '';
my $footer = $footer_begin. "\n";;
if (defined($link_prev_id) && $link_prev_id) {
$footer .= " \n";
$footer .= "Prev by ID: ";
if (defined $subject->{ prev_id } ) {
$footer .= _sprintf_safe_str( $subject->{ prev_id } );
}
$footer .= "\n";
}
if (defined($link_next_id) && $link_next_id) {
$footer .= " \n";
$footer .= "Next by ID: ";
if (defined $subject->{ next_id } ) {
$footer .= _sprintf_safe_str( $subject->{ next_id } );
}
$footer .= "\n";
}
if (defined $link_prev_thread_id && $link_prev_thread_id) {
$footer .= " \n";
$footer .=
"Prev by Thread: ";
if (defined $subject->{ prev_thread_id }) {
$footer .= _sprintf_safe_str($subject->{ prev_thread_id });
}
$footer .= "\n";
}
elsif (defined($link_prev_id) && $link_prev_id) {
$footer .= " \n";
$footer .=
"Prev by Thread: ";
if (defined $subject->{ prev_id }) {
$footer .= _sprintf_safe_str($subject->{ prev_id });
}
$footer .= "\n";
}
if (defined $link_next_thread_id && $link_next_thread_id) {
$footer .= " \n";
$footer .=
"Next by Thread: ";
if (defined $subject->{ next_thread_id }) {
$footer .= _sprintf_safe_str($subject->{ next_thread_id });
}
$footer .= "\n";
}
elsif (defined($link_next_id) && $link_next_id) {
$footer .= " \n";
$footer .=
"Next by Thread: ";
if (defined $subject->{ next_id }) {
$footer .= _sprintf_safe_str($subject->{ next_id });
}
$footer .= "\n";
}
$footer .= qq{ \n};
$footer .= _format_index_navigator( { use_subdir => $use_subdir } );
$footer .= $footer_end. "\n";;
return $footer;
}
=head2 update_id_index($args)
update index.html.
=cut
# Descriptions: print navigation bar et.al. at upper half of indexes
# Arguments: OBJ($self) HASH_REF($args)
# Side Effects: create $new html
# Return Value: none
sub _print_index_begin
{
my ($self, $args) = @_;
my $old = $args->{ old };
my $new = $args->{ new };
my $title = $args->{ title };
my $code = _charset_to_code($self->{ _charset });
my $mask = umask();
umask(022);
use FileHandle;
my $wh = new FileHandle "> $new";
$args->{ wh } = $wh;
$self->html_begin($wh, { title => $title });
_print_raw_str($wh, _format_index_navigator(), $code);
$self->mhl_separator($wh);
umask($mask);
}
# Descriptions: print navigation bar et.al. at the end of indexes.
# Arguments: OBJ($self) HASH_REF($args)
# Side Effects: create $new html
# Return Value: none
sub _print_index_end
{
my ($self, $args) = @_;
my $wh = $args->{ wh };
my $old = $args->{ old };
my $new = $args->{ new };
my $title = $args->{ title };
my $code = $args->{ code };
$self->mhl_separator($wh);
_print_raw_str($wh, _format_index_navigator(), $code);
# append version information
_print_raw_str($wh, " Genereated by $version\n", $code);
$self->html_end($wh);
unless (rename($new, $old)) {
croak("rename($new, $old) fail\n");
}
}
# Descriptions: create Top index.html if no index.html
# Arguments: OBJ($self) HASH_REF($args)
# Side Effects: create Top index.html
# Return Value: none
sub create_top_index
{
my ($self, $args) = @_;
my $html_base_dir = $self->{ _html_base_directory };
my $code = _charset_to_code($self->{ _charset });
my $order = $self->{ _html_id_order } || 'normal';
my $htmlinfo = {
title => defined($args->{ title }) ? $args->{ title } : "Top Index",
old => "$html_base_dir/index.html",
new => "$html_base_dir/index.html.new.$$",
code => $code,
};
return if ( -f $htmlinfo-> { old } );
$self->_print_index_begin( $htmlinfo );
my $wh = $htmlinfo->{ wh };
$self->_print_index_end( $htmlinfo );
}
# Descriptions: update index_all.html
# Arguments: OBJ($self) HASH_REF($args)
# Side Effects: rewrite index_all.html
# Return Value: none
sub update_id_index
{
my ($self, $args) = @_;
my $html_base_dir = $self->{ _html_base_directory };
my $code = _charset_to_code($self->{ _charset });
my $order = $self->{ _html_id_order } || 'normal';
my $htmlinfo = {
title => defined($args->{ title }) ? $args->{ title } : "ID Index",
old => "$html_base_dir/index_all.html",
new => "$html_base_dir/index_all.html.new.$$",
code => $code,
};
if ($self->is_ignore($args->{id})) {
warn("not update index_all.html around $args->{id}") if $debug;
return undef;
}
$self->_print_index_begin( $htmlinfo );
my $wh = $htmlinfo->{ wh };
my $db = $self->ndb();
my $max_id = $db->get('hint', 'max_id');
$self->_print_ul($wh, $db, $code);
if ($order eq 'reverse') {
for my $id (reverse (1 .. $max_id)) {
$self->_print_li_filename($wh, $db, $id, $code);
}
}
else {
for my $id (1 .. $max_id) {
$self->_print_li_filename($wh, $db, $id, $code);
}
}
$self->_print_end_of_ul($wh, $db, $code);
$self->_print_index_end( $htmlinfo );
}
=head2 update_monthly_id_index($args)
=cut
# Descriptions: update monthly index
# Arguments: OBJ($self) HASH_REF($args)
# Side Effects: rewrite monthly index
# Return Value: none
sub update_monthly_id_index
{
my ($self, $args) = @_;
my $affected_list = $self->{ _affected_idlist };
if ($self->is_ignore($args->{id})) {
warn("not update index.html around $args->{id}") if $debug;
return undef;
}
# open databaes
my $db = $self->ndb();
my %month_update = ();
IDLIST:
for my $id (@$affected_list) {
next IDLIST unless $id =~ /^\d+$/o;
next IDLIST if $id =~ /^\s*$/o;
my $month = $db->get('month', $id);
if (defined $month && $month !~ /^\s*$/o) {
$month_update{ $month } = 1;
}
}
# todo list
for my $month (sort keys %month_update) {
my $this_month = $month; # yyyy/mm
my $suffix = $month; $suffix =~ s@/@@go; # yyyymm
$self->_update_monthly_id_index($args, {
this_month => $this_month,
suffix => $suffix,
});
}
# update monthly_index.html
$self->_update_id_montly_index_master($args);
}
# Descriptions: update monthly index master
# Arguments: OBJ($self) HASH_REF($args)
# Side Effects: rewrite monthly_index.html
# Return Value: none
sub _update_id_montly_index_master
{
my ($self, $args) = @_;
my $html_base_dir = $self->{ _html_base_directory };
my $code = _charset_to_code($self->{ _charset });
use File::Spec;
my $old = File::Spec->catfile($html_base_dir, "monthly_index.html");
my $new = File::Spec->catfile($html_base_dir, "monthly_index.html.new.$$");
my $htmlinfo = {
title => defined($args->{ title }) ? $args->{ title } : "ID Index",
old => $old,
new => $new,
code => $code,
};
$self->_print_index_begin( $htmlinfo );
my $wh = $htmlinfo->{ wh };
my $db = $self->ndb();
my $mlist = $db->get_table_as_hash_ref('inv_month'); # month => (id ...)
my (@list) = sort __sort_yyyymm keys %$mlist;
my ($years) = _yyyy_range(\@list);
_print_raw_str($wh, "
", $code);
for my $year (sort {$b <=> $a} @$years) {
_print_raw_str($wh, "
", $code);
for my $month (1 .. 12) {
_print_raw_str($wh, "
", $code) if $month == 7;
my $id = sprintf("%04d/%02d", $year, $month); # YYYY/MM
my $xx = sprintf("%04d%02d", $year, $month); # YYYYMM
my $fn = "month.$xx.html";
use File::Spec;
my $file = File::Spec->catfile($html_base_dir, $fn);
if (-f $file) {
_print_raw_str($wh, "