Manitou-Mail logo title

Source file: mdx/lib/Manitou/Mailing.pm

# Copyright (C) 2004-2014 Daniel Verite

# This file is part of Manitou-Mail (see http://www.manitou-mail.org)

# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.

package Manitou::Mailing;

use strict;
use vars qw(@ISA @EXPORT_OK);


use Manitou::MailFormat;
use Manitou::Config qw(getconf);
use Manitou::Database qw(db_connect);
use Manitou::Encoding qw(decode_dbtxt);
use Manitou::Log qw(error_log notice_log);
use IPC::Open3;
use IO::Handle;
use Manitou::xSV;
use File::Temp qw(tempfile);

require Exporter;
@ISA = qw(Exporter);
@EXPORT_OK = qw();

sub merge_fields {
  my ($template_txt, $p_values) = @_;
  my $txt=$template_txt;
  for my $k (keys %{$p_values}) {
    $txt =~ s/\Q{{$k}}\E/$$p_values{$k}/g;
  }
  return $txt;
}

# Newlines in header fields are filtered out since it's
# the separator between headers for MailFormat::encode_header
sub merge_fields_header {
  my ($template_txt, $p_values) = @_;
  my $txt=$template_txt;
  for my $k (keys %{$p_values}) {
    my $v = $$p_values{$k};
    $v =~ s/\r\n/\n/g;
    $v =~ s/\r/\n/g;
    $v =~ s/\n/ /g;
    $txt =~ s/\Q{{$k}}\E/$v/g;
  }
  return $txt;
}

# return 1 if no error
sub deliver_message {
  my ($top, $cmd)=@_;

  # Pipe the message to the delivery agent
  my $ret=0;
  my $in=IO::Handle->new();
  my $out=IO::Handle->new();
  my $err=IO::Handle->new();
  eval {
    $SIG{'PIPE'} = 'IGNORE';
    my $pid = open3($in, $out, $err, $cmd);
    die $! if ($pid==0);
    $top->print($in) or die $!;
    close($in);
    waitpid($pid, 0);
  };
  if ($@) {
    error_log("Error while passing outgoing mail to the local delivery agent (\`$cmd\`): $@");
  }
  else {
    my $e=<$err>;
    close($err);
    close($out);
    if ($e ne "") {
      error_log("Local delivery agent error: (\`$cmd\`): $e");
    }
    else {
      $ret=1;
    }
  }
  return $ret;
}

#  my ($from, $to, $text_body, $html_body, $charset) = @_;
sub send_one_mail {
  my %args=@_;

  my $html_body=$args{html_body};

  my $decl_charset = getconf("preferred_charset", $args{from}) || 'iso-8859-1';
  my @charsets = split(/\s+/, $decl_charset);

  my ($text_body, $charset) = Manitou::MailFormat::encode_text_body($args{text_body}, @charsets);
  my $top;

  my %mime_args = (From => $args{from},
		   To => $args{to},
		   Encoding => '-SUGGEST',
		   'X-Mailer' => undef);
  if (length($text_body)>0 && length($html_body)==0) {
    $mime_args{Charset} = $charset;
    $mime_args{Data} = $text_body;
    $top = MIME::Entity->build(%mime_args);
  }
  elsif (length($html_body)>0 && length($text_body)==0) {
    $mime_args{Type} = 'text/html';
    $mime_args{Charset} = 'utf-8';
    $mime_args{Data} = $html_body;
    $top = MIME::Entity->build(%mime_args);
  }
  elsif (length($text_body)>0 && length($html_body)>0) {
    $mime_args{'Type'} = 'multipart/alternative';
    $top = MIME::Entity->build(%mime_args);

    my $p = MIME::Entity->build('Charset' => $charset,
				'Encoding' => '-SUGGEST',
				'Data' => $text_body,
			        'X-Mailer' => undef);
    $top->add_part($p);

    my $html_part = MIME::Entity->build('Data' => $html_body,
				'Encoding' => '-SUGGEST',
			        'Type' => 'text/html',
			        'Charset' => 'utf-8',
			        'X-Mailer' => undef);
    $top->add_part($html_part);
  }
  else {
    return 1;  # don't send anything if both text and html parts are empty (shouldn't happen)
  }

  Manitou::MailFormat::encode_header($top, $args{header}, @charsets);
  Manitou::MailFormat::add_date_header($top);

  my $cmd=getconf("local_delivery_agent", $args{from});
  if (!defined($cmd)) {
    print STDERR "mailing: unable to pass the mail to a local delivery agent.\nCheck your configuration file for the 'local_delivery_agent' entry\n";
    return 0;
  }
  $cmd =~ s/\$FROM\$/$args{from}/g;

  my $r=deliver_message($top, $cmd);
  $top->purge;

  return $r;
}

