4

Is there is a way to prevent postfix from sending them to the sender? My boss asked me to do so, so I'm not going to argue with him, just need to disable it.

I've tried to discard them in header_checks, by filtering "From: MAILER-DAEMON" field, but originally these mails were send "from=<>" and then it will be replaced to MAILER-DAEMON after mail passes header_checks. Any ideas will be appreciated

Shirker
  • 579

2 Answers2

8

Found the answer here How to disable "sender non-delivery notification" in postfix? Even it was downvoted, but works for me

The answer:

master.cf

bounce    unix  -       -       n       -       0       bounce

should be changed to:

master.cf

bounce    unix  -       -       n       -       0       discard
Shirker
  • 579
1

I have been using the solution of tesst on How to disable "sender non-delivery notification" in postfix to prevent forwarded spam that was rejected from bouncing back to (feigned) senders, but with Postfix 3.7 it no longer worked. I got the following errors:

postfix/smtp[424401]: warning: read private/bounce socket: Application error

postfix/smtp[424401]: warning: unexpected protocol delivery_request_protocol from private/bounce socket (expected: delivery_status_protocol)

The mail got status=deferred (bounce or trace service failure) and it remained in the queue until it expired and a sender non-delivery notification was sent.

My alternative is to revert the master.cf config to:

bounce    unix  -       -       n       -       0       bounce

and in main.cf:

soft_bounce = yes
minimal_backoff_time = 30m
bounce_queue_lifetime = 1d

This results in rejected/bouced mails to remain in the queue for one day (bounce_queue_lifetime). From a PHP script called every 30 minutes i remove those i do not want to be bounced back to the sender. The others are retried a little later (minimal_backoff_time) and if not removed manually, bounced with a sender non-delivery notification.

Here is the PHP script:

// array with patterns matching rejected spam by domain forwarded to 
$spamPatterns = [
    "me.com" => '~said\: 554 5.7.1 \[CS01\] |said\: 554 5.7.1 \[CS02\] |said\: 554 5.7.1 \[HM07\] |said\: 550 5.7.1 Your email was rejected .+ Spamhaus DBL|said\: 554 5.7.1 Your message was rejected due to .+ DMARC policy~'
        // said: 554 5.7.1 [CS01] Message rejected due to local policy. Please visit https://support.apple.com/en-us/HT204137
        // said: 554 5.7.1 [CS02] Message rejected due to local policy. Please visit https://support.apple.com/en-us/HT204137
        // said: 554 5.7.1 [HM07] Message rejected due to local policy. Please visit https://support.apple.com/en-us/HT204137
        // said: 550 5.7.1 Your email was rejected due to having a domain present in the Spamhaus DBL
        // said: 554 5.7.1 Your message was rejected due to wetransfer.com's DMARC policy
];

$scrubber = new PostfixQueueScrubber($spamPatterns); try { $toBeInspected = $scrubber->scrub(); if (!empty($toBeInspected)) { print "Please inspect: \n"; print json_encode($toBeInspected, JSON_PRETTY_PRINT); print "\n"; } } catch (Exception $e) { print "Error: ". $e->message. "\n"; }

