Skip to content

Commit 2bff540

Browse files
committed
net_nntp: Allow articles to be expired.
Add configuration settings that allow retention to be controlled on a per-group/pattern basis, with control over how the Expires header is obeyed, if at all. Efficiency at present is not great if a lot of articles are expired at once, but this can be improved in the future.
1 parent a860d58 commit 2bff540

11 files changed

Lines changed: 755 additions & 205 deletions

File tree

configs/net_nntp.conf

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ local = Local to this news server.
5656
[articles] ; Global settings that apply to all article processing (acceptance from both readers and peers alike)
5757
maxacceptage=10 ; Articles older than this many days will be rejected (allowing the history file to forget entries after a while, rather than growing unbounded). Default is 10, minimum is 3.
5858
; This is equivalent to the 'artcutoff' setting in INN.
59+
minhistory=11 ; Minimum number of days to keep history records for articles, regardless of whether they are present in the spool. If 0, expired articles are immediately removed from history.
60+
; Best practice is to set this to maxacceptage+1, so that expired articles are not accepted again (if they were expired during a time where they could be accepted again).
61+
; Since maxacceptage defaults to 10, this should usually be set to 11. Similar to INN's 'remember' setting in expire.ctl
5962
maxsize=100000 ; Absolute max article size, in bytes. Default is 100000 (~100 KB). Should be at least as large as maxpostsize in [readers].
6063
maxgroups=100 ; Absolute max number of groups for an article. Applies to local groups authorized for a sender. Should be at least as large as maxpostgroups in [readers].
6164
;maxcrossposts=100 ; Absolute max number of groups that may appear in the Newsgroups or Xref header. 'maxgroups' doesn't need to be any larger than this value. Default is 10.
@@ -112,6 +115,24 @@ minreadpriv = 1 ; If greater than 0, further restrict reading to users with this
112115
minpostpriv = 1 ; If greater than 0, further restrict reading to users with this privilege level (and block guests).
113116
minapprovepriv = 2 ; Restrict Approval headers to users with this privilege level. Guests are always blocked. Minimum is 1, if set to 0, approvals will be disabled for this ACL.
114117

118+
; Article retention (expiration) settings. The first matching entry is used, so put your catch-all at the end!
119+
; The format of an entry is similar to INN's expire.ctl:
120+
; <pattern> = <min>:<default>:<max>/<flags>
121+
; min = The minimum amount of time to retain ANY article (regardless of Expires header)
122+
; default = The amount of time to retain articles that do NOT contain an Expires header
123+
; max = The maximum amount of time to retain ANY article (regardless of Expires header)
124+
; Decimal values may be specified for any of the above 3 values for fractions of a day.
125+
; flags = Optional flags (reserved). Currently, no flags are supported.
126+
; So to ignore the Expires header, you would set these all the same. To perfectly honor Expires, you'd set min to 0 and max to never.
127+
; The arrival time of an article is used for expiration, not the article's Date.
128+
; To actually expire articles, run 'rsysop "news expire"' from a cron job at a suitable interval.
129+
[retention]
130+
;local.* = never:never:never ; Retain articles for local.* indefinitely (ignoring Expires header)
131+
;local.foo = 0:180:never ; Retain articles for local.foo for 180 days by default, but fully honor Expires headers
132+
;misc.very.busy.group = 0.25:10:20 ; Generally delete articles after 10 days, and after no more than 20 days. Keep all articles for at least 6 hours.
133+
;news.lists.filters = 3:3:3 ; Only keep automated filtering messages for 3 days
134+
* = never:never:never ; the implicit default entry (never expire any articles). However, there SHOULD be at least one pattern matching every group (even just *) or you will see warnings.
135+
115136
; Feed configuration:
116137
[incoming] ; Specify other news servers that can send us articles, in peer = groups[/distributions] format. These are peers that "push" articles to us (similar to incoming.conf in INN).
117138
; You can specify a local username on this server, an IPv4 address (CIDR range allowed), or a hostname (though this may incur a performance penalty).

nets/net_nntp.c

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757

5858
#define MAIN_NNTP_FILE
5959
#include "net_nntp/nntp.h"
60+
#include "net_nntp/nntp_history.h"
6061
#include "net_nntp/nntp_feed.h"
6162
#include "net_nntp/nntp_suck.h"
6263

