aboutsummaryrefslogtreecommitdiffstats
path: root/perllib/FixMyStreet
diff options
context:
space:
mode:
Diffstat (limited to 'perllib/FixMyStreet')
-rw-r--r--perllib/FixMyStreet/App.pm40
-rw-r--r--perllib/FixMyStreet/App/Controller/Contact.pm20
-rw-r--r--perllib/FixMyStreet/App/View/Email.pm7
-rw-r--r--perllib/FixMyStreet/Cobrand/Default.pm2
-rw-r--r--perllib/FixMyStreet/Email.pm126
-rw-r--r--perllib/FixMyStreet/Script/Alerts.pm89
-rw-r--r--perllib/FixMyStreet/Script/Questionnaires.pm3
-rw-r--r--perllib/FixMyStreet/Script/Reports.pm3
-rw-r--r--perllib/FixMyStreet/SendReport/Email.pm1
-rw-r--r--perllib/FixMyStreet/TestMech.pm42
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);