• 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

60.58
/src/mod_offline.erl
1
%%%----------------------------------------------------------------------
2
%%% File    : mod_offline.erl
3
%%% Author  : Alexey Shchepin <alexey@process-one.net>
4
%%% Purpose : Store and manage offline messages.
5
%%% Created :  5 Jan 2003 by Alexey Shchepin <alexey@process-one.net>
6
%%%
7
%%%
8
%%% ejabberd, Copyright (C) 2002-2026   ProcessOne
9
%%%
10
%%% This program is free software; you can redistribute it and/or
11
%%% modify it under the terms of the GNU General Public License as
12
%%% published by the Free Software Foundation; either version 2 of the
13
%%% License, or (at your option) any later version.
14
%%%
15
%%% This program is distributed in the hope that it will be useful,
16
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
17
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18
%%% General Public License for more details.
19
%%%
20
%%% You should have received a copy of the GNU General Public License along
21
%%% with this program; if not, write to the Free Software Foundation, Inc.,
22
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23
%%%
24
%%%----------------------------------------------------------------------
25

26
-module(mod_offline).
27

28
-author('alexey@process-one.net').
29

30
-protocol({xep, 13, '1.3', '16.02', "complete", ""}).
31
-protocol({xep, 22, '1.4', '0.1.0', "complete", ""}).
32
-protocol({xep, 23, '1.3', '0.7.5', "complete", ""}).
33
-protocol({xep, 160, '1.0.1', '16.01', "complete", ""}).
34
-protocol({xep, 203, '2.0', '2.1.0', "complete", ""}).
35
-protocol({xep, 334, '1.0.0', '16.01', "complete", ""}).
36

37
-behaviour(gen_mod).
38

39
-export([start/2,
40
         stop/1,
41
         reload/3,
42
         store_packet/1,
43
         store_offline_msg/1,
44
         c2s_self_presence/1,
45
         get_sm_features/5,
46
         get_sm_identity/5,
47
         get_sm_items/5,
48
         get_info/5,
49
         handle_offline_query/1,
50
         remove_expired_messages/1,
51
         remove_old_messages/2,
52
         remove_user/2,
53
         import_info/0,
54
         import_start/2,
55
         import/5,
56
         export/1,
57
         get_queue_length/2,
58
         count_offline_messages/2,
59
         get_offline_els/2,
60
         find_x_expire/2,
61
         c2s_handle_info/2,
62
         c2s_copy_session/2,
63
         get_offline_messages/2,
64
         webadmin_menu_hostuser/4,
65
         webadmin_page_hostuser/4,
66
         webadmin_user/4,
67
         webadmin_user_parse_query/5,
68
         c2s_handle_bind2_inline/1]).
69

70
-export([mod_opt_type/1, mod_options/1, mod_doc/0, depends/2]).
71

72
-import(ejabberd_web_admin, [make_command/4, make_command/2]).
73

74
-deprecated({get_queue_length,2}).
75

76
-include("logger.hrl").
77

78
-include_lib("xmpp/include/xmpp.hrl").
79

80
-include("ejabberd_http.hrl").
81

82
-include("ejabberd_web_admin.hrl").
83

84
-include("mod_offline.hrl").
85

86
-include("translate.hrl").
87

88
%% default value for the maximum number of user messages
89
-define(MAX_USER_MESSAGES, infinity).
90

91
-define(SPOOL_COUNTER_CACHE, offline_msg_counter_cache).
92

93
-type c2s_state() :: ejabberd_c2s:state().
94

