fix parsing of multiple To: addresses, #73241
[freeside.git] / FS / FS / msg_template / email.pm
index 275dc82..4ae89f0 100644 (file)
@@ -164,7 +164,7 @@ Options are passed as a list of name/value pairs:
 
 =item cust_main
 
-Customer object (required).
+Customer object
 
 =item object
 
@@ -200,6 +200,18 @@ A hash reference of additional substitutions
 A string identifying the kind of message this is. Currently can be "invoice", 
 "receipt", "admin", or null. Expand this list as necessary.
 
+=item override_content
+
+A string to use as the HTML body; if specified, replaces the entire
+body of the message. This should be used ONLY by L<FS::report_batch> and may
+go away in the future.
+
+=item attach
+
+A L<MIME::Entity> (or arrayref of them) to attach to the message.
+
+=cut
+
 =back
 
 =cut
@@ -209,7 +221,7 @@ sub prepare {
   my( $self, %opt ) = @_;
 
   my $cust_main = $opt{'cust_main'}; # or die 'cust_main required';
-  my $object = $opt{'object'} or die 'object required';
+  my $object = $opt{'object'}; # or die 'object required';
 
   my $hashref = $self->prepare_substitutions(%opt);
 
@@ -265,15 +277,32 @@ sub prepare {
   warn "$me filling in body template\n" if $DEBUG;
   $body = $body_tmpl->fill_in( HASH => $hashref );
 
+  # override $body if requested
+  if ( $opt{'override_content'} ) {
+    warn "$me overriding template body with requested content" if $DEBUG;
+    $body = $opt{'override_content'};
+  }
+
   ###
   # and email
   ###
 
   my @to;
   if ( exists($opt{'to'}) ) {
-    @to = split(/\s*,\s*/, $opt{'to'});
+
+    @to = map { $_->format } Email::Address->parse($opt{'to'});
+
   } elsif ( $cust_main ) {
-    @to = $cust_main->invoicing_list_emailonly;
+
+    my $classnum = $opt{'to_contact_classnum'} || '';
+    my @classes = ref($classnum) ? @$classnum : split(',', $classnum);
+    # traditional behavior: send to all invoice recipients
+    @classes = ('invoice') unless @classes;
+    @to = $cust_main->contact_list_email(@classes);
+    # not guaranteed to produce contacts, but then customers aren't
+    # guaranteed to have email addresses on file. in that case, env_to
+    # will be null and sending this message will fail.
+
   } else {
     die 'no To: address or cust_main object specified';
   }
@@ -306,13 +335,16 @@ sub prepare {
   );
 
   warn "$me creating message headers\n" if $DEBUG;
+  # strip display-name from envelope addresses
+  # (use Email::Address for this? it chokes on non-ASCII characters in
+  # the display-name, which is not great for us)
   my $env_from = $from_addr;
-  $env_from =~ s/^\s*//; $env_from =~ s/\s*$//;
-  if ( $env_from =~ /^(.*)\s*<(.*@.*)>$/ ) {
-    # a common idiom
-    $env_from = $2;
-  } 
-  
+  foreach ($env_from, @to) {
+    s/^\s*//;
+    s/\s*$//;
+    s/^(.*)\s*<(.*@.*)>$/$2/;
+  }
+
   my $domain;
   if ( $env_from =~ /\@([\w\.\-]+)/ ) {
     $domain = $1;
@@ -336,13 +368,24 @@ sub prepare {
     'Type'        => 'multipart/related',
   );
 
+  if ( $opt{'attach'} ) {
+    my @attach;
+    if (ref $opt{'attach'} eq 'ARRAY') {
+      @attach = @{ $opt{'attach'} };
+    } else {
+      @attach = $opt{'attach'};
+    }
+    foreach (@attach) {
+      $message->add_part($_);
+    }
+  }
+
   #$message->head->replace('Content-type',
   #  'multipart/related; '.
   #  'boundary="' . $message->head->multipart_boundary . '"; ' .
   #  'type=multipart/alternative'
   #);
-  
-  # XXX a facility to attach additional parts is necessary at some point
+
   foreach my $part (@{ $email{mimeparts} }) {
     warn "$me appending part ".$part->mime_type."\n" if $DEBUG;
     $message->add_part( $part );
@@ -350,14 +393,17 @@ sub prepare {
 
   # effective To: address (not in headers)
   push @to, $self->bcc_addr if $self->bcc_addr;
-  my $env_to = join(', ', @to);
+  my @env_to;
+  foreach my $dest (@to) {
+    push @env_to, map { $_->address } Email::Address->parse($dest);
+  }
 
   my $cust_msg = FS::cust_msg->new({
-      'custnum'   => $cust_main->custnum,
+      'custnum'   => $cust_main ? $cust_main->custnum : '',
       'msgnum'    => $self->msgnum,
       '_date'     => $time,
       'env_from'  => $env_from,
-      'env_to'    => $env_to,
+      'env_to'    => join(',', @env_to),
       'header'    => $message->header_as_string,
       'body'      => $message->body_as_string,
       'error'     => '',
@@ -448,17 +494,10 @@ sub content {
 
 =cut
 
-=back
-
-=head2 CLASS METHODS
-
-=over 4
-
 =item send_prepared CUST_MSG
 
-Takes the CUST_MSG object and sends it to its recipient. This is a class 
-method because everything needed to send the message is stored in the 
-CUST_MSG already.
+Takes the CUST_MSG object and sends it to its recipient. The "smtpmachine"
+configuration option will be used to find the outgoing mail server.
 
 =cut
 
@@ -471,7 +510,9 @@ sub send_prepared {
     $domain = $1;
   }
 
-  my @to = split(/\s*,\s*/, $cust_msg->env_to);
+  # in principle should already be a list of bare addresses, but run it
+  # through Email::Address to make sure
+  my @env_to = map { $_->address } Email::Address->parse($cust_msg->env_to);
 
   my %smtp_opt = ( 'host' => $conf->config('smtpmachine'),
                    'helo' => $domain );
@@ -492,12 +533,12 @@ sub send_prepared {
   }
 
   warn "$me sending message\n" if $DEBUG;
-  my $message = join("\n\n", $cust_msg->header, $cust_msg->body);
+  my $message = join("\n", $cust_msg->header, $cust_msg->body);
   local $@;
   eval {
     sendmail( $message, { transport => $transport,
                           from      => $cust_msg->env_from,
-                          to        => \@to })
+                          to        => \@env_to })
   };
   my $error = '';
   if(ref($@) and $@->isa('Email::Sender::Failure')) {