• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

processone / ejabberd / 1296

19 Jan 2026 11:25AM UTC coverage: 33.562% (+0.09%) from 33.468%
1296

push

github

badlop
mod_conversejs: Cosmetic change: sort paths alphabetically

0 of 4 new or added lines in 1 file covered. (0.0%)

11245 existing lines in 174 files now uncovered.

15580 of 46421 relevant lines covered (33.56%)

1074.56 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.92
/src/mod_antispam_filter.erl
1
%%%----------------------------------------------------------------------
2
%%% File    : mod_antispam_filter.erl
3
%%% Author  : Holger Weiss <holger@zedat.fu-berlin.de>
4
%%% Author  : Stefan Strigler <stefan@strigler.de>
5
%%% Purpose : Filter C2S and S2S stanzas
6
%%% Created : 31 Mar 2019 by Holger Weiss <holger@zedat.fu-berlin.de>
7
%%%
8
%%%
9
%%% ejabberd, Copyright (C) 2019-2026 ProcessOne
10
%%%
11
%%% This program is free software; you can redistribute it and/or
12
%%% modify it under the terms of the GNU General Public License as
13
%%% published by the Free Software Foundation; either version 2 of the
14
%%% License, or (at your option) any later version.
15
%%%
16
%%% This program is distributed in the hope that it will be useful,
17
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
18
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
19
%%% General Public License for more details.
20
%%%
21
%%% You should have received a copy of the GNU General Public License along
22
%%% with this program; if not, write to the Free Software Foundation, Inc.,
23
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24
%%%
25
%%%----------------------------------------------------------------------
26

27
%%| Definitions
28
%% @format-begin
29

30
-module(mod_antispam_filter).
31

32
-author('holger@zedat.fu-berlin.de').
33
-author('stefan@strigler.de').
34

35
-export([init_filtering/1, terminate_filtering/1]).
36
%% ejabberd_hooks callbacks
37
-export([s2s_in_handle_info/2, s2s_receive_packet/1, sm_receive_packet/1]).
38

39
-include("logger.hrl").
40
-include("translate.hrl").
41
-include("mod_antispam.hrl").
42

43
-include_lib("xmpp/include/xmpp.hrl").
44

45
-type s2s_in_state() :: ejabberd_s2s_in:state().
46

47
-define(HTTPC_TIMEOUT, timer:seconds(3)).
48

49
%%--------------------------------------------------------------------
50
%%| Exported
51

52
init_filtering(Host) ->
UNCOV
53
    ejabberd_hooks:add(s2s_in_handle_info, Host, ?MODULE, s2s_in_handle_info, 90),
2✔
UNCOV
54
    ejabberd_hooks:add(s2s_receive_packet, Host, ?MODULE, s2s_receive_packet, 50),
2✔
UNCOV
55
    ejabberd_hooks:add(sm_receive_packet, Host, ?MODULE, sm_receive_packet, 50).
2✔
56

57
terminate_filtering(Host) ->
UNCOV
58
    ejabberd_hooks:delete(s2s_receive_packet, Host, ?MODULE, s2s_receive_packet, 50),
2✔
UNCOV
59
    ejabberd_hooks:delete(sm_receive_packet, Host, ?MODULE, sm_receive_packet, 50),
2✔
UNCOV
60
    ejabberd_hooks:delete(s2s_in_handle_info, Host, ?MODULE, s2s_in_handle_info, 90).
2✔
61

62
%%--------------------------------------------------------------------
63
%%| Hook callbacks
64

65
-spec s2s_receive_packet({stanza() | drop, s2s_in_state()}) ->
66
                            {stanza() | drop, s2s_in_state()} | {stop, {drop, s2s_in_state()}}.
67
s2s_receive_packet({A, State}) ->
UNCOV
68
    case sm_receive_packet(A) of
32✔
69
        {stop, drop} ->
UNCOV
70
            {stop, {drop, State}};
16✔
71
        Result ->
UNCOV
72
            {Result, State}
16✔
73
    end.
74

75
-spec sm_receive_packet(stanza() | drop) -> stanza() | drop | {stop, drop}.
76
sm_receive_packet(drop = Acc) ->
77
    Acc;
×
78
sm_receive_packet(#message{from = From,
79
                           to = #jid{lserver = LServer} = To,
80
                           type = Type} =
81
                      Msg)
82
    when Type /= groupchat, Type /= error ->
UNCOV
83
    do_check(From, To, LServer, Msg);
4,300✔
84
sm_receive_packet(#presence{from = From,
85
                            to = #jid{lserver = LServer} = To,
86
                            type = subscribe} =
87
                      Presence) ->
UNCOV
88
    do_check(From, To, LServer, Presence);
76✔
89
sm_receive_packet(Acc) ->
UNCOV
90
    Acc.
13,576✔
91

92
%%--------------------------------------------------------------------
93
%%| Filtering deciding
94

95
do_check(From, To, LServer, Stanza) ->
UNCOV
96
    case needs_checking(From, To) of
