[TUTORIAL] Block Google Groups, Firebasemail/Firebaseapp, Googleusercontent.com in Postfix (before DATA)

ivenae

Well-Known Member
Feb 11, 2022
193
95
48
42
Google operates several services that are commonly abused for sending spam. The following sections describe each service and the filtering strategies used.

Firebasemail / Firebaseapp​

Firebase is a Google-hosted platform that is frequently exploited for sending unsolicited email.

  • Emails sent directly through Firebase use the domain firebaseapp.com and can be blocked by sender domain.
  • Some senders use Firebase with their own custom domains, making direct domain blocking ineffective. However, these senders can still be identified early: their domain's SPF record (DNS TXT) contains _spf.firebasemail.com. This allows rejection before the SMTP DATA command, conserving server resources and preventing spam from entering the queue.

Google Groups​

Google Groups provides no opt-in or opt-out mechanism, making it trivially abusable: a spammer can add millions of recipients to a group and broadcast spam to all of them at once.

  • The sender domain can be any arbitrary registered domain, making domain-based filtering unreliable.
  • A Google Groups-specific header is present in each message, but inspecting it requires waiting for the full email data to be transmitted.
  • Fortunately, the envelope sender address always matches a predictable regular expression, which allows filtering before the DATA command is issued — the approach used by this policy daemon.

Google Usercontent​

googleusercontent.com is a Google server that, in practice, is a source of spam exclusively. Connections from this host are blocked at the client level.

Google Workspace & Gmail​

This filter intentionally does not block legitimate Google-based senders, including:

  • Google Workspace customers (e.g. ryanair.com, doctolib.com, willidrop.com)
  • Personal Gmail accounts (gmail.com, googlemail.com)
  • Google Notification Mails vom bounces.google.com (Security Alerts, Google Mybusiness Information, etc.)

Code:
apt install python3-dnspython

cat > /usr/local/bin/policyguard.py
Code:
#!/usr/bin/env python3

import sys
import time
import re
import dns.resolver
from functools import lru_cache

MAX_SPF_DEPTH = 10
CACHE_SIZE = 4096
SPF_BLOCK_INCLUDE = ["_spf.firebasemail.com"]
SENDER_BLOCK_INCLUDE = ["firebaseapp.com"]
BLOCKLIST_CLIENTNAME = ["googleusercontent.com"]
SENDER_REGEX_PATTERNS = [
    r'^[a-z0-9]{1,3}\+bncB[A-Z0-9]{25,}@',
]
SENDER_REGEXES = [re.compile(p) for p in SENDER_REGEX_PATTERNS]

resolver = dns.resolver.Resolver()
resolver.timeout = 2
resolver.lifetime = 3


def log(msg):
    sys.stderr.write(f"spf-policy: {msg}\n")
    sys.stderr.flush()


@lru_cache(maxsize=CACHE_SIZE)
def get_spf(domain):
    try:
        answers = resolver.resolve(domain, "TXT")
        for r in answers:
            txt = "".join([s.decode() for s in r.strings])
            if txt.startswith("v=spf1"):
                return txt
    except Exception:
        pass
    return None


def spf_contains_block(domain, depth=0, visited=None):
    if visited is None:
        visited = set()

    if depth > MAX_SPF_DEPTH:
        return False

    if domain in visited:
        return False

    visited.add(domain)

    spf = get_spf(domain)
    if not spf:
        return False

    parts = spf.split()

    for p in parts:
        if p.startswith("include:"):
            include = p.split(":", 1)[1]

            if include in SPF_BLOCK_INCLUDE:
                return True

            if spf_contains_block(include, depth + 1, visited):
                return True

    return False


def sender_matches_regex(sender):
    for regex in SENDER_REGEXES:
        if regex.match(sender):
            return True, regex.pattern
    return False, None


def handle_request(attrs):
    sender = attrs.get("sender", "")
    client_name = attrs.get("client_name", "").lower()

    # Block Clients
    if any(client_name == b or client_name.endswith("." + b) for b in BLOCKLIST_CLIENTNAME):
        log(f"blocked client hostname: {client_name}")
        return "reject reject for policy reasons [policyguard] [guc]"
 
    if sender:
        matched, pattern = sender_matches_regex(sender)
        if matched:
            log(f"blocked sender by regex ({pattern}): {sender}")
            return "reject reject for policy reasons [gg]"

    if "@" not in sender:
        return "dunno"

    domain = sender.split("@", 1)[1].lower()
    try:
        if any(domain == base or domain.endswith('.' + base) for base in SENDER_BLOCK_INCLUDE):
            log(f"blocked sender domain {domain}")
            return "reject reject for policy reasons [fbm]"
 
        elif spf_contains_block(domain):
            log(f"blocked sender domain {domain}")
            return "reject reject for policy reasons [fbs]"
    except Exception as e:
        log(f"error checking {domain}: {e}")

    return "dunno"


