summaryrefslogtreecommitdiffstats
path: root/mbsyncloop/.bin/mbsyncloop
diff options
context:
space:
mode:
Diffstat (limited to 'mbsyncloop/.bin/mbsyncloop')
-rwxr-xr-xmbsyncloop/.bin/mbsyncloop312
1 files changed, 312 insertions, 0 deletions
diff --git a/mbsyncloop/.bin/mbsyncloop b/mbsyncloop/.bin/mbsyncloop
new file mode 100755
index 0000000..bb16df1
--- /dev/null
+++ b/mbsyncloop/.bin/mbsyncloop
@@ -0,0 +1,312 @@
+#!/usr/bin/env perl
+use 5.016;
+use strict;
+use warnings;
+
+use File::Path;
+use File::Spec;
+use File::Temp;
+use JSON::PP;
+use POSIX 'mkfifo';
+
+# running a local build until
+# https://gitlab.com/shackra/goimapnotify/-/merge_requests/14 makes it into a
+# release
+my $GOIMAPNOTIFY = "$ENV{HOME}/.bin/local/goimapnotify";
+
+my $RUN_DIR = make_run_dir();
+my ($PW_PID, $GOIMAPNOTIFY_PID);
+
+$SIG{INT} = $SIG{TERM} = sub { cleanup(); exit };
+END { cleanup() }
+
+my $config = "$ENV{HOME}/.config/mbsyncloop/config.json";
+
+main($config);
+
+sub main {
+ my ($config) = @_;
+
+ my $pw_pipe = make_pw_pipe();
+ my $password_command_pipe = "head -n1 '$pw_pipe'";
+
+ my $config_data;
+ if (-f $config) {
+ $config_data = JSON::PP::decode_json(slurp($config));
+ }
+ else {
+ $config_data = {};
+ }
+
+ my $mbsync_config_data = slurp(
+ ($config_data->{mbsync_config} // '~/.mbsyncrc')
+ =~ s{^~/}{$ENV{HOME}/}r
+ );
+ my $password_command = extract_password_command($mbsync_config_data);
+
+ spawn_pw_proc($pw_pipe, $password_command);
+
+ my $goimapnotify_config_data = extract_goimapnotify_config(
+ $config_data,
+ $mbsync_config_data,
+ $password_command_pipe,
+ );
+
+ my $goimapnotify_r = spawn_goimapnotify_proc(
+ $goimapnotify_config_data,
+ );
+
+ my $generated_mbsync_config_file = write_mbsync_config(
+ $mbsync_config_data,
+ $goimapnotify_config_data->{boxes},
+ $password_command_pipe,
+ );
+
+ loop(
+ $config_data,
+ $generated_mbsync_config_file,
+ $goimapnotify_r,
+ );
+}
+
+sub make_pw_pipe {
+ my $file = File::Spec->catfile($RUN_DIR, "mbsyncloop");
+ unlink($file);
+ mkfifo($file, 0700) or die "couldn't create $file: $!";
+ $file
+}
+
+sub extract_goimapnotify_config {
+ my ($config_data, $mbsync_config_data, $password_command) = @_;
+
+ (my $host) = $mbsync_config_data =~ /^Host (.*)$/m;
+ (my $port) = $mbsync_config_data =~ /^Port (.*)$/m;
+ (my $user) = $mbsync_config_data =~ /^User (.*)$/m;
+
+ my $tls;
+ if ($mbsync_config_data =~ /SSLType\s+IMAPS/) {
+ $tls = JSON::PP::true;
+ $port //= 993;
+ }
+ else {
+ $tls = JSON::PP::false;
+ $port //= 143;
+ }
+
+ my $goimapnotify_config = {
+ host => $host,
+ port => $port,
+ tls => $tls,
+ username => $user,
+ passwordCmd => $password_command,
+ };
+
+ my @mailboxes;
+ if ($config_data->{boxes}) {
+ @mailboxes = @{ $config_data->{boxes} };
+ }
+ else {
+ @mailboxes = read_mailboxes($goimapnotify_config);
+ if ($config_data->{box_patterns}) {
+ @mailboxes = grep {
+ my $mailbox = $_;
+ grep {
+ $mailbox =~ /$_/
+ } @{ $config_data->{box_patterns} }
+ } @mailboxes
+ }
+ }
+
+ $goimapnotify_config->{onNewMail} = "echo new";
+ $goimapnotify_config->{boxes} = \@mailboxes;
+
+ $goimapnotify_config
+}
+
+sub read_mailboxes {
+ my ($config) = @_;
+
+ my $tmp = File::Temp->new(DIR => $RUN_DIR);
+ $tmp->print(JSON::PP::encode_json($config));
+ $tmp->flush;
+ open my $fh, '-|', $GOIMAPNOTIFY, '--conf', $tmp->filename, '--list'
+ or die "couldn't run goimapnotify: $!";
+ <$fh>;
+
+ map { chomp; s/^[^ ]* //r } <$fh>
+}
+
+sub extract_password_command {
+ my ($mbsync_config) = @_;
+ (my $password_command) = $mbsync_config =~ /^PassCmd "(.*)"$/m;
+ $password_command
+}
+
+sub spawn_pw_proc {
+ my ($pw_pipe, $password_command) = @_;
+
+ my $pw = fetch_password($password_command);
+
+ $PW_PID = fork;
+ die "fork failed: $!" unless defined $PW_PID;
+ if (!$PW_PID) {
+ $SIG{PIPE} = 'IGNORE';
+ setpgrp(0, 0);
+ while (1) {
+ open my $fh, '>', $pw_pipe or die "couldn't open $pw_pipe";
+ $fh->print("$pw\n");
+ close $fh;
+ }
+ }
+}
+
+sub fetch_password {
+ my ($password_command) = @_;
+ my $pw = `$password_command`;
+ die "failed to fetch password: command returned $?" if $?;
+ $pw
+}
+
+sub spawn_goimapnotify_proc {
+ my ($config) = @_;
+
+ pipe(my $goimapnotify_r, my $goimapnotify_w)
+ or die "failed to create unnamed pipe: $!";
+
+ $GOIMAPNOTIFY_PID = fork;
+ die "fork failed: $!" unless defined $GOIMAPNOTIFY_PID;
+ if (!$GOIMAPNOTIFY_PID) {
+ setpgrp(0, 0);
+ close $goimapnotify_r;
+
+ my $tmp = File::Temp->new(DIR => $RUN_DIR);
+ $tmp->print(JSON::PP::encode_json($config));
+ $tmp->flush;
+
+ while (1) {
+ open my $fh, '-|', $GOIMAPNOTIFY, '--conf', $tmp->filename
+ or die "couldn't run goimapnotify: $!";
+ while (<$fh>) {
+ $goimapnotify_w->print("N\n");
+ $goimapnotify_w->flush;
+ }
+ }
+ }
+ close $goimapnotify_w;
+
+ $goimapnotify_r
+}
+
+sub write_mbsync_config {
+ my ($mbsync_config, $mailboxes, $password_command) = @_;
+
+ $mbsync_config =~ s/^PassCmd .*$/PassCmd "$password_command"/m;
+ my ($far) = $mbsync_config =~ /^IMAPStore (.*)$/m;
+ my ($near) = $mbsync_config =~ /^MaildirStore (.*)$/m;
+
+ my $patterns = join "\n", map { "Pattern $_" } @$mailboxes;
+ my $mbsync_channels = <<EOF;
+Channel mbsyncloop_priority
+Far :$far:
+Near :$near:
+Sync Pull Push
+$patterns
+
+Channel mbsyncloop_all
+Far :$far:
+Near :$near:
+Sync Pull Push
+Patterns *
+EOF
+
+ my $tmp = File::Temp->new(DIR => $RUN_DIR);
+ $tmp->print($mbsync_config);
+ $tmp->print("\n");
+ $tmp->print($mbsync_channels);
+ $tmp->flush;
+
+ $tmp
+}
+
+sub loop {
+ my ($config_data, $mbsync_config, $goimapnotify_r) = @_;
+
+ my $poll_interval = $config_data->{poll_interval} // 15 * 60;
+ my $last_all = 0;
+ $SIG{HUP} = sub { $last_all = 0 };
+ while (1) {
+ my $now = time;
+ if (($now - $last_all) >= $poll_interval) {
+ sync(
+ $mbsync_config,
+ "mbsyncloop_all",
+ $config_data->{on_new_mail},
+ );
+ $last_all = $now;
+ }
+ if (idle($goimapnotify_r, $poll_interval)) {
+ sync(
+ $mbsync_config,
+ "mbsyncloop_priority",
+ $config_data->{on_new_mail},
+ );
+ }
+ }
+}
+
+sub sync {
+ my ($config, $channel, $on_new_mail) = @_;
+ my $config_file = $config->filename;
+ while (1) {
+ my $status = system("mbsync -c '$config_file' $channel");
+ if (!$status) {
+ system($on_new_mail) if defined $on_new_mail;
+ last;
+ }
+ sleep 5;
+ }
+}
+
+sub idle {
+ my ($goimapnotify_r, $max_delay) = @_;
+ my $rin = '';
+ vec($rin, fileno($goimapnotify_r), 1) = 1;
+ my $ready = select(my $rout = $rin, undef, undef, $max_delay);
+ return 0 if $ready == -1 && $! == POSIX::EINTR;
+ die "failed to read goimapnotify output: $!" if $ready == -1;
+ if ($ready) {
+ while (1) {
+ my $ready = select(my $rout = $rin, undef, undef, 0.01);
+ return 0 if $ready == -1 && $! == POSIX::EINTR;
+ die "failed to read goimapnotify output: $!" if $ready == -1;
+ last unless $ready;
+ sysread $goimapnotify_r, my $data, 4096;
+ }
+ return 1;
+ }
+ return 0;
+}
+
+sub slurp {
+ my ($file) = @_;
+ local $/;
+ open my $fh, '<', $file or die "couldn't open $file: $!";
+ <$fh>
+}
+
+sub make_run_dir {
+ my $dir = "/run/user/$>";
+ if (!-d $dir) {
+ $dir = File::Spec->tmpdir();
+ }
+ unlink File::Spec->catfile($dir, "mbsyncloop");
+ mkdir File::Spec->catfile($dir, "mbsyncloop");
+ mkdir File::Spec->catfile($dir, "mbsyncloop", $$);
+ File::Spec->catfile($dir, "mbsyncloop", $$)
+}
+
+sub cleanup {
+ kill KILL => -$PW_PID if $PW_PID;
+ kill KILL => -$GOIMAPNOTIFY_PID if $GOIMAPNOTIFY_PID;
+ File::Path::remove_tree($RUN_DIR) if $RUN_DIR;
+}