sub async_process_mailing {
  my ($parent_dbh, $mailing_id) = @_;
  my $pid = fork();
  if ($pid==-1) {
    die "Unable to fork process";
  }
  elsif ($pid>0) {
    return $pid;
  }
  else {
    notice_log("start of mailing process pid=$$ for mailing #$mailing_id");
    $parent_dbh->{InactiveDestroy}=1;
    my $dbh=db_connect();
    $dbh->{AutoCommit}=1;
    my ($ok,$errs);
    eval {
      ($ok,$errs)=do_mailing($dbh, $mailing_id);
    };
    if ($@) {
      error_log("mailing job failed: $@");
    }
    else {
      notice_log("mailing job ended: $ok sent, $errs in error");
    }
    exit(0);
  }
}

sub do_mailing {
  my ($dbh,$mailing_id)=@_;

  my $sth = $dbh->prepare(q{SELECT
	 throughput, header_template, text_template, html_template, sender_email, csv_columns
         FROM mailing_definition m JOIN mailing_run USING(mailing_id)
         WHERE m.mailing_id=? AND status=1});
  $sth->execute($mailing_id);
  my $row_mailing = $sth->fetchrow_hashref;
  if (!defined $row_mailing) {
    die "No entry found in mailing_definition or mailing_run for mailing_id=$mailing_id";
  }

  my $text_template = decode_dbtxt($row_mailing->{text_template});
  my $html_template = decode_dbtxt($row_mailing->{html_template});
  my $header_template = decode_dbtxt($row_mailing->{header_template});
  my @csv_columns = split /,/, decode_dbtxt($row_mailing->{csv_columns});

  my $sth1 = $dbh->prepare("SELECT recipient_email,mailing_data_id,csv_data FROM mailing_data WHERE mailing_id=? AND (sent is null OR sent='N')");
  my $sthu = $dbh->prepare("UPDATE mailing_data SET sent='Y' where mailing_data_id=?");

  my $sthu1 = $dbh->prepare("UPDATE mailing_run SET nb_sent=nb_sent+1,last_sent=now() WHERE mailing_id=?");

  my $stht = $dbh->prepare("SELECT status FROM mailing_run WHERE mailing_id=?");

  $sth1->execute($mailing_id);
  my $nb_ok=0;
  my $nb_errors=0;
  while (my $row_data = $sth1->fetchrow_hashref) {
    my $csv_line = decode_dbtxt($row_data->{csv_data});
    my ($text_body,$html_body,$header);
    # merge
    if (@csv_columns) {
      my $csvp=new Manitou::xSV;
      my ($fh,$filename) = tempfile() or die "unable to create temp file: $!";
      print $fh $csv_line;
      close $fh;
      $csvp->open_file($filename);
      $csvp->set_sep(",");
      $csvp->bind_fields(@csv_columns);

      my $fields_values = $csvp->fetchrow_hash();
      $text_body = merge_fields($text_template, $fields_values);
      $html_body = merge_fields($html_template, $fields_values);
      $header = merge_fields_header($header_template, $fields_values);
      unlink($filename);
    }
    else {
      $text_body = $text_template;
      $html_body = $html_template;
      $header = $header_template;
    }
    # send
    my $r=send_one_mail("from"=> $row_mailing->{sender_email},
			"to" => $row_data->{recipient_email},
			"text_body" => $text_body,
			"html_body" => $html_body,
			"header" => $header);
    if ($r) {
      $sthu->execute($row_data->{mailing_data_id});
      $sthu1->execute($mailing_id);
      $nb_ok++;
    }
    else {
      $nb_errors++;
    }
    # pause between messages
    select(undef,undef,undef,$row_mailing->{throughput});

    # The mailing can be stopped from the outside by setting mailing.status to 2
    # We check that before every message
    $stht->execute($mailing_id);
    my ($status)=$stht->fetchrow_array;
    if ($status!=1) {
      notice_log("mailing #$mailing_id stopped by change of status");
      last;
    }
  }
  # Updates for normal finish. If the mailing is just stopped (status=2) the queries below
  # won't update anything
  my $sth2=$dbh->prepare("UPDATE mailing_definition SET end_date=now() WHERE EXISTS (select 1 FROM mailing_run WHERE mailing_id=? AND status=1)");
  $sth2->execute($mailing_id);

  my $sth3=$dbh->prepare("UPDATE mailing_run SET status=3 WHERE mailing_id=? AND status=1");
  $sth3->execute($mailing_id);
  
  return ($nb_ok, $nb_errors);
}

1;

HTML source code generated by GNU Source-Highlight plus some custom post-processing

List of all available source files