#!/usr/bin/perl
# Usage:
#  ./myshadowmail.pl psswrd_cmd psswrd_remote1 psswrd_remote2 ...
#
# Run this perl script as a cron job to set up an email service
# to remote-control one or several remote accounts via a command
# account. The main idea is that you can send emails with 
# commands from a low-bandwidth link (e.g. email via SSB radio or
# satphone) to the command acount and receive replies with info
# on the remote account(s) or get selected emails forwarded.
#
# The commands are in the body of the email for the command 
# account. One command per line. Any number of commands is allowed.
#
# Those commands are currently implemented:
#
#   unread NN ACCOUNTNAME   ... returns mail which contains list
#                               of at most NN unread emails on 
#                               remote account ACCOUNTNAME.
#                               the list contains FROM, DATE, 
#                               SUBJECT, SIZE, UID.
#
#   latest NN ACCOUNTNAME   ... returns mail which contains list
#                               of at most NN latest (read and 
#                               unread) emails on remote account
#                               ACCOUNTNAME. list also includes
#                               SEEN/UNSEEN.
#
#   search FIELD "SEARCH PHRASE" NN ACCOUNTNAME   ... returns mail 
#                               with at most NN search results.
#                               FILELD must be one of the following:
#                               FROM, SUBJECT, BODY, TEXT (where 
#                               TEXT searches the body as well as
#                               the entire header). Your SEARCH 
#                               PHRASE must be delimited by double
#                               quotes.	
#  
#   forward attached UID ACCOUNTNAME ... forwards mail with UID 
#                               from remote account ACCOUNTNAME
#                               to requesting email address.
#                               the mail is sent as attachment.
#                               THIS DOES NOT WORK WITH AIRMAIL
#
#   forward inline UID ACCOUNTNAME ... forwards mail with UID 
#                               from remote account ACCOUNTNAME
#                               inline with ALL attachments.
#
#   forward stripped UID ACCOUNTNAME ... forwards mail with UID 
#                               from remote account ACCOUNTNAME
#                               inline without any attachments.
#                               (only the plain-text part. if the
#                               mail has none, a stripped version
#                               of the html part is sent.)
#                                 
#   mark as read UID ACCOUNTNAME ... marks mail with UID 
#                               from remote account ACCOUNTNAME
#                               as read (or seen).
#

# Author: Christian Feldbauer <christian _at_ pitufa.at>
# Last modified on Oct. 30, 2014
# Version: 1.1
#
# This program is free software; you can redistribute under the 
# same terms as Perl itself.
#
# This program is distributed in the hope that it will be useful, 
# but WITHOUT ANY WARRANTY; without even the implied warranty of 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See either 
# the GNU General Public License or the Artistic License for more 
# details.

use Email::Address;
use Mail::IMAPClient;
use Courriel qw();
use Courriel::Builder;
use Email::Sender::Simple qw(sendmail);
use Email::Sender::Transport::SMTP qw();
use HTML::FormatText;


# disable output buffering
$| = 1;


#############################################
# enter your own servers and login data below

#print "Let's login with pwd $ARGV[0]\n";
# command-imap account
my $imap_cmd = Mail::IMAPClient->new(
  Server => 'imap.yourserver.net',
  User => 'cmmnds@yourserver.net',
  Password => $ARGV[0], # password from command line 
  Ssl => 1,
  Port => 993,
  Uid => 1,
  Reconnectretry => 10,
  Ignoresizeerrors => 1,
  Peek => 1,
) or die "IMAP new(): $@";
# we are connected now...
$imap_cmd->select("INBOX") or die "Could not select INBOX: $@\n";
#print "Logged into CMD account\n";

# IMPORTANT NOTE: we use a dedicated imap account that receives commands and we want to keep it tidy, so we delete ALL messages after processing. If you don't use a dedicated account, MAKE SURE THE DELETE FUNCTION IS NOT CALLED in the script below, otherwise your messages are gone!
my $dedicated_cmd_account = 1;

# commands are allowed only from the following senders (case-sensitiv?):
@allowed_addresses = ('XY0ABC@Winlink.org','your_other_mail@yahoo.com');

