# mailcheck_imap.pl

# Contains code from centericq.pl (public domain) and imapbiff (GPL) and
# hence this is also GPL'd.

use strict;
use vars qw($VERSION %IRSSI);
$VERSION = "0.5";
%IRSSI = (
    authors     => "David \"Legooolas\" Gardner",
    contact     => "irssi\@icmfp.com",
    name        => "mailcheck_imap",
    description => "Staturbar item which indicates how many new emails you have in the specified IMAP[S] mailbox",
    sbitems     => "mailcheck_imap",
    license     => "GNU GPLv2",
    url         => "http://icmfp.com/irssi",
);


# TODO:
#
# - command to show status, so we can see if we are currently connected
#  - add to statusbar item to say connected/not
#
# ? get user to type in password instead of storing it in a setting...
#  - eg. /mailcheck_imap_pass <password>
#
# - settings
#  - execute arbitrary command (with /exec?) on new mail?
#   - for 'spoing' or something  ;)
#  - auto-reconnect on/off
#
#
# LATER:
# - show subject/sender/whatever of new mail (customizable)
# - multiple accounts?
# - multiple mailboxes?


# Known bugs: segfaults on exit of irssi when script loaded  :/


use Irssi;
use Irssi::TextUI;
use IO::Socket;

# TODO : avoid requiring SSL when it's not in use?
#if (Irssi::settings_get_bool('mailcheck_imap_use_ssl')) {
#	Irssi::print("Using SSL.") if $debug_msgs;
#	$port = 993;
        require IO::Socket::SSL;
#  - you need the package libio-socket-ssl-perl on Debian
#}

#
# TODO : Set up signal handling for clean shutdown...
#
#$SIG{'ALRM'} = sub { die "socket timeout" };
#$SIG{'QUIT'} = 'cleanup';
#$SIG{'HUP'}  = 'cleanup';
#$SIG{'INT'}  = 'cleanup';
#$SIG{'KILL'} = 'cleanup';
#$SIG{'TERM'} = 'cleanup';



sub draw_box ($$$$) {
  my ($title, $text, $footer, $colour) = @_;
  my $box = '';
  $box .= '%R,--[%n%9%U'.$title.'%U%9%R]%n'."\n";
  foreach (split(/\n/, $text)) {
    $box .= '%R|%n '.$_."\n";
  }
  $box .= '%R`--<%n'.$footer.'%R>->%n';
  $box =~ s/%.//g unless $colour;
  return $box;
}


sub show_help() {
  my $help = $IRSSI{name}." ".$VERSION."
/mailcheck_imap_help
    Display this help.
/mailcheck_imap
    Check for new mail immediately, opening the connection if required.
/mailcheck_imap_stop
    Close connection to server and stop checking for new mail.
/set mailcheck_imap
    Show all mailcheck_imap settings.
    Note: You need to set at least host, user and password.
/statusbar <name> add mailcheck_imap
    Add statusbar item for mailcheck.


Formats in theme for statusbar item:
(number of new mails in $0, total number of message in $1)
  sb_mailcheck_imap = \"{sb Mail: $0 new, $1 total}\";
  sb_mailcheck_imap_zero = \"{sb Mail: None new, $1 total}\";

Format in theme for 'new mail arrived' message in current window:
(number of new mails in $0, total number of message in $1)
      mailcheck_imap_echo = \"You have $0 new message(s)!\";

Note: You have to set at least the mailcheck_imap_host, user,
      and password settings.

IMPORTANT NOTE: As this stores the password in your irssi config
file, you should really set the mode of the file to 0600 so that
it's only readable by your user.
";
  my $text = "";
  foreach (split(/\n/, $help)) {
    $_ =~ s/^\/(.*)$/%9\/$1%9/;
    $text .= $_."\n";
  }
  print CLIENTCRAP draw_box($IRSSI{name}, $text, "Help", 1);
}


sub cmd_mailcheck_imap_help {
  show_help();
}


#
# Global variables.
#
my $handle;
my ($logged_in, $sleep);
my ($last_refresh_time, $refresh_tag);
my ($new_messages, $old_new_messages);
my ($total_messages, $old_total_messages);