def main():
    while True:

        attrs = {}

        while True:
            line = sys.stdin.readline()

            if line == "":
                return

            line = line.strip()

            if not line:
                break

            if "=" in line:
                k, v = line.split("=", 1)
                attrs[k] = v

        if not attrs:
            continue

        action = handle_request(attrs)

        print(f"action={action}\n")
        sys.stdout.flush()


if __name__ == "__main__":
    main()

Don't forget to set execution bit:
Code:
chmod +x /usr/local/bin/policyguard.py

copy templates, if not already done:


Code:
mkdir -p /etc/pmg/templates/
cp /var/lib/pmg/templates/master.cf.in /etc/pmg/templates/
cp /var/lib/pmg/templates/main.cf.in /etc/pmg/templates/


Add the policy_service to the existing smtpd_sender_restrictions to main.cf.in:
Code:
smtpd_sender_restrictions =
        check_policy_service    unix:private/policygrd
Add the two lines to the bottom of the master.cf.in
Code:
policygrd unix  -       n       n       -       0       spawn
    user=nobody argv=/usr/local/bin/policyguard.py


pmgconfig sync --restart


How to test?

Connect to service:
Code:
apt install socat
socat - UNIX-CONNECT:/var/spool/postfix/private/policygrd

Enter address and an empty line:
Code:
sender=test@example.com
        <--- empty line

Output:
Code:
action=dunno

Enter firebasespammail
Code:
sender=fckw@babyamerica.com
        <--- empty line


Output:
Code:
firebase-spf-policy: blocked sender domain babyamerica.com
action=reject Rejected for policy reasons [fbs]


Enter google groups spam
Code:
sender=aw7+bncBDIYHYXXTENBBUE76TGQMGQEVOOGNYY@teamkurofune.com
        <--- empty line


Output:
Code:
spf-policy: blocked sender by regex (^[a-z0-9]{1,3}\+bnc[A-Z0-9]{10,}@): aw7+bncBDIYHYXXTENBBUE76TGQMGQEVOOGNYY@teamkurofune.com
action=reject reject for policy reasons [gg]
 

Attachments

  • ksnip_20260319-113935.png
    ksnip_20260319-113935.png
    304.4 KB · Views: 30
  • ksnip_20260403-023054.png
    ksnip_20260403-023054.png
    553.7 KB · Views: 22
Last edited:
Version 7.0:
- Enhances Version with evaluation of SPF python3-spf, enhanced checks and autoblocklist of bad senders
- Refactoring with Claude Opus: Optimizing speed and memory usage when first import of logfile
- External config file
- Be careful if you upgrade from previous versions: Some config options in the /etc/policyguard.toml are renamed!

This script will do an awesome filterjob:
- filters very early (before DATA) with very little CPU power
- high filter-rate of many google Spam
- more or less no false positive


Installation

Install neccessary python addons.
Code:
apt install python3-spf python3-dnspython socat

Code:
mkdir /p /t/
chmod -R 777 /t/
visudo -f /etc/sudoers.d/policyguard

allow journalctl to run as nobody
Code:
nobody ALL=(root) NOPASSWD: /usr/bin/journalctl

Copy attached script to /usr/local/bin/policyguard.py and make it executable
Code:
chmod +x /usr/local/bin/policyguard.py

Code:
mkdir -p /etc/pmg/templates/
cp /var/lib/pmg/templates/master.cf.in /etc/pmg/templates/
cp /var/lib/pmg/templates/main.cf.in /etc/pmg/templates/

Add the two lines to the bottom of the master.cf.in
The "9" will limit the maximum instances to 9. More mails are queued and – after a few seconds – temporarily defered. This prevent the machine from being OOM.
Code:
policygrd unix  -       n       n       -       9       spawn
    user=nobody argv=/usr/local/bin/policyguard.py