# command-smtp account: we can use the same user/psswrd as for the command-imap account
my $SMTP_server = 'smtp.yourserver.net';
my $SMTP_user   = 'cmmnds@yourserver.net';
my $SMTP_psswrd = $ARGV[0];
my $SMTP_from   = 'cmmnds@yourserver.net';
my $SMTP_ssl    = 1;



# remote account no 1
my %imap_pars_christian = (
  Server => 'imap.yourserver.net', 
  User => 'christian@yourdomain.net', 
  Password => $ARGV[1], 
  Ssl => 1, 
  Port => 993,
  Uid => 1,
  Peek => 1,
  Reconnectretry => 10,
  Ignoresizeerrors => 1,
  SelectFolder => "INBOX",
);  


# remote account no 2
my %imap_pars_birgit = (
  Server => 'imap.anotherserver.com', 
  User => 'birgit@otherdomain.com', 
  Password => $ARGV[2], 
  Ssl => 1,
  Port => 993,
  Uid => 1,
  Peek => 1,
  Reconnectretry => 10,
  Ignoresizeerrors => 1,
  SelectFolder => "INBOX",
); 

# remote account no 3: OUTBOX ACCOUNT
my %imap_pars_christian_out = (
  Server => 'imap.yourserver.net', 
  User => 'christian@yourdomain.net', 
  Password => $ARGV[1], 
  Ssl => 1, 
  Port => 993,
  Uid => 1,
  Peek => 1,
  Reconnectretry => 10,
  Ignoresizeerrors => 1,
  SelectFolder => "INBOX.Sent",
);  

# feel free to add more accounts/mail folders here...


# the accounts get (short) names here, which are used in the commands. An
# account name must be a single word of alphanumeric characters or _, i.e., 
# matches [0-9a-zA-Z_]+
my %named_account = (
  'christian' => \%imap_pars_christian,
  'birgit'    => \%imap_pars_birgit,
  'christian_out' => \%imap_pars_christian_out,
);

my @account_names = keys %named_account;

# end of section for server and login data
##########################################






# unread msgs on command account
my @recent_cmd = $imap_cmd->unseen or die "No recent msgs in inbox, so nothing to do.\n";