$handle    = 0;
$logged_in = 0;
$old_new_messages = -1;
$old_total_messages = -1;


#
# Subroutine to update status, called every N seconds.
#
sub refresh_mailcheck_imap {

  # For now, just print a message and return  :)
  Irssi::print("update hit.") if Irssi::settings_get_bool('mailcheck_imap_debug');

  # ensure we have details for the login..
  if(!check_details()) {
    return 0;
  }

  if(!$handle) {
    if(!setup_socket()) {
      error("Couldn't setup socket to imap server!",0);
      return 0;
    }
  }
  Irssi::print("Socket is setup.") if Irssi::settings_get_bool('mailcheck_imap_debug');

  if(!$logged_in) {
    if(!login()) {
      return 0;
    }
  }
  $new_messages = check_imap("UNSEEN");
  $total_messages = check_imap("MESSAGES");

  $new_messages = 0 if (! $new_messages);
  $total_messages = 0 if (! $total_messages);

  if ($new_messages eq "-1" || $total_messages eq "-1") {
    Irssi::print("check_imap returned an error, no updates.") if Irssi::settings_get_bool('mailcheck_imap_debug');
  }

  # update statusbar if changed rather than updating every the time...
  if(($new_messages != $old_new_messages) ||
     ($total_messages != $old_total_messages)) {
    update_statusbar_item();
  }


  # TODO : This doesn't work if you get a sequence such as:
  #        check -> arrive, delete, arrive -> check
  #        as it is just done on the number of unseen messages and won't know..
  if(($new_messages > $old_new_messages) &&
     (Irssi::settings_get_bool('mailcheck_imap_echo_new_in_window'))) {
    # If set, echo to the current window...
    my $theme = Irssi::current_theme();
    my $format = $theme->format_expand("{mailcheck_imap_echo}");

    if ($format) {
      # use theme-specific look
      $format = $theme->format_expand("{mailcheck_imap_echo $new_messages $total_messages}", Irssi::EXPAND_FLAG_IGNORE_REPLACES);
    } else {
      # use the default look
      $format = "mailcheck_imap: You have ".$new_messages." new message(s).";
    }

    print CLIENTCRAP $format;
  }
  $old_new_messages = $new_messages;
  $old_total_messages = $total_messages;

  # Adding new timeout to make sure that this function will be called again
  if ($refresh_tag) {
    Irssi::timeout_remove($refresh_tag);
  }
  my $time = Irssi::settings_get_int('mailcheck_imap_interval');
  $refresh_tag = Irssi::timeout_add($time*1000, 'refresh_mailcheck_imap', undef);

  return 1;
}


#
# Subroutine to setup socket handle.
#
sub setup_socket {
	# Set an alarm in case we can not connect or get hung.  Older versions
	# the IO::Socket perl module caused errors with the alarm we set before
	# setting up the socket.  If this program dies in debug mode saying:
	# "Alarm clock", then you can probably fix it by upgrading your perl
	# IO module.
	my ($host,$port);

	$host = Irssi::settings_get_str('mailcheck_imap_host');
	$port = Irssi::settings_get_int('mailcheck_imap_port');

	# change port number if SSL enabled and original imap port unchanged
	if($port == 143 && Irssi::settings_get_bool('mailcheck_imap_use_ssl')) {
	  $port = 993;
	}

	eval {
		alarm 30;
		Irssi::print("mailcheck_imap connecting to mail server...");

		if (Irssi::settings_get_bool('mailcheck_imap_use_ssl')) {
			Irssi::print("Using ssl...") if Irssi::settings_get_bool('mailcheck_imap_debug');
			$handle = IO::Socket::SSL->new(Proto           => "tcp",
			                               SSL_verify_mode => 0x00,
                                                       PeerAddr        => $host,
			                               PeerPort        => $port,
		                               	)
			or error("Can't connect to port $port on $host: $!",0), return 0;
		} else {
			$handle = IO::Socket::INET->new(Proto    => "tcp",
			                                PeerAddr => $host,
                                                        PeerPort => $port,
		                               	)
			or error("Can't connect to port $port on $host: $!",0), return 0;
		}
		$handle->autoflush(1);    # So output gets there right away.
		Irssi::print("...done");
		receive();
		alarm 0;
	};
	if ($@) {
		alarm 0;
		if ($@ =~ /timeout/) {
			alarm();
			return 0;
		} else {
			error("$@",0);
			return 0;
		}
	}
	return 1;
}