@@ -106,6 +107,7 @@ static unsigned int max_article_size = 100000; /* ~100 KB should be plenty */
106107
unsigned int max_groups = 100; /* used extern in nntp_suck.c */
107108
static unsigned int max_crossposts = 10;
108109
static unsigned int max_accept_age = 10;
110+
unsigned int min_history = 11; /* used extern in nntp_history.c */
109111
static unsigned int min_lines = 0;
110112
static unsigned int max_lines = 0;
111113
static char poisongroups[NNTP_MAX_LINE_LENGTH];
@@ -2151,6 +2153,20 @@ static int cli_delarticle(struct bbs_cli_args *a)
21512153
return 0;
21522154
}
21532155

2156+
static int cli_expire(struct bbs_cli_args *a)
2157+
{
2158+
const char *group = a->argv[2];
2159+
int res;
2160+
2161+
res = history_expire(group); /* OK if group is NULL */
2162+
if (res < 0) {
2163+
bbs_dprintf(a->fdout, "Failed to expire articles\n");
2164+
return -1;
2165+
}
2166+
bbs_dprintf(a->fdout, "Expired %d article%s\n", res, ESS(res));
2167+
return 0;
2168+
}
2169+
21542170
static int identity_allowed_for_posting(struct nntp_session *nntp, const char *fromaddr)
21552171
{
21562172
char dup_addr[256];
@@ -2855,7 +2871,7 @@ int check_article(enum nntp_mode mode, struct nntp_session *nntp, struct article
28552871

28562872
/* Could be a race condition, maybe we didn't have the article when the client said CHECK/IHAVE,
28572873
* but now we do (possibly from some other server). Check one last time before we attempt to store it in the spool. */
2858-
if (spool_article_exists(artinfo->messageid)) {
2874+
if (history_messageid_exists(artinfo->messageid)) {
28592875
snprintf(errbuf, errbuflen, "%s is a duplicate", artinfo->messageid);
28602876
return 1; /* Use specific response to refuse articles with IHAVE, otherwise use default */
28612877
}
@@ -4000,7 +4016,7 @@ static int handle_check(struct nntp_session *nntp, const char *articleid)
40004016
/* Just because it doesn't exist, doesn't necessarily mean we want it right now.
40014017
* If another peer is currently sending us the same article, then we should defer
40024018
* it from this peer until we've received it successfully. */
4003-
if (spool_article_exists(articleid)) {
4019+
if (history_messageid_exists(articleid)) {
40044020
nntp_send(nntp, NNTP_FAIL_CHECK_REFUSE, "%s", articleid);
40054021
} else if (is_wip_article(articleid)) {
40064022
nntp_rx_reply2_streaming(nntp, 1, 0, articleid, LOG_DEFER, NNTP_FAIL_CHECK_DEFER, ""); /* Defer due to in-progress delivery */
@@ -4463,7 +4479,7 @@ static int nntp_process(struct nntp_session *nntp, struct readline_data *rldata,
44634479
}
44644480
nntp_send(nntp, NNTP_OK_NEWNEWS, "List of new articles by message-ID follows");
44654481
ACL_RDLOCK(nntp);
4466-
spool_newnews(nntp, wildmat, epoch);
4482+
history_newnews(nntp, wildmat, epoch);
44674483
ACL_UNLOCK(nntp);
44684484
} else if (!strcasecmp(command, "XOVER")) {
44694485
/* RFC 2980 XOVER */
@@ -4689,7 +4705,7 @@ static int nntp_process(struct nntp_session *nntp, struct readline_data *rldata,
46894705
} else if (!strcasecmp(command, "IHAVE")) {
46904706
REQUIRE_TRANSIT();
46914707
REQUIRE_ARGS(s); /* Do not strip <> around Message-ID, as that is part of the Message-ID */
4692-
if (spool_article_exists(s)) {
4708+
if (history_messageid_exists(s)) {
46934709
nntp_rx_reply2_streaming(nntp, 0, 0, s, LOG_DUPLICATE, NNTP_FAIL_IHAVE_REFUSE, "Duplicate");
46944710
return 0;
46954711
}
@@ -4798,6 +4814,7 @@ static struct bbs_cli_entry cli_commands_nntp[] = {
47984814
BBS_CLI_COMMAND(cli_rmgroup, "news rmgroup", 3, "Remove a newsgroup", "news rmgroup <group> [confirm]"),
47994815
BBS_CLI_COMMAND(cli_setstatus, "news setstatus", 4, "Edit posting status for a newsgroup", "news setstatus <group> <y/n/m>"),
48004816
BBS_CLI_COMMAND(cli_delarticle, "news delarticle", 4, "Delete an article", "news delarticle <group> <article number>"),
4817+
BBS_CLI_COMMAND(cli_expire, "news expire", 2, "Remove expired articles from the spool (optionally just for one group)", "news expire <group>"),
48014818
BBS_CLI_COMMAND(cli_feedflush, "news feedflush", 2, "Flush queued articles for feed(s)", "news feedflush [<site>]"),
48024819
BBS_CLI_COMMAND(cli_feedstats, "news feedstats", 2, "Show outgoing feed stats", "news feedstats [<site>]"),
48034820
};
@@ -4820,7 +4837,7 @@ static int load_config(void)
48204837

48214838
/* Note: nntp_suckfeed_init needs to be initialized before the bulk of load_config() so the list is ready to receive items when processing the config
48224839
* However, it also needs to be after loading newsdir */
4823-
if (active_init() || spool_init() || nntp_suckfeed_init()) {
4840+
if (active_init() || spool_init() || history_init() || nntp_suckfeed_init()) {
48244841
bbs_config_unlock(cfg);
48254842
return -1;
48264843
}
@@ -4863,6 +4880,7 @@ static int load_config(void)
48634880
max_accept_age = 3;
48644881
}
48654882
}
4883+
bbs_config_val_set_uint(cfg, "articles", "minhistory", &min_history);
48664884
bbs_config_val_set_uint(cfg, "articles", "minlines", &min_lines);
48674885
bbs_config_val_set_uint(cfg, "articles", "maxlines", &max_lines);
48684886

@@ -4957,6 +4975,10 @@ static int load_config(void)
49574975
while ((keyval = bbs_config_section_walk(section, keyval))) {
49584976
add_killpat(bbs_keyval_key(keyval), bbs_keyval_val(keyval));
49594977
}
4978+
} else if (!strcasecmp(bbs_config_section_name(section), "retention")) {
4979+
while ((keyval = bbs_config_section_walk(section, keyval))) {
4980+
history_add_retention_pattern(bbs_keyval_key(keyval), bbs_keyval_val(keyval));
4981+
}
49604982
} else {
49614983
/* The only config section type that isn't defined by the section name is user ACLs */
49624984
const char *type = bbs_config_sect_val(section, "type");
@@ -5109,6 +5131,13 @@ static void cleanup_lists(void)
51095131
stringlist_empty_destroy(&subscriptions);
51105132
}
51115133

5134+
static void cleanup_subsystems(void)
5135+
{
5136+
active_cleanup();
5137+
spool_cleanup();
5138+
history_cleanup();
5139+
}
5140+
51125141
static int load_module(void)
51135142
{
51145143
char newslogpath[512];
@@ -5186,8 +5215,7 @@ static int load_module(void)
51865215

51875216
cleanup:
51885217
cleanup_lists();
5189-
active_cleanup();
5190-
spool_cleanup();
5218+
cleanup_subsystems();
51915219
return -1;
51925220
}
51935221

@@ -5207,8 +5235,7 @@ static int unload_module(void)
52075235
}
52085236
cleanup_lists();
52095237
bbs_rwlock_destroy(&nntp_lock);
5210-
active_cleanup();
5211-
spool_cleanup();
5238+
cleanup_subsystems();
52125239
fclose(newslog);
52135240
fclose(postlog);
52145241
return 0;

nets/net_nntp/nntp.h

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -513,14 +513,6 @@ int spool_article_create(struct article_groups *groups, struct article_info *art
513513

514514
int spool_article_delete_by_number(const char *groupname, int article_num);
515515

516-
/*!
517-
* \brief Whether any message with this Message-ID exists in any group
518-
* \param messageid Message-ID of article, if searching by message-ID
519-
* \retval 0 if no message exists
520-
* \retval 1 if article exists in some group
521-
*/
522-
int spool_article_exists(const char *messageid);
523-
524516
/*!
525517
* \brief Whether any message with this Message-ID exists in any group
526518
* \param messageid Message-ID of article, if searching by message-ID

0 commit comments

Comments
 (0)