# go through all unseen mails in the command account
foreach my $id (@recent_cmd) {
#print "processing msg on CMD account\n";

  my $raw = $imap_cmd->message_string($id) or die $imap_cmd->LastError; # also sets the message's "\Seen" flag unless Peek parameter is set true
  
  my $email = Courriel->parse(text => $raw, is_character => 0 );
  my $addr_obj = $email->from();
  my $cmd_sender = $addr_obj->address(); # only the actual address in the form username@hostname.domain
  
  # first check whether sender is allowed
  if ($cmd_sender ~~ @allowed_addresses) {
    # mail is from allowed sender $cmd_sender, so parse for commands now.
#print "Msg is from allowed sender: $cmd_sender\n";
    
    # plain text message!
    my $part = $email->plain_body_part();
    my $content = $part->content();


# ++++++++++++++++++++++++++++
    # 'unread' command: create list of the newest mails on remote account
    while( $content =~ m/unread\s+(\d+)\s+(\w+)/gxm ) {
#print "processing UNREAD cmd\n";
      
      $imap_cmd->set_flag("Seen", ($id)) or warn "Could not flag msg as SEEN.\n";
      
      my $num_mails = $1; # max number
      my $account_name = $2;
      if ($account_name ~~ @account_names) {
#print "account $account_name does exist\n";

        my %imap_pars = %{$named_account{$account_name}};
#print "Let's login with pwd ", $imap_pars{Password}, "\n";
        my $imap_remote = Mail::IMAPClient->new( %imap_pars ) or die "IMAP new(): $@"; # we are connected now
        $imap_remote->select($imap_pars{SelectFolder}) or die "Could not select $imap_pars{SelectFolder}: $@\n";
#print "Connected to remote imap server\n";  
      
        my $count = 0;
        my $plain_text = "";
        
        my $num_new_messages = $imap_remote->unseen_count;
        $plain_text .= "There are $num_new_messages unseen messages in the account $account_name.\r\r";
#print $plain_text;
      
        my @recent_remote = $imap_remote->unseen or die "No recent msgs in inbox.\n";
        
        foreach my $id_remote (reverse @recent_remote) {
#print "going through unread msgs... ";
          my $subject_remote = $imap_remote->get_header( $id_remote, "Subject" ) or die "Could not get header entry.\n";
          my $from_remote    = $imap_remote->get_header( $id_remote, "From" ) or die "Could not get header entry.\n";
          my $date_remote    = $imap_remote->get_header( $id_remote, "Date" ) or die "Could not get header entry.\n";
          my $msg_id_remote  = $imap_remote->get_header( $id_remote, "Message-Id" ) or die "Could not get header entry.\n";
          my $size_remote    = $imap_remote->size($id_remote) or die "Could not find size of message $id_remote: $@\n";
      
          $plain_text .= "Subject: " . $subject_remote . "\r";
          $plain_text .= "From: " . $from_remote . "\r";
          $plain_text .= "Date: " . $date_remote . "\r";
          $plain_text .= "Size: " . $size_remote . " bytes\r";
          #$plain_text .= "Message-ID: " . $msg_id_remote->value() . "\n"; # if usage of UID works, we don't need Message-ID
          $plain_text .= "UID: " . $id_remote . "\r\r";
 
          $count++;
          if($count >= $num_mails){
            last;
          }
        }
        $imap_remote->logout or die "Logout error: ", $imap_remote->LastError, "\n";
#print "logged out from remote imap server\n";  
              
        # create email with list of unread msgs
#print "create email with content:\n";
#print "$plain_text\n";

        my $new_email = build_email(
          subject("Re: unread $num_mails $account_name"),
          from($SMTP_from),
          to($cmd_sender),
          plain_body($plain_text),
        );

#print "sending mail\n";           
        sendmail( $new_email,
          {
            transport => Email::Sender::Transport::SMTP->new({
                host => $SMTP_server,
                ssl => $SMTP_ssl,
                sasl_username => $SMTP_user,
                sasl_password => $SMTP_psswrd,
            })
          }
        );
#print "sent\n";  


      } else {
#print "account $account_name does NOT exist\n";
      }
    } # end of parsing for the 'unread' command
    

# ++++++++++++++++++++++++++++
    # 'latest' command: create list of the newest mails on remote account
    while( $content =~ m/latest\s+(\d+)\s+(\w+)/gxm ) {
#print "processing LATEST cmd\n";
      
      $imap_cmd->set_flag("Seen", ($id)) or warn "Could not flag msg as SEEN.\n";
      
      my $num_mails = $1; # max number
      my $account_name = $2;
      if ($account_name ~~ @account_names) {
#print "account $account_name does exist\n";

        my %imap_pars = %{$named_account{$account_name}};
#print "Let's login with pwd ", $imap_pars{Password}, "\n";
        my $imap_remote = Mail::IMAPClient->new( %imap_pars ) or die "IMAP new(): $@"; # we are connected now
        $imap_remote->select($imap_pars{SelectFolder}) or die "Could not select $imap_pars{SelectFolder}: $@\n";
#print "Connected to remote imap server\n";  
      
        my $count = 0;
        my $plain_text = "";
        
         my @recent_remote = $imap_remote->search("ALL") or die "No msgs in inbox.\n";
        
        foreach my $id_remote (reverse @recent_remote) {
#print "going through msgs...";
          my $subject_remote = $imap_remote->get_header( $id_remote, "Subject" ) or die "Could not get header entry.\n";
          my $from_remote    = $imap_remote->get_header( $id_remote, "From" ) or die "Could not get header entry.\n";
          my $date_remote    = $imap_remote->get_header( $id_remote, "Date" ) or die "Could not get header entry.\n";
          my $msg_id_remote  = $imap_remote->get_header( $id_remote, "Message-Id" ) or die "Could not get header entry.\n";
          my $size_remote    = $imap_remote->size($id_remote) or die "Could not find size of message $id_remote: $@\n";
 
          my @flags = $imap_remote->flags($id_remote);
          my $seenstr = '';
          if ("\\Seen" ~~ @flags) {
            $seenstr = 'Read';
          } else {
            $seenstr = 'Unread';
          }
          $plain_text .= "Subject: " . $subject_remote . "\r";
          $plain_text .= "From: " . $from_remote . "\r";
          $plain_text .= "Date: " . $date_remote . "\r";
          $plain_text .= "Size: " . $size_remote . " bytes\r";
          $plain_text .= "UID: " . $id_remote . "\r";
          $plain_text .= $seenstr . "\r\r";
 
          $count++;
          if($count >= $num_mails){
            last;
          }
        }
        $imap_remote->logout or die "Logout error: ", $imap_remote->LastError, "\n";
#print "logged out from remote imap server\n";  
              
        # create email with list of latest msgs
#print "create email with content:\n";
#print "$plain_text\n";

        my $new_email = build_email(
          subject("Re: latest $num_mails $account_name"),
          from($SMTP_from),
          to($cmd_sender),
          plain_body($plain_text),
        );

#print "sending mail\n";           
        sendmail( $new_email,
          {
            transport => Email::Sender::Transport::SMTP->new({
                host => $SMTP_server,
                ssl => $SMTP_ssl,
                sasl_username => $SMTP_user,
                sasl_password => $SMTP_psswrd,
            })
          }
        );
#print "sent\n";  


      } else {
#print "account $account_name does NOT exist\n";
      }
    } # end of parsing for the 'latest' command


# ++++++++++++++++++++++++++++
    # 'search' command
    while( $content =~ m/search\s+(\w+)\s+"(.+)"\s+(\d+)\s+(\w+)/gxm ) {
#print "processing SEARCH cmd\n";
      
      $imap_cmd->set_flag("Seen", ($id)) or warn "Could not flag msg as SEEN.\n";
      
      my $field = uc $1; # we don't verify FIELD, we assume user knows what he is doing. Also CC or BCC work. Also, one can set up an outbox account and use TO as the search field.
      my $search_str = $2;
      my $num_mails = $3; # max number
      my $account_name = $4;
      if ($account_name ~~ @account_names) {
#print "account $account_name does exist\n";

        my %imap_pars = %{$named_account{$account_name}};
#print "Let's login with pwd ", $imap_pars{Password}, "\n";
        my $imap_remote = Mail::IMAPClient->new( %imap_pars ) or die "IMAP new(): $@"; # we are connected now
        $imap_remote->select($imap_pars{SelectFolder}) or die "Could not select $imap_pars{SelectFolder}: $@\n";
#print "Connected to remote imap server\n";  
      
        my $count = 0;
        my $plain_text = "";
        
        my $search_crit = "$field \"$search_str\"";
        my @recent_remote = $imap_remote->search($search_crit) or die "No msgs found by search criterion $search_crit\n";
        
        foreach my $id_remote (reverse @recent_remote) {
#print "going through msgs...";
          my $subject_remote = $imap_remote->get_header( $id_remote, "Subject" ) or die "Could not get header entry.\n";
          my $from_remote    = $imap_remote->get_header( $id_remote, "From" ) or die "Could not get header entry.\n";
          my $date_remote    = $imap_remote->get_header( $id_remote, "Date" ) or die "Could not get header entry.\n";
          my $msg_id_remote  = $imap_remote->get_header( $id_remote, "Message-Id" ) or die "Could not get header entry.\n";
          my $size_remote    = $imap_remote->size($id_remote) or die "Could not find size of message $id_remote: $@\n";
 
          my @flags = $imap_remote->flags($id_remote);
          my $seenstr = '';
          if ("\\Seen" ~~ @flags) {
            $seenstr = 'Read';
          } else {
            $seenstr = 'Unread';
          }
          $plain_text .= "Subject: " . $subject_remote . "\r";
          $plain_text .= "From: " . $from_remote . "\r";
          $plain_text .= "Date: " . $date_remote . "\r";
          $plain_text .= "Size: " . $size_remote . " bytes\r";
          $plain_text .= "UID: " . $id_remote . "\r";
          $plain_text .= $seenstr . "\r\r";
 
          $count++;
          if($count >= $num_mails){
            last;
          }
        }
        $imap_remote->logout or die "Logout error: ", $imap_remote->LastError, "\n";
#print "logged out from remote imap server\n";  
              
        # create email with list of latest msgs
#print "create email with content:\n";
#print "$plain_text\n";

        my $new_email = build_email(
          subject("Re: search $field \"$search_str\" $num_mails $account_name"),
          from($SMTP_from),
          to($cmd_sender),
          plain_body($plain_text),
        );

#print "sending mail\n";           
        sendmail( $new_email,
          {
            transport => Email::Sender::Transport::SMTP->new({
                host => $SMTP_server,
                ssl => $SMTP_ssl,
                sasl_username => $SMTP_user,
                sasl_password => $SMTP_psswrd,
            })
          }
        );
#print "sent\n";  


      } else {
#print "account $account_name does NOT exist\n";
      }
    } # end of parsing for the 'search' command


# ++++++++++++++++++++++++++++
    # 'forward attached' command:
    # A message/rfc822 part contains the forwarded e-mail message, including any headers.
    while ( $content =~ m/forward\s+attached\s+(\d+)\s+(\w+)/gxm ) {
#print "processing FORWARD ATTACHED command\n";     
      $imap_cmd->set_flag("Seen",($id) ) or warn "Could not flag msg as SEEN.\n";
      
      my $mail_uid = $1;
      my $account_name = $2;
      if ($account_name ~~ @account_names) {
#print "account $account_name does exist\n";

        my %imap_pars = %{$named_account{$account_name}};
        my $imap_remote = Mail::IMAPClient->new( %imap_pars ) or die "IMAP new(): $@"; # we are connected now
#print "connected to remote imap server\n";        
        $imap_remote->select($imap_pars{SelectFolder}) or die "Could not select $imap_pars{SelectFolder}: $@\n";
        
        open(my $fileh, '>', "message.eml") or die $!;
        $imap_remote->message_to_file( $fileh, ($mail_uid)) or die "Could not message_to_file: $@\n";
        close $fileh;
        
        $imap_remote->logout or die "Logout error: ", $imap_remote->LastError, "\n";
#print "logged out from remote imap server\n";

        # create email with attachment
        
        my $new_email = build_email( 
          subject("Re: forward attached $mail_uid $account_name"),
          from($SMTP_from),
          to($cmd_sender),
          plain_body('see attachment'),
          attach(mime_type=>'message/rfc822', file=>"message.eml",),
        );
        
#print "sending mail\n";           
        sendmail( $new_email,
          {
            transport => Email::Sender::Transport::SMTP->new({
                host => $SMTP_server,
                ssl => $SMTP_ssl,
                sasl_username => $SMTP_user,
                sasl_password => $SMTP_psswrd,
            })
          }
        );
#print "sent\n";          
        
      } # if account exists
      else {
#print "account $account_name does not exist\n";
      }
    } # end of parsing for the 'forward attached' command


# ++++++++++++++++++++++++++++
    # 'forward inline' command:
    while ( $content =~ m/forward\s+inline\s+(\d+)\s+(\w+)/gxm ) {
#print "processing FORWARD INLINE command\n";     
      $imap_cmd->set_flag("Seen",($id) ) or warn "Could not flag msg as SEEN.\n";
      
      my $mail_uid = $1;
      my $account_name = $2;
      if ($account_name ~~ @account_names) {
#print "account $account_name does exist\n";

        my %imap_pars = %{$named_account{$account_name}};
        my $imap_remote = Mail::IMAPClient->new( %imap_pars ) or die "IMAP new(): $@"; # we are connected now
#print "connected to remote imap server\n";        
        $imap_remote->select($imap_pars{SelectFolder}) or die "Could not select $imap_pars{SelectFolder}: $@\n";
        
        my $msg_raw = $imap_remote->message_string($mail_uid) or die $imap_remote->LastError;
        
        $imap_remote->logout or die "Logout error: ", $imap_remote->LastError, "\n";
#print "logged out from remote imap server\n";

        # create email        
        my $new_email = build_email( 
          subject("Re: forward inline $mail_uid $account_name"),
          from($SMTP_from),
          to($cmd_sender),
          plain_body("Forwarded message:\n\r\n\r$msg_raw"),
        );
        
#print "sending mail\n";           
        sendmail( $new_email,
          {
            transport => Email::Sender::Transport::SMTP->new({
                host => $SMTP_server,
                ssl => $SMTP_ssl,
                sasl_username => $SMTP_user,
                sasl_password => $SMTP_psswrd,
            })
          }
        );
#print "sent\n";          
        
      } # if account exists
      else {
#print "account $account_name does not exist\n";
      }
    } # end of parsing for the 'forward inline' command


# ++++++++++++++++++++++++++++
    # 'forward stripped' command:
    while ( $content =~ m/forward\s+stripped\s+(\d+)\s+(\w+)/gxm ) {
#print "processing FORWARD STRIPPED command\n";     
      $imap_cmd->set_flag("Seen",($id) ) or warn "Could not flag msg as SEEN.\n";
      
      my $mail_uid = $1;
      my $account_name = $2;
      if ($account_name ~~ @account_names) {
#print "account $account_name does exist\n";

        my %imap_pars = %{$named_account{$account_name}};
        my $imap_remote = Mail::IMAPClient->new( %imap_pars ) or die "IMAP new(): $@"; # we are connected now
#print "connected to remote imap server\n";        
        $imap_remote->select($imap_pars{SelectFolder}) or die "Could not select $imap_pars{SelectFolder}: $@\n";
        
        my $msg_raw = $imap_remote->message_string($mail_uid) or die $imap_remote->LastError;
        
        $imap_remote->logout or die "Logout error: ", $imap_remote->LastError, "\n";
#print "logged out from remote imap server\n";

        # create email        
        my $email = Courriel->parse(text => $msg_raw, is_character => 0 );
        # plain text message!
        my $from_obj = $email->from();
        my $from_addr = $from_obj->address(); # only the actual address in the form
        my $subj = $email->subject();
        my $content = "";
        if( my $part = $email->plain_body_part() ) {
          $content = $part->content();
        } else { # NO PLAIN-BODY PART, let's take the HTML PART instead
          if( my $part = $email->html_body_part() ) {
            my $html_string = $part->content();
            
            $content = HTML::FormatText->format_string(
                       $html_string,
                       leftmargin => 0, rightmargin => 60
                       );
          } else {
            $content = "Neither a plain-text nor an html part in this e-mail.";
          }
        }
        
        my $new_email = build_email( 
          subject("Re: forward stripped $mail_uid $account_name"),
          from($SMTP_from),
          to($cmd_sender),
          plain_body("Forwarded message:\r\rFrom: $from_addr\rSubject: $subj\r\r$content"),
        );
        
#print "sending mail\n";           
        sendmail( $new_email,
          {
            transport => Email::Sender::Transport::SMTP->new({
                host => $SMTP_server,
                ssl => $SMTP_ssl,
                sasl_username => $SMTP_user,
                sasl_password => $SMTP_psswrd,
            })
          }
        );
#print "sent\n";          
        
      } # if account exists
      else {
#print "account $account_name does not exist\n";
      }
    } # end of parsing for the 'forward stripped' command


# ++++++++++++++++++++++++++++
    # 'mark as read' command:
    while ( $content =~ m/mark\s+as\s+read\s+(\d+)\s+(\w+)/gxm ) {
#print "processing MARK AS READ command\n";     
      $imap_cmd->set_flag("Seen",($id) ) or warn "Could not flag cmd msg as SEEN.\n";
      
      my $mail_uid = $1;
      my $account_name = $2;
      if ($account_name ~~ @account_names) {
#print "account $account_name does exist\n";

        my %imap_pars = %{$named_account{$account_name}};
        my $imap_remote = Mail::IMAPClient->new( %imap_pars ) or die "IMAP new(): $@"; # we are connected now
#print "connected to remote imap server\n";        
        $imap_remote->select($imap_pars{SelectFolder}) or die "Could not select $imap_pars{SelectFolder}: $@\n";
        
        $imap_remote->set_flag("Seen",($mail_uid) ) or warn "Could not flag msg as SEEN.\n";
        
        $imap_remote->logout or warn "Logout error: ", $imap_remote->LastError, "\n";
#print "logged out from remote imap server\n";

      } # if account exists
      else {
#print "account $account_name does not exist\n";
      }
    } # end of parsing for the 'mark as read' command 




    
  } # mail is from allowed sender
  else {
#print "Sender not allowed\n";
  }

  if ($dedicated_cmd_account) {
    $imap_cmd->delete_message($id) or die "Could not mark for deletion: $@\n"; # we keep our dedicated command account tidy (or rather empty)
  }
} # through all unseen mails in the command account

if ($dedicated_cmd_account) {
  $imap_cmd->close or die "Could not close: $@\n"; # closes folder and delete msgs marked for deletion
}

$imap_cmd->logout or die "Logout error: ", $imap_cmd->LastError, "\n";
#print 'Logged out from cmd account', "\n\n";  


