diff --git a/cassandane/tiny-tests/JMAPCalendars/calendarevent_copy_state b/cassandane/tiny-tests/JMAPCalendars/calendarevent_copy_state new file mode 100644 index 0000000000..3a8fa55b9a --- /dev/null +++ b/cassandane/tiny-tests/JMAPCalendars/calendarevent_copy_state @@ -0,0 +1,109 @@ +#!perl +use Cassandane::Tiny; + +sub test_calendarevent_copy_state + :needs_component_jmap +{ + my ($self) = @_; + my $jmap = $self->{jmap}; + my $caldav = $self->{caldav}; + my $admintalk = $self->{adminstore}->get_client(); + my $service = $self->{instance}->get_service("http"); + + xlog $self, "create shared accounts"; + $admintalk->create("user.other"); + + my $othercaldav = Net::CalDAVTalk->new( + user => "other", + password => 'pass', + host => $service->host(), + port => $service->port(), + scheme => 'http', + url => '/', + expandurl => 1, + ); + + $admintalk->setacl('user.other', admin => 'lrswipkxtecdan'); + $admintalk->setacl('user.other', other => 'lrswipkxtecdn'); + + xlog $self, "create source calendar"; + my $srcCalendarId = $caldav->NewCalendar({name => 'Source Calendar'}); + $self->assert_not_null($srcCalendarId); + + xlog $self, "create destination calendar"; + my $dstCalendarId = $othercaldav->NewCalendar({name => 'Destination Calendar'}); + $self->assert_not_null($dstCalendarId); + + xlog $self, "share calendar"; + $admintalk->setacl("user.other.#calendars.$dstCalendarId", "cassandane" => 'lrswipkxtecdn') or die; + + my $event = { + calendarIds => { + $srcCalendarId => JSON::true, + }, + "uid" => "58ADE31-custom-UID", + "title"=> "foo", + "start"=> "2015-11-07T09:00:00", + "duration"=> "PT5M", + "sequence"=> 42, + "timeZone"=> "Etc/UTC", + "showWithoutTime"=> JSON::false, + "locale" => "en", + "status" => "tentative", + "description"=> "", + "freeBusyStatus"=> "busy", + "participants" => undef, + "alerts"=> undef, + }; + + xlog $self, "create event"; + my $res = $jmap->CallMethods([ + ['CalendarEvent/set', { + create => {"1" => $event} + }, "R1"], + ['CalendarEvent/get', { + accountId => 'other', + ids => ['foo'], # Just fetching current state for 'other' + }, 'R2'], + ]); + $self->assert_not_null($res->[0][1]{created}); + my $eventId = $res->[0][1]{created}{"1"}{id}; + my $fromState = $res->[0][1]->{newState}; + $self->assert_not_null($fromState); + my $state = $res->[1][1]->{state}; + $self->assert_not_null($state); + + xlog $self, "move event"; + $res = $jmap->CallMethods([ + ['CalendarEvent/copy', { + fromAccountId => 'cassandane', + accountId => 'other', + ifFromInState => $fromState, + ifInState => $state, + create => { + 1 => { + id => $eventId, + calendarIds => { + $dstCalendarId => JSON::true, + }, + }, + }, + onSuccessDestroyOriginal => JSON::true, + destroyFromIfInState => $fromState, + }, "R1"], + ['CalendarEvent/get', { + accountId => 'other', + ids => ['#1'], + properties => ['title'], + }, 'R2'], + ]); + $self->assert_not_null($res->[0][1]{created}); + my $copiedEventId = $res->[0][1]{created}{"1"}{id}; + my $oldState = $res->[0][1]->{oldState}; + $self->assert_str_equals($oldState, $state); + my $newState = $res->[0][1]->{newState}; + $self->assert_not_null($newState); + $self->assert_str_equals('CalendarEvent/set', $res->[1][0]); + $self->assert_str_equals($eventId, $res->[1][1]{destroyed}[0]); + $self->assert_str_equals('foo', $res->[2][1]{list}[0]{title}); +} diff --git a/cassandane/tiny-tests/JMAPContacts/card_copy_state b/cassandane/tiny-tests/JMAPContacts/card_copy_state new file mode 100644 index 0000000000..4630e47898 --- /dev/null +++ b/cassandane/tiny-tests/JMAPContacts/card_copy_state @@ -0,0 +1,83 @@ +#!perl +use Cassandane::Tiny; + +sub test_card_copy_state + :needs_component_jmap :needs_dependency_icalvcard +{ + my ($self) = @_; + my $jmap = $self->{jmap}; + my $carddav = $self->{carddav}; + my $admintalk = $self->{adminstore}->get_client(); + my $service = $self->{instance}->get_service("http"); + + xlog $self, "create shared account"; + $admintalk->create("user.other"); + + my $othercarddav = Net::CardDAVTalk->new( + user => "other", + password => 'pass', + host => $service->host(), + port => $service->port(), + scheme => 'http', + url => '/', + expandurl => 1, + ); + + xlog $self, "share addressbook"; + $admintalk->setacl("user.other.#addressbooks.Default", + "cassandane" => 'lrswipkxtecdn') or die; + + my $card = { + "addressBookId" => "Default", + name => { full => "foo bar" }, + }; + + xlog $self, "create card"; + $res = $jmap->CallMethods([ + ['ContactCard/set', { + create => {"1" => $card} + }, "R1"], + ['ContactCard/get', { + accountId => 'other', + ids => ['foo'], # Just fetching current state for 'other' + }, 'R2'], + ]); + $self->assert_not_null($res->[0][1]{created}); + my $cardId = $res->[0][1]{created}{"1"}{id}; + my $fromState = $res->[0][1]->{newState}; + $self->assert_not_null($fromState); + my $state = $res->[1][1]->{state}; + $self->assert_not_null($state); + + xlog $self, "move card"; + $res = $jmap->CallMethods([ + ['ContactCard/copy', { + fromAccountId => 'cassandane', + accountId => 'other', + ifFromInState => $fromState, + ifInState => $state, + create => { + 1 => { + id => $cardId, + addressBookId => "Default", + }, + }, + onSuccessDestroyOriginal => JSON::true, + destroyFromIfInState => $fromState, + }, "R1"], + ['ContactCard/get', { + accountId => 'other', + ids => ['#1'], + properties => ['name'], + }, 'R2'], + ]); + $self->assert_not_null($res->[0][1]{created}); + my $copiedCardId = $res->[0][1]{created}{"1"}{id}; + my $oldState = $res->[0][1]->{oldState}; + $self->assert_str_equals($oldState, $state); + my $newState = $res->[0][1]->{newState}; + $self->assert_not_null($newState); + $self->assert_str_equals('ContactCard/set', $res->[1][0]); + $self->assert_str_equals($cardId, $res->[1][1]{destroyed}[0]); + $self->assert_str_equals('foo bar', $res->[2][1]{list}[0]{name}{full}); +} diff --git a/cassandane/tiny-tests/JMAPContacts/contact_copy_state b/cassandane/tiny-tests/JMAPContacts/contact_copy_state new file mode 100644 index 0000000000..51e807cabd --- /dev/null +++ b/cassandane/tiny-tests/JMAPContacts/contact_copy_state @@ -0,0 +1,84 @@ +#!perl +use Cassandane::Tiny; + +sub test_contact_copy_state + :needs_component_jmap +{ + my ($self) = @_; + my $jmap = $self->{jmap}; + my $carddav = $self->{carddav}; + my $admintalk = $self->{adminstore}->get_client(); + my $service = $self->{instance}->get_service("http"); + + xlog $self, "create shared account"; + $admintalk->create("user.other"); + + my $othercarddav = Net::CardDAVTalk->new( + user => "other", + password => 'pass', + host => $service->host(), + port => $service->port(), + scheme => 'http', + url => '/', + expandurl => 1, + ); + + xlog $self, "share addressbook"; + $admintalk->setacl("user.other.#addressbooks.Default", + "cassandane" => 'lrswipkxtecdn') or die; + + my $card = { + "addressbookId" => "Default", + "firstName"=> "foo", + "lastName"=> "bar", + }; + + xlog $self, "create card"; + $res = $jmap->CallMethods([ + ['Contact/set', { + create => {"1" => $card} + }, "R1"], + ['Contact/get', { + accountId => 'other', + ids => ['foo'], # Just fetching current state for 'other' + }, 'R2'], + ]); + $self->assert_not_null($res->[0][1]{created}); + my $cardId = $res->[0][1]{created}{"1"}{id}; + my $fromState = $res->[0][1]->{newState}; + $self->assert_not_null($fromState); + my $state = $res->[1][1]->{state}; + $self->assert_not_null($state); + + xlog $self, "move card"; + $res = $jmap->CallMethods([ + ['Contact/copy', { + fromAccountId => 'cassandane', + accountId => 'other', + ifFromInState => $fromState, + ifInState => $state, + create => { + 1 => { + id => $cardId, + addressbookId => "Default", + }, + }, + onSuccessDestroyOriginal => JSON::true, + destroyFromIfInState => $fromState, + }, "R1"], + ['Contact/get', { + accountId => 'other', + ids => ['#1'], + properties => ['firstName'], + }, 'R2'], + ]); + $self->assert_not_null($res->[0][1]{created}); + my $copiedCardId = $res->[0][1]{created}{"1"}{id}; + my $oldState = $res->[0][1]->{oldState}; + $self->assert_str_equals($oldState, $state); + my $newState = $res->[0][1]->{newState}; + $self->assert_not_null($newState); + $self->assert_str_equals('Contact/set', $res->[1][0]); + $self->assert_str_equals($cardId, $res->[1][1]{destroyed}[0]); + $self->assert_str_equals('foo', $res->[2][1]{list}[0]{firstName}); +} diff --git a/cassandane/tiny-tests/JMAPEmail/email_copy_state b/cassandane/tiny-tests/JMAPEmail/email_copy_state new file mode 100644 index 0000000000..86ea652a40 --- /dev/null +++ b/cassandane/tiny-tests/JMAPEmail/email_copy_state @@ -0,0 +1,90 @@ +#!perl +use Cassandane::Tiny; + +sub test_email_copy_state + :needs_component_jmap +{ + my ($self) = @_; + my $jmap = $self->{jmap}; + my $imaptalk = $self->{store}->get_client(); + my $admintalk = $self->{adminstore}->get_client(); + + xlog $self, "Create user and share mailbox"; + $self->{instance}->create_user("other"); + $admintalk->setacl("user.other", "cassandane", "lrsiwntex") or die; + + my $srcInboxId = $self->getinbox()->{id}; + $self->assert_not_null($srcInboxId); + + my $dstInboxId = $self->getinbox({accountId => 'other'})->{id}; + $self->assert_not_null($dstInboxId); + + xlog $self, "create email"; + my $res = $jmap->CallMethods([ + ['Email/set', { + create => { + 1 => { + mailboxIds => { + $srcInboxId => JSON::true, + }, + keywords => { + 'foo' => JSON::true, + }, + subject => 'hello', + bodyStructure => { + type => 'text/plain', + partId => 'part1', + }, + bodyValues => { + part1 => { + value => 'world', + } + }, + }, + }, + }, 'R1'], + ['Email/get', { + accountId => 'other', + ids => ['foo'], # Just fetching current state for 'other' + }, 'R2'] + ]); + my $emailId = $res->[0][1]->{created}{1}{id}; + $self->assert_not_null($emailId); + my $fromState = $res->[0][1]->{newState}; + $self->assert_not_null($fromState); + my $state = $res->[1][1]->{state}; + $self->assert_not_null($state); + + xlog $self, "move email"; + $res = $jmap->CallMethods([ + ['Email/copy', { + fromAccountId => 'cassandane', + accountId => 'other', + ifFromInState => $fromState, + ifInState => $state, + create => { + 1 => { + id => $emailId, + mailboxIds => { + $dstInboxId => JSON::true, + }, + }, + }, + onSuccessDestroyOriginal => JSON::true, + destroyFromIfInState => $fromState, + }, 'R1'], + ['Email/get', { + accountId => 'other', + ids => ['#1'], + properties => ['mailboxIds'], + }, 'R2'] + ]); + $self->assert_not_null($res->[0][1]{created}); + my $oldState = $res->[0][1]->{oldState}; + $self->assert_str_equals($oldState, $state); + my $newState = $res->[0][1]->{newState}; + $self->assert_not_null($newState); + $self->assert_str_equals('Email/set', $res->[1][0]); + $self->assert_str_equals($emailId, $res->[1][1]{destroyed}[0]); + $self->assert_not_null($res->[2][1]{list}[0]{mailboxIds}{$dstInboxId}); +} diff --git a/imap/jmap_api.c b/imap/jmap_api.c index 85bcea5e35..249527d663 100644 --- a/imap/jmap_api.c +++ b/imap/jmap_api.c @@ -2013,25 +2013,49 @@ HIDDEN void jmap_copy_parse(jmap_req_t *req, struct jmap_parser *parser, continue; } - /* blobIds */ - else if (copy->blob_copy && - !strcmp(key, "blobIds") && json_is_array(arg)) { - struct buf buf = BUF_INITIALIZER; - json_t *id; - size_t i; - json_array_foreach(arg, i, id) { - if (!json_is_string(id)) { - buf_printf(&buf, "blobIds[%zu]", i); - jmap_parser_invalid(parser, buf_cstring(&buf)); - buf_reset(&buf); + else if (copy->blob_copy) { + /* blobIds */ + if (!strcmp(key, "blobIds") && json_is_array(arg)) { + struct buf buf = BUF_INITIALIZER; + json_t *id; + size_t i; + json_array_foreach(arg, i, id) { + if (!json_is_string(id)) { + buf_printf(&buf, "blobIds[%zu]", i); + jmap_parser_invalid(parser, buf_cstring(&buf)); + buf_reset(&buf); + } + else json_array_append(copy->create, id); } - else json_array_append(copy->create, id); + } + + else if (!args_parse || !args_parse(req, parser, key, arg, args_rock)) { + jmap_parser_invalid(parser, key); + } + } + + /* ifFromInState */ + else if (!strcmp(key, "ifFromInState")) { + if (json_is_string(arg)) { + copy->if_from_in_state = json_string_value(arg); + } + else if (JNOTNULL(arg)) { + jmap_parser_invalid(parser, "ifFromInState"); + } + } + + /* ifInState */ + else if (!strcmp(key, "ifInState")) { + if (json_is_string(arg)) { + copy->if_in_state = json_string_value(arg); + } + else if (JNOTNULL(arg)) { + jmap_parser_invalid(parser, "ifInState"); } } /* create */ - else if (!copy->blob_copy && - !strcmp(key, "create") && json_is_object(arg)) { + else if (!strcmp(key, "create") && json_is_object(arg)) { jmap_parser_push(parser, "create"); const char *creation_id; json_t *obj; @@ -2050,11 +2074,21 @@ HIDDEN void jmap_copy_parse(jmap_req_t *req, struct jmap_parser *parser, } /* onSuccessDestroyOriginal */ - else if (!copy->blob_copy && !strcmp(key, "onSuccessDestroyOriginal") && + else if (!strcmp(key, "onSuccessDestroyOriginal") && json_is_boolean(arg)) { copy->on_success_destroy_original = json_boolean_value(arg); } + /* destroyFromIfInState */ + else if (!strcmp(key, "destroyFromIfInState")) { + if (json_is_string(arg)) { + copy->destroy_from_if_in_state = json_string_value(arg); + } + else if (JNOTNULL(arg)) { + jmap_parser_invalid(parser, "destroyFromIfInState"); + } + } + else if (!args_parse || !args_parse(req, parser, key, arg, args_rock)) { jmap_parser_invalid(parser, key); } @@ -2075,6 +2109,8 @@ HIDDEN void jmap_copy_parse(jmap_req_t *req, struct jmap_parser *parser, HIDDEN void jmap_copy_fini(struct jmap_copy *copy) { + free(copy->old_state); + free(copy->new_state); json_decref(copy->create); json_decref(copy->created); json_decref(copy->not_created); @@ -2085,6 +2121,9 @@ HIDDEN json_t *jmap_copy_reply(struct jmap_copy *copy) json_t *res = json_object(); json_object_set_new(res, "fromAccountId", json_string(copy->from_account_id)); + json_object_set_new(res, "oldState", + copy->old_state ? json_string(copy->old_state) : json_null()); + json_object_set_new(res, "newState", json_string(copy->new_state)); json_object_set(res, copy->blob_copy ? "copied" : "created", json_object_size(copy->created) ? copy->created : json_null()); diff --git a/imap/jmap_api.h b/imap/jmap_api.h index 4d52489a1d..af4efa0f04 100644 --- a/imap/jmap_api.h +++ b/imap/jmap_api.h @@ -425,11 +425,16 @@ extern json_t *jmap_changes_reply(struct jmap_changes *changes); struct jmap_copy { /* Request arguments */ const char *from_account_id; + const char *if_from_in_state; + const char *if_in_state; json_t *create; int blob_copy; int on_success_destroy_original; + const char *destroy_from_if_in_state; /* Response fields */ + char *old_state; + char *new_state; json_t *created; json_t *not_created; }; diff --git a/imap/jmap_calendar.c b/imap/jmap_calendar.c index a9f3eb70fc..530c9b1fd4 100644 --- a/imap/jmap_calendar.c +++ b/imap/jmap_calendar.c @@ -7687,6 +7687,8 @@ static int jmap_calendarevent_copy(struct jmap_req *req) mbentry_t *notifmb = NULL; struct mboxlock *srcnamespacelock = NULL; struct mboxlock *dstnamespacelock = NULL; + char *srcinbox = NULL; + char *dstinbox = NULL; /* Parse request */ jmap_copy_parse(req, &parser, NULL, NULL, ©, &err); @@ -7695,8 +7697,8 @@ static int jmap_calendarevent_copy(struct jmap_req *req) goto done; } - char *srcinbox = mboxname_user_mbox(copy.from_account_id, NULL); - char *dstinbox = mboxname_user_mbox(req->accountid, NULL); + srcinbox = mboxname_user_mbox(copy.from_account_id, NULL); + dstinbox = mboxname_user_mbox(req->accountid, NULL); if (strcmp(srcinbox, dstinbox) < 0) { srcnamespacelock = mboxname_usernamespacelock(srcinbox); dstnamespacelock = mboxname_usernamespacelock(dstinbox); @@ -7705,8 +7707,26 @@ static int jmap_calendarevent_copy(struct jmap_req *req) dstnamespacelock = mboxname_usernamespacelock(dstinbox); srcnamespacelock = mboxname_usernamespacelock(srcinbox); } - free(srcinbox); - free(dstinbox); + + if (copy.if_from_in_state) { + struct mboxname_counters counters; + assert (!mboxname_read_counters(srcinbox, &counters)); + if (atomodseq_t(copy.if_from_in_state) != counters.caldavmodseq) { + jmap_error(req, json_pack("{s:s}", "type", "stateMismatch")); + goto done; + } + } + + if (copy.if_in_state) { + if (atomodseq_t(copy.if_in_state) != jmap_modseq(req, MBTYPE_CALENDAR, 0)) { + jmap_error(req, json_pack("{s:s}", "type", "stateMismatch")); + goto done; + } + copy.old_state = xstrdup(copy.if_in_state); + } + else { + copy.old_state = modseqtoa(jmap_modseq(req, MBTYPE_CALENDAR, 0)); + } // now we can open the cstate int r = conversations_open_user(req->accountid, 0, &req->cstate); @@ -7761,6 +7781,7 @@ static int jmap_calendarevent_copy(struct jmap_req *req) } /* Build response */ + copy.new_state = modseqtoa(jmap_modseq(req, MBTYPE_CALENDAR, JMAP_MODSEQ_RELOAD)); jmap_ok(req, jmap_copy_reply(©)); /* Destroy originals, if requested */ @@ -7768,6 +7789,10 @@ static int jmap_calendarevent_copy(struct jmap_req *req) json_t *subargs = json_object(); json_object_set(subargs, "destroy", destroy_events); json_object_set_new(subargs, "accountId", json_string(copy.from_account_id)); + if (copy.destroy_from_if_in_state) { + json_object_set_new(subargs, "ifInState", + json_string(copy.destroy_from_if_in_state)); + } jmap_add_subreq(req, "CalendarEvent/set", subargs, NULL); } @@ -7781,6 +7806,8 @@ static int jmap_calendarevent_copy(struct jmap_req *req) mboxname_release(&dstnamespacelock); jmap_parser_fini(&parser); jmap_copy_fini(©); + free(srcinbox); + free(dstinbox); return 0; } diff --git a/imap/jmap_contact.c b/imap/jmap_contact.c index 7d362a3ab2..55926412a7 100644 --- a/imap/jmap_contact.c +++ b/imap/jmap_contact.c @@ -4525,6 +4525,8 @@ static int _contacts_copy(struct jmap_req *req, json_t *destroy_cards = json_array(); struct mboxlock *srcnamespacelock = NULL; struct mboxlock *dstnamespacelock = NULL; + char *srcinbox = NULL; + char *dstinbox = NULL; /* Parse request */ jmap_copy_parse(req, &parser, NULL, NULL, ©, &err); @@ -4533,8 +4535,8 @@ static int _contacts_copy(struct jmap_req *req, goto done; } - char *srcinbox = mboxname_user_mbox(copy.from_account_id, NULL); - char *dstinbox = mboxname_user_mbox(req->accountid, NULL); + srcinbox = mboxname_user_mbox(copy.from_account_id, NULL); + dstinbox = mboxname_user_mbox(req->accountid, NULL); if (strcmp(srcinbox, dstinbox) < 0) { srcnamespacelock = mboxname_usernamespacelock(srcinbox); dstnamespacelock = mboxname_usernamespacelock(dstinbox); @@ -4543,8 +4545,26 @@ static int _contacts_copy(struct jmap_req *req, dstnamespacelock = mboxname_usernamespacelock(dstinbox); srcnamespacelock = mboxname_usernamespacelock(srcinbox); } - free(srcinbox); - free(dstinbox); + + if (copy.if_from_in_state) { + struct mboxname_counters counters; + assert (!mboxname_read_counters(srcinbox, &counters)); + if (atomodseq_t(copy.if_from_in_state) != counters.carddavmodseq) { + jmap_error(req, json_pack("{s:s}", "type", "stateMismatch")); + goto done; + } + } + + if (copy.if_in_state) { + if (atomodseq_t(copy.if_in_state) != jmap_modseq(req, MBTYPE_ADDRESSBOOK, 0)) { + jmap_error(req, json_pack("{s:s}", "type", "stateMismatch")); + goto done; + } + copy.old_state = xstrdup(copy.if_in_state); + } + else { + copy.old_state = modseqtoa(jmap_modseq(req, MBTYPE_ADDRESSBOOK, 0)); + } // now we can open the cstate int r = conversations_open_user(req->accountid, 0, &req->cstate); @@ -4586,6 +4606,7 @@ static int _contacts_copy(struct jmap_req *req, } /* Build response */ + copy.new_state = modseqtoa(jmap_modseq(req, MBTYPE_ADDRESSBOOK, JMAP_MODSEQ_RELOAD)); jmap_ok(req, jmap_copy_reply(©)); /* Destroy originals, if requested */ @@ -4595,6 +4616,10 @@ static int _contacts_copy(struct jmap_req *req, json_t *subargs = json_object(); json_object_set(subargs, "destroy", destroy_cards); json_object_set_new(subargs, "accountId", json_string(copy.from_account_id)); + if (copy.destroy_from_if_in_state) { + json_object_set_new(subargs, "ifInState", + json_string(copy.destroy_from_if_in_state)); + } jmap_add_subreq(req, submethod, subargs, NULL); } @@ -4605,6 +4630,8 @@ static int _contacts_copy(struct jmap_req *req, mboxname_release(&dstnamespacelock); jmap_parser_fini(&parser); jmap_copy_fini(©); + free(srcinbox); + free(dstinbox); return 0; } diff --git a/imap/jmap_mail.c b/imap/jmap_mail.c index e3896a2626..e60c7d16f4 100644 --- a/imap/jmap_mail.c +++ b/imap/jmap_mail.c @@ -14095,6 +14095,8 @@ static int jmap_email_copy(jmap_req_t *req) const mbentry_t *scheduled_mbe = NULL; struct mboxlock *srcnamespacelock = NULL; struct mboxlock *dstnamespacelock = NULL; + char *srcinbox = NULL; + char *dstinbox = NULL; /* Parse request */ jmap_copy_parse(req, &parser, NULL, NULL, ©, &err); @@ -14107,8 +14109,8 @@ static int jmap_email_copy(jmap_req_t *req) abort(); } - char *srcinbox = mboxname_user_mbox(copy.from_account_id, NULL); - char *dstinbox = mboxname_user_mbox(req->accountid, NULL); + srcinbox = mboxname_user_mbox(copy.from_account_id, NULL); + dstinbox = mboxname_user_mbox(req->accountid, NULL); if (strcmp(srcinbox, dstinbox) < 0) { srcnamespacelock = mboxname_usernamespacelock(srcinbox); dstnamespacelock = mboxname_usernamespacelock(dstinbox); @@ -14117,8 +14119,26 @@ static int jmap_email_copy(jmap_req_t *req) dstnamespacelock = mboxname_usernamespacelock(dstinbox); srcnamespacelock = mboxname_usernamespacelock(srcinbox); } - free(srcinbox); - free(dstinbox); + + if (copy.if_from_in_state) { + struct mboxname_counters counters; + assert (!mboxname_read_counters(srcinbox, &counters)); + if (atomodseq_t(copy.if_from_in_state) != counters.mailmodseq) { + jmap_error(req, json_pack("{s:s}", "type", "stateMismatch")); + goto done; + } + } + + if (copy.if_in_state) { + if (atomodseq_t(copy.if_in_state) != jmap_modseq(req, MBTYPE_EMAIL, 0)) { + jmap_error(req, json_pack("{s:s}", "type", "stateMismatch")); + goto done; + } + copy.old_state = xstrdup(copy.if_in_state); + } + else { + copy.old_state = modseqtoa(jmap_modseq(req, MBTYPE_EMAIL, 0)); + } // now we can open the cstate int r = conversations_open_user(req->accountid, 0, &req->cstate); @@ -14174,6 +14194,7 @@ static int jmap_email_copy(jmap_req_t *req) } /* Build response */ + copy.new_state = modseqtoa(jmap_modseq(req, MBTYPE_EMAIL, JMAP_MODSEQ_RELOAD)); jmap_ok(req, jmap_copy_reply(©)); /* Destroy originals, if requested */ @@ -14181,6 +14202,10 @@ static int jmap_email_copy(jmap_req_t *req) json_t *subargs = json_object(); json_object_set(subargs, "destroy", destroy_emails); json_object_set_new(subargs, "accountId", json_string(copy.from_account_id)); + if (copy.destroy_from_if_in_state) { + json_object_set_new(subargs, "ifInState", + json_string(copy.destroy_from_if_in_state)); + } jmap_add_subreq(req, "Email/set", subargs, NULL); } @@ -14191,6 +14216,8 @@ static int jmap_email_copy(jmap_req_t *req) jmap_parser_fini(&parser); jmap_copy_fini(©); seen_close(&seendb); + free(srcinbox); + free(dstinbox); return 0; }