summaryrefslogtreecommitdiffstats
path: root/dmarc_milter.pl
blob: ba638d5e16791f78f94b242cefe85f8a6245f328 (plain)
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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
#!/usr/bin/perl

use strict;
use warnings;
use feature 'say';

no warnings 'experimental::signatures';
use feature 'signatures';

use Find::Lib '.';

use Getopt::Long;
use Pod::Usage;
use Sendmail::PMilter;
use Spline::DMARC qw(check_addresses);
use Spline::Data;
use Spline::Log qw(set_verbose set_stderr debug info);


# This is the mainloop. This method will not exit before shutdown of
# the milter interface.
sub main($listen, $mailman, $message) {

    my $milter = new Sendmail::PMilter;

    umask(0002);
    $milter->setconn($listen);

    # Setup the callbacks
    $milter->register('dmarc_milter', {
        'envfrom' => sub($ctx, $from, @) {
            # We only need this callback to initialize the private
            # data and the logging context.
            my $data = Spline::Data->new($ctx);
            $data->set('counter', 0);

            debug "MAIL FROM: $from";
            return Sendmail::PMilter::SMFIS_CONTINUE;
        },

        'envrcpt' => sub($ctx, $rcpt_to, @) {
            my $data = Spline::Data->load($ctx);
            debug "RCPT TO: $rcpt_to";

            my $next_hop = $ctx->getsymval('{rcpt_host}');
            if ($next_hop eq $mailman) {
                info "Mailinglist address: $rcpt_to";
                $data->set('counter', 1);
            }

            return Sendmail::PMilter::SMFIS_CONTINUE;
        },

        'header' => sub($ctx, $name, $value) {
            my $data = Spline::Data->load($ctx);

            # If there was no Mailinglist address, we can simply
            # accept this mail and skip all following callbacks
            return Sendmail::PMilter::SMFIS_ACCEPT if $data->get('counter') == 0;

            debug "HEADER '$name': $value";
            if (lc($name) eq 'from') {
                my $reject = check_addresses($value);
                if ($reject) {
                    info 'Rejecting mail!';
                    $ctx->setreply('550', '5.7.2', $message);

                    # REJECT here. No more callbacks, are called for
                    # this message.
                    return Sendmail::PMilter::SMFIS_REJECT;
                }
            }

            return Sendmail::PMilter::SMFIS_CONTINUE;
        },

        'eoh' => sub($ctx) {
            my $data = Spline::Data->load($ctx);
            debug 'END OF HEADER';

            # If we did not reject this message during the headers, we
            # can now accept it and do not call anymore callbacks for
            # this message.
            return Sendmail::PMilter::SMFIS_ACCEPT;
        },

        'close' => sub($ctx) {
            my $data = Spline::Data->load($ctx);
            return Sendmail::PMilter::SMFIS_CONTINUE unless defined $data->get('counter');

            # Free the connection-private memory.
            $ctx->setpriv(undef);

            debug 'CLOSE';
            return Sendmail::PMilter::SMFIS_CONTINUE;
        },
    });

    # Use prefork dispatcher (should be faster then the default
    # postfork variant)
    $milter->set_dispatcher(Sendmail::PMilter::prefork_dispatcher(
        "max_children" => 30,
        "max_requests_per_child" => 100,
    ));

    # Start the mainloop:
    $milter->main();
}

my $help;
my $verbose;
my $stderr;
my $message = 'Your provider does not permit sending to ' .
              'mailing lists (DMARC policy)';
my $mailman = '[lists.spline.inf.fu-berlin.de]';

# work on options
GetOptions(
    "verbose|v"   => \$verbose,
    "help|h|?"    => \$help,
    "stderr|s"    => \$stderr,
    "message|r=s" => \$message,
    "mailman|m=s" => \$mailman,
);

# show help
if ($help) {
    if ($verbose) {
        pod2usage(-verbose => 2,
                  -noperldoc => 1);
    }
    else {
        pod2usage();
    }

    exit;
}

# check argument count
if ($#ARGV < 0) {
    say STDERR 'SOCKET, PORT or CONNECTION_INFO required!';
    pod2usage();
    exit;
}

# Setup logging
if ($verbose) {
    set_verbose(1);
}

if ($stderr) {
    set_stderr(1);
}

# Build the connection info if only a PORT or Path is given
my $arg = shift;
my $listen;
if ($arg =~ /^\d+$/ ) {
    $listen = "inet:$arg\@localhost";
}
elsif ($arg =~ /^\//) {
    $listen = "local:$arg";
}
else {
    $listen = "$arg";
}

# Start the mainloop
info "Listening on $listen..." if $stderr;
main($listen, $mailman, $message);

__END__

=head1 NAME

dmarc_milter.pl - Milter to check if a mailinglist mail would be rejected because of DMARC.

=head1 SYNOPSIS

dmarc_milter [options] ( SOCKET | PORT | CONNECTION_INFO )

 Options:
   --help|-h|-?       show usage info and exit
   --verbose|-v       enable more output (even for --help)

   --stderr|-s        log to stderr
   --mailman|-m HOST  specify an alternativ mailman host
   --message|-r MSG   specify an alternativ reject message

You have to specify where the milter should listen for connections
from your MTA. You can specify a single TCP port (on localhost) or an
absolute path to a socket file. If you have special requirements you
could specify a full connection info string. The format is described
in the Milter documentation. Some examples are
C<local:/var/run/f1.sock>, C<inet6:999@localhost>,
C<inet:3333@localhost>.

(Note: The format of the connection string in the postfix config is
different.)

=head1 DESCRIPTION

B<dmarc_milter.pl> is a Perl script that listen on a socket or tcp
port and retrieves requests from a MTA via the milter protocol
(originaly by sendmail).

The script will scan all emails and check if there are destination
addresses "Envelope To" of mailinglists. If there is at least one, all
the milter will check if any address in the "From" header has
specified a DMARC reject policy.

Such mail would be bounced by all MTAs that respect DMARC, after
mailman resends the message.

=head1 OPTIONS

=over 8

=item B<--verbose>, B<-v>

More verbose output. It will log DEBUG messages, too.

=item B<--help>, B<-h>, B<-?>

Print brief help message and exit.

=item B<--stderr>, B<-s>

Log to stderr instead of syslog.

=item B<--mailman>, B<-m> C<HOST>

Set the mailman host to the specified value. This is the value of the
I<{rtpc_host}> macro for the mailinglist mails. It should be in the
same format as set by the MTA. The default value is:
C<[lists.spline.inf.fu-berlin.de]>.

=item B<--message>, B<-r> C<MESSAGE>

Set the message, if rejecting a mail. The default messages is: "Your
provider does not permit sending to mailing lists (DMARC policy)."

=back

=head1 AUTHORS

Alexander Sulfrian <alex@spline.inf.fu-berlin.de>

=head1 SEE ALSO

Sendmail::Milter(3pm), postconf(7)

=cut

# vim: set et tabstop=4 tw=70: