webgui/lib/WebGUI/Mail/Send.pm

598 lines
16 KiB
Perl

package WebGUI::Mail::Send;
=head1 LEGAL
-------------------------------------------------------------------
WebGUI is Copyright 2001-2009 Plain Black Corporation.
-------------------------------------------------------------------
Please read the legal notices (docs/legal.txt) and the license
(docs/license.txt) that came with this distribution before using
this software.
-------------------------------------------------------------------
http://www.plainblack.com info@plainblack.com
-------------------------------------------------------------------
=cut
use strict;
use LWP::MediaTypes qw(guess_media_type);
use MIME::Entity;
use MIME::Parser;
use Net::SMTP;
use WebGUI::Group;
use WebGUI::Macro;
use WebGUI::User;
use WebGUI::HTML;
use Encode qw(encode);
=head1 NAME
Package WebGUI::Mail::Send
=head1 DESCRIPTION
This package is used for sending emails via SMTP.
=head1 SYNOPSIS
use WebGUI::Mail::Send;
my $mail = WebGUI::Mail::Send->create($session, { to=>$to, from=>$from, subject=>$subject});
my $mail = WebGUI::Mail::Send->retrieve($session, $messageId);
$mail->addText($text);
$mail->addHtml($html);
$mail->addAttachment($pathToFile);
$mail->send;
$mail->queue;
=head1 METHODS
These methods are available from this class:
=cut
#-------------------------------------------------------------------
=head2 addAttachment ( pathToFile [ , mimetype ] )
Adds an attachment to the message.
=head3 pathToFile
The filesystem path to the file you wish to attach.
=head3 mimetype
Optionally specify a mime type for this attachment. If one is not specified it will be guessed based upon the file extension.
=cut
sub addAttachment {
my $self = shift;
my $path = shift;
my $mimetype = shift || guess_media_type($path);
$self->getMimeEntity->attach(
Path=>$path,
Encoding=>'-SUGGEST',
Type=>$mimetype
);
}
#-------------------------------------------------------------------
=head2 addFooter ( )
Adds the mail footer as set by the site admin to the end of the first
part of this message. If the first part of the message has an HTML MIME-type,
then it will translate the footer to HTML.
If the message is empty, it will create a MIME entity part to hold it.
Macros in the footer will be evaluated.
=cut
sub addFooter {
my $self = shift;
return if $self->{_footerAdded};
my $footer = "\n\n".$self->session->setting->get("mailFooter");
WebGUI::Macro::process($self->session, \$footer);
my $text = encode("utf8", $footer);
$self->{_footerAdded} = 1;
my @parts = $self->getMimeEntity->parts();
##No parts yet, add one with the footer content.
if (! $parts[0]) {
$self->addText($text);
return;
}
##Get the content of the first part, drop it from the set of parts
my $mime_body = $parts[0]->bodyhandle;
my $body_content = join '', $mime_body->as_lines;
my $mime_type;
if ($parts[0]->effective_type eq 'text/plain') {
$body_content .= $text;
my $new_part = MIME::Entity->build(
Charset => "UTF-8",
Encoding => "quoted-printable",
Type => 'text/plain',
Data => $body_content,
);
shift @parts;
unshift @parts, $new_part;
$self->getMimeEntity->parts(\@parts);
}
elsif ($parts[0]->effective_type eq 'text/html') {
$text = WebGUI::HTML::format($text, 'mixed');
$body_content =~ s{(?=</body>)}{$text};
my $new_part = MIME::Entity->build(
Charset => "UTF-8",
Encoding => "quoted-printable",
Type => 'text/html',
Data => $body_content,
);
shift @parts;
unshift @parts, $new_part;
$self->getMimeEntity->parts(\@parts);
}
}
#-------------------------------------------------------------------
=head2 addHeaderField ( name, value )
Adds a header field to the mail message. See also replaceHeaderField().
=head3 name
The name of the field to add.
=head3 value
The value of the field to add.
=cut
sub addHeaderField {
my $self = shift;
my $name = shift;
my $value = shift;
#$self->getMimeEntity->head->add($name, $value);
$self->getMimeEntity->head->add($name, encode('MIME-Q', $value));
}
#-------------------------------------------------------------------
=head2 addHtml ( html )
Appends an HTML block to the message.
=head3 html
A string of HTML.
=cut
sub addHtml {
my $self = shift;
my $text = shift;
if ($text !~ /<(?:html|body)/) {
my $site = $self->session->url->getSiteURL;
$text = <<END_HTML;
<html>
<head>
<base href="$site">
</head>
<body>
$text
</body>
</html>
END_HTML
}
$self->addHtmlRaw($text);
}
#-------------------------------------------------------------------
=head2 addHtmlRaw ( html )
Appends an HTML block to the message without wrapping in a document.
=head3 html
A string of HTML.
=cut
sub addHtmlRaw {
my $self = shift;
my $text = shift;
$self->getMimeEntity->attach(
Charset => "UTF-8",
Encoding => "quoted-printable",
Data => encode('utf8', $text ),
Type => "text/html",
);
return undef;
}
#-------------------------------------------------------------------
=head2 addText ( text )
Adds a text message to the email.
=head3 text
A string of text.
=cut
sub addText {
my $self = shift;
my $text = shift;
$self->getMimeEntity->attach(
Charset => "UTF-8",
Encoding => "quoted-printable",
Data => encode('utf8', $text ),
Type => 'text/plain',
);
return undef;
}
#-------------------------------------------------------------------
=head2 create ( session, headers )
Creates a new message and returns a WebGUI::Mail::Send object. This is a class method.
=head3 session
A reference to the current session.
=head3 headers
A hash reference containing addressing and other header level options.
=head4 to
A string containing a comma seperated list of email addresses to send to.
=head4 toUser
A WebGUI userId of a user you'd like to send this message to.
=head4 toGroup
A WebGUI groupId. The email address of the users in this group will be looked up and will each be sent a copy of this message.
=head4 subject
A short string of text to be placed in the subject line.
=head4 cc
A string containing a comma seperated list of email addresses to carbon copy on this message.
=head4 bcc
A string containing a comma seperated list of email addresses to blind carbon copy on this message.
=head4 from
A single email address that this message will originate from. Defaults to the company email address stored in the settings.
=head4 replyTo
A single email address that responses to this message will be sent to.
=head4 returnPath
The email address to send bounces to.
=head4 contentType
A mime type for the message. Defaults to "multipart/mixed".
=head4 messageId
A unique id for this message, in case you want to see what replies come in for it later. One will be automatically generated if you don't specify this.
=head4 inReplyTo
If this is a reply to a previous message, then you should specify the messageId of the previous message here.
=head3 isInbox
A flag indicating that this email message is from the Inbox, and should follow per user settings
for delivery.
=cut
sub create {
my $class = shift;
my $session = shift;
my $headers = shift;
my $isInbox = shift;
if ($headers->{toUser}) {
my $user = WebGUI::User->new($session, $headers->{toUser});
if (defined $user) {
my $email;
if ($isInbox) {
$email = $user->getInboxNotificationAddresses;
}
else {
$email = $user->profileField("email");
}
if ($email) {
if ($headers->{to}) {
$headers->{to} .= ','.$email;
} else {
$headers->{to} = $email;
}
}
}
}
my $from = $headers->{from} || $session->setting->get('comanyName') . " <".$session->setting->get("companyEmail").">";
my $type = $headers->{contentType} || "multipart/mixed";
my $replyTo = $headers->{replyTo} || $session->setting->get("mailReturnPath");
# format of Message-Id should be '<unique-id@domain>'
my $id = $headers->{messageId} || "WebGUI-" . $session->id->generate;
if ($id !~ m/\@/) {
my $domain = $from;
$domain =~ s/^.*\@//msx;
$domain =~ s/>$//msx;
$id .= '@' . $domain;
}
if ($id !~ m/^<.+?>$/msx) {
$id =~ s/(^<)|(>$)//msxg;
$id = "<".$id.">";
}
my $message = MIME::Entity->build(
Type=>$type,
From=> encode('MIME-Q', $from),
To=> encode('MIME-Q', $headers->{to}),
Cc=> encode('MIME-Q', $headers->{cc}),
Bcc=> encode('MIME-Q', $headers->{bcc}),
"Reply-To"=> encode('MIME-Q', $replyTo),
"In-Reply-To"=> encode('MIME-Q', $headers->{inReplyTo}),
Subject=> encode('MIME-Q', $headers->{subject}),
"Message-Id"=>$id,
Date=>$session->datetime->epochToMail,
"X-Mailer"=>"WebGUI"
);
$message->head->add("X-Return-Path", $headers->{returnPath} || $session->setting->get("mailReturnPath") || $from);
$type = $headers->{contentType};
if ($session->config->get("emailOverride")) {
my $to = $headers->{to};
$to = "WebGUI Group ".$headers->{toGroup} if ($headers->{toGroup});
$message->head->replace("to", $session->config->get("emailOverride"));
$message->head->replace("cc",undef);
$message->head->replace("bcc",undef);
delete $headers->{toGroup};
$message->attach(Data=>"This message was intended for ".$to." but was overridden in the config file.\n\n");
}
return bless {
_message => $message,
_session => $session,
_toGroup => $headers->{toGroup},
_isInbox => $isInbox,
_footerAdded => 0,
}, $class;
}
#-------------------------------------------------------------------
=head2 getMessageIdsInQueue ( session )
Returns an array reference of the message IDs in the mail queue. Use with the retrieve() method. This is a class method.
=head3 session
A reference to the current session.
=cut
sub getMessageIdsInQueue {
my $class = shift;
my $session = shift;
return $session->db->buildArrayRef("select messageId from mailQueue");
}
#----------------------------------------------------------------------------
=head2 getMimeEntity ( )
Returns the MIME::Entity object associated with this mail message.
=cut
sub getMimeEntity {
my $self = shift;
return $self->{_message};
}
#-------------------------------------------------------------------
=head2 queue ( )
Puts this message in the mail queue so it can be sent out later by the workflow system. Returns a messageId so that the message can be retrieved later if necessary. Note that this is the preferred method of sending messages because it keeps WebGUI running faster.
=cut
sub queue {
my $self = shift;
return $self->session->db->setRow("mailQueue", "messageId", { messageId=>"new", message=>$self->getMimeEntity->stringify, toGroup=>$self->{_toGroup} });
}
#-------------------------------------------------------------------
=head2 replaceHeaderField ( name, value )
Replaces an existing header field in the mail message, or creates it if it doesn't exist. See also addHeaderField().
=head3 name
The name of the field to replace.
=head3 value
The value of the field to replace.
=cut
sub replaceHeaderField {
my $self = shift;
my $name = shift;
my $value = shift;
$self->getMimeEntity->head->replace($name, $value);
}
#-------------------------------------------------------------------
=head2 retrieve ( session, messageId )
Retrieves a message from the mail queue, which thusly deletes it from the queue. This is a class method.
=head3 session
A reference to the current session.
=head3 messageId
The unique id for a message in the queue.
=cut
sub retrieve {
my $class = shift;
my $session = shift;
my $messageId = shift;
return undef unless $messageId;
my $data = $session->db->getRow("mailQueue","messageId", $messageId);
return undef unless $data->{messageId};
$session->db->deleteRow("mailQueue","messageId", $messageId);
my $parser = MIME::Parser->new;
$parser->output_to_core(1);
bless {_session=>$session, _message=>$parser->parse_data($data->{message}), _toGroup=>$data->{toGroup}}, $class;
}
#-------------------------------------------------------------------
=head2 send ( )
Sends the message via the SMTP server defined in the settings. If the config file setting
emailToLog is set to a true value, then the message is sent to the WebGUI log file with
priority WARN.
Returns 1 if successful.
=cut
sub send {
my $self = shift;
my $session = $self->session;
my $log = $session->log;
my $mail = $self->getMimeEntity;
my $smtpServer = $session->setting->get("smtpServer");
my $status = 1;
if ($mail->parts <= 1) {
$mail->make_singlepart;
}
if ($mail->head->get("To")) {
if ($session->config->get("emailToLog")){
my $message = $mail->stringify;
$log->warn(qq{$message
\nTHIS MESSAGE WAS NOT SENT THROUGH THE MAIL SERVER. TO RE-ENABLE MAIL, DISABLE THE emailToLog SETTING IN THE CONFIG FILE.
});
}
elsif ($smtpServer =~ /\/sendmail/) {
if (open(MAIL,"| ".$smtpServer." -t -oi -oem")) {
$mail->print(\*MAIL);
close(MAIL) or $log->error("Couldn't close connection to mail server: ".$smtpServer);
}
else {
$log->error("Couldn't connect to mail server: ".$smtpServer);
$status = 0;
}
}
else {
my $smtp = Net::SMTP->new($smtpServer); # connect to an SMTP server
if (defined $smtp) {
$smtp->mail($mail->head->get('X-Return-Path'));
$smtp->to( split(',', $mail->head->get('to') ));
$smtp->cc( split(',', $mail->head->get('cc') ));
$smtp->bcc( split(',', $mail->head->get('bcc') ));
$smtp->data(); # Start the mail
$smtp->datasend($mail->stringify);
$smtp->dataend(); # Finish sending the mail
$smtp->quit; # Close the SMTP connection
}
else {
$log->error("Couldn't connect to mail server: ".$smtpServer);
$status = 0;
}
}
}
# due to the large number of emails that may be generated by sending emails to a group,
# emails to members of a group are queued rather than sent directly
my $group = $self->{_toGroup};
delete $self->{_toGroup};
if ($group) {
my $group = WebGUI::Group->new($self->session, $group);
return $status if !defined $group;
$mail->head->replace('bcc', undef);
$mail->head->replace('cc', undef);
USER: foreach my $userId (@{$group->getAllUsers(1)}) {
my $user = WebGUI::User->new($self->session, $userId);
next USER unless $user->status eq 'Active'; ##Don't send this to invalid user accounts
my $emailAddress;
if ($self->{_isInbox}) {
$emailAddress = $user->getInboxNotificationAddresses;
}
else {
$emailAddress = $user->profileField('email');
}
next USER unless $emailAddress;
$mail->head->replace('To', $emailAddress);
$self->queue;
}
#Delete the group if it is flagged as an AdHocMailGroup
$group->delete if ($group->isAdHocMailGroup);
}
return $status;
}
#-------------------------------------------------------------------
=head2 session ( )
Returns a reference to the current session.
=cut
sub session {
my $self = shift;
return $self->{_session};
}
1;