Skip to content

Commit

Permalink
jmap_notif: automatically prune calendar event notifications
Browse files Browse the repository at this point in the history
Mark all JMAP CalendarEventNotification objects but the last
200 items as expunged, each time a new one is created. The
maximum count can be set in the jmap_max_calendareventnotifs
config option.

Signed-off-by: Robert Stepanek <[email protected]>
  • Loading branch information
rsto committed Nov 24, 2023
1 parent edb56f3 commit 0fd56ad
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 2 deletions.
5 changes: 5 additions & 0 deletions cassandane/Cassandane/Cyrus/TestCase.pm
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,11 @@ magic(NoCheckSyslog => sub {
my $self = shift;
$self->{no_check_syslog} = 1;
});
magic(JmapMaxCalendarEventNotifs => sub {
my $conf = shift;
# set to some small number
$conf->config_set('jmap_max_calendareventnotifs' => 10);
});

# Run any magic handlers indicated by the test name or attributes
sub _run_magic
Expand Down
128 changes: 128 additions & 0 deletions cassandane/tiny-tests/JMAPCalendars/calendareventnotification-prune
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
#!perl
use Cassandane::Tiny;

sub test_calendareventnotification_prune
:min_version_3_9 :needs_component_jmap :JmapMaxCalendarEventNotifs
{
my ($self) = @_;
my $jmap = $self->{jmap};

my $jmap_max_calendareventnotifs = $self->{instance}->{
config}->get('jmap_max_calendareventnotifs');
$self->assert_not_null($jmap_max_calendareventnotifs);

my ($manJmap) = $self->create_user('manifold');
$manJmap->DefaultUsing([
'urn:ietf:params:jmap:core',
'urn:ietf:params:jmap:calendars',
'urn:ietf:params:jmap:principals',
'https://cyrusimap.org/ns/jmap/calendars',
]);

xlog $self, "Share calendar";
my $res = $jmap->CallMethods([
['Calendar/set', {
update => {
Default => {
shareWith => {
manifold => {
mayReadFreeBusy => JSON::true,
mayReadItems => JSON::true,
mayUpdatePrivate => JSON::true,
mayWriteOwn => JSON::true,
mayAdmin => JSON::false
},
},
},
},
}, 'R1'],
]);
$self->assert(exists $res->[0][1]{updated}{Default});

xlog $self, "Create event notification";
my $res = $jmap->CallMethods([
['CalendarEvent/set', {
create => {
event1 => {
title => 'event1',
calendarIds => {
Default => JSON::true,
},
start => '2011-01-01T04:05:06',
duration => 'PT1H',
},
},
}, 'R1'],
]);
$self->assert_not_null($res->[0][1]{created}{event1});

xlog $self, "Get event notification";
$res = $manJmap->CallMethods([
['CalendarEventNotification/get', {
accountId => 'cassandane',
}, 'R1'],
]);
$self->assert_num_equals(1, scalar @{$res->[0][1]{list}});
my $notif1Id = $res->[0][1]{list}[0]{id};
$self->assert_not_null($notif1Id);

xlog $self, "Create maximum count of allowed notifications";
for my $i (2 .. $jmap_max_calendareventnotifs) {
my $res = $jmap->CallMethods([
['CalendarEvent/set', {
create => {
"event$i" => {
title => "event$i",
calendarIds => {
Default => JSON::true,
},
start => '2011-01-01T04:05:06',
duration => 'PT1H',
},
},
}, 'R1'],
]);
$self->assert_not_null($res->[0][1]{created}{"event$i"});
}

xlog $self, "Get event notifications";
$res = $manJmap->CallMethods([
['CalendarEventNotification/get', {
accountId => 'cassandane',
properties => ['id'],
}, 'R1'],
]);
$self->assert_num_equals($jmap_max_calendareventnotifs, scalar @{$res->[0][1]{list}});

xlog $self, "Assert first event notification exists";
$self->assert_equals(1, scalar grep { $_->{id} eq $notif1Id } @{$res->[0][1]{list}});

xlog $self, "Create one more event notification";
my $res = $jmap->CallMethods([
['CalendarEvent/set', {
create => {
eventX => {
title => 'eventX',
calendarIds => {
Default => JSON::true,
},
start => '2011-01-01T04:05:06',
duration => 'PT1H',
},
},
}, 'R1'],
]);
$self->assert_not_null($res->[0][1]{created}{eventX});

xlog $self, "Get event notifications";
$res = $manJmap->CallMethods([
['CalendarEventNotification/get', {
accountId => 'cassandane',
properties => ['id'],
}, 'R1'],
]);
$self->assert_num_equals($jmap_max_calendareventnotifs, scalar @{$res->[0][1]{list}});

xlog $self, "Assert first event notification does not exist";
$self->assert_equals(0, scalar grep { $_->{id} eq $notif1Id } @{$res->[0][1]{list}});
}
15 changes: 15 additions & 0 deletions changes/next/prune_calendareventnotifs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Description:

