1# mailcheck_imap.pl
2
3# Contains code from centericq.pl (public domain) and imapbiff (GPL) and
4# hence this is also GPL'd.
5
6use strict;
7use vars qw($VERSION %IRSSI);
8$VERSION = "0.5";
9%IRSSI = (
10    authors     => "David \"Legooolas\" Gardner",
11    contact     => "irssi\@icmfp.com",
12    name        => "mailcheck_imap",
13    description => "Staturbar item which indicates how many new emails you have in the specified IMAP[S] mailbox",
14    sbitems     => "mailcheck_imap",
15    license     => "GNU GPLv2",
16    url         => "http://icmfp.com/irssi",
17);
18
19
20# TODO:
21#
22# - command to show status, so we can see if we are currently connected
23#  - add to statusbar item to say connected/not
24#
25# ? get user to type in password instead of storing it in a setting...
26#  - eg. /mailcheck_imap_pass <password>
27#
28# - settings
29#  - execute arbitrary command (with /exec?) on new mail?
30#   - for 'spoing' or something  ;)
31#  - auto-reconnect on/off
32#
33#
34# LATER:
35# - show subject/sender/whatever of new mail (customizable)
36# - multiple accounts?
37# - multiple mailboxes?
38
39
40# Known bugs: segfaults on exit of irssi when script loaded  :/
41
42
43use Irssi;
44use Irssi::TextUI;
45use IO::Socket;
46
47# TODO : avoid requiring SSL when it's not in use?
48#if (Irssi::settings_get_bool('mailcheck_imap_use_ssl')) {
49#	Irssi::print("Using SSL.") if $debug_msgs;
50#	$port = 993;
51        require IO::Socket::SSL;
52#  - you need the package libio-socket-ssl-perl on Debian
53#}
54
55#
56# TODO : Set up signal handling for clean shutdown...
57#
58#$SIG{'ALRM'} = sub { die "socket timeout" };
59#$SIG{'QUIT'} = 'cleanup';
60#$SIG{'HUP'}  = 'cleanup';
61#$SIG{'INT'}  = 'cleanup';
62#$SIG{'KILL'} = 'cleanup';
63#$SIG{'TERM'} = 'cleanup';
64
65
66
67sub draw_box ($$$$) {
68  my ($title, $text, $footer, $colour) = @_;
69  my $box = '';
70  $box .= '%R,--[%n%9%U'.$title.'%U%9%R]%n'."\n";
71  foreach (split(/\n/, $text)) {
72    $box .= '%R|%n '.$_."\n";
73  }
74  $box .= '%R`--<%n'.$footer.'%R>->%n';
75  $box =~ s/%.//g unless $colour;
76  return $box;
77}
78
79
80sub show_help() {
81  my $help = $IRSSI{name}." ".$VERSION."
82/mailcheck_imap_help
83    Display this help.
84/mailcheck_imap
85    Check for new mail immediately, opening the connection if required.
86/mailcheck_imap_stop
87    Close connection to server and stop checking for new mail.
88/set mailcheck_imap
89    Show all mailcheck_imap settings.
90    Note: You need to set at least host, user and password.
91/statusbar <name> add mailcheck_imap
92    Add statusbar item for mailcheck.
93
94
95Formats in theme for statusbar item:
96(number of new mails in $0, total number of message in $1)
97  sb_mailcheck_imap = \"{sb Mail: $0 new, $1 total}\";
98  sb_mailcheck_imap_zero = \"{sb Mail: None new, $1 total}\";
99
100Format in theme for 'new mail arrived' message in current window:
101(number of new mails in $0, total number of message in $1)
102      mailcheck_imap_echo = \"You have $0 new message(s)!\";
103
104Note: You have to set at least the mailcheck_imap_host, user,
105      and password settings.
106
107IMPORTANT NOTE: As this stores the password in your irssi config
108file, you should really set the mode of the file to 0600 so that
109it's only readable by your user.
110";
111  my $text = "";
112  foreach (split(/\n/, $help)) {
113    $_ =~ s/^\/(.*)$/%9\/$1%9/;
114    $text .= $_."\n";
115  }
116  print CLIENTCRAP draw_box($IRSSI{name}, $text, "Help", 1);
117}
118
119
120sub cmd_mailcheck_imap_help {
121  show_help();
122}
123
124
125#
126# Global variables.
127#
128my $handle;
129my ($logged_in, $sleep);
130my ($last_refresh_time, $refresh_tag);
131my ($new_messages, $old_new_messages);
132my ($total_messages, $old_total_messages);
133
134$handle    = 0;
135$logged_in = 0;
136$old_new_messages = -1;
137$old_total_messages = -1;
138
139
140#
141# Subroutine to update status, called every N seconds.
142#
143sub refresh_mailcheck_imap {
144
145  # For now, just print a message and return  :)
146  Irssi::print("update hit.") if Irssi::settings_get_bool('mailcheck_imap_debug');
147
148  # ensure we have details for the login..
149  if(!check_details()) {
150    return 0;
151  }
152
153  if(!$handle) {
154    if(!setup_socket()) {
155      error("Couldn't setup socket to imap server!",0);
156      return 0;
157    }
158  }
159  Irssi::print("Socket is setup.") if Irssi::settings_get_bool('mailcheck_imap_debug');
160
161  if(!$logged_in) {
162    if(!login()) {
163      return 0;
164    }
165  }
166  $new_messages = check_imap("UNSEEN");
167  $total_messages = check_imap("MESSAGES");
168
169  $new_messages = 0 if (! $new_messages);
170  $total_messages = 0 if (! $total_messages);
171
172  if ($new_messages eq "-1" || $total_messages eq "-1") {
173    Irssi::print("check_imap returned an error, no updates.") if Irssi::settings_get_bool('mailcheck_imap_debug');
174  }
175
176  # update statusbar if changed rather than updating every the time...
177  if(($new_messages != $old_new_messages) ||
178     ($total_messages != $old_total_messages)) {
179    update_statusbar_item();
180  }
181
182
183  # TODO : This doesn't work if you get a sequence such as:
184  #        check -> arrive, delete, arrive -> check
185  #        as it is just done on the number of unseen messages and won't know..
186  if(($new_messages > $old_new_messages) &&
187     (Irssi::settings_get_bool('mailcheck_imap_echo_new_in_window'))) {
188    # If set, echo to the current window...
189    my $theme = Irssi::current_theme();
190    my $format = $theme->format_expand("{mailcheck_imap_echo}");
191
192    if ($format) {
193      # use theme-specific look
194      $format = $theme->format_expand("{mailcheck_imap_echo $new_messages $total_messages}", Irssi::EXPAND_FLAG_IGNORE_REPLACES);
195    } else {
196      # use the default look
197      $format = "mailcheck_imap: You have ".$new_messages." new message(s).";
198    }
199
200    print CLIENTCRAP $format;
201  }
202  $old_new_messages = $new_messages;
203  $old_total_messages = $total_messages;
204
205  # Adding new timeout to make sure that this function will be called again
206  if ($refresh_tag) {
207    Irssi::timeout_remove($refresh_tag);
208  }
209  my $time = Irssi::settings_get_int('mailcheck_imap_interval');
210  $refresh_tag = Irssi::timeout_add($time*1000, 'refresh_mailcheck_imap', undef);
211
212  return 1;
213}
214
215
216#
217# Subroutine to setup socket handle.
218#
219sub setup_socket {
220	# Set an alarm in case we can not connect or get hung.  Older versions
221	# the IO::Socket perl module caused errors with the alarm we set before
222	# setting up the socket.  If this program dies in debug mode saying:
223	# "Alarm clock", then you can probably fix it by upgrading your perl
224	# IO module.
225	my ($host,$port);
226
227	$host = Irssi::settings_get_str('mailcheck_imap_host');
228	$port = Irssi::settings_get_int('mailcheck_imap_port');
229
230	# change port number if SSL enabled and original imap port unchanged
231	if($port == 143 && Irssi::settings_get_bool('mailcheck_imap_use_ssl')) {
232	  $port = 993;
233	}
234
235	eval {
236		alarm 30;
237		Irssi::print("mailcheck_imap connecting to mail server...");
238
239		if (Irssi::settings_get_bool('mailcheck_imap_use_ssl')) {
240			Irssi::print("Using ssl...") if Irssi::settings_get_bool('mailcheck_imap_debug');
241			$handle = IO::Socket::SSL->new(Proto           => "tcp",
242			                               SSL_verify_mode => 0x00,
243                                                       PeerAddr        => $host,
244			                               PeerPort        => $port,
245		                               	)
246			or error("Can't connect to port $port on $host: $!",0), return 0;
247		} else {
248			$handle = IO::Socket::INET->new(Proto    => "tcp",
249			                                PeerAddr => $host,
250                                                        PeerPort => $port,
251		                               	)
252			or error("Can't connect to port $port on $host: $!",0), return 0;
253		}
254		$handle->autoflush(1);    # So output gets there right away.
255		Irssi::print("...done");
256		receive();
257		alarm 0;
258	};
259	if ($@) {
260		alarm 0;
261		if ($@ =~ /timeout/) {
262			alarm();
263			return 0;
264		} else {
265			error("$@",0);
266			return 0;
267		}
268	}
269	return 1;
270}
271
272#
273# Subroutine to login to the mailbox.
274#
275sub login {
276  my ($response,$success);
277  my ($user,$password);
278
279
280  $user = Irssi::settings_get_str('mailcheck_imap_user');
281  $password = Irssi::settings_get_str('mailcheck_imap_password');
282
283
284  $logged_in = 0;
285  # Set an alarm in case we can not connect or get hung.  Older versions
286  # the IO::Socket perl module caused errors with the alarm we set before
287  # setting up the socket.  If this program dies in debug mode saying:
288  # "Alarm clock", then you can probably fix it by upgrading your perl
289  # IO module.
290  eval {
291    alarm 30;
292    send_data("A001 LOGIN \"$user\" \"$password\"","\"$user\"");
293    while (1) {
294      ($success,$response) = receive();
295      if (! $success) {
296	return 0;
297      }
298      last if $response =~ /LOGIN|OK/;
299    }
300    if ($response =~ /fail|BAD/) {
301      return 0;
302    } else {
303      $logged_in = 1;
304    }
305    alarm 0;
306  };
307  if ($@) {
308    alarm 0;
309    if ($@ =~ /timeout/) {
310      alarm();
311      return 0;
312    } else {
313      error("$@",0);
314      return 0;
315    }
316  }
317  # Success!  :D
318  return 1;
319}
320
321#
322# Subroutine that does check of imap mailbox.
323#
324sub check_imap {
325  my ($type) = @_;
326
327  #my ($type) = ("MESSAGES");
328
329  my ($response,$success,$num_messages);
330  # Set an alarm in case we can not connect or get hung.  Older versions
331  # the IO::Socket perl module caused errors with the alarm we set before
332  # setting up the socket.  If this program dies in debug mode saying:
333  # "Alarm clock", then you can probably fix it by upgrading your perl
334  # IO module.
335  eval {
336    alarm 30;
337    send_data("A003 STATUS INBOX ($type)");
338    while (1) {
339      ($success,$response) = receive();
340      if (! $success) {
341	return "-1";
342      }
343      last if $response =~ /STATUS\s+.*?\s+\($type/;
344    }
345    ($num_messages) = $response =~ /\($type\s+(\d+)\)/;
346    alarm 0;
347  };
348  if ($@) {
349    alarm 0;
350    if ($@ =~ /timeout/) {
351      alarm();
352      return "-1";
353    } else {
354      error("$@",0);
355      return "-1";
356    }
357  }
358  return $num_messages;
359}
360
361
362#
363# Subroutine to send a line to the imap server.
364# Block everything after $block.
365#
366sub send_data {
367	my ($line,$block) = (@_);
368	print $handle "$line\r\n";
369	$line =~ s/(.*$block).*/$1 ----/ if ($block);
370	Irssi::print("sent: $line") if Irssi::settings_get_bool('mailcheck_imap_debug');
371	return 1;
372}
373
374
375#
376# Subroutine to get a response from the imap server and print.
377# that response if in debug mode.
378#
379sub receive {
380	my ($response,$success);
381	$response = "";
382	$success  = 0;
383	chomp($response = <$handle>);
384	if ($response) {
385		Irssi::print("got: $response") if Irssi::settings_get_bool('mailcheck_imap_debug');
386		$success = 1;
387	} else {
388		Irssi::print("no response!") if Irssi::settings_get_bool('mailcheck_imap_debug');
389	}
390	return ($success,$response);
391}
392
393#
394# Subroutine to display and error message in a text box.
395#
396sub error {
397  my ($error,$fatal) = (@_);
398
399  if ($fatal) {
400    # TODO : Print some useful message and die?
401    Irssi::print("mailcheck_imap FATAL : $error");
402    return 0;
403  } else {
404    Irssi::print("mailcheck_imap error : $error");
405
406    if ($refresh_tag) {
407      Irssi::timeout_remove($refresh_tag)
408    }
409    my $time = Irssi::settings_get_int('mailcheck_imap_interval');
410    $refresh_tag = Irssi::timeout_add($time*1000, 'refresh_mailcheck_imap', undef);
411    $handle = 0;
412    return 0;
413  }
414}
415
416#
417# Subroutine to call when alarm times out.
418#
419sub alarm {
420  Irssi::print("Alarm went off!") if Irssi::settings_get_bool('mailcheck_imap_debug');
421  return 1;
422}
423
424
425#
426# Subroutine to clean up.
427#
428sub cleanup {
429  if ($handle) {
430    send_data("A999 LOGOUT");
431    $handle->close();
432  }
433  Irssi::print("mailcheck_imap logged out.");
434}
435
436
437
438#######################################################################
439# Simply requests a statusbar item redraw.
440
441sub update_statusbar_item {
442  Irssi::statusbar_items_redraw('mailcheck_imap');
443}
444
445
446#######################################################################
447# This is the function called by irssi to obtain the statusbar item.
448
449sub mailcheck_imap {
450  my ($item, $get_size_only) = @_;
451
452  my $theme = Irssi::current_theme();
453  my $format = $theme->format_expand("{sb_mailcheck_imap}");
454
455  if ($format) {
456    # use theme-specific look
457    $format = $theme->format_expand("{sb_mailcheck_imap $new_messages $total_messages}", Irssi::EXPAND_FLAG_IGNORE_REPLACES);
458  } else {
459    # use the default look
460    $format = "{sb Mail: ".$new_messages." new, ".$total_messages." total}";
461  }
462
463  if($new_messages == 0) {
464    if(Irssi::settings_get_bool('mailcheck_imap_show_zero')) {
465      $format = $theme->format_expand("{sb_mailcheck_imap_zero $new_messages $total_messages}", Irssi::EXPAND_FLAG_IGNORE_REPLACES);
466
467      if (!$format) {
468	# use the default look
469	$format = "{sb Mail: None new, ".$total_messages." total}";
470      }
471    } else {
472      $format = "";
473    }
474  }
475
476  if (length($format) == 0) {
477    # nothing to print, so don't print at all
478    if ($get_size_only) {
479      $item->{min_size} = $item->{max_size} = 0;
480    }
481  } else {
482    $item->default_handler($get_size_only, $format, undef, 1);
483  }
484}
485
486
487################################################################################
488# Ensure that all required details are filled in:
489# host, user, password
490sub check_details {
491  my $host = Irssi::settings_get_str('mailcheck_imap_host');
492  my $user = Irssi::settings_get_str('mailcheck_imap_user');
493  my $password = Irssi::settings_get_str('mailcheck_imap_password');
494
495  if(!$host || !$user || !$password) {
496    show_help();
497    return 0;
498  }
499  return 1;
500}
501
502
503################################################################################
504# Immediately check for new mail (updates statusbar item too)
505
506sub cmd_mailcheck_imap {
507  refresh_mailcheck_imap();
508}
509
510
511################################################################################
512# Kill the connection and stop the refresh.
513sub cmd_mailcheck_imap_stop {
514  if ($refresh_tag) {
515    Irssi::timeout_remove($refresh_tag);
516  }
517  cleanup();
518}
519
520# Also close connection on script unload?
521sub sig_command_script_unload ($$$) {
522  my ($script, $server, $witem) = @_;
523
524  if($script =~ /^mailcheck_imap\.pl$/ ||
525     $script =~ /^mailcheck_imap/) {
526    cleanup();
527  }
528}
529
530Irssi::signal_add_first('command script unload', \&sig_command_script_unload);
531
532
533#######################################################################
534# Adding stuff to irssi
535
536Irssi::settings_add_int('mail', 'mailcheck_imap_interval', 120);
537Irssi::settings_add_bool('mail', 'mailcheck_imap_use_ssl', 0);
538Irssi::settings_add_bool('mail', 'mailcheck_imap_debug', 0);
539Irssi::settings_add_bool('mail', 'mailcheck_imap_show_zero', 0);
540Irssi::settings_add_bool('mail', 'mailcheck_imap_echo_new_in_window', 1);
541
542Irssi::settings_add_str('mail', 'mailcheck_imap_host', '');
543Irssi::settings_add_int('mail', 'mailcheck_imap_port', 143);
544Irssi::settings_add_str('mail', 'mailcheck_imap_user', '');
545Irssi::settings_add_str('mail', 'mailcheck_imap_password', '');
546
547
548Irssi::statusbar_item_register('mailcheck_imap', '{sb $0-}', 'mailcheck_imap');
549
550Irssi::command_bind('mailcheck_imap_help','cmd_mailcheck_imap_help');
551Irssi::command_bind('mailcheck_imap','cmd_mailcheck_imap');
552Irssi::command_bind('mailcheck_imap_stop','cmd_mailcheck_imap_stop');
553
554
555#######################################################################
556# Startup functions
557
558# Check that everything is fiiiine and start checking if so
559if(check_details()) {
560  # All is ok, so start running it
561  refresh_mailcheck_imap();
562  update_statusbar_item();
563}
564
565
566#######################################################################
567