File: amavisd-custom.conf

package info (click to toggle)
amavisd-new 1:2.11.0-6.1
  • links: PTS, VCS
  • area: main
  • in suites: buster, sid
  • size: 12,432 kB
  • sloc: perl: 33,770; sh: 523; sql: 158; makefile: 8
file content (305 lines) | stat: -rw-r--r-- 13,546 bytes parent folder | download | duplicates (4)
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
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
package Amavis::Custom;
use strict;
use warnings;
no warnings qw(uninitialized redefine);

# Example use of custom hooks, available since amavisd-new-2.5.0

# This code can be placed directly at end of file amavisd.conf,
# or invoked from there by a call to include_config_files such as:
#   include_config_files('/etc/amavisd-custom.conf');
# or specified on amavisd command line by using additional -c options.
#
# It replaces dummy hooks in package Amavis::Custom (in file amavisd)
# with replacement subroutines of the same name, and thus enable them.
#
# The code below demonstrates obtaining and displaying some of the more
# interesting information on each passing mail, and inserting some custom
# header fields in passed mail.
# The example below also illustrates how to use existing code in amavisd
# to interface with a SQL database server (e.g. MySQL or PostgreSQL),
# allowing for persistent connections and automatic reconnect in case
# of a connection failure.
#
# Modifying recipient address, sending a copy to a mailbox quarantine,
# or creating and sending a short notification alert is illustrated.


#testing database:
# $ mysqladmin create user_presence
# $ mysql user_presence
# CREATE TABLE users (
#   email   varchar(255) NOT NULL UNIQUE,
#   present char(1)
# );
# INSERT INTO users VALUES ('test@example.com',       'Y');
# INSERT INTO users VALUES ('absent@example.com',     'N');
# INSERT INTO users VALUES ('postmaster@example.com', 'Y');


# replaces placeholder routines in Amavis::Custom with actual code

use DBI qw(:sql_types);
use DBD::mysql;
BEGIN {
  import Amavis::Conf qw(:platform :confvars c cr ca $myhostname);
  import Amavis::Util qw(do_log untaint safe_encode safe_decode);
  import Amavis::rfc2821_2822_Tools;
  import Amavis::Notify qw(build_mime_entity);
}

# MAIL PROCESSING SEQUENCE:
#
# child process initialization
# loop for each mail:
#   receive mail, parse and make available some basic information
#  *custom hook: new() - may inspect info, may load policy banks
#   mail checking and collecting results
#  *custom hook: checks() - called after virus and spam checks but before
#     taking decisions what to do with mail; may inspect or modify results
#   deciding mail fate (lookup on *_lovers, thresholds, ...)
#   quarantining
#   sending notifications (to admin and recip)
#  *custom hook: before_send() - may send other notif., quarantine, modify mail
#   forwarding (unless blocked)
#  *custom hook: after_send() - may suppress DSN, send reports, quarantine
#   sending delivery status notification (if needed)
#   issue main log entry, manage statistics (timing, counters, nanny)
#  *custom hook: mail_done() - may inspect results
# endloop after $max_requests or earlier

# invoked at child process creation time;
# return an object, or just undef when custom checks are not needed
sub new {
  my($class,$conn,$msginfo) = @_;
  my($self) = bless {}, $class;
  my($conn_h) = Amavis::Out::SQL::Connection->new(
    ['DBI:mysql:database=user_presence;host=127.0.0.1', 'user1', 'passwd1'] );
  $self->{'conn_h'} = $conn_h;
  $self;  # returning an object activates further callbacks,
          # returning undef disables them
}

#sub checks {  # may be left out if not needed
#  my($self,$conn,$msginfo) = @_;
#}

