From a054b4573c7160d5c056be5e01614c5a2ed99108 Mon Sep 17 00:00:00 2001 From: Robert Stepanek Date: Tue, 13 Jun 2023 11:56:47 +0200 Subject: [PATCH 1/5] annotate: support durations in archive/delete/expire annotations cyr_expire already supports reading durations such as '1h' but only numeric durations defaulting to day units could be set via IMAP. Signed-off-by: Robert Stepanek --- changes/next/cyr_expire_noexpire | 15 +++ cunit/annotate.testc | 106 ++++++++++++++++++ .../manpages/systemcommands/cyr_expire.rst | 5 +- imap/annotate.c | 21 +++- 4 files changed, 140 insertions(+), 7 deletions(-) create mode 100644 changes/next/cyr_expire_noexpire diff --git a/changes/next/cyr_expire_noexpire b/changes/next/cyr_expire_noexpire new file mode 100644 index 0000000000..60c98ad773 --- /dev/null +++ b/changes/next/cyr_expire_noexpire @@ -0,0 +1,15 @@ +Description: + +Supports non-day durations in the archive/delete/expire annotations. + +Config changes: + +None + +Upgrade instructions: + +None + +GitHub issue: + +If theres a github issue number for this, put it here. diff --git a/cunit/annotate.testc b/cunit/annotate.testc index 75fb5207f3..8e6baa6209 100644 --- a/cunit/annotate.testc +++ b/cunit/annotate.testc @@ -2053,6 +2053,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) { diff --git a/docsrc/imap/reference/manpages/systemcommands/cyr_expire.rst b/docsrc/imap/reference/manpages/systemcommands/cyr_expire.rst index 15e66cf760..9d4cd5fe78 100644 --- a/docsrc/imap/reference/manpages/systemcommands/cyr_expire.rst +++ b/docsrc/imap/reference/manpages/systemcommands/cyr_expire.rst @@ -43,8 +43,9 @@ There are various annotations that **cyr_expire** respects: - ``/vendor/cmu/cyrus-imapd/delete`` which controls the deletion of messages -These mailbox annotations specify the age(in days) of messages in the -given mailbox that should be expired/archived/deleted. +These 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 value of the ``/vendor/cmu/cyrus-imapd/expire`` annotation is inherited by all children of the mailbox on which it is set, so an diff --git a/imap/annotate.c b/imap/annotate.c index 0541717d58..2512023bb6 100644 --- a/imap/annotate.c +++ b/imap/annotate.c @@ -191,7 +191,8 @@ enum { ATTRIB_TYPE_STRING, ATTRIB_TYPE_BOOLEAN, ATTRIB_TYPE_UINT, - ATTRIB_TYPE_INT + ATTRIB_TYPE_INT, + ATTRIB_TYPE_DURATION }; #define ATTRIB_NO_FETCH_ACL_CHECK (1<<30) @@ -2270,7 +2271,7 @@ static const annotate_entrydesc_t mailbox_builtin_entries[] = NULL },{ IMAP_ANNOT_NS "archive", - ATTRIB_TYPE_UINT, + ATTRIB_TYPE_DURATION, BACKEND_ONLY, ATTRIB_VALUE_SHARED, ACL_ADMIN, @@ -2280,7 +2281,7 @@ static const annotate_entrydesc_t mailbox_builtin_entries[] = NULL },{ IMAP_ANNOT_NS "delete", - ATTRIB_TYPE_UINT, + ATTRIB_TYPE_DURATION, BACKEND_ONLY, ATTRIB_VALUE_SHARED, ACL_ADMIN, @@ -2300,7 +2301,7 @@ static const annotate_entrydesc_t mailbox_builtin_entries[] = (void *)OPT_IMAP_DUPDELIVER },{ IMAP_ANNOT_NS "expire", - ATTRIB_TYPE_UINT, + ATTRIB_TYPE_DURATION, BACKEND_ONLY, ATTRIB_VALUE_SHARED, ACL_ADMIN, @@ -2574,7 +2575,7 @@ static const annotate_entrydesc_t server_builtin_entries[] = NULL },{ IMAP_ANNOT_NS "expire", - ATTRIB_TYPE_UINT, + ATTRIB_TYPE_DURATION, PROXY_AND_BACKEND, ATTRIB_VALUE_SHARED, ACL_ADMIN, @@ -3409,6 +3410,16 @@ static int annotate_canon_value(struct buf *value, int type) } break; + case ATTRIB_TYPE_DURATION: + /* make sure it is a valid positive duration ( >= 0 ) */ + { + buf_cstring(value); + int secs = -1; + if (config_parseduration(value->s, 'd', &secs) || secs < 0) + return IMAP_ANNOTATION_BADVALUE; + } + break; + default: /* unknown type */ return IMAP_ANNOTATION_BADVALUE; From a0c2f193d22ef2b069d25160805b5fa9b1442764 Mon Sep 17 00:00:00 2001 From: Robert Stepanek Date: Tue, 13 Jun 2023 12:17:17 +0200 Subject: [PATCH 2/5] cyr_expire: fix reading mailbox parent annotations cyr_expire annotations are either set on a mailbox or any of its parents. But looking up annotations on the parent mailboxes got broken in 2018 during mailbox name to mailbox id migration. Signed-off-by: Robert Stepanek --- cassandane/Cassandane/Cyrus/Delete.pm | 34 +++++++++++++++++++++++++++ imap/cyr_expire.c | 2 +- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/cassandane/Cassandane/Cyrus/Delete.pm b/cassandane/Cassandane/Cyrus/Delete.pm index 2018f4af9c..885b386df6 100644 --- a/cassandane/Cassandane/Cyrus/Delete.pm +++ b/cassandane/Cassandane/Cyrus/Delete.pm @@ -910,4 +910,38 @@ 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')); +} + 1; diff --git a/imap/cyr_expire.c b/imap/cyr_expire.c index e17eef247d..79854972b0 100644 --- a/imap/cyr_expire.c +++ b/imap/cyr_expire.c @@ -305,7 +305,7 @@ static int get_annotation_value(const mbentry_t *mbentry, */ do { buf_free(&attrib); - ret = annotatemore_lookup_mbe(mbentry, annot_entry, "", &attrib); + ret = annotatemore_lookup(buf, annot_entry, "", &attrib); if (ret || /* error */ attrib.s) /* found an entry */ break; From 927aa1e255054e900145f11e2f0e917837b79eda Mon Sep 17 00:00:00 2001 From: Robert Stepanek Date: Tue, 13 Jun 2023 13:01:20 +0200 Subject: [PATCH 3/5] cyr_expire: remove get_annotation_value debug logging It's just noisy and in its current form does not provide enough context for debugging anyway. Signed-off-by: Robert Stepanek --- imap/cyr_expire.c | 1 - 1 file changed, 1 deletion(-) diff --git a/imap/cyr_expire.c b/imap/cyr_expire.c index 79854972b0..ff58f5ff89 100644 --- a/imap/cyr_expire.c +++ b/imap/cyr_expire.c @@ -319,7 +319,6 @@ static int get_annotation_value(const mbentry_t *mbentry, buf_free(&attrib); free(buf); - syslog(LOG_DEBUG, "get_annotation_value: ret(%d):secondsp(%d)", ret, *secondsp); return ret; } From f6c83861131eb853945882c586cba8682fb947fa Mon Sep 17 00:00:00 2001 From: Robert Stepanek Date: Wed, 14 Jun 2023 16:57:25 +0200 Subject: [PATCH 4/5] cyr_expire: add noexpire_until annotation to block cyr_expire Signed-off-by: Robert Stepanek --- cassandane/Cassandane/Cyrus/Delete.pm | 108 +++++++++++++++++ cassandane/Cassandane/Cyrus/Metadata.pm | 12 ++ changes/next/cyr_expire_noexpire | 2 + .../manpages/systemcommands/cyr_expire.rst | 10 +- imap/annotate.c | 10 ++ imap/cyr_expire.c | 113 +++++++++++++++--- 6 files changed, 238 insertions(+), 17 deletions(-) diff --git a/cassandane/Cassandane/Cyrus/Delete.pm b/cassandane/Cassandane/Cyrus/Delete.pm index 885b386df6..fc37064f23 100644 --- a/cassandane/Cassandane/Cyrus/Delete.pm +++ b/cassandane/Cassandane/Cyrus/Delete.pm @@ -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; diff --git a/cassandane/Cassandane/Cyrus/Metadata.pm b/cassandane/Cassandane/Cyrus/Metadata.pm index d85a25244b..d3db78191e 100644 --- a/cassandane/Cassandane/Cyrus/Metadata.pm +++ b/cassandane/Cassandane/Cyrus/Metadata.pm @@ -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: @@ -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"); diff --git a/changes/next/cyr_expire_noexpire b/changes/next/cyr_expire_noexpire index 60c98ad773..3de59d4df5 100644 --- a/changes/next/cyr_expire_noexpire +++ b/changes/next/cyr_expire_noexpire @@ -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 diff --git a/docsrc/imap/reference/manpages/systemcommands/cyr_expire.rst b/docsrc/imap/reference/manpages/systemcommands/cyr_expire.rst index 9d4cd5fe78..e8ceed1afd 100644 --- a/docsrc/imap/reference/manpages/systemcommands/cyr_expire.rst +++ b/docsrc/imap/reference/manpages/systemcommands/cyr_expire.rst @@ -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 diff --git a/imap/annotate.c b/imap/annotate.c index 2512023bb6..c01f8a082e 100644 --- a/imap/annotate.c +++ b/imap/annotate.c @@ -2359,6 +2359,16 @@ static const annotate_entrydesc_t mailbox_builtin_entries[] = 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 */ diff --git a/imap/cyr_expire.c b/imap/cyr_expire.c index ff58f5ff89..44aab0b644 100644 --- a/imap/cyr_expire.c +++ b/imap/cyr_expire.c @@ -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 */ @@ -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, ignoring any pre-epoch timestamps + time_t until; + if (get_time_annotation(mbname_intname(mbname), + IMAP_ANNOT_NS "noexpire_until", &until, false)) + ret = !until || (until > 0 && 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; } @@ -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; @@ -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 ? @@ -539,9 +615,13 @@ static int delete(const mbentry_t *mbentry, void *rock) if (!mboxname_isdeletedmailbox(mbentry->name, ×tamp)) 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; @@ -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); From f8e55e7a7ba45e8940c273c6fd98a0df1464b7bc Mon Sep 17 00:00:00 2001 From: Robert Stepanek Date: Mon, 19 Jun 2023 16:49:09 +0200 Subject: [PATCH 5/5] cyr_expire: remove support for fractional durations Signed-off-by: Robert Stepanek --- changes/next/cyr_expire_noexpire | 6 +- .../manpages/systemcommands/cyr_expire.rst | 15 +++-- imap/cyr_expire.c | 58 ++----------------- 3 files changed, 19 insertions(+), 60 deletions(-) diff --git a/changes/next/cyr_expire_noexpire b/changes/next/cyr_expire_noexpire index 3de59d4df5..268c7fcc85 100644 --- a/changes/next/cyr_expire_noexpire +++ b/changes/next/cyr_expire_noexpire @@ -1,7 +1,7 @@ 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: @@ -10,7 +10,9 @@ None Upgrade instructions: -None +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: diff --git a/docsrc/imap/reference/manpages/systemcommands/cyr_expire.rst b/docsrc/imap/reference/manpages/systemcommands/cyr_expire.rst index e8ceed1afd..5e87e392f3 100644 --- a/docsrc/imap/reference/manpages/systemcommands/cyr_expire.rst +++ b/docsrc/imap/reference/manpages/systemcommands/cyr_expire.rst @@ -48,6 +48,7 @@ There are various annotations that **cyr_expire** respects: 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. @@ -101,6 +102,8 @@ 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| @@ -108,25 +111,25 @@ Options 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 diff --git a/imap/cyr_expire.c b/imap/cyr_expire.c index 44aab0b644..13449f19ef 100644 --- a/imap/cyr_expire.c +++ b/imap/cyr_expire.c @@ -235,53 +235,6 @@ static void usage(void) exit(EX_USAGE); } -/* - * Parse a non-negative duration string as seconds. - * - * Convert "23.5m" to fractional days. Accepts the suffixes "d" (day), - * (day), "h" (hour), "m" (minute) and "s" (second). If no suffix, assume - * days. - * Returns 1 if successful and *secondsp is filled in, or 0 if the suffix - * is unknown or on error. - */ -static int parse_duration(const char *s, int *secondsp) -{ - char *end = NULL; - double val; - int multiplier = SECS_IN_A_DAY; /* default is days */ - - /* no negative or empty numbers please */ - if (!*s || *s == '-') - return 0; - - val = strtod(s, &end); - /* Allow 'd', 'h', 'm' and 's' as end, else return error. */ - if (*end) { - if (end[1]) return 0; /* trailing extra junk */ - - switch (*end) { - case 'd': - /* already the default */ - break; - case 'h': - multiplier = SECS_IN_AN_HR; - break; - case 'm': - multiplier = SECS_IN_A_MIN; - break; - case 's': - multiplier = 1; - break; - default: - return 0; - } - } - - *secondsp = multiplier * val; - - return 1; -} - /* * Given an annotation, reads it from the mailbox or any of its * parents if iterate is true. @@ -325,7 +278,7 @@ static int get_duration_annotation(const char *mboxname, int ret = 0; if (get_annotation_value(mboxname, annot_entry, &attrib, iterate) && - parse_duration(buf_cstring(&attrib), secondsp)) + !config_parseduration(buf_cstring(&attrib), 'd', secondsp)) ret = 1; buf_free(&attrib); @@ -877,7 +830,8 @@ static int parse_args(int argc, char *argv[], struct arguments *args) { switch (opt) { case 'A': - if (!parse_duration(optarg, &args->archive_seconds)) usage(); + if (config_parseduration(optarg, 'd', &args->archive_seconds) < 0) + usage(); break; case 'C': @@ -885,17 +839,17 @@ static int parse_args(int argc, char *argv[], struct arguments *args) break; case 'D': - if (!parse_duration(optarg, &args->delete_seconds)) + if (config_parseduration(optarg, 'd', &args->delete_seconds) < 0) usage(); break; case 'E': - if (!parse_duration(optarg, &args->expire_seconds)) + if (config_parseduration(optarg, 'd', &args->expire_seconds) < 0) usage(); break; case 'X': - if (!parse_duration(optarg, &args->expunge_seconds)) + if (config_parseduration(optarg, 'd', &args->expunge_seconds) < 0) usage(); break;