#!/usr/bin/perl package Mail::Summary::Tools::Summary::Thread; use Moose; use Mail::Summary::Tools::ArchiveLink::Easy; use Mail::Summary::Tools::ArchiveLink::Hardcoded; use Mail::Address; has subject => ( isa => "Str", is => "rw", required => 1, ); has message_id => ( isa => "Str", is => "rw", required => 1, ); has hidden => ( isa => "Bool|Str", is => "rw", required => 0, ); has extra => ( isa => "HashRef", is => "rw", default => sub { return { } }, required => 0, ); has summary => ( isa => "Str", is => "rw", required => 0, default => "", ); has default_archive => ( isa => "Str", is => "rw", default => "google", ); has archive_link => ( isa => "Mail::Summary::Tools::ArchiveLink", is => "rw", lazy => 1, default => sub { $_[0]->make_archive_link }, ); has archive_link_params => ( isa => "HashRef", is => "rw", auto_deref => 1, default => sub { return {} }, ); sub _extract_name { # this is a slightly less eager _extract_name from Mail::Address # that version was mean to chromatic, making him turn out as Chromatic # the case changing logic has been removed my ( $self, $name ) = @_; local $_ = $name; # trim whitespace s/^\s+//; s/\s+$//; s/\s+/ /; # Disregard numeric names (e.g. 123456.1234@compuserve.com) return "" if /^[\d ]+$/; # remove outermost parenthesis s/^\((.*)\)$/$1/; # remove outer quotation marks s/^"(.*)"$/$1/; # remove minimal embedded comments s/\(.*?\)//g; # remove all escapes s/\\//g; # remove internal quotation marks s/^"(.*)"$/$1/; # reverse "Last, First M." if applicable s/^([^\s]+) ?, ?(.*)$/$2 $1/; s/,.*//; # some cleanup s/\[[^\]]*\]//g; s/(^[\s'"]+|[\s'"]+$)//g; s/\s{2,}/ /g; return $_; } sub from_mailbox_thread { my ( $class, $thread, %options ) = @_; my @messages = $thread->threadMessages; my $root = $messages[0]; my $subject = $root->subject; $subject = $options{process_subject}->($subject) if $options{process_subject}; my %extra; if ( $options{collect_posters} ) { my @from_fields = map { $_->head->get('From')->study } @messages; my %seen_email; my @addresses = grep { !$seen_email{$_->address}++ } map { Mail::Address->parse($_->decodedBody) } @from_fields; my @posters = map {{ name => $class->_extract_name($_->phrase) || $class->_extract_name($_->comment) || $_->user, email => $_->address, }} @addresses; $extra{posters} = \@posters; } if ( $options{collect_dates} ) { $extra{date_from} = $thread->startTimeEstimate; $extra{date_to} = $thread->endTimeEstimate; } if ( $options{collect_rt} ) { eval { $extra{rt_ticket} = $root->head->get('RT-Ticket')->unfoldedBody }; } my @message_ids = grep { $_ ne $root->messageId } map { $_->messageId } @messages; $extra{messages} = \@message_ids if @message_ids; $class->new( subject => $subject, message_id => $root->messageId, extra => \%extra, ); } sub load { my ( $class, $hash, %options ) = @_; my @good_keys = qw/summary message_id subject hidden/; my %hash = %$hash; my %good_values; @good_values{@good_keys} = delete @hash{@good_keys}; my ( $thread_uri, $message_uri ) = delete @hash{qw/thread_uri message_uri/}; if ( defined($thread_uri) || defined($message_uri) ) { $good_values{archive_link} = Mail::Summary::Tools::ArchiveLink::Hardcoded->new( thread_uri => $thread_uri, message_uri => $message_uri, ); } $class->new( %{ $options{thread} }, %good_values, extra => \%hash, ); } sub to_hash { my $self = shift; my @link_info; if ( exists $self->{archive_link} ) { # FIXME $self->meta->get_attribute_by_name("archive_link)->is_initialized( $self ) if ( (my $link = $self->archive_link)->isa("Mail::Summary::Tools::ArchiveLink::Hardcoded") ) { @link_info = ( eval { thread_uri => $link->thread_uri->as_string }, eval { message_uri => $link->message_uri->as_string }, ); } } return { subject => $self->subject, message_id => $self->message_id, summary => $self->summary, hidden => $self->hidden, @link_info, %{ $self->extra }, }; } sub make_archive_link { my $self = shift; my $constructor = $self->default_archive; Mail::Summary::Tools::ArchiveLink::Easy->$constructor( $self->message_id, $self->archive_link_params ); } # FIXME # redo with an attribute grammar sub merge { my ( $self, $thread ) = @_; $self->merge_extra( $thread ); $self->merge_summary( $thread ); $self->merge_subject( $thread ); $self->merge_hidden( $thread ); $self->merge_archive_link( $thread ); } sub merge_hidden { my ( $self, $thread ) = @_; $self->hidden( $self->hidden || $thread->hidden ); } sub merge_summary { my ( $self, $thread ) = @_; $self->summary( $self->summary || $thread->summary ); } sub merge_subject { my ( $self, $thread ) = @_; # noop } sub merge_archive_link { my ( $self, $thread ) = @_; my @hard_coded = grep { $_->isa("Mail::Summary::Tools::ArchiveLink::Hardcoded") } $self->archive_link, $thread->archive_link; if ( @hard_coded ) { $self->archive_link( $hard_coded[0] ); } } sub merge_extra { my ( $self, $thread ) = @_; $self->extra({ %{ $thread->extra }, %{ $self->extra }, $self->merge_dates( $thread ), $self->merge_posters( $thread ), }); } sub merge_dates { my ( $self, $thread ) = @_; return ( $self->merge_date_from($thread), $self->merge_date_to($thread), $self->merge_out_of_date($thread), ); } sub merge_out_of_date { my ( $self, $thread ) = @_; # it can't be out of date if there's no summary return unless $self->summary or $thread->summary or $self->hidden or $thread->hidden; # if any thread is out of date then this one becomes out of date my $prev_out_of_date = $self->extra->{out_of_date}; my $out_of_date = $prev_out_of_date || $thread->extra->{out_of_date}; my $earliest = $self->earlier_thread( $thread ); my $latest = $self->later_thread( $thread ); if ( $self->summary || $self->hidden ) { # we keep the existing summary, and if the other thread extends beyond # our range it's out of date $out_of_date = 1 if $earliest != $self or $latest != $self; } elsif ( $thread->summary || $thread->hidden ) { # we take the new summary, and if we extend beyond the other range then # it's out of date $out_of_date = 1 if $earliest != $thread or $latest != $thread; } else { $out_of_date = 0; } if ( $out_of_date xor $prev_out_of_date ) { return out_of_date => $out_of_date; } else { return; } } sub earlier_thread { my ( $self, $thread ) = @_; my $date_from = $self->extra->{date_from}; my $other_date_from = $thread->extra->{date_from}; if ( $date_from and $other_date_from ) { return $date_from <= $other_date_from ? $self : $thread; } elsif( $date_from || $other_date_from ) { return $date_from ? $self : $thread; } else { return $self; } } sub later_thread { my ( $self, $thread ) = @_; my $date_to = $self->extra->{date_to}; my $other_date_to = $thread->extra->{date_to}; if ( $date_to and $other_date_to ) { return $date_to >= $other_date_to ? $self : $thread; } elsif ( $date_to || $other_date_to ) { return $date_to ? $self : $thread; } else { return $self; } } sub merge_date_from { my ( $self, $thread ) = @_; # earliest my $min = ( $self->earlier_thread( $thread ) )->extra->{date_from}; no warnings 'uninitialized'; unless ( $min == $self->extra->{date_from} ) { return ( date_from => $min ); } else { return; } } sub merge_date_to { my ( $self, $thread ) = @_; my $max = ( $self->later_thread( $thread ) )->extra->{date_to}; no warnings 'uninitialized'; unless ( $max == $self->extra->{date_to} ) { return ( date_to => $max ); } else { return; } } sub merge_posters { my ( $self, $thread ) = @_; my %seen; my @posters = grep { !$seen{$_->{name}}++ } @{ $self->extra->{posters} || [] }, @{ $thread->extra->{posters} || [] }; return ( @posters ? (posters => \@posters) : () ); } __PACKAGE__; __END__ =pod =head1 NAME Mail::Summary::Tools::Summary::Thread - =head1 SYNOPSIS use Mail::Summary::Tools::Summary::Thread; =head1 DESCRIPTION =cut