Skip to content

Commit

Permalink
IMAP: add support for UIDONLY (draft-ietf-extra-imap-uidonly)
Browse files Browse the repository at this point in the history
  • Loading branch information
ksmurchison committed Nov 9, 2023
1 parent c89f215 commit cc7ab0b
Show file tree
Hide file tree
Showing 14 changed files with 594 additions and 14 deletions.
88 changes: 88 additions & 0 deletions cassandane/Cassandane/Cyrus/UIDonly.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/usr/bin/perl
#
# Copyright (c) 2011-2023 FastMail Pty Ltd. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# 3. The name "Fastmail Pty Ltd" must not be used to
# endorse or promote products derived from this software without
# prior written permission. For permission or any legal
# details, please contact
# FastMail Pty Ltd
# PO Box 234
# Collins St West 8007
# Victoria
# Australia
#
# 4. Redistributions of any form whatsoever must retain the following
# acknowledgment:
# "This product includes software developed by Fastmail Pty. Ltd."
#
# FASTMAIL PTY LTD DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO
# EVENT SHALL OPERA SOFTWARE AUSTRALIA BE LIABLE FOR ANY SPECIAL, INDIRECT
# OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF
# USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
# OF THIS SOFTWARE.
#

package Cassandane::Cyrus::UIDonly;
use strict;
use warnings;
use DateTime;
use JSON;
use JSON::XS;
use Mail::JMAPTalk 0.13;
use Data::Dumper;
use Storable 'dclone';
use File::Basename;
use IO::File;

use lib '.';
use base qw(Cassandane::Cyrus::TestCase);
use Cassandane::Util::Log;

use charnames ':full';

sub new
{
my ($class, @args) = @_;

my $config = Cassandane::Config->default()->clone();

$config->set(conversations => 'yes');

return $class->SUPER::new({
config => $config,
deliver => 1,
adminstore => 1,
services => [ 'imap', 'sieve' ]
}, @args);
}

sub set_up
{
my ($self) = @_;
$self->SUPER::set_up();
}

sub tear_down
{
my ($self) = @_;
$self->SUPER::tear_down();
}

use Cassandane::Tiny::Loader 'tiny-tests/UIDonly';

1;
49 changes: 49 additions & 0 deletions cassandane/tiny-tests/UIDonly/copy-move
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!perl
use Cassandane::Tiny;

sub test_copy_move
:min_version_3_9 :NoAltNameSpace
{
my ($self) = @_;

my $imaptalk = $self->{store}->get_client();
my $folder = 'INBOX.foo';

xlog $self, "append some messages";
my %exp;
my $N = 10;
for (1..$N)
{
my $msg = $self->make_message("Message $_");
$exp{$_} = $msg;
}
xlog $self, "check the messages got there";
$self->check_messages(\%exp);

xlog $self, "create a second mailbox";
my $res =$imaptalk->create($folder);
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());

xlog $self, "ENABLE UIDONLY";
$res = $imaptalk->_imap_cmd('ENABLE', 0, 'enabled', 'UIDONLY');
$self->assert_num_equals(1, $res->{uidonly});

xlog $self, "attempt COPY";
$res = $imaptalk->copy(1, $folder);
$self->assert_str_equals('bad', $imaptalk->get_last_completion_response());
# get_response_code() doesn't (yet) handle [UIDREQUIRED]
$self->assert_matches(qr/[UIDREQUIRED]/, $imaptalk->get_last_error());

xlog $self, "attempt MOVE";
$res = $imaptalk->move(1, $folder);
$self->assert_str_equals('bad', $imaptalk->get_last_completion_response());
# get_response_code() doesn't (yet) handle [UIDREQUIRED]
$self->assert_matches(qr/[UIDREQUIRED]/, $imaptalk->get_last_error());

xlog $self, "UID MOVE";
$res = $imaptalk->_imap_cmd('UID MOVE', 1, 'vanished', '1', $folder);
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
$self->assert_str_equals('1', $res->[0]);
}

1;
36 changes: 36 additions & 0 deletions cassandane/tiny-tests/UIDonly/expunge
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!perl
use Cassandane::Tiny;

