# ==================================================================
# Gossamer Mail - enhanced email management system
#
# Website : http://gossamer-threads.com/
# Support : http://gossamer-threads.com/scripts/support/
# Revision : $Id: Compose.pm,v 1.127 2004/06/29 22:05:57 brewt Exp $
#
# Copyright (c) 2003 Gossamer Threads Inc. All Rights Reserved.
# Redistribution in part or in whole strictly prohibited. Please
# see LICENSE file for full details.
# ==================================================================
#
# Description: Class to handle mail composition and sending for
# Gossamer Mail.
#
package GMail::Compose;
# ==================================================================
# Pragmas
use strict;
use vars qw/@ISA $DEBUG/;
# Internal modules
use GMail qw{:objects};
use GMail::Config;
use GMail::Email::Attach;
# GT modules
use GT::Mail;
use GT::AutoLoader;
# Global vars
$DEBUG = 0;
@ISA = qw(GMail);
$COMPILE{test_convert_to_text} = __LINE__ . <<'END_OF_SUB';
sub test_convert_to_text {
# ------------------------------------------------------------------
# Takes the text in compose_body and checks it for html tags. If
# it contains html tags converts it to text. If it does not just
# returns it.
#
my ($self) = @_;
my $body = $IN->param('convert_body') || 'compose_body';
my $text = $IN->param($body) || '';
($text =~ /<\/?(?:br|p|html)>/i) or return {};
$self->html_to_text(\$text);
$text =~ s/</g;
$text =~ s/>/>/g;
$text =~ s/"/"/g;
$IN->delete('compose_is_html');
return { $body => \$text };
}
END_OF_SUB
$COMPILE{test_convert_to_html} = __LINE__ . <<'END_OF_SUB';
sub test_convert_to_html {
# ------------------------------------------------------------------
# Checks compose body for html tags, if it contains html this method
# will just return it. If it does not this method will convert the
# text to html. This means converting \n to
amoung other things.
#
my ($self) = @_;
my $body = $IN->param('convert_body') || 'compose_body';
my $text = $IN->param($body) || '';
($text =~ /<\/?(?:br|p|html)>/i) and return {};
$IN->html_escape(\$text);
$self->text_to_html(\$text);
return { $body => $text };
}
END_OF_SUB
sub info {
# ------------------------------------------------------------------
# This method should be called from the page that displays the
# compose form. It returns all the tags defaulted to cgi input or
# empty.
#
my ($self) = @_;
my $tags = $self->{tags};
my $attach = $IN->param('attach_id') || GMail::Email::Attach->create;
$IN->param(attach_id => $attach);
return {
compose_id => $tags->{compose_id} || $IN->param('compose_id') || '',
compose_priority => $tags->{compose_priority} || $IN->param('compose_priority') || '',
compose_to => $tags->{compose_to} || $IN->param('compose_to') || '',
compose_cc => $tags->{compose_cc} || $IN->param('compose_cc') || '',
compose_bcc => $tags->{compose_bcc} || $IN->param('compose_bcc') || '',
compose_subject => $tags->{compose_subject} || $IN->param('compose_subject') || '',
compose_body => $tags->{compose_body} || $IN->param('compose_body') || '',
attach_id => $attach,
};
}
$COMPILE{forward} = __LINE__ . <<'END_OF_SUB';
sub forward {
# ------------------------------------------------------------------
# Should be called when forwarding a message. Formats the message
# for forwarding. The message used is identified by msgtrack_id form
# input.
#
my ($self, $id) = @_;
$id ||= $IN->param('msgtrack_id');
# Must have a valid message id
$id or return $self->error('SENDERR_NOID', 'WARN');
$id =~ /^\d+$/ or return $self->error('SENDERR_ID', 'WARN', $id);
# Delete any old attachments
$PLG->dispatch('GMail::Email::Attach::clean', sub { GMail::Email::Attach->clean(@_) });
my $fmt = $USER->{opts}->{compose}->{format} || 'text';
# Get the attachment list and the body
require GMail::Messages;
my ($attach, $msg) = $PLG->dispatch('GMail::Messages::get_attachlist', sub { GMail::Messages->get_attachlist(@_) }, $id, $fmt, 1);
# Format the body and subject
my $format = $fmt eq 'text' ? $USER->{opts}->{compose}->{forward_text} : $USER->{opts}->{compose}->{forward_html};
my ($message, $subject) = $PLG->dispatch('GMail::Compose::format_body', sub { $self->format_body(@_) }, $format, $USER->{opts}->{compose}->{forward_subject} || 'Fw: <%formatted_subject%>', $msg);
unless ($IN->param('profs_id')) {
if (my $pop_account = $msg->{msgtrack_account}) {
if (my $prof_id = $DB->table('pop_accounts')->select(
pop_accounts_prof => { pop_accounts_id => $pop_account, pop_accounts_userid => $USER->{userid}
})->fetchrow) {
if ($DB->table('profs')->count({ profs_id => $prof_id, profs_userid => $USER->{userid} })) {
$IN->param(profs_id => $prof_id);
}
}
}
}
# Copy messages attachments to the users upload directory
my $attach_id = $IN->param('attach_id') || GMail::Email::Attach->create;
$IN->param(attach_id => $attach_id);
my $attachdir = "$USER->{dir}/upload/$attach_id";
my $msgdir = "$CFG->{location}->{path}->{data}/msgs";
# Forward as attachment
if ($USER->{opts}->{compose}->{forward_as_attachment} or $IN->param('as_attach')) {
$message = ''; # Empty message body.
my $write = "$attachdir/";
$write .= ($IN->escape($msg->{msgs_subject}) || 'message') . '.eml';
my $checksum = $msg->{msgs_checksum};
my $p = substr($msg->{msgdata_filename}, 0, 1) . '/' . substr($msg->{msgdata_filename}, 1, 1) . '/' . substr($msg->{msgdata_filename}, 2, 1);
if (open READ, "$CFG->{location}->{path}->{data}/msgs/$p/$checksum") {
$write = GMail->clobber($write);
binmode READ; binmode $write;
print $write $_ while ();
close READ;
close $write;
}
else {
return $self->error("ATTACHERR_EXIST", "WARN");
}
}
else {
# If format is text we need to move all inline attachments
# to normal attachments
$PLG->dispatch('GMail::Email::Attach::inlines_to_attach', sub { GMail::Email::Attach->inlines_to_attach(@_) }, $attach_id);
my $i = 0;
my $url = $CFG->{location}->{url}->{user} . '/' . $IN->url(query_string => 0);
my $t = $PLG->dispatch('GMail::template_set', sub { $self->template_set });
my $sid = $IN->param('sid') || '';
for (@{$attach}) {
my $write;
if (
$_->{msgdata_cid} and
($_->{msgdata_type} =~ /image/) and
($fmt eq 'html') and
($msg->{msgdata_type} eq 'text/html') and
$_->{msgdata_suggestedname}
) {
my $cid = $_->{msgdata_cid};
my @p = unpack('C*', $_->{msgdata_suggestedname});
my $name = pack('A3' x scalar(@p), @p);
$name =~ s/ /_/g;
my $file = "inline-". $i++ . $$ . time . '-' . $name;
$write = "$attachdir/$file";
$message =~ s[("|')cid:\Q$cid\E\1]["$url?sid=$sid;t=$t;do=email-attach-print_inline;attach_name=$file;attach_id=$attach_id"]ig;
}
else {
$write = "$attachdir/" . $IN->escape($_->{msgdata_suggestedname} || $i++);
}
my $p = substr($_->{msgdata_filename}, 0, 1) . '/' . substr($_->{msgdata_filename}, 1, 1) . '/' . substr($_->{msgdata_filename}, 2, 1);
my $read = "$msgdir/$p/$_->{msgdata_filename}";
local *READ;
open READ, "< $read" or return $self->error(READOPEN => FATAL => $read, "$!");
$write = GMail->clobber($write);
binmode READ; binmode $write;
print $write $_ while ;
}
}
# Print the message compose window with everything filled out
return {
compose_id => $id,
compose_priority => $IN->param('priority') || '',
compose_to => $IN->param('compose_to') || '',
compose_cc => $IN->param('compose_cc') || '',
compose_bcc => $IN->param('compose_bcc') || '',
compose_subject => \$subject,
compose_is_html => ($fmt eq 'html'),
compose_body => \$message,
};
}
END_OF_SUB
sub reply_all {
my ($self, $id) = @_;
return $self->reply($id, 1);
}
$COMPILE{reply} = __LINE__ . <<'END_OF_SUB';
sub reply {
# ------------------------------------------------------------------
# Should be called when replying to a message. Formats the message
# being replied which is pulled using form input msgtrack_id.
#
my ($self, $id, $all) = @_;
$id ||= $IN->param('msgtrack_id');
# Must have a valid message id
$id or return $self->error('SENDERR_NOMID', 'WARN');
$id =~ /^\d+$/ or return $self->error('SENDERR_ID', 'WARN', $id);
# Delete any attachments
$PLG->dispatch('GMail::Email::Attach::clean', sub { GMail::Email::Attach->clean(@_) });
my $fmt = $USER->{opts}->{compose}->{format} || 'text';
# Get the message and the attach list
require GMail::Messages;
my ($attach, $msg) = $PLG->dispatch('GMail::Messages::get_attachlist', sub { GMail::Messages->get_attachlist(@_) }, $id, $fmt);
# Format the body and subject
my $format = $fmt eq 'text' ? $USER->{opts}->{compose}->{reply_text} : $USER->{opts}->{compose}->{reply_html};
my ($message, $subject) = $PLG->dispatch('GMail::Compose::format_body', sub { $self->format_body(@_) }, $format, $USER->{opts}->{compose}->{reply_subject} || 'Re: <%formatted_subject%>', $msg);
unless ($IN->param('profs_id')) {
if (my $pop_account = $msg->{msgtrack_account}) {
if (my $prof_id = $DB->table('pop_accounts')->select(
pop_accounts_prof => { pop_accounts_id => $pop_account, pop_accounts_userid => $USER->{userid}
})->fetchrow) {
if ($DB->table('profs')->count({ profs_id => $prof_id, profs_userid => $USER->{userid} })) {
$IN->param(profs_id => $prof_id);
}
}
}
}
# Copy message attachments to the user's upload directory
my $attach_id = $IN->param('attach_id') || $PLG->dispatch('GMail::Email::Attach::create', sub { GMail::Email::Attach->create(@_) });
$IN->param(attach_id => $attach_id);
my $attachdir = "$USER->{dir}/upload/$attach_id";
my $msgdir = "$CFG->{location}->{path}->{data}/msgs";
my $i = 0;
my $url = $CFG->{location}->{url}->{user} . '/' . $IN->url(query_string => 0);
my $t = $PLG->dispatch('GMail::template_set', sub { $self->template_set });
my $sid = $IN->param('sid') || '';
for (@{$attach}) {
my $write;
if (
$_->{msgdata_cid} and
($_->{msgdata_type} =~ /image/) and
($fmt eq 'html') and
($msg->{msgdata_type} eq 'text/html') and
$_->{msgdata_suggestedname}
)
{
my $cid = $_->{msgdata_cid};
my @p = unpack('C*', $_->{msgdata_suggestedname});
my $name = pack('A3' x scalar(@p), @p);
$name =~ y/ /_/;
my $file = "inline-". $i++ . $$ . time . '-' . $name;
$write = "$attachdir/$file";
$message =~ s[("|')cid:\Q$cid\E\1]["$url?t=$t;sid=$sid;do=email-attach-print_inline;attach_name=$file;attach_id=$attach_id"]ig;
}
else {
next;
}
my $p = substr($_->{msgdata_filename}, 0, 1) . '/' . substr($_->{msgdata_filename}, 1, 1) . '/' . substr($_->{msgdata_filename}, 2, 1);
my $read = "$msgdir/$p/$_->{msgdata_filename}";
local *READ;
open READ, "< $read" or return $self->error(READOPEN => FATAL => $read, "$!");
$write = GMail->clobber($write);
binmode READ; binmode $write;
print $write $_ while ;
}
# Make sure we have a to
my ($to, $cc) = ('', '');
$msg->{msgs_reply_to} ||= $msg->{msgs_sent_from};
# Reply all. We get all the address - it goes "To" the person it came from,
# and anyone else in the list goes into the "Cc" list. However, we strip out
# the user's e-mail address. In the case of an e-mail being received from a
# POP account, we strip out the e-mail set in the profile associated with that
# account as well.
if ($all) {
my %cache;
my @filter = ($USER->{email}, @{$USER->{opts}->{compose}->{reply_filter} || []});
my $profs_email = $DB->table('profs')->select(profs_email => { profs_id => $IN->param('profs_id'), profs_userid => $USER->{userid} })->fetchrow();
push @filter, $profs_email if $profs_email;
require GT::Mail::Parts;
# Email came from this person, so the reply should be sent To this person
FROM: foreach my $from (
GT::Mail::Parts->split_line('\s*[,;]+\s*', $msg->{msgs_reply_to})
) {
next unless $from and $from =~ /[\w.-]+\@[\w.-]+\.[\w.-]+/;
$from =~ s/\([^)]*\)//g;
next if exists $cache{$from};
foreach my $email (@filter) {
next FROM if ($from =~ m/^\Q$email\E$/i or $from =~ m/<\Q$email\E>/i);
}
$to .= ', ' if $to;
$to .= $from;
$cache{$from} = 1;
}
# Email was delivered To/Cc to these people, so a reply should be Cc'ed to them
FROM: foreach my $from (
GT::Mail::Parts->split_line('\s*[,;]+\s*', $msg->{msgs_sent_to}),
GT::Mail::Parts->split_line('\s*[,;]+\s*', $msg->{msgs_sent_cc})
) {
next unless $from and $from =~ /[\w.-]+\@[\w.-]+\.[\w.-]+/;
$from =~ s/\([^)]*\)//g;
next if exists $cache{$from};
foreach my $email (@filter) {
next FROM if ($from =~ m/^\Q$email\E$/i or $from =~ m/<\Q$email\E>/i);
}
$cc .= ', ' if $cc;
$cc .= $from;
$cache{$from} = 1;
}
}
else {
($to = $msg->{msgs_reply_to}) =~ s/\([^)]*\)//g;
}
# Print the message compose window with everything filled out
return {
compose_id => $id,
compose_priority => $IN->param('priority') || '',
compose_to => $to,
compose_cc => $cc,
compose_bcc => $IN->param('compose_bcc') || '',
compose_subject => \$subject,
compose_is_html => ($fmt eq 'html'),
compose_body => \$message,
};
}
END_OF_SUB
$COMPILE{draft} = __LINE__ . <<'END_OF_SUB';
sub draft {
# -----------------------------------------------------------------------------
# This is called when opening up a draft message to continue writing it.
#
my ($self, $id) = @_;
$id ||= $IN->param('msgtrack_id');
# Must have a valid message id
$id or return $self->error('SENDERR_NOMID', 'WARN');
$id =~ /^\d+$/ or return $self->error('SENDERR_ID', 'WARN', $id);
# Delete any attachments
$PLG->dispatch('GMail::Email::Attach::clean', sub { GMail::Email::Attach->clean(@_) });
# Get the message and the attach list
require GMail::Messages;
my ($attach, $msg) = $PLG->dispatch('GMail::Messages::get_attachlist', sub { GMail::Messages->get_attachlist(@_) }, $id, 'html', 1);
my $fmt = $msg->{msgdata_type} eq 'text/html' ? 'html' : 'text';
my $header = $PLG->dispatch('GMail::Messages::raw_header', sub { GMail::Messages->raw_header(@_) }, $id);
my ($profs_id) = $$header =~ /^X-GMail-Profile:\s+(\d+)\s*$/im;
if ($profs_id and $DB->table('profs')->count({ profs_id => $profs_id, profs_userid => $USER->{userid} })) {
$IN->param(profs_id => $profs_id);
}
my ($sig_id) = $$header =~ /^X-GMail-Signature:\s+(\d+)\s*$/im;
if ($sig_id and $DB->table('sigs')->count({ sigs_sigid => $sig_id, sigs_userid => $USER->{userid} })) {
$IN->param(sigs_sigid => $sig_id);
}
my ($x_priority) = $$header =~ /^X-Priority:\s+(.*?)\s*$/im;
$IN->param(compose_priority => $x_priority) if $x_priority;
# Determine the type of draft (i.e. Reply, Forward, or original message)
my $draft_type;
if ($msg->{msgtrack_status} eq 'Draft Reply') {
$draft_type = 'reply';
}
elsif ($msg->{msgtrack_status} eq 'Draft Forward') {
$draft_type = 'forward';
}
else {
$draft_type = 'draft';
}
# Handle resuming an HTML draft
my $format_fmt = 'text';
$format_fmt = 'html' if $msg->{msgtrack_draft_html};
# Format the body and subject
my ($message, $subject, $top) = $PLG->dispatch('GMail::Compose::format_body', sub { $self->format_body(@_) }, '<%message%>', '<%subject%>', $msg, $format_fmt);
# Copy message attachments to the user's upload directory
my $attach_id = $IN->param('attach_id') || $PLG->dispatch('GMail::Email::Attach::create', sub { GMail::Email::Attach->create(@_) });
$IN->param(attach_id => $attach_id);
my $attachdir = "$USER->{dir}/upload/$attach_id";
my $msgdir = "$CFG->{location}->{path}->{data}/msgs";
my $i = 0;
my $url = $CFG->{location}->{url}->{user} . '/' . $IN->url(query_string => 0);
my $t = $PLG->dispatch('GMail::template_set', sub { $self->template_set });
my $sid = $IN->param('sid') || '';
for (@{$attach}) {
# In the case where the user has composed a html email that includes an
# attachment, the text part will be added as an attachment. Don't do this.
next if $fmt eq 'html' and $_->{msgdata_suggestedname} eq '1.txt';
my $write;
if ($_->{msgdata_cid} and
$_->{msgdata_type} =~ /image/ and
$fmt eq 'html' and
$msg->{msgdata_type} eq 'text/html' and
$_->{msgdata_suggestedname}) {
my $cid = $_->{msgdata_cid};
my @p = unpack('C*', $_->{msgdata_suggestedname});
my $name = pack('A3' x scalar(@p), @p);
$name =~ y/ /_/;
my $file = "inline-". $i++ . $$ . time . '-' . $name;
$write = "$attachdir/$file";
$message =~ s[("|')cid:\Q$cid\E\1]["$url?t=$t;sid=$sid;do=email-attach-print_inline;attach_name=$file;attach_id=$attach_id"]ig;
}
else {
$write = "$attachdir/" . $IN->escape($_->{msgdata_suggestedname} || $i++);
}
my $p = substr($_->{msgdata_filename}, 0, 1) . '/' . substr($_->{msgdata_filename}, 1, 1) . '/' . substr($_->{msgdata_filename}, 2, 1);
my $read = "$msgdir/$p/$_->{msgdata_filename}";
local *READ;
open READ, "< $read" or return $self->error(READOPEN => FATAL => $read, "$!");
$write = GMail->clobber($write);
binmode READ; binmode $write;
print $write $_ while ;
}
# Print the message compose window with everything filled out
return {
compose_id => $id,
compose_priority => scalar $IN->param('priority') || '',
compose_to => scalar $IN->param('compose_to') || $msg->{msgs_sent_to},
compose_cc => scalar $IN->param('compose_cc') || $msg->{msgs_sent_cc},
compose_bcc => scalar $IN->param('compose_bcc') || $msg->{msgs_sent_bcc},
compose_subject => \$subject,
compose_is_html => ($fmt eq 'html'),
compose_body => \$message,
draft_type => $draft_type
};
}
END_OF_SUB
sub new_send {
# ------------------------------------------------------------------
# Simple function that basically wraps to _send(). Should be called
# for non-reply/forward.
#
my ($self, $id) = @_;
my $cgi = $IN->get_hash;
$PLG->dispatch('GMail::Compose::send', sub { $self->_send(@_) }, $cgi) or return $self->error($GMail::Compose::error, 'WARN');
return { message => 'SEND_SENT' }
}
$COMPILE{reply_send} = __LINE__ . <<'END_OF_SUB';
sub reply_send {
# ------------------------------------------------------------------
# Takes the message from form input, formats it for inline,
# attachments etc. and calls _send() to send it.
#
my ($self, $id) = @_;
my $cgi = $IN->get_hash;
$id ||= $cgi->{compose_id};
$id or return $self->error('SENDERR_NOMID', 'WARN');
$id =~ /^\d+$/ or return $self->error('SENDERR_ID', 'WARN', $id);
my $reply_id = $id;
if ($cgi->{compose_state} and $cgi->{compose_state} eq 'draft') {
$reply_id = $DB->table('msgtrack')->select(msgtrack_draft_ref => { msgtrack_id => $id })->fetchrow;
}
# Get the message id for reference to other mail clients
my $sth = $DB->table('msgs', 'msgtrack')->select(
msgs_message_id => {
msgtrack_id => $reply_id,
msgtrack_userid => $USER->{userid}
}
);
my $ref = $sth->fetchrow;
$cgi->{references} = $ref if $ref and $ref ne '<>';
# Send the email
$PLG->dispatch('GMail::Compose::send', sub { $self->_send(@_) }, $cgi, 'reply') or return $self->error($GMail::Compose::error, 'WARN');
return { message => 'SEND_REPLY' };
}
END_OF_SUB
sub forward_send {
# ------------------------------------------------------------------
# Takes the message/attach_id from form input and formats it for
# forwarding. Calls _send() to do the actual message creation and
# sending.
my ($self, $id) = @_;
my $cgi = $IN->get_hash;
$id ||= $cgi->{compose_id};
$id or return $self->error('SENDERR_NOMID', 'WARN');
$id =~ /^\d+$/ or return $self->error('SENDERR_ID', 'WARN', $id);
$PLG->dispatch('GMail::Compose::send', sub { $self->_send(@_) }, $cgi, 'forward') or return $self->error($GMail::Compose::error, 'WARN');
return { message => 'SEND_FORWARD' };
}
sub save_draft {
# -----------------------------------------------------------------------------
# Takes the message from form input, formats it for inline, attachments etc.,
# and calls _save to save it. This is used for saving a draft whether the
# message is a reply, forward, or new message.
#
my $self = shift;
my $cgi = $IN->get_hash;
$PLG->dispatch('GMail::Compose::send', sub { $self->_send(@_) }, $cgi, 'draft');
return { message => 'DRAFT_SAVED' };
}
$COMPILE{_send} = __LINE__ . <<'END_OF_SUB';
sub _send {
# -----------------------------------------------------------------------------
# This is the real workhorse of this module. Takes a fields list which in most
# cases is form input slightly modified for forward or replies. Creates the
# email and sends it using GT::Mail.
#
# Optionally takes a second variable; if 'draft', the mail is inserted into the
# drafts folder but NOT sent to the Sent folder. 'reply' and 'forward' sets
# the status of the message being replied to or forwarded appropriately.
#
my ($self, $fields, $sending) = @_;
$sending ||= '';
my $draft = $sending eq 'draft';
my $profs_id = $IN->param('profs_id');
my $profile;
if ($profs_id) {
$profile = $DB->table('profs')->select({ profs_id => $profs_id, profs_userid => $USER->{userid} })->fetchrow_hashref
or $profs_id = undef;
}
if (!$profs_id) {
require GMail::EmailProfiles;
my $info = GMail::EmailProfiles->info();
$profs_id = $info->{profs_default_id};
for (@{$info->{profs_list}}) {
if ($_->{profs_id} == $profs_id) {
$profile = $_;
last;
}
}
$profile ||= $info->{profs_list}->[0];
}
my ($size, %send) = (0);
exists($fields->{compose_to}) or $draft or return $self->error("SENDERR_NORECIP", "WARN");
exists($fields->{compose_subject}) or $draft or return $self->error("SENDERR_NOSUB", "WARN");
if (my $name = $profile->{profs_name}) {
$name =~ s/([\\"])/\\$1/g;
$send{from} = qq|"$name" <$profile->{profs_email}>|;
}
else {
$send{from} = $profile->{profs_email};
}
$send{subject} = $fields->{compose_subject};
$send{'X-Priority'} = $fields->{compose_priority} if $fields->{compose_priority};
# Add an X-GMail-Account header, to be able to track a GMail e-mail back to its account
$send{'X-GMail-Account'} = $USER->{email} unless $send{from} eq $USER->{email};
$send{encoding} = '-guess';
$send{msg} = $fields->{compose_body};
if ($CFG->{email}->{x_originating_ip} and $ENV{REMOTE_ADDR}) {
$send{'X-Originating-IP'} = $ENV{REMOTE_ADDR};
}
# See if there are any address nicks in the to fields
require GMail::AddressBook;
# Keep track of how many recipients there are.
my $total_to = 0;
for my $to (qw{to cc bcc}) {
$fields->{"compose_$to"} or next;
my @t = GT::Mail::Parts->split_line('\s*[,;]+\s*', $fields->{"compose_$to"});
@t or next;
for (@t) {
next unless $_ and /\S/;
s/^\s+//; s/\s+$//;
$total_to++;
if ($draft) {
$send{$to} .= "$_, ";
}
else {
# See if we can extract an e-mail address out of it:
require GT::Mail::Send;
if (GT::Mail::Send->extract_email($_)) {
$send{$to} .= "$_, ";
}
elsif (my $nick = GMail::AddressBook->nick_lookup("$_")) {
$send{$to} .= "$nick, ";
}
else {
return $self->error('SENDERR_BADADDR', 'WARN', $IN->html_escape("$_"));
}
}
}
chop $send{$to}; chop $send{$to};
}
unless ($draft) {
$send{to} or return $self->error("SENDERR_NORECIP", "WARN");
if ($CFG->{limits}{max_addressees} and $CFG->{limits}{max_addressees} > 0) {
return $self->error("SENDERR_TOOMANY_TO", "WARN") if $total_to > $CFG->{limits}{max_addressees};
}
}
unless ($draft) {
require GMail::Email;
my %headers = $PLG->dispatch(
'GMail::Email::prepare_message',
sub { GMail::Email->prepare_message(@_) },
\$send{msg},
signature => $fields->{sigs_sigid},
html => $fields->{compose_is_html},
footer => 1,
headers => 1
);
@send{keys %headers} = values %headers;
}
# If we are replying set the message id
$send{References} = $send{'In-Reply-To'} = $fields->{references} if $fields->{references};
# Find out what we are sending with
if ($CFG->{email}->{out} eq 'smtp') {
my ($host, $port) = $CFG->{email}->{smtp} =~ /^([\w.-]+)(?::(\d+))?$/;
$send{smtp} = $host;
$send{smtp_port} = $port;
$send{smtp_ssl} = $CFG->{email}->{smtp_ssl};
}
else {
$send{sendmail} = $CFG->{email}->{sendmail};
}
# Build the email
$send{debug} = $CFG->{debug};
# Build up a list of attachments and inline images
my $attach_id = $fields->{attach_id};
$attach_id =~ /^(\S+)$/ and $attach_id = $1;
my $attach_dir = "$USER->{dir}/upload/" . $attach_id;
my @attachments;
my @inline_images;
for ($IN->param('compose_attachments')) {
my $tmp = $IN->escape($_);
-e "$attach_dir/$tmp" or return $self->error("ATTACHERR_EXIST", "WARN");
push @attachments, {
body_path => "$attach_dir/$tmp",
encoding => '-guess',
filename => $_
};
}
if (-d $attach_dir and $fields->{compose_is_html}) {
my $upl = \do { local *FH; *FH };
opendir $upl, $attach_dir or $self->error("READOPEN", "FATAL", $attach_dir, $!);
my @files = grep(/^inline-/, readdir($upl));
closedir $upl;
for (@files) {
my ($cid, $name) = /^inline-([^\-]+?)-([\d_]+)$/;
my @p = $name =~ /(\d\d\d?)/g;
(my $unenc = pack('C' x scalar(@p), @p)) =~ /.+\.(\w+)$/;
my $ext = $1;
# Change the url in the message to point to the inline image
unless ($send{msg} =~ s/src\s*=\s*"[^"]+?\Q$_\E[^"]*?"/src="cid:$cid"/gi) {
# Skip any inline images that have been uploaded, but deleted from the email itself
next;
}
my $content = exists($GT::Mail::CONTENT->{$ext}) ? $GT::Mail::CONTENT->{$ext} : 'text/plain';
$content .= '; name="' . $unenc . '"';
push @inline_images, {
body_path => "$attach_dir/$_",
'Content-Type' => $content,
encoding => "Base64",
'Content-ID' => "<$cid>"
};
}
}
=cut
These are the possible combinations for mail created by Gossamer Mail:
html + text:
multipart/alternative
text/plain
text/html
html + text + inline image(s):
multipart/related; type="multipart/alternative"
multipart/alternative
text/plain
text/html
image(s)
html + text + attachment(s):
multipart/mixed
multipart/alternative
text/plain
text/html
attachment(s)
html + text + inline image(s) + attachment(s):
multpart/mixed
multipart/related; type="multipart/alternative"
multipart/alternative
text/plain
text/html
image(s)
attachment(s)
text:
text/plain
text + attachment(s):
multipart/mixed
text/plain
attachment(s)
To create mail structured as above, start from the inside and work our way out.
=cut
my $m = GT::Mail->new(debug => $CFG->{debug});
my $parts;
if ($fields->{compose_is_html}) {
my $html_body = delete $send{msg};
my $text_body = $html_body;
$self->html_to_text(\$text_body);
$IN->html_unescape(\$text_body);
$text_body = $self->linewrap($USER->{opts}->{compose}->{line_wrap}, \$text_body) if $USER->{opts}->{compose}->{line_wrap};
$size += length($html_body) + length($text_body);
$parts = $m->new_part('Content-Type' => 'multipart/alternative');
$parts->parts($m->new_part(
'Content-Type' => 'text/plain; charset="' . ($USER->{opts}->{compose}->{charset} || "US-ASCII") . '"',
body_data => $text_body,
encoding => 'quoted-printable'
));
$parts->parts($m->new_part(
'Content-Type' => 'text/html; charset="' . ($USER->{opts}->{compose}->{charset} || "US-ASCII") . '"',
body_data => $html_body,
encoding => 'quoted-printable'
));
if (@inline_images) {
my $ipart = $m->new_part('Content-Type' => 'multipart/related; type="multipart/alternative"');
$ipart->parts($parts);
for (@inline_images) {
$size += -s $_->{body_path};
$ipart->parts($m->new_part($_));
}
$parts = $ipart;
}
}
else {
my $text_body;
# The email is generated from %send, so for a text/plain email, the body must
# be in $send{msg} instead of the in the GT::Mail::Part object.
if (@attachments) {
$text_body = delete $send{msg};
$text_body = $self->linewrap($USER->{opts}->{compose}->{line_wrap}, \$text_body) if $USER->{opts}->{compose}->{line_wrap};
}
else {
$send{msg} = $self->linewrap($USER->{opts}->{compose}->{line_wrap}, \$send{msg}) if $USER->{opts}->{compose}->{line_wrap};
}
$size += length($text_body);
# If the email is a text part only email, then this will end up being just a
# dummy part to grab the Content-Type from.
$parts = $m->new_part(
'Content-Type' => 'text/plain; charset="' . ($USER->{opts}->{compose}->{charset} || "US-ASCII") . '"',
body_data => $text_body,
encoding => 'quoted-printable'
);
}
if (@attachments) {
my $apart = $m->new_part('Content-Type' => 'multipart/mixed');
$apart->parts($parts);
for (@attachments) {
$size += -s $_->{body_path};
$apart->parts($m->new_part($_));
}
$parts = $apart;
}
# We've generated the email as outlined above, but the outermost layer of this
# mess has to be moved into the %send hash and then the rest attached back onto
# the new GT::Mail object
$send{'Content-Type'} = $parts->get('Content-Type');
my $mail = GT::Mail->new(%send);
for ($parts->parts()) {
$mail->attach($_);
}
require GMail::Folders;
my $drafts_fid = GMail::Folders->drafts_fid;
my $state = $fields->{compose_state};
my $type = $fields->{draft_type};
my $compose_id = $fields->{compose_id};
my $draft_ref; # In case we're resaving the draft, we need to preserve the draft reference
if ($draft) {
# Insert the message in the drafts folders, since we're saving to draft instead of sending.
# If the message was a draft being resaved to the Drafts folder, delete the old draft before resaving
if ($state eq 'draft' and $compose_id) {
# See if it's already a draft message; if so, delete the old draft, and
# grab the old draft reference id
if (0 + $DB->table('msgtrack')->count({
msgtrack_userid => $USER->{userid},
msgtrack_id => $compose_id,
msgtrack_status => $type eq 'reply' ? 'Draft Reply' : $type eq 'forward' ? 'Draft Forward' : 'Draft'
})) {
my $draft_fid; # The folder the draft is *actually* in.
($draft_ref, $draft_fid) = $DB->table('msgtrack')->select('msgtrack_draft_ref', 'msgtrack_fid', { msgtrack_id => $compose_id })->fetchrow;
require GMail::Messages;
local $USER->{opts}->{mailbox}->{delete_to_trash} = 0;
$PLG->dispatch('GMail::Messages::delete_message', sub { GMail::Messages->delete_message(@_) }, $draft_fid, $compose_id);
}
}
# Check to see that no user limits would be exceeded
require GMail::Messages;
my $ret = $PLG->dispatch('GMail::Messages::validate', sub { GMail::Messages->validate(@_) }, $size);
if ($ret > 0) {
# We set a few headers to set the date (Date), to uniquely identify the draft
# (X-GMail-Draft), and to save the 'From' profile selected (X-GMail-Profile).
$mail->top_part->set(Date => $mail->date_stamp);
$mail->top_part->set('X-GMail-Draft' => time);
my $profs_id;
if ($profs_id = $IN->param('profs_id')) {
$DB->table('profs')->count({ profs_id => $profs_id, profs_userid => $USER->{userid} })
or $profs_id = undef;
}
if ($profs_id) {
$mail->top_part->set('X-GMail-Profile' => $profs_id);
}
my $sig_id;
if ($sig_id = $IN->param('sigs_sigid')) {
$DB->table('sigs')->count({ sigs_sigid => $sig_id, sigs_userid => $USER->{userid} })
or $sig_id = undef;
}
if ($sig_id) {
$mail->top_part->set('X-GMail-Signature' => $sig_id);
}
$mail->top_part->set('X-Priority' => $fields->{compose_priority}) if $fields->{compose_priority};
my ($size, $id) = $PLG->dispatch('GMail::Messages::insert_data', sub { GMail::Messages->insert_data(@_) }, $mail)
or return $self->error($GMail::Messages::error, 'WARN');
$PLG->dispatch('GMail::Messages::insert_user', sub { GMail::Messages->insert_user(@_) }, {
msgtrack_userid => $USER->{userid},
msgtrack_mid => $id,
msgtrack_fid => $drafts_fid,
msgtrack_status => $fields->{draft_reply} ? 'Draft Reply' : $fields->{draft_forward} ? 'Draft Forward' : 'Draft',
msgtrack_account => 0,
msgtrack_draft_ref => $state eq 'draft' ? $draft_ref : $compose_id,
msgtrack_draft_html => scalar($IN->param('compose_is_html')) ? 1 : 0,
}, $size) or return $self->error($GMail::Messages::error, 'WARN');
$PLG->dispatch('GMail::Folders::update', sub { GMail::Folders->update(@_) }, $drafts_fid);
}
else {
return $self->error($GMail::Messages::error => 'WARN');
}
}
else {
$mail->top_part->set(date => $mail->date_stamp);
$mail->top_part->set('Message-Id' => '{email}->{out} eq 'smtp' ? $CFG->{email}->{smtp} : $USER->{domain}) . '>');
# Check if we are over max for today
if ($CFG->{limits}{max_emails_per_day} and $CFG->{limits}{max_emails_per_day} > 0) {
my $date = join(",", (localtime)[3,4,5]);
if (!$USER->{opts}{limit_send_track} or $USER->{opts}{limit_send_track}{date} ne $date) {
$USER->{opts}{limit_send_track} = { date => $date, count => 0 };
}
if ($USER->{opts}{limit_send_track}{count} >= $CFG->{limits}{max_emails_per_day}) {
return $self->error("SENDERR_TOOMANY_TODAY", "WARN");
}
$USER->{opts}{limit_send_track}{count}++;
require GMail::Options;
GMail::Options->new->save;
}
# Send the email
$mail->send() or return $self->error('SENDERR_SENDERR', 'WARN', $mail->error());
# Now that the mail has been sent successfully, we can go ahead and remove the draft.
if ($state eq 'draft' and $compose_id) {
# See if it's already a draft message; if so, delete the old draft, and
# grab the old draft reference id
if (0 + $DB->table('msgtrack')->count({
msgtrack_userid => $USER->{userid},
msgtrack_id => $compose_id,
msgtrack_status => $type eq 'reply' ? 'Draft Reply' : $type eq 'forward' ? 'Draft Forward' : 'Draft'
})) {
my $draft_fid; # The folder the draft is *actually* in.
($draft_ref, $draft_fid) = $DB->table('msgtrack')->select('msgtrack_draft_ref', 'msgtrack_fid', { msgtrack_id => $compose_id })->fetchrow;
require GMail::Messages;
local $USER->{opts}->{mailbox}->{delete_to_trash} = 0;
$PLG->dispatch('GMail::Messages::delete_message', sub { GMail::Messages->delete_message(@_) }, $draft_fid, $compose_id);
}
}
# Option was added in 2.1.0, if the user doesn't have this key set, then save it by default.
if (!exists($USER->{opts}->{compose}->{save_copy_to_sent})) {
$USER->{opts}->{compose}->{save_copy_to_sent} = 1;
}
# Check to see if the user has selected to save outgoing email.
if ($USER->{opts}->{compose}->{save_copy_to_sent}) {
require GMail::Folders;
my $sent_fid = GMail::Folders->sent_fid;
# Check to see no user limits are reached
require GMail::Messages;
my $ret = $PLG->dispatch('GMail::Messages::validate', sub { GMail::Messages->validate(@_) }, $size);
if ($ret > 0) {
my ($size, $id) = $PLG->dispatch('GMail::Messages::insert_data', sub { GMail::Messages->insert_data(@_) }, $mail) or return $self->error($GMail::Messages::error, 'WARN');
$PLG->dispatch('GMail::Messages::insert_user', sub { GMail::Messages->insert_user(@_) }, {
msgtrack_userid => $USER->{userid},
msgtrack_mid => $id,
msgtrack_fid => $sent_fid,
msgtrack_status => 'Read',
msgtrack_account => 0,
}, $size);
# Update the folders list
$PLG->dispatch('GMail::Folders::update', sub { GMail::Folders->update(@_) }, $sent_fid);
}
else {
# Warn user that sent email wasn't saved due to too many emails in their account
$IN->param(error_page => $CFG->{templates}->{default});
return $self->error('SENDERR_MAXMSGS', 'WARN', $CFG->{limits}->{msgs_max});
}
}
# Update the reference message status to Replied or Forward, if appropriate
if ($sending eq 'reply' or $sending eq 'forward') {
$DB->table('msgtrack')->update({
msgtrack_status => $sending eq 'forward' ? 'Forward' : 'Replied'
}, {
msgtrack_id => $state eq 'draft' ? $draft_ref : $compose_id,
msgtrack_userid => $USER->{userid}
}) if $state eq 'draft' ? $draft_ref : $compose_id;
}
}
# Get rid of the attachments
$PLG->dispatch('GMail::Email::Attach::clean', sub { GMail::Email::Attach->clean(@_) }, $attach_id) if $attach_id;
# All done
return 1;
}
END_OF_SUB
$COMPILE{_get_value} = __LINE__ . <<'END_OF_SUB';
sub _get_value {
# ------------------------------------------------------------------
# This is similar to the parser in GT::Template but it was
# specifically designed to be used by a non-trusted user and only with
# the tags specified in the user help section for tags. This method
# is called internally by format_body
#
my ($self, $tag, $msg, $body, $top) = @_;
my $value;
if ($tag =~ /^header (.+)$/) {
my $field = $1;
return GT::CGI::html_escape($top->get($field));
}
for (qw/to date subject/) {
if ($tag eq $_) {
# Strip out the extra spaces that are a result grabbing the addresses right from the headers
(my $addresses = $top->get($_)) =~ s/,\s*/, /g;
return GT::CGI::html_escape($addresses);
}
}
if ($tag =~ /transform_local_date\s*\(\s*.*\s*\)\s*/) {
my $format = $1;
$format =~ s/^['"]// and $format =~ s/['"]$//;
require GT::Date;
return GT::CGI::html_escape(GT::Date::date_get(time, $format));
}
if ($tag =~ /transform_sent_date\s*\(\s*.*\s*\)\s*/) {
my $format = $1;
$format =~ s/^['"]// and $format =~ s/['"]$//;
require GT::Date;
return GT::CGI::html_escape(GT::Date::date_transform($msg->{msgs_sent}, '%ddd%, %dd% %mmm% %yyyy% %HH%:%MM%:%ss% %o%', $format));
}
if ($tag eq 'sent_date') {
return GT::CGI::html_escape($msg->{msgs_sent});
}
if ($tag eq 'current_date') {
return GT::CGI::html_escape(scalar localtime);
}
if ($tag eq 'formatted_subject') {
$value = $top->get('subject') or return;
$value =~ s/^
\s*
(?:
(?:fw\s*:) |
(?:fwd\s*:) |
(?:forward\s*:) |
(?:re\s*:) |
(?:reply\s*:)
)
\s*//igx;
return GT::CGI::html_escape($value);
}
if ($tag eq 'from_name') {
$value = $msg->{msgs_sent_from} or return;
my @values = $top->split_line($value);
for (@values) {
next if /\@/;
return GT::CGI::html_escape($_);
}
}
if ($tag eq 'from_email') {
$value = $msg->{msgs_sent_from} or return;
my @values = $top->split_line($value);
for (@values) {
next unless /\@/;
return GT::CGI::html_escape($_);
}
}
if ($tag eq 'from') {
$value = $msg->{msgs_sent_from} or return;
return GT::CGI::html_escape($value);
}
if ($tag eq 'message') {
return $$body;
}
if ($tag eq 'quoted_message') {
$USER->{opts}->{compose}->{format} ||= 'text';
if ($USER->{opts}->{compose}->{format} eq 'text') {
my $q = GT::CGI::html_escape($USER->{opts}->{compose}->{line_prefix}) || '';
my $ret = "$q " . join("\n$q ", split(/\r?\n/, $$body || ''));
return $ret;
}
else {
return $$body || '';
}
}
if ($tag eq 'email') {
return $USER->{email};
}
if ($tag eq 'name') {
return $USER->{dgraph}->{first_name};
}
return "['$tag' tag not found]";
}
END_OF_SUB
$COMPILE{format_body} = __LINE__ . <<'END_OF_SUB';
sub format_body {
# -----------------------------------------------------------------------------
# This method takes a format, subject format and message and parses it for user
# tags. message is a hash of message values as it is stored in the database.
# This method is not normally called from a template. Optionally takes a
# fourth argument which overrides the message format; if not provided, it will
# be automatically determined based on user settings.
#
my ($self, $format, $sbj_format, $msg, $fmt) = @_;
require GMail::Messages;
GMail::Messages->format_msgs_row($msg);
$fmt ||= $USER->{opts}->{compose}->{format};
my $header = '';
my $checksum = $msg->{msgs_checksum};
my $p = substr($msg->{msgdata_filename}, 0, 1) . '/' . substr($msg->{msgdata_filename}, 1, 1) . '/' . substr($msg->{msgdata_filename}, 2, 1);
if (open FH, "$CFG->{location}->{path}->{data}/msgs/$p/$checksum") {
while () {
last if /^\r?\n/;
$header .= $_;
}
}
my $type = $msg->{msgdata_type} || 'text/plain';
my $body;
{
my $tmp = $PLG->dispatch('GMail::Messages::format_message', sub { GMail::Messages->format_message(@_) }, $msg, 1);
$tmp ||= '';
$body = \$tmp;
}
$fmt ||= 'text';
if (!defined $format) {
if ($fmt eq 'text') {
$format = qq|On <%sent_date%> <%from%> was overheard saying:
---------------------------------------------------
<%quoted_message%>
|;
}
else {
$format = qq|
From: <%from%>
'reply_html' => '
From: <%from%>
Sent: <%date%>
To: <%to%>
Subject: <%subject%>
<%message%>
|;
}
}
if ($fmt eq 'text' and $type eq 'text/html') {
$self->html_to_text($body);
$IN->html_escape($body);
$$body = $self->linewrap($USER->{opts}->{compose}->{line_wrap} - 3, $body) if ($USER->{opts}->{compose}->{line_wrap} and $USER->{opts}->{compose}->{line_wrap} > 3);
}
elsif ($fmt eq 'text' and $type eq 'text/plain') {
$$body = $self->linewrap($USER->{opts}->{compose}->{line_wrap} - 3, $body) if ($USER->{opts}->{compose}->{line_wrap} and $USER->{opts}->{compose}->{line_wrap} > 3);
$IN->html_escape($body);
}
elsif ($fmt eq 'html' and $type eq 'text/plain') {
$IN->html_escape($body);
$IN->html_escape($body);
$self->text_to_html($body);
}
elsif ($fmt eq 'html' and $type eq 'text/html') {
$$body =~ s/(?:.*?]*>)?//si;
$$body =~ s[][]gi;
$$body =~ s/]*>//gsi;
$$body =~ s[][]gi;
$IN->html_escape($body);
}
$IN->html_escape(\$format);
my $message = '';
require GT::Mail::Parse;
my $top = GT::Mail::Parse->parse_head([split /\r?\n/ => $header]);
my $last_pos = 0;
while ($format =~ /(<%(.+?)%>)/g) {
my $tag = $2;
my $tag_len = length($1);
my $print_start = $last_pos;
$last_pos = pos($format);
my $print_length = $last_pos - $tag_len - $print_start;
$message .= substr($format, $print_start, $print_length);
my $value = $self->_get_value($tag, $msg, $body, $top) or next;
# The body is already html escaped above.
if ($fmt eq 'html' and $tag !~ /^(?:quoted_)?message$/) {
$value = $IN->html_escape($value);
}
$message .= $value;
}
$message .= substr($format, $last_pos);
$last_pos = 0;
my $subject = '';
$format = $IN->html_escape($sbj_format);
while ($format =~ /(<%(.+?)%>)/g) {
my $tag = $2;
my $tag_len = length($1);
my $print_start = $last_pos;
$last_pos = pos($format);
my $print_length = $last_pos - $tag_len - $print_start;
$subject .= substr($format, $print_start, $print_length);
my $value = $self->_get_value($tag, $msg, $body, $top) or next;
$subject .= $value;
}
$subject .= substr($format, $last_pos);
if ($fmt eq 'html') {
$message = $IN->html_escape(q{}) . $message . $IN->html_escape(q{});
}
return($message, $subject, $top);
}
END_OF_SUB
1;