Skip to content

Commit

Permalink
Merge pull request #4533 from cyrusimap/cyr_expire_noexpire
Browse files Browse the repository at this point in the history
cyr_expire: add noexpire_until annotation
  • Loading branch information
rsto authored Jul 26, 2023
2 parents 633582e + f8e55e7 commit f5cb034
Show file tree
Hide file tree
Showing 7 changed files with 427 additions and 81 deletions.
142 changes: 142 additions & 0 deletions cassandane/Cassandane/Cyrus/Delete.pm
Original file line number Diff line number Diff line change
Expand Up @@ -910,4 +910,146 @@ sub test_no_delete_with_children
$self->assert_str_equals('no', $talk->get_last_completion_response());
}

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

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 inbox";
$talk->setmetadata('INBOX', "/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;

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

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

$talk->unselect();
$talk->select($subfolder);
$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;
12 changes: 12 additions & 0 deletions cassandane/Cassandane/Cyrus/Metadata.pm
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,12 @@ sub test_shared
if ($maj > 3 or ($maj == 3 and $min >= 3)) {
$specific_entries{'/shared/vendor/cmu/cyrus-imapd/search-fuzzy-always'} = undef;
}

# We introduced vendor/cmu/cyrus-imapd/noexpire_until in 3.9.0
if ($maj > 3 or ($maj == 3 and $min >= 9)) {
$specific_entries{'/shared/vendor/cmu/cyrus-imapd/noexpire_until'} = undef;
}

$self->assert_deep_equals(\%specific_entries, $r);

# individual item fetch:
Expand Down Expand Up @@ -1123,6 +1129,12 @@ sub test_private
if ($maj > 3 or ($maj == 3 and $min >= 3)) {
$specific_entries{'/private/vendor/cmu/cyrus-imapd/search-fuzzy-always'} = undef;
}

# We introduced vendor/cmu/cyrus-imapd/noexpire_until in 3.9.0
if ($maj > 3 or ($maj == 3 and $min >= 9)) {
$specific_entries{'/private/vendor/cmu/cyrus-imapd/noexpire_until'} = undef;
}

$self->assert_deep_equals(\%specific_entries, $r);

$imaptalk->setmetadata('INBOX', "/private/comment", "This is a comment");
Expand Down
19 changes: 19 additions & 0 deletions changes/next/cyr_expire_noexpire
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Description:

Supports non-day durations in the archive/delete/expire annotations.
Removes support for fractional durations in cyr_expire arguments.
Adds the 'noexpire_until' annotation to disable cyr_expire per user.

Config changes:

None

Upgrade instructions:

Installations that passed fractional durations such as "1.5d" to any of the
-E, -X, -D, or -A arguments must adapt these to only use integer durations
such as "1d12h".

GitHub issue:

If theres a github issue number for this, put it here.
106 changes: 106 additions & 0 deletions cunit/annotate.testc
Original file line number Diff line number Diff line change
Expand Up @@ -2050,6 +2050,112 @@ static int create_messages(struct mailbox *mailbox, int count)
return 0;
}

