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{(?=)}{$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 = < $text 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 '' 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;