1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
|
#!/usr/bin/perl -w
# Copyright (C) all contributors <meta@public-inbox.org>
# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
use strict;
use Sendmail::PMilter qw(:all);
use IO::Socket;
use Crypt::CBC;
use MIME::Base64 qw(encode_base64url);
my $key_file = shift @ARGV or die "Usage: $0 KEY_FILE\n";
open my $fh, '<', $key_file or die "failed to open $key_file\n";
my ($key, $iv);
if (read($fh, $key, 8) != 8 || read($fh, $iv, 8) != 8 ||
read($fh, my $end, 8) != 0) {
die "KEY_FILE must be 16 bytes\n";
}
# optionally support unique mailto: subject in List-Unsubscribe,
# requires a custom rule in front of mlmmj, see __END__
my $unique_mailto = $ENV{UNIQUE_MAILTO};
# these parameters were chosen to generate shorter parameters
# to reduce the possibility of copy+paste errors
my $crypt = Crypt::CBC->new(-key => $key,
-iv => $iv,
-header => 'none',
-cipher => 'Blowfish');
$fh = $iv = $key = undef;
my %cbs;
$cbs{connect} = sub {
my ($ctx) = @_;
eval { $ctx->setpriv({ header => {}, envrcpt => {} }) };
warn $@ if $@;
SMFIS_CONTINUE;
};
$cbs{envrcpt} = sub {
my ($ctx, $addr) = @_;
eval {
$addr =~ tr!<>!!d;
$ctx->getpriv->{envrcpt}->{$addr} = 1;
};
warn $@ if $@;
SMFIS_CONTINUE;
};
$cbs{header} = sub {
my ($ctx, $k, $v) = @_;
eval {
my $k_ = lc $k;
if ($k_ eq 'list-unsubscribe') {
my $header = $ctx->getpriv->{header} ||= {};
my $ary = $header->{$k_} ||= [];
# we create placeholders in case there are
# multiple headers of the same name
my $cur = [];
push @$ary, $cur;
# This relies on mlmmj convention:
# $LIST+unsubscribe@$DOMAIN
if ($v =~ /\A<mailto:([^@]+)\+unsubscribe@([^>]+)>\z/) {
@$cur = ($k, $v, $1, $2);
# Mailman convention:
# $LIST-request@$DOMAIN?subject=unsubscribe
} elsif ($v =~ /\A<mailto:([^@]+)-request@
([^\?]+)\?subject=unsubscribe>\z/x) {
# @$cur = ($k, $v, $1, $2);
}
}
};
warn $@ if $@;
SMFIS_CONTINUE;
};
# We don't want people unsubscribing archivers:
sub archive_addr {
my ($addr) = @_;
return 1 if ($addr =~ /\@m\.gmane(?:-mx)?\.org\z/);
return 1 if ($addr eq 'archive@mail-archive.com');
0
}
$cbs{eom} = sub {
my ($ctx) = @_;
eval {
my $priv = $ctx->getpriv;
$ctx->setpriv({ header => {}, envrcpt => {} });
my @rcpt = keys %{$priv->{envrcpt}};
# one recipient, one unique HTTP(S) URL
return SMFIS_CONTINUE if @rcpt != 1;
return SMFIS_CONTINUE if archive_addr(lc($rcpt[0]));
my $unsub = $priv->{header}->{'list-unsubscribe'} || [];
my $n = 0;
my $added;
foreach my $u (@$unsub) {
# Milter indices are 1-based,
# not 0-based like Perl arrays
my $index = ++$n;
my ($k, $v, $list, $domain) = @$u;
next unless $k && $v && $list && $domain;
my $u = $crypt->encrypt($rcpt[0]);
$u = encode_base64url($u);
if ($unique_mailto) {
# $u needs to be in the Subject: header since
# +$EXTENSION is case-insensitive
my $s = "subject=$u";
$v = "<mailto:$list+unique-unsub\@$domain?$s>";
}
$v .= ",\n <https://$domain/u/$u/$list>";
$ctx->chgheader($k, $index, $v);
$added = 1;
}
# RFC 8058
$added and $ctx->addheader('List-Unsubscribe-Post',
'List-Unsubscribe=One-Click');
};
warn $@ if $@;
SMFIS_CONTINUE;
};
my $milter = Sendmail::PMilter->new;
# Try to inherit a socket from systemd or similar:
my $fds = $ENV{LISTEN_FDS};
if ($fds && (($ENV{LISTEN_PID} || 0) == $$)) {
die "$0 can only listen on one FD\n" if $fds != 1;
my $start_fd = 3;
my $s = IO::Socket->new_from_fd($start_fd, 'r') or
die "inherited bad FD from LISTEN_FDS: $!\n";
$milter->set_socket($s);
} else {
# fall back to binding a socket:
my $sock = 'unix:/var/spool/postfix/unsubscribe/unsubscribe.sock';
$milter->set_listen(1024);
my $umask = umask 0000;
$milter->setconn($sock);
umask $umask;
}
$milter->register('unsubscribe', \%cbs, SMFI_CURR_ACTS);
$milter->main();
__END__
# TMPMSG comes from dc-dlvr, it's populated before the above runs:
# TMPMSG=$(mktemp -t dc-dlvr.orig.$USER.XXXXXX || exit 1)
# cat >$TMPMSG
# I use something like this in front of mlmmj for UNIQUE_MAILTO
# $EXTENSION and $ORIGINAL_RECIPIENT are set by postfix, $list
# is a local mapping of addresses to mailing list names.
case $ORIGINAL_RECIPIENT in
foo+*) list=foo ;;
# ...
esac
case $EXTENSION in
unique-unsub)
u="$(formail -z -c -x Subject <$TMPMSG)"
d=$(expr "$ORIGINAL_RECIPIENT" : '^.*@\(.*\)')
# forward this to the unsubscribe.psgi service
curl -sSf https://$d/u/$u/$list >/dev/null
exit
;;
esac
/usr/bin/mlmmj-receive -L /path/to/mlmmj-spool/$list <"$TMPMSG"
exit
|