Bivio::Biz::Model::MailPartList
# Copyright (c) 2006-2026 Bivio Software, Inc. All Rights Reserved.
package Bivio::Biz::Model::MailPartList;
use strict;
use Bivio::Base 'Biz.ListModel';
use Encode ();
my($_IDI) = __PACKAGE__->instance_data_index;
my($_A) = b_use('Mail.Address');
my($_DT) = b_use('Type.DateTime');
my($_E) = b_use('Type.Email');
my($_FN) = b_use('Type.FileName');
my($_MP) = b_use('Ext.MIMEParser');
my($_RM) = b_use('Model.RealmMail');
my($_S) = b_use('Type.String');
my($_T) = b_use('MIME.Type');
my($_W) = b_use('MIME.Word');
my($_LFP) = b_use('Action.LocalFilePlain');
sub execute_from_realm_mail_list {
my($proto, $req) = @_;
$proto->new($req)->load_all({
parent_id => $req->get_nested(
qw(Model.RealmMailList RealmMail.realm_file_id)),
});
return;
}
sub execute_from_realm_file_id {
my($proto, $req, $rfid) = @_;
$proto->new($req)->load_all({
parent_id => $rfid,
});
return;
}
sub execute_part {
my($proto, $req) = @_;
my($self) = $proto->new($req);
my($q) = $self->parse_query_from_request;
if ($q->get('this')) {
$self->load_this($q);
}
elsif (my $cid = $q->unsafe_get('mime_cid')) {
$self->load_all($q);
my($cursor) = $self->unsafe_get_cursor_for_mime_cid($cid);
$self->throw_die(MODEL_NOT_FOUND => {
message => 'mime_cid not found',
entity => $cid,
}) unless defined($cursor);
$self->set_cursor($cursor);
}
else {
$self->throw_die(CORRUPT_QUERY => {
message => 'missing this or mime_cid',
entity => $q,
});
}
$_LFP->set_cacheable_output(
\((
$self->get('mime_entity')->bodyhandle
|| $self->throw_die('MODEL_NOT_FOUND')
)->as_string),
$self->get('mime_type'),
$req,
)->set_last_modified($self->get_nested(qw(realm_file modified_date_time)));
return 1;
}
sub format_uri_for_mime_cid {
my($self, $mime_cid, $task_id) = @_;
# Don't bother verifying is in list(?)
return $self->internal_format_uri($task_id, {mime_cid => $mime_cid});
}
sub format_uri_for_part {
my($self, $task_id) = @_;
return $self->internal_format_uri(
$task_id, {'ListQuery.this' => $self->get('index')});
}
sub get_body {
my($e) = shift->get('mime_entity');
return ''
unless my $v = $e->bodyhandle;
return _to_utf8(
$v->as_string, $e->head->mime_attr('content-type.charset'));
}
sub get_file_name {
my($self) = @_;
my($fn) = $self->get('mime_entity')->head->recommended_filename;
return $_FN->get_tail(
$fn && _canonicalize_file_name($fn) || _default_file_name($self),
);
}
sub get_from_name {
my($self) = @_;
return $self->get_header('from_name')
|| $_E->get_local_part($self->get_header('from_email'));
}
sub get_header {
my($self, $name) = @_;
my($from) = $name =~ s/^(from)_(name|email)$/$1/ && $2;
return ''
unless defined(my $v = $self->get('mime_entity')->head->get($name));
chomp($v);
$v = $_W->decode($v);
return ($from ? ($_A->parse($v))[$from eq 'name' ? 1 : 0]
: $name eq 'date' ? ($_DT->from_literal($v))[0]
: $v
) || '';
}
sub has_mime_cid {
return defined(shift->get('mime_cid')) ? 1 : 0;
}
sub internal_format_uri {
my($self, $task_id, $other, $realm_name) = @_;
my($req) = $self->get_request;
$self->die(
$self->get('RealmFile.realm_id'), ': not same as auth_realm',
) unless $realm_name
|| $req->get('auth_id') eq $self->get('RealmFile.realm_id');
return $self->get_request->format_uri({
$realm_name ? (realm => $realm_name) : (),
task_id => $task_id,
path_info => $self->get_file_name,
query => {
'ListQuery.parent_id' => $self->get_query->get('parent_id'),
%$other,
},
});
}
sub internal_initialize {
my($self) = @_;
return $self->merge_initialize_info($self->SUPER::internal_initialize, {
version => 1,
$self->field_decl(
primary_key => [[qw(index Integer)]],
other => [
[qw(mime_type Line)],
[qw(mime_entity Object)],
[qw(mime_cid Line NONE)],
],
undef, 'NOT_NULL',
),
parent_id => 'RealmFile.realm_file_id',
auth_id => 'RealmFile.realm_id',
other_query_keys => [qw(content_ref mime_cid)],
other => [
$self->field_decl([[qw(realm_file Model.RealmFile)]]),
],
});
}
sub internal_load_rows {
my($self, $query) = @_;
my($index) = 0;
my($rid) = $query->get('auth_id');
b_die($rid, ': auth_id not same as auth_realm: ', $self->req('auth_realm'))
unless $query->unsafe_get('content_ref') || $self->req('auth_id') eq $rid;
my($rf);
my($res) = [map({
$_->{index} = $index++;
$_->{'RealmFile.realm_id'} = $rid;
$_->{realm_file} = $rf;
$_;
} @{_walk(
_parser(
"Content-Type: message/rfc822\n\n"
. ${$query->unsafe_get('content_ref')
|| ($rf = $self->new_other('RealmFile')->unauth_load_or_die({
realm_id => $query->get('auth_id'),
realm_file_id => $query->get('parent_id'),
$_RM->access_is_public_only($self->req)
? (is_public => 1)
: (),
}))->get_content},
))},
)];
return $res
unless my $t = $query->unsafe_get('this');
return [$res->[$t->[0]] || $self->throw_die('MODEL_NOT_FOUND')];
}
sub load_from_content {
my($self, $content) = @_;
return $self->load_all({
content_ref => $content,
parent_id => 1,
});
}
sub set_attachment_visited {
my($self, $mime_cid) = @_;
my($fields) = $self->[$_IDI] ||= {
visited => {},
};
$fields->{visited}->{$mime_cid} = 1;
return;
}
sub show_as_attachment {
my($self) = @_;
return $self->get('index') > 1 ? 1 : 0;
}
sub unsafe_get_cursor_for_mime_cid {
my($self, $mime_cid) = @_;
return $self->save_excursion(sub {
my($cursor);
$mime_cid = "<$mime_cid>";
$self->do_rows(sub {
my($it) = @_;
return 1
unless my $cid = $it->unsafe_get('mime_cid');
chomp($cid);
return 1
unless $mime_cid eq $cid;
$cursor = $it->get_cursor;
return 0;
});
return $cursor;
});
}
sub was_attachment_visited {
my($self) = @_;
my($fields) = $self->[$_IDI];
return 0 unless $fields && $self->unsafe_get('mime_cid');
my($cid) = $self->get('mime_cid');
$cid =~ s/^<|>\s*$//sg;
return $fields->{visited} && $fields->{visited}->{$cid}
? 1
: 0;
}
sub _canonicalize_file_name {
my($file_name) = @_;
$file_name = ${$_S->canonicalize_charset($_W->decode($file_name))};
$file_name =~ s/ +/-/g;
$file_name =~ s/-{2,}/-/g;
return $file_name;
}
sub _default_file_name {
my($self) = @_;
return 'attachment'
. $self->get('index')
. '.'
. ($_T->to_extension($self->get('mime_type') || '')
|| $_T->to_extension('application/octet')
);
}
sub _parser {
return $_MP->parse_data(\$_[0]);
}
sub _to_utf8 {
my($octets, $charset) = @_;
# Pages are displayed as utf-8, so transcode if possible.
return $octets
unless defined($charset)
&& $charset =~ /\S/
&& $charset !~ /^\s*(?:us-ascii|ascii|utf-?8)\s*$/i;
return $octets
unless my $enc = Encode::find_encoding($charset);
return Encode::encode('utf-8', $enc->decode($octets, Encode::FB_DEFAULT()));
}
sub _walk {
my($me) = @_;
$me->head->unfold;
my($parts) = [$me->parts];
my($ct) = $me->mime_type;
if ($ct eq 'message/rfc822') {
#TODO: Do not die; ignore other parts(?)
Bivio::Die->die($parts, ': expected one part')
unless 1 == @$parts;
my($h) = $parts->[0]->head->dup;
$h->replace('Content-Type', 'x-message/rfc822-headers');
unshift(@$parts, _parser($h->as_string . "\n"));
}
my($related_index) = 0;
return @$parts ? [map(
$ct eq 'x-message/rfc822-headers'
? $_
: $ct eq 'multipart/alternative' && $_->mime_type =~ m{^text/}
&& $_->mime_type ne 'text/html'
? ()
: @{_walk($_)},
@$parts,
)] : [{
mime_type => $ct,
mime_entity => $me,
mime_cid => $me->head->get('content-id') || undef,
}];
}
1;