Files
selima-perl/lib/perl5/Selima/Mail.pm
2026-03-10 21:31:43 +08:00

1418 lines
44 KiB
Perl

# Selima Website Content Management System
# Mail.pm: The mail composer and sender.
# Copyright (c) 2004-2018 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Author: imacat <imacat@mail.imacat.idv.tw>
# First written: 2004-10-18
package Selima::Mail;
use 5.008;
use strict;
use warnings;
BEGIN {
# Prototype declaration
sub quote($);
sub rfc822_phrase_need_quoting($);
sub rfc1521_value_need_quoting($);
sub b64hdr_encode($$);
sub rfc822_time(;$);
}
use Digest::MD5 qw(md5_base64);
use Date::Format qw(time2str);
use Encode qw(encode is_utf8 FB_CROAK);
use File::Basename qw(basename);
use File::MMagic qw();
use MIME::Base64 qw(encode_base64);
use MIME::QuotedPrint qw(encode_qp);
use Net::SMTP;
use Time::HiRes qw();
use Socket qw(inet_aton inet_ntoa AF_INET);
BEGIN {
if (exists $ENV{"MOD_PERL_API_VERSION"} && $ENV{"MOD_PERL_API_VERSION"} >= 2) {
require Apache2::Connection;
}
}
use Selima::Cache qw(:mail);
use Selima::DataVars qw(:env :lninfo :mail);
use Selima::GeoIP;
use Selima::HTTP;
use Selima::HTTPS;
use Selima::LnInfo;
use Selima::LogIn;
use Selima::Server;
use Selima::Unicode;
use Selima::XFileIO;
use vars qw($MM);
$MM = new File::MMagic;
# new: Initialize the e-mail message
sub new : method {
local ($_, %_);
my ($class, $self);
$class = $_[0];
$self = bless {}, $class;
$self->{"body"} = undef;
$self->{"parts"} = [];
$self->{"charset"} = undef;
$self->{"modified"} = 0;
$self->{"output"} = undef;
$self->{"attaches"} = [];
if (-x "/usr/sbin/sendmail") {
$self->{"send_with"} = "sendmail";
} else {
$self->{"send_with"} = "smtp";
}
# RFC 821 headers/properties
$self->{"mail_from"} = undef;
$self->{"rcpt_to"} = undef;
# RFC 822 headers/properties
$self->{"from"} = undef;
$self->{"subject"} = undef;
$self->{"sender"} = undef;
$self->{"to"} = undef;
$self->{"cc"} = undef;
$self->{"bcc"} = undef;
$self->{"reply_to"} = undef;
# RFC 1521 MIME headers/properties
$self->{"type"} = "text/plain";
$self->{"id"} = undef;
# RFC 1766 Content-Language
$self->{"lang"} = undef;
# RFC 1806/2183 Content-Disposition
$self->{"disposition"} = undef;
$self->{"filename"} = undef;
$self->{"filemtime"} = undef;
$self->{"filesize"} = undef;
# RFC 1864 Content-MD5
$self->{"md5"} = 0;
# RFC 2387 MIME multipart/related headers/properties
$self->{"related_type"} = undef;
$self->{"related_start"} = undef;
# RFC 2369/2919 Mailing Lists headers/properties
$self->{"list_id"} = undef;
$self->{"list_help"} = undef;
$self->{"list_subscribe"} = undef;
$self->{"list_unsubscribe"} = undef;
$self->{"list_post"} = undef;
$self->{"list_owner"} = undef;
$self->{"list_archive"} = undef;
# Non-standard headers
$self->{"errors_to"} = undef;
$self->{"precedence"} = undef;
$self->{"mailer"} = "$class http://www.imacat.idv.tw/";
$self->{"any_header"} = [];
$self->{"boundary_len"} = 32;
return $self;
}
# RFC 821 headers/properties
# mail_from: Set/retrieve the MAIL FROM: sender
sub mail_from : method {
local ($_, %_);
my ($self, $email);
($self, $email) = @_;
if (@_ > 1) {
$self->{"mail_from"} = $email;
$self->{"modified"} = 1;
}
return $self->{"mail_from"};
}
# rcpt_to: Set/retrieve the real recipients (to be used in send())
sub rcpt_to : method {
local ($_, %_);
my ($self, $email);
($self, $email) = @_;
# Reset it
if (!defined $email) {
undef $self->{"rcpt_to"};
# Add it
} else {
$self->{"rcpt_to"} = []
if !defined $self->{"rcpt_to"} || ref $self->{"rcpt_to"} ne "ARRAY";
%_ = map { $_ => 1 } @{$self->{"rcpt_to"}};
push @{$self->{"rcpt_to"}}, $email
if !exists $_{$email};
}
return undef if !defined $self->{"rcpt_to"};
return join ",", @{$self->{"rcpt_to"}};
}
# RFC 822 headers/properties
# from: Set/retrieve the From: header
sub from : method {
local ($_, %_);
my ($self, $email, $name);
($self, $email, $name) = @_;
$self->_add_addr(\$self->{"from"}, $email, $name)
if @_ > 1;
return $self->_ret_addrs($self->{"from"});
}
# subject: Set/retrieve the subject
sub subject : method {
local ($_, %_);
my ($self, $subject);
($self, $subject) = @_;
if (@_ > 1) {
$self->{"subject"} = $subject;
$self->{"modified"} = 1;
}
return $self->{"subject"};
}
# sender: Set/retrieve the Sender: header
sub sender : method {
local ($_, %_);
my ($self, $email, $name);
($self, $email, $name) = @_;
$self->_set_addr(\$self->{"sender"}, $email, $name)
if @_ > 1;
return $self->_ret_addr($self->{"sender"});
}
# to: Set/retrieve the To: header
sub to : method {
local ($_, %_);
my ($self, $email, $name);
($self, $email, $name) = @_;
$self->_add_addr(\$self->{"to"}, $email, $name)
if @_ > 1;
return $self->_ret_addrs($self->{"to"});
}
# cc: Set/retrieve the Cc: header
sub cc : method {
local ($_, %_);
my ($self, $email, $name);
($self, $email, $name) = @_;
$self->_add_addr(\$self->{"cc"}, $email, $name)
if @_ > 1;
return $self->_ret_addrs($self->{"cc"});
}
# bcc: Set/retrieve the Bcc: header
sub bcc : method {
local ($_, %_);
my ($self, $email, $name);
($self, $email, $name) = @_;
$self->_add_addr(\$self->{"bcc"}, $email, $name)
if @_ > 1;
return $self->_ret_addrs($self->{"bcc"});
}
# reply_to: Set/retrieve the Reply-To: header
sub reply_to : method {
local ($_, %_);
my ($self, $email, $name);
($self, $email, $name) = @_;
$self->_add_addr(\$self->{"reply_to"}, $email, $name)
if @_ > 1;
return $self->_ret_addrs($self->{"reply_to"});
}
# RFC 1521 MIME headers/properties
# charset: Set/retrieve the desired character set
# Note that this is the desired character set. The input information
# should still in UTF-8
sub charset : method {
local ($_, %_);
my ($self, $charset);
($self, $charset) = @_;
if (@_ > 1) {
$self->{"charset"} = $charset;
$self->{"modified"} = 1;
}
return $self->{"charset"};
}
# type: Set/retrieve the MIME content type
sub type : method {
local ($_, %_);
my ($self, $type);
($self, $type) = @_;
if (@_ > 1) {
$self->{"type"} = $type;
$self->{"modified"} = 1;
}
return $self->{"type"};
}
# id: Set/retrieve the content ID.
sub id : method {
local ($_, %_);
my ($self, $id);
($self, $id) = @_;
if (@_ > 1) {
# Set a content ID.
if ($id) {
if (!defined $self->{"id"}) {
$self->{"id"} = $self->_new_msgid;
$self->{"modified"} = 1;
}
# Unset the content ID.
} else {
if (defined $self->{"id"}) {
undef $self->{"id"};
$self->{"modified"} = 1;
}
}
}
return $self->{"id"};
}
# RFC 1766 Content-Language
# lang: Set/retrieve the language
sub lang : method {
local ($_, %_);
my ($self, $lang);
($self, $lang) = @_;
if (@_ > 1) {
# Reset it
if (!defined $lang) {
undef $self->{"lang"};
# Add it
} else {
http_500 "bad language $lang"
unless is_usascii_printable $lang;
$lang = encode("US-ASCII", $lang) if is_utf8($lang);
$self->{"lang"} = []
if !defined $self->{"lang"} || ref $self->{"lang"} ne "ARRAY";
push @{$self->{"lang"}}, $lang;
}
$self->{"modified"} = 1;
}
return undef if !defined $self->{"lang"};
return join ", ", map ln($_, LN_NAME), @{$self->{"lang"}};
}
# RFC 1806/2183 Content-Disposition
# disposition: Set/retrieve the MIME content disposition
# "inline" or "attachment"
sub disposition : method {
local ($_, %_);
my ($self, $disposition);
($self, $disposition) = @_;
if (@_ > 1) {
if (!defined $disposition) {
undef $self->{"disposition"};
} elsif ($disposition =~ /^(?:inline|attachment)$/) {
$disposition = encode("US-ASCII", $disposition)
if is_utf8($disposition);
$self->{"disposition"} = $disposition;
} else {
http_500 "bad content disposition $disposition";
}
$self->{"modified"} = 1;
}
return $self->{"disposition"};
}
# filename: Set/retrieve the MIME content disposition filename
sub filename : method {
local ($_, %_);
my ($self, $filename);
($self, $filename) = @_;
if (@_ > 1) {
$self->{"filename"} = filename
$self->{"modified"} = 1;
}
return $self->{"filename"};
}
# filemtime: Set/retrieve the MIME content disposition modification time
sub filemtime : method {
local ($_, %_);
my ($self, $filemtime);
($self, $filemtime) = @_;
if (@_ > 1) {
http_500 "bad mtime $filemtime"
unless $filemtime =~ /^\d+$/;
$filemtime = encode("US-ASCII", $filemtime) if is_utf8($filemtime);
$self->{"filemtime"} = $filemtime;
$self->{"modified"} = 1;
}
return $self->{"filemtime"};
}
# filesize: Set/retrieve the MIME content disposition size
sub filesize : method {
local ($_, %_);
my ($self, $filesize);
($self, $filesize) = @_;
if (@_ > 1) {
http_500 "bad size $filesize"
unless $filesize =~ /^\d+$/;
$filesize = encode("US-ASCII", $filesize) if is_utf8($filesize);
$self->{"filesize"} = $filesize;
$self->{"modified"} = 1;
}
return $self->{"filesize"};
}
# RFC 1864 Content-MD5
# md5: Set/retrieve the MD5 digest enable/disable flag
sub md5 : method {
local ($_, %_);
my ($self, $md5);
($self, $md5) = @_;
if (@_ > 1) {
$self->{"md5"} = $md5? 1: 0;
$self->{"modified"} = 1;
}
return $self->{"md5"};
}
# RFC 2387 MIME multipart/related headers/properties
# related_type: Set/retrieve the multipart/related type
sub related_type : method {
local ($_, %_);
my ($self, $reltype);
($self, $reltype) = @_;
if (@_ > 1) {
$self->{"related_type"} = $reltype;
$self->{"modified"} = 1;
}
return $self->{"related_type"};
}
# related_start: Set/retrieve the multipart/related start
sub related_start : method {
local ($_, %_);
my ($self, $relstart);
($self, $relstart) = @_;
if (@_ > 1) {
$self->{"related_start"} = $relstart;
$self->{"modified"} = 1;
}
return $self->{"related_start"};
}
# RFC 2369/2919 Mailing Lists headers/properties
# list_id: Set/retrieve the list ID.
sub list_id : method {
local ($_, %_);
my ($self, $id, $phrase);
($self, $id, $phrase) = @_;
if (@_ > 1) {
# Reset it
if (!defined $id) {
undef $self->{"list_id"};
# Set it
} else {
$self->{"list_id"} = {
"id" => $id,
"phrase" => $phrase,
};
}
$self->{"modified"} = 1;
}
return undef if !defined $self->{"list_id"};
$_ = "<" . ${$self->{"list_id"}}{"id"} . ">";
if (defined ${$self->{"list_id"}}{"phrase"}) {
$phrase = ${$self->{"list_id"}}{"phrase"};
$phrase = quote $phrase if rfc822_phrase_need_quoting $phrase;
$_ = "$phrase $_";
}
return $_;
}
# list_help: Set/retrieve the list help address
sub list_help : method {
local ($_, %_);
my ($self, $url);
($self, $url) = @_;
$self->_add_list_url(\$self->{"list_help"}, $url)
if @_ > 1;
return $self->_ret_list_urls($self->{"list_help"});
}
# list_subscribe: Set/retrieve the list subscribe address
sub list_subscribe : method {
local ($_, %_);
my ($self, $url);
($self, $url) = @_;
$self->_add_list_url(\$self->{"list_subscribe"}, $url)
if @_ > 1;
return $self->_ret_list_urls($self->{"list_subscribe"});
}
# list_unsubscribe: Set/retrieve the list unsubscribe address
sub list_unsubscribe : method {
local ($_, %_);
my ($self, $url);
($self, $url) = @_;
$self->_add_list_url(\$self->{"list_unsubscribe"}, $url)
if @_ > 1;
return $self->_ret_list_urls($self->{"list_unsubscribe"});
}
# list_post: Set/retrieve the list post address
# "" means "List-Post: NO"
sub list_post : method {
local ($_, %_);
my ($self, $url);
($self, $url) = @_;
if (@_ > 1) {
if ($url eq "") {
$self->{"list_post"} = "";
} else {
$self->_add_list_url(\$self->{"list_post"}, $url);
}
}
return undef if !defined $self->{"list_post"};
return "NO" if ref $self->{"list_post"} ne "ARRAY";
return $self->_ret_list_urls($self->{"list_post"});
}
# list_owner: Set/retrieve the list owner address
sub list_owner : method {
local ($_, %_);
my ($self, $url);
($self, $url) = @_;
$self->_add_list_url(\$self->{"list_owner"}, $url)
if @_ > 1;
return $self->_ret_list_urls($self->{"list_owner"});
}
# list_archive: Set/retrieve the list archive address
sub list_archive : method {
local ($_, %_);
my ($self, $url);
($self, $url) = @_;
$self->_add_list_url(\$self->{"list_archive"}, $url)
if @_ > 1;
return $self->_ret_list_urls($self->{"list_archive"});
}
# Non-standard headers
# errors_to: Set/retrieve the Errors-To: header
sub errors_to : method {
local ($_, %_);
my ($self, $email);
($self, $email) = @_;
if (@_ > 1) {
if (!defined $email) {
undef $self->{"errors_to"};
} else {
$self->{"errors_to"} = [];
push @{$self->{"errors_to"}}, $email;
}
$self->{"modified"} = 1;
}
return $self->{"errors_to"};
}
# precedence: Set/retrieve the precedence, mentioned in RFC 2076
# "bulk" or "first-class"
sub precedence : method {
local ($_, %_);
my ($self, $precedence);
($self, $precedence) = @_;
if (@_ > 1) {
if (!defined $precedence) {
undef $self->{"precedence"};
} elsif ($precedence =~ /^(?:bulk|first-class)$/) {
$precedence = encode("US-ASCII", $precedence)
if is_utf8($precedence);
$self->{"precedence"} = $precedence;
} else {
http_500 "bad precedence $precedence";
}
$self->{"modified"} = 1;
}
return $self->{"precedence"};
}
# mailer: Set/retrieve the mailer
sub mailer : method {
local ($_, %_);
my ($self, $mailer);
($self, $mailer) = @_;
if (@_ > 1) {
$self->{"mailer"} = $mailer;
$self->{"modified"} = 1;
}
return $self->{"mailer"};
}
# any_header: Set/retrieve any header
sub any_header : method {
local ($_, %_);
my ($self, $name, $value, $ndx);
($self, $name, $value) = @_;
# Find the index of that header
undef $ndx;
for ($_ = 0; $_ < @{$self->{"any_header"}}; $_++) {
next if ${${$self->{"any_header"}}[$_]}{"name"} ne $name;
$ndx = $_;
last;
}
# Set the value
if (@_ > 1) {
if (defined $ndx) {
${${$self->{"any_header"}}[$ndx]}{"value"} = $value;
} else {
# Only add a defined value
if (defined $value) {
push @{$self->{"any_header"}}, {
"name" => $name,
"value" => $value,
}
}
}
$self->{"modified"} = 1;
return $value;
# Retrieve the value
} else {
return ${${$self->{"any_header"}}[$ndx]}{"value"}
if defined $ndx;
return undef;
}
}
# body: Set/retrieve the body
sub body : method {
local ($_, %_);
my ($self, $body);
($self, $body) = @_;
if (@_ > 1) {
$self->{"body"} = $body;
$self->{"modified"} = 1;
}
return $self->{"body"};
}
# addpart: Add an MIME part
# $mail is a Selima::Mail object
sub addpart : method {
local ($_, %_);
my ($self, $mail);
($self, $mail) = @_;
if (!defined $mail) {
$self->{"parts"} = [];
} else {
push @{$self->{"parts"}}, $mail;
}
$self->{"modified"} = 1;
return $mail;
}
# from_file: Set the mail from a file
sub from_file : method {
local ($_, %_);
my ($self, $file);
($self, $file) = @_;
$self->{"type"} = $MM->checktype_filename($file);
$self->{"body"} = xfread($file);
$self->{"modified"} = 1;
return;
}
# as_attach: Set the mail as an attachment
sub as_attach : method {
local ($_, %_);
my ($self, $file, $filename);
($self, $file, $filename) = @_;
$self->{"type"} = $MM->checktype_filename($file);
$self->{"disposition"} = "attachment";
$self->{"filename"} = defined $filename? $filename: basename($file);
($self->{"filemtime"}, $self->{"filesize"}) = (stat $file)[9,7];
$self->{"body"} = xfread($file);
$self->{"modified"} = 1;
return $self->{"body"};
}
# as_attach_upload: Set the an attachment from $SESSION
# To be done
sub as_attach_upload : method {
local ($_, %_);
my ($self, $sn);
($self, $sn) = @_;
}
# add_attach: Add an attachment
sub add_attach : method {
local ($_, %_);
my ($self, $file, $filename, $attach);
($self, $file, $filename) = @_;
if (!defined $file) {
$self->{"attaches"} = [];
} else {
# Create the attachment
$attach = new Selima::Mail;
$attach->as_attach($file, $filename);
push @{$self->{"attaches"}}, $attach;
}
$self->{"modified"} = 1;
return;
}
# add_attach_upload: Add an attachment from $SESSION
# To be done
sub add_attach_upload : method {
local ($_, %_);
my ($self, $sn);
($self, $sn) = @_;
}
# send_with: Set the mail sending method
sub send_with : method {
local ($_, %_);
my ($self, $send_with);
($self, $send_with) = @_;
# Set the value
if (@_ > 1) {
http_500 "you must specify a sending method"
if !defined $send_with;
if ($send_with eq "sendmail") {
# Check if sendmail exists
http_500 "/usr/sbin/sendmail missing"
if !-e "/usr/sbin/sendmail";
$self->{"send_with"} = $send_with;
} elsif ($send_with eq "smtp") {
# SMTP is always possible
$self->{"send_with"} = $send_with;
} else {
http_500 "unknown sending method: $send_with";
}
}
return $self->{"send_with"};
}
# output: Output the mail message
sub output : method {
local ($_, %_);
my ($self, $is_full_msg, $mail, $body, @headers);
my ($content_transfer_encoding, $boundary, $md5);
($self, $is_full_msg) = @_;
$is_full_msg = 1 if !defined $is_full_msg;
# Output before
return $self->{"output"}
if defined $self->{"output"} && !$self->{"modified"};
# Set the attachment
$mail = $self->_get_attached_mail;
# Check the message
$self->_check if $is_full_msg;
# Set the mail body
# Set the body first, since we need to know Content-Transfer-Encoding
# and MIME multipart boundary first
$body = "";
undef $content_transfer_encoding;
# A single content
if ( !defined $mail->{"type"}
|| $mail->{"type"} !~ /^multipart\//) {
if (defined $mail->{"body"}) {
$body = $mail->{"body"};
# A text message
if ($mail->{"type"} =~ /^text\//) {
# Encode it
# This piece of code shall be refined
$body = encode($mail->{"charset"}, $body)
if $mail->{"type"} eq "text/plain"
&& !is_usascii_printable $body
&& defined $mail->{"charset"};
$body =~ s/\r\n/\n/g;
# Not in US-ASCII, containing long lines, or MD5 is in use
# encode_qp() only work with Unix "\n" text
if ( !is_usascii_printable_text $body
|| $body =~ /[^\r\n]{76}/
|| $mail->{"md5"}) {
$body = encode_qp($body);
$content_transfer_encoding = "quoted-printable";
}
$body =~ s/\n/\r\n/g;
$md5 = md5_base64($body) if $mail->{"md5"};
# A piece of RFC-822 e-mail message
} elsif ($mail->{"type"} =~ /^message\//) {
# Do nothing
# A piece of binary data
} else {
$md5 = md5_base64($body) if $mail->{"md5"};
$body = encode_base64($body, "\r\n");
$content_transfer_encoding = "base64";
}
}
# A multipart MIME content
} else {
my (@parts, $everything);
@parts = map $_->output(0), @{$mail->{"parts"}};
$everything = join "\r\n", @parts;
# Create a boundary
do {
$boundary = "=_";
while (length $boundary < $mail->{"boundary_len"}) {
$_ = int rand 3;
if ($_ == 0) {
$boundary .= chr(ord "0" + int rand 10);
} elsif ($_ == 1) {
$boundary .= chr(ord "A" + int rand 26);
} else {
$boundary .= chr(ord "a" + int rand 26);
}
}
} until $everything !~ /$boundary/;
# Add the boundary and compose the body
$body = join "", map "--$boundary\r\n$_\r\n", @parts;
}
# Ensure a CRLF is in the end
$body =~ s/(\r\n)?$/\r\n/;
@headers = qw();
# RFC 822 headers
# RFC-822 suggests the header order as:
# "Return-Path", "Received", "Date", "From", "Subject", "Sender",
# "To", "cc", etc.
# We do not set the Received: header. It is added before mail is sent.
# Set the Date: header
push @headers, "Date: " . rfc822_time . "\r\n"
if $is_full_msg;
# Set the From: header
if (defined $mail->{"from"}) {
push @headers, "From: " . $mail->_out_addrs($mail->{"from"}) . "\r\n";
} elsif ($is_full_msg) {
@_ = getpwuid $>;
$_ = {
"name" => $_[6],
"email" => $_[0] . "@" . fqdn,
};
push @headers, "From: " . $mail->_out_addr($_) . "\r\n";
}
# Set the Subject: header
if (defined $mail->{"subject"}) {
$_ = $mail->{"subject"};
$_ = b64hdr_encode $_, $mail->{"charset"};
push @headers, "Subject: $_\r\n";
}
# Set the Sender: header
if (defined $mail->{"sender"}) {
my $need;
$need = 0;
# Multiple From:
if (@{$mail->{"from"}} > 1) {
$need = 1;
# Different than the only From:
} else {
my ($from, $sender);
$from = ${$mail->{"from"}}[0];
$sender = ${$mail->{"sender"}};
if ($$from{"email"} ne $$sender{"email"}) {
$need = 1;
} elsif (!defined $$from{"name"} && defined $$sender{"name"}) {
$need = 1;
} elsif (defined $$from{"name"}) {
if (!defined $$sender{"name"}) {
$need = 1;
} elsif ($$from{"name"} ne $$sender{"name"}) {
$need = 1;
}
}
}
push @headers, "Sender: " . $mail->_out_addr($mail->{"sender"}) . "\r\n"
if $need;
}
# Set the To: header
push @headers, "To: " . $mail->_out_addrs($mail->{"to"}) . "\r\n"
if defined $mail->{"to"};
# Set the Cc: header
push @headers, "Cc: " . $mail->_out_addrs($mail->{"cc"}) . "\r\n"
if defined $mail->{"cc"};
# Set the Bcc: header
push @headers, "Bcc: " . $mail->_out_addrs($mail->{"bcc"}) . "\r\n"
if defined $mail->{"bcc"};
# Destination must exist (by RFC 822 4.1)
#push @headers, "Bcc: \r\n"
# if $is_full_msg
# && !defined $mail->{"to"}
# && !defined $mail->{"cc"}
# && !defined $mail->{"bcc"};
# Set the Reply-To: header
push @headers, "Reply-To: " . $mail->_out_addrs($mail->{"reply_to"}) . "\r\n"
if defined $mail->{"reply_to"};
# Set the Message-ID: header
if ($is_full_msg) {
$_ = defined $mail->{"id"}? $mail->{"id"}: $mail->_new_msgid;
push @headers, "Message-ID: <$_>\r\n";
}
# RFC 1521 MIME headers
# Set the MIME-Version: header
push @headers, "MIME-Version: 1.0\r\n"
if $is_full_msg && defined $mail->{"type"};
# Set the Content-Type: header
if (defined $mail->{"type"}) {
my $type;
$type = $mail->{"type"};
$type = encode($mail->{"charset"}, $type)
if is_utf8($type) && defined $mail->{"charset"};
$type .= "; charset=" . $mail->{"charset"}
if $type =~ /^text\// && defined $mail->{"charset"};
# Attachment filename -- deprecated
#if (defined $mail->{"filename"}) {
# $_ = $mail->{"filename"};
# $_ = b64hdr_encode $_, $mail->{"charset"};
# $_ = quote $_ if rfc1521_value_need_quoting $_;
# $type .= ";\r\n name=$_";
#}
if ($type =~ /^multipart\//) {
# Add type and start parameter to multipart/related
if ($type eq "multipart/related") {
$_ = ${$mail->{"parts"}}[0]->type
if !defined($_ = $mail->{"related_type"});
$_ = encode($mail->{"charset"}, $_)
if is_utf8($_) && defined $mail->{"charset"};
$type .= ";\r\n type=\"$_\"";
# "start" parameter is broken in most e-mail client (??? why?)
#$_ = ${$mail->{"parts"}}[0]->id(1)
# if !defined($_ = $mail->{"related_start"});
#$_ = encode($mail->{"charset"}, $_)
# if is_utf8($_) && defined $mail->{"charset"};
#$type .= ";\r\n start=\"$_\"";
}
$type .= ";\r\n boundary=\"$boundary\"";
}
push @headers, "Content-Type: $type\r\n";
}
# Set the Content-ID: header
# Do not generate Content-ID for complete messages. Complete
# messages has Message-ID instead.
push @headers, "Content-ID: <" . $mail->{"id"} . ">\r\n"
if !$is_full_msg && defined $mail->{"id"};
# Set the Content-Transfer-Encoding: header
push @headers, "Content-Transfer-Encoding: $content_transfer_encoding\r\n"
if defined $content_transfer_encoding;
# RFC 1766 Content-Language
push @headers, join ", ", map ln($_, LN_NAME), @{$mail->{"lang"}}
if defined $mail->{"lang"};
# RFC 1806/2183 Content-Disposition
if (defined $mail->{"disposition"}) {
my $disposition;
$disposition = $mail->{"disposition"};
if (defined $mail->{"filename"}) {
# Note: Eudora cannot handle encoded MIME parameter values
$_ = $mail->{"filename"};
$_ = b64hdr_encode $_, $mail->{"charset"};
$_ = quote $_ if rfc1521_value_need_quoting $_;
$disposition .= ";\r\n filename=$_";
}
$disposition .= ";\r\n modification-date=\""
. rfc822_time($mail->{"filemtime"}) . "\""
if defined $mail->{"filemtime"};
$disposition .= ";\r\n size=" . $mail->{"filesize"}
if defined $mail->{"filesize"};
push @headers, "Content-Disposition: $disposition\r\n";
}
# RFC 1864 Content-MD5
push @headers, "Content-MD5: $md5\r\n" if $mail->{"md5"};
# RFC 2369/2919 Mailing Lists headers
# Set the List-ID: header
if (defined $mail->{"list_id"}) {
$_ = ${$mail->{"list_id"}}{"id"};
$_ = encode($mail->{"charset"}, $_)
if is_utf8($_) && defined $mail->{"charset"};
$_ = "<$_>";
if (defined ${$mail->{"list_id"}}{"phrase"}) {
my $phrase;
$phrase = ${$mail->{"list_id"}}{"phrase"};
$phrase = quote $phrase if rfc822_phrase_need_quoting $phrase;
$phrase = b64hdr_encode $phrase, $mail->{"charset"};
$_ = "$phrase $_";
}
push @headers, "List-ID: $_\r\n";
}
# Set the List-Help: header
push @headers, "List-Help: "
. $mail->_out_list_urls($mail->{"list_help"}) . "\r\n"
if defined $mail->{"list_help"};
# Set the List-Subscribe: header
push @headers, "List-Subscribe: "
. $mail->_out_list_urls($mail->{"list_subscribe"}) . "\r\n"
if defined $mail->{"list_subscribe"};
# Set the List-Unsubscribe: header
push @headers, "List-Unsubscribe: "
. $mail->_out_list_urls($mail->{"list_unsubscribe"}) . "\r\n"
if defined $mail->{"list_unsubscribe"};
# Set the List-Post: header
if (defined $mail->{"list_post"}) {
$_ = ref $mail->{"list_post"} eq "ARRAY"?
$mail->_out_list_urls($mail->{"list_post"}): "NO";
push @headers, "List-Post: $_\r\n";
}
# Set the List-Owner: header
push @headers, "List-Owner: "
. $mail->_out_list_urls($mail->{"list_owner"}) . "\r\n"
if defined $mail->{"list_owner"};
# Set the List-Archive: header
push @headers, "List-Archive: "
. $mail->_out_list_urls($mail->{"list_archive"}) . "\r\n"
if defined $mail->{"list_archive"};
# Set the Errors-To: header
push @headers, "Errors-To: " . join(", ", $mail->{"errors_to"}) . "\r\n"
if $is_full_msg && defined $mail->{"errors_to"};
# Set the Precedence: header
push @headers, "Precedence: " . $mail->{"precedence"} . "\r\n"
if $is_full_msg && defined $mail->{"precedence"};
# Set the X-Mailer: header
if ($is_full_msg && defined $mail->{"mailer"}) {
$_ = $mail->{"mailer"};
$_ = encode($mail->{"charset"}, $_)
if is_utf8($_) && defined $mail->{"charset"};
push @headers, "X-Mailer: " . $mail->{"mailer"} . "\r\n";
}
# Compose it
$self->{"output"} = join("", @headers) . "\r\n" . $body;
$self->{"modified"} = 0;
return $self->{"output"};
}
# send: Send the mail message
sub send : method {
local ($_, %_);
my $self;
$self = $_[0];
# Send the mail with Sendmail
if ($self->{"send_with"} eq "sendmail") {
return $self->_send_with_sendmail;
# Send the mail with SMTP
} elsif ($self->{"send_with"} eq "smtp") {
return $self->_send_with_smtp;
}
}
# _new_msgid: Obtain a new message ID.
sub _new_msgid : method {
local ($_, %_);
my ($sec, $msec, $sn, $fqdn);
# Epoch time and minisecond
$sec = int($_ = Time::HiRes::time);
$msec = int(($_ - $sec) * 1000000);
# A random serial number
do {
$sn = int rand 10000;
} until !exists $Mail_MSGIDS{$sn};
$Mail_MSGIDS{$sn} = 1;
# Compose it
return sprintf "%10d.%06d.%05d.%04d.selima@%s",
$sec, $msec, $$, $sn, fqdn;
}
# _get_attached_mail: Get the mail with its attachment
# Actually, this will convert the mail to multipart/mixed
# and add the attachments
sub _get_attached_mail : method {
local ($_, %_);
my ($self, $mail);
$self = $_[0];
# Make a copy of myself
$mail = $self;
# Return myself if there is no attachment
return $mail if @{$mail->{"attaches"}} == 0;
# Not multipart/mixed -- convert to multipart/mixed
unless (defined $mail->{"type"} && $mail->{"type"} eq "multipart/mixed") {
# Pass the content to the body part
my $body;
$body = new Selima::Mail;
$body->{"charset"} = $mail->{"charset"};
$body->{"type"} = $mail->{"type"};
$body->{"id"} = $mail->{"id"};
$body->{"lang"} = $mail->{"lang"};
$body->{"md5"} = $mail->{"md5"};
# Set the body
# The origin is multipart/xxx
if ( defined $mail->{"type"}
&& $mail->{"type"} =~ /^multipart\//) {
$body->{"parts"} = $mail->{"parts"};
} else {
$body->{"body"} = $mail->{"body"};
}
# Reset these values
$mail->{"parts"} = [];
undef $mail->{"body"};
$mail->{"type"} = "multipart/mixed";
undef $mail->{"id"};
undef $mail->{"lang"};
$mail->{"md5"} = 0;
$mail->addpart($body);
}
# Add each attachment
$mail->addpart($_) foreach @{$mail->{"attaches"}};
return $mail;
}
# _set_addr: Set an address
sub _set_addr : method {
local ($_, %_);
my ($self, $addr, $email, $name);
($self, $addr, $email, $name) = @_;
# Reset it
if (!defined $email) {
undef $$addr;
# Set it
} else {
$$addr = {
"name" => $name,
"email" => $email,
};
}
$self->{"modified"} = 1;
}
# _add_addr: Add an address to an address list
sub _add_addr : method {
local ($_, %_);
my ($self, $list, $email, $name);
($self, $list, $email, $name) = @_;
# Reset it
if (!defined $email) {
undef $$list;
# Add it
} else {
$$list = [] if !defined $$list || ref $$list ne "ARRAY";
push @$$list, {
"name" => $name,
"email" => $email,
};
}
$self->{"modified"} = 1;
}
# _ret_addr: Return an address
sub _ret_addr : method {
local ($_, %_);
my ($self, $addr, $name);
($self, $addr) = @_;
return undef if !defined $addr;
return $$addr{"email"} if !defined $$addr{"name"};
$name = $$addr{"name"};
$name = quote $name if rfc822_phrase_need_quoting $name;
return sprintf "%s <%s>", $name, $$addr{"email"};
}
# _ret_addrs: Return a list of addresses
sub _ret_addrs : method {
local ($_, %_);
my ($self, $list);
($self, $list) = @_;
return undef if !defined $list;
return join ", ", map $self->_ret_addr($_), @$list;
}
# _out_addr: Output an address
sub _out_addr : method {
local ($_, %_);
my ($self, $addr, $name, $email);
($self, $addr) = @_;
$email = $$addr{"email"};
$email = encode($self->{"charset"}, $email)
if is_utf8($email) && defined $self->{"charset"};
return $email if !defined $$addr{"name"};
$name = $$addr{"name"};
$name = quote $name if rfc822_phrase_need_quoting $name;
$name = b64hdr_encode $name, $self->{"charset"};
return sprintf "%s <%s>", $name, $email;
}
# _out_addrs: Output a list of addresses
sub _out_addrs : method {
local ($_, %_);
my ($self, $list);
($self, $list) = @_;
return join ",\r\n ", map $self->_out_addr($_), @$list;
}
# _add_list_url:Add a list URL to a list URL list
sub _add_list_url : method {
local ($_, %_);
my ($self, $list, $url);
($self, $list, $url) = @_;
# Reset it
if (!defined $url) {
undef $$list;
# Add it
} else {
$$list = [] if !defined $$list || ref $$list ne "ARRAY";
push @$$list, $url;
}
$self->{"modified"} = 1;
}
# _ret_list_urls: Return a list of list URLs
sub _ret_list_urls : method {
local ($_, %_);
my ($self, $list);
($self, $list) = @_;
return join ", ", map "<$_>", @$list;
}
# _out_list_urls: Output a list of list URLs
sub _out_list_urls : method {
local ($_, %_);
my ($self, $list);
($self, $list) = @_;
@_ = @$list;
if (defined $self->{"charset"}) {
foreach (@_) {
$_ = encode($self->{"charset"}, $_) if is_utf8($_);
}
}
return join ",\r\n ", map "<$_>", @_;
}
# _out_trace: Output the trace information (the Received: header)
# See RFC 2821 4.4 Trace Information
sub _out_trace : method {
local ($_, %_);
my ($self, @trace_info);
my ($from_address_literal, $from_tcpinfo_domain, $from_tcp_info);
my ($by_address_literal, $by_domain, $by_tcpinfo_domain, $by_tcp_info);
$self = $_[0];
# The trace information
@trace_info = qw();
# From-domain
# mod_perl: use Apache->request->connection->remote_ip
$from_address_literal = $IS_MODPERL? ($IS_MP2?
Apache2::RequestUtil->request->connection->remote_ip:
Apache->request->connection->remote_ip):
$ENV{"REMOTE_ADDR"};
$from_tcpinfo_domain = gethostbyaddr inet_aton($from_address_literal), AF_INET;
if (!defined $from_tcpinfo_domain) {
$from_tcp_info = sprintf "[%s]", $from_address_literal;
} else {
$from_tcp_info = sprintf "%s [%s]",
$from_tcpinfo_domain, $from_address_literal;
}
push @trace_info, "from webclient ($from_tcp_info)";
# The invoking user, as a from comment
if (!defined get_login_sn) {
$_ = "invoked by anonymous web user"
. " country " . country_lookup;
} else {
$_ = "invoked by web user " . get_login_id
. " S/N " . get_login_sn
. " country " . country_lookup;
}
push @trace_info, "($_)";
# By-domain
# Apache implementation
$by_address_literal = is_apache? $ENV{"SERVER_ADDR"}:
# Microsoft IIS implementation
is_iis? $ENV{"LOCAL_ADDR"}:
# Else, do DNS query
inet_ntoa(scalar gethostbyname $ENV{"SERVER_NAME"});
if (exists $ENV{"HTTP_HOST"}) {
$by_domain = $ENV{"HTTP_HOST"};
$by_domain =~ s/:\d+$//;
} else {
$by_domain = $by_address_literal;
}
$by_tcpinfo_domain = gethostbyaddr inet_aton($by_address_literal), AF_INET;
if (!defined $by_tcpinfo_domain) {
$by_tcp_info = sprintf "[%s]", $by_address_literal;
} else {
$by_tcp_info = sprintf "%s [%s]",
$by_tcpinfo_domain, $by_address_literal;
}
push @trace_info, sprintf("by %s (%s)", $by_domain, $by_tcp_info);
# VIA Link and WITH Protocol
push @trace_info, "via TCP with HTTP";
# For recipients
push @trace_info, "for " . join(" ", map "<$_>", @_)
if (@_ = $self->_get_rcpts) > 0;
return sprintf("Received: %s ;\r\n\t%s\r\n",
join("\r\n\t", @trace_info), rfc822_time);
}
# _check: Check if it is ready to be output
# Refer to RFC 822
sub _check : method {
local ($_, %_);
my $self;
$self = $_[0];
# When no valid From: exists, default to the current user of the running process
# Check if a valid Sender: exists with multiple From:
http_500 "Multiple From: without a valid Sender: (RFC-822)"
if defined $self->{"from"}
&& @{$self->{"from"}} > 1
&& !defined $self->{"sender"};
# Check if valid contents exist
http_500 "MIME Multipart without any valid part"
if defined $self->{"type"}
&& $self->{"type"} =~ /^multipart\//
&& $self->{"parts"} == 0;
}
# _get_sender: Collect the sender
sub _get_sender : method {
local ($_, %_);
my ($self, $fallback);
($self, $fallback) = @_;
$fallback = 0 if !defined $fallback;
# MAIL FROM: specified
return $self->{"mail_from"}
if defined $self->{"mail_from"};
# Sender specified
return ${$self->{"sender"}}{"email"}
if defined $self->{"sender"};
# Obtain the sender from From:
return ${${$self->{"from"}}[0]}{"email"}
if defined $self->{"from"};
# Nothing left
return undef if !$fallback;
# Use the current user
return (getpwuid $>)[0] . "@" . fqdn;
}
# _get_rcpts: Collect the recipients list
sub _get_rcpts : method {
local ($_, %_);
my ($self, @rcpts);
$self = $_[0];
# Recipients specified
return @{$self->{"rcpt_to"}}
if defined $self->{"rcpt_to"};
# Obtain the recipients from To:, Cc: and Bcc:
@rcpts = qw();
%_ = qw();
@_ = qw();
push @_, @{$self->{"to"}} if defined $self->{"to"};
push @_, @{$self->{"cc"}} if defined $self->{"cc"};
push @_, @{$self->{"bcc"}} if defined $self->{"bcc"};
foreach (@_) {
next if exists $_{$$_{"email"}};
push @rcpts, $$_{"email"};
$_{$$_{"email"}} = 1;
}
return @rcpts;
}
# _send_with_sendmail: Send the mail with Sendmail
sub _send_with_sendmail : method {
local ($_, %_);
my ($self, $sender, @rcpts, $SENDMAIL);
$self = $_[0];
# Get the sender
$sender = $self->_get_sender;
# Collect the Recipients list
@rcpts = $self->_get_rcpts;
# No recipients found
http_500 "No recipients found\n"
if @rcpts == 0;
# Obtain the mail content
$_ = $self->_out_trace . $self->output;
# Sendmail must escape "."s on a line. See RFC 821 SMTP 4.5.2
s/^(\.+\r)/.$1/gm;
# Send with Sendmail
@_ = qw(/usr/sbin/sendmail -odb);
push @_, ("-f", $sender) if defined $sender;
push @_, @rcpts;
open $SENDMAIL, "|-", @_ or http_500 $_[0] . ": $!";
print $SENDMAIL $_ or http_500 $_[0] . ": $!";
close $SENDMAIL or http_500 $_[0] . ": $!";
return;
}
# _send_with_smtp: Send the mail with SMTP
sub _send_with_smtp : method {
local ($_, %_);
my ($self, $sender, @rcpts, $SMTP);
$self = $_[0];
# Get the sender
$sender = $self->_get_sender(1);
# Collect the Recipients list
@rcpts = $self->_get_rcpts;
# No recipients found
http_500 "No recipients found\n"
if @rcpts == 0;
# Obtain the mail content
$_ = $self->_out_trace . $self->output;
# SMTP must escape lines with leading "."s. See RFC 821 SMTP 4.5.2
# Net::SMTP does this for me.
# Send with SMTP
$SMTP = new Net::SMTP(SMTP_HOST) or http_500 "$!";
$SMTP->mail($sender, Size=>length $_) or http_500 "MAIL FROM:<$sender>: " . $SMTP->code . " " . $SMTP->message;
foreach (@rcpts) {
$SMTP->to($_) or http_500 "RCPT TO:<$_>:: " . $SMTP->code . " " . $SMTP->message;
}
$SMTP->data or http_500 "DATA: " . $SMTP->code . " " . $SMTP->message;
$SMTP->datasend($_) or http_500 "DATA: " . $SMTP->code . " " . $SMTP->message;
$SMTP->dataend($_) or http_500 "DATA: " . $SMTP->code . " " . $SMTP->message;
$SMTP->quit or http_500 "QUIT: " . $SMTP->code . " " . $SMTP->message;
return;
}
#######################
# Below are Functions #
#######################
# quote: Quote a text string
# Quoting is REQUIRED for CR and \ and ", by RFC 822 3.4.1 # " gettext
sub quote($) {
local ($_, %_);
$_ = $_[0];
s/\\/\\\\/g;
s/"/\\"/g;
s/\r/\\\r/g;
return "\"$_\"";
}
# rfc822_phrase_need_quoting: Whether a phrase need to be quoted by RFC-822
sub rfc822_phrase_need_quoting($) {
return $_[0] =~ /[()<>@,;:\\"\.\[\]\x01-\x1F\x7F]/; # " gettext
}
# rfc1521_value_need_quoting: Whether a value need to be quoted by RFC-1521
sub rfc1521_value_need_quoting($) {
return $_[0] =~ /[()<>@,;:\\"\/\[\]\?= \x01-\x1F\x7F]/; # " gettext
}
# b64hdr_encode: Encode a piece of header text with Base-64
# Refer to RFC-1522 4.1
sub b64hdr_encode($$) {
local ($_, %_);
my ($text, $charset);
($text, $charset) = @_;
# US-ASCII printable -- no need to encode it
return $text if is_usascii_printable $text;
# No desired character set available
return $text if !defined $charset;
# Successfully encoded
return "=?$charset?B?" . encode_base64($_, "") . "?="
if eval { $_ = encode($charset, $text, FB_CROAK); 1; };
# Else -- send in UTF-8
$_ = encode("UTF-8", $text, FB_CROAK);
return "=?UTF-8?B?" . encode_base64($_, "") . "?=";
}
# rfc822_time: Output the time as RFC-822
# Day should be displayed as one digit, without the padding space.
sub rfc822_time(;$) {
local ($_, %_);
$_ = $_[0];
$_ = time if !defined $_;
return sprintf time2str("%a, %%d %b %Y %X %z", $_), (localtime $_)[3];
}
return 1;