#
# Subroutine to login to the mailbox.
#
sub login {
  my ($response,$success);
  my ($user,$password);


  $user = Irssi::settings_get_str('mailcheck_imap_user');
  $password = Irssi::settings_get_str('mailcheck_imap_password');


  $logged_in = 0;
  # Set an alarm in case we can not connect or get hung.  Older versions
  # the IO::Socket perl module caused errors with the alarm we set before
  # setting up the socket.  If this program dies in debug mode saying:
  # "Alarm clock", then you can probably fix it by upgrading your perl
  # IO module.
  eval {
    alarm 30;
    send_data("A001 LOGIN \"$user\" \"$password\"","\"$user\"");
    while (1) {
      ($success,$response) = receive();
      if (! $success) {
	return 0;
      }
      last if $response =~ /LOGIN|OK/;
    }
    if ($response =~ /fail|BAD/) {
      return 0;
    } else {
      $logged_in = 1;
    }
    alarm 0;
  };
  if ($@) {
    alarm 0;
    if ($@ =~ /timeout/) {
      alarm();
      return 0;
    } else {
      error("$@",0);
      return 0;
    }
  }
  # Success!  :D
  return 1;
}

#
# Subroutine that does check of imap mailbox.
#
sub check_imap {
  my ($type) = @_;

  #my ($type) = ("MESSAGES");

  my ($response,$success,$num_messages);
  # Set an alarm in case we can not connect or get hung.  Older versions
  # the IO::Socket perl module caused errors with the alarm we set before
  # setting up the socket.  If this program dies in debug mode saying:
  # "Alarm clock", then you can probably fix it by upgrading your perl
  # IO module.
  eval {
    alarm 30;
    send_data("A003 STATUS INBOX ($type)");
    while (1) {
      ($success,$response) = receive();
      if (! $success) {
	return "-1";
      }
      last if $response =~ /STATUS\s+.*?\s+\($type/;
    }
    ($num_messages) = $response =~ /\($type\s+(\d+)\)/;
    alarm 0;
  };
  if ($@) {
    alarm 0;
    if ($@ =~ /timeout/) {
      alarm();
      return "-1";
    } else {
      error("$@",0);
      return "-1";
    }
  }
  return $num_messages;
}


#
# Subroutine to send a line to the imap server.
# Block everything after $block.
#
sub send_data {
	my ($line,$block) = (@_);
	print $handle "$line\r\n";
	$line =~ s/(.*$block).*/$1 ----/ if ($block);
	Irssi::print("sent: $line") if Irssi::settings_get_bool('mailcheck_imap_debug');
	return 1;
}


#
# Subroutine to get a response from the imap server and print.
# that response if in debug mode.
#
sub receive {
	my ($response,$success);
	$response = "";
	$success  = 0;
	chomp($response = <$handle>);
	if ($response) {
		Irssi::print("got: $response") if Irssi::settings_get_bool('mailcheck_imap_debug');
		$success = 1;
	} else {
		Irssi::print("no response!") if Irssi::settings_get_bool('mailcheck_imap_debug');
	}
	return ($success,$response);
}

#
# Subroutine to display and error message in a text box.
#
sub error {
  my ($error,$fatal) = (@_);

  if ($fatal) {
    # TODO : Print some useful message and die?
    Irssi::print("mailcheck_imap FATAL : $error");
    return 0;
  } else {
    Irssi::print("mailcheck_imap error : $error");

    if ($refresh_tag) {
      Irssi::timeout_remove($refresh_tag)
    }
    my $time = Irssi::settings_get_int('mailcheck_imap_interval');
    $refresh_tag = Irssi::timeout_add($time*1000, 'refresh_mailcheck_imap', undef);
    $handle = 0;
    return 0;
  }	
}

#
# Subroutine to call when alarm times out.
#
sub alarm {
  Irssi::print("Alarm went off!") if Irssi::settings_get_bool('mailcheck_imap_debug');
  return 1;
}


#
# Subroutine to clean up.
#
sub cleanup {
  if ($handle) {
    send_data("A999 LOGOUT");
    $handle->close();
  }
  Irssi::print("mailcheck_imap logged out.");
}



#######################################################################
# Simply requests a statusbar item redraw.

sub update_statusbar_item {
  Irssi::statusbar_items_redraw('mailcheck_imap');
}


#######################################################################
# This is the function called by irssi to obtain the statusbar item.

sub mailcheck_imap {
  my ($item, $get_size_only) = @_;

  my $theme = Irssi::current_theme();
  my $format = $theme->format_expand("{sb_mailcheck_imap}");

  if ($format) {
    # use theme-specific look
    $format = $theme->format_expand("{sb_mailcheck_imap $new_messages $total_messages}", Irssi::EXPAND_FLAG_IGNORE_REPLACES);
  } else {
    # use the default look
    $format = "{sb Mail: ".$new_messages." new, ".$total_messages." total}";
  }

  if($new_messages == 0) {
    if(Irssi::settings_get_bool('mailcheck_imap_show_zero')) {
      $format = $theme->format_expand("{sb_mailcheck_imap_zero $new_messages $total_messages}", Irssi::EXPAND_FLAG_IGNORE_REPLACES);

      if (!$format) {
	# use the default look
	$format = "{sb Mail: None new, ".$total_messages." total}";
      }
    } else {
      $format = "";
    }
  }

  if (length($format) == 0) {
    # nothing to print, so don't print at all
    if ($get_size_only) {
      $item->{min_size} = $item->{max_size} = 0;
    }
  } else {
    $item->default_handler($get_size_only, $format, undef, 1);
  }
}


################################################################################
# Ensure that all required details are filled in:
# host, user, password
sub check_details {
  my $host = Irssi::settings_get_str('mailcheck_imap_host');
  my $user = Irssi::settings_get_str('mailcheck_imap_user');
  my $password = Irssi::settings_get_str('mailcheck_imap_password');

  if(!$host || !$user || !$password) {
    show_help();
    return 0;
  }
  return 1;
}


################################################################################
# Immediately check for new mail (updates statusbar item too)

sub cmd_mailcheck_imap {
  refresh_mailcheck_imap();
}


################################################################################
# Kill the connection and stop the refresh.
sub cmd_mailcheck_imap_stop {
  if ($refresh_tag) {
    Irssi::timeout_remove($refresh_tag);
  }
  cleanup();
}

# Also close connection on script unload?
sub sig_command_script_unload ($$$) {
  my ($script, $server, $witem) = @_;

  if($script =~ /^mailcheck_imap\.pl$/ ||
     $script =~ /^mailcheck_imap/) {
    cleanup();
  }
}

Irssi::signal_add_first('command script unload', \&sig_command_script_unload);


#######################################################################
# Adding stuff to irssi

Irssi::settings_add_int('mail', 'mailcheck_imap_interval', 120);
Irssi::settings_add_bool('mail', 'mailcheck_imap_use_ssl', 0);
Irssi::settings_add_bool('mail', 'mailcheck_imap_debug', 0);
Irssi::settings_add_bool('mail', 'mailcheck_imap_show_zero', 0);
Irssi::settings_add_bool('mail', 'mailcheck_imap_echo_new_in_window', 1);

Irssi::settings_add_str('mail', 'mailcheck_imap_host', '');
Irssi::settings_add_int('mail', 'mailcheck_imap_port', 143);
Irssi::settings_add_str('mail', 'mailcheck_imap_user', '');
Irssi::settings_add_str('mail', 'mailcheck_imap_password', '');


Irssi::statusbar_item_register('mailcheck_imap', '{sb $0-}', 'mailcheck_imap');

Irssi::command_bind('mailcheck_imap_help','cmd_mailcheck_imap_help');
Irssi::command_bind('mailcheck_imap','cmd_mailcheck_imap');
Irssi::command_bind('mailcheck_imap_stop','cmd_mailcheck_imap_stop');


#######################################################################
# Startup functions

# Check that everything is fiiiine and start checking if so
if(check_details()) {
  # All is ok, so start running it
  refresh_mailcheck_imap();
  update_statusbar_item();
}


#######################################################################
