use Carp qw( confess );
use FS::UID qw(dbh);
use FS::cust_main;
+use FS::Record qw( qsearch qsearchs );
+use FS::Misc qw( send_email generate_email );
$DEBUG = 0;
$me = '[FS::cust_main_Mixin]';
sub cust_unlinked_msg { '(unlinked)'; }
sub cust_linked { $_[0]->custnum; }
+sub cust_main {
+ my $self = shift;
+ $self->cust_linked ? qsearchs('cust_main', {custnum => $self->custnum}) : '';
+}
+
=item display_custnum
Given an object that contains fields from cust_main (say, from a JOINed
}
+=item email_search_result HASHREF
+
+Emails a notice to the specified customers. Customers without
+invoice email destinations will be skipped.
+
+Parameters:
+
+=over 4
+
+=item job
+
+Queue job for status updates. Required.
+
+=item search
+
+Hashref of params to the L<search()> method. Required.
+
+=item msgnum
+
+Message template number (see L<FS::msg_template>). Overrides all
+of the following options.
+
+=item from
+
+From: address
+
+=item subject
+
+Email Subject:
+
+=item html_body
+
+HTML body
+
+=item text_body
+
+Text body
+
+=back
+
+Returns an error message, or false for success.
+
+If any messages fail to send, they will be queued as individual
+jobs which can be manually retried. If the first ten messages
+in the job fail, the entire job will abort and return an error.
+
+=cut
+
+use Storable qw(thaw);
+use MIME::Base64;
+use Data::Dumper qw(Dumper);
+use Digest::SHA qw(sha1); # for duplicate checking
+
+sub email_search_result {
+ my($class, $param) = @_;
+
+ my $msgnum = $param->{msgnum};
+ my $from = delete $param->{from};
+ my $subject = delete $param->{subject};
+ my $html_body = delete $param->{html_body};
+ my $text_body = delete $param->{text_body};
+ my $error = '';
+
+ my $job = delete $param->{'job'}
+ or die "email_search_result must run from the job queue.\n";
+
+ my $msg_template;
+ if ( $msgnum ) {
+ $msg_template = qsearchs('msg_template', { msgnum => $msgnum } )
+ or die "msgnum $msgnum not found\n";
+ }
+
+ my $sql_query = $class->search($param->{'search'});
+ $sql_query->{'select'} = $sql_query->{'table'} . '.*';
+
+ my $count_query = delete($sql_query->{'count_query'});
+ my $count_sth = dbh->prepare($count_query)
+ or die "Error preparing $count_query: ". dbh->errstr;
+ $count_sth->execute
+ or die "Error executing $count_query: ". $count_sth->errstr;
+ my $count_arrayref = $count_sth->fetchrow_arrayref;
+ my $num_cust = $count_arrayref->[0];
+
+ my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
+ my @retry_jobs = ();
+ my $dups = 0;
+ my $success = 0;
+ my %sent_to = ();
+
+ #eventually order+limit magic to reduce memory use?
+ foreach my $obj ( qsearch($sql_query) ) {
+
+ #progressbar first, so that the count is right
+ $num++;
+ if ( time - $min_sec > $last ) {
+ my $error = $job->update_statustext(
+ int( 100 * $num / $num_cust )
+ );
+ die $error if $error;
+ $last = time;
+ }
+
+ my $cust_main = $obj->cust_main;
+ tie my %message, 'Tie::IxHash';
+ if ( !$cust_main ) {
+ next; # unlinked object; nothing else we can do
+ }
+
+ if ( $msg_template ) {
+ # Now supports other context objects.
+ %message = $msg_template->prepare(
+ 'cust_main' => $cust_main,
+ 'object' => $obj,
+ );
+ }
+ else {
+ my @to = $cust_main->invoicing_list_emailonly;
+ next if !@to;
+
+ %message = (
+ 'from' => $from,
+ 'to' => \@to,
+ 'subject' => $subject,
+ 'html_body' => $html_body,
+ 'text_body' => $text_body,
+ 'custnum' => $cust_main->custnum,
+ );
+ } #if $msg_template
+
+ # For non-cust_main searches, we avoid duplicates based on message
+ # body text.
+ my $unique = $cust_main->custnum;
+ $unique .= sha1($message{'text_body'}) if $class ne 'FS::cust_main';
+ if( $sent_to{$unique} ) {
+ # avoid duplicates
+ $dups++;
+ next;
+ }
+
+ $sent_to{$unique} = 1;
+
+ $error = send_email( generate_email( %message ) );
+
+ if($error) {
+ # queue the sending of this message so that the user can see what we
+ # tried to do, and retry if desired
+ my $queue = new FS::queue {
+ 'job' => 'FS::Misc::process_send_email',
+ 'custnum' => $cust_main->custnum,
+ 'status' => 'failed',
+ 'statustext' => $error,
+ };
+ $queue->insert(%message);
+ push @retry_jobs, $queue;
+ }
+ else {
+ $success++;
+ }
+
+ if($success == 0 and
+ (scalar(@retry_jobs) > 10 or $num == $num_cust)
+ ) {
+ # 10 is arbitrary, but if we have enough failures, that's
+ # probably a configuration or network problem, and we
+ # abort the batch and run away screaming.
+ # We NEVER do this if anything was successfully sent.
+ $_->delete foreach (@retry_jobs);
+ return "multiple failures: '$error'\n";
+ }
+ } # foreach $obj
+
+ if(@retry_jobs) {
+ # fail the job, but with a status message that makes it clear
+ # something was sent.
+ return "Sent $success, skipped $dups duplicate(s), failed ".scalar(@retry_jobs).". Failed attempts placed in job queue.\n";
+ }
+
+ return '';
+}
+
+sub process_email_search_result {
+ my $job = shift;
+ #warn "$me process_re_X $method for job $job\n" if $DEBUG;
+
+ my $param = thaw(decode_base64(shift));
+ warn Dumper($param) if $DEBUG;
+
+ $param->{'job'} = $job;
+
+ $param->{'search'} = thaw(decode_base64($param->{'search'}))
+ or die "process_email_search_result requires search params.\n";
+
+# $param->{'payby'} = [ split(/\0/, $param->{'payby'}) ]
+# unless ref($param->{'payby'});
+
+ my $table = $param->{'table'}
+ or die "process_email_search_result requires table.\n";
+
+ eval "use FS::$table;";
+ die "error loading FS::$table: $@\n" if $@;
+
+ my $error = "FS::$table"->email_search_result( $param );
+ dbh->commit; # save failed jobs before rethrowing the error
+ die $error if $error;
+
+}
+
+=item conf
+
+Returns a configuration handle (L<FS::Conf>) set to the customer's locale,
+if they have one. If not, returns an FS::Conf with no locale.
+
+=cut
+
+sub conf {
+ my $self = shift;
+ return $self->{_conf} if (ref $self and $self->{_conf});
+ my $cust_main = $self->cust_main;
+ my $conf = new FS::Conf {
+ 'locale' => ($cust_main ? $cust_main->locale : '')
+ };
+ $self->{_conf} = $conf if ref $self;
+ return $conf;
+}
+
+=item mt TEXT [, ARGS ]
+
+Localizes a text string (see L<Locale::Maketext>) for the customer's locale,
+if they have one.
+
+=cut
+
+sub mt {
+ my $self = shift;
+ return $self->{_lh}->maketext(@_) if (ref $self and $self->{_lh});
+ my $cust_main = $self->cust_main;
+ my $locale = $cust_main ? $cust_main->locale : '';
+ my $lh = FS::L10N->get_handle($locale);
+ $self->{_lh} = $lh if ref $self;
+ return $lh->maketext(@_);
+}
+
=back
=head1 BUGS