Skip to content

Commit

Permalink
cyr_expire: add noexpire_until annotation to block cyr_expire
Browse files Browse the repository at this point in the history
Signed-off-by: Robert Stepanek <[email protected]>
  • Loading branch information
rsto committed Jun 14, 2023
1 parent baba2f9 commit f63918e
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 17 deletions.
108 changes: 108 additions & 0 deletions cassandane/Cassandane/Cyrus/Delete.pm
Original file line number Diff line number Diff line change
Expand Up @@ -944,4 +944,112 @@ sub test_cyr_expire_inherit_annot
$self->assert_num_equals(0, $talk->get_response_code('exists'));
}

sub test_cyr_expire_noexpire
:DelayedDelete :min_version_3_9 :NoAltNameSpace
{
my ($self) = @_;
my $store = $self->{store};
my $talk = $store->get_client();

my $noexpire_annot = '/shared/vendor/cmu/cyrus-imapd/noexpire_until';

xlog $self, "Create subfolder";
my $subfolder = 'INBOX.A';
$talk->create($subfolder)
or $self->fail("Cannot create folder $subfolder: $@");
$self->assert_str_equals('ok', $talk->get_last_completion_response());

xlog $self, "Set /vendor/cmu/cyrus-imapd/expire annotation on subfolder";
$talk->setmetadata($subfolder, "/shared/vendor/cmu/cyrus-imapd/expire", '1s');
$self->assert_str_equals('ok', $talk->get_last_completion_response);

xlog $self, "Create message";
$store->set_folder($subfolder);
$self->make_message('msg1') or die;

xlog $self, "Set $noexpire_annot annotation on inbox";
$talk->setmetadata('INBOX', $noexpire_annot, '0');
$self->assert_str_equals('ok', $talk->get_last_completion_response);

$talk->unselect();
$talk->select($subfolder);
$self->assert_num_equals(1, $talk->get_response_code('exists'));

sleep(2);
xlog $self, "Run cyr_expire";
$self->{instance}->run_command({ cyrus => 1 }, 'cyr_expire', '-X' => '1s', '-v', '-v', '-v' );

$talk->unselect();
$talk->select($subfolder);
$self->assert_num_equals(1, $talk->get_response_code('exists'));

xlog $self, "Remove $noexpire_annot from inbox";
$talk->setmetadata('INBOX', $noexpire_annot, '');
$self->assert_str_equals('ok', $talk->get_last_completion_response);

xlog $self, "Run cyr_expire";
$self->{instance}->run_command({ cyrus => 1 }, 'cyr_expire', '-X' => '1s', '-v', '-v', '-v' );

$talk->unselect();
$talk->select($subfolder);
$self->assert_num_equals(0, $talk->get_response_code('exists'));
}

sub test_cyr_expire_delete_noexpire
:DelayedDelete :min_version_3_9 :NoAltNameSpace
{
my ($self) = @_;
my $store = $self->{store};
my $adminstore = $self->{adminstore};
my $talk = $store->get_client();
my $admintalk = $adminstore->get_client();

my $noexpire_annot = '/shared/vendor/cmu/cyrus-imapd/noexpire_until';

my $subfoldername = 'foo';
my $subfolder = 'INBOX.foo';
$talk->create($subfolder)
or $self->fail("Cannot create folder $subfolder: $@");
$self->assert_str_equals('ok', $talk->get_last_completion_response());

xlog $self, "Setting /vendor/cmu/cyrus-imapd/delete annotation.";
$talk->setmetadata($subfolder, "/shared/vendor/cmu/cyrus-imapd/delete", '1s');

$self->check_folder_ondisk($subfolder);
$self->check_folder_not_ondisk($subfolder, deleted => 1);

xlog $self, "Delete $subfolder";
$talk->unselect();
$talk->delete($subfolder)
or $self->fail("Cannot delete folder $subfolder: $@");
$self->assert_str_equals('ok', $talk->get_last_completion_response());

xlog $self, "Ensure we can't select $subfolder anymore";
$talk->select($subfolder);
$self->assert_str_equals('no', $talk->get_last_completion_response());
$self->assert_matches(qr/Mailbox does not exist/i, $talk->get_last_error());

$self->check_folder_not_ondisk($subfolder);

my ($path) = $self->{instance}->folder_to_deleted_directories("user.cassandane.$subfoldername");
$self->assert(-d "$path");

xlog $self, "Set $noexpire_annot annotation on inbox";
$talk->setmetadata('INBOX', $noexpire_annot, '0');
$self->assert_str_equals('ok', $talk->get_last_completion_response);

sleep(2);
xlog $self, "Run cyr_expire";
$self->{instance}->run_command({ cyrus => 1 }, 'cyr_expire', '-D' => '1s' );
$self->assert(-d "$path");

xlog $self, "Remove $noexpire_annot annotation from inbox";
$talk->setmetadata('INBOX', $noexpire_annot, '');
$self->assert_str_equals('ok', $talk->get_last_completion_response);

xlog $self, "Run cyr_expire";
$self->{instance}->run_command({ cyrus => 1 }, 'cyr_expire', '-D' => '1s' );
$self->assert(!-d "$path");
}

