summaryrefslogtreecommitdiffstats
path: root/mailer.pl
diff options
context:
space:
mode:
Diffstat (limited to 'mailer.pl')
-rw-r--r--mailer.pl155
1 files changed, 155 insertions, 0 deletions
diff --git a/mailer.pl b/mailer.pl
new file mode 100644
index 0000000..c6c1af2
--- /dev/null
+++ b/mailer.pl
@@ -0,0 +1,155 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use utf8;
+use Time::Piece;
+use Sys::Syslog qw(:standard :macros);
+use OpenBSD::Pledge;
+use OpenBSD::Unveil;
+
+my $dict_dir = '/var/www/var/lex/dict';
+my $state_file = '/var/www/var/lex/mailer.tsv';
+my $to = 'me@example.com';
+my $from = 'Lex <lex@example.com>';
+my $subject = 'Daily word';
+my $sendmail = '/usr/sbin/sendmail';
+my $syslog_tag = 'lex-mail';
+
+# Connect to /dev/log before pledge/unveil so no late 'unix'
+# socket creation occurs
+openlog($syslog_tag, 'ndelay,pid', LOG_MAIL);
+
+unveil($dict_dir, 'r') or log_die("unveil $dict_dir: $!");
+unveil($state_file, 'rwc') or log_die("unveil $state_file: $!");
+unveil($sendmail, 'rx') or log_die("unveil $sendmail: $!");
+unveil('/etc/mailer.conf', 'r') or log_die("unveil /etc/mailer.conf: $!");
+unveil('/var/run/smtpd.sock', 'rw') or log_die("unveil smtpd.sock: $!");
+unveil('/dev/log', 'w') or log_die("unveil /dev/log: $!"); # Writing to pre-opened socket handle
+unveil(); # Lock unveil rules
+
+pledge(qw(stdio rpath wpath cpath proc exec)) or log_die("pledge: $!");
+
+sub log_die {
+ my ($msg) = @_;
+ syslog(LOG_ERR, "%s", $msg);
+ closelog();
+ die $msg;
+}
+
+# Read dictionary files
+if (!opendir(my $dh, $dict_dir)) {
+ log_die("Cannot open $dict_dir: $!");
+}
+my @current_files = grep { -f "$dict_dir/$_" && !/^\./ } readdir($dh);
+closedir($dh);
+
+if (!@current_files) {
+ syslog(LOG_WARNING, "No files found in %s", $dict_dir);
+ closelog();
+ exit 0;
+}
+
+my %active_files = map { $_ => 1 } @current_files;
+my %state;
+my $today = localtime->ymd;
+
+if (-f $state_file) {
+ if (!open(my $sfh, '<', $state_file)) {
+ log_die("Cannot open $state_file: $!");
+ }
+ while (my $line = <$sfh>) {
+ chomp $line;
+ next if $line =~ /^\s*$/;
+ my ($file, $next_due, $interval, $reviews, $factor) = split(/\t/, $line);
+
+ if ($file && $active_files{$file}) {
+ $state{$file} = {
+ next_due => $next_due // $today,
+ interval => $interval // 1,
+ reviews => $reviews // 0,
+ factor => $factor // 2.5,
+ };
+ }
+ }
+ close($sfh);
+}
+
+# Initialize new files not yet in tsv
+foreach my $file (@current_files) {
+ if (!$state{$file}) {
+ $state{$file} = {
+ next_due => $today,
+ interval => 1,
+ reviews => 0,
+ factor => 2.5,
+ };
+ }
+}
+
+# Select candidate file (spaced repetition)
+my @due_files = grep { $state{$_}{next_due} le $today } @current_files;
+
+my $selected_file;
+if (@due_files) {
+ $selected_file = $due_files[rand @due_files];
+} else {
+ @current_files = sort { $state{$a}{next_due} cmp $state{$b}{next_due} } @current_files;
+ $selected_file = $current_files[0];
+ syslog(LOG_INFO, "No files due today; falling back to nearest due file: %s", $selected_file);
+}
+
+# Read content
+my $file_path = "$dict_dir/$selected_file";
+if (!open(my $fh, '<:utf8', $file_path)) {
+ log_die("Cannot open $file_path: $!");
+}
+local $/; # slurp mode
+my $body = <$fh>;
+close($fh);
+
+# Send email via direct pipe
+if (!open(my $mail_pipe, '|-', $sendmail, '-i', '-f', $from, $to)) {
+ log_die("Cannot execute $sendmail: $!");
+}
+binmode($mail_pipe, ':utf8');
+
+print $mail_pipe "From: $from\n";
+print $mail_pipe "To: $to\n";
+print $mail_pipe "Subject: $subject - $selected_file\n";
+print $mail_pipe "Content-Type: text/plain; charset=UTF-8\n\n";
+print $mail_pipe $body;
+
+if (!close($mail_pipe)) {
+ log_die("Error closing pipe to sendmail: $!");
+}
+
+# Update schedule
+my $item = $state{$selected_file};
+my $interval = $item->{interval};
+my $factor = $item->{factor};
+
+if ($item->{reviews} == 0) {
+ $interval = 1;
+} elsif ($item->{reviews} == 1) {
+ $interval = 6;
+} else {
+ $interval = int($interval * $factor);
+}
+
+$item->{interval} = $interval;
+$item->{reviews} += 1;
+
+my $now = localtime;
+$item->{next_due} = ($now + ($interval * 86400))->ymd;
+
+if (!open(my $out_fh, '>', $state_file)) {
+ log_die("Cannot write to $state_file: $!");
+}
+foreach my $file (keys %state) {
+ my $i = $state{$file};
+ print $out_fh join("\t", $file, $i->{next_due}, $i->{interval}, $i->{reviews}, $i->{factor}), "\n";
+}
+close($out_fh);
+
+syslog(LOG_INFO, "Mailed %s to %s (next due: %s)", $selected_file, $to, $item->{next_due});
+closelog();