275 lines
8.6 KiB
Perl
275 lines
8.6 KiB
Perl
|
|
#!/usr/bin/perl
|
||
|
|
#
|
||
|
|
# This code was forked from the LiveJournal project owned and operated
|
||
|
|
# by Live Journal, Inc. The code has been modified and expanded by
|
||
|
|
# Dreamwidth Studios, LLC. These files were originally licensed under
|
||
|
|
# the terms of the license supplied by Live Journal, Inc, which can
|
||
|
|
# currently be found at:
|
||
|
|
#
|
||
|
|
# http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt
|
||
|
|
#
|
||
|
|
# In accordance with the original license, this code and all its
|
||
|
|
# modifications are provided under the GNU General Public License.
|
||
|
|
# A copy of that license can be found in the LICENSE file included as
|
||
|
|
# part of this distribution.
|
||
|
|
|
||
|
|
package LJ;
|
||
|
|
|
||
|
|
use strict;
|
||
|
|
use v5.10;
|
||
|
|
use Log::Log4perl;
|
||
|
|
my $log = Log::Log4perl->get_logger(__PACKAGE__);
|
||
|
|
|
||
|
|
use Encode qw( encode from_to );
|
||
|
|
use IO::Socket::INET;
|
||
|
|
use Mail::Address;
|
||
|
|
use MIME::Base64 qw( encode_base64 );
|
||
|
|
use MIME::Lite;
|
||
|
|
use MIME::Words qw( encode_mimeword );
|
||
|
|
use Text::Wrap ();
|
||
|
|
use Time::HiRes qw( gettimeofday tv_interval );
|
||
|
|
|
||
|
|
use DW::Stats;
|
||
|
|
use DW::Task::SendEmail;
|
||
|
|
use LJ::CleanHTML;
|
||
|
|
|
||
|
|
# <LJFUNC>
|
||
|
|
# name: LJ::send_mail
|
||
|
|
# des: Sends email. Character set will only be used if message is not ASCII.
|
||
|
|
# args: opt, async_caller
|
||
|
|
# des-opt: Hashref of arguments. Required: to, from, subject, body.
|
||
|
|
# Optional: toname, fromname, cc, bcc, charset, wrap, html.
|
||
|
|
# All text must be in UTF-8 (without UTF flag, as usual in LJ code).
|
||
|
|
# Body and subject are converted to recipient-user mail encoding.
|
||
|
|
# Subject line is encoded according to RFC 2047.
|
||
|
|
# Warning: opt can be a MIME::Lite ref instead, in which
|
||
|
|
# case it is sent as-is.
|
||
|
|
# </LJFUNC>
|
||
|
|
sub send_mail {
|
||
|
|
my $opt = shift;
|
||
|
|
my $async_caller = shift;
|
||
|
|
|
||
|
|
my $msg = $opt;
|
||
|
|
|
||
|
|
# Record stats about who called us. This is pretty gross, but there are many, many
|
||
|
|
# callers so it seems easier to amend this instead of going back and redefining
|
||
|
|
# the LJ::send_mail API. For now.
|
||
|
|
my ( $package, $filename, $line ) = caller;
|
||
|
|
DW::Stats::increment( 'dw.mail.send', 1, [ 'caller:' . "$package/$line" ] );
|
||
|
|
|
||
|
|
# did they pass a MIME::Lite object already?
|
||
|
|
unless ( ref $msg eq 'MIME::Lite' ) {
|
||
|
|
|
||
|
|
my $clean_name = sub {
|
||
|
|
my ( $name, $email ) = @_;
|
||
|
|
return $email unless $name;
|
||
|
|
$name =~ s/[\n\t\"<>]//g;
|
||
|
|
return $name ? "\"$name\" <$email>" : $email;
|
||
|
|
};
|
||
|
|
|
||
|
|
my $body = $opt->{'wrap'} ? Text::Wrap::wrap( '', '', $opt->{'body'} ) : $opt->{'body'};
|
||
|
|
my $subject = $opt->{'subject'};
|
||
|
|
my $fromname = $opt->{'fromname'};
|
||
|
|
|
||
|
|
# if it's not ascii, add a charset header to either what we were explictly told
|
||
|
|
# it is (for instance, if the caller transcoded it), or else we assume it's utf-8.
|
||
|
|
# Note: explicit us-ascii default charset suggested by RFC2854 sec 6.
|
||
|
|
$opt->{'charset'} ||= "utf-8";
|
||
|
|
my $charset;
|
||
|
|
if ( !LJ::is_ascii($subject)
|
||
|
|
|| !LJ::is_ascii($body)
|
||
|
|
|| ( $opt->{html} && !LJ::is_ascii( $opt->{html} ) )
|
||
|
|
|| !LJ::is_ascii($fromname) )
|
||
|
|
{
|
||
|
|
$charset = $opt->{'charset'};
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
$charset = 'us-ascii';
|
||
|
|
}
|
||
|
|
|
||
|
|
# Don't convert from us-ascii and utf-8 charsets.
|
||
|
|
unless ( ( $charset =~ m/us-ascii/i ) || ( $charset =~ m/^utf-8$/i ) ) {
|
||
|
|
from_to( $body, "utf-8", $charset );
|
||
|
|
|
||
|
|
# Convert also html-part if we has it.
|
||
|
|
if ( $opt->{html} ) {
|
||
|
|
from_to( $opt->{html}, "utf-8", $charset );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
from_to( $subject, "utf-8", $charset ) unless $charset =~ m/^utf-8$/i;
|
||
|
|
if ( !LJ::is_ascii($subject) ) {
|
||
|
|
$subject = MIME::Words::encode_mimeword( $subject, 'B', $charset );
|
||
|
|
}
|
||
|
|
|
||
|
|
from_to( $fromname, "utf-8", $charset ) unless $charset =~ m/^utf-8$/i;
|
||
|
|
if ( !LJ::is_ascii($fromname) ) {
|
||
|
|
$fromname = MIME::Words::encode_mimeword( $fromname, 'B', $charset );
|
||
|
|
}
|
||
|
|
$fromname = $clean_name->( $fromname, $opt->{'from'} );
|
||
|
|
|
||
|
|
if ( $opt->{html} ) {
|
||
|
|
|
||
|
|
# do multipart, with plain and HTML parts
|
||
|
|
|
||
|
|
$msg = new MIME::Lite(
|
||
|
|
'From' => $fromname,
|
||
|
|
'To' => $clean_name->( $opt->{'toname'}, $opt->{'to'} ),
|
||
|
|
'Cc' => $opt->{cc} || '',
|
||
|
|
'Bcc' => $opt->{bcc} || '',
|
||
|
|
'Subject' => $subject,
|
||
|
|
'Type' => 'multipart/alternative'
|
||
|
|
);
|
||
|
|
|
||
|
|
# add the plaintext version
|
||
|
|
my $plain = $msg->attach(
|
||
|
|
'Type' => 'text/plain',
|
||
|
|
'Data' => "$body\n",
|
||
|
|
'Encoding' => 'quoted-printable',
|
||
|
|
);
|
||
|
|
$plain->attr( "content-type.charset" => $charset );
|
||
|
|
|
||
|
|
# add the html version
|
||
|
|
my $html = $msg->attach(
|
||
|
|
'Type' => 'text/html',
|
||
|
|
'Data' => $opt->{html},
|
||
|
|
'Encoding' => 'quoted-printable',
|
||
|
|
);
|
||
|
|
$html->attr( "content-type.charset" => $charset );
|
||
|
|
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
# no html version, do simple email
|
||
|
|
$msg = new MIME::Lite(
|
||
|
|
'From' => $fromname,
|
||
|
|
'To' => $clean_name->( $opt->{'toname'}, $opt->{'to'} ),
|
||
|
|
'Cc' => $opt->{cc} || '',
|
||
|
|
'Bcc' => $opt->{bcc} || '',
|
||
|
|
'Subject' => $subject,
|
||
|
|
'Type' => 'text/plain',
|
||
|
|
'Data' => $body,
|
||
|
|
'Encoding' => 'quoted-printable'
|
||
|
|
);
|
||
|
|
|
||
|
|
$msg->attr( "content-type.charset" => $charset );
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( $opt->{headers} ) {
|
||
|
|
while ( my ( $tag, $value ) = each %{ $opt->{headers} } ) {
|
||
|
|
$msg->add( $tag, $value );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# at this point $msg is a MIME::Lite
|
||
|
|
|
||
|
|
# Enqueue in the task system for sending out by a worker
|
||
|
|
my $starttime = [ gettimeofday() ];
|
||
|
|
my ($env_from) = map { $_->address } Mail::Address->parse( $msg->get('From') );
|
||
|
|
my @rcpts;
|
||
|
|
push @rcpts, map { $_->address } Mail::Address->parse( $msg->get($_) ) foreach (qw(To Cc Bcc));
|
||
|
|
my $host;
|
||
|
|
if ( @rcpts == 1 ) {
|
||
|
|
$rcpts[0] =~ /(.+)@(.+)$/;
|
||
|
|
$host = lc($2) . '@' . lc($1); # we store it reversed in database
|
||
|
|
}
|
||
|
|
my $h = DW::TaskQueue->dispatch(
|
||
|
|
DW::Task::SendEmail->new(
|
||
|
|
{
|
||
|
|
env_from => $env_from,
|
||
|
|
rcpts => \@rcpts,
|
||
|
|
data => $msg->as_string,
|
||
|
|
logger_mdc => ref $opt eq 'HASH' ? $opt->{logger_mdc} : undef,
|
||
|
|
},
|
||
|
|
)
|
||
|
|
);
|
||
|
|
return $h ? 1 : 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
=head2 C<< LJ::send_formatted_mail( %opts ) >>
|
||
|
|
|
||
|
|
Wrapper around LJ::send_mail.
|
||
|
|
|
||
|
|
Sends an email in the form of:
|
||
|
|
|
||
|
|
[[greeting]],
|
||
|
|
[[body as plaintext/html]]
|
||
|
|
[[footer]]
|
||
|
|
|
||
|
|
The greeting and footer are generated automatically. The body must not include these.
|
||
|
|
|
||
|
|
Required arguments:
|
||
|
|
|
||
|
|
=over
|
||
|
|
=item to - email address
|
||
|
|
=item from - email address
|
||
|
|
=item subject
|
||
|
|
=item body - The body is formatted automatically using Markdown; there's no need to do any text processing yourself.
|
||
|
|
=back
|
||
|
|
|
||
|
|
Optional arguments:
|
||
|
|
=over
|
||
|
|
=item greeting_user - the name to greet this user by. If not provided, we don't show the greeting
|
||
|
|
=item toname - display name
|
||
|
|
=item fromname - display name
|
||
|
|
=item cc
|
||
|
|
=item bcc
|
||
|
|
=item charset
|
||
|
|
=back
|
||
|
|
|
||
|
|
|
||
|
|
=cut
|
||
|
|
|
||
|
|
sub send_formatted_mail {
|
||
|
|
my (%opts) = @_;
|
||
|
|
|
||
|
|
my ( $html_body, $plain_body ) = LJ::format_mail( $opts{body}, $opts{greeting_user} );
|
||
|
|
return LJ::send_mail(
|
||
|
|
{
|
||
|
|
to => $opts{to},
|
||
|
|
from => $opts{from},
|
||
|
|
subject => $opts{subject},
|
||
|
|
|
||
|
|
body => $plain_body,
|
||
|
|
html => $html_body,
|
||
|
|
|
||
|
|
toname => $opts{toname},
|
||
|
|
fromname => $opts{fromname},
|
||
|
|
cc => $opts{cc},
|
||
|
|
bcc => $opts{bcc},
|
||
|
|
charset => $opts{charset},
|
||
|
|
}
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
=head2 C<< LJ::format_mail( $text )>>
|
||
|
|
|
||
|
|
Returns the formatted version of the text as a list of: ( $html_body, $plaintext_body )
|
||
|
|
|
||
|
|
Automatically appends greeting and footer.
|
||
|
|
|
||
|
|
=cut
|
||
|
|
|
||
|
|
sub format_mail {
|
||
|
|
my ( $text, $greeting_user ) = @_;
|
||
|
|
|
||
|
|
my $greeting =
|
||
|
|
$greeting_user ? LJ::Lang::ml( "email.greeting", { user => $greeting_user } ) : "";
|
||
|
|
my $footer = LJ::Lang::ml( "email.footer",
|
||
|
|
{ sitename => $LJ::SITENAMESHORT, siteroot => $LJ::SITEROOT } );
|
||
|
|
|
||
|
|
$text = "$greeting\n\n$text\n\n$footer";
|
||
|
|
|
||
|
|
# use markdown to format from text to HTML
|
||
|
|
my $html = $text;
|
||
|
|
my $opts = { editor => 'markdown' };
|
||
|
|
LJ::CleanHTML::clean_event( \$html, $opts );
|
||
|
|
|
||
|
|
# use plaintext as-is, but look for "[links like these](url)", and change them to "links like these (url)"
|
||
|
|
my $plaintext = LJ::strip_html($text);
|
||
|
|
$plaintext =~ s/\[(.*?)\]\(/$1 (/g;
|
||
|
|
|
||
|
|
return ( $html, $plaintext );
|
||
|
|
}
|
||
|
|
1;
|