recipient_restriction
Add the policy_service to the existing smtpd_recipient_restrictions to main.cf.in:
Code:
smtpd_recipient_restrictions =
        check_policy_service    unix:private/policygrd

or alternatively sender_restriction (you should not use both)
Add the policy_service to the existing smtpd_sender_restrictions to main.cf.in:
Code:
smtpd_sender_restrictions =
        check_policy_service    unix:private/policygrd

and finally

Code:
pmgconfig sync --restart

If Policyguard is placed in the recipient restriction, it is able to return a 550 5.1.1 response and pretend that a mailbox does not exist. This can be effective with some newsletter providers, but not with real spam.

To avoid potential issues, Policyguard only returns this error outside normal business hours (22:00–06:00, Monday–Saturday, and 00:00–24:00 on Sunday), and only within a configurable percentage defined in the policyguard.toml. By default, this value is set to 0, meaning that Policyguard will always return 550 5.7.1 (policy reason) instead.


What is the difference between sender restriction and recipient restriction?​

Sender restriction is evaluated earlier than recipient restriction.

It operates in the same context as Configuration → Mail Proxy → Welcomelist.
If Policyguard is added to the sender restriction below /etc/postfix/senderaccess, the Mail Proxy → Welcomelist can effectively act as a whitelist for Policyguard. This can be a useful feature for some administrators.

However, during sender restriction evaluation, no recipient is yet known. As a result, Postfix refuses to return a 550 5.1.1 (mailbox does not exist) response at this stage, since it cannot make any statement about the recipient. Therefore, in /etc/policyguard.toml, the setting pretend_non_existing_mailbox should be set to 0, ensuring that a 550 5.1.1 response is never returned.


Recipient restriction, on the other hand, is evaluated later in a separate processing stage.

At this point, the Mail Proxy → Welcomelist no longer applies, and a rejection by Policyguard cannot be bypassed using it. For whitelisting within Policyguard, you must therefore use the built-in whitelist options (sender, HELO, client) in /etc/policyguard.toml.

In this stage, Policyguard can return a 550 5.1.1 (mailbox does not exist) response.


What it does
Does additional some SPF checks:

On any HELO:
- blocks some bad senders permanently
- block some bad clients permanently
- Reject SPF-Hardfail
- do DNSBL (if not done by postscreen)
- reject Hunter Mailcheck


If HELO=google:
- reject google groups (no change to last version)
- reject additional google spam regex ( '^[^@]+@s\.[a-zA-Z0-9.-]+$' )
- reject SPF-Permerror
- reject SPF-softfails
- reject if SPF- Contains firebase
- reject if TXT record "firebase=" exist


- defers unknown hostname forever and add this to permanent blocklist after a few tries.

Autoblacklist
Uses some other non-spoofable reject information for building a permanent reject-list
The Autoblacklist is derived from historical mail data. A domain is automatically blacklisted if it meets the following criteria:

At least 10 messages received within a 50-day window
At least 81% of those messages were blocked

Exclusions & Whitelisting:
To prevent database poisoning and false positives, certain rejections are deliberately excluded from the statistical analysis:

SPF failures are ignored — rejected mails with SPF errors are likely spoofed and would corrupt the dataset.
DNSBL rejections are ignored — these typically involve spoofed sender domains, and since the rejection occurs before the SMTP connection is established, no spoof-proof sender verification is available.
The following sources are always whitelisted and will never be blacklisted automatically:

All .de domains — whitelisted for safety reasons. The source code can be modified to whitelist a different TLD if needed.
A curated list of known newsletter providers and other important services — to prevent legitimate bulk senders from being blocked accidentally.

Configuration
Configuration now made via /etc/policyguard.toml
Just edit the policyguard.toml, no restart neccessary (the service is restarted after every few minutes by spawn daemon).
There are some bad senders and clients already in the config. Tidy up what you do not need or want to block.


How to test
After connecting with socat you can enter "stats" or current blocklist with "blocklist"

Code:
 socat - UNIX-CONNECT:/var/spool/postfix/private/policygrd
spf-policy: load_cache: 50 days loaded from /t/postfix_cache.json
spf-policy: initialize_cache: all historical days present in cache file
spf-policy: initialize_cache: loading today's stats from journal
blocklist
==== BEGIN CURRENT BLOCKLIST ====
acmbms.com
agenciatrendingmedia.com
arbentio.buzz
aristabd.com
artemishomeautomation.co.uk
babyamerica.com
businessworking.com.br
capturesoul.com
casang.vip
cidder.pro
com.tr
csyxzn.com
customyourproduct.com
dachser.com
datadecodedlabs.com
devalser.hair
domointelligence.com
[...]
wftelecompi.com.br
winstells.my
wuyedongman.info
zaralokakalyanafoundation.org
zuoyoujixie.com
==== END CURRENT BLOCKLIST ====
(leave programm with CTRL + C)