sub before_send {
  my($self,$conn,$msginfo) = @_;
  # $self    ... whatever was returned by new()
  # $conn    ... object with information about a SMTP connection
  # $msginfo ... object with info. about a mail message being processed

  my($ll) = 2;  # log level (0 is the most important level, 1, 2,... 5 less so)
  do_log($ll,"CUSTOM: new message");

  # examine some data pertaining to the SMTP connection from client
  # See methods in Amavis::In::Connection for the full set of available data.
  #
  # SMTP client's IP address as a string (IPv4 or IPv6)
  my($client_ip) = $msginfo->client_addr;
  # does client IP address match @mynetworks_maps? (boolean)
  my($is_client_ip_internal) = $msginfo->client_addr_mynets;
  do_log($ll,"CUSTOM: [%s], is internal IP: %s, %s",
           $client_ip, $is_client_ip_internal ? 'YES' : 'NO',
           $msginfo->originating ? 'ORIGINATING' : 'incoming');

  # examine some data pertaining to the message as a whole (not per-pecipient)
  # See methods in Amavis::In::Message for the full set of available data.
  #
  my($log_id)  = $msginfo->log_id;  # log ID string, e.g. '48262-21-2'
  my($mail_id) = $msginfo->mail_id; # long-term unique id, e.g. 'yxqmZgS+M09R'
  my($sender)  = $msginfo->sender;  # envelope sender address, e.g. 'usr@e.com'
  my($mail_size) = $msginfo->msg_size;   # mail size in bytes
  my($spam_level)= $msginfo->spam_level; # spam level (without per-recip boost)
  do_log($ll,"CUSTOM: %d bytes, score: %.2f",
           $log_id,$mail_id,$mail_size,$spam_level);
  do_log($ll,"CUSTOM: Return-Path (env. sender): <%s>", $sender);
  my($sigs_ref) = $msginfo->dkim_signatures_valid;
  do_log($ll,"CUSTOM: dkim valid, d=%s", join(',', map {$_->domain} @$sigs_ref)
        )  if defined $sigs_ref && @$sigs_ref;

  # full mail is only stored in file, which may be read if desired (see below);
  # full mail header is available in ->orig_header;

  # some mail header fields are available through $msginfo->orig_header_fields
  # these may be multiline, may contain folding whitespace or comments;
  # alternatively, the whole original mail header is available in ->orig_header
  my($m_id) = $msginfo->get_header_field_body('message-id');  # e.g. <12@e.n>
  my($subj) = $msginfo->get_header_field_body('subject');
  my($from) = $msginfo->get_header_field_body('from');
    # e.g.: "=?ISO-8859-1?Q?Ren=E9_van_den_Berg?=" <vd@example.com>
  my($is_bulk) = $msginfo->orig_header_fields->{'precedence'};  # e.g. List
  $is_bulk = $is_bulk=~/^[ \t]*(bulk|list|junk)\b/i ? $1 : undef;
  for ($m_id,$from,$subj) {  # RFC2047-decode char. sets in some header fields
    local($1); chomp; my($str);
    s/\n([ \t])/$1/sg; s/^[ \t]+//s; s/[ \t]+\z//s;  # unfold, trim
    eval { $str = safe_decode('MIME-Header',$_) };   # to string of logical chr
    $_ = $str  if $@ eq '';  # replace if all ok, otherwise keep unchanged
  }
  # $m_id, $from, and $subj are now ready for examination - Perl logical chars
  do_log($ll,"CUSTOM: Subject: %s",safe_encode('iso-8859-1',$subj)); #as Latin1
  do_log($ll,"CUSTOM: From: %s", safe_encode('iso-8859-1',$from));  # as Latin1
  # NOTE: rfc2822 allows multiple addresses in the From field!
  my($rfc2822_sender) = $msginfo->rfc2822_sender;  # undef or scalar
  my(@rfc2822_from) = do { my $f = $msginfo->rfc2822_from; ref $f ? @$f : $f };
  do_log($ll,"CUSTOM: From (parsed): %s", join(', ',@rfc2822_from));
  do_log($ll,"CUSTOM: Sender: %s", $rfc2822_sender) if defined $rfc2822_sender;

  my($tempdir) = $msginfo->mail_tempdir;  # working directory for this process
  # $tempdir/parts/  is a directory where mail parts were extracted to
  my($mail_file_name) = $msginfo->mail_text_fn;
  # filename of the original mail, normally $tempdir/email.txt
  do_log($ll,"CUSTOM: temp.dir: %s", $tempdir);
  do_log($ll,"CUSTOM: filename: %s", $mail_file_name);

  # full mail header is available in ->orig_header;
  # some individual header fields are quickly accessible ->orig_header_fields

  # mail body is only stored in file, which may be read if desired
  my($fh) = $msginfo->mail_text;  # file handle of our original mail
  my($line); my($line_cnt) = 0;
# $fh->seek(0,0) or die "Can't rewind mail file: $!";
# for ($! = 0; defined($line = $fh->getline); $! = 0) {
#   $line_cnt++;
#   # examine one $line at a time;  (or read by blocks for speed)
# }
# defined $line || $!==0  or die "Error reading mail file: $!";
# do_log($ll,"CUSTOM: %d lines", $line_cnt);

  my($all_local) = !grep { !$_->recip_is_local } @{$msginfo->per_recip_data};
  if ($all_local) {
    my($hdr_edits) = $msginfo->header_edits;
    my($rly_country) = $msginfo->supplementary_info('RELAYCOUNTRY');
    $hdr_edits->add_header('X-Relay-Countries', $rly_country)
      if defined $rly_country && $rly_country ne '';
    my($languages) = $msginfo->supplementary_info('LANGUAGES');
    $hdr_edits->add_header('X-Spam-Languages', $languages)
      if defined $languages && $languages ne '';
  }

  # examine some data pertaining to the each recipient of the message
  # See methods in Amavis::In::Message::PerRecip for the full set of data.
  #
  my($any_passed) = 0;
  for my $r (@{$msginfo->per_recip_data}) {  # $r contains per-recipient data
    next  if $r->recip_done;  # skip recipient that won't receive a message
    # if all recipients have ->recip_done true, mail will not be passed at all
    $any_passed++;
    my($recip) = $r->recip_addr;  # recipient envelope address, e.g. rc@ex.com
    my($is_local) = $r->recip_is_local; # recipient matches @local_domains_maps
    my($localpart,$domain) = split_address($recip);

    my($spam_level_boost) = $r->recip_score_boost;  # per-recip score contrib.
    # $spam_level + $spam_level_boost   is the actual per-recipient spam score
    my($do_tag)  = $r->is_in_contents_category(CC_CLEAN,1);  # >= tag_level
    my($do_tag2) = $r->is_in_contents_category(CC_SPAMMY);   # >= tag2_level
    my($do_kill) = $r->is_in_contents_category(CC_SPAM);     # >= kill_level
    do_log($ll,"CUSTOM: recip: %s, score: %.2f, %s, %s, %s, %s",
             $recip, $spam_level+$spam_level_boost,
             $is_local ? 'IS LOCAL' : 'not local',
             $do_tag  ? 'tag'  : 'no-tag',
             $do_tag2 ? 'tag2' : 'no-tag2',
             $do_kill ? 'kill' : 'no-kill');

    # don't bother with outgoing mail!
    next  if !$is_local;

    # do a SQL lookup
    my($conn_h) = $self->{'conn_h'};
    $conn_h->begin_work_nontransaction;  # (re)connect if not connected
    #
    my($select_clause) =
      'SELECT present,email FROM users WHERE users.email=?';
    # list of actual arguments replacing '?' placeholders
    my(@pos_args) = ( lc(untaint($recip)) );
    $conn_h->execute($select_clause,@pos_args);  # do the query
    #
    my($a_ref); my($user_is_offline);
    while ( defined($a_ref=$conn_h->fetchrow_arrayref($select_clause)) ) {
      do_log($ll,"CUSTOM: SQL fields %s", join(", ", @$a_ref));
      $user_is_offline = 1  if $a_ref->[0] =~ /^(0|N)$/i;
    }
    $conn_h->finish($select_clause)  if defined $a_ref;  # only if not all read

    if ($user_is_offline) {
      # we have three choices of alerting the recipient:
      #   - redirect his mail to dedicated e-mail address;
      #   - use quarantining code to deliver a copy of the message to
      #     a dedicated address;
      #   - construct and send a notification to a dedicated address
      #
      my($choice) = 0;
      if ($choice == 0) {
        # ignore
      } elsif ($choice == 1) {
        # rewrite address and deliver normally
        my($new_addr) = $localpart . '+redirect' . $domain;
        $r->recip_addr_modified($new_addr);  # replaces delivery address!
      } elsif ($choice == 2) {
        # quarantine (i.e. send a mail copy) to a dedicated mailbox
        # in addition to delivering normally
        my($new_addr) = 'alert+' . $localpart . $domain;
        Amavis::do_quarantine($conn, $msginfo, undef,
                              [$new_addr], 'local:all-%m');
      } elsif ($choice == 3) {
        # construct and send a short notification,
        # in addition to delivering normally
        my($when) = rfc2822_timestamp($msginfo->rx_time);
        my($text) = <<"EOD";
From: Alerting Service <alerter\@$myhostname>
To: <$recip>
Subject: New message from $sender
Message-ID: <AA$log_id\@$myhostname>

A new message just arrived on $when
from $from (return-path <$sender>)
Subject: $subj
EOD
        my($notification) = Amavis::In::Message->new;
        $notification->rx_time($msginfo->rx_time);  # copy the reception time
        $notification->log_id($log_id);  # copy log id
        $notification->delivery_method(c('notify_method'));
        $notification->sender('');  # use null return path to avoid loops
        $notification->sender_smtp('<>');
        my($new_addr) = 'alert+' . $localpart . $domain;
        $notification->recips([$new_addr]);
        # character set is controlled through $hdr_encoding and $bdy_encoding
        #   config variables, defaults to 'iso-8859-1'
        $notification->mail_text(
                         string_to_mime_entity(\$text, $msginfo, undef,0,0));
        Amavis::mail_dispatch($conn, $notification, 1, 0);
        my($n_smtp_resp, $n_exit_code, $n_dsn_needed) =
          one_response_for_all($notification, 0);  # check status
        if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) {  # ok
        } elsif ($n_smtp_resp =~ /^4/) {
          die "temporarily unable to alert recipient: $n_smtp_resp";
        } else {
          do_log(-1, "FAILED to alert recipient: %s", $n_smtp_resp);
        }
      }
    }

  }
  if (!$any_passed) {
    do_log($ll,"CUSTOM: mail is blocked for all recipients");
  } else {  # will do delivery
    do_log($ll,"CUSTOM: being delivered to %d recips", $any_passed);
    # add a custom header field if desired (for all recipients of this message)
    # $msginfo->header_edits->add_header('X-Amavis-Example',
    #     sprintf("a custom header field, mail contains %d lines",$line_cnt) );
  }
  do_log($ll,"CUSTOM: done");
};

#sub after_send {  # may be left out if not needed
#  my($self,$conn,$msginfo) = @_;
#}

#sub mail_done {  # may be left out if not needed
#  my($self,$conn,$msginfo) = @_;
#}

1;  # insure a defined return

# vacation: see RFC 3834