# ================================================================== # Gossamer Mail - enhanced email management system # # Website : http://gossamer-threads.com/ # Support : http://gossamer-threads.com/scripts/support/ # CVS Info : 087,071,084,093,086 # Revision : $Id: Compose.pm,v 1.107 2003/10/12 23:05:52 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/; use constant MAX_TO => 0; use constant MAX_PER_DAY => 0; # 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 $text = $IN->param('compose_body') || ''; ($text =~ /<\/?(?:br|p|html)>/i) or return {}; $self->html_to_text(\$text); $text =~ s//>/g; $text =~ s/"/"/g; $IN->delete('compose_is_html'); return { compose_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 $text = $IN->param('compose_body') || ''; ($text =~ /<\/?(?:br|p|html)>/i) and return {}; $IN->html_escape(\$text); $self->text_to_html(\$text); return { compose_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 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); # 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 $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}) { use GT::Dumper; 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. Formates 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}) { 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 bassically wraps to _send(). Should be called # for non-reply/forward. # my ($self, $id) = @_; my $cgi = $IN->get_hash; $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, formarts it for inline, # attachments etc.. and calles _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} } ); $cgi->{references} = $sth->fetchrow; # Send the email $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. Calles _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); $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; $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}; } } # Find out the content type for the main message my ($content, $file) = $fields->{compose_is_html} ? ('text/html', 'email_footer.htm') : ('text/plain', 'email_footer.txt'); $send{'Content-Type'} = $content . '; charset="' . ($USER->{opts}->{compose}->{charset} || "US-ASCII") . '"'; 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; } my $attach_id = $fields->{attach_id}; $attach_id =~ /^(\S+)$/ and $attach_id = $1; my $attach_dir = "$USER->{dir}/upload/" . $attach_id; # Check for inline images my @attach = $IN->param('compose_attachments'); 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 $send{msg} =~ s/src\s*=\s*"[^"]+?\Q$_\E[^"]*?"/src="cid:$cid"/gi; my $content = exists($GT::Mail::CONTENT->{$ext}) ? $GT::Mail::CONTENT->{$ext} : 'text/plain'; $content .= '; name="' . $unenc . '"'; # Save for attaching after the mail object is created push @attach, { body_path => "$attach_dir/$_", 'Content-Type' => $content, encoding => "Base64", 'Content-ID' => "<$cid>" }; } } # 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}; # If this is an HTML message, we make two parts, text version and html version. my $m = GT::Mail->new(debug => $CFG->{debug}); if ($fields->{compose_is_html}) { $send{'Content-Type'} = 'multipart/related; type="multipart/alternative"; charset="' . ($USER->{opts}->{compose}->{charset} || "US-ASCII") . '"'; my $msg_body = delete $send{msg}; my $ipart = $m->new_part('Content-Type' => 'multipart/alternative'); $size += length($msg_body); my $copy = $msg_body; $self->html_to_text(\$copy); $IN->html_unescape(\$copy); $size += length($copy); $ipart->parts($m->new_part( 'Content-Type' => 'text/plain; charset="' . ($USER->{opts}->{compose}->{charset} || "US-ASCII") . '"', body_data => $copy, encoding => 'quoted-printable' )); $ipart->parts($m->new_part( 'Content-Type' => 'text/html; charset="' . ($USER->{opts}->{compose}->{charset} || "US-ASCII") . '"', body_data => $msg_body, encoding => 'quoted-printable' )); unshift @attach, $ipart; } # Set content type if we have regular attachments. elsif (@attach) { my $msg_body = delete $send{msg}; $size += length($msg_body); unshift @attach, $m->new_part( 'Content-Type' => 'text/plain; charset="' . ($USER->{opts}->{compose}->{charset} || "US-ASCII") . '"', body_data => $msg_body, encoding => 'quoted-printable', ); $send{'Content-Type'} = 'multipart/mixed; charset="' . ($USER->{opts}->{compose}->{charset} || "US-ASCII") . '"'; } # Otherwise text/plain. else { $send{'Content-Type'} = 'text/plain; charset="' . ($USER->{opts}->{compose}->{charset} || "US-ASCII") . '"'; } # Add the attachments my @parts; for (@attach) { if (ref $_ eq 'HASH') { $size += -s $_->{body_path}; push @parts, $m->new_part($_); } elsif (ref $_) { push @parts, $_; } else { my $tmp = $IN->escape($_); -e "$attach_dir/$tmp" or return $self->error("ATTACHERR_EXIST", "WARN"); $size += -s _; push @parts, $m->new_part( body_path => "$attach_dir/$tmp", encoding => '-guess', filename => $_ ); } } $size += length($send{msg}) if exists $send{msg}; my $mail = new GT::Mail(%send); for (@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); if ($CFG->{email}->{out} eq 'smtp') { $mail->top_part->set('Message-Id' => '{email}->{smtp} . '>'); } # 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); } } # 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 'arive_date') { return GT::CGI::html_escape($msg->{msgs_timestamp}); } 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); $self->linewrap($USER->{opts}->{compose}->{line_wrap}, $body) if ($USER->{opts}->{compose}->{line_wrap}); } elsif ($fmt eq 'text' and $type eq 'text/plain') { $self->linewrap($USER->{opts}->{compose}->{line_wrap}, $body) if ($USER->{opts}->{compose}->{line_wrap}); $IN->html_escape($body); } elsif ($fmt eq 'html' and $type eq 'text/plain') { $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; $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;