diff options
Diffstat (limited to 'perllib/FixMyStreet')
-rw-r--r-- | perllib/FixMyStreet/App.pm | 40 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Contact.pm | 20 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/View/Email.pm | 7 | ||||
-rw-r--r-- | perllib/FixMyStreet/Cobrand/Default.pm | 2 | ||||
-rw-r--r-- | perllib/FixMyStreet/Email.pm | 126 | ||||
-rw-r--r-- | perllib/FixMyStreet/Script/Alerts.pm | 89 | ||||
-rw-r--r-- | perllib/FixMyStreet/Script/Questionnaires.pm | 3 | ||||
-rw-r--r-- | perllib/FixMyStreet/Script/Reports.pm | 3 | ||||
-rw-r--r-- | perllib/FixMyStreet/SendReport/Email.pm | 1 | ||||
-rw-r--r-- | perllib/FixMyStreet/TestMech.pm | 42 |
10 files changed, 270 insertions, 63 deletions
diff --git a/perllib/FixMyStreet/App.pm b/perllib/FixMyStreet/App.pm index be0e91101..ea7d43512 100644 --- a/perllib/FixMyStreet/App.pm +++ b/perllib/FixMyStreet/App.pm @@ -306,30 +306,38 @@ sub send_email { my $sender_name = $c->cobrand->contact_name; # create the vars to pass to the email template + my @include_path = @{ $c->cobrand->path_to_email_templates($c->stash->{lang_code}) }; my $vars = { from => [ $sender, _($sender_name) ], %{ $c->stash }, %$extra_stash_values, - additional_template_paths => $c->cobrand->path_to_email_templates($c->stash->{lang_code}), + additional_template_paths => \@include_path, }; return if FixMyStreet::Email::is_abuser($c->model('DB')->schema, $vars->{to}); - my $email = mySociety::Locale::in_gb_locale { FixMyStreet::Email::construct_email( - { - _body_ => $c->view('Email')->render( $c, $template, $vars ), - _attachments_ => $extra_stash_values->{attachments}, - From => $vars->{from}, - To => $vars->{to}, - 'Message-ID' => sprintf('<fms-%s-%s@%s>', - time(), unpack('h*', random_bytes(5, 1)), $c->config->{EMAIL_DOMAIN} - ), - $vars->{subject} ? (Subject => $vars->{subject}) : (), - $vars->{'Reply-To'} ? ('Reply-To' => $vars->{'Reply-To'}) : (), - } - ) }; - - # send the email + my @inline_images; + $vars->{inline_image} = sub { FixMyStreet::Email::add_inline_image(\@inline_images, @_); }, + + my $html_template = FixMyStreet::Email::get_html_template($template, @include_path); + my $html_compiled = eval { + $c->view('Email')->render($c, $html_template, $vars) if $html_template; + }; + $c->log->debug("Error compiling HTML $template: $@") if $@; + + my $data = { + _body_ => $c->view('Email')->render( $c, $template, $vars ), + _attachments_ => $extra_stash_values->{attachments}, + From => $vars->{from}, + To => $vars->{to}, + 'Message-ID' => FixMyStreet::Email::message_id(), + }; + $data->{Subject} = $vars->{subject} if $vars->{subject}; + $data->{'Reply-To'} = $vars->{'Reply-To'} if $vars->{'Reply-To'}; + $data->{_html_} = $html_compiled if $html_compiled; + $data->{_html_images_} = \@inline_images if @inline_images; + + my $email = mySociety::Locale::in_gb_locale { FixMyStreet::Email::construct_email($data) }; $c->model('EmailSend')->send($email); return $email; diff --git a/perllib/FixMyStreet/App/Controller/Contact.pm b/perllib/FixMyStreet/App/Controller/Contact.pm index e20011471..5527256a6 100644 --- a/perllib/FixMyStreet/App/Controller/Contact.pm +++ b/perllib/FixMyStreet/App/Controller/Contact.pm @@ -168,26 +168,22 @@ sub prepare_params_for_email : Private { if ( $c->stash->{update} ) { - my $problem_url = $base_url . '/report/' . $c->stash->{update}->problem_id + $c->stash->{problem_url} = $base_url . '/report/' . $c->stash->{update}->problem_id . '#update_' . $c->stash->{update}->id; - my $admin_url = " - $admin_url" . '/update_edit/' . $c->stash->{update}->id - if $admin_url; - $c->stash->{message} .= sprintf( - " \n\n[ Complaint about update %d on report %d - %s%s ]", + $c->stash->{admin_url} = $admin_url . '/update_edit/' . $c->stash->{update}->id; + $c->stash->{complaint} = sprintf( + "Complaint about update %d on report %d", $c->stash->{update}->id, $c->stash->{update}->problem_id, - $problem_url, $admin_url ); } elsif ( $c->stash->{problem} ) { - my $problem_url = $base_url . '/report/' . $c->stash->{problem}->id; - $admin_url = " - $admin_url" . '/report_edit/' . $c->stash->{problem}->id - if $admin_url; - $c->stash->{message} .= sprintf( - " \n\n[ Complaint about report %d - %s%s ]", + $c->stash->{problem_url} = $base_url . '/report/' . $c->stash->{problem}->id; + $c->stash->{admin_url} = $admin_url . '/report_edit/' . $c->stash->{problem}->id; + $c->stash->{complaint} = sprintf( + "Complaint about report %d", $c->stash->{problem}->id, - $problem_url, $admin_url ); # flag this so it's automatically listed in the admin interface diff --git a/perllib/FixMyStreet/App/View/Email.pm b/perllib/FixMyStreet/App/View/Email.pm index 86d5c1d60..6073ee814 100644 --- a/perllib/FixMyStreet/App/View/Email.pm +++ b/perllib/FixMyStreet/App/View/Email.pm @@ -14,7 +14,7 @@ __PACKAGE__->config( ], ENCODING => 'utf8', render_die => 1, - expose_methods => ['loc'], + expose_methods => ['loc', 'file_exists'], ); =head1 NAME @@ -40,5 +40,10 @@ sub loc { return _(@args); } +sub file_exists { + my ( $self, $c, @args ) = @_; + -e FixMyStreet->path_to(@args); +} + 1; diff --git a/perllib/FixMyStreet/Cobrand/Default.pm b/perllib/FixMyStreet/Cobrand/Default.pm index e5ec0c13a..686684a05 100644 --- a/perllib/FixMyStreet/Cobrand/Default.pm +++ b/perllib/FixMyStreet/Cobrand/Default.pm @@ -395,7 +395,7 @@ Return an override type of map if necessary. =cut sub map_type { my $self = shift; - return 'OSM' if $self->{c}->req->uri->host =~ /^osm\./; + return 'OSM' if $self->{c} && $self->{c}->req->uri->host =~ /^osm\./; return; } diff --git a/perllib/FixMyStreet/Email.pm b/perllib/FixMyStreet/Email.pm index d955f6f72..34ac1514c 100644 --- a/perllib/FixMyStreet/Email.pm +++ b/perllib/FixMyStreet/Email.pm @@ -8,6 +8,7 @@ package FixMyStreet::Email; use Email::MIME; use Encode; +use File::Spec; use POSIX qw(); use Template; use Digest::HMAC_SHA1 qw(hmac_sha1_hex); @@ -72,10 +73,77 @@ sub is_abuser { sub _render_template { my ($tt, $template, $vars, %options) = @_; my $var; - $tt->process($template, $vars, \$var); + $tt->process($template, $vars, \$var) || print "Template processing error: " . $tt->error() . "\n"; return $var; } +sub _unique_id { + sprintf('fms-%s-%s@%s', + time(), unpack('h*', random_bytes(5, 1)), + FixMyStreet->config('EMAIL_DOMAIN')); +} + +sub message_id { + '<' . _unique_id() . '>' +} + +sub add_inline_image { + my ($inline_images, $obj, $name) = @_; + if (ref $obj eq 'HASH') { + return _add_inline($inline_images, $name, $obj->{data}, $obj->{content_type}); + } else { + my $file = FixMyStreet->path_to($obj); + return _add_inline($inline_images, $file->basename, scalar $file->slurp); + } +} + +sub _add_inline { + my ($inline_images, $name, $data, $type) = @_; + + return unless $data; + + $name ||= 'photo'; + if ($type) { + if ($name !~ /\./) { + my ($suffix) = $type =~ m{image/(.*)}; + $name .= ".$suffix"; + } + } else { + my ($b, $t) = split /\./, $name; + $type = "image/$t"; + } + + my $cid = _unique_id(); + push @$inline_images, { + body => $data, + attributes => { + id => $cid, + filename => $name, + content_type => $type, + encoding => 'base64', + name => $name, + }, + }; + return "cid:$cid"; +} + +# We only want an HTML template from the same directory as the .txt +sub get_html_template { + my ($template, @include_path) = @_; + push @include_path, FixMyStreet->path_to( 'templates', 'email', 'default' ); + (my $html_template = $template) =~ s/\.txt$/\.html/; + my $template_dir = find_template_dir($template, @include_path); + my $html_template_dir = find_template_dir($html_template, @include_path); + return $html_template if $template_dir eq $html_template_dir; +} + +sub find_template_dir { + my ($template, @include_path) = @_; + foreach (@include_path) { + return $_ if -e File::Spec->catfile($_, $template); + } +} + sub send_cron { my ( $schema, $template, $vars, $hdrs, $env_from, $nomail, $cobrand, $lang_code ) = @_; @@ -88,11 +156,11 @@ sub send_cron { return 1 if is_abuser($schema, $hdrs->{To}); - $hdrs->{'Message-ID'} = sprintf('<fms-cron-%s-%s@%s>', time(), - unpack('h*', random_bytes(5, 1)), FixMyStreet->config('EMAIL_DOMAIN') - ); + $hdrs->{'Message-ID'} = message_id(); my @include_path = @{ $cobrand->path_to_email_templates($lang_code) }; + my $html_template = get_html_template($template, @include_path); + push @include_path, FixMyStreet->path_to( 'templates', 'email', 'default' ); my $tt = Template->new({ ENCODING => 'utf8', @@ -102,6 +170,14 @@ sub send_cron { $vars->{site_name} = Utils::trim_text(_render_template($tt, 'site-name.txt', $vars)); $hdrs->{_body_} = _render_template($tt, $template, $vars); + if ($html_template) { + my @inline_images; + $vars->{inline_image} = sub { add_inline_image(\@inline_images, @_) }; + $vars->{file_exists} = sub { -e FixMyStreet->path_to(@_) }; + $hdrs->{_html_} = _render_template($tt, $html_template, $vars); + $hdrs->{_html_images_} = \@inline_images; + } + my $email = mySociety::Locale::in_gb_locale { construct_email($hdrs) }; if ($nomail) { @@ -236,6 +312,47 @@ sub construct_email ($) { ), ]; + my $overall_type; + if ($p->{_html_}) { + my $html = _mime_create( + body_str => $p->{_html_}, + attributes => { + charset => 'utf-8', + encoding => 'quoted-printable', + content_type => 'text/html', + }, + ); + if ($p->{_html_images_} || $p->{_attachments_}) { + $parts = [ _mime_create( + attributes => { content_type => 'multipart/alternative' }, + parts => [ $parts->[0], $html ] + ) ]; + } else { + # The top level will be the alternative multipart if there are + # no images and no other attachments + push @$parts, $html; + $overall_type = 'multipart/alternative'; + } + if ($p->{_html_images_}) { + foreach (@{$p->{_html_images_}}) { + my $cid = delete $_->{attributes}->{id}; + my $part = _mime_create(%$_); + $part->header_set('Content-ID' => "<$cid>"); + push @$parts, $part; + } + if ($p->{_attachments_}) { + $parts = [ _mime_create( + attributes => { content_type => 'multipart/related' }, + parts => $parts, + ) ]; + } else { + # The top level will be the related multipart if there are + # images but no other attachments + $overall_type = 'multipart/related'; + } + } + } + if ($p->{_attachments_}) { push @$parts, map { _mime_create(%$_) } @{$p->{_attachments_}}; } @@ -245,6 +362,7 @@ sub construct_email ($) { parts => $parts, attributes => { charset => 'utf-8', + $overall_type ? (content_type => $overall_type) : (), }, ); diff --git a/perllib/FixMyStreet/Script/Alerts.pm b/perllib/FixMyStreet/Script/Alerts.pm index 062601044..91f5cd6ef 100644 --- a/perllib/FixMyStreet/Script/Alerts.pm +++ b/perllib/FixMyStreet/Script/Alerts.pm @@ -15,9 +15,13 @@ use RABX; use FixMyStreet::Cobrand; use FixMyStreet::DB; use FixMyStreet::Email; +use FixMyStreet::Map; +use FixMyStreet::App::Model::PhotoSet; FixMyStreet->configure_mysociety_dbhandle; +my $parser = DateTime::Format::Pg->new(); + # Child must have confirmed, id, email, state(!) columns # If parent/child, child table must also have name and text # and foreign key to parent must be PARENT_id @@ -37,6 +41,7 @@ sub send() { $item_table.id as item_id, $item_table.text as item_text, $item_table.name as item_name, $item_table.anonymous as item_anonymous, $item_table.confirmed as item_confirmed, + $item_table.photo as item_photo, $head_table.* from alert, $item_table, $head_table where alert.parameter::integer = $head_table.id @@ -63,7 +68,7 @@ sub send() { $query = dbh()->prepare($query); $query->execute(); my $last_alert_id; - my %data = ( template => $alert_type->template, data => '', schema => $schema ); + my %data = ( template => $alert_type->template, data => [], schema => $schema ); while (my $row = $query->fetchrow_hashref) { my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($row->{alert_cobrand})->new(); @@ -84,7 +89,7 @@ sub send() { } ); if ($last_alert_id && $last_alert_id != $row->{alert_id}) { _send_aggregated_alert_email(%data); - %data = ( template => $alert_type->template, data => '', schema => $schema ); + %data = ( template => $alert_type->template, data => [], schema => $schema ); } # create problem status message for the templates @@ -116,30 +121,50 @@ sub send() { } else { $data{problem_url} = $url . "/report/" . $row->{id}; } - $data{data} .= $row->{item_name} . ' : ' if $row->{item_name} && !$row->{item_anonymous}; - if ( $cobrand->include_time_in_update_alerts ) { - my $parser = DateTime::Format::Pg->new(); - my $dt = $parser->parse_timestamp( $row->{item_confirmed} ); - # We need to always set this otherwise we end up with the DateTime - # object being in the floating timezone in which case applying a - # subsequent timezone set will have no effect. - # this is basically recreating the code from the inflate wrapper - # in the database model. - FixMyStreet->set_time_zone($dt); - $data{data} .= $cobrand->prettify_dt( $dt, 'alert' ) . "\n\n"; - } - $data{data} .= $row->{item_text} . "\n\n------\n\n"; + + my $dt = $parser->parse_timestamp( $row->{item_confirmed} ); + # We need to always set this otherwise we end up with the DateTime + # object being in the floating timezone in which case applying a + # subsequent timezone set will have no effect. + # this is basically recreating the code from the inflate wrapper + # in the database model. + FixMyStreet->set_time_zone($dt); + $row->{confirmed} = $dt; + + # Hack in the image for the non-object updates + $row->{get_first_image_fp} = sub { + return FixMyStreet::App::Model::PhotoSet->new({ + db_data => $row->{item_photo}, + })->get_image_data( num => 0, size => 'fp' ); + }; + # this is ward and council problems } else { - $data{data} .= $url . "/report/" . $row->{id} . " - $row->{title}\n\n"; if ( exists $row->{geocode} && $row->{geocode} && $ref =~ /ward|council/ ) { my $nearest_st = _get_address_from_gecode( $row->{geocode} ); - $data{data} .= $nearest_st if $nearest_st; + $row->{nearest} = $nearest_st; } - $data{data} .= "\n\n------\n\n"; + + my $dt = $parser->parse_timestamp( $row->{confirmed} ); + FixMyStreet->set_time_zone($dt); + $row->{confirmed} = $dt; + + # Hack in the image for the non-object reports + $row->{get_first_image_fp} = sub { + return FixMyStreet::App::Model::PhotoSet->new({ + db_data => $row->{photo}, + })->get_image_data( num => 0, size => 'fp' ); + }; } + + push @{$data{data}}, $row; + if (!$data{alert_user_id}) { %data = (%data, %$row); + if ($ref eq 'new_updates') { + # Get a report object for its photo and static map + $data{report} = $schema->resultset('Problem')->find({ id => $row->{id} }); + } if ($ref eq 'area_problems' || $ref eq 'council_problems' || $ref eq 'ward_problems') { my $va_info = mySociety::MaPit::call('area', $row->{alert_parameter}); $data{area_name} = $va_info->{name}; @@ -149,7 +174,7 @@ sub send() { $data{ward_name} = $va_info->{name}; } } - $data{cobrand} = $row->{alert_cobrand}; + $data{cobrand} = $cobrand; $data{cobrand_data} = $row->{alert_cobrand_data}; $data{lang} = $row->{alert_lang}; $last_alert_id = $row->{alert_id}; @@ -183,15 +208,16 @@ sub send() { my $states = "'" . join( "', '", FixMyStreet::DB::Result::Problem::visible_states() ) . "'"; my %data = ( template => $template, - data => '', + data => [], alert_id => $alert->id, alert_email => $alert->user->email, lang => $alert->lang, - cobrand => $alert->cobrand, + cobrand => $cobrand, cobrand_data => $alert->cobrand_data, schema => $schema, ); - my $q = "select problem.id, problem.bodies_str, problem.postcode, problem.geocode, problem.title from problem_find_nearby(?, ?, ?) as nearby, problem, users + my $q = "select problem.id, problem.bodies_str, problem.postcode, problem.geocode, problem.confirmed, + problem.title, problem.detail, problem.photo from problem_find_nearby(?, ?, ?) as nearby, problem, users where nearby.problem_id = problem.id and problem.user_id = users.id and problem.state in ($states) @@ -207,24 +233,31 @@ sub send() { alert_id => $alert->id, parameter => $row->{id}, } ); - my $url = $cobrand->base_url_for_report($row); - $data{data} .= $url . "/report/" . $row->{id} . " - $row->{title}\n\n"; if ( exists $row->{geocode} && $row->{geocode} ) { my $nearest_st = _get_address_from_gecode( $row->{geocode} ); - $data{data} .= $nearest_st if $nearest_st; + $row->{nearest} = $nearest_st; } - $data{data} .= "\n\n------\n\n"; + my $dt = $parser->parse_timestamp( $row->{confirmed} ); + FixMyStreet->set_time_zone($dt); + $row->{confirmed} = $dt; + $row->{get_first_image_fp} = sub { + return FixMyStreet::App::Model::PhotoSet->new({ + db_data => $row->{photo}, + })->get_image_data( num => 0, size => 'fp' ); + }; + push @{$data{data}}, $row; } - _send_aggregated_alert_email(%data) if $data{data}; + _send_aggregated_alert_email(%data) if @{$data{data}}; } } sub _send_aggregated_alert_email(%) { my %data = @_; - my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($data{cobrand})->new(); + my $cobrand = $data{cobrand}; $cobrand->set_lang_and_domain( $data{lang}, 1, FixMyStreet->path_to('locale')->stringify ); + FixMyStreet::Map::set_map_class($cobrand->map_type); if (!$data{alert_email}) { my $user = $data{schema}->resultset('User')->find( { diff --git a/perllib/FixMyStreet/Script/Questionnaires.pm b/perllib/FixMyStreet/Script/Questionnaires.pm index c5bc6bfe0..3f22eb150 100644 --- a/perllib/FixMyStreet/Script/Questionnaires.pm +++ b/perllib/FixMyStreet/Script/Questionnaires.pm @@ -5,6 +5,7 @@ use warnings; use Utils; use FixMyStreet::DB; use FixMyStreet::Email; +use FixMyStreet::Map; use FixMyStreet::Cobrand; sub send { @@ -41,6 +42,7 @@ sub send_questionnaires_period { my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($row->cobrand)->new(); $cobrand->set_lang_and_domain($row->lang, 1); + FixMyStreet::Map::set_map_class($cobrand->map_type); # Not all cobrands send questionnaires next unless $cobrand->send_questionnaires; @@ -53,6 +55,7 @@ sub send_questionnaires_period { next unless $cobrand->email_host; my %h = map { $_ => $row->$_ } qw/name title detail category/; + $h{report} = $row; $h{created} = Utils::prettify_duration( time() - $row->confirmed->epoch, 'week' ); my $questionnaire = $rs->create( { diff --git a/perllib/FixMyStreet/Script/Reports.pm b/perllib/FixMyStreet/Script/Reports.pm index 30d24f640..311d8fec4 100644 --- a/perllib/FixMyStreet/Script/Reports.pm +++ b/perllib/FixMyStreet/Script/Reports.pm @@ -14,6 +14,7 @@ use FixMyStreet; use FixMyStreet::Cobrand; use FixMyStreet::DB; use FixMyStreet::Email; +use FixMyStreet::Map; use FixMyStreet::SendReport; sub send(;$) { @@ -60,6 +61,7 @@ sub send(;$) { } $cobrand->set_lang_and_domain($row->lang, 1); + FixMyStreet::Map::set_map_class($cobrand->map_type); if ( $row->is_from_abuser) { $row->update( { state => 'hidden' } ); debug_print("hiding because its sender is flagged as an abuser", $row->id) if $debug_mode; @@ -73,6 +75,7 @@ sub send(;$) { # Template variables for the email my $email_base_url = $cobrand->base_url_for_report($row); my %h = map { $_ => $row->$_ } qw/id title detail name category latitude longitude used_map/; + $h{report} = $row; map { $h{$_} = $row->user->$_ || '' } qw/email phone/; $h{confirmed} = DateTime::Format::Pg->format_datetime( $row->confirmed->truncate (to => 'second' ) ) if $row->confirmed; diff --git a/perllib/FixMyStreet/SendReport/Email.pm b/perllib/FixMyStreet/SendReport/Email.pm index 8582ebb3b..2eab1c754 100644 --- a/perllib/FixMyStreet/SendReport/Email.pm +++ b/perllib/FixMyStreet/SendReport/Email.pm @@ -52,7 +52,6 @@ sub build_recipient_list { sub get_template { my ( $self, $row ) = @_; - return 'submit-oxfordshire.txt' if $row->cobrand eq 'fixmystreet' && $row->bodies_str eq 2237; return 'submit.txt'; } diff --git a/perllib/FixMyStreet/TestMech.pm b/perllib/FixMyStreet/TestMech.pm index 937780a31..5f4a6ceed 100644 --- a/perllib/FixMyStreet/TestMech.pm +++ b/perllib/FixMyStreet/TestMech.pm @@ -221,6 +221,48 @@ sub get_email { return $emails[0]; } +sub get_text_body_from_email { + my ($mech, $email, $obj) = @_; + unless ($email) { + $email = $mech->get_email; + $mech->clear_emails_ok; + } + + my $body; + $email->walk_parts(sub { + my $part = shift; + return if $part->subparts; + return if $part->content_type !~ m{text/plain}; + $body = $obj ? $part : $part->body; + ok $body, "Found text body"; + }); + return $body; +} + +sub get_link_from_email { + my ($mech, $email, $multiple) = @_; + unless ($email) { + $email = $mech->get_email; + $mech->clear_emails_ok; + } + + my @links; + $email->walk_parts(sub { + my $part = shift; + return if $part->subparts; + return if $part->content_type !~ m{text/}; + if (@links) { + # Must be an HTML part now, first two links are in header + my @html_links = $part->body =~ m{https?://[^"]+}g; + is $links[0], $html_links[2], 'HTML link matches text link'; + } else { + @links = $part->body =~ m{https?://\S+}g; + ok @links, "Found links in email '@links'"; + } + }); + return $multiple ? @links : $links[0]; +} + =head2 get_first_email $email = $mech->get_first_email(@emails); |