#!/usr/bin/perl use strict; use MIME::Base64 qw(encode_base64 decode_base64); use POSIX (); my $spoolDirectory = "/data/webtop/mailspool/"; package Pos::Webtop::Web::ContentModule::Mail::Email; sub dateStringToOracleDate { my $date = shift; if ( not $date =~ /^(?:[^,]+,)?\s*(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{4})\s(\d{1,2}):(\d{1,2}):(\d{1,2})(?:\s+(.*)?)?$/ ) { return undef; } my $tz = POSIX::strftime("\%z",gmtime); my ($day,$month,$year,$hours,$mins,$seconds,$tzStuff) = ($1,$2,$3,$4,$5,$6,$7); my $minuteMod = 0; $date = ((Pos::defaults::isPostgresDb()) ? "to_timestamp" : "to_date" ) . "('$day $month $year $hours:$mins:$seconds','DD MON YYYY HH24:MI:SS')"; if ( defined($tzStuff) && length($tzStuff) && $tzStuff =~ /^(?:GMT)?(?:(\-|\+)(\d\d)(\d\d))?$/ ) { # we have to take into account the TZ goodness. my ($tzSign,$tzHours,$tzMins) = ($1,$2,$3); $tz =~ /(\-|\+)(\d\d)(\d\d)/; my ($tzSignUs,$tzHoursUs,$tzMinsUs) = ($1,$2,$3); $tzSign = "+" unless defined $tzSign; $tzHours = 0 unless defined $tzHours; $tzMins = 0 unless defined $tzMins; my $tzMinOffset = ($tzHours * 60) + $tzMins; my $tzMinOffsetUs = ($tzHoursUs * 60) + $tzMinsUs; if ( $tzSign eq "-" ) { $tzMinOffset *= -1; } if ( $tzSignUs eq "-" ) { $tzMinOffsetUs *= -1; } # now we have the timezone offsets of the message and of the database server, lets go ahead and # calculate the minutes difference we need to use $minuteMod = $tzMinOffsetUs - $tzMinOffset; } if ( $minuteMod ) { $date .= " + interval '1 minute' * $minuteMod"; } return $date; } sub new { my $this = shift; my $class = ref($this) || $this; my ($self, $db, $tb) = @{{@_}}{qw/self db tb/}; if (not defined($self)) { $self = {}; bless($self, $class); } $self->{'db'} = $db; $self->{'tb'} = $tb; $self->{'folder'} = 'Inbox'; $self->{'isNew'} = 0; # Save the userId my $client = $tb->getClient; $self->{'userId'} = $client->getUserId; return $self; } sub initializeFromLines { my $self = shift; my ($linesScalar) = @{{@_}}{qw/lines/}; my $mode = 'header'; my %header; my @lines = @$linesScalar; $self->{'header'} = \%header; $self->{'currentName'} = undef; # Header attribute name $self->{'currentValue'} = undef; # Header attribute value $self->{'body'} = ""; $self->{'bodyEncoding'} = ""; $self->{'multipart'} = 0; # Whether or not we're multipart $self->{'boundary'} = undef; # Our multipart boundary $self->{'attachments'} = (); # Array of Email objects $self->{'attachmentFirst'} = 0; # Whether or not we've hit our first boundary $self->{'currentEmail'} = undef; # Multipart email body foreach my $line (@lines) { if ($mode eq 'header') { $mode = $self->parseHeaderLine(line => $line); } else { $mode = $self->parseBodyLine(line => $line); } if ($mode eq "EOE") { last; } } if ($self->{'multipart'}) { my $bodyEmail = $self->extractBodyEmail; if (defined($bodyEmail)) { $self->{'body'} = $bodyEmail->getBody; $self->{'bodyEncoding'} = $bodyEmail->getBodyEncoding; } } return 1; } sub extractBodyEmail { my $self = shift; my $bodyEmail = undef; foreach my $currentEmail (@{ $self->{'attachments'} }) { my $contentType = $currentEmail->getAttribute(name => 'Content-Type'); if (not defined($contentType)) { $contentType = "text/plain"; } my @info = split /;/, $contentType; my ($media, $sub) = split /\//, $info[0]; if (lc($media) eq 'multipart') { $bodyEmail = $currentEmail->extractBodyEmail; } elsif (lc($info[0]) eq 'text/plain') { $bodyEmail = $currentEmail; } if (defined($bodyEmail)) { last; } } return $bodyEmail; } sub parseHeaderLine { my $self = shift; my ($line) = @{{@_}}{qw/line/}; my $header = $self->{'header'}; # If we've reached a blank line, we're done processing the header if (($line eq "\r\n") or ($line eq "\n")) { # If we've got a pending attribute, add it. if (defined($self->{'currentName'})) { $header->{uc($self->{'currentName'})} = $self->{'currentValue'}; } my $contentType = $self->getAttribute(name => 'Content-Type'); if (not defined($contentType)) { $contentType = "text/plain"; } my @info = split /;/, $contentType; my ($media, $sub) = split /\//, $info[0]; # Is this a multpart message, if so, extract the boundary. if (lc($media) eq 'multipart') { $self->{'multipart'} = 1; $contentType =~ /boundary=[\"](.*)[\"]/i; $self->{'boundary'} = $1; } return "body"; } if ($line =~ /^([A-Za-z0-9\-]*):(.*)/) { # If we've got a pending attribute, add it. if (defined($self->{'currentName'})) { $header->{uc($self->{'currentName'})} = $self->{'currentValue'}; } $self->{'currentName'} = $1; my $value = $2; $value =~ s/( *)(.*)/$2/g; $self->{'currentValue'} = $self->safeChomp(value => $value); } else { $self->{'currentValue'} .= $self->safeChomp(value => $line); } return "header"; } sub parseBodyLine { my $self = shift; my ($line) = @{{@_}}{qw/line/}; # If this isn't a multipart message, we simply concatenate and return if (!($self->{'multipart'})) { $self->{'body'} .= $line; return "body"; } # Otherwise things get a tad bit more complex. my $boundary = $self->{'boundary'}; if ($line =~ /--$boundary/) { # If this is the first boundary, we shall ignore it as it is the marker of our start if ($self->{'attachmentFirst'} == 0) { $self->{'attachmentFirst'} = 1; return "body"; } if (defined($self->{'currentEmail'})) { my $mpEmail = Pos::Webtop::Web::ContentModule::Mail::Email->new(db => $self->{'db'}, tb => $self->{'tb'}); if (defined($mpEmail)) { my @lines = @{ $self->{'currentEmail'} }; $mpEmail->initializeFromLines(lines => \@lines); push ( @{ $self->{'attachments'} }, $mpEmail ); } } $self->{'currentEmail'} = undef; if ($line =~ /--$boundary--/) { return "EOE"; } } elsif ($self->{'attachmentFirst'}) { if (not defined($self->{'currentEmail'})) { $self->{'currentEmail'} = (); } push ( @{ $self->{'currentEmail'} }, $line ); } return "body"; } sub copy { my $self = shift; my ($source, $excludeFolder, $excludeIsNew) = @{{@_}}{qw/source excludeFolder excludeIsNew/}; if (!$excludeFolder) { $self->{'folder'} = $source->{'folder'}; } if (!$excludeIsNew) { $self->{'isNew'} = 1; } $self->{'header'} = $source->{'header'}; $self->{'body'} = $source->{'body'}; $self->{'bodyEncoding'} = $source->{'bodyEncoding'}; $self->{'attachments'} = $source->{'attachments'}; $self->{'multipart'} = $source->{'multipart'}; $self->{'boundary'} = $source->{'boundary'}; return 1; } sub flagAsNew { my $self = shift; $self->{'isNew'} = 1; } sub isNew { my $self = shift; return $self->{'isNew'}; } sub getBody { my $self = shift; return $self->{'body'}; } sub getBodyEncoding { my $self = shift; my $encoding; if ($self->{'multipart'}) { $encoding = $self->{'bodyEncoding'}; } else { $encoding = $self->getAttribute(name => 'Content-Transfer-Encoding'); } if (not defined($encoding)) { $encoding = '7bit'; } return $encoding; } sub getAttribute { my $self = shift; my ($hash, $name) = @{{@_}}{qw/hash name/}; my $value; if (not defined($hash)) { $hash = $self->{'header'}; } $value = $hash->{uc($name)}; return $value; } sub getFolder { my $self = shift; return $self->{'folder'}; } sub getAttachments { my $self = shift; my @translated = (); foreach my $currentEmail (@{ $self->{'attachments'} }) { my $contentDisposition = $currentEmail->getAttribute(name => 'Content-Disposition'); if (not defined($contentDisposition)) { next; } if ($contentDisposition =~ /attachment/i) { my %emailHash; $emailHash{'ATTACHMENT_CONTENT_TYPE'} = $currentEmail->getAttribute(name => 'Content-Type'); $emailHash{'ATTACHMENT_CONTENT_ENCODING'} = $currentEmail->getAttribute(name => 'Content-Transfer-Encoding'); $emailHash{'ATTACHMENT_DISPOSITION'} = $currentEmail->getAttribute(name => 'Content-Disposition'); $emailHash{'ATTACHMENT_DESC'} = $currentEmail->getAttribute(name => 'Content-Description'); $emailHash{'ATTACHMENT_BODY'} = $currentEmail->getBody(); push (@translated, \%emailHash); } } return \@translated; } sub getEmailFolderPath { my $self = shift; my $folderPath = $spoolDirectory . $self->{'userId'} . "/" . $self->getFolder; $folderPath =~ /(.*)/; $folderPath = $1; # Someone is trying to do something naughty if ($folderPath =~ /\.\./) { return undef; } return $folderPath; } sub openEmailFolderRead { my $self = shift; my $fd; my $folderPath = $self->getEmailFolderPath; open($fd, "<$folderPath"); return $fd; } sub openEmailFolderWrite { my $self = shift; my ($ext, $append) = @{{@_}}{qw/ext append/}; my $folderPath = $self->getEmailFolderPath . ((defined($ext))?$ext:""); my $fd; if (defined($append) and $append == 1) { open($fd, ">>$folderPath"); } else { open($fd, ">$folderPath"); } return $fd; } sub copyEmailFolderFromTemporary { my $self = shift; my $folderPath = $self->getEmailFolderPath; rename($folderPath . ".tmp", $folderPath); return 1; } sub enumerateEmailFolder { my $self = shift; my ($fd) = @{{@_}}{qw/fd/}; my @lines; my $line; my $ret; while ($line = <$fd>) { if ($line =~ /^From => [^:]*$/) { last; } push (@lines, $line); } if (not defined($line) and scalar(@lines) == 0) { return undef; } if (scalar(@lines) == 0) { return $self->enumerateEmailFolder(fd => $fd); } $ret = Pos::Webtop::Web::ContentModule::Mail::Email->new(db => $self->{'db'}, tb => $self->{'tb'}); if (defined($ret)) { $ret->initializeFromLines(lines => \@lines); } return $ret; } sub closeEmailFolder { my $self = shift; my ($fd) = @{{@_}}{qw/fd/}; close($fd); return 1; } sub toString { my $self = shift; my ($insideMultipart, $boundary, $output) = @{{@_}}{qw/insideMultipart boundary output/}; my $header = $self->{'header'}; if ($insideMultipart) { $$output .= "--$boundary\n"; } else { $$output .= "From => Email Starting\n"; } # Save the header foreach my $key (keys(%{ $header })) { $$output .= "$key: " . $header->{$key} . "\n"; } $$output .= "\n"; # Now for the body if ($self->{'multipart'}) { foreach my $currentEmail (@{ $self->{'attachments'} }) { $currentEmail->toString(insideMultipart => 1, boundary => $self->{'boundary'}, output => $output); } if ($insideMultipart) { $$output .= $self->getBody; } } else { $$output .= $self->{'body'}; } if ($self->{'multipart'}) { $$output .= "--" . $self->{'boundary'} . "--\n"; } if (!($$output =~ /(\r\n\r\n|\n\n|\r\n\n)$/)) { $$output .= "\n"; } } sub persistToDisk { my $self = shift; my $fdRead = $self->openEmailFolderRead; my $current; my $found = 0; if (defined($fdRead)) { while (defined($current = $self->enumerateEmailFolder(fd => $fdRead))) { my $currentMessageId = $current->getAttribute(name => 'Message-Id'); my $myMessageId = $self->getAttribute(name => 'Message-Id'); if (defined($currentMessageId) and defined($myMessageId) and $currentMessageId eq $myMessageId) { $found = 1; last; } } $self->closeEmailFolder(fd => $fdRead); } else { print STDERR "persistToDisk(): Warning: could not open mailbox for reading ($!)\n"; } if (!$found) { if (!mkdir($spoolDirectory . $self->{'userId'})) { my $err = $!; if ($err ne 'File exists') { print STDERR "persistToDisk(): mkdir failed '$err')\n"; } } my $fdWrite = $self->openEmailFolderWrite(append => 1); if (defined($fdWrite)) { $self->writeToFile(fd => $fdWrite); $self->closeEmailFolder(fd => $fdWrite); } else { print STDERR "persistToDisk(): Failed to open mailbox for writing ($!)\n"; return undef; } } return 1; } sub removeFromDisk { my $self = shift; my ($emailId) = @{{@_}}{qw/emailId/}; my $fdRead = $self->openEmailFolderRead; my $fdWrite = $self->openEmailFolderWrite(ext => ".tmp"); my $folderPath = $self->getEmailFolderPath; my $current; my ($messageId) = $self->{'db'}->query(sql => " select wue.message_id from w_user_emails wue where wue.email_id = '" . $self->{'db'}->escape(dirty => $emailId) . "' AND wue.user_id = '" . $self->{'userId'} . "' "); if (defined($fdRead)) { while (defined($current = $self->enumerateEmailFolder(fd => $fdRead))) { my $currentMessageId = $current->getAttribute(name => 'Message-Id'); # Skip this email as we're removing it. if (defined($currentMessageId) and defined($messageId) and $currentMessageId eq $messageId) { next; } else { $current->writeToFile(fd => $fdWrite); } } $self->closeEmailFolder(fd => $fdWrite); rename($folderPath . ".tmp", $folderPath); } else { print STDERR "removeFromDisk(): Couldn't open mailbox for reading. ($!)\n"; return undef; } return 1; } sub writeToFile { my $self = shift; my ($fd) = @{{@_}}{qw/fd/}; my $ps = {}; $ps->{'output'} = ""; $self->toString(output => \( $ps->{'output'} )); print $fd $ps->{'output'}; } sub removeFromDatabase { my $self = shift; my ($emailId) = @{{@_}}{qw/emailId/}; $self->{'db'}->{'debug'} = 1; $self->{'db'}->do(sql => " delete from w_user_emails where email_id = '" . $self->{'db'}->escape(dirty => $emailId) . "' AND user_id = '" . $self->{'userId'} . "' "); $self->{'db'}->{'debug'} = 0; return 1; } sub initializeFromDatabase { my $self = shift; my $db = $self->{'db'}; my ($emailId) = @{{@_}}{qw/emailId/}; my @res = $db->query(sql => " select wuef.folder_name, wue.message_id, wue.is_new from w_user_emails wue, w_user_email_folders wuef where wue.email_id = '" . $db->escape(dirty => $emailId) . "' AND wuef.folder_id = wue.folder_id AND wuef.user_id = wue.user_id AND wue.user_id = '" . $self->{'userId'} . "' "); if (scalar(@res) == 0) { return undef; } my ($folderName, $messageId, $isNew) = @{ $res[0] }; $self->{'folder'} = $folderName; $self->{'isNew'} = $isNew; # Now read the contents of the email from the file my $current; my $fdRead = $self->openEmailFolderRead; if (defined($fdRead)) { while (defined($current = $self->enumerateEmailFolder(fd => $fdRead))) { my $currentMessageId = $current->getAttribute(name => 'Message-Id'); if (defined($currentMessageId) and defined($messageId) and $currentMessageId eq $messageId) { $self->copy(source => $current, excludeFolder => 1, excludeIsNew => 1); last; } } $self->closeEmailFolder(fd => $fdRead); } else { print STDERR "initializeFromDatabase(): Warning: could not open mailbox for reading ($!)\n"; } return 1; } sub synchronizeToDatabase { my $self = shift; my ($uidl, $emailSize) = @{{@_}}{qw/uidl emailSize/}; my $db = $self->{'db'}; if (not defined($uidl)) { $uidl = ""; } if (not defined($emailSize)) { $emailSize = 0; } if (not defined($self->persistToDisk())) { return undef; } my $id = $self->getAttribute(name => 'Message-Id'); my $from = $self->getAttribute(name => 'From'); my $to = $self->getAttribute(name => 'To'); my $subject = $self->getAttribute(name => 'Subject'); my $date = $self->getAttribute(name => 'Date'); my ($emailId) = $db->query(sql => " select email_id from w_user_emails where message_id = '" . $db->escape(dirty => $id) . "' AND user_id = '" . $self->{'userId'} . "' for update "); my ($folderId) = $db->query(sql => " select folder_id from w_user_email_folders where upper(folder_name) = upper('" . $db->escape(dirty => $self->{'folder'}) . "') AND user_id = '" . $self->{'userId'} . "' "); if (not defined($folderId)) { ($folderId) = $db->getNextSequenceValue( sequence => "w_user_email_folders_pk"); $db->do(sql => " insert into w_user_email_folders ( folder_id, user_id, folder_name ) values ( '$folderId', '" . $self->{'userId'} . "', '" . $db->escape(dirty => $self->{'folder'}) . "' )"); } if ($emailId) { $db->do(sql => " update w_user_emails set folder_id = '$folderId' where email_id = '$emailId' AND user_id = '" . $self->{'userId'} . "' "); } else { ($emailId) = $db->getNextSequenceValue( sequence => "w_user_emails_pk"); # convert the date to an oracle string warn "The date was $date\n"; $date = dateStringToOracleDate($date); $date = $db->getCurrentDateString() unless defined $date; # Truncate to prevent database problems $from = substr($from, 0, 254); $to = substr($to, 0, 254); $db->{'debug'} = 1; $db->do(sql => " insert into w_user_emails ( email_id, user_id, message_id, is_new, email_from, email_to, email_subject, email_date, folder_id, uidl, email_size ) values ( '$emailId', '" . $self->{'userId'} . "', '" . $db->escape(dirty => $id) . "', '1', '" . $db->escape(dirty => $from) . "', '" . $db->escape(dirty => $to) . "', '" . $db->escape(dirty => $subject) . "', $date, '$folderId', '" . $db->escape( dirty=> $uidl ) . "', '$emailSize' )"); } $db->{'debug'} = 0; return 1; } sub safeChomp { my $self = shift; my ($value) = @{{@_}}{qw/value/}; my $save = $/; $/ = "\n"; chomp($value); $/ = "\r"; chomp($value); $/ = $save; return $value; }