1;
2 changes: 2 additions & 0 deletions changes/next/cyr_expire_noexpire
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ Description:

Supports non-day durations in the archive/delete/expire annotations.

Adds the 'noexpire_until' annotation to disable cyr_expire per user.

Config changes:

None
Expand Down
10 changes: 9 additions & 1 deletion docsrc/imap/reference/manpages/systemcommands/cyr_expire.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,19 @@ There are various annotations that **cyr_expire** respects:
messages
- ``/vendor/cmu/cyrus-imapd/delete`` which controls the deletion of
messages
- ``/vendor/cmu/cyrus-imapd/noexpire_until`` which disables the expire
and delete operations per user

These mailbox annotations specify the age of messages in the
The first three mailbox annotations specify the age of messages in the
given mailbox that should be expired/archived/deleted. The
age is specified as a duration, the default unit are days.

The last mailbox annotation specifies the UNIX epoch time in seconds
until which expiring messages or removing deleted mailboxes is blocked.
The zero epoch time represents infinity. This annotation has precedence
over any of the other annotations or command line flags. It must only
be set on the user inbox and applies to all mailboxes of that user.

The value of the ``/vendor/cmu/cyrus-imapd/expire`` annotation is
inherited by all children of the mailbox on which it is set, so an
entire mailbox tree can be configured by setting a single annotation on
Expand Down
20 changes: 20 additions & 0 deletions imap/annotate.c
Original file line number Diff line number Diff line change
Expand Up @@ -2359,6 +2359,26 @@ static const annotate_entrydesc_t mailbox_builtin_entries[] =
annotation_set_todb,
NULL,
NULL
},{
IMAP_ANNOT_NS "nodelete_until",
ATTRIB_TYPE_UINT,
BACKEND_ONLY,
ATTRIB_VALUE_SHARED,
ACL_ADMIN,
annotation_get_fromdb,
annotation_set_todb,
NULL,
NULL
},{
IMAP_ANNOT_NS "noexpire_until",
ATTRIB_TYPE_UINT,
BACKEND_ONLY,
ATTRIB_VALUE_SHARED,
ACL_ADMIN,
annotation_get_fromdb,
annotation_set_todb,
NULL,
NULL
},{
IMAP_ANNOT_NS "partition",
/* _get_partition does its own access control check */
Expand Down
113 changes: 97 additions & 16 deletions imap/cyr_expire.c
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
/* global state */
static int verbose = 0;
static const char *progname = NULL;
static time_t progtime = 0;
static struct namespace expire_namespace; /* current namespace */

/* command line arguments */
Expand Down Expand Up @@ -282,43 +283,114 @@ static int parse_duration(const char *s, int *secondsp)
}

/*
* Given an annotation, reads it, and converts it into 'seconds',
* using `parse_duration`.
* Given an annotation, reads it from the mailbox or any of its
* parents if iterate is true.
*
* On Success: Returns 1
* On Failure: Returns 0
*/
static int get_annotation_value(const mbentry_t *mbentry,
static int get_annotation_value(const char *mboxname,
const char *annot_entry,
int *secondsp, bool iterate)
struct buf *annot_value,
bool iterate)
{
struct buf attrib = BUF_INITIALIZER;
int ret = 0;
/* mboxname needs to be copied since `mboxname_make_parent`
* runs a strrchr() on it.
*/
char *buf = xstrdup(mbentry->name);
char *buf = xstrdup(mboxname);

/*
* Mailboxes inherit /vendo/cmu/cyrus-imapd/{expire, archive, delete},
* so we need to iterate all the way up to "" (server entry).
*/
do {
buf_free(&attrib);
ret = annotatemore_lookup(buf, annot_entry, "", &attrib);
buf_reset(annot_value);
ret = annotatemore_lookup(buf, annot_entry, "", annot_value);
if (ret || /* error */
attrib.s) /* found an entry */
buf_len(annot_value)) /* found an entry */
break;
} while (mboxname_make_parent(buf) && iterate);