Prune JMAP CalendarEventNotification objects each time a new one is created.


Config changes:

jmap_max_calendareventnotifs


Upgrade instructions:

The default maximum count of CalendarEventNotifications is set to 200
per account. Installations that need any other count or want to not
prune notifications must update the jmap_max_calendareventnotifs config.
45 changes: 43 additions & 2 deletions imap/jmap_notif.c
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,27 @@ HIDDEN char *jmap_caleventnotif_format_fromheader(const char *userid)
return notfrom;
}

static void prune_notifications(struct mailbox *notifmbox,
size_t notifications_max)
{
struct mailbox_iter *iter = mailbox_iter_init(notifmbox, 0,
ITER_STEP_BACKWARD|ITER_SKIP_UNLINKED|
ITER_SKIP_EXPUNGED|ITER_SKIP_DELETED);

// Remove all but the last notifications_max messages.
size_t n_notifications = 0;
message_t *msg;
while ((msg = (message_t *) mailbox_iter_step(iter))) {
if (++n_notifications >= notifications_max) {
struct index_record record = *msg_record(msg);
record.internal_flags |= FLAG_INTERNAL_EXPUNGED;
mailbox_rewrite_index_record(notifmbox, &record);
}
}

mailbox_iter_done(&iter);
}

static int append_eventnotif(const char *from,
const char *authuserid,
const struct auth_state *authstate,
Expand All @@ -128,12 +149,31 @@ static int append_eventnotif(const char *from,
struct buf buf = BUF_INITIALIZER;
const char *type = json_string_value(json_object_get(jnotif, "type"));
const char *ical_uid = json_string_value(json_object_get(jnotif, "calendarEventId"));
int notifications_max = config_getint(IMAPOPT_JMAP_MAX_CALENDAREVENTNOTIFS);
if (notifications_max < 0) notifications_max = 0;

// Prune notifications each time we create a new one.
if (notifications_max) {
prune_notifications(notifmbox, (size_t) notifications_max);
}

// Expunge all former notifications for destroyed events.
if (!strcmp(type, "destroyed")) {
/* Expunge all former event notifications for this UID */
struct mailbox_iter *iter = mailbox_iter_init(notifmbox, 0, 0);
struct mailbox_iter *iter = mailbox_iter_init(notifmbox, 0,
ITER_STEP_BACKWARD|ITER_SKIP_UNLINKED|
ITER_SKIP_EXPUNGED|ITER_SKIP_DELETED);
size_t n_notifications = 0;

message_t *msg;
while ((msg = (message_t *) mailbox_iter_step(iter))) {

// Notifications exceeding notifications_max must
// have been pruned already, no need to check.
if (notifications_max &&
(++n_notifications >= (size_t) notifications_max)) {
break;
}

buf_reset(&buf);
if (message_get_subject(msg, &buf) ||
strcmp(JMAP_NOTIF_CALENDAREVENT, buf_cstring(&buf))) {
Expand Down Expand Up @@ -165,6 +205,7 @@ static int append_eventnotif(const char *from,
}
buf_reset(&buf);

// Append new notification.
FILE *fp = append_newstage(mailbox_name(notifmbox), created,
strhash(ical_uid), &stage);
if (!fp) {
Expand Down
6 changes: 6 additions & 0 deletions lib/imapoptions
Original file line number Diff line number Diff line change
Expand Up @@ -1293,6 +1293,12 @@ Blank lines and lines beginning with ``#'' are ignored.
For backward compatibility, if no unit is specified, kibibytes is assumed.
*/

{ "jmap_max_calendareventnotifs", 200, INT, "UNRELEASED" }
/* The maximum count of CalendarEventNotification objects to keep per account.
Any notifications exceeding this count are expunged to make room for new
ones. Zero or any negative number disables this limit.
*/

{ "jmap_max_concurrent_upload", 5, INT, "3.1.6" }
/* The value to return for the maxConcurrentUpload property of
the JMAP \"urn:ietf:params:jmap:core\" capabilities object. The Cyrus JMAP
Expand Down

0 comments on commit 0fd56ad

Please sign in to comment.