+sub fetch_blob_cb { # called by git->cat_async via git_async_cat
+ my ($bref, $oid, $type, $size, $fetch_arg) = @_;
+ my ($self, undef, $msgs, $range_info, $ops, $partial) = @$fetch_arg;
+ my $ibx = $self->{ibx} or return $self->close; # client disconnected
+ my $smsg = shift @$msgs or die 'BUG: no smsg';
+ if (!defined($oid)) {
+ # it's possible to have TOCTOU if an admin runs
+ # public-inbox-(edit|purge), just move onto the next message
+ warn "E: $smsg->{blob} missing in $ibx->{inboxdir}\n";
+ return requeue_once($self);
+ } else {
+ $smsg->{blob} eq $oid or die "BUG: $smsg->{blob} != $oid";
+ }
+ my $pre;
+ if (!$self->{wbuf} && (my $nxt = $msgs->[0])) {
+ $pre = git_async_prefetch($ibx->git, $nxt->{blob},
+ \&fetch_blob_cb, $fetch_arg);
+ }
+ fetch_run_ops($self, $smsg, $bref, $ops, $partial);
+ $pre ? $self->zflush : requeue_once($self);
+}
+
+sub emit_rfc822 {
+ my ($self, $k, undef, $bref) = @_;
+ $self->msg_more(" $k {" . length($$bref)."}\r\n");
+ $self->msg_more($$bref);
+}
+
+# Mail::IMAPClient::message_string cares about this by default,
+# (->Ignoresizeerrors attribute). Admins are encouraged to
+# --reindex for IMAP support, anyways.
+sub emit_rfc822_size {
+ my ($self, $k, $smsg) = @_;
+ $self->msg_more(' RFC822.SIZE ' . $smsg->{bytes});
+}
+
+sub emit_internaldate {
+ my ($self, undef, $smsg) = @_;
+ $self->msg_more(' INTERNALDATE "'.$smsg->internaldate.'"');
+}
+
+sub emit_flags { $_[0]->msg_more(' FLAGS ()') }
+
+sub emit_envelope {
+ my ($self, undef, undef, undef, $eml) = @_;
+ $self->msg_more(' ENVELOPE '.eml_envelope($eml));
+}
+
+sub emit_rfc822_header {
+ my ($self, $k, undef, undef, $eml) = @_;
+ $self->msg_more(" $k {".length(${$eml->{hdr}})."}\r\n");
+ $self->msg_more(${$eml->{hdr}});
+}
+
+# n.b. this is sorted to be after any emit_eml_new ops
+sub emit_rfc822_text {
+ my ($self, $k, undef, $bref) = @_;
+ $self->msg_more(" $k {".length($$bref)."}\r\n");
+ $self->msg_more($$bref);
+}
+
+sub emit_bodystructure {
+ my ($self, undef, undef, undef, $eml) = @_;
+ $self->msg_more(' BODYSTRUCTURE '.fetch_body($eml, 1));
+}
+
+sub emit_body {
+ my ($self, undef, undef, undef, $eml) = @_;
+ $self->msg_more(' BODY '.fetch_body($eml));
+}
+
+# set $eml once ($_[4] == $eml, $_[3] == $bref)
+sub op_eml_new { $_[4] = PublicInbox::Eml->new($_[3]) }
+
+# s/From / fixes old bug from import (pre-a0c07cba0e5d8b6a)
+sub to_crlf_full {
+ ${$_[0]} =~ s/(?<!\r)\n/\r\n/sg;
+ ${$_[0]} =~ s/\A[\r\n]*From [^\r\n]*\r\n//s;
+}
+
+sub op_crlf_bref { to_crlf_full($_[3]) }
+
+sub op_crlf_hdr { to_crlf_full($_[4]->{hdr}) }
+
+sub op_crlf_bdy { ${$_[4]->{bdy}} =~ s/(?<!\r)\n/\r\n/sg if $_[4]->{bdy} }
+
+sub uid_clamp ($$$) {
+ my ($self, $beg, $end) = @_;
+ my $uid_min = $self->{uid_base} + 1;
+ my $uid_end = $uid_min + UID_SLICE - 1;
+ $$beg = $uid_min if $$beg < $uid_min;
+ $$end = $uid_end if $$end > $uid_end;
+}
+
+sub range_step ($$) {
+ my ($self, $range_csv) = @_;
+ my ($beg, $end, $range);
+ if ($$range_csv =~ s/\A([^,]+),//) {
+ $range = $1;
+ } else {
+ $range = $$range_csv;
+ $$range_csv = undef;
+ }
+ my $uid_base = $self->{uid_base};
+ my $uid_end = $uid_base + UID_SLICE;
+ if ($range =~ /\A([0-9]+):([0-9]+)\z/) {
+ ($beg, $end) = ($1 + 0, $2 + 0);
+ uid_clamp($self, \$beg, \$end);
+ } elsif ($range =~ /\A([0-9]+):\*\z/) {
+ $beg = $1 + 0;
+ $end = $self->{ibx}->over->max;
+ $end = $uid_end if $end > $uid_end;
+ $beg = $end if $beg > $end;
+ uid_clamp($self, \$beg, \$end);
+ } elsif ($range =~ /\A[0-9]+\z/) {
+ $beg = $end = $range + 0;
+ # just let the caller do an out-of-range query if a single
+ # UID is out-of-range
+ ++$beg if ($beg <= $uid_base || $end > $uid_end);
+ } else {
+ return 'BAD fetch range';
+ }
+ [ $beg, $end, $$range_csv ];
+}
+
+sub refill_range ($$$) {
+ my ($self, $msgs, $range_info) = @_;
+ my ($beg, $end, $range_csv) = @$range_info;
+ if (scalar(@$msgs = @{$self->{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($self, \$range_csv);
+ return $next_range if !ref($next_range); # error
+ @$range_info = @$next_range;
+ undef; # keep looping
+}
+
+sub fetch_blob { # long_response
+ my ($self, $tag, $msgs, $range_info, $ops, $partial) = @_;
+ while (!@$msgs) { # rare
+ if (my $end = refill_range($self, $msgs, $range_info)) {
+ $self->write(\"$tag $end\r\n");