You can also use some filter criteria
Code:
blocklist domo
==== BEGIN CURRENT BLOCKLIST ====
domointelligence.com
==== END CURRENT BLOCKLIST ====
stats domo
==== TOTAL DOMAIN STATS (all days + today) ====
  domointelligence.com           accept: 4  blocked: 31  total: 35  percent blocked: 88 %
==== END TOTAL DOMAIN STATS ====
stats -b                       (will show only blocked domains)
==== TOTAL DOMAIN STATS (all days + today) ====
0%<<
[...]
kitzingmetallbaugmbh.com                 accept: 0     blocked: 4     total: 4      >>100%<<
biz.id                                   accept: 0     blocked: 4     total: 4      >>100%<<
forest-host.art                          accept: 0     blocked: 4     total: 4      >>100%<<
googleusercontent.com                    accept: 0     blocked: 4     total: 4      >>100%<<
ac.id                                    accept: 0     blocked: 4     total: 4      >>100%<<
mindmatrix.io                            accept: 0     blocked: 4     total: 4      >>100%<<
cimm.com                                 accept: 0     blocked: 4     total: 4      >>100%<<
ultra-hoster.online                      accept: 0     blocked: 4     total: 4      >>100%<<
truebearing.ca                           accept: 0     blocked: 4     total: 4      >>100%<<
agentgraphique.com                       accept: 0     blocked: 4     total: 4      >>100%<<
confiserie-moehlenkamp.online            accept: 0     blocked: 4     total: 4      >>100%<<
satokensetsu-99.co.jp                    accept: 0     blocked: 4     total: 4      >>100%<<
drenlia.com                              accept: 0     blocked: 4     total: 4      >>100%<<
vdartinc.com                             accept: 0     blocked: 4     total: 4      >>100%<<
[...]
carvertical.com                          accept: 1     blocked: 5     total: 6      >>83%<<
mtasv.net                                accept: 1     blocked: 5     total: 6      >>83%<<
table.media                              accept: 1     blocked: 5     total: 6      >>83%<<
babyamerica.com                          accept: 4     blocked: 19    total: 23     >>82%<<
texbangla.com                            accept: 6     blocked: 24    total: 30     >>80%<<
premium-box.eu                           accept: 11    blocked: 39    total: 50     >>78%<<
==== END TOTAL DOMAIN STATS ====


A manual test need more parameters to do the SPF checks. postfix will transmit this parameters automagically
Code:
helo_name=google.com
sender=bla@firebaseapp.com
client_address=8.8.8.8
client_name=mail.google.com
< new line >

spf-policy: blocked sender domain via autoblocklist firebaseapp.com
action=reject reject for policy reasons [policyguard-500] [abl]
 

Attachments

Last edited:
  • Like
Reactions: msc90 and Onslow
Yes.
The last version do spf check and need therefore more mandatory parameter to work (which postfix will transmit, but you didn't)
Sorry i didn't explain that.

helo_name=google.com
sender=bla@firebaseapp.com
client_address=8.8.8.8
client_name=mail.google.com
< new line >

spf-policy: blocked sender domain via autoblocklist firebaseapp.com
action=reject reject for policy reasons [policyguard-500] [abl]


For the "firebaseapp.com"-test the only parameter which is evaluated is the sender, so the others could be bogus.

What about the "policyguard-500": If the script return a policyguard-500 message, the sender will be added to autoblocklist.
If the script returns "policyguard-400", the sender won't be added to the autoblocklist. You'll get a "400" if the sender is possibly spoofed. (spf-softfail, spf-permerror, etc.). You also get a policyguard-400 on google groups, because i never see any google groups sender domain in other spam sender addresses.


Please post some statistics about filter quote if you have tested this script for a few days.
 
Last edited:
  • Like
Reactions: msc90
Output now as expected. Triggered on some mails already. Tyvm!

spf-policy: blocked sender domain via blocklist firebaseapp.com
action=reject reject for policy reasons [policyguard-500] [blacklist-sender]