Packages
zotonic_stdlib
1.27.0
1.31.2
1.31.1
1.31.0
1.30.1
1.30.0
1.29.1
1.29.0
1.28.1
1.28.0
1.27.0
1.26.1
1.25.0
1.24.0
1.23.1
1.23.0
1.22.0
1.21.0
1.20.3
1.20.2
1.20.1
1.20.0
1.19.0
1.18.0
1.17.0
1.16.0
1.15.1
1.15.0
1.14.0
1.13.0
1.12.0
1.11.2
1.11.1
1.11.0
1.10.0
1.9.0
1.8.0
1.7.0
1.6.0
1.5.11
1.5.10
1.5.9
1.5.8
1.5.7
1.5.6
1.5.5
1.5.4
1.5.3
1.5.2
1.5.1
1.5.0
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.11
1.2.10
1.2.9
1.2.8
1.2.7
1.2.6
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.1.0
1.0.3
1.0.2
1.0.1
1.0.0
1.0.0-alpha5
1.0.0-alpha4
1.0.0-alpha3
1.0.0-alpha2
1.0.0-alpha1
Zotonic standard library
Current section
Files
Jump to
Current section
Files
src/z_email_dnsbl.erl
%% @author Marc Worrell
%% @copyright 2015-2025 Marc Worrell
%% @doc Check an IP address against the list that are maintained by
%% DNSBL or DNSWL providers (rfc5782). There is a list of default
%% black list providers in dns_blocklist/0 and of white list providers
%% in dns_allowlist/0.
%% @end
%% Copyright 2015-2025 Marc Worrell
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
-module(z_email_dnsbl).
-author("Marc Worrell <marc@worrell.nl>").
-export([
is_blocked/1,
is_blocked/2,
is_blocked/3,
status/1,
status/2,
status/3,
dns_allowlist/0,
dns_blocklist/0
]).
% Not all ISPs have a subscription to the dnswl.org allowlist, Google has.
-define(ALLOWLIST_NAMESERVERS, [{{8,8,8,8},53}, {{8,8,4,4},53}]).
-include_lib("kernel/include/inet.hrl").
%% @doc Check if the IP address is on one of the default blocklists.
-spec is_blocked(inet:ip_address()) -> boolean().
is_blocked(IP) ->
is_blocked(IP, dns_blocklist(), dns_allowlist()).
%% @doc Check if the IP address is on one of the given blocklists.
-spec is_blocked(inet:ip_address(), list(string())) -> boolean().
is_blocked(IP, RTBLs) ->
is_blocked(IP, RTBLs, dns_allowlist()).
%% @doc Check if the IP address is on one of the given blocklists and not on one of the white lists.
%% If an IP address is white listen then this routine always return true.
-spec is_blocked(inet:ip_address(), list(string()), list(string())) -> boolean().
is_blocked(IP, RTBLs, WLs) ->
case status(IP, RTBLs, WLs) of
{ok, {blocked, _DNSBL}} -> true;
_ -> false
end.
%% @doc Check the block- or allowlist status of an IP address. If it is blocked then the blocklists
%% where the IP address is blocked are returned.
-spec status(inet:ip_address()) -> {ok, notlisted|allowed|{blocked, list(string())}}.
status(IP) ->
status(IP, dns_blocklist(), dns_allowlist()).
%% @doc Check the block- or allowlist status of an IP address with the given block lists.
%% If it is blocked then the blocklists where the IP address is blocked are returned.
-spec status(inet:ip_address(), list()) -> {ok, notlisted|allowed|{blocked, list(string())}}.
status(IP, DNSBLs) ->
status(IP, DNSBLs, []).
%% @doc Check the block- or whitelist status of an IP address with the given block- and whitelists.
%% If it is blocked then the blocklists where the IP address is blocked are returned.
-spec status(inet:ip_address(), list(), list()) ->
{ok, {blocked, list()}}
| {ok, notlisted}
| {ok, allowed}.
status(IP, DNSBLs, DNSWLs) ->
Dotted = reverse(IP),
case status_1(Dotted, IP, DNSWLs, true) of
{ok, {blocked, _}} -> {ok, allowed};
_ ->
case is_allowed(IP) of
true -> {ok, allowed};
false -> status_1(Dotted, IP, DNSBLs, false)
end
end.
status_1(_Dotted, _IP, [], _IsAllowList) ->
{ok, notlisted};
status_1(Dotted, IP, [DNSBL|Rest], IsAllowList) ->
case gethostbyname(Dotted++DNSBL, IsAllowList) of
{ok, #hostent{h_addr_list=AddrList}} ->
case check_addr_list(DNSBL, AddrList) of
{ok, notlisted} ->
status_1(Dotted, IP, Rest, IsAllowList);
Other ->
Other
end;
{error, nxdomain} ->
status_1(Dotted, IP, Rest, IsAllowList);
{error, _} ->
status_1(Dotted, IP, Rest, IsAllowList)
end.
gethostbyname(Name, false) ->
inet:gethostbyname(Name);
gethostbyname(Name, true) ->
ResolverOptions = [
{nameservers, ?ALLOWLIST_NAMESERVERS}
],
case inet_res:lookup(Name, in, a, ResolverOptions) of
[] -> {error, enoent};
IPs when is_list(IPs) ->
{ok, #hostent{ h_addr_list=IPs, h_addrtype = inet, h_length = 0 }}
end.
%% @todo Use the SPF records of well-known email hosters for allowlist checks
is_allowed(IP) ->
case inet_res:gethostbyaddr(IP) of
{ok, #hostent{h_name=Hostname}} ->
case is_allowed_hostname(Hostname) of
true -> true;
false -> false
end;
{error, nxdomain} ->
false;
{error, _} ->
% servfail or other errors -- allow for now
true
end.
is_allowed_hostname(Hostname) ->
Reversed = lists:reverse(string:tokens(Hostname, ".")),
case Reversed of
["com","google","mail-"++_] -> true;
["net","msn","ntwk"|_] -> true; % "ten2-1-111.sn1-6nx-mms-1a.ntwk.msn.net"
_ -> false
end.
%% @doc Check the returned address against RFC5782 return values
%% Here we could check on the addresses returned to check for
%% the reason an address is blocked
%% Filter on 127.0.0.x addresses to prevent problems where a
%% DNS server 'hijacks' queries to resolve to an ISPs own pages.
check_addr_list(_DNSBL, []) ->
{ok, notlisted};
check_addr_list(DNSBL, List) ->
List1 = lists:filter(fun
({127,0,_,255}) -> false;
({127,0,_,_}) -> true;
(_) -> false
end,
List),
case List1 of
[] -> {ok, notlisted};
_ -> {ok, {blocked, DNSBL}}
end.
%% @doc Default list of DNSWL services
-spec dns_allowlist() -> list( string() ).
dns_allowlist() ->
[
"list.dnswl.org", % https://www.dnswl.org/?page_id=15
"swl.spamhaus.org" % http://www.spamhauswhitelist.com/en/usage.html
].
%% @doc Default list of DNSBL services
-spec dns_blocklist() -> list( string() ).
dns_blocklist() ->
[
"zen.spamhaus.org", % http://www.spamhaus.org/zen/
"dnsbl.sorbs.net" % http://dnsbl.sorbs.net/general/using.shtml
].
%% @doc Reverse the ip address so that it can be prefixed to the DNSBL domain
reverse({_,_,_,_} = IPv4) ->
lists:flatten([ [ integer_to_list(V), $. ] || V <- lists:reverse(tuple_to_list(IPv4)) ]);
reverse(IPv6) when is_tuple(IPv6) ->
Nibbles = lists:flatten([ nibbles(P) || P <- tuple_to_list(IPv6) ]),
lists:flatten([ [ to_hex(Nibble), $. ] || Nibble <- lists:reverse(Nibbles) ]).
nibbles(N) ->
<<A:4,B:4,C:4,D:4>> = <<N:16/unsigned>>,
[ A, B, C, D ].
to_hex(C) when C =< 9 -> C + $0;
to_hex(C) -> C - 10 + $a.
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
reverse_test() ->
"b.a.9.8.7.6.5.0.4.0.0.0.3.0.0.0.2.0.0.0.1.0.0.0.8.b.d.0.1.0.0.2." = reverse({16#2001,16#db8,1,2,3,4,16#567,16#89ab}),
"1.0.0.127." = reverse({127,0,0,1}),
ok.
-endif.