2 # Copyright (C) 2016-2020 all contributors <meta@public-inbox.org>
3 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
6 use Sendmail::PMilter qw(:all);
9 use MIME::Base64 qw(encode_base64url);
11 my $key_file = shift @ARGV or die "Usage: $0 KEY_FILE\n";
12 open my $fh, '<', $key_file or die "failed to open $key_file\n";
14 if (read($fh, $key, 8) != 8 || read($fh, $iv, 8) != 8 ||
15 read($fh, my $end, 8) != 0) {
16 die "KEY_FILE must be 16 bytes\n";
19 # optionally support unique mailto: subject in List-Unsubscribe,
20 # requires a custom rule in front of mlmmj, see __END__
21 my $unique_mailto = $ENV{UNIQUE_MAILTO};
23 # these parameters were chosen to generate shorter parameters
24 # to reduce the possibility of copy+paste errors
25 my $crypt = Crypt::CBC->new(-key => $key,
28 -cipher => 'Blowfish');
29 $fh = $iv = $key = undef;
34 eval { $ctx->setpriv({ header => {}, envrcpt => {} }) };
40 my ($ctx, $addr) = @_;
43 $ctx->getpriv->{envrcpt}->{$addr} = 1;
50 my ($ctx, $k, $v) = @_;
53 if ($k_ eq 'list-unsubscribe') {
54 my $header = $ctx->getpriv->{header} ||= {};
55 my $ary = $header->{$k_} ||= [];
57 # we create placeholders in case there are
58 # multiple headers of the same name
62 # This relies on mlmmj convention:
63 # $LIST+unsubscribe@$DOMAIN
64 if ($v =~ /\A<mailto:([^@]+)\+unsubscribe@([^>]+)>\z/) {
65 @$cur = ($k, $v, $1, $2);
68 # $LIST-request@$DOMAIN?subject=unsubscribe
69 } elsif ($v =~ /\A<mailto:([^@]+)-request@
70 ([^\?]+)\?subject=unsubscribe>\z/x) {
71 # @$cur = ($k, $v, $1, $2);
79 # We don't want people unsubscribing archivers:
82 return 1 if ($addr =~ /\@m\.gmane(?:-mx)?\.org\z/);
83 return 1 if ($addr eq 'archive@mail-archive.com');
90 my $priv = $ctx->getpriv;
91 $ctx->setpriv({ header => {}, envrcpt => {} });
92 my @rcpt = keys %{$priv->{envrcpt}};
94 # one recipient, one unique HTTP(S) URL
95 return SMFIS_CONTINUE if @rcpt != 1;
96 return SMFIS_CONTINUE if archive_addr(lc($rcpt[0]));
98 my $unsub = $priv->{header}->{'list-unsubscribe'} || [];
100 foreach my $u (@$unsub) {
101 # Milter indices are 1-based,
102 # not 0-based like Perl arrays
104 my ($k, $v, $list, $domain) = @$u;
106 next unless $k && $v && $list && $domain;
107 my $u = $crypt->encrypt($rcpt[0]);
108 $u = encode_base64url($u);
109 if ($unique_mailto) {
110 # $u needs to be in the Subject: header since
111 # +$EXTENSION is case-insensitive
112 my $s = "subject=$u";
113 $v = "<mailto:$list+unique-unsub\@$domain?$s>";
115 $v .= ",\n <https://$domain/u/$u/$list>";
117 $ctx->chgheader($k, $index, $v);
124 my $milter = Sendmail::PMilter->new;
126 # Try to inherit a socket from systemd or similar:
127 my $fds = $ENV{LISTEN_FDS};
128 if ($fds && (($ENV{LISTEN_PID} || 0) == $$)) {
129 die "$0 can only listen on one FD\n" if $fds != 1;
131 my $s = IO::Socket->new_from_fd($start_fd, 'r') or
132 die "inherited bad FD from LISTEN_FDS: $!\n";
133 $milter->set_socket($s);
135 # fall back to binding a socket:
136 my $sock = 'unix:/var/spool/postfix/unsubscribe/unsubscribe.sock';
137 $milter->set_listen(1024);
138 my $umask = umask 0000;
139 $milter->setconn($sock);
143 $milter->register('unsubscribe', \%cbs, SMFI_CURR_ACTS);
146 # TMPMSG comes from dc-dlvr, it's populated before the above runs:
147 # TMPMSG=$(mktemp -t dc-dlvr.orig.$USER.XXXXXX || exit 1)
150 # I use something like this in front of mlmmj for UNIQUE_MAILTO
151 # $EXTENSION and $ORIGINAL_RECIPIENT are set by postfix, $list
152 # is a local mapping of addresses to mailing list names.
153 case $ORIGINAL_RECIPIENT in
160 u="$(formail -z -c -x Subject <$TMPMSG)"
161 d=$(expr "$ORIGINAL_RECIPIENT" : '^.*@\(.*\)')
163 # forward this to the unsubscribe.psgi service
164 curl -sSf https://$d/u/$u/$list >/dev/null
168 /usr/bin/mlmmj-receive -L /path/to/mlmmj-spool/$list <"$TMPMSG"