summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xfml/lib/Mail/ThreadTrack.pm362
-rw-r--r--fml/lib/Mail/ThreadTrack/Analyze.pm530
-rw-r--r--fml/lib/Mail/ThreadTrack/DB.pm122
-rw-r--r--fml/lib/Mail/ThreadTrack/ErrorStatus.pm111
-rw-r--r--fml/lib/Mail/ThreadTrack/HeaderRewrite.pm72
-rw-r--r--fml/lib/Mail/ThreadTrack/Print.pm288
-rw-r--r--fml/lib/Mail/ThreadTrack/Print/CGI.pm192
-rw-r--r--fml/lib/Mail/ThreadTrack/Print/Text.pm0
8 files changed, 1677 insertions, 0 deletions
diff --git a/fml/lib/Mail/ThreadTrack.pm b/fml/lib/Mail/ThreadTrack.pm
new file mode 100755
index 00000000..0f39fba5
--- /dev/null
+++ b/fml/lib/Mail/ThreadTrack.pm
@@ -0,0 +1,362 @@
+#-*- perl -*-
+#
+# Copyright (C) 2001 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: @template.pm,v 1.1 2001/08/07 12:23:48 fukachan Exp $
+#
+
+package Mail::ThreadTrack;
+use strict;
+use vars qw(@ISA @EXPORT @EXPORT_OK $AUTOLOAD);
+use Carp;
+use Mail::ThreadTrack::ErrorStatus qw(error_set error error_clear);
+
+use Mail::ThreadTrack::Analyze;
+use Mail::ThreadTrack::HeaderRewrite;
+use Mail::ThreadTrack::DB;
+
+@ISA = qw(Mail::ThreadTrack::Analyze
+ Mail::ThreadTrack::DB
+ Mail::ThreadTrack::HeaderRewrite
+ );
+
+
+=head1 NAME
+
+Mail::ThreadTrack - analyze mail threading
+
+=head1 SYNOPSIS
+
+=head1 DESCRIPTION
+
+=head1 METHODS
+
+=head2 C<new($args)>
+
+ $args = {
+ db_base_dir => "/var/spool/ml/\@db\@/ticket",
+ fd => \*STDOUT,
+ config => {
+ ml_name => 'elena',
+ },
+ };
+
+C<db_base_dir> and C<ml_name> in C<config> are mandatory.
+
+=cut
+
+
+# Descriptions: constructor
+# Arguments: $self
+# Side Effects: none
+# Return Value: object
+sub new
+{
+ my ($self, $args) = @_;
+ my ($type) = ref($self) || $self;
+ my $me = {};
+
+ # config
+ my $config = $me->{ _config } = {};
+
+ for my $key (qw(ml_name spool_dir)) {
+ $config->{ $key } = $args->{ config }->{ $key };
+ }
+
+ my $ml_name = $config->{ ml_name };
+ if (defined $ml_name) {
+ $config->{ _ml_name } = $ml_name;
+ }
+ else {
+ croak("specify \$ml_name\n");
+ }
+
+ unless (defined $args->{ ticket_id_syntax }) {
+ $config->{ ticket_id_syntax } = "$ml_name/\%d";
+ }
+
+ unless (defined $args->{ ticket_subject_tag }) {
+ my $id_syntax = $config->{ ticket_id_syntax };
+ $config->{ ticket_subject_tag } = "[$id_syntax]";
+ }
+
+ # database directory used to store thread information et. al.
+ use File::Spec;
+ my $base_dir = $args->{ db_base_dir };
+ $me->{ _db_base_dir } = $base_dir;
+ $me->{ _db_dir } = File::Spec->catfile($base_dir, $ml_name);
+ $me->{ _fd } = $args->{ fd } || \*STDOUT;
+ $me->{ _pcb } = {};
+
+ # initialize directory
+ _init_ticket_db_dir($me);
+
+ return bless $me, $type;
+}
+
+
+sub DESTROY {}
+
+
+# Descriptions: "mkdir -p" or "mkdirhier"
+# Arguments: directory [file_mode]
+# Side Effects: set $ErrorString
+# Return Value: succeeded to create directory or not
+sub _mkdirhier
+{
+ my ($dir, $mode) = @_;
+ $mode = defined $mode ? $mode : 0700;
+
+ error_clear();
+
+ # XXX $mode (e.g. 0755) should be a numeric not a string
+ eval q{
+ use File::Path;
+ mkpath($dir, 0, $mode);
+ };
+
+ return ($@ ? undef : 1);
+}
+
+
+# Descriptions: set up directory which is taken from
+# $self->{ _db_dir }
+# Arguments: $self $curproc $args
+# Side Effects: create a "_db_dir" directory if needed
+# Return Value: 1 (success) or undef (fail)
+sub _init_ticket_db_dir
+{
+ my ($self, $args) = @_;
+
+ if (defined $self->{ _db_dir }) {
+ my $db_dir = $self->{ _db_dir };
+ unless (-d $db_dir) {
+ _mkdirhier($db_dir) || do {
+ croak("cannot make \$db_dir=$db_dir\n");
+ };
+ }
+ }
+ else {
+ croak("no \$db_dir\n");
+ }
+
+ return 1;
+}
+
+
+=head2 C<increment_id(file)>
+
+increment ticket number which is taken up from C<file>
+and save its new number to C<file>.
+
+=cut
+
+
+# Descriptions: increment ticket number $id holded in $seq_file
+# Arguments: $self $seq_file
+# Side Effects: increment id holded in $seq_file
+# Return Value: number
+sub increment_id
+{
+ my ($self, $seq_file) = @_;
+ my $seq = 0;
+
+ $self->db_open();
+
+ # prepare hash table tied to db_dir/*db's
+ my $rh = $self->{ _hash_table };
+
+ if (defined $rh->{ _info }->{ sequence }) {
+ $rh->{ _info }->{ sequence }++;
+ return $rh->{ _info }->{ sequence };
+ }
+ else {
+ $seq = $rh->{ _info }->{ sequence } = 1;
+ }
+
+ $self->db_close();
+
+ return $seq;
+}
+
+
+=head2 list_up_ticket_id()
+
+return @ticket_id ARRAY
+
+=cut
+
+# return @ticket_id ARRAY
+sub list_up_ticket_id
+{
+ my ($self) = @_;
+ my ($tid, $status, @ticket_id);
+
+ # self->{ _hash_table } is tied to DB's.
+ $self->db_open();
+
+ my $rh_status = $self->{ _hash_table }->{ _status };
+ my $mode = 'default';
+
+ TICEKT_LIST:
+ while (($tid, $status) = each %$rh_status) {
+ if ($mode eq 'default') {
+ next TICEKT_LIST if $status =~ /close/o;
+ }
+
+ push(@ticket_id, $tid);
+ }
+
+ $self->db_close();
+
+ \@ticket_id;
+}
+
+
+=head2 sort($ticket_id_list)
+
+=cut
+
+
+sub sort
+{
+ my ($self, $ticket_id_list) = @_;
+
+ # get age HASH TABLE
+ my ($age, $cost) = $self->_calculate_age($ticket_id_list);
+ $self->{ _age } = $age;
+ $self->{ _cost } = $cost;
+
+ $self->_sort_ticket_id($ticket_id_list, $cost);
+}
+
+
+sub _sort_ticket_id
+{
+ my ($self, $ticket_id_list, $cost) = @_;
+
+ @$ticket_id_list = sort {
+ $cost->{$b} cmp $cost->{$a};
+ } @$ticket_id_list;
+}
+
+
+sub _calculate_age
+{
+ my ($self, $ticket_id_list) = @_;
+ my (%age, %cost) = ();
+ my $now = time; # save the current UTC for convenience
+ my $rh = $self->{ _hash_table } || {};
+ my $day = 24*3600;
+
+ # $age hash referehence = { $ticket_id => $age };
+ my (@aid, $last, $age, $date, $status, $tid) = ();
+ for $tid (sort @$ticket_id_list) {
+ # $last: get the latest one of article_id's
+ (@aid) = split(/\s+/, $rh->{ _articles }->{ $tid });
+ $last = $aid[ $#aid ] || 0;
+
+ # how long this ticket is not concerned ?
+ $age = sprintf("%2.1f%s", ($now - $rh->{ _date }->{ $last })/$day);
+ $age{ $tid } = $age;
+
+ # evaluate cost hash table which is { $ticket_id => $cost }
+ $cost{ $tid } = $rh->{ _status }->{ $tid }.'-'. $age;
+ }
+
+ return (\%age, \%cost);
+}
+
+
+=head2 set_mode($mode)
+
+specify output format by $mode string.
+"text" and "html" are available.
+"text" by default.
+
+=head2 get_mode()
+
+get output format.
+
+=cut
+
+
+# Descriptions: set output format
+# Arguments: $self $string
+# Side Effects: none
+# Return Value: string
+sub set_mode
+{
+ my ($self, $mode) = @_;
+ $self->{ _mode } = $mode || 'text';
+}
+
+
+# Descriptions: set output format
+# Arguments: $self
+# Side Effects: none
+# Return Value: string
+sub get_mode
+{
+ my ($self) = @_;
+ return(defined $self->{ _mode } ? $self->{ _mode } : undef);
+}
+
+
+#
+# DEBUG
+#
+if ($0 eq __FILE__) {
+ eval q{
+ my $args = {
+ db_base_dir => "/var/spool/ml/\@db\@/ticket",
+ fd => \*STDOUT,
+ config => {
+ ml_name => 'elena',
+ spool_dir => '/var/spool/ml/elena/spool',
+ },
+ };
+
+ for my $f (@ARGV) {
+ use Mail::Message;
+ my $fh = new FileHandle $f;
+ my $msg = Mail::Message->parse( { fd => $fh } );
+
+ my $ticket = new Mail::ThreadTrack $args;
+ $ticket->analyze($msg);
+
+ use Mail::ThreadTrack::Print;
+ push(@ISA, 'Mail::ThreadTrack::Print');
+ $ticket->show_summary();
+
+ use Data::Dumper;
+ print Dumper( $ticket );
+ }
+
+ };
+ croak($@) if $@;
+}
+
+
+=head1 AUTHOR
+
+Ken'ichi Fukamachi
+
+=head1 COPYRIGHT
+
+Copyright (C) 2001 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.
+
+=head1 HISTORY
+
+Mail::ThreadTrack appeared in fml5 mailing list driver package.
+See C<http://www.fml.org/> for more details.
+
+=cut
+
+
+1;
diff --git a/fml/lib/Mail/ThreadTrack/Analyze.pm b/fml/lib/Mail/ThreadTrack/Analyze.pm
new file mode 100644
index 00000000..ec395f2b
--- /dev/null
+++ b/fml/lib/Mail/ThreadTrack/Analyze.pm
@@ -0,0 +1,530 @@
+#-*- perl -*-
+#
+# Copyright (C) 2001 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: @template.pm,v 1.1 2001/08/07 12:23:48 fukachan Exp $
+#
+
+package Mail::ThreadTrack::Analyze;
+use strict;
+use vars qw(@ISA @EXPORT @EXPORT_OK $AUTOLOAD);
+use Carp;
+
+=head1 NAME
+
+Mail::ThreadTrack::Analyze - analyze mail thread relation
+
+=head1 SYNOPSIS
+
+ my $ticket = $pkg->new($args);
+ if (defined $ticket) {
+ $ticket->analyze($msg);
+ $ticket->rewrite_header($msg);
+ }
+
+=head1 DESCRIPTION
+
+=head1 METHODS
+
+=cut
+
+
+=head2 C<analyze($mesg)>
+
+C<$mesg> is Mail::Message object.
+
+1) assign a new ticket or extract the existing ticket-id from the subject.
+
+2) update ticket status if needed.
+
+=cut
+
+
+sub analyze
+{
+ my ($self, $msg) = @_;
+ $self->_assign($msg);
+ $self->update_ticket_status($msg);
+ $self->update_db($msg);
+}
+
+
+=head2 assign($msg)
+
+=cut
+
+
+sub _is_reply
+{
+ my ($subject) = @_;
+
+ use Mail::Message::Language::Japanese::Subject;
+ return Mail::Message::Language::Japanese::Subject::is_reply($subject);
+}
+
+
+# Descriptions: assign a new ticket or
+# extract the existing ticket-id from the subject
+# Arguments: $self $curproc $args
+# Side Effects: a new ticket_id may be assigned
+# article header is rewritten
+# Return Value: none
+sub _assign
+{
+ my ($self, $msg) = @_;
+ my $config = $self->{ _config };
+ my $header = $msg->rfc822_message_header();
+ my $subject = $header->get('subject');
+ my $pcb = $self->{ _pcb };
+ my $is_reply = _is_reply($subject);
+
+ # 1. try to extract $ticket_id from subject: field
+ my $ticket_id = $self->_extract_ticket_id_in_subject($header, $config);
+ unless ($ticket_id) {
+ # 1.1 hmm, we tail to but we try to speculate ticket_id
+ # from other header information.
+ $ticket_id = $self->_speculate_ticket_id($msg);
+ if ($ticket_id) {
+ $is_reply = 1;
+ $self->{ _ticket_id } = $ticket_id;
+ Log("speculated id=$ticket_id");
+ }
+ else {
+ Log("(debug) fail to spelucate ticket_id");
+ }
+ }
+
+ # 2. check "X-Ticket-Pragma:" field,
+ # we ignore this mail if the pragma is specified as "ignore".
+ if (defined $header->get('x-ticket-pragma')) {
+ my $pragma = $header->get('x-ticket-pragma') || '';
+ if ($pragma =~ /ignore/i) {
+ $self->{ _pragma } = 'ignore';
+ $self->_append_ticket_status_info("ignored");
+ return undef;
+ }
+ }
+
+ # if the header carries "Subject: Re: ..." with ticket-id,
+ # we do not rewrite the subject but save the extracted $ticket_id.
+ if ($is_reply && $ticket_id) {
+ Log("reply message with ticket_id=$ticket_id");
+ $self->{ _ticket_id } = $ticket_id;
+ $self->{ _status } = 'analyzed';
+ $self->_append_ticket_status_info('analyzed');
+ }
+ elsif ($ticket_id) {
+ Log("usual message with ticket_id=$ticket_id");
+ $self->{ _ticket_id } = $ticket_id;
+ $self->_append_ticket_status_info("found");
+ }
+ else {
+ Log("message with no ticket_id");
+
+ # assign a new ticket number for a new message
+ my $id = $self->increment_id();
+
+ # O.K. rewrite Subject: of the article to distribute
+ unless ($self->error) {
+ $pcb->{'article'}->{'id'} = $id; # save $id info in PCB
+
+ my $header = $msg->rfc822_message_header();
+ $self->_get_ticket_id($header, $config, $id);
+ $self->_rewrite_header($header, $config, $id);
+ $self->_append_ticket_status_info("newly assigned");
+ }
+ else {
+ Log( $self->error );
+ }
+ }
+}
+
+
+# For example, consider a posting to both elena ML and rudo (DM) from kenken.
+#
+# From: kenken
+# To: elena-ml
+# Cc: rudo
+#
+# The reply to this DM (direct message) from rudo is
+#
+# From: rudo
+# To: elena-ml
+#
+# This reply message has no ticket_id since the message from kenken to
+# rudo comes directly from kenken not through fml driver.
+# In this case, we try to speculdate the reply relation and the ticket_id
+# of this thread by using _speculate_ticket_id().
+#
+sub _speculate_ticket_id
+{
+ my ($self, $msg) = @_;
+ my $header = $msg->rfc822_message_header();
+ my $midlist = _extract_message_id_references( $header );
+ my $result = '';
+
+ for (@$midlist) { Log("(debug) mid=$_");}
+
+ if (defined $midlist) {
+ $self->db_open();
+
+ # prepare hash table tied to db_dir/*db's
+ my $rh = $self->{ _hash_table };
+
+ for my $mid (@$midlist) {
+ $result = $rh->{ _message_id }->{ $mid };
+ last if $result;
+ }
+
+ $self->db_close();
+ }
+
+ Log("(debug) not speculated") unless $result;
+ $result;
+}
+
+
+sub _append_ticket_status_info
+{
+ my ($self, $s) = @_;
+ $self->{ _status_info } .= $self->{ _status_info } ? " -> ".$s : $s;
+}
+
+
+=head2 update_ticket_status($msg)
+
+=cut
+
+
+sub update_ticket_status
+{
+ my ($self, $msg) = @_;
+
+ return if $self->{ _pragma } eq 'ignore';
+
+ # entries to check
+ my $header = $msg->rfc822_message_header();
+ my $subject = $header->get('subject');
+ my $pragma = $header->get('x-ticket-pragma') || '';
+
+ my $content = '';
+ my $message = $msg->get_first_plaintext_message();
+ if (ref($message) eq 'Mail::Message') {
+ $content = $message->data_in_body_part();
+ }
+ else {
+ croak("invalid object");
+ }
+
+ if ($content =~ /^\s*close/ ||
+ $subject =~ /^\s*close/ ||
+ $pragma =~ /close/ ) {
+ $self->{ _status } = "closed";
+ $self->_append_ticket_status_info("closed");
+ Log("ticket is closed");
+ }
+ else {
+ Log("ticket status not changed");
+ }
+}
+
+
+# Descriptions: create regexp for a subject tag, for example
+# "[%s %05d]" => "\[\S+ \d+\]"
+# Arguments: a subject tag string
+# XXX non OO type function
+# Side Effects: none
+# Return Value: a regexp for the given tag
+sub _regexp_compile
+{
+ my ($s) = @_;
+
+ $s = quotemeta( $s );
+ $s =~ s@\\\%@\%@g;
+ $s =~ s@\%s@\\S+@g;
+ $s =~ s@\%d@\\d+@g;
+ $s =~ s@\%0\d+d@\\d+@g;
+ $s =~ s@\%\d+d@\\d+@g;
+ $s =~ s@\%\-\d+d@\\d+@g;
+
+ # quote for regexp substitute: [ something ] -> \[ something \]
+ # $s =~ s/^(.)/quotemeta($1)/e;
+ # $s =~ s/(.)$/quotemeta($1)/e;
+
+ return $s;
+}
+
+
+sub _extract_message_id_references
+{
+ my ($header) = @_;
+ my $buf =
+ $header->get('in-reply-to') ."\n". $header->get('references');
+
+ use Mail::Address;
+ my @addrs = Mail::Address->parse($buf);
+
+ my @r = ();
+ my %uniq = ();
+ foreach my $addr (@addrs) {
+ my $a = $addr->address;
+ unless ($uniq{ $a }) {
+ push(@r, $addr->address);
+ $uniq{ $a } = 1;
+ }
+ }
+
+ \@r;
+}
+
+
+sub _extract_ticket_id_in_subject
+{
+ my ($self, $header, $config) = @_;
+ my $tag = $config->{ ticket_subject_tag };
+ my $subject = $header->get('subject');
+ my $regexp = _regexp_compile($tag);
+
+ # Subject: ... [ticket_id]
+ if (($config->{ ticket_subject_tag_location } eq 'appended') &&
+ ($subject =~ /($regexp)\s*$/)) {
+ my $id = $1;
+ $id =~ s/^(\[|\(|\{)//;
+ $id =~ s/(\]|\)|\})$//;
+ return $id;
+ }
+ # XXX incomplete, we check subject after cutting off "Re:" et. al.
+ # Subject: [ticket_id] ...
+ # Subject: Re: [ticket_id] ...
+ elsif (($config->{ ticket_subject_tag_location } eq 'appended') &&
+ ($subject =~ /^\s*($regexp)/)) {
+ my $id = $1;
+ $id =~ s/^(\[|\(|\{)//;
+ $id =~ s/(\]|\)|\})$//;
+ return $id;
+ }
+ else {
+ Log("no ticket id /$regexp/ in subject");
+ return 0;
+ }
+}
+
+
+sub _get_ticket_id
+{
+ my ($self, $header, $config, $id) = @_;
+ my $subject_tag = $config->{ ticket_subject_tag };
+ my $id_syntax = $config->{ ticket_id_syntax };
+
+ # ticket_id in subject
+ my $ticket_id = sprintf($subject_tag, $id);
+ $self->{ _ticket_subject_tag } = $ticket_id;
+
+ $ticket_id = sprintf($id_syntax, $id);
+ $self->{ _ticket_id } = $ticket_id;
+
+ return $ticket_id;
+}
+
+
+sub _rewrite_header
+{
+ my ($self, $header, $config, $id) = @_;
+
+ # append the ticket tag to the subject
+ my $subject = $header->get('subject') || '';
+ $header->replace('Subject',
+ $subject." " . $self->{ _ticket_subject_tag });
+}
+
+
+# clean up given C<address>.
+# It parse it by C<Mail::Address::parse()> and nuke < and >.
+sub _address_clean_up
+{
+ my ($self, $addr) = @_;
+
+ use Mail::Address;
+ my @addrlist = Mail::Address->parse($addr);
+
+ # only the first element in the @addrlist array is effective.
+ $addr = $addrlist[0]->address;
+ $addr =~ s/^\s*<//;
+ $addr =~ s/>\s*$//;
+
+ # return the result.
+ return $addr;
+}
+
+
+=head2 update_db($msg)
+
+=cut
+
+
+sub update_db
+{
+ my ($self, $msg) = @_;
+ my $config = $self->{ _config };
+ my $ml_name = $config->{ ml_name };
+
+ return if $self->{ _pragma } eq 'ignore';
+
+ $self->db_open();
+
+ # save $ticke_id et.al. in db_dir/$ml_name
+ $self->_update_db($msg);
+
+ # save cross reference pointers among $ml_name
+ $self->_update_index_db();
+
+ $self->db_close();
+}
+
+
+sub _update_db
+{
+ my ($self, $msg) = @_;
+ my $config = $self->{ _config };
+ my $pcb = $self->{ _pcb };
+ my $article_id = $pcb->{'article'}->{'id'};
+ my $ticket_id = $self->{ _ticket_id };
+
+ # 0. logging
+ Log("article_id=$article_id ticket_id=$ticket_id");
+
+ # prepare hash table tied to db_dir/*db's
+ my $rh = $self->{ _hash_table };
+
+ # 1.
+ $rh->{ _ticket_id }->{ $article_id } = $ticket_id;
+ $rh->{ _date }->{ $article_id } = time;
+ $rh->{ _articles }->{ $ticket_id } .= $article_id . " ";
+
+ # 2. record the sender information
+ my $header = $msg->rfc822_message_header;
+ $rh->{ _sender }->{ $article_id } = $header->get('from');
+
+ # 3. update status information
+ if (defined $self->{ _status }) {
+ $self->_set_status($ticket_id, $self->{ _status });
+ }
+ else {
+ # set the default status value for the first time.
+ unless (defined $rh->{ _status }->{ $ticket_id }) {
+ $self->_set_status($ticket_id, 'open');
+ }
+ }
+
+ # 4. save optional/additional information
+ # message_id hash is { message_id => ticket_id };
+ my $mid = $header->get('message-id');
+ $mid = $self->_address_clean_up($mid);
+ $rh->{ _message_id }->{ $mid } = $ticket_id;
+
+ # 5. history
+ my $buf = '';
+ my (@aid) = split(/\s+/, $rh->{ _articles }->{ $ticket_id });
+ my $sender = $rh->{ _sender }->{ $aid[0] };
+ my $when = $rh->{ _date }->{ $aid[0] };
+
+ # clean up
+ $sender =~ s/[\s\n]*$//;
+ $when =~ s/[\s\n]*$//;
+
+ use Mail::Message::Date;
+ $when = Mail::Message::Date->new($when)->mail_header_style();
+
+ $buf .= "\t\n";
+ $buf .= "\tthis ticket/thread is opended at article $aid[0]\n";
+ $buf .= "\tby $sender\n";
+ $buf .= "\ton $when\n";
+ $buf .= "\tarticle references: @aid\n";
+ $self->{ _status_history } = $buf;
+}
+
+
+# register myself to index_db for further reference among mailing lists
+sub _update_index_db
+{
+ my ($self) = @_;
+ my $config = $self->{ _config };
+ my $ticket_id = $self->{ _ticket_id };
+ my $rh = $self->{ _hash_table };
+ my $ml_name = $config->{ ml_name };
+
+ my $ref = $rh->{ _index }->{ $ticket_id } || '';
+ if ($ref !~ /^$ml_name|\s$ml_name\s|$ml_name$/) {
+ $rh->{ _index }->{ $ticket_id } .= $ml_name." ";
+ }
+}
+
+
+=head2 C<set_status($args)>
+
+set $status for $ticket_id. It rewrites DB (file).
+C<$args>, HASH reference, must have two keys.
+
+ $args = {
+ ticket_id => $ticket_id,
+ status => $status,
+ }
+
+C<set_status()> calls db_open() an db_close() automatically within it.
+
+=cut
+
+
+# Descriptions:
+# Arguments: $self $curproc $args
+# Side Effects:
+# Return Value: none
+sub set_status
+{
+ my ($self, $args) = @_;
+ my $ticket_id = $args->{ ticket_id };
+ my $status = $args->{ status };
+
+ Log("ticket.set_status($ticket_id, $status)");
+
+ $self->db_open();
+ $self->_set_status($ticket_id, $status);
+ $self->db_close();
+}
+
+
+sub _set_status
+{
+ my ($self, $ticket_id, $value) = @_;
+ $self->{ _hash_table }->{ _status }->{ $ticket_id } = $value;
+}
+
+
+sub Log
+{
+ print STDERR "Log> @_\n";
+}
+
+
+=head1 AUTHOR
+
+Ken'ichi Fukamachi
+
+=head1 COPYRIGHT
+
+Copyright (C) 2001 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.
+
+=head1 HISTORY
+
+Mail::ThreadTrack::Analyze appeared in fml5 mailing list driver package.
+See C<http://www.fml.org/> for more details.
+
+=cut
+
+
+1;
diff --git a/fml/lib/Mail/ThreadTrack/DB.pm b/fml/lib/Mail/ThreadTrack/DB.pm
new file mode 100644
index 00000000..87d4ba02
--- /dev/null
+++ b/fml/lib/Mail/ThreadTrack/DB.pm
@@ -0,0 +1,122 @@
+#-*- perl -*-
+#
+# Copyright (C) 2001 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: @template.pm,v 1.1 2001/08/07 12:23:48 fukachan Exp $
+#
+
+package Mail::ThreadTrack::DB;
+use strict;
+use vars qw(@ISA @EXPORT @EXPORT_OK $AUTOLOAD);
+use Carp;
+
+=head1 NAME
+
+Mail::ThreadTrack::DB - what is this
+
+=head1 SYNOPSIS
+
+=head1 DESCRIPTION
+
+=head1 METHODS
+
+=cut
+
+=head2 C<db_open()>
+
+open DB.
+It uses tie() to bind a hash to a DB file.
+Our minimal_states uses several DB files for
+C<%ticket_id>,
+C<%date>,
+C<%status>,
+C<%sender>,
+C<%articles>,
+C<%message_id>
+and
+C<%index>.
+
+=head2 C<db_close()>
+
+untie() corresponding hashes opened by C<db_open()>.
+
+=cut
+
+
+my @kind_of_databases = qw(ticket_id
+ info
+ date
+ status
+ sender
+ articles
+ message_id
+ index);
+
+
+sub db_open
+{
+ my ($self) = @_;
+ my $db_type = $self->{ config }->{ ticket_db_type } || 'AnyDBM_File';
+ my $db_dir = $self->{ _db_dir };
+
+ my $index_file = $self->{ _index_db };
+
+ eval qq{ use $db_type; use Fcntl;};
+ unless ($@) {
+ for my $db (@kind_of_databases) {
+ my $file = "$db_dir/${db}";
+ my $str = qq{
+ my \%$db = ();
+ tie \%$db, \$db_type, \$file, O_RDWR|O_CREAT, 0644;
+ \$self->{ _hash_table }->{ _$db } = \\\%$db;
+ };
+ eval $str;
+ croak($@) if $@;
+ }
+ }
+ else {
+ croak("cannot use $db_type");
+ }
+
+ 1;
+}
+
+
+sub db_close
+{
+ my ($self) = @_;
+
+ for my $db (@kind_of_databases) {
+ my $str = qq{
+ my \$${db} = \$self->{ _hash_table }->{ _$db };
+ untie \%\$${db};
+ };
+ eval $str;
+ croak($@) if $@;
+ }
+}
+
+
+
+=head1 AUTHOR
+
+Ken'ichi Fukamachi
+
+=head1 COPYRIGHT
+
+Copyright (C) 2001 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.
+
+=head1 HISTORY
+
+Mail::ThreadTrack::DB appeared in fml5 mailing list driver package.
+See C<http://www.fml.org/> for more details.
+
+=cut
+
+
+1;
diff --git a/fml/lib/Mail/ThreadTrack/ErrorStatus.pm b/fml/lib/Mail/ThreadTrack/ErrorStatus.pm
new file mode 100644
index 00000000..670f0a76
--- /dev/null
+++ b/fml/lib/Mail/ThreadTrack/ErrorStatus.pm
@@ -0,0 +1,111 @@
+#-*- perl -*-
+#
+# Copyright (C) 2001 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: Mail::ThreadTrack::ErrorStatus.pm,v 1.1 2001/04/08 13:25:37 fukachan Exp $
+#
+
+package Mail::ThreadTrack::ErrorStatus;
+
+use strict;
+use vars qw(@ISA @EXPORT @EXPORT_OK);
+use Carp;
+
+require Exporter;
+@ISA = qw(Exporter);
+@EXPORT_OK = qw(errstr error error_set error_clear);
+
+=head1 NAME
+
+Mail::ThreadTrack::ErrorStatus - error handling component
+
+=head1 SYNOPSIS
+
+Use this module in your C<Something> class module like this:
+
+ package Something;
+ use Mail::ThreadTrack::ErrorStatus qw(errstr error error_set error_clear);
+
+ sub xxx
+ {
+ if something errors ...
+ $self->error_set( why this error occurs ... );
+ }
+
+You use C<Something> module like this.
+
+ use Something;
+ $obj = new Something;
+ $obj->xxx();
+ unless ($obj->error) { $obj->do_somting( ...); };
+
+=head1 DESCRIPTION
+
+simple utility functions to manipulate error messages.
+
+=head1 METHODS
+
+=head2 C<error_set($message)>
+
+save $message as an error message.
+
+=head2 C<error()>
+
+return $message which is saved by C<error_set($msg)>.
+
+=cut
+
+
+sub error_set
+{
+ my ($self, $mesg) = @_;
+ $self->{'_error_reason'} = $mesg;
+}
+
+
+sub error
+{
+ my ($self, $args) = @_;
+ return $self->{'_error_reason'};
+}
+
+
+sub errstr
+{
+ my ($self, $args) = @_;
+ return $self->{'_error_reason'};
+}
+
+
+sub error_clear
+{
+ my ($self, $args) = @_;
+ my $msg = $self->{'_error_reason'};
+ undef $self->{'_error_reason'} if defined $self->{'_error_reason'};
+ undef $self->{'_error_action'} if defined $self->{'_error_action'};
+ return $msg;
+}
+
+
+=head1 AUTHOR
+
+Ken'ichi Fukamachi
+
+=head1 COPYRIGHT
+
+Copyright (C) 2001 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.
+
+=head1 HISTORY
+
+Mail::ThreadTrack::ErrorStatus appeared in fml5 mailing list driver package.
+See C<http://www.fml.org/> for more details.
+
+=cut
+
+
+1;
diff --git a/fml/lib/Mail/ThreadTrack/HeaderRewrite.pm b/fml/lib/Mail/ThreadTrack/HeaderRewrite.pm
new file mode 100644
index 00000000..ee6f860a
--- /dev/null
+++ b/fml/lib/Mail/ThreadTrack/HeaderRewrite.pm
@@ -0,0 +1,72 @@
+#-*- perl -*-
+#
+# Copyright (C) 2001 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: @template.pm,v 1.2 2001/10/27 04:27:18 fukachan Exp $
+#
+
+package Mail::ThreadTrack::HeaderRewrite;
+use strict;
+use vars qw(@ISA @EXPORT @EXPORT_OK $AUTOLOAD);
+use Carp;
+
+=head1 NAME
+
+Mail::ThreadTrack::HeaderRewrite - what is this
+
+=head1 SYNOPSIS
+
+=head1 DESCRIPTION
+
+=head1 METHODS
+
+=cut
+
+=head2 C<rewrite_header($msg)>
+
+C<$msg> is Mail::Message object.
+
+=cut
+
+
+sub rewrite_header
+{
+ my ($self, $msg) = @_;
+ my $header = $msg->rfc822_message_header;
+
+ if (defined $self->{ _status_info }) {
+ $header->add('X-Ticket-Status', $self->{ _status_info });
+ }
+
+ if (defined $self->{ _ticket_id }) {
+ $header->add('X-Ticket-ID', $self->{ _ticket_id });
+ }
+
+ if (defined $self->{ _status_history }) {
+ $header->add('X-Ticket-History', $self->{ _status_history });
+ }
+}
+
+
+=head1 AUTHOR
+
+Ken'ichi Fukamachi
+
+=head1 COPYRIGHT
+
+Copyright (C) 2001 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.
+
+=head1 HISTORY
+
+Mail::ThreadTrack::HeaderRewrite appeared in fml5 mailing list driver package.
+See C<http://www.fml.org/> for more details.
+
+=cut
+
+
+1;
diff --git a/fml/lib/Mail/ThreadTrack/Print.pm b/fml/lib/Mail/ThreadTrack/Print.pm
new file mode 100644
index 00000000..d9db0ae5
--- /dev/null
+++ b/fml/lib/Mail/ThreadTrack/Print.pm
@@ -0,0 +1,288 @@
+#-*- perl -*-
+#
+# Copyright (C) 2001 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: @template.pm,v 1.1 2001/08/07 12:23:48 fukachan Exp $
+#
+
+package Mail::ThreadTrack::Print;
+use strict;
+use vars qw(@ISA @EXPORT @EXPORT_OK $AUTOLOAD);
+use Carp;
+
+my $is_show_cost_indicate = 0;
+
+=head1 NAME
+
+Mail::ThreadTrack::Print - print out thread relation
+
+=head1 SYNOPSIS
+
+=head1 DESCRIPTION
+
+=head1 METHODS
+
+=head2 C<show_summary(>)
+
+top level entrance for routines to show the ticket summary.
+
+See L<simple_print()> for more detail.
+Either of
+C<simple_print()>
+or
+C<_summary_print()>
+is used for purposes.
+
+Each row that C<show_summary()> returns has a set of
+C<date>, C<age>, C<status>, C<ticket-id> and
+C<articles>, which is a list of articles with the ticket-id.
+
+=head2 C<simple_print()>
+
+show entries by the ticket_id order. For example,
+
+ date age status ticket id articles
+ ------------------------------------------------------------
+ 2001/02/07 3.6 going elena_#00000450 807 808 809
+ 2001/02/07 3.1 open elena_#00000451 810
+ 2001/02/07 3.0 open elena_#00000452 812
+ 2001/02/07 3.0 open elena_#00000453 813
+ 2001/02/07 3.0 going elena_#00000454 814 815
+ 2001/02/10 0.1 open elena_#00000456 821
+
+=head2 C<_summary_print()>
+
+show entries in the C<cost> larger order.
+The cost is evaluated by $status and $age.
+The cost is larger as $age is larger.
+It is also larger if $status is C<open>.
+
+=cut
+
+
+sub show_summary
+{
+ my ($self) = @_;
+ my ($tid, $status, $ticket_id);
+ my $mode = $self->get_mode || 'text';
+
+ # rh: ticket id list, which is ARRAY REFERENCE tied to db_dir/*db's
+ $ticket_id = $self->list_up_ticket_id();
+
+ # self->{ _hash_table } is tied to DB's.
+ $self->db_open();
+
+ if (@$ticket_id) {
+ # sort the ticket output order it out by cost
+ # and print the ticket summary in that order.
+ $self->sort($ticket_id);
+
+ if ($mode eq 'html') {
+ print "<TABLE BORDER=4>\n" if $mode eq 'html';
+ $self->_print_ticket_summary($ticket_id);
+ print "</TABLE>\n" if $mode eq 'html';
+ }
+ else {
+ $self->_print_ticket_summary($ticket_id);
+
+ # show short summary for each article
+ $self->_print_article_summary($ticket_id);
+ }
+ }
+
+ # self->{ _hash_table } is untied from DB's.
+ $self->db_close();
+}
+
+
+
+sub _print_ticket_summary
+{
+ my ($self, $ticket_id) = @_;
+ my $mode = $self->get_mode || 'text';
+ my $rh_age = $self->{ _age } || {};
+ my $fd = $self->{ _fd } || \*STDOUT;
+ my $rh = $self->{ _hash_table };
+ my $format = "%10s %5s %6s %-20s %s\n";
+
+ if ($mode eq 'text') {
+ printf($fd $format, 'date', 'age', 'status', 'ticket id', 'articles');
+ print $fd "-" x60;
+ print $fd "\n";
+ }
+ else {
+ print "<TD>action\n";
+ print "<TD>date\n"."<TD>age\n"."<TD>status\n"."<TD>ticket id\n";
+ print "<TD>article summary\n";
+ }
+
+ my ($tid, @article_id, $article_id, $date, $age, $status) = ();
+ my $dh = new Mail::Message::Date;
+ for $tid (@$ticket_id) {
+ # get the first $article_id from the article_id list
+ (@article_id) = split(/\s+/, $rh->{ _articles }->{ $tid });
+ $article_id = $article_id[0];
+
+ # determine $date for the $article_id
+ # $age and $status for $ticket_id
+ $date = $dh->YYYYxMMxDD( $rh->{ _date }->{ $article_id } , '/');
+ $age = $rh_age->{ $tid };
+ $status = $rh->{ _status }->{ $tid };
+
+ if ($mode eq 'html') {
+ $self->_show_ticket_by_html_table( {
+ format => $format,
+ date => $date,
+ age => $age,
+ status => $status,
+ tid => $tid,
+ articles => $rh->{ _articles }->{ $tid },
+ });
+ }
+ else {
+ printf($fd $format,
+ $date, $age, $status, $tid, $rh->{ _articles }->{ $tid });
+ }
+ }
+}
+
+
+sub _cost_to_indicator
+{
+ my ($cost) = @_;
+ my $how_bad = 0;
+
+ if ($cost =~ /(\w+)\-(\d+)/) {
+ $how_bad += $2;
+ $how_bad += 2 if $1 =~ /open/;
+ $how_bad = "!" x ($how_bad > 6 ? 6 : $how_bad);
+ }
+}
+
+
+sub _print_article_summary
+{
+ my ($self, $ticket_id) = @_;
+ my $config = $self->{ _config };
+ my $age = $self->{ _age } || {};
+ my $cost = $self->{ _cost } || {};
+ my $fd = $self->{ _fd } || \*STDOUT;
+ my $rh = $self->{ _hash_table };
+
+ if (defined $config->{ spool_dir }) {
+ my ($aid, @aid);
+
+ if ($is_show_cost_indicate) {
+ print $fd "\n\"!\" mark: stalled? please check and reply it.\n";
+ }
+
+ my $spool_dir = $config->{ spool_dir };
+ for my $tid (@$ticket_id) {
+ if ($is_show_cost_indicate) {
+ my $how_bad = _cost_to_indicator( $cost->{ $tid } );
+ printf $fd "\n%6s %-10s %s\n", $how_bad, $tid;
+ }
+ else {
+ printf $fd "\n>Thread-Id: %-10s %s\n", $tid;
+ }
+
+ (@aid) = split(/\s+/, $rh->{ _articles }->{ $tid });
+ $aid = $aid[0];
+ my $file = File::Spec->catfile($spool_dir, $aid);
+ print $fd $self->__article_summary($file);
+ }
+ }
+}
+
+
+sub __article_summary
+{
+ my ($self, $file) = @_;
+ my (@header) = ();
+ my $buf = '';
+ my $line = 3;
+ my $mode = $self->get_mode || 'text';
+ my $padding = $mode eq 'text' ? ' ' : '';
+
+ use FileHandle;
+ my $fh = new FileHandle $file;
+
+ if (defined $fh) {
+ LINE:
+ while (<$fh>) {
+ # nuke useless lines
+ next LINE if /^\>/;
+ next LINE if /^\-/;
+
+ if (1 ../^$/) {
+ push(@header, $_);
+ }
+ else {
+ next LINE if /^\s*$/;
+
+ # ignore mail header like patterns
+ next LINE if /^X-[-A-Za-z0-9]+:/i;
+ next LINE if /^Return-[-A-Za-z0-9]+:/i;
+ next LINE if /^Mime-[-A-Za-z0-9]+:/i;
+ next LINE if /^Content-[-A-Za-z0-9]+:/i;
+ next LINE if /^(To|From|Subject|Reply-To|Received):/i;
+ next LINE if /^(Message-ID|Date):/i;
+
+ # pick up effetive the first $line lines
+ if ($line-- > 0) {
+ $buf .= $padding. $_;
+ }
+ else {
+ last LINE;
+ }
+ }
+ }
+ close($fh);
+ }
+
+ use Mail::Header;
+ my $h = new Mail::Header \@header;
+ my $header_info = $self->_header_summary({
+ header => $h,
+ padding => $padding,
+ });
+
+ return $header_info . $buf;
+}
+
+
+sub _delete_subject_tag_like_string
+{
+ my ($str) = @_;
+ $str =~ s/\W[-\w]+.\s*\d+\W//g;
+ $str =~ s/\s+/ /g;
+ $str =~ s/^\s*//g;
+ $str;
+}
+
+
+sub _header_summary
+{
+ my ($self, $args) = @_;
+ my $from = $args->{ header }->get('from');
+ my $subject = $args->{ header }->get('subject');
+ my $padding = $args->{ padding };
+
+ use FML::MIME qw(decode_mime_string);
+ $subject = decode_mime_string($subject, { charset => 'euc-japan' });
+ $subject =~ s/\n/ /g;
+ $subject = _delete_subject_tag_like_string($subject);
+
+ $from = decode_mime_string($from, { charset => 'euc-japan' });
+ $from =~ s/\n/ /g;
+
+ my $br = $self->get_mode eq 'html' ? '<BR>' : '';
+ return
+ $padding. " From: ". $from ."$br\n".
+ $padding. "Subject: ". $subject ."$br\n";
+}
+
+
+1;
diff --git a/fml/lib/Mail/ThreadTrack/Print/CGI.pm b/fml/lib/Mail/ThreadTrack/Print/CGI.pm
new file mode 100644
index 00000000..6fe0e844
--- /dev/null
+++ b/fml/lib/Mail/ThreadTrack/Print/CGI.pm
@@ -0,0 +1,192 @@
+sub STR2EUC
+{
+ my ($str) = @_;
+
+ use Jcode;
+ &Jcode::convert(\$str, 'euc');
+ return $str;
+}
+
+
+sub show_articles_for_ticket
+{
+ my ($self, $ticket_id) = @_;
+ my $mode = $self->get_mode || 'text';
+ my $config = $self->{ _config };
+ my $spool_dir = $config->{ spool_dir };
+
+ $self->db_open();
+
+ my $articles = $self->{ _hash_table }->{ _articles }->{ $ticket_id };
+
+ print "<B>";
+ print "show contents related with ticket_id=$ticket_id\n";
+ print "</B>";
+ print "<HR>";
+ print "<PRE>\n";
+
+ if (defined($articles) && defined($spool_dir) && -d $spool_dir) {
+ use FileHandle;
+
+ my $s = '';
+ for (split(/\s+/, $articles)) {
+ my $file = File::Spec->catfile($spool_dir, $_);
+ my $fh = new FileHandle $file;
+ while (defined($_ = $fh->getline())) {
+ next if 1 .. /^$/;
+
+ $s = STR2EUC($_);
+ $s =~ s/&/&amp;/g;
+ $s =~ s/</&lt;/g;
+ $s =~ s/>/&gt;/g;
+ $s =~ s/\"/&quot;/g;
+ print $s;
+ }
+ $fh->close;
+ }
+ }
+
+ print "</PRE>";
+
+ $self->db_close();
+}
+
+
+sub cgi_top_menu
+{
+ my ($self) = @_;
+ my $config = $self->{ _config };
+ my $action = 'fmlticket.cgi';
+ my $target = $config->{ ticket_cgi_target_window } || 'TicketCGIWindow';
+
+ use DirHandle;
+ my $dh = new DirHandle $config->{ ml_home_prefix };
+ my @dirlist;
+ my $prefix = $config->{ ml_home_prefix };
+ while ($_ = $dh->read()) {
+ next if /^\./;
+ next if /^\@/;
+ push(@dirlist, $_) if -f "$prefix/$_/config.cf";
+ }
+ $dh->close;
+
+ if ($self->get_mode eq 'html') {
+ require 'ctime.pl';
+ my $time = ctime(time);
+ my $ml_name = $config->{ ml_name };
+ print "[$time] the brief summary for \"$ml_name\" ML<BR>";
+ }
+
+ use CGI qw/:standard/;
+ print start_form(-action=>$action, -target=>$target);
+ print "mailing list: ",
+ popup_menu(-name => 'ml_name', -values => \@dirlist),
+ submit(-name => 'go'),
+ end_form;
+}
+
+
+
+# This shows summary on C<$ticket_id> in HTML language.
+# It is used in C<FML::CGI::TicketSystem>.
+sub _show_ticket_by_html_table
+{
+ my ($self, $optargs) = @_;
+ my $config = $self->{ _config };
+ my $ml_name = $config->{ ml_name };
+ my $spool_dir = $config->{ spool_dir };
+ my $action = 'fmlticket.cgi';
+ my $target = $config->{ ticket_cgi_target_window } || 'TicketCGIWindow';
+
+ # printf($fd $format,
+ # $date, $age, $status, $tid, $rh->{ _articles }->{ $tid });
+ my $format = $optargs->{ format };
+ my $date = $optargs->{ date };
+ my $age = $optargs->{ age };
+ my $status = $optargs->{ status };
+ my $tid = $optargs->{ tid };
+ my $articles = $optargs->{ articles };
+ my $aid = (split(/\s+/, $articles))[0];
+
+ # do nothing if the $ticket_id is unknown.
+ return unless $tid;
+
+ # <FORM ACTION=> ..>
+ my $xtid = CGI::escape($tid);
+ $action = "${action}?ml_name=${ml_name}";
+ $action .= "&ticket_id=$xtid&article_id=$aid";
+
+ print "<TR>\n";
+ print "<TD>";
+ print "<A HREF=\"$action&action=close\" TARGET=\"$target.close\">";
+ print "[close]</A>\n";
+ print "<BR>\n";
+ print "<A HREF=\"$action&action=show\" TARGET=\"$target.show\">";
+ print "[see articles]</A>\n";
+ print "<TD>$date\n";
+ print "<TD>$age\n";
+ print "<TD>$status\n";
+ print "<TD>$tid\n";
+ print "<TD>";
+
+ $aid = (split(/\s+/, $articles))[0];
+ my $buf = $self->_article_summary(File::Spec->catfile($spool_dir, $aid));
+ print STR2EUC($buf);
+}
+
+
+=head2 C<run_cgi()>
+
+execute CGI.
+
+=cut
+
+sub run_cgi
+{
+ my ($self) = @_;
+ my $config = $self->{ _config };
+ my $title = $config->{ ticket_cgi_title } || 'ticket system interface';
+ my $color = $config->{ ticket_cgi_bgcolor } || '#E6E6FA';
+
+ # XXX $ml_name may change by HTTP request
+ $config->{ ml_name } = param('ml_name') if param('ml_name');
+
+ # ensure the current mode
+ $self->mode('html');
+
+ # load standard CGI routines
+ use CGI qw/:standard/;
+
+ # get action parameter via HTTP
+ my $action = param('action') || 'list';
+ my $ticket_id = param('ticket_id');
+
+ # o.k start html
+ print start_html(-title=>$title,-BGCOLOR=>$color), "\n";
+
+ if ($action eq 'close') {
+ $self->set_status({
+ ticket_id => $ticket_id,
+ status => 'closed',
+ });
+ }
+
+ if ($action eq 'show') {
+ Log("run.cgi.show_articles for $ticket_id");
+ $self->show_articles_for_ticket($ticket_id);
+ }
+ else {
+ # menu at the top of scrren
+ $self->cgi_top_menu();
+
+ # show summary
+ $self->show_summary();
+ }
+
+ # o.k. end of html
+ print end_html;
+ print "\n";
+}
+
+
+1;
diff --git a/fml/lib/Mail/ThreadTrack/Print/Text.pm b/fml/lib/Mail/ThreadTrack/Print/Text.pm
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/fml/lib/Mail/ThreadTrack/Print/Text.pm