From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.2 (2018-09-13) on dcvr.yhbt.net X-Spam-Level: X-Spam-Status: No, score=-4.0 required=3.0 tests=ALL_TRUSTED,BAYES_00 shortcircuit=no autolearn=ham autolearn_force=no version=3.4.2 Received: from localhost (dcvr.yhbt.net [127.0.0.1]) by dcvr.yhbt.net (Postfix) with ESMTP id 7915A1FBC7 for ; Wed, 10 Jun 2020 07:06:24 +0000 (UTC) From: Eric Wong To: meta@public-inbox.org Subject: [PATCH 28/82] imap: FETCH: support comma-delimited ranges Date: Wed, 10 Jun 2020 07:04:25 +0000 Message-Id: <20200610070519.18252-29-e@yhbt.net> In-Reply-To: <20200610070519.18252-1-e@yhbt.net> References: <20200610070519.18252-1-e@yhbt.net> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit List-Id: The RFC 3501 `sequence-set' definition allows comma-delimited ranges, so we'll support it in case clients send them. Coalescing overlapping ranges isn't required, so we won't support it as such an attempt to save bandwidth would waste memory on the server, instead. --- lib/PublicInbox/IMAP.pm | 92 +++++++++++++++++++++++++---------------- t/imapd.t | 2 + 2 files changed, 59 insertions(+), 35 deletions(-) diff --git a/lib/PublicInbox/IMAP.pm b/lib/PublicInbox/IMAP.pm index 51ab8b8c2f2..917833f7c47 100644 --- a/lib/PublicInbox/IMAP.pm +++ b/lib/PublicInbox/IMAP.pm @@ -61,6 +61,9 @@ for my $att (keys %FETCH_ATT) { $FETCH_ATT{$att} = \%h; } +my $valid_range = '[0-9]+|[0-9]+:[0-9]+|[0-9]+:\*'; +$valid_range = qr/\A(?:$valid_range)(?:,(?:$valid_range))*\z/; + sub greet ($) { my ($self) = @_; my $capa = capa($self); @@ -387,7 +390,7 @@ sub requeue_once ($) { sub uid_fetch_cb { # called by git->cat_async via git_async_msg my ($bref, $oid, $type, $size, $fetch_m_arg) = @_; - my ($self, undef, $ibx, undef, undef, $msgs, $want) = @$fetch_m_arg; + my ($self, undef, $ibx, $msgs, undef, $want) = @$fetch_m_arg; my $smsg = shift @$msgs or die 'BUG: no smsg'; if (!defined($oid)) { # it's possible to have TOCTOU if an admin runs @@ -441,15 +444,48 @@ sub uid_fetch_cb { # called by git->cat_async via git_async_msg requeue_once($self); } +sub range_step ($$) { + my ($ibx, $range_csv) = @_; + my ($beg, $end, $range); + if ($$range_csv =~ s/\A([^,]+),//) { + $range = $1; + } else { + $range = $$range_csv; + $$range_csv = undef; + } + if ($range =~ /\A([0-9]+):([0-9]+)\z/) { + ($beg, $end) = ($1, $2); + } elsif ($range =~ /\A([0-9]+):\*\z/) { + ($beg, $end) = ($1, $ibx->mm->max // 0); + } elsif ($range =~ /\A[0-9]+\z/) { + $beg = $end = $range; + } else { + return 'BAD fetch range'; + } + [ $beg, $end, $$range_csv ]; +} + +sub refill_range ($$$) { + my ($ibx, $msgs, $range_info) = @_; + my ($beg, $end, $range_csv) = @$range_info; + if (scalar(@$msgs = @{$ibx->over->query_xover($beg, $end)})) { + $range_info->[0] = $msgs->[-1]->{num} + 1; + return; + } + return 'OK Fetch done' if !$range_csv; + my $next_range = range_step($ibx, \$range_csv); + return $next_range if !ref($next_range); # error + @$range_info = @$next_range; + undef; # keep looping +} + sub uid_fetch_m { # long_response - my ($self, $tag, $ibx, $beg, $end, $msgs, $want) = @_; - if (!@$msgs) { # refill - @$msgs = @{$ibx->over->query_xover($$beg, $end)}; - if (!@$msgs) { - $self->write(\"$tag OK Fetch done\r\n"); + my ($self, $tag, $ibx, $msgs, $range_info, $want) = @_; + while (!@$msgs) { # rare + if (my $end = refill_range($ibx, $msgs, $range_info)) { + $self->write(\"$tag $end\r\n"); return; } - $$beg = $msgs->[-1]->{num} + 1; } git_async_msg($ibx, $msgs->[0], \&uid_fetch_cb, \@_); } @@ -635,7 +671,7 @@ sub partial_emit ($$$) { } sub fetch_common ($$$$) { - my ($self, $tag, $range, $want) = @_; + my ($self, $tag, $range_csv, $want) = @_; my $ibx = $self->{ibx} or return "$tag BAD No mailbox selected\r\n"; if ($want->[0] =~ s/\A\(//s) { $want->[-1] =~ s/\)\z//s or return "$tag BAD no rparen\r\n"; @@ -658,41 +694,27 @@ sub fetch_common ($$$$) { [ $_, @{$partial{$_}} ] } sort keys %partial ]; } - - my ($beg, $end); - my $msgs = []; - if ($range =~ /\A([0-9]+):([0-9]+)\z/s) { - ($beg, $end) = ($1, $2); - } elsif ($range =~ /\A([0-9]+):\*\z/s) { - ($beg, $end) = ($1, $ibx->mm->max // 0); - } elsif ($range =~ /\A[0-9]+\z/) { - my $smsg = $ibx->over->get_art($range) or - return "$tag OK Fetch done\r\n"; # really OK(!) - push @$msgs, $smsg; - ($beg, $end) = ($range, 0); - } else { - return "$tag BAD fetch range\r\n"; - } - [ $tag, $ibx, \$beg, $end, $msgs, \%want ]; + $range_csv = 'bad' if $range_csv !~ $valid_range; + my $range_info = range_step($ibx, \$range_csv); + return "$tag $range_info\r\n" if !ref($range_info); + [ $tag, $ibx, [], $range_info, \%want ]; } sub cmd_uid_fetch ($$$;@) { - my ($self, $tag, $range, @want) = @_; - my $args = fetch_common($self, $tag, $range, \@want); + my ($self, $tag, $range_csv, @want) = @_; + my $args = fetch_common($self, $tag, $range_csv, \@want); ref($args) eq 'ARRAY' ? long_response($self, \&uid_fetch_m, @$args) : $args; # error } sub seq_fetch_m { # long_response - my ($self, $tag, $ibx, $beg, $end, $msgs, $want) = @_; - if (!@$msgs) { # refill - @$msgs = @{$ibx->over->query_xover($$beg, $end)}; - if (!@$msgs) { - $self->write(\"$tag OK Fetch done\r\n"); + my ($self, $tag, $ibx, $msgs, $range_info, $want) = @_; + while (!@$msgs) { # rare + if (my $end = refill_range($ibx, $msgs, $range_info)) { + $self->write(\"$tag $end\r\n"); return; } - $$beg = $msgs->[-1]->{num} + 1; } my $seq = $want->{-seqno}++; my $cur_num = $msgs->[0]->{num}; @@ -711,11 +733,11 @@ sub seq_fetch_m { # long_response } sub cmd_fetch ($$$;@) { - my ($self, $tag, $range, @want) = @_; - my $args = fetch_common($self, $tag, $range, \@want); + my ($self, $tag, $range_csv, @want) = @_; + my $args = fetch_common($self, $tag, $range_csv, \@want); ref($args) eq 'ARRAY' ? do { my $want = $args->[-1]; - $want->{-seqno} = ${$args->[2]}; # $$beg + $want->{-seqno} = $args->[3]->[0]; # $beg == $range_info->[0]; long_response($self, \&seq_fetch_m, @$args) } : $args; # error } diff --git a/t/imapd.t b/t/imapd.t index 8172a91923d..7af14f1b150 100644 --- a/t/imapd.t +++ b/t/imapd.t @@ -309,6 +309,8 @@ EOF # message sequence numbers :< is($mic->Uid(0), 0, 'disable UID on '.ref($mic)); ok($mic->reconnect, 'reconnected'); +$ret = $mic->fetch_hash('1,2:3', 'RFC822') or BAIL_OUT "FETCH $@"; +is(scalar keys %$ret, 3, 'got all 3 messages with comma-separated sequence'); $ret = $mic->fetch_hash('1:*', 'RFC822') or BAIL_OUT "FETCH $@"; is(scalar keys %$ret, 3, 'got all 3 messages'); {