95
-callback init(binary(), gen_mod:opts()) -> any().
96
-callback import(#offline_msg{}) -> ok.
97
-callback store_message(#offline_msg{}) -> ok | {error, any()}.
98
-callback pop_messages(binary(), binary()) ->
99
    {ok, [#offline_msg{}]} | {error, any()}.
100
-callback remove_expired_messages(binary()) -> {atomic, any()}.
101
-callback remove_old_messages(non_neg_integer(), binary()) -> {atomic, any()}.
102
-callback remove_user(binary(), binary()) -> any().
103
-callback read_message_headers(binary(), binary()) ->
104
    [{non_neg_integer(), jid(), jid(), undefined | erlang:timestamp(), xmlel()}] | error.
105
-callback read_message(binary(), binary(), non_neg_integer()) ->
106
    {ok, #offline_msg{}} | error.
107
-callback remove_message(binary(), binary(), non_neg_integer()) -> ok | {error, any()}.
108
-callback read_all_messages(binary(), binary()) -> [#offline_msg{}].
109
-callback remove_all_messages(binary(), binary()) -> {atomic, any()}.
110
-callback count_messages(binary(), binary()) -> {ets_cache:tag(), non_neg_integer()}.
111
-callback use_cache(binary()) -> boolean().
112
-callback cache_nodes(binary()) -> [node()].
113
-callback remove_old_messages_batch(binary(), non_neg_integer(), pos_integer()) ->
114
    {ok, non_neg_integer()} | {error, term()}.
115
-callback remove_old_messages_batch(binary(), non_neg_integer(), pos_integer(), any()) ->
116
    {ok, any(), non_neg_integer()} | {error, term()}.
117

118
-optional_callbacks([remove_expired_messages/1, remove_old_messages/2,
119
                     use_cache/1, cache_nodes/1, remove_old_messages_batch/3,
120
                     remove_old_messages_batch/4]).
121

122
depends(_Host, _Opts) ->
UNCOV
123
    [].
8✔
124

125
start(Host, Opts) ->
UNCOV
126
    Mod = gen_mod:db_mod(Opts, ?MODULE),
8✔
UNCOV
127
    Mod:init(Host, Opts),
8✔
UNCOV
128
    init_cache(Mod, Host, Opts),
8✔
UNCOV
129
    {ok, [{hook, offline_message_hook, store_packet, 50},
8✔
130
          {hook, c2s_self_presence, c2s_self_presence, 50},
131
          {hook, remove_user, remove_user, 50},
132
          {hook, disco_sm_features, get_sm_features, 50},
133
          {hook, disco_local_features, get_sm_features, 50},
134
          {hook, disco_sm_identity, get_sm_identity, 50},
135
          {hook, disco_sm_items, get_sm_items, 50},
136
          {hook, disco_info, get_info, 50},
137
          {hook, c2s_handle_info, c2s_handle_info, 50},
138
          {hook, c2s_copy_session, c2s_copy_session, 50},
139
          {hook, c2s_handle_bind2_inline, c2s_handle_bind2_inline, 50},
140
          {hook, webadmin_menu_hostuser, webadmin_menu_hostuser, 50},
141
          {hook, webadmin_page_hostuser, webadmin_page_hostuser, 50},
142
          {hook, webadmin_user, webadmin_user, 50},
143
          {hook, webadmin_user_parse_query,  webadmin_user_parse_query, 50},
144
          {iq_handler, ejabberd_sm, ?NS_FLEX_OFFLINE, handle_offline_query}]}.
145

146
stop(_Host) ->
UNCOV
147
    ok.
8✔
148

149
reload(Host, NewOpts, OldOpts) ->
UNCOV
150
    NewMod = gen_mod:db_mod(NewOpts, ?MODULE),
32✔
UNCOV
151
    OldMod = gen_mod:db_mod(OldOpts, ?MODULE),
32✔
UNCOV
152
    init_cache(NewMod, Host, NewOpts),
32✔
UNCOV
153
    if NewMod /= OldMod ->
32✔
154
            NewMod:init(Host, NewOpts);
×
155
       true ->
UNCOV
156
            ok
32✔
157
    end.
158

159
init_cache(Mod, Host, Opts) ->
UNCOV
160
    CacheOpts = [{max_size, mod_offline_opt:cache_size(Opts)},
40✔
161
                 {life_time, mod_offline_opt:cache_life_time(Opts)},
162
                 {cache_missed, false}],
UNCOV
163
    case use_cache(Mod, Host) of
40✔
164
        true ->
UNCOV
165
            ets_cache:new(?SPOOL_COUNTER_CACHE, CacheOpts);
40✔
166
        false ->
167
            ets_cache:delete(?SPOOL_COUNTER_CACHE)
×
168
    end.
169

170
-spec use_cache(module(), binary()) -> boolean().
171
use_cache(Mod, Host) ->
UNCOV
172
    case erlang:function_exported(Mod, use_cache, 1) of
3,888✔
173
        true -> Mod:use_cache(Host);
×
UNCOV
174
        false -> mod_offline_opt:use_cache(Host)
3,888✔
175
    end.
176

177
-spec cache_nodes(module(), binary()) -> [node()].
178
cache_nodes(Mod, Host) ->
UNCOV
179
    case erlang:function_exported(Mod, cache_nodes, 1) of
2,768✔
180
        true -> Mod:cache_nodes(Host);
×
UNCOV
181
        false -> ejabberd_cluster:get_nodes()
2,768✔
182
    end.
183

184
-spec flush_cache(module(), binary(), binary()) -> ok.
185
flush_cache(Mod, User, Server) ->
UNCOV
186
    case use_cache(Mod, Server) of
296✔
187
        true ->
UNCOV
188
            ets_cache:delete(?SPOOL_COUNTER_CACHE,
296✔
189
                             {User, Server},
190
                             cache_nodes(Mod, Server));
191
        false ->
192
            ok
×
193
    end.
194

195
-spec store_offline_msg(#offline_msg{}) -> ok | {error, full | any()}.
196
store_offline_msg(#offline_msg{us = {User, Server}, packet = Pkt} = Msg) ->
UNCOV
197
    UseMam = use_mam_for_user(User, Server),
1,992✔
UNCOV
198
    Mod = gen_mod:db_mod(Server, ?MODULE),
1,992✔
UNCOV
199
    case UseMam andalso xmpp:get_meta(Pkt, mam_archived, false) of
1,992✔
200
        true ->
UNCOV
201
            case count_offline_messages(User, Server) of
976✔
202
                0 ->
UNCOV
203
                    store_message_in_db(Mod, Msg);
16✔
204
                _ ->
UNCOV
205
                    case use_cache(Mod, Server) of
960✔
206
                        true ->
UNCOV
207
                            ets_cache:incr(
960✔
208
                                ?SPOOL_COUNTER_CACHE,
209
                                {User, Server}, 1,
210
                                cache_nodes(Mod, Server));
211
                        false ->
212
                            ok
×
213
                    end
214
            end;
215
        false ->
UNCOV
216
            case get_max_user_messages(User, Server) of
1,016✔
217
                infinity ->
UNCOV
218
                    store_message_in_db(Mod, Msg);
1,016✔
219
                Limit ->
220
                    Num = count_offline_messages(User, Server),
×
221
                    if Num < Limit ->
×
222
                            store_message_in_db(Mod, Msg);
×
223
                       true ->
224
                            {error, full}
×
225
                    end
226
            end
227
    end.
228

229
get_max_user_messages(User, Server) ->
UNCOV
230
    Access = mod_offline_opt:access_max_user_messages(Server),
1,032✔
UNCOV
231
    case ejabberd_shaper:match(Server, Access, jid:make(User, Server)) of
1,032✔
232
        Max when is_integer(Max) -> Max;
×
UNCOV
233
        infinity -> infinity;
1,032✔
234
        _ -> ?MAX_USER_MESSAGES
×
235
    end.
236

237
get_sm_features(Acc, _From, _To, <<"">>, _Lang) ->
UNCOV
238
    Feats = case Acc of
1,904✔
UNCOV
239
                {result, I} -> I;
1,904✔
240
                _ -> []
×
241
            end,
UNCOV
242
    {result, Feats ++ [?NS_FEATURE_MSGOFFLINE, ?NS_FLEX_OFFLINE]};
1,904✔
243

244
get_sm_features(_Acc, _From, _To, ?NS_FEATURE_MSGOFFLINE, _Lang) ->
245
    %% override all lesser features...
246
    {result, []};
×
247

248
get_sm_features(_Acc, #jid{luser = U, lserver = S}, #jid{luser = U, lserver = S},
249
                ?NS_FLEX_OFFLINE, _Lang) ->
UNCOV
250
    {result, [?NS_FLEX_OFFLINE]};
24✔
251

252
get_sm_features(Acc, _From, _To, _Node, _Lang) ->
253
    Acc.
×
254

255
get_sm_identity(Acc, #jid{luser = U, lserver = S}, #jid{luser = U, lserver = S},
256
                ?NS_FLEX_OFFLINE, _Lang) ->
UNCOV
257
    [#identity{category = <<"automation">>,
24✔
258
               type = <<"message-list">>}|Acc];
259
get_sm_identity(Acc, _From, _To, _Node, _Lang) ->
UNCOV
260
    Acc.
32✔
261

262
get_sm_items(_Acc, #jid{luser = U, lserver = S} = JID,
263
             #jid{luser = U, lserver = S},
264
             ?NS_FLEX_OFFLINE, _Lang) ->
UNCOV
265
    ejabberd_sm:route(JID, {resend_offline, false}),
8✔
UNCOV
266
            Mod = gen_mod:db_mod(S, ?MODULE),
8✔
UNCOV
267
            Hdrs = case Mod:read_message_headers(U, S) of
8✔
268
                       L when is_list(L) ->
UNCOV
269
                           L;
8✔
270
                       _ ->
271
                           []
×
272
                   end,
UNCOV
273
            BareJID = jid:remove_resource(JID),
8✔
UNCOV
274
            {result, lists:map(
8✔
275
                       fun({Seq, From, _To, _TS, _El}) ->
UNCOV
276
                               Node = integer_to_binary(Seq),
40✔
UNCOV
277
                               #disco_item{jid = BareJID,
40✔
278
                                           node = Node,
279
                                           name = jid:encode(From)}
280
                       end, Hdrs)};
281
get_sm_items(Acc, _From, _To, _Node, _Lang) ->
282
    Acc.
×
283

284
-spec get_info([xdata()], binary(), module(), binary(), binary()) -> [xdata()];
285
              ([xdata()], jid(), jid(), binary(), binary()) -> [xdata()].
286
get_info(_Acc, #jid{luser = U, lserver = S} = JID,
287
         #jid{luser = U, lserver = S}, ?NS_FLEX_OFFLINE, Lang) ->
UNCOV
288
    ejabberd_sm:route(JID, {resend_offline, false}),
24✔
UNCOV
289
    [#xdata{type = result,
24✔
290
            fields = flex_offline:encode(
291
                       [{number_of_messages, count_offline_messages(U, S)}],
292
                       Lang)}];
293
get_info(Acc, _From, _To, _Node, _Lang) ->
UNCOV
294
    Acc.
1,928✔
295

296
-spec c2s_handle_info(c2s_state(), term()) -> c2s_state().
297
c2s_handle_info(State, {resend_offline, Flag}) ->
UNCOV
298
    {stop, State#{resend_offline => Flag}};
40✔
299
c2s_handle_info(State, _) ->
UNCOV
300
    State.
48,832✔
301

302
-spec c2s_copy_session(c2s_state(), c2s_state()) -> c2s_state().
303
c2s_copy_session(State, #{resend_offline := Flag}) ->
304
    State#{resend_offline => Flag};
×
305
c2s_copy_session(State, _) ->
306
    State.
×
307

308
c2s_handle_bind2_inline({#{jid := #jid{luser = LUser, lserver = LServer}} = State, Els, Results}) ->
309
    case mod_mam:is_archiving_enabled(LUser, LServer) of
×
310
        true ->
311
            delete_all_msgs(LUser, LServer);
×
312
        false ->
313
            ok
×
314
    end,
315
    {State, Els, Results}.
×
316

317
-spec handle_offline_query(iq()) -> iq().
318
handle_offline_query(#iq{from = #jid{luser = U1, lserver = S1},
319
                         to = #jid{luser = U2, lserver = S2},
320
                         lang = Lang,
321
                         sub_els = [#offline{}]} = IQ)
322
  when {U1, S1} /= {U2, S2} ->
UNCOV
323
    Txt = ?T("Query to another users is forbidden"),
128✔
UNCOV
324
    xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang));
128✔
325
handle_offline_query(#iq{from = #jid{luser = U, lserver = S} = From,
326
                         to = #jid{luser = U, lserver = S} = _To,
327
                         type = Type, lang = Lang,
328
                         sub_els = [#offline{} = Offline]} = IQ) ->
UNCOV
329
    case {Type, Offline} of
136✔
330
        {get, #offline{fetch = true, items = [], purge = false}} ->
331
            %% TODO: report database errors
UNCOV
332
            handle_offline_fetch(From),
8✔
UNCOV
333
            xmpp:make_iq_result(IQ);
8✔
334
        {get, #offline{fetch = false, items = [_|_] = Items, purge = false}} ->
UNCOV
335
            case handle_offline_items_view(From, Items) of
32✔
UNCOV
336
                true -> xmpp:make_iq_result(IQ);
16✔
UNCOV
337
                false -> xmpp:make_error(IQ, xmpp:err_item_not_found())
16✔
338
            end;
339
        {set, #offline{fetch = false, items = [], purge = true}} ->
UNCOV
340
            case delete_all_msgs(U, S) of
8✔
341
                {atomic, ok} ->
UNCOV
342
                    xmpp:make_iq_result(IQ);
8✔
343
                _Err ->
344
                    Txt = ?T("Database failure"),
×
345
                    xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang))
×
346
            end;
347
        {set, #offline{fetch = false, items = [_|_] = Items, purge = false}} ->
UNCOV
348
            case handle_offline_items_remove(From, Items) of
24✔
UNCOV
349
                true -> xmpp:make_iq_result(IQ);
16✔
UNCOV
350
                false -> xmpp:make_error(IQ, xmpp:err_item_not_found())
8✔
351
            end;
352
        _ ->
UNCOV
353
            xmpp:make_error(IQ, xmpp:err_bad_request())
64✔
354
    end;
355
handle_offline_query(#iq{lang = Lang} = IQ) ->
UNCOV
356
    Txt = ?T("No module is handling this query"),
16✔
UNCOV
357
    xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)).
16✔
358

359
-spec handle_offline_items_view(jid(), [offline_item()]) -> boolean().
360
handle_offline_items_view(JID, Items) ->
UNCOV
361
    {U, S, R} = jid:tolower(JID),
32✔
UNCOV
362
    case use_mam_for_user(U, S) of
32✔
363
        true ->
364
            false;
×
365
        _ ->
UNCOV
366
            lists:foldl(
32✔
367
                fun(#offline_item{node = Node, action = view}, Acc) ->
UNCOV
368
                    case fetch_msg_by_node(JID, Node) of
56✔
369
                        {ok, OfflineMsg} ->
UNCOV
370
                            case offline_msg_to_route(S, OfflineMsg) of
40✔
371
                                {route, El} ->
UNCOV
372
                                    NewEl = set_offline_tag(El, Node),
40✔
UNCOV
373
                                    case ejabberd_sm:get_session_pid(U, S, R) of
40✔
374
                                        Pid when is_pid(Pid) ->
UNCOV
375
                                            ejabberd_c2s:route(Pid, {route, NewEl});
40✔
376
                                        none ->
377
                                            ok
×
378
                                    end,
UNCOV
379
                                    Acc or true;
40✔
380
                                error ->
381
                                    Acc or false
×
382
                            end;
383
                        error ->
UNCOV
384
                            Acc or false
16✔
385
                    end
386
                end, false, Items)    end.
387

388
-spec handle_offline_items_remove(jid(), [offline_item()]) -> boolean().
389
handle_offline_items_remove(JID, Items) ->
UNCOV
390
    {U, S, _R} = jid:tolower(JID),
24✔
UNCOV
391
    case use_mam_for_user(U, S) of
24✔
392
        true ->
393
            false;
×
394
        _ ->
UNCOV
395
            lists:foldl(
24✔
396
                fun(#offline_item{node = Node, action = remove}, Acc) ->
UNCOV
397
                    Acc or remove_msg_by_node(JID, Node)
32✔
398
                end, false, Items)
399
    end.
400

401
-spec set_offline_tag(message(), binary()) -> message().
402
set_offline_tag(Msg, Node) ->
UNCOV
403
    xmpp:set_subtag(Msg, #offline{items = [#offline_item{node = Node}]}).
80✔
404

405
-spec handle_offline_fetch(jid()) -> ok.
406
handle_offline_fetch(#jid{luser = U, lserver = S} = JID) ->
UNCOV
407
    ejabberd_sm:route(JID, {resend_offline, false}),
8✔
UNCOV
408
    lists:foreach(
8✔
409
        fun({Node, El}) ->
UNCOV
410
            El1 = set_offline_tag(El, Node),
40✔
UNCOV
411
            ejabberd_router:route(El1)
40✔
412
        end, read_messages(U, S)).
413

414
-spec fetch_msg_by_node(jid(), binary()) -> error | {ok, #offline_msg{}}.
415
fetch_msg_by_node(To, Seq) ->
UNCOV
416
    case catch binary_to_integer(Seq) of
56✔
417
        I when is_integer(I), I >= 0 ->
UNCOV
418
            LUser = To#jid.luser,
48✔
UNCOV
419
            LServer = To#jid.lserver,
48✔
UNCOV
420
            Mod = gen_mod:db_mod(LServer, ?MODULE),
48✔
UNCOV
421
            Mod:read_message(LUser, LServer, I);
48✔
422
        _ ->
UNCOV
423
            error
8✔
424
    end.
425

426
-spec remove_msg_by_node(jid(), binary()) -> boolean().
427
remove_msg_by_node(To, Seq) ->
UNCOV
428
    case catch binary_to_integer(Seq) of
32✔
429
        I when is_integer(I), I>= 0 ->
UNCOV
430
            LUser = To#jid.luser,
24✔
UNCOV
431
            LServer = To#jid.lserver,
24✔
UNCOV
432
            Mod = gen_mod:db_mod(LServer, ?MODULE),
24✔
UNCOV
433
            Mod:remove_message(LUser, LServer, I),
24✔
UNCOV
434
            flush_cache(Mod, LUser, LServer),
24✔
UNCOV
435
            true;
24✔
436
        _ ->
UNCOV
437
            false
8✔
438
    end.
439

440
-spec need_to_store(binary(), message()) -> boolean().
441
need_to_store(_LServer, #message{type = error}) -> false;
×
442
need_to_store(LServer, #message{type = Type} = Packet) ->
UNCOV
443
    case xmpp:has_subtag(Packet, #offline{}) of
5,864✔
444
        false ->
UNCOV
445
            case misc:unwrap_mucsub_message(Packet) of
4,904✔
446
                #message{type = groupchat} = Msg ->
UNCOV
447
                    need_to_store(LServer, Msg#message{type = chat});
24✔
448
                #message{} = Msg ->
449
                    need_to_store(LServer, Msg);
×
450
                _ ->
UNCOV
451
                    case check_store_hint(Packet) of
4,880✔
452
                        store ->
UNCOV
453
                            true;
960✔
454
                        no_store ->
UNCOV
455
                            false;
968✔
456
                        none ->
UNCOV
457
                            Store = case Type of
2,952✔
458
                                        groupchat ->
459
                                            mod_offline_opt:store_groupchat(LServer);
×
460
                                        headline ->
UNCOV
461
                                            false;
960✔
462
                                        _ ->
UNCOV
463
                                            true
1,992✔
464
                                    end,
UNCOV
465
                            case {misc:get_mucsub_event_type(Packet), Store,
2,952✔
466
                                  mod_offline_opt:store_empty_body(LServer)} of
467
                                {?NS_MUCSUB_NODES_PRESENCE, _, _} ->
468
                                    false;
×
469
                                {_, false, _} ->
UNCOV
470
                                    false;
960✔
471
                                {_, _, true} ->
472
                                    true;
×
473
                                {_, _, false} ->
474
                                    Packet#message.body /= [];
×
475
                                {_, _, unless_chat_state} ->
UNCOV
476
                                    not misc:is_standalone_chat_state(Packet)
1,992✔
477
                            end
478
                    end
479
            end;
480
        true ->
UNCOV
481
            false
960✔
482
    end.
483

484
-spec store_packet({any(), message()}) -> {any(), message()}.
485
store_packet({_Action, #message{from = From, to = To} = Packet} = Acc) ->
UNCOV
486
    case need_to_store(To#jid.lserver, Packet) of
5,840✔
487
        true ->
UNCOV
488
            case check_event(Packet) of
2,472✔
489
                true ->
UNCOV
490
                    #jid{luser = LUser, lserver = LServer} = To,
1,992✔
UNCOV
491
                    TimeStamp = erlang:timestamp(),
1,992✔
UNCOV
492
                    Expire = find_x_expire(TimeStamp, Packet),
1,992✔
UNCOV
493
                    OffMsg = #offline_msg{us = {LUser, LServer},
1,992✔
494
                                          timestamp = TimeStamp,
495
                                          expire = Expire,
496
                                          from = From,
497
                                          to = To,
498
                                          packet = Packet},
UNCOV
499
                    case store_offline_msg(OffMsg) of
1,992✔
500
                        ok ->
UNCOV
501
                            {offlined, Packet};
1,992✔
502
                        {error, Reason} ->
503
                            discard_warn_sender(Packet, Reason),
×
504
                            stop
×
505
                    end;
506
                _ ->
UNCOV
507
                    maybe_update_cache(To, Packet),
480✔
UNCOV
508
                    Acc
480✔
509
            end;
510
        false ->
UNCOV
511
            maybe_update_cache(To, Packet),
3,368✔
UNCOV
512
            Acc
3,368✔
513
    end.
514

515
-spec maybe_update_cache(jid(), message()) -> ok.
516
maybe_update_cache(#jid{lserver = Server, luser = User}, Packet) ->
UNCOV
517
    case xmpp:get_meta(Packet, mam_archived, false) of
3,848✔
518
        true ->
UNCOV
519
            Mod = gen_mod:db_mod(Server, ?MODULE),
480✔
UNCOV
520
            case use_mam_for_user(User, Server) andalso use_cache(Mod, Server) of
480✔
521
                true ->
UNCOV
522
                    ets_cache:incr(
480✔
523
                        ?SPOOL_COUNTER_CACHE,
524
                        {User, Server}, 1,
525
                        cache_nodes(Mod, Server));
526
                _ ->
527
                    ok
×
528
            end;
529
        _ ->
UNCOV
530
            ok
3,368✔
531
    end.
532

533
-spec check_store_hint(message()) -> store | no_store | none.
534
check_store_hint(Packet) ->
UNCOV
535
    case has_store_hint(Packet) of
4,880✔
536
        true ->
UNCOV
537
            store;
960✔
538
        false ->
UNCOV
539
            case has_no_store_hint(Packet) of
3,920✔
540
                true ->
UNCOV
541
                    no_store;
968✔
542
                false ->
UNCOV
543
                    none
2,952✔
544
            end
545
    end.
546

547
-spec has_store_hint(message()) -> boolean().
548
has_store_hint(Packet) ->
UNCOV
549
    xmpp:has_subtag(Packet, #hint{type = 'store'}).
4,880✔
550

551
-spec has_no_store_hint(message()) -> boolean().
552
has_no_store_hint(Packet) ->
UNCOV
553
    xmpp:has_subtag(Packet, #hint{type = 'no-store'})
3,920✔
554
        orelse
UNCOV
555
        xmpp:has_subtag(Packet, #hint{type = 'no-storage'}).
2,952✔
556

557
%% Check if the packet has any content about XEP-0022
558
-spec check_event(message()) -> boolean().
559
check_event(#message{from = From, to = To, id = ID, type = Type} = Msg) ->
UNCOV
560
    case xmpp:get_subtag(Msg, #xevent{}) of
2,472✔
561
        false ->
UNCOV
562
            true;
552✔
563
        #xevent{id = undefined, offline = false} ->
UNCOV
564
            true;
480✔
565
        #xevent{id = undefined, offline = true} ->
UNCOV
566
            NewMsg = #message{from = To, to = From, id = ID, type = Type,
480✔
567
                              sub_els = [#xevent{id = ID, offline = true}]},
UNCOV
568
            ejabberd_router:route(NewMsg),
480✔
UNCOV
569
            true;
480✔
570
        % Don't store composing events
571
        #xevent{id = V, composing = true} when V /= undefined ->
572
            false;
×
573
        % Nor composing stopped events
574
        #xevent{id = V, composing = false, delivered = false,
575
                displayed = false, offline = false} when V /= undefined ->
UNCOV
576
            false;
480✔
577
        % But store other received notifications
578
        #xevent{id = V} when V /= undefined ->
UNCOV
579
            true;
480✔
580
        _ ->
581
            false
×
582
    end.
583

584
-spec find_x_expire(erlang:timestamp(), message()) -> erlang:timestamp() | never.
585
find_x_expire(TimeStamp, Msg) ->
UNCOV
586
    case xmpp:get_subtag(Msg, #expire{seconds = 0}) of
4,180✔
587
        #expire{seconds = Int} ->
588
            {MegaSecs, Secs, MicroSecs} = TimeStamp,
×
589
            S = MegaSecs * 1000000 + Secs + Int,
×
590
            MegaSecs1 = S div 1000000,
×
591
            Secs1 = S rem 1000000,
×
592
            {MegaSecs1, Secs1, MicroSecs};
×
593
        false ->
UNCOV
594
            never
4,180✔
595
    end.
596

597
c2s_self_presence({_Pres, #{resend_offline := false}} = Acc) ->
UNCOV
598
    Acc;
8✔
599
c2s_self_presence({#presence{type = available} = NewPres, State} = Acc) ->
UNCOV
600
    NewPrio = get_priority_from_presence(NewPres),
224✔
UNCOV
601
    LastPrio = case maps:get(pres_last, State, undefined) of
224✔
UNCOV
602
                   undefined -> -1;
224✔
603
                   LastPres -> get_priority_from_presence(LastPres)
×
604
               end,
UNCOV
605
    if LastPrio < 0 andalso NewPrio >= 0 ->
224✔
UNCOV
606
            route_offline_messages(State);
224✔
607
       true ->
608
            ok
×
609
    end,
UNCOV
610
    Acc;
224✔
611
c2s_self_presence(Acc) ->
UNCOV
612
    Acc.
24✔
613

614
-spec route_offline_messages(c2s_state()) -> ok.
615
route_offline_messages(#{jid := #jid{luser = LUser, lserver = LServer}} = State) ->
UNCOV
616
    Mod = gen_mod:db_mod(LServer, ?MODULE),
224✔
UNCOV
617
    Msgs = case Mod:pop_messages(LUser, LServer) of
224✔
618
               {ok, OffMsgs} ->
UNCOV
619
                   case use_mam_for_user(LUser, LServer) of
224✔
620
                       true ->
UNCOV
621
                           flush_cache(Mod, LUser, LServer),
32✔
UNCOV
622
                           lists:map(
32✔
623
                               fun({_, #message{from = From, to = To} = Msg}) ->
UNCOV
624
                                   #offline_msg{from = From, to = To,
1,456✔
625
                                                us = {LUser, LServer},
626
                                                packet = Msg}
627
                               end, read_mam_messages(LUser, LServer, OffMsgs));
628
                       _ ->
UNCOV
629
                           flush_cache(Mod, LUser, LServer),
192✔
UNCOV
630
                           OffMsgs
192✔
631
                   end;
632
               _ ->
633
                   []
×
634
           end,
UNCOV
635
    lists:foreach(
224✔
636
        fun(OffMsg) ->
UNCOV
637
            route_offline_message(State, OffMsg)
2,432✔
638
        end, Msgs).
639

640
-spec route_offline_message(c2s_state(), #offline_msg{}) -> ok.
641
route_offline_message(#{lserver := LServer} = State,
642
                      #offline_msg{expire = Expire} = OffMsg) ->
UNCOV
643
    case offline_msg_to_route(LServer, OffMsg) of
2,432✔
644
        error ->
645
            ok;
×
646
        {route, Msg} ->
UNCOV
647
            case is_message_expired(Expire, Msg) of
2,432✔
648
                true ->
649
                    ok;
×
650
                false ->
UNCOV
651
                    case privacy_check_packet(State, Msg, in) of
2,432✔
UNCOV
652
                        allow -> ejabberd_router:route(Msg);
2,432✔
653
                        deny -> ok
×
654
                    end
655
            end
656
    end.
657

658
-spec is_message_expired(erlang:timestamp() | never, message()) -> boolean().
659
is_message_expired(Expire, Msg) ->
UNCOV
660
    TS = erlang:timestamp(),
2,432✔
UNCOV
661
    Expire1 = case Expire of
2,432✔
UNCOV
662
                  undefined -> find_x_expire(TS, Msg);
2,188✔
UNCOV
663
                  _ -> Expire
244✔
664
              end,
UNCOV
665
    Expire1 /= never andalso Expire1 =< TS.
2,432✔
666

667
-spec privacy_check_packet(c2s_state(), stanza(), in | out) -> allow | deny.
668
privacy_check_packet(#{lserver := LServer} = State, Pkt, Dir) ->
UNCOV
669
    ejabberd_hooks:run_fold(privacy_check_packet,
2,432✔
670
                            LServer, allow, [State, Pkt, Dir]).
671

672
remove_expired_messages(Server) ->
673
    LServer = jid:nameprep(Server),
×
674
    Mod = gen_mod:db_mod(LServer, ?MODULE),
×
675
    case erlang:function_exported(Mod, remove_expired_messages, 1) of
×
676
        true ->
677
            Ret = Mod:remove_expired_messages(LServer),
×
678
            ets_cache:clear(?SPOOL_COUNTER_CACHE),
×
679
            Ret;
×
680
        false ->
681
            erlang:error(not_implemented)
×
682
    end.
683

684
remove_old_messages(Days, Server) ->
685
    LServer = jid:nameprep(Server),
×
686
    Mod = gen_mod:db_mod(LServer, ?MODULE),
×
687
    case erlang:function_exported(Mod, remove_old_messages, 2) of
×
688
        true ->
689
            Ret = Mod:remove_old_messages(Days, LServer),
×
690
            ets_cache:clear(?SPOOL_COUNTER_CACHE),
×
691
            Ret;
×
692
        false ->
693
            erlang:error(not_implemented)
×
694
    end.
695

696
-spec remove_user(binary(), binary()) -> ok.
697
remove_user(User, Server) ->
UNCOV
698
    LUser = jid:nodeprep(User),
40✔
UNCOV
699
    LServer = jid:nameprep(Server),
40✔
UNCOV
700
    Mod = gen_mod:db_mod(LServer, ?MODULE),
40✔
UNCOV
701
    Mod:remove_user(LUser, LServer),
40✔
UNCOV
702
    flush_cache(Mod, LUser, LServer).
40✔
703

704
%% Helper functions:
705

706
-spec check_if_message_should_be_bounced(message()) -> boolean().
707
check_if_message_should_be_bounced(Packet) ->
708
    case Packet of
×
709
        #message{type = groupchat, to = #jid{lserver = LServer}} ->
710
            mod_offline_opt:bounce_groupchat(LServer);
×
711
        #message{to = #jid{lserver = LServer}} ->
712
            case misc:is_mucsub_message(Packet) of
×
713
                true ->
714
                    mod_offline_opt:bounce_groupchat(LServer);
×
715
                _ ->
716
                    true
×
717
            end;
718
        _ ->
719
            true
×
720
    end.
721

722
%% Warn senders that their messages have been discarded:
723

724
-spec discard_warn_sender(message(), full | any()) -> ok.
725
discard_warn_sender(Packet, Reason) ->
726
    case check_if_message_should_be_bounced(Packet) of
×
727
        true ->
728
            Lang = xmpp:get_lang(Packet),
×
729
            Err = case Reason of
×
730
                      full ->
731
                          ErrText = ?T("Your contact offline message queue is "
×
732
                                       "full. The message has been discarded."),
733
                          xmpp:err_resource_constraint(ErrText, Lang);
×
734
                      _ ->
735
                          ErrText = ?T("Database failure"),
×
736
                          xmpp:err_internal_server_error(ErrText, Lang)
×
737
                  end,
738
            ejabberd_router:route_error(Packet, Err);
×
739
        _ ->
740
            ok
×
741
    end.
742

743
%%%
744
%%% Commands
745
%%%
746

747
get_offline_messages(User, Server) ->
748
    LUser = jid:nodeprep(User),
×
749
    LServer = jid:nameprep(Server),
×
750
    Mod = gen_mod:db_mod(LServer, ?MODULE),
×
751
    HdrsAll = case Mod:read_message_headers(LUser, LServer) of
×
752
                  error -> [];
×
753
                  L -> L
×
754
              end,
755
    format_user_queue(HdrsAll).
×
756

757
%%%
758
%%% WebAdmin
759
%%%
760

761
webadmin_menu_hostuser(Acc, _Host, _Username, _Lang) ->
UNCOV
762
    Acc ++ [{<<"queue">>, <<"Offline Queue">>}].
24✔
763

764
webadmin_page_hostuser(_, Host, U,
765
              #request{us = _US, path = [<<"queue">> | RPath],
766
                       lang = Lang} = R) ->
767
    US = {U, Host},
×
768
    PageTitle = str:translate_and_format(Lang, ?T("~ts's Offline Messages Queue"), [us_to_list(US)]),
×
769
    Head = ?H1GL(PageTitle, <<"modules/#mod_offline">>, <<"mod_offline">>),
×
770
    Res = make_command(get_offline_messages, R, [{<<"user">>, U},
×
771
                                                     {<<"host">>, Host}],
772
                        [{table_options, {10, RPath}},
773
                         {result_links, [{packet, paragraph, 1, <<"">>}]}]),
774
    {stop, Head ++ [Res]};
×
775
webadmin_page_hostuser(Acc, _, _, _) -> Acc.
×
776

777
get_offline_els(LUser, LServer) ->
778
    [Packet || {_Seq, Packet} <- read_messages(LUser, LServer)].
×
779

780
-spec offline_msg_to_route(binary(), #offline_msg{}) ->
781
                                  {route, message()} | error.
782
offline_msg_to_route(LServer, #offline_msg{from = From, to = To} = R) ->
UNCOV
783
    CodecOpts = ejabberd_config:codec_options(),
2,472✔
UNCOV
784
    try xmpp:decode(R#offline_msg.packet, ?NS_CLIENT, CodecOpts) of
2,472✔
785
        Pkt ->
UNCOV
786
            Pkt1 = xmpp:set_from_to(Pkt, From, To),
2,472✔
UNCOV
787
            Pkt2 = add_delay_info(Pkt1, LServer, R#offline_msg.timestamp),
2,472✔
UNCOV
788
            {route, Pkt2}
2,472✔
789
    catch _:{xmpp_codec, Why} ->
790
            ?ERROR_MSG("Failed to decode packet ~p of user ~ts: ~ts",
×
791
                       [R#offline_msg.packet, jid:encode(To),
792
                        xmpp:format_error(Why)]),
×
793
            error
×
794
    end.
795

796
-spec read_messages(binary(), binary()) -> [{binary(), message()}].
797
read_messages(LUser, LServer) ->
UNCOV
798
    Res = case read_db_messages(LUser, LServer) of
8✔
799
              error ->
800
                  [];
×
801
              L when is_list(L) ->
UNCOV
802
                  L
8✔
803
          end,
UNCOV
804
    case use_mam_for_user(LUser, LServer) of
8✔
805
        true ->
806
            read_mam_messages(LUser, LServer, Res);
×
807
        _ ->
UNCOV
808
            Res
8✔
809
    end.
810

811
-spec read_db_messages(binary(), binary()) -> [{binary(), message()}] | error.
812
read_db_messages(LUser, LServer) ->
UNCOV
813
    Mod = gen_mod:db_mod(LServer, ?MODULE),
24✔
UNCOV
814
    CodecOpts = ejabberd_config:codec_options(),
24✔
UNCOV
815
    case Mod:read_message_headers(LUser, LServer) of
24✔
816
        error ->
817
            error;
×
818
        L ->
UNCOV
819
            lists:flatmap(
24✔
820
                fun({Seq, From, To, TS, El}) ->
UNCOV
821
                    Node = integer_to_binary(Seq),
40✔
UNCOV
822
                    try xmpp:decode(El, ?NS_CLIENT, CodecOpts) of
40✔
823
                        Pkt ->
UNCOV
824
                            Node = integer_to_binary(Seq),
40✔
UNCOV
825
                            Pkt1 = add_delay_info(Pkt, LServer, TS),
40✔
UNCOV
826
                            Pkt2 = xmpp:set_from_to(Pkt1, From, To),
40✔
UNCOV
827
                            [{Node, Pkt2}]
40✔
828
                    catch _:{xmpp_codec, Why} ->
829
                        ?ERROR_MSG("Failed to decode packet ~p "
×
830
                                   "of user ~ts: ~ts",
831
                                   [El, jid:encode(To),
832
                                    xmpp:format_error(Why)]),
×
833
                        []
×
834
                    end
835
                end, L)
836
    end.
837

838
-spec parse_marker_messages(binary(), [#offline_msg{} | {any(), message()}]) ->
839
    {integer() | none, [message()]}.
840
parse_marker_messages(LServer, ReadMsgs) ->
UNCOV
841
    {Timestamp, ExtraMsgs} = lists:foldl(
48✔
842
        fun({_Node, #message{id = <<"ActivityMarker">>,
843
                             body = [], type = error} = Msg}, {T, E}) ->
844
            case xmpp:get_subtag(Msg, #delay{stamp = {0,0,0}}) of
×
845
                #delay{stamp = Time} ->
846
                    if T == none orelse T > Time ->
×
847
                        {Time, E};
×
848
                        true ->
849
                            {T, E}
×
850
                    end
851
            end;
852
           (#offline_msg{from = From, to = To, timestamp = TS, packet = Pkt},
853
            {T, E}) ->
UNCOV
854
               try xmpp:decode(Pkt) of
16✔
855
                   #message{id = <<"ActivityMarker">>,
856
                            body = [], type = error} = Msg ->
857
                       TS2 = case TS of
×
858
                                 undefined ->
859
                                     case xmpp:get_subtag(Msg, #delay{stamp = {0,0,0}}) of
×
860
                                         #delay{stamp = TS0} ->
861
                                             TS0;
×
862
                                         _ ->
863
                                             erlang:timestamp()
×
864
                                     end;
865
                                 _ ->
866
                                     TS
×
867
                             end,
868
                       if T == none orelse T > TS2 ->
×
869
                           {TS2, E};
×
870
                           true ->
871
                               {T, E}
×
872
                       end;
873
                   Decoded ->
UNCOV
874
                       Pkt1 = add_delay_info(Decoded, LServer, TS),
16✔
UNCOV
875
                       {T, [xmpp:set_from_to(Pkt1, From, To) | E]}
16✔
876
               catch _:{xmpp_codec, _Why} ->
877
                   {T, E}
×
878
               end;
879
           ({_Node, Msg}, {T, E}) ->
880
               {T, [Msg | E]}
×
881
        end, {none, []}, ReadMsgs),
UNCOV
882
    Start = case {Timestamp, ExtraMsgs} of
48✔
883
                {none, [First|_]} ->
UNCOV
884
                    case xmpp:get_subtag(First, #delay{stamp = {0,0,0}}) of
16✔
885
                        #delay{stamp = {Mega, Sec, Micro}} ->
UNCOV
886
                            {Mega, Sec, Micro+1};
16✔
887
                        _ ->
888
                            none
×
889
                    end;
890
                {none, _} ->
UNCOV
891
                    none;
32✔
892
                _ ->
893
                    Timestamp
×
894
            end,
UNCOV
895
    {Start, ExtraMsgs}.
48✔
896

897
-spec read_mam_messages(binary(), binary(), [#offline_msg{} | {any(), message()}]) ->
898
    [{integer(), message()}].
899
read_mam_messages(LUser, LServer, ReadMsgs) ->
UNCOV
900
    {Start, ExtraMsgs} = parse_marker_messages(LServer, ReadMsgs),
32✔
UNCOV
901
    AllMsgs = case Start of
32✔
902
                  none ->
UNCOV
903
                      ExtraMsgs;
16✔
904
                  _ ->
UNCOV
905
                      MaxOfflineMsgs = case get_max_user_messages(LUser, LServer) of
16✔
906
                                           Number when is_integer(Number) ->
907
                                               max(0, Number - length(ExtraMsgs));
×
908
                                           infinity ->
UNCOV
909
                                               undefined
16✔
910
                                       end,
UNCOV
911
                      JID = jid:make(LUser, LServer, <<>>),
16✔
UNCOV
912
                      {MamMsgs, _, _} = mod_mam:select(LServer, JID, JID,
16✔
913
                                                       [{start, Start}],
914
                                                       #rsm_set{max = MaxOfflineMsgs,
915
                                                                before = <<"9999999999999999">>},
916
                                                       chat, only_messages),
UNCOV
917
                      MamMsgs2 = lists:map(
16✔
918
                          fun({_, _, #forwarded{sub_els = [MM | _], delay = #delay{stamp = MMT}}}) ->
UNCOV
919
                              add_delay_info(MM, LServer, MMT)
1,440✔
920
                          end, MamMsgs),
921

UNCOV
922
                      ExtraMsgs ++ MamMsgs2
16✔
923
              end,
UNCOV
924
    AllMsgs2 = lists:sort(
32✔
925
        fun(A, B) ->
UNCOV
926
            DA = case xmpp:get_subtag(A, #stanza_id{by = #jid{}}) of
3,098✔
927
                     #stanza_id{id = IDA} ->
UNCOV
928
                         IDA;
3,098✔
929
                     _ -> case xmpp:get_subtag(A, #delay{stamp = {0,0,0}}) of
×
930
                              #delay{stamp = STA} ->
931
                                  integer_to_binary(misc:now_to_usec(STA));
×
932
                              _ ->
933
                                  <<"unknown">>
×
934
                          end
935
                 end,
UNCOV
936
            DB = case xmpp:get_subtag(B, #stanza_id{by = #jid{}}) of
3,098✔
937
                     #stanza_id{id = IDB} ->
UNCOV
938
                         IDB;
3,098✔
939
                     _ -> case xmpp:get_subtag(B, #delay{stamp = {0,0,0}}) of
×
940
                              #delay{stamp = STB} ->
941
                                  integer_to_binary(misc:now_to_usec(STB));
×
942
                              _ ->
943
                                  <<"unknown">>
×
944
                          end
945
                 end,
UNCOV
946
            DA < DB
3,098✔
947
        end, AllMsgs),
UNCOV
948
    {AllMsgs3, _} = lists:mapfoldl(
32✔
949
        fun(Msg, Counter) ->
UNCOV
950
            {{Counter, Msg}, Counter + 1}
1,456✔
951
        end, 1, AllMsgs2),
UNCOV
952
    AllMsgs3.
32✔
953

954
-spec count_mam_messages(binary(), binary(), [#offline_msg{} | {any(), message()}] | error) ->
955
    {cache, integer()} | {nocache, integer()}.
956
count_mam_messages(_LUser, _LServer, error) ->
957
    {nocache, 0};
×
958
count_mam_messages(LUser, LServer, ReadMsgs) ->
UNCOV
959
    {Start, ExtraMsgs} = parse_marker_messages(LServer, ReadMsgs),
16✔
UNCOV
960
    case Start of
16✔
961
        none ->
UNCOV
962
            {cache, length(ExtraMsgs)};
16✔
963
        _ ->
964
            MaxOfflineMsgs = case get_max_user_messages(LUser, LServer) of
×
965
                                 Number when is_integer(Number) -> Number - length(ExtraMsgs);
×
966
                                 infinity -> undefined
×
967
                             end,
968
            JID = jid:make(LUser, LServer, <<>>),
×
969
            {_, _, Count} = mod_mam:select(LServer, JID, JID,
×
970
                                           [{start, Start}],
971
                                           #rsm_set{max = MaxOfflineMsgs,
972
                                                    before = <<"9999999999999999">>},
973
                                           chat, only_count),
974
            {cache, Count + length(ExtraMsgs)}
×
975
    end.
976

977
format_user_queue(Hdrs) ->
978
    lists:map(
×
979
      fun({_Seq, From, To, TS, El}) ->
980
              FPacket = ejabberd_web_admin:pretty_print_xml(El),
×
981
              SFrom = jid:encode(From),
×
982
              STo = jid:encode(To),
×
983
              Time = case TS of
×
984
                         undefined ->
985
                             Stamp = fxml:get_path_s(El, [{elem, <<"delay">>},
×
986
                                                          {attr, <<"stamp">>}]),
987
                             try xmpp_util:decode_timestamp(Stamp) of
×
988
                                 {_, _, _} = Now -> format_time(Now)
×
989
                             catch _:_ ->
990
                                     <<"">>
×
991
                             end;
992
                         {_, _, _} = Now ->
993
                             format_time(Now)
×
994
                     end,
995
              {Time, SFrom, STo, FPacket}
×
996
      end, Hdrs).
997

998
format_time(Now) ->
999
    {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:now_to_local_time(Now),
×
1000
    str:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w",
×
1001
               [Year, Month, Day, Hour, Minute,        Second]).
1002

1003
us_to_list({User, Server}) ->
1004
    jid:encode({User, Server, <<"">>}).
×
1005

1006
get_queue_length(LUser, LServer) ->
1007
    count_offline_messages(LUser, LServer).
×
1008

1009
webadmin_user(Acc, User, Server, R) ->
UNCOV
1010
    Acc ++ [make_command(get_offline_count, R, [{<<"user">>, User}, {<<"host">>, Server}],
24✔
1011
                         [{result_links, [{value, arg_host, 4, <<"user/", User/binary, "/queue/">>}]}]
1012
                        )].
1013

1014
%%%
1015
%%%
1016
%%%
1017

1018
-spec delete_all_msgs(binary(), binary()) -> {atomic, any()}.
1019
delete_all_msgs(User, Server) ->
UNCOV
1020
    LUser = jid:nodeprep(User),
8✔
UNCOV
1021
    LServer = jid:nameprep(Server),
8✔
UNCOV
1022
    Mod = gen_mod:db_mod(LServer, ?MODULE),
8✔
UNCOV
1023
    Ret = Mod:remove_all_messages(LUser, LServer),
8✔
UNCOV
1024
    flush_cache(Mod, LUser, LServer),
8✔
UNCOV
1025
    Ret.
8✔
1026

1027
webadmin_user_parse_query(_, <<"removealloffline">>,
1028
                          User, Server, _Query) ->
1029
    case delete_all_msgs(User, Server) of
×
1030
        {atomic, ok} ->
1031
            ?INFO_MSG("Removed all offline messages for ~ts@~ts",
×
1032
                      [User, Server]),
×
1033
            {stop, ok};
×
1034
        Err ->
1035
            ?ERROR_MSG("Failed to remove offline messages: ~p",
×
1036
                       [Err]),
×
1037
            {stop, error}
×
1038
    end;
1039
webadmin_user_parse_query(Acc, _Action, _User, _Server,
1040
                          _Query) ->
UNCOV
1041
    Acc.
40✔
1042

1043
%% Returns as integer the number of offline messages for a given user
1044
-spec count_offline_messages(binary(), binary()) -> non_neg_integer().
1045
count_offline_messages(User, Server) ->
UNCOV
1046
    LUser = jid:nodeprep(User),
1,080✔
UNCOV
1047
    LServer = jid:nameprep(Server),
1,080✔
UNCOV
1048
    Mod = gen_mod:db_mod(LServer, ?MODULE),
1,080✔
UNCOV
1049
    case use_mam_for_user(User, Server) of
1,080✔
1050
        true ->
UNCOV
1051
            case use_cache(Mod, LServer) of
984✔
1052
                true ->
UNCOV
1053
                    ets_cache:lookup(
984✔
1054
                        ?SPOOL_COUNTER_CACHE, {LUser, LServer},
1055
                        fun() ->
UNCOV
1056
                            Res = read_db_messages(LUser, LServer),
16✔
UNCOV
1057
                            count_mam_messages(LUser, LServer, Res)
16✔
1058
                        end);
1059
                false ->
1060
                    Res = read_db_messages(LUser, LServer),
×
1061
                    ets_cache:untag(count_mam_messages(LUser, LServer, Res))
×
1062
            end;
1063
        _ ->
UNCOV
1064
            case use_cache(Mod, LServer) of
96✔
1065
                true ->
UNCOV
1066
                    ets_cache:lookup(
96✔
1067
                        ?SPOOL_COUNTER_CACHE, {LUser, LServer},
1068
                        fun() ->
UNCOV
1069
                            Mod:count_messages(LUser, LServer)
54✔
1070
                        end);
1071
                false ->
1072
                    ets_cache:untag(Mod:count_messages(LUser, LServer))
×
1073
            end
1074
    end.
1075

1076
-spec store_message_in_db(module(), #offline_msg{}) -> ok | {error, any()}.
1077
store_message_in_db(Mod, #offline_msg{us = {User, Server}} = Msg) ->
UNCOV
1078
    case Mod:store_message(Msg) of
1,032✔
1079
        ok ->
UNCOV
1080
            case use_cache(Mod, Server) of
1,032✔
1081
                true ->
UNCOV
1082
                    ets_cache:incr(
1,032✔
1083
                      ?SPOOL_COUNTER_CACHE,
1084
                      {User, Server}, 1,
1085
                      cache_nodes(Mod, Server));
1086
                false ->
1087
                    ok
×
1088
            end;
1089
        Err ->
1090
            Err
×
1091
    end.
1092

1093
-spec add_delay_info(message(), binary(),
1094
                     undefined | erlang:timestamp()) -> message().
1095
add_delay_info(Packet, LServer, TS) ->
UNCOV
1096
    NewTS = case TS of
3,968✔
UNCOV
1097
                undefined -> erlang:timestamp();
2,260✔
UNCOV
1098
                _ -> TS
1,708✔
1099
            end,
UNCOV
1100
    Packet1 = xmpp:put_meta(Packet, from_offline, true),
3,968✔
UNCOV
1101
    misc:add_delay_info(Packet1, jid:make(LServer), NewTS,
3,968✔
1102
                        <<"Offline storage">>).
1103

1104
-spec get_priority_from_presence(presence()) -> integer().
1105
get_priority_from_presence(#presence{priority = Prio}) ->
UNCOV
1106
    case Prio of
224✔
UNCOV
1107
        undefined -> 0;
224✔
1108
        _ -> Prio
×
1109
    end.
1110

1111
export(LServer) ->
1112
    Mod = gen_mod:db_mod(LServer, ?MODULE),
×
1113
    Mod:export(LServer).
×
1114

1115
import_info() ->
1116
    [{<<"spool">>, 4}].
×
1117

1118
import_start(LServer, DBType) ->
1119
    Mod = gen_mod:db_mod(DBType, ?MODULE),
×
1120
    Mod:import(LServer, []).
×
1121

1122
import(LServer, {sql, _}, DBType, <<"spool">>,
1123
       [LUser, XML, _Seq, _TimeStamp]) ->
1124
    El = fxml_stream:parse_element(XML),
×
1125
    #message{from = From, to = To} = Msg = xmpp:decode(El, ?NS_CLIENT, [ignore_els]),
×
1126
    TS = case xmpp:get_subtag(Msg, #delay{stamp = {0,0,0}}) of
×
1127
             #delay{stamp = {MegaSecs, Secs, _}} ->
1128
                 {MegaSecs, Secs, 0};
×
1129
             false ->
1130
                 erlang:timestamp()
×
1131
         end,
1132
    US = {LUser, LServer},
×
1133
    Expire = find_x_expire(TS, Msg),
×
1134
    OffMsg = #offline_msg{us = US, packet = El,
×
1135
                          from = From, to = To,
1136
                          timestamp = TS, expire = Expire},
1137
    Mod = gen_mod:db_mod(DBType, ?MODULE),
×
1138
    Mod:import(OffMsg).
×
1139

1140
use_mam_for_user(_User, Server) ->
UNCOV
1141
    mod_offline_opt:use_mam_for_storage(Server).
3,840✔
1142

1143
mod_opt_type(access_max_user_messages) ->
UNCOV
1144
    econf:shaper();
8✔
1145
mod_opt_type(store_groupchat) ->
UNCOV
1146
    econf:bool();
8✔
1147
mod_opt_type(bounce_groupchat) ->
UNCOV
1148
    econf:bool();
8✔
1149
mod_opt_type(use_mam_for_storage) ->
UNCOV
1150
    econf:bool();
8✔
1151
mod_opt_type(store_empty_body) ->
UNCOV
1152
    econf:either(
8✔
1153
      unless_chat_state,
1154
      econf:bool());
1155
mod_opt_type(db_type) ->
UNCOV
1156
    econf:db_type(?MODULE);
8✔
1157
mod_opt_type(use_cache) ->
UNCOV
1158
    econf:bool();
8✔
1159
mod_opt_type(cache_size) ->
UNCOV
1160
    econf:pos_int(infinity);
8✔
1161
mod_opt_type(cache_life_time) ->
UNCOV
1162
    econf:timeout(second, infinity).
8✔
1163

1164
mod_options(Host) ->
UNCOV
1165
    [{db_type, ejabberd_config:default_db(Host, ?MODULE)},
8✔
1166
     {access_max_user_messages, max_user_offline_messages},
1167
     {store_empty_body, unless_chat_state},
1168
     {use_mam_for_storage, false},
1169
     {bounce_groupchat, false},
1170
     {store_groupchat, false},
1171
     {use_cache, ejabberd_option:use_cache(Host)},
1172
     {cache_size, ejabberd_option:cache_size(Host)},
1173
     {cache_life_time, ejabberd_option:cache_life_time(Host)}].
1174

1175
mod_doc() ->
1176
    #{desc =>
×
1177
          [?T("This module implements "
1178
              "https://xmpp.org/extensions/xep-0160.html"
1179
              "[XEP-0160: Best Practices for Handling Offline Messages] "
1180
              "and https://xmpp.org/extensions/xep-0013.html"
1181
              "[XEP-0013: Flexible Offline Message Retrieval]. "
1182
              "This means that all messages sent to an offline user "
1183
              "will be stored on the server until that user comes online "
1184
              "again. Thus it is very similar to how email works. A user "
1185
              "is considered offline if no session presence priority > 0 "
1186
              "are currently open."), "",
1187
           ?T("The _`delete_expired_messages`_ API allows to delete expired messages, "
1188
              "and _`delete_old_messages`_ API deletes older ones.")],
1189
      opts =>
1190
          [{access_max_user_messages,
1191
            #{value => ?T("AccessName"),
1192
              desc =>
1193
                  ?T("This option defines which access rule will be "
1194
                     "enforced to limit the maximum number of offline "
1195
                     "messages that a user can have (quota). When a user "
1196
                     "has too many offline messages, any new messages that "
1197
                     "they receive are discarded, and a '<resource-constraint/>' "
1198
                     "error is returned to the sender. The default value is "
1199
                     "'max_user_offline_messages'.")}},
1200
           {store_empty_body,
1201
            #{value => "true | false | unless_chat_state",
1202
              desc =>
1203
                  ?T("Whether or not to store messages that lack a '<body/>' "
1204
                     "element. The default value is 'unless_chat_state', "
1205
                     "which tells ejabberd to store messages even if they "
1206
                     "lack the '<body/>' element, unless they only contain a "
1207
                     "chat state notification (as defined in "
1208
                     "https://xmpp.org/extensions/xep-0085.html"
1209
                     "[XEP-0085: Chat State Notifications].")}},
1210
           {store_groupchat,
1211
            #{value => "true | false",
1212
              desc =>
1213
                  ?T("Whether or not to store groupchat messages. "
1214
                     "The default value is 'false'.")}},
1215
           {use_mam_for_storage,
1216
            #{value => "true | false",
1217
              desc =>
1218
                  ?T("This is an experimental option. By enabling the option, "
1219
                     "this module uses the 'archive' table from _`mod_mam`_ instead "
1220
                     "of its own spool table to retrieve the messages received "
1221
                     "when the user was offline. This allows client "
1222
                     "developers to slowly drop XEP-0160 and rely on XEP-0313 "
1223
                     "instead. It also further reduces the "
1224
                     "storage required when you enable MucSub. Enabling this "
1225
                     "option has a known drawback for the moment: most of "
1226
                     "flexible message retrieval queries don't work (those that "
1227
                     "allow retrieval/deletion of messages by id), but this "
1228
                     "specification is not widely used. The default value "
1229
                     "is 'false' to keep former behaviour as default.")}},
1230
           {bounce_groupchat,
1231
            #{value => "true | false",
1232
              desc =>
1233
                  ?T("This option is use the disable an optimization that "
1234
                     "avoids bouncing error messages when groupchat messages "
1235
                     "could not be stored as offline. It will reduce chat "
1236
                     "room load, without any drawback in standard use cases. "
1237
                     "You may change default value only if you have a custom "
1238
                     "module which uses offline hook after 'mod_offline'. This "
1239
                     "option can be useful for both standard MUC and MucSub, "
1240
                     "but the bounce is much more likely to happen in the context "
1241
                     "of MucSub, so it is even more important to have it on "
1242
                     "large MucSub services. The default value is 'false', meaning "
1243
                     "the optimization is enabled.")}},
1244
           {db_type,
1245
            #{value => "mnesia | sql",
1246
              desc =>
1247
                  ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}},
1248
           {use_cache,
1249
            #{value => "true | false",
1250
              desc =>
1251
                  ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
1252
           {cache_size,
1253
            #{value => "pos_integer() | infinity",
1254
              desc =>
1255
                  ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
1256
           {cache_life_time,
1257
            #{value => "timeout()",
1258
              desc =>
1259
                  ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}],
1260
      example =>
1261
          [{?T("This example allows power users to have as much as 5000 "
1262
               "offline messages, administrators up to 2000, and all the "
1263
               "other users up to 100:"),
1264
            ["acl:",
1265
             "  admin:",
1266
             "    user:",
1267
             "      - admin1@localhost",
1268
             "      - admin2@example.org",
1269
             "  poweruser:",
1270
             "    user:",
1271
             "      - bob@example.org",
1272
             "      - jane@example.org",
1273
             "",
1274
             "shaper_rules:",
1275
             "  max_user_offline_messages:",
1276
             "    - 5000: poweruser",
1277
             "    - 2000: admin",
1278
             "    - 100",
1279
             "",
1280
             "modules:",
1281
             "  ...",
1282
             "  mod_offline:",
1283
             "    access_max_user_messages: max_user_offline_messages",
1284
             "  ..."
1285
            ]}]}.
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