static void test_canon_value(void)
{
struct {
const char *annot;
const char *attrb;
const char *value;
int is_invalid;
} tests[] = {{
.annot = "/check",
.attrb = "value.shared",
.value = "true",
}, {
.annot = "/check",
.attrb = "value.shared",
.value = "false",
}, {
.annot = "/check",
.attrb = "value.shared",
.value = "yes",
.is_invalid = 1,
}, {
.annot = "/vendor/cmu/cyrus-imapd/sortorder",
.attrb = "value.priv",
.value = "123",
}, {
.annot = "/vendor/cmu/cyrus-imapd/sortorder",
.attrb = "value.priv",
.value = "12298189749871298739829873",
.is_invalid = 1
}, {
.annot = "/vendor/cmu/cyrus-imapd/sortorder",
.attrb = "value.priv",
.value = "-1",
.is_invalid = 1
}, {
.annot = "/vendor/cmu/cyrus-imapd/expire",
.attrb = "value.shared",
.value = "1",
}, {
.annot = "/vendor/cmu/cyrus-imapd/expire",
.attrb = "value.shared",
.value = "1s",
}, {
.annot = "/vendor/cmu/cyrus-imapd/expire",
.attrb = "value.shared",
.value = "1x",
.is_invalid = 1,
}, {
.annot = "/vendor/cmu/cyrus-imapd/archive",
.attrb = "value.shared",
.value = "1",
}, {
.annot = "/vendor/cmu/cyrus-imapd/archive",
.attrb = "value.shared",
.value = "1s",
}, {
.annot = "/vendor/cmu/cyrus-imapd/archive",
.attrb = "value.shared",
.value = "1x",
.is_invalid = 1,
}, {
.annot = "/vendor/cmu/cyrus-imapd/delete",
.attrb = "value.shared",
.value = "1",
}, {
.annot = "/vendor/cmu/cyrus-imapd/delete",
.attrb = "value.shared",
.value = "1s",
}, {
.annot = "/vendor/cmu/cyrus-imapd/delete",
.attrb = "value.shared",
.value = "1x",
.is_invalid = 1,
}};

struct mailbox *mailbox = NULL;
annotate_state_t *astate = NULL;
struct entryattlist *ealist = NULL;
struct buf val = BUF_INITIALIZER;

annotate_init(NULL, NULL);
annotatemore_open();

int r = mailbox_open_iwl(MBOXNAME1_INT, &mailbox);
CU_ASSERT_EQUAL_FATAL(r, 0);

astate = annotate_state_new();
r = annotate_state_set_mailbox(astate, mailbox);
CU_ASSERT_EQUAL(r, 0);
annotate_state_set_auth(astate, isadmin, userid, auth_state);

/* validate value */
for (size_t i = 0; i < sizeof(tests)/sizeof(tests[0]); i++) {
buf_setcstr(&val, tests[i].value);
setentryatt(&ealist, tests[i].annot, tests[i].attrb, &val);
r = annotate_state_store(astate, ealist);
CU_ASSERT_EQUAL(r, tests[i].is_invalid ? IMAP_ANNOTATION_BADVALUE : 0);
freeentryatts(ealist);
ealist = NULL;
}

annotate_state_abort(&astate);
mailbox_close(&mailbox);
annotatemore_close();
}


static int set_up(void)
{
Expand Down
28 changes: 20 additions & 8 deletions docsrc/imap/reference/manpages/systemcommands/cyr_expire.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +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(in days) of messages in the
given mailbox that should be expired/archived/deleted.
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 duration format is defined in :cyrusman:`imapd.conf(5)`.

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
Expand Down Expand Up @@ -92,32 +102,34 @@ Options
``archivepartition-*`` has been set in your config.
This value is only used for entries which do not have a
corresponding ``/vendonr/cmu/cyrus-imapd/archive`` mailbox annotation.
The duration format is defined in :cyrusman:`imapd.conf(5)`. The default
unit are days.

|v3-new-feature|

.. option:: -D delete-duration, --delete-duration=delete-duration

Remove previously deleted mailboxes older than *delete-duration*
(when using the "delayed" delete mode).
The value can be a floating point number, and may have a suffix to
specify the unit of time. If no suffix, the value is number of days.
Valid suffixes are **d** (days), **h** (hours), **m** (minutes) and
**s** (seconds).
This value is only used for entries which do not have a
corresponding ``/verdor/cmu/cyrus-imapd/delete`` mailbox annotation.
The duration format is defined in :cyrusman:`imapd.conf(5)`. The default
unit are days.

.. option:: -E expire-duration, --expire-duration=expire-duration

Prune the duplicate database of entries older than *expire-duration*.
This value is only used for entries which do not have a corresponding
``/vendor/cmu/cyrus-imapd/expire`` mailbox annotation.
Format is the same as delete-duration.
The duration format is defined in :cyrusman:`imapd.conf(5)`. The default
unit are days.

.. option:: -X expunge-duration, --expunge-duration=expunge-duration

Expunge previously deleted messages older than *expunge-duration*
(when using the "delayed" expunge mode).
Format is the same as delete-duration.
The duration format is defined in :cyrusman:`imapd.conf(5)`. The default
unit are days.

.. option:: -c, --no-conversations

Expand Down
Loading

0 comments on commit f5cb034

Please sign in to comment.