4,376✔
97
        true ->
UNCOV
98
            case check_from(LServer, From) of
2,594✔
99
                ham ->
UNCOV
100
                    case check_stanza(LServer, From, Stanza) of
2,580✔
101
                        ham ->
UNCOV
102
                            Stanza;
2,578✔
103
                        spam ->
UNCOV
104
                            reject(Stanza),
2✔
UNCOV
105
                            {stop, drop}
2✔
106
                    end;
107
                spam ->
UNCOV
108
                    reject(Stanza),
14✔
UNCOV
109
                    {stop, drop}
14✔
110
            end;
111
        false ->
UNCOV
112
            Stanza
1,782✔
113
    end.
114

115
check_stanza(LServer, From, #message{body = Body}) ->
UNCOV
116
    check_body(LServer, From, xmpp:get_text(Body));
2,520✔
117
check_stanza(_, _, _) ->
UNCOV
118
    ham.
60✔
119

120
-spec s2s_in_handle_info(s2s_in_state(), any()) ->
121
                            s2s_in_state() | {stop, s2s_in_state()}.
122
s2s_in_handle_info(State, {_Ref, {spam_filter, _}}) ->
123
    ?DEBUG("Dropping expired spam filter result", []),
×
124
    {stop, State};
×
125
s2s_in_handle_info(State, _) ->
126
    State.
×
127

128
-spec needs_checking(jid(), jid()) -> boolean().
129
needs_checking(#jid{lserver = FromHost} = From, #jid{lserver = LServer} = To) ->
UNCOV
130
    case gen_mod:is_loaded(LServer, ?MODULE_ANTISPAM) of
4,376✔
131
        true ->
UNCOV
132
            Access = gen_mod:get_module_opt(LServer, ?MODULE_ANTISPAM, access_spam),
4,376✔
UNCOV
133
            case acl:match_rule(LServer, Access, To) of
4,376✔
134
                allow ->
135
                    ?DEBUG("Spam not filtered for ~s", [jid:encode(To)]),
×
136
                    false;
×
137
                deny ->
UNCOV
138
                    ?DEBUG("Spam is filtered for ~s", [jid:encode(To)]),
4,376✔
UNCOV
139
                    not mod_roster:is_subscribed(From, To)
4,376✔
UNCOV
140
                    andalso not
2,594✔
141
                                mod_roster:is_subscribed(
142
                                    jid:make(<<>>, FromHost),
143
                                    To) % likely a gateway
144
            end;
145
        false ->
146
            ?DEBUG("~s not loaded for ~s", [?MODULE_ANTISPAM, LServer]),
×
147
            false
×
148
    end.
149

150
-spec check_from(binary(), jid()) -> ham | spam.
151
check_from(Host, From) ->
UNCOV
152
    Proc = get_proc_name(Host),
2,594✔
UNCOV
153
    LFrom =
2,594✔
154
        {_, FromDomain, _} =
155
            jid:remove_resource(
156
                jid:tolower(From)),
UNCOV
157
    try
2,594✔
UNCOV
158
        case gen_server:call(Proc, {is_blocked_domain, FromDomain}) of
2,594✔
159
            true ->
UNCOV
160
                ?DEBUG("Spam JID found in blocked domains: ~p", [From]),
6✔
UNCOV
161
                ejabberd_hooks:run(spam_found, Host, [{jid, From}]),
6✔
UNCOV
162
                spam;
6✔
163
            false ->
UNCOV
164
                case gen_server:call(Proc, {check_jid, LFrom}) of
2,588✔
165
                    {spam_filter, Result} ->
UNCOV
166
                        Result
2,588✔
167
                end
168
        end
169
    catch
170
        exit:{timeout, _} ->
171
            ?WARNING_MSG("Timeout while checking ~s against list of blocked domains or spammers",
×
172
                         [jid:encode(From)]),
×
173
            ham
×
174
    end.
175

176
-spec check_body(binary(), jid(), binary()) -> ham | spam.
177
check_body(Host, From, Body) ->
UNCOV
178
    case {extract_urls(Host, Body), extract_jids(Body)} of
2,520✔
179
        {none, none} ->
UNCOV
180
            ?DEBUG("No JIDs/URLs found in message", []),
2,508✔
UNCOV
181
            ham;
2,508✔
182
        {URLs, JIDs} ->
UNCOV
183
            Proc = get_proc_name(Host),
12✔
UNCOV
184
            LFrom =
12✔
185
                jid:remove_resource(
186
                    jid:tolower(From)),
UNCOV
187
            try gen_server:call(Proc, {check_body, URLs, JIDs, LFrom}) of
12✔
188
                {spam_filter, Result} ->
UNCOV
189
                    Result
12✔
190
            catch
191
                exit:{timeout, _} ->
192
                    ?WARNING_MSG("Timeout while checking body", []),
×
193
                    ham
×
194
            end
195
    end.
196

197
%%--------------------------------------------------------------------
198
%%| Auxiliary
199

200
-spec extract_urls(binary(), binary()) -> {urls, [url()]} | none.
201
extract_urls(Host, Body) ->
UNCOV
202
    RE = <<"https?://\\S+">>,
2,520✔
UNCOV
203
    Options = [global, {capture, all, binary}],
2,520✔
UNCOV
204
    case re:run(Body, RE, Options) of
2,520✔
205
        {match, Captured} when is_list(Captured) ->
UNCOV
206
            Urls = resolve_redirects(Host, lists:flatten(Captured)),
2✔
UNCOV
207
            {urls, Urls};
2✔
208
        nomatch ->
UNCOV
209
            none
2,518✔
210
    end.
211

212
-spec resolve_redirects(binary(), [url()]) -> [url()].
213
resolve_redirects(_Host, URLs) ->
UNCOV
214
    try do_resolve_redirects(URLs, []) of
2✔
215
        ResolvedURLs ->
UNCOV
216
            ResolvedURLs
2✔
217
    catch
218
        exit:{timeout, _} ->
219
            ?WARNING_MSG("Timeout while resolving redirects: ~p", [URLs]),
×
220
            URLs
×
221
    end.
222

223
-spec do_resolve_redirects([url()], [url()]) -> [url()].
224
do_resolve_redirects([], Result) ->
UNCOV
225
    Result;
2✔
226
do_resolve_redirects([URL | Rest], Acc) ->
UNCOV
227
    case httpc:request(get,
2✔
228
                       {URL, [{"user-agent", "curl/8.7.1"}]},
229
                       [{autoredirect, false}, {timeout, ?HTTPC_TIMEOUT}],
230
                       [])
231
    of
232
        {ok, {{_, StatusCode, _}, Headers, _Body}} when StatusCode >= 300, StatusCode < 400 ->
233
            Location = proplists:get_value("location", Headers),
×
234
            case Location == undefined orelse lists:member(Location, Acc) of
×
235
                true ->
236
                    do_resolve_redirects(Rest, [URL | Acc]);
×
237
                false ->
238
                    do_resolve_redirects([Location | Rest], [URL | Acc])
×
239
            end;
240
        _Res ->
UNCOV
241
            do_resolve_redirects(Rest, [URL | Acc])
2✔
242
    end.
243

244
-spec extract_jids(binary()) -> {jids, [ljid()]} | none.
245
extract_jids(Body) ->
UNCOV
246
    RE = <<"\\S+@\\S+">>,
2,520✔
UNCOV
247
    Options = [global, {capture, all, binary}],
2,520✔
UNCOV
248
    case re:run(Body, RE, Options) of
2,520✔
249
        {match, Captured} when is_list(Captured) ->
UNCOV
250
            {jids, lists:filtermap(fun try_decode_jid/1, lists:flatten(Captured))};
10✔
251
        nomatch ->
UNCOV
252
            none
2,510✔
253
    end.
254

255
-spec try_decode_jid(binary()) -> {true, ljid()} | false.
256
try_decode_jid(S) ->
UNCOV
257
    try jid:decode(S) of
20✔
258
        #jid{} = JID ->
UNCOV
259
            {true,
20✔
260
             jid:remove_resource(
261
                 jid:tolower(JID))}
262
    catch
263
        _:{bad_jid, _} ->
264
            false
×
265
    end.
266

267
-spec reject(stanza()) -> ok.
268
reject(#message{from = From,
269
                to = To,
270
                type = Type,
271
                lang = Lang} =
272
           Msg)
273
    when Type /= groupchat, Type /= error ->
UNCOV
274
    ?INFO_MSG("Rejecting unsolicited message from ~s to ~s",
16✔
UNCOV
275
              [jid:encode(From), jid:encode(To)]),
16✔
UNCOV
276
    Txt = <<"Your message is unsolicited">>,
16✔
UNCOV
277
    Err = xmpp:err_policy_violation(Txt, Lang),
16✔
UNCOV
278
    ejabberd_hooks:run(spam_stanza_rejected, To#jid.lserver, [Msg]),
16✔
UNCOV
279
    ejabberd_router:route_error(Msg, Err);
16✔
280
reject(#presence{from = From,
281
                 to = To,
282
                 lang = Lang} =
283
           Presence) ->
284
    ?INFO_MSG("Rejecting unsolicited presence from ~s to ~s",
×
285
              [jid:encode(From), jid:encode(To)]),
×
286
    Txt = <<"Your traffic is unsolicited">>,
×
287
    Err = xmpp:err_policy_violation(Txt, Lang),
×
288
    ejabberd_router:route_error(Presence, Err);
×
289
reject(_) ->
290
    ok.
×
291

292
-spec get_proc_name(binary()) -> atom().
293
get_proc_name(Host) ->
UNCOV
294
    gen_mod:get_module_proc(Host, ?MODULE_ANTISPAM).
2,606✔
295

296
%%--------------------------------------------------------------------
297

298
%%| vim: set foldmethod=marker foldmarker=%%|,%%-:
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc