#!/usr/bin/perl -I../../../../.. -I. use strict; use Pos::Webtop::Web::ContentModule::Mail::Proxy; package Pos::Webtop::Web::ContentModule::Mail::ImapProxy; our @ISA = qw( Pos::Webtop::Web::ContentModule::Mail::Proxy ); my $debug = 0; sub new { my $this = shift; my $class = ref($this) || $this; my ($self,$tb,$smtp,$imap,$username,$password) = @{{@_}}{qw/self tb smtpServer imapServer username password/}; if ( not defined $self ) { $self = {}; bless($self,$class); } # setup base class my %args = @_; $args{'self'} = $self; $args{'tb'} = $tb; Pos::Webtop::Web::ContentModule::Mail::Proxy->new( %args ); # initialize attributes $self->{'smtpServer'} = $smtp; $self->{'imapServer'} = $imap; $self->{'username'} = $username; $self->{'password'} = $password; $self->{'userId'} = $tb->getClient()->getUserId; $self->{'internalId'} = 0; $self->{'loggedIn'} = 0; return $self; } sub login { my $self = shift; my %hash; my $id; if ($self->{'loggedIn'} == 1) { return 1; } if (not defined($self->connect(host => $self->{'imapServer'}, port => 143))) { print STDERR "IMAP: failed to connect to " . $self->{'imapServer'} . "\n"; return undef; } # Read in the banner if (!(%hash = %{ $self->readUntilResponse(id => '*') }) or ($hash{'SUCCESS'} == 0)) { print STDERR "IMAP: did not get banner\n"; return undef; } # Send the login message $id = $self->writeCommand(command => "LOGIN " . $self->{'username'} . " " . $self->{'password'}); if (!(%hash = %{ $self->readUntilResponse(id => $id) }) or ($hash{'SUCCESS'} == 0)) { print STDERR "IMAP: login authentication failed\n"; return undef; } $self->{'loggedIn'} = 1; return 1; } sub logout { my $self = shift; $self->disconnect; $self->{'loggedIn'} = 0; return 1; } sub checkEmail { # noop for imap return 1; } sub deleteEmail { my $self = shift; my ($emailId) = @{{@_}}{qw/emailId/}; my $id; my %hash; if (not defined($self->login())) { return undef; } my %idParts = $self->getIdParts(emailId => $emailId); # Select the folder $id = $self->writeCommand(command => "SELECT " . $idParts{'FOLDER'}); if (!(%hash = %{ $self->readUntilResponse(id => $id) }) or ($hash{'SUCCESS'} == 0)) { print STDERR "IMAP: select failed [delete]\n"; return undef; } my $uid = $self->getUidFromMessageId(messageId => $idParts{'MESSAGE-ID'}); # Select the folder $id = $self->writeCommand(command => "STORE $uid +FLAGS \\Deleted"); if (!(%hash = %{ $self->readUntilResponse(id => $id) }) or ($hash{'SUCCESS'} == 0)) { print STDERR "IMAP: store failed [delete]\n"; return undef; } # Close the folder $id = $self->writeCommand(command => "CLOSE"); if (!(%hash = %{ $self->readUntilResponse(id => $id) }) or ($hash{'SUCCESS'} == 0)) { print STDERR "IMAP: close failed [delete]\n"; return undef; } return 1; } sub sendEmail { my $self = shift; my ($to,$cc,$bcc,$subject,$body,$attachments) = @{{@_}}{qw/to cc bcc subject body attachments/}; my $smtp = Pos::Webtop::Web::ContentModule::Mail::Smtp->new(db => $self->{'db'}, tb => $self->{'tb'}); if (not defined($smtp)) { return undef; } $smtp->startMessage(to => $to, cc => $cc, bcc => $bcc, subject => $subject, body => $body, attachments => $attachments); if (not defined($smtp->transmit(host => $self->{'smtpServer'}, port => 25))) { return undef; } return 1; } sub createFolder { my $self = shift; my ($folderName) = @{{@_}}{qw/folderName/}; my $id; my %hash; if (not defined($self->login)) { return undef; } if ($self->doesFolderExist(folderName => $folderName)) { return undef; } if ($folderName =~ /^\./) { print STDERR "IMAP: folder begins with a dot.\n"; return undef; } $folderName =~ s/^\\//; # List all the folders $id = $self->writeCommand(command => "CREATE \"$folderName\""); if (!(%hash = %{ $self->readUntilResponse(id => $id) }) or ($hash{'SUCCESS'} == 0)) { print STDERR "IMAP: create failed\n"; return undef; } return 1; } sub getFolders { my $self = shift; my %hash; my $id; my @folders = (); if (not defined($self->login())) { return undef; } # List all the folders $id = $self->writeCommand(command => "LIST \"\" *"); if (!(%hash = %{ $self->readUntilResponse(id => $id) }) or ($hash{'SUCCESS'} == 0)) { print STDERR "IMAP: list failed\n"; return undef; } foreach my $line (@{ $hash{'LINES'} }) { $line = $self->safeChomp(value => $line); my $folderName = $line; my $found = 0; my $flags; $folderName =~ s/(.*)\((.*)\) (.*?) (.*)/$4/; $flags = $2; $folderName =~ s/\"//g; # Skip folders with dot's in them or ones that are marked as noselect if (($folderName =~ /^\./) or ($flags =~ /\\Noselect/i)) { next; } foreach my $current (@folders) { my ($name, $count) = @{ $current }; if ($name eq $folderName) { $found = 1; } } if (not defined($folderName)) { $found = 1; } if (!$found) { my @info; push(@info, $folderName); my ($id) = $self->{'db'}->query(sql => " select folder_id from w_user_email_folders where folder_name = '" . $self->{'db'}->escape(dirty => $folderName) . "' AND user_id = '" . $self->{'userId'} . "' "); if (not defined($id)) { ($id) = $self->{'db'}->getNextSequenceValue( sequence => "w_user_email_folders_pk"); $self->{'db'}->do(sql => " insert into w_user_email_folders ( user_id, folder_id, folder_name ) values( '" . $self->{'userId'} . "', '$id', '" . $self->{'db'}->escape(dirty => $folderName) . "' )"); } # SELECT my %newHash; $id = $self->writeCommand(command => "SELECT \"$folderName\""); # If we can't open this folder, skip it. if (!(%newHash = %{ $self->readUntilResponse(id => $id) }) or ($newHash{'SUCCESS'} == 0)) { print STDERR "IMAP: warning: search failed on folder $folderName, skipping [select]\n"; next; } # Search for new emails $id = $self->writeCommand(command => "SEARCH all UNSEEN"); if (!(%newHash = %{ $self->readUntilResponse(id => $id) }) or ($newHash{'SUCCESS'} == 0)) { print STDERR "IMAP: search failed [search]\n"; return undef; } my $newEmailCount = 0; foreach my $newLine (@{ $newHash{'LINES'} }) { $newLine = $self->safeChomp(value => $newLine); my @array = split / /, $newLine; $newEmailCount = scalar(@array); $newEmailCount -= 2; last; } # SELECT $id = $self->writeCommand(command => "CLOSE"); if (!(%newHash = %{ $self->readUntilResponse(id => $id) }) or ($newHash{'SUCCESS'} == 0)) { print STDERR "IMAP: search failed [close]\n"; return undef; } push(@info, $newEmailCount); if ($folderName =~ /^INBOX$/i) { unshift(@folders, \@info); } else { push(@folders, \@info); } } } return (@folders); } sub moveToFolder { my $self = shift; my ($emailsSc, $folderName) = @{{@_}}{qw/emails folderName/}; my %hash; my $id; if (not defined($self->login)) { return undef; } if (!$self->doesFolderExist(folderName => $folderName)) { print STDERR "IMAP: folder does not exist ($folderName)\n"; return undef; } foreach my $emailId (@{ $emailsSc }) { my %idParts = $self->getIdParts(emailId => $emailId); my $currentFolderName = $idParts{'FOLDER'}; my $messageId = $idParts{'MESSAGE-ID'}; # skip this if ($currentFolderName eq $folderName) { next; } # Select the folder $id = $self->writeCommand(command => "SELECT \"$currentFolderName\""); if (!(%hash = %{ $self->readUntilResponse(id => $id) }) or ($hash{'SUCCESS'} == 0)) { print STDERR "IMAP: select failed [move]\n"; return undef; } # Search for the message $id = $self->writeCommand(command => "SEARCH all HEADER Message-Id " . $messageId); if (!(%hash = %{ $self->readUntilResponse(id => $id) }) or ($hash{'SUCCESS'} == 0)) { print STDERR "IMAP: search failed [move]\n"; return undef; } foreach my $line (@{ $hash{'LINES'} }) { $line = $self->safeChomp(value => $line); my ($ignore, $ignore1, $uid) = split / /, $line; if (not defined($uid)) { next; } # Copy the email to the destination folder $id = $self->writeCommand(command => "COPY $uid \"$folderName\""); if (!(%hash = %{ $self->readUntilResponse(id => $id) }) or ($hash{'SUCCESS'} == 0)) { print STDERR "IMAP: search failed [move]\n"; return undef; } # mark the message as deleted in the current folder $id = $self->writeCommand(command => "STORE $uid +FLAGS (\\Deleted)"); if (!(%hash = %{ $self->readUntilResponse(id => $id) }) or ($hash{'SUCCESS'} == 0)) { print STDERR "IMAP: store failed [move]\n"; return undef; } # expunge the email $id = $self->writeCommand(command => "EXPUNGE"); if (!(%hash = %{ $self->readUntilResponse(id => $id) }) or ($hash{'SUCCESS'} == 0)) { print STDERR "IMAP: search failed [move]\n"; return undef; } last; } # Close the folder $id = $self->writeCommand(command => "CLOSE"); if (!(%hash = %{ $self->readUntilResponse(id => $id) }) or ($hash{'SUCCESS'} == 0)) { print STDERR "IMAP: close failed [move]\n"; return undef; } } return 1; } sub getEmail { my $self = shift; my ($emailId) = @{{@_}}{qw/emailId/}; if (not defined($self->login)) { return undef; } my %idParts = $self->getIdParts(emailId => $emailId); my %hash; my $email = Pos::Webtop::Web::ContentModule::Mail::Email->new(db => $self->{'db'}, tb => $self->{'tb'}); my $folderName; my $isNew; my @lines = $self->getEmailContentsBare(messageId => $idParts{'MESSAGE-ID'}, folderName => $idParts{'FOLDER'}, isNew => \$isNew, wantText => 1); $email->initializeFromLines(lines => \@lines); my $date = $email->getAttribute(name => 'DATE', chomp => 1); my $ignore; if ($date =~ /\+/) { ($date,$ignore) = split /\+/, $date; } elsif ($date =~ /\-/) { ($date,$ignore) = split /\-/, $date; } $hash{'EMAIL_ID'} = $emailId; $hash{'MESSAGE_ID'} = $idParts{'MESSAGE-ID'}; $hash{'EMAIL_TO'} = $email->getAttribute(name => 'TO', chomp => 1); $hash{'EMAIL_FROM'} = $email->getAttribute(name => 'FROM', chomp => 1); $hash{'EMAIL_SUBJECT'} = $email->getAttribute(name => 'SUBJECT', chomp => 1); $hash{'EMAIL_DATE'} = $date; $hash{'IS_NEW'} = $isNew; $hash{'EMAIL_ATTACHMENTS'} = $email->getAttachments; $hash{'EMAIL_BODY'} = $email->getBody; $hash{'EMAIL_BODY_ENCODING'} = $email->getBodyEncoding; return %hash; } sub getPreviewEmails { my $self = shift; my ($max, $folder) = @{{@_}}{qw/max folder/}; my %hash; my $id; my @emails; if (not defined($self->login())) { return undef; } # Select the folder $id = $self->writeCommand(command => "SELECT \"$folder\""); if (!(%hash = %{ $self->readUntilResponse(id => $id) }) or ($hash{'SUCCESS'} == 0)) { print STDERR "IMAP: select failed [preview]\n"; return undef; } # Search for messages in this folder $id = $self->writeCommand(command => "SEARCH all"); if (!(%hash = %{ $self->readUntilResponse(id => $id) }) or ($hash{'SUCCESS'} == 0)) { print STDERR "IMAP: search failed [preview]\n"; return undef; } foreach my $line (@{ $hash{'LINES'} }) { $line = $self->safeChomp(value => $line); my @uids = split / /, $line; my $count = 0; foreach my $uid (@uids) { $count++; if ($count <= 2) { next; } my @lines = $self->getEmailContents(uid => $uid, wantText => 0); my $email = Pos::Webtop::Web::ContentModule::Mail::Email->new(tb => $self->{'tb'}, db => $self->{'db'}); $email->initializeFromLines(lines => \@lines); my $isNew = $self->isEmailNew(uid => $uid); my $date = $email->getAttribute(name => 'DATE', chomp => 1); my $ignore; if ($date =~ /\+/) { ($date,$ignore) = split /\+/, $date; } elsif ($date =~ /\-/) { ($date,$ignore) = split /\-/, $date; } my $emailId = $folder . "\\" . $email->getAttribute(name => 'MESSAGE-ID', chomp => 1); my @result; push @result, $emailId; push @result, $email->getAttribute(name => 'MESSAGE-ID', chomp => 1); push @result, $email->getAttribute(name => 'TO', chomp => 1); push @result, $email->getAttribute(name => 'FROM', chomp => 1); push @result, $email->getAttribute(name => 'SUBJECT', chomp => 1); push @result, $date; push @result, $isNew; push @emails, \@result; } } # Close the folder $id = $self->writeCommand(command => "CLOSE"); if (!(%hash = %{ $self->readUntilResponse(id => $id) }) or ($hash{'SUCCESS'} == 0)) { print STDERR "IMAP: close failed [preview]\n"; return undef; } return @emails; } sub getAdjacentEmails { my $self = shift; my ($emailId) = @{{@_}}{qw/emailId/}; if (not defined($self->login())) { return undef; } my %idParts = $self->getIdParts(emailId => $emailId); my @emails = $self->getPreviewEmails(folder => $idParts{'FOLDER'}); my $prevId = undef; my $nextId = undef; my $counter = 0; foreach my $email (@emails) { if (not defined($email)) { next; } my ($id,$msgId,$to,$from,$subject,$date,$isNew) = @{ $email }; if ($id eq $emailId) { if ($counter > 0) { $prevId = @{ $emails[$counter-1] }[0]; } if ($counter+1 < scalar(@emails)) { $nextId = @{ $emails[$counter+1] }[0]; } last; } $counter++; } my %hash; $hash{'PREV'} = $prevId; $hash{'NEXT'} = $nextId; return \%hash; } sub cacheCredentials { my $self = shift; my ($username, $password) = @{{@_}}{qw/username password/}; $self->{'db'}->do(sql => " update w_user_email_settings_imap set username = '" . $self->{'db'}->escape(dirty => $username) . "', password = '" . $self->{'db'}->escape(dirty => $password) . "' where user_id = '" . $self->{'userId'} . "' "); return 1; } sub markEmailAsRead { my $self = shift; my ($emailId) = @{{@_}}{qw/emailId/}; my %hash; my $id; my %idParts = $self->getIdParts(emailId => $emailId); if (not defined($self->login())) { return undef; } # Select the folder $id = $self->writeCommand(command => "SELECT \"" . $idParts{'FOLDER'} . "\""); if (!(%hash = %{ $self->readUntilResponse(id => $id) }) or ($hash{'SUCCESS'} == 0)) { print STDERR "IMAP: select failed [mark]\n"; return undef; } my $uid = $self->getUidFromMessageId(messageId => $idParts{'MESSAGE-ID'}); # Select the folder $id = $self->writeCommand(command => "STORE $uid +FLAGS \\Seen"); if (!(%hash = %{ $self->readUntilResponse(id => $id) }) or ($hash{'SUCCESS'} == 0)) { print STDERR "IMAP: store failed [mark]\n"; return undef; } # Close the folder $id = $self->writeCommand(command => "CLOSE"); if (!(%hash = %{ $self->readUntilResponse(id => $id) }) or ($hash{'SUCCESS'} == 0)) { print STDERR "IMAP: close failed [mark]\n"; return undef; } return 1; } sub doesFolderExist { my $self = shift; my ($folderName) = @{{@_}}{qw/folderName/}; if (not defined($self->login)) { return undef; } my @folders = $self->getFolders; foreach my $folder (@folders) { my ($name, $count) = @{ $folder }; if (not defined($name)) { next; } if ($name eq $folderName) { return 1; } } return undef; } sub getNextOutgoingMailId { my $self = shift; my $id = ""; foreach my $a (1..32) { $id .= int(rand(9)); } return $id; } # Internal methods sub getNextInternalId { my $self = shift; $self->{'internalId'} = $self->{'internalId'} + 1; return $self->{'internalId'}; } sub writeCommand { my $self = shift; my ($command) = @{{@_}}{qw/command/}; my $id = $self->getNextInternalId; if ($debug == 1) { print STDERR "IMAP: writing '$id $command'\n"; } if (defined($self->writeLine(text => $id . " " . $command))) { return $id; } else { return undef; } } sub readUntilResponse { my $self = shift; my $id = @{{@_}}{qw/id/}; my $success = 0; my $line; my $count = 0; my @lines = (); my %hash; if ($id eq '*') { $id = '\*'; } while (defined($line = $self->readLine)) { $count++; if ($line =~ /^$id (OK|FAIL|ERR|BAD|NO|PREAUTH)/i) { last; } push @lines, $line; } if ($count == 0) { $success = 1; } elsif ($line =~ /^$id OK/i) { $success = 1; } else { chomp($line); print STDERR "IMAP: Got err from server: $line\n"; $success = 0; } $hash{'SUCCESS'} = $success; $hash{'LINES'} = \@lines; return \%hash; } sub safeChomp { my $self = shift; my ($value) = @{{@_}}{qw/value/}; my $save = $/; $/ = "\n"; chomp($value); $/ = "\r"; chomp($value); $/ = $save; return $value; } sub getEmailContents { my $self = shift; my ($uid,$wantText) = @{{@_}}{qw/uid wantText/}; my %hash; my $id; my @emailLines = (); my $count = 0; # Read the header of the email $id = $self->writeCommand(command => "FETCH $uid RFC822.HEADER"); if (!(%hash = %{ $self->readUntilResponse(id => $id) }) or ($hash{'SUCCESS'} == 0)) { print STDERR "IMAP: close failed\n"; return undef; } $count = 0; foreach my $line (@{ $hash{'LINES'} }) { $count++; if ($line =~ /^\* (.*) FETCH/ and $count == 1) { next; } push(@emailLines, $line); if ($line eq "\r\n" or $line eq "\n") { last; } } if (defined($wantText) and $wantText) { # Read the body of the email $id = $self->writeCommand(command => "FETCH $uid RFC822.TEXT"); if (!(%hash = %{ $self->readUntilResponse(id => $id) }) or ($hash{'SUCCESS'} == 0)) { print STDERR "IMAP: fetch failed\n"; return undef; } $count = 0; foreach my $line (@{ $hash{'LINES'} }) { $count++; if ($line =~ /^\* (.*) FETCH/ and $count == 1) { next; } my $temp = $self->safeChomp(value => $line); if ($temp eq ')' and $count == scalar(@{ $hash{'LINES' }})) { next; } push(@emailLines, $line); } } return (@emailLines); } sub getEmailContentsBare { my $self = shift; my ($messageId,$folderName,$isNew,$wantText) = @{{@_}}{qw/messageId folderName isNew wantText/}; my %hash; my $id; my @lines = (); # Select the folder $id = $self->writeCommand(command => "SELECT $folderName"); if (!(%hash = %{ $self->readUntilResponse(id => $id) }) or ($hash{'SUCCESS'} == 0)) { print STDERR "IMAP: select failed\n"; return undef; } my $uid = $self->getUidFromMessageId(messageId => $messageId); @lines = $self->getEmailContents(uid => $uid, wantText => $wantText); if (defined($uid) and defined($isNew)) { $$isNew = $self->isEmailNew(uid => $uid); } # Close the folder $id = $self->writeCommand(command => "CLOSE"); if (!(%hash = %{ $self->readUntilResponse(id => $id) }) or ($hash{'SUCCESS'} == 0)) { print STDERR "IMAP: close failed\n"; return undef; } return (@lines); } sub getUidFromMessageId { my $self = shift; my ($messageId) = @{{@_}}{qw/messageId/}; my %hash; my $id; # Search for unseen messages $id = $self->writeCommand(command => "SEARCH all HEADER Message-Id " . $messageId); if (!(%hash = %{ $self->readUntilResponse(id => $id) }) or ($hash{'SUCCESS'} == 0)) { print STDERR "IMAP: search failed\n"; return undef; } my $ignore; my $ignore1; my $uid; foreach my $line (@{ $hash{'LINES'} }) { $line = $self->safeChomp(value => $line); ($ignore, $ignore1, $uid) = split / /, $line; if (not defined($uid)) { next; } last; } return $uid; } sub isEmailNew { my $self = shift; my ($uid) = @{{@_}}{qw/uid/}; my %hash; my $id; # Search for the message flags $id = $self->writeCommand(command => "FETCH $uid FLAGS"); if (!(%hash = %{ $self->readUntilResponse(id => $id) }) or ($hash{'SUCCESS'} == 0)) { print STDERR "IMAP: fetch failed [isEmailNew]\n"; return undef; } my $isNew = 1; foreach my $t (@{ $hash{'LINES'} }) { if ($t =~ /Seen/i) { $isNew = 0; } last; } return $isNew; } sub getIdParts { my $self = shift; my ($emailId) = @{{@_}}{qw/emailId/}; my %hash; my $temp = $emailId; $temp =~ s/(.*)\\(.*)$/$1 $2/; my ($folder,$msg) = split / /, $temp; $hash{'FOLDER'} = $folder; $hash{'MESSAGE-ID'} = $msg; return %hash; } 1;