if (attrib.s && parse_duration(attrib.s, secondsp))
free(buf);

return buf_len(annot_value) ? 1 : 0;
}

static int get_duration_annotation(const char *mboxname,
const char *annot_entry,
int *secondsp, bool iterate)
{
struct buf attrib = BUF_INITIALIZER;
int ret = 0;

if (get_annotation_value(mboxname, annot_entry, &attrib, iterate) &&
parse_duration(buf_cstring(&attrib), secondsp))
ret = 1;
else
ret = 0;

buf_free(&attrib);
free(buf);
return ret;
}

static int get_time_annotation(const char *mboxname,
const char *annot_entry,
time_t *timep, bool iterate)
{
struct buf attrib = BUF_INITIALIZER;
int ret = 0;

if (get_annotation_value(mboxname, annot_entry, &attrib, iterate)) {
const char *end = NULL;
bit64 v64 = 0;
if (!parsenum(buf_cstring(&attrib), &end, 0, &v64) && !*end) {
*timep = v64;
ret = 1;
}
}

buf_free(&attrib);
return ret;
}

static int noexpire_mailbox(const mbentry_t *mbentry)
{
int ret = 0;
mbname_t *mbname = mbname_from_intname(mbentry->name);

if (mbname_userid(mbname)) {
// Cache result for the last seen userid
static struct {
struct buf userid;
int has_noexpire;
} last_seen = { BUF_INITIALIZER, -1 };

if (!strcmp(mbname_userid(mbname), buf_cstring(&last_seen.userid))) {
ret = last_seen.has_noexpire;
goto done;
}

// Determine user inbox name
if (mbname_isdeleted(mbname)) {
mbname_t *tmp = mbname_from_userid(mbname_userid(mbname));
mbname_free(&mbname);
mbname = tmp;
}
mbname_truncate_boxes(mbname, 0);

// Lookup annotation
time_t until;
if (get_time_annotation(mbname_intname(mbname),
IMAP_ANNOT_NS "noexpire_until", &until, false))
ret = !until || progtime < until;

// Update cache
buf_setcstr(&last_seen.userid, mbname_userid(mbname));
last_seen.has_noexpire = ret;
}

done:
if (ret) verbosep("(noexpire) %s", mbname_intname(mbname));
mbname_free(&mbname);
return ret;
}

Expand Down Expand Up @@ -378,7 +450,7 @@ static int archive(const mbentry_t *mbentry, void *rock)

/* check /vendor/cmu/cyrus-imapd/archive */
if (!arock->skip_annotate &&
get_annotation_value(mbentry, IMAP_ANNOT_NS "archive",
get_duration_annotation(mbentry->name, IMAP_ANNOT_NS "archive",
&archive_seconds, false)) {
arock->archive_mark = archive_seconds ?
time(0) - archive_seconds : 0;
Expand Down Expand Up @@ -462,12 +534,16 @@ static int expire(const mbentry_t *mbentry, void *rock)
goto done;
}

/* see if this mailbox should be ignored */
if (noexpire_mailbox(mbentry))
goto done;

/* see if we need to expire messages.
* since mailboxes inherit /vendor/cmu/cyrus-imapd/expire,
* we need to iterate all the way up to "" (server entry)
*/
if (!erock->skip_annotate &&
get_annotation_value(mbentry, IMAP_ANNOT_NS "expire",
get_duration_annotation(mbentry->name, IMAP_ANNOT_NS "expire",
&expire_seconds, true)) {
/* add mailbox to table */
erock->expire_mark = expire_seconds ?
Expand Down Expand Up @@ -539,9 +615,13 @@ static int delete(const mbentry_t *mbentry, void *rock)
if (!mboxname_isdeletedmailbox(mbentry->name, &timestamp))
goto done;

/* see if this mailbox should be ignored */
if (noexpire_mailbox(mbentry))
goto done;

/* check /vendor/cmu/cyrus-imapd/delete */
if (!drock->skip_annotate &&
get_annotation_value(mbentry, IMAP_ANNOT_NS "delete",
get_duration_annotation(mbentry->name, IMAP_ANNOT_NS "delete",
&delete_seconds, false)) {
drock->delete_mark = delete_seconds ?
time(0) - delete_seconds: 0;
Expand Down Expand Up @@ -886,6 +966,7 @@ int main(int argc, char *argv[])
int r = 0;

progname = basename(argv[0]);
progtime = time(NULL);

if (parse_args(argc, argv, &ctx.args) != 0)
exit(EXIT_FAILURE);
Expand Down

0 comments on commit f63918e

Please sign in to comment.