Bivio::Biz::Model::MailReceiveDispatchForm
# Copyright (c) 2002-2012 bivio Software, Inc. All Rights Reserved.
# $Id$
package Bivio::Biz::Model::MailReceiveDispatchForm;
use strict;
use Bivio::Base 'Model.MailReceiveBaseForm';
b_use('IO.Trace');
b_use('IO.ClassLoaderAUTOLOAD');
our($_TRACE);
my($_A) = b_use('Mail.Address');
my($_DT) = b_use('Type.DateTime');
my($_E) = b_use('Type.Email');
my($_F) = b_use('Biz.File');
my($_FCT) = b_use('FacadeComponent.Text');
my($_FP) = b_use('Type.FilePath');
my($_I) = b_use('Mail.Incoming');
my($_O) = b_use('Mail.Outgoing');
my($_RI) = b_use('Agent.RequestId');
my($_TEST_RECIPIENT_HDR) = qr{^@{[b_use('Mail.Common')->TEST_RECIPIENT_HDR]}:}m;
my($_OUT_OF_OFFICE_FILTER_CLASSES) = [qw(
out_of_office_negatives
out_of_office_positives
)];
b_use('IO.Config')->register(my $_CFG = {
filter_spam => 0,
filter_out_of_office => 1,
out_of_office_negatives => [
[qw(X-Bugzilla)],
[qw(Sender .*calendar-notification@google.com)],
[qw(To bounce\*)],
],
out_of_office_positives => [
[qw(Auto-Submitted auto-generated)],
[qw(Auto-Submitted auto-replied)],
[qw(X-GeneratedBy OOService)],
[qw(X-Autoreply yes)],
[qw(Subject out\s+of\s+(the\s+)?office)],
],
ignore_model_not_found => 0,
});
sub execute_ok {
my($self) = @_;
# Unpacks and stores an incoming mail message.
# Requires form fields: client_addr, recipient, message.
#
# Sets facade, realm, user, and server_redirects to task.
#
# User is set from From: or Apparently-From:, in that order.
#
# op then maps to a URI:
#
# Text->get_value('MailReceiveDispatchForm.uri_prefix') . $op
#
# I<op> must contain only \w and dashes (-).
my($req) = $self->req;
Type_UserAgent()->MAIL->execute($req, 1);
$req->put_durable(client_addr => $self->get('client_addr'));
$self->put_on_request(1);
my($redirect, undef, $realm, $plus, $op) = _email_alias($self);
return _redirect($redirect)
if $redirect;
my($mi) = _new_incoming($self);
$self->internal_put_field(
mail_incoming => $mi,
plus_tag => $plus,
);
return _redirect('ignore_task')
if _ignore(
$self,
\&_ignore_email,
\&_ignore_forwarded,
\&_ignore_spam,
\&_ignore_out_of_office,
\&_ignore_no_message_id,
);
my($die) = Bivio::Die->catch(
sub {
$self->internal_set_realm($realm);
});
if ($die) {
return _redirect('ignore_task')
if $_CFG->{ignore_model_not_found}
&& $die->get('code')->eq_model_not_found;
$die->throw;
}
return _redirect('ignore_task')
if _ignore($self, \&_ignore_duplicate, \&_ignore_mailer_daemon);
$self->internal_put_field(
task_id => _task($self, $op),
from_email => ($mi->get_from)[0],
);
_trace($self->get('from_email'), ' ', $self->get('task_id')) if $_TRACE;
$self->throw_die('FORBIDDEN', {
entity => $realm,
message => 'message missing "From"',
}) unless $self->get('from_email');
$self->new_other('UserLoginForm')->process({
login => $self->internal_get_login($mi),
via_mta => 1,
});
return _redirect('ignore_task')
if _ignore(
$self,
\&_ignore_unsubscribe,
);
return {
method => 'server_redirect',
task_id => $self->get('task_id'),
query => undef,
};
}
sub format_recipient {
my($self, $realm, $plus, $op) = @_;
return $_E->format_email(
$realm,
undef,
$plus,
$op,
$self->req,
);
}
sub handle_config {
my(undef, $cfg) = @_;
$_CFG = $cfg;
foreach my $fc (@{$_OUT_OF_OFFICE_FILTER_CLASSES}) {
$_CFG->{$fc} = [map({
my($fa) = $_;
grep(ref($_) eq 'Regexp', @$fa)
? $fa
: [map({
my($f) = $_;
ref($f) ? () : _compile_filter($f);
} (
$fa->[0],
join(':\s+', @$fa),
))];
} (
@{$_CFG->{$fc} || []},
))];
}
return;
}
sub internal_get_login {
my($self, $in) = @_;
return $in->get_from_user_id($self->req);
}
sub internal_initialize {
my($self) = @_;
return $self->merge_initialize_info($self->SUPER::internal_initialize, {
$self->field_decl(
other => [
['mail_incoming', $_I],
['from_email', 'Email'],
['task_id', 'Agent.TaskId'],
['plus_tag', 'String'],
['email_alias_incoming', 'Email'],
],
),
});
}
sub internal_set_realm {
my($self, $realm) = @_;
# Sets I<realm> or throws not_found.
$realm = ref($realm) ? $realm
: $self->new_other('RealmOwner')
->unauth_load_by_id_or_name_or_die($realm);
$self->throw_die('NOT_FOUND', {
entity => $realm,
message => 'cannot mail to a default realm or offline user',
}) if $realm->is_default || $realm->is_offline_user;
$self->req->set_realm($realm);
return;
}
sub _compile_filter {
my($filter) = @_;
return undef
unless my $res = Type_Regexp()->from_literal_or_die($filter, 1);
return Type_Regexp()->add_regexp_modifiers($res, 'is');
}
sub _email_alias {
my($self) = @_;
my($req) = $self->req;
my($domain, $realm, $plus, $op) = _parse_recipient($self);
Bivio::UI::Facade->setup_request($domain, $req);
return (undef, $domain, $realm, $plus, $op)
unless $req->get('task')->unsafe_get_redirect('email_alias_task', $req)
and my $new = $self->new_other('EmailAlias')
->incoming_to_outgoing($self->get('recipient'));
$self->internal_put_field(email_alias_incoming => $self->get('recipient'));
if ($_E->is_valid($new)) {
_trace($self->get('recipient'), ' => ', $new) if $_TRACE;
$self->internal_put_field(recipient => $new);
return 'email_alias_task';
}
$self->internal_put_field(recipient => $_E->join_parts($new, $domain));
return (undef, _parse_recipient($self));
}
sub _from_email {
my($from) = @_;
($from) = $from && $_A->parse($from);
return $from && lc($from);
}
sub _from_mailer_daemon {
return shift->get('mail_incoming')->get('header')
=~ /^Return-Path:\s*/im;
}
sub _ignore {
my($self, @ops) = @_;
foreach my $op (@ops) {
next
unless my $which = $op->($self);
$_F->write(
$_FP->join(
$self->simple_package_name,
$which,
$_RI->current($self->req) . '.eml',
),
$self->get('message')->{content},
);
return 1;
}
return 0;
}
sub _ignore_duplicate {
my($self) = @_;
my($mi) = $self->get('mail_incoming');
return undef
if $self->req->if_test(
sub {$mi->get('header') !~ $_TEST_RECIPIENT_HDR});
return $mi->is_duplicate($self->req) ? 'duplicate' : undef;
}
sub _ignore_email {
my($self) = @_;
return undef
unless $_E->is_ignore($self->get('recipient'));
return 'ignore-mail';
}
sub _ignore_forwarded {
my($self) = @_;
return $self->get('mail_incoming')->is_forwarding_loop
? 'too-many-forwards' : undef;
}
sub _ignore_mailer_daemon {
my($self) = @_;
return undef
unless $self->new_other('RowTag')->get_value(
$self->req('auth_id'),
Type_RowTagKey()->FILTER_MAILER_DAEMON,
);
return undef
unless _from_mailer_daemon($self);
foreach my $id (_related_message_ids($self)) {
return undef
if $self->new_other('RealmMail')->unsafe_load({
message_id => $id,
});
}
return 'mailer-daemon';
}
sub _ignore_no_message_id {
my($self) = @_;
my($mi) = $self->get('mail_incoming');
# Invalid message id.
#TODO(robnagler) rename to invalid_message_id
return $mi->get_message_id eq $mi->NO_MESSAGE_ID
? 'no-message-id'
: undef;
}
sub _ignore_out_of_office {
my($self) = @_;
return undef
unless $_CFG->{filter_out_of_office};
my($mi) = $self->get('mail_incoming');
foreach my $fc (@{$_OUT_OF_OFFICE_FILTER_CLASSES}) {
foreach my $filter (@{$_CFG->{$fc}}) {
my($match) = $mi->grep_headers(@$filter);
next
unless @$match;
_trace($filter, ': match ', $match) if $_TRACE;
return $fc =~ qr{negative}
? undef
: 'out-of-office';
}
}
return undef;
}
sub _ignore_spam {
my($self) = @_;
return undef
unless $_CFG->{filter_spam};
my($is_spam) =
$self->get('mail_incoming')->get('header') =~ /^X-Spam-Flag:\s+Y/im;
return undef
unless $is_spam;
my($support_email) = $self->req->format_email(
$_FCT->get_value('support_email', $self->req));
if ((
$self->get('recipient') eq $support_email
|| ($self->unsafe_get('email_alias_incoming') || '') eq $support_email
) && $self->internal_get_login($self->get('mail_incoming'))) {
# don't filter support mail from a real user
$self->req->warn('support mail from user marked as spam, overriding');
return undef;
}
return 'spam';
}
sub _ignore_unsubscribe {
my($self) = @_;
return undef
unless $self->req('auth_user_id')
&& $self->get('mail_incoming')->get('header')
=~ /^Subject:\s+(unsubscribe)\s*$/im;
my($subject) = $1 || b_die();
my($subscription) = $self->new_other('UserRealmSubscription');
if ($subscription->unsafe_load({
user_id => $self->req('auth_user_id'),
is_subscribed => 1,
})) {
$subscription->req->warn(
'unsubscribing user from realm, subject: ',
$subject,
);
$self->new_other('MailUnsubscribeForm')->unsubscribe;
return 'unsubscribe';
}
return 'not-subscribed';
}
sub _new_incoming {
my($self) = @_;
my($c) = $self->get('message')->{content};
my($mi) = $_I->new($c);
if (! $mi->get_headers->{'message-id'}) {
# No Message-ID so add one. Can't use Mail::Incoming, because it doesn't
# distinguish between invalid and missing for its "NO_MESSAGE_ID".
substr($$c, 0, 0, 'Message-ID: ' . $_O->generate_message_id($self->req) . "\n");
$mi = $_I->new($c);
}
return $mi;
}
sub _parse_recipient {
my($self) = @_;
my($to) = $self->get('recipient');
my(undef, $domain, $name, $plus, $op) = $_E->split_parts($to);
$self->throw_die(
'NOT_FOUND',
{
entity => $to,
message => 'invalid recipient',
},
) unless defined($name);
return ($domain, $name, $plus, $op);
}
sub _redirect {
my($task) = @_;
_trace($task) if $_TRACE;
return {
method => 'server_redirect',
task_id => $task,
query => undef,
};
}
sub _related_message_ids {
return (shift->get('mail_incoming')->get_rfc822
=~ /^Message-Id:\s+<(.+?)>/mig);
}
sub _task {
my($self, $op) = @_;
my($req) = $self->req;
$op ||= '';
$self->throw_die('NOT_FOUND', {
entity => $op,
message => 'operation is invalid',
}) unless $op =~ /^\w*$/;
return b_use('FacadeComponent.Task')->unsafe_get_from_uri(
b_use('FacadeComponent.Text')
->get_value('MailReceiveDispatchForm.uri_prefix', $req)
. $op,
$req->get('auth_realm')->get('type'),
$req,
) || $self->throw_die('NOT_FOUND', {
entity => $op,
message => 'task not found',
});
}
1;