sub test_expunge
:min_version_3_9
{
my ($self) = @_;

my $imaptalk = $self->{store}->get_client();

xlog $self, "append some messages";
my %exp;
my $N = 10;
for (1..$N)
{
my $msg = $self->make_message("Message $_");
$exp{$_} = $msg;
}
xlog $self, "check the messages got there";
$self->check_messages(\%exp);

xlog $self, "delete the 1st and 6th";
$imaptalk->store('1,6', '+FLAGS', '(\\Deleted)');
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());

xlog $self, "ENABLE UIDONLY";
$res = $imaptalk->_imap_cmd('ENABLE', 0, 'enabled', 'UIDONLY');
$self->assert_num_equals(1, $res->{uidonly});

xlog $self, "EXPUNGE";
$res = $imaptalk->_imap_cmd('EXPUNGE', 0, 'vanished');
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
$self->assert_str_equals('1,6', $res->[0]);
}

1;
78 changes: 78 additions & 0 deletions cassandane/tiny-tests/UIDonly/fetch
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!perl
use Cassandane::Tiny;

sub test_fetch
:min_version_3_9
{
my ($self) = @_;

my $imaptalk = $self->{store}->get_client();

xlog $self, "append some messages";
my %exp;
my $N = 10;
for (1..$N)
{
my $msg = $self->make_message("Message $_");
$exp{$_} = $msg;
}
xlog $self, "check the messages got there";
$self->check_messages(\%exp);

xlog $self, "EXPUNGE the 1st and 6th";
$imaptalk->store('1,6', '+FLAGS', '(\\Deleted)');
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
$imaptalk->expunge();
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());

xlog $self, "FETCH all";
my $res = $imaptalk->fetch('1:*', '(UID FLAGS)');
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
$self->assert_str_equals($res->{'1'}->{uid}, "2");
$self->assert_str_equals($res->{'2'}->{uid}, "3");
$self->assert_str_equals($res->{'3'}->{uid}, "4");
$self->assert_str_equals($res->{'4'}->{uid}, "5");
$self->assert_str_equals($res->{'5'}->{uid}, "7");
$self->assert_str_equals($res->{'6'}->{uid}, "8");
$self->assert_str_equals($res->{'7'}->{uid}, "9");
$self->assert_str_equals($res->{'8'}->{uid}, "10");

xlog $self, "ENABLE UIDONLY";
$res = $imaptalk->_imap_cmd('ENABLE', 0, 'enabled', 'UIDONLY');
$self->assert_num_equals(1, $res->{uidonly});

xlog $self, "attempt FETCH again";
$res = $imaptalk->fetch('1:*', '(UID FLAGS)');
$self->assert_str_equals('bad', $imaptalk->get_last_completion_response());
# get_response_code() doesn't (yet) handle [UIDREQUIRED]
$self->assert_matches(qr/[UIDREQUIRED]/, $imaptalk->get_last_error());

my %fetched= {};
my %handlers =
(
uidfetch => sub
{
my (undef, undef, $uid) = @_;

$fetched{$uid} = $imaptalk->_next_atom();
},
);

xlog $self, "UID FETCH all";
$res = $imaptalk->_imap_cmd("UID FETCH", 1, \%handlers,
'1:10', '(UID FLAGS)');
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
$self->assert(exists $fetched{'2'});
# make sure UID isn't in the response
$self->assert_num_equals(2, scalar @{$fetched{'2'}});
$self->assert_str_equals('FLAGS', $fetched{'2'}->[0]);
$self->assert(exists $fetched{'3'});
$self->assert(exists $fetched{'4'});
$self->assert(exists $fetched{'5'});
$self->assert(exists $fetched{'7'});
$self->assert(exists $fetched{'8'});
$self->assert(exists $fetched{'9'});
$self->assert(exists $fetched{'10'});
}

1;
53 changes: 53 additions & 0 deletions cassandane/tiny-tests/UIDonly/qresync
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!perl
use Cassandane::Tiny;

