+sub msg_by_smsg ($$) {
+ my ($self, $smsg) = @_;
+
+ # ghosts may have undef smsg (from SearchThread.node) or
+ # no {blob} field
+ return unless defined $smsg;
+ defined(my $blob = $smsg->{blob}) or return;
+
+ git($self)->cat_file($blob);
+}
+
+sub smsg_eml {
+ my ($self, $smsg) = @_;
+ my $bref = msg_by_smsg($self, $smsg) or return;
+ my $eml = PublicInbox::Eml->new($bref);
+ $smsg->populate($eml) unless exists($smsg->{num}); # v1 w/o SQLite
+ $eml;
+}
+
+sub mid2num($$) {
+ my ($self, $mid) = @_;
+ my $mm = mm($self) or return;
+ $mm->num_for($mid);
+}
+
+sub smsg_by_mid ($$) {
+ my ($self, $mid) = @_;
+ my $over = over($self) or return;
+ # favor the Message-ID we used for the NNTP article number:
+ defined(my $num = mid2num($self, $mid)) or return;
+ my $smsg = $over->get_art($num) or return;
+ PublicInbox::Smsg::psgi_cull($smsg);
+}
+
+sub msg_by_mid ($$) {
+ my ($self, $mid) = @_;
+
+ over($self) or
+ return msg_by_path($self, mid2path($mid));
+
+ my $smsg = smsg_by_mid($self, $mid);
+ $smsg ? msg_by_smsg($self, $smsg) : undef;
+}
+
+sub recent {
+ my ($self, $opts, $after, $before) = @_;
+ over($self)->recent($opts, $after, $before);
+}
+
+sub modified {
+ my ($self) = @_;
+ if (my $over = over($self)) {
+ my $msgs = $over->recent({limit => 1});
+ if (my $smsg = $msgs->[0]) {
+ return $smsg->{ts};
+ }
+ return time;
+ }
+ git($self)->modified; # v1
+}
+
+# returns prefix => pathname mapping
+# (pathname is NOT public, but prefix is used for Xapian queries)
+sub altid_map ($) {
+ my ($self) = @_;
+ $self->{-altid_map} //= eval {
+ require PublicInbox::AltId;
+ my $altid = $self->{altid} or return {};
+ my %h = map {;
+ my $x = PublicInbox::AltId->new($self, $_);
+ "$x->{prefix}" => $x->{filename}
+ } @$altid;
+ \%h;
+ } // {};
+}
+
+# $obj must respond to ->on_inbox_unlock, which takes Inbox ($self) as an arg
+sub subscribe_unlock {
+ my ($self, $ident, $obj) = @_;
+ $self->{unlock_subs}->{$ident} = $obj;
+}
+
+sub unsubscribe_unlock {
+ my ($self, $ident) = @_;
+ delete $self->{unlock_subs}->{$ident};