/**

  • Deletes mails rejected by configured domains that where softbouced from the postfix mail queue
  • Assumed to be run from cron so that any output results in an error mail
  • To prevent unnecessary retries minimal_backoff_time should be longer then the time between this scrips runs

*/ class PostfixQueueScrubber {

protected $localHostName;

public function __construct(protected array $spamPatterns) {
    $this-&gt;localHostName = trim(file_get_contents('/etc/hostname'));
}

public function scrub() {
    $queue = shell_exec(&quot;/usr/sbin/postqueue -j&quot;);
    if ($queue === false) {
        throw new RuntimeException(&quot;Could not run postqueue&quot;);
    }

    $lines = explode(&quot;\n&quot;, $queue);
    $toBeInspected = [];
    foreach ($lines as $line) {
        if (strlen($line) == 0) continue; // last line is empty
        $entry = json_decode($line);
        if (!isset($entry-&gt;queue_id)) {
            throw new RuntimeException(&quot;Queue entry without queue_id: '$line'&quot;);
        }
        if ($entry-&gt;queue_id == &quot;ALL&quot;) continue; // should never happen

        if ($this-&gt;isForwardedToSpamRejector($entry)) {
            if ($this-&gt;isRejectedSpam($entry)
                &amp;&amp; !$this-&gt;isSuspicious($entry)
            ) {

               $this-&gt;removeFromQueue($entry);

            } else {
                // Deferred mail to spam rejector that is suspicous or without delay_reason or no known reason why it is spam \n&quot;;
                // To be inpected by postmaster to decide what to do with it and eventually adapt this script
                // (If the postmaster does nothing it stays in the queue until sender non delivery notification and automatic deletion)

                $toBeInspected[] = $entry;
            }
        }
    }
    return $toBeInspected;
}

protected function isForwardedToSpamRejector($entry) {
    if ($entry-&gt;queue_name != 'deferred' 
        || count($entry-&gt;recipients) != 1
        || !isset($entry-&gt;recipients[0]-&gt;delay_reason)) {
        return false;
    }
    $host = $this-&gt;host($entry-&gt;recipients[0]-&gt;address);
    return isset($this-&gt;spamPatterns[$host]);
}

protected function host($emailAdress) {
    $pieces = explode('@', $emailAdress);
    return isset($pieces[1]) ? $pieces[1] : null;
}

protected function isRejectedSpam($entry) {
    $host = $this-&gt;host($entry-&gt;recipients[0]-&gt;address);
    $match = preg_match($this-&gt;spamPatterns[$host], $entry-&gt;recipients[0]-&gt;delay_reason);
    if ($match === false) {
        throw new RuntimeException(&quot;Spam pattern '$host' match error: &quot;. preg_last_error_msg());
    }
    return $match;
}

protected function isSuspicious($entry) {
    // Locally sent spam may indicate that your server is hacked.
    // You may want to add more criteria here, for example if your server accepts submission(s)

    return false !== $this-&gt;isSentLocally($entry); 
    // Handles postcat error as suspicious so that the mail is not removed. true === handles error as not suspicious
}

protected function isSentLocally($entry) {
    $qid = escapeshellarg($entry-&gt;queue_id);
    $headers = shell_exec(&quot;/usr/sbin/postcat -hq $qid&quot;);
    if ($headers === false) {
        // Error produces output - one error is the file $qid missing in the queue, can happen if it has just been removed
        return null;
    }
    if ($headers === &quot;&quot;) {
        throw new RuntimeException(&quot;No headers from postcat -hq $qid&quot;);
    }

    $match = preg_match(&quot;~\nReceived: from $this-&gt;localHostName ~&quot;, $headers);
    if ($match === false) {
        throw new RuntimeException(&quot;Received header pattern for '$this-&gt;localHostName' match error: &quot;. preg_last_error_msg());
    }
    return $match ? true : false;
}

protected function removeFromQueue($entry) {
    $qid = escapeshellarg($entry-&gt;queue_id);
    $descriptorspec = [
        2 =&gt; [&quot;pipe&quot;, &quot;w&quot;],  // stderr
    ];
    $pipes = null;

    $process = proc_open(&quot;/usr/sbin/postsuper -d $qid&quot;, $descriptorspec, $pipes);

    if (is_resource($process)) {
        $output = stream_get_contents($pipes[2]);
        fclose($pipes[2]);
        $return_value = proc_close($process);
    } else {
        throw new RuntimeException(&quot;could not run postsuper -d $qid&quot;);
    }

    if ($return_value != '0' || false === strpos($output, 'postsuper: Deleted: ')) {
        // If $qid is missing this is not executed

// For debugging
// print "Error? from postsuper -d $qid: '$return_value' '$output'\n"; } } }

You will probably want to adapt/extend array with patterns matching rejected spam by domain forwarded to.

Mail sent locally is not removed because it does not make the server send around bounced spam to arbitrary 'from' adresses and because it may be an indication that spam is being sent from the server.

minimal_backoff_time should be set longer then the time between two calls from cron to the PHP script otherwise sending will be retried before the script can remove eventual rejected spam from the queue. But it should be set short enough for retries to happen within an hour to avoid greylisting

Be aware that the postscript documentation states that soft bounce is only for testing purposes and that it is not to be used in production.

MetaClass
  • 111