sub test_qresync
:min_version_3_9
{
my ($self) = @_;

my $imaptalk = $self->{store}->get_client();

xlog $self, "Deliver a message";
my $msg = $self->{gen}->generate(subject => "Message 1");
$self->{instance}->deliver($msg);

$imaptalk->select("INBOX");
my $uidvalidity = $imaptalk->get_response_code('uidvalidity');

xlog $self, "ENABLE QRESYNC";
my $res = $imaptalk->_imap_cmd('ENABLE', 0, 'enabled', 'QRESYNC');
$self->assert_num_equals(1, $res->{qresync});

xlog "QResync mailbox with message sequence map";
$imaptalk->unselect();
$imaptalk->select("INBOX", "(QRESYNC ($uidvalidity 0 1 (1 1)))" => 1);
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
$res = $imaptalk->get_response_code('fetch');
$self->assert_not_null($res);
$self->assert(exists $res->{'1'}{'flags'});
$self->assert(exists $res->{'1'}{'modseq'});
$self->assert(exists $res->{'1'}{'uid'});

xlog $self, "ENABLE UIDONLY";
$res = $imaptalk->_imap_cmd('ENABLE', 0, 'enabled', 'UIDONLY');
$self->assert_num_equals(1, $res->{uidonly});

xlog "attenpt to QResync mailbox with message sequence map";
$imaptalk->unselect();
$imaptalk->select("INBOX", "(QRESYNC ($uidvalidity 0 1 (1 1)))" => 1);
$self->assert_str_equals('bad', $imaptalk->get_last_completion_response());

xlog "QResync mailbox";
$imaptalk->unselect();
$imaptalk->select("INBOX", "(QRESYNC ($uidvalidity 0 1))" => 1);
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
$res = $imaptalk->get_response_code('uidfetch');
# make sure UID isn't in the response
$self->assert_not_null($res);
$self->assert_num_equals(4, scalar @{$res});
$self->assert_str_equals('FLAGS', $res->[0]);
$self->assert_str_equals('MODSEQ', $res->[2]);
}

1;
72 changes: 72 additions & 0 deletions cassandane/tiny-tests/UIDonly/search
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!perl
use Cassandane::Tiny;

sub test_search
:min_version_3_9
{
my ($self) = @_;

my $imaptalk = $self->{store}->get_client();

xlog $self, "append some messages";
my %exp;
my $N = 10;
for (1..$N)
{
my $msg = $self->make_message("Message $_");
$exp{$_} = $msg;
}
xlog $self, "check the messages got there";
$self->check_messages(\%exp);

xlog $self, "delete the 1st and 6th";
$imaptalk->store('1,6', '+FLAGS', '(\\Deleted)');
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());

xlog $self, "SEARCH";
$res = $imaptalk->search('not', 'deleted');
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
$self->assert_num_equals(8, scalar @{$res});
$self->assert_num_equals(2, $res->[0]);
$self->assert_num_equals(3, $res->[1]);
$self->assert_num_equals(4, $res->[2]);
$self->assert_num_equals(5, $res->[3]);
$self->assert_num_equals(7, $res->[4]);
$self->assert_num_equals(8, $res->[5]);
$self->assert_num_equals(9, $res->[6]);
$self->assert_num_equals(10, $res->[7]);

xlog $self, "ENABLE UIDONLY";
$res = $imaptalk->_imap_cmd('ENABLE', 0, 'enabled', 'UIDONLY');
$self->assert_num_equals(1, $res->{uidonly});

xlog $self, "attempt SEARCH";
$res = $imaptalk->search('not', 'deleted');
$self->assert_str_equals('bad', $imaptalk->get_last_completion_response());
# get_response_code() doesn't (yet) handle [UIDREQUIRED]
$self->assert_matches(qr/[UIDREQUIRED]/, $imaptalk->get_last_error());

xlog $self, "attempt UID SEARCH with msgnos";
$res = $imaptalk->_imap_cmd('UID SEARCH', 1, 'search', '1:10');
$self->assert_str_equals('bad', $imaptalk->get_last_completion_response());

xlog $self, "UID SEARCH";
$res = $imaptalk->_imap_cmd('UID SEARCH', 1, 'search', 'not', 'deleted');
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
$self->assert_num_equals(8, scalar @{$res});
$self->assert_num_equals(2, $res->[0]);
$self->assert_num_equals(3, $res->[1]);
$self->assert_num_equals(4, $res->[2]);
$self->assert_num_equals(5, $res->[3]);
$self->assert_num_equals(7, $res->[4]);
$self->assert_num_equals(8, $res->[5]);
$self->assert_num_equals(9, $res->[6]);
$self->assert_num_equals(10, $res->[7]);

xlog $self, "UID SEARCH with UIDs";
$res = $imaptalk->_imap_cmd('UID SEARCH', 1, 'search', 'uid', '1:10');
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
$self->assert_num_equals(10, scalar @{$res});
}

1;
Loading

0 comments on commit cc7ab0b

Please sign in to comment.