about summary refs log tree commit homepage
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/PublicInbox/Config.pm7
-rw-r--r--lib/PublicInbox/DummyInbox.pm21
-rw-r--r--lib/PublicInbox/IMAP.pm64
-rw-r--r--lib/PublicInbox/IMAPD.pm88
4 files changed, 88 insertions, 92 deletions
diff --git a/lib/PublicInbox/Config.pm b/lib/PublicInbox/Config.pm
index c18c9c75..19535beb 100644
--- a/lib/PublicInbox/Config.pm
+++ b/lib/PublicInbox/Config.pm
@@ -105,13 +105,16 @@ sub iterate_start {
         $self->{-iter} = [ \$i, $cb, $arg ];
 }
 
-# for PublicInbox::DS::next_tick
+# for PublicInbox::DS::next_tick, we only call this is if
+# PublicInbox::DS is already loaded
 sub event_step {
         my ($self) = @_;
         my ($i, $cb, $arg) = @{$self->{-iter}};
         my $section = $self->{-section_order}->[$$i++];
         delete($self->{-iter}) unless defined($section);
-        $cb->($self, $section, $arg);
+        eval { $cb->($self, $section, $arg) };
+        warn "E: $@ in ${self}::event_step" if $@;
+        PublicInbox::DS::requeue($self) if defined($section);
 }
 
 sub lookup_newsgroup {
diff --git a/lib/PublicInbox/DummyInbox.pm b/lib/PublicInbox/DummyInbox.pm
new file mode 100644
index 00000000..e38f9e5a
--- /dev/null
+++ b/lib/PublicInbox/DummyInbox.pm
@@ -0,0 +1,21 @@
+# Copyright (C) 2020 all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+#
+# An EXAMINE-able, PublicInbox::Inbox-like object for IMAP.  Some
+# IMAP clients don't like having unselectable parent mailboxes,
+# so we have a dummy
+package PublicInbox::DummyInbox;
+use strict;
+
+sub created_at { 0 } # Msgmap::created_at
+sub mm { shift }
+sub max { undef } # Msgmap::max
+sub msg_range { [] } # Msgmap::msg_range
+
+no warnings 'once';
+*query_xover = \&msg_range;
+*over = \&mm;
+*subscribe_unlock = *unsubscribe_unlock =
+        *get_art = *description = *base_url = \&max;
+
+1;
diff --git a/lib/PublicInbox/IMAP.pm b/lib/PublicInbox/IMAP.pm
index 959dcfbe..6f64dff9 100644
--- a/lib/PublicInbox/IMAP.pm
+++ b/lib/PublicInbox/IMAP.pm
@@ -192,26 +192,23 @@ sub cmd_done ($$) {
         "$idle_tag OK Idle done\r\n";
 }
 
-sub ensure_old_ranges_exist ($$$) {
-        my ($self, $ibx, $uid_min) = @_;
-        my $groups = $self->{imapd}->{groups};
-        my $mailbox = $ibx->{newsgroup};
+sub ensure_ranges_exist ($$$) {
+        my ($imapd, $ibx, $max) = @_;
+        my $mailboxes = $imapd->{mailboxes};
+        my $mb_top = $ibx->{newsgroup};
         my @created;
-        $uid_min -= UID_BLOCK;
+        my $uid_min = UID_BLOCK * int($max/UID_BLOCK) + 1;
         my $uid_end = $uid_min + UID_BLOCK - 1;
         while ($uid_min > 0) {
-                my $sub_mailbox = "$mailbox.$uid_min-$uid_end";
-                last if exists $groups->{$sub_mailbox};
-                $groups->{$sub_mailbox} = $ibx;
+                my $sub_mailbox = "$mb_top.$uid_min-$uid_end";
+                last if exists $mailboxes->{$sub_mailbox};
+                $mailboxes->{$sub_mailbox} = $ibx;
                 $uid_end -= UID_BLOCK;
                 $uid_min -= UID_BLOCK;
                 push @created, $sub_mailbox;
         }
         return unless @created;
-        my $l = $self->{imapd}->{inboxlist};
-        grep {
-                / \Q$mailbox\E\r\n\z/ and s/\(\\HasNoChildren/\(\\HasChildren/;
-        } @$l;
+        my $l = $imapd->{inboxlist} or return;
         push @$l, map { qq[* LIST (\\HasNoChildren) "." $_\r\n] } @created;
 }
 
@@ -222,41 +219,23 @@ sub cmd_examine ($$$) {
         if ($mailbox =~ /\A(.+)\.([0-9]+)-([0-9]+)\z/) {
                 # old mail: inbox.comp.foo.$uid_min-$uid_end
                 my ($mb_top, $uid_min, $uid_end) = ($1, $2 + 0, $3 + 0);
-                $ibx = $self->{imapd}->{groups}->{lc $mb_top};
-                if (!$ibx || ($uid_end % UID_BLOCK) != 0 ||
-                                ($uid_min + UID_BLOCK - 1) != $uid_end) {
-                        return "$tag NO Mailbox doesn't exist: $mailbox\r\n";
-                }
-                $mm = $ibx->mm;
-                $max = $mm->max // 0;
 
-                # don't let users create inboxes w/ not-yet-possible range:
-                $uid_min > $max and
+                $ibx = $self->{imapd}->{mailboxes}->{lc $mailbox} or
                         return "$tag NO Mailbox doesn't exist: $mailbox\r\n";
 
-                $max = $uid_min + UID_BLOCK + 1;
+                $mm = $ibx->mm;
+                $max = $mm->max // 0;
                 $self->{uid_min} = $uid_min;
-                ensure_old_ranges_exist($self, $ibx, $uid_min);
-        } else { # current mailbox (most recent UID_BLOCK messages)
-                $ibx = $self->{imapd}->{groups}->{lc $mailbox} or
+                ensure_ranges_exist($self->{imapd}, $ibx, $max);
+                $max = $uid_end if $max > $uid_end;
+        } else { # check for dummy inboxes
+                $ibx = $self->{imapd}->{mailboxes}->{lc $mailbox} or
                         return "$tag NO Mailbox doesn't exist: $mailbox\r\n";
-
+                delete $self->{uid_min};
+                $max = 0;
                 $mm = $ibx->mm;
-                $max = $mm->max // 0;
-
-                my $uid_min = UID_BLOCK * int($max/UID_BLOCK) + 1;
-                if ($uid_min == 1) { # normal inbox with <UID_BLOCK messages
-                        delete $self->{uid_min}; # implicit cmd_close
-                } else { # we have a giant inbox:
-                        $self->{uid_min} = $uid_min;
-                        ensure_old_ranges_exist($self, $ibx, $uid_min);
-                }
         }
 
-        # RFC 3501 2.3.1.1 -  "A good UIDVALIDITY value to use in
-        # this case is a 32-bit representation of the creation
-        # date/time of the mailbox"
-        my $uidvalidity = $mm->created_at or return "$tag BAD UIDVALIDITY\r\n";
         my $uidnext = $max + 1;
 
         # XXX: do we need this? RFC 5162/7162
@@ -269,7 +248,7 @@ sub cmd_examine ($$$) {
 * OK [PERMANENTFLAGS ()] Read-only mailbox\r
 * OK [UNSEEN $max]\r
 * OK [UIDNEXT $uidnext]\r
-* OK [UIDVALIDITY $uidvalidity]\r
+* OK [UIDVALIDITY $ibx->{uidvalidity}]\r
 $tag OK [READ-ONLY] EXAMINE/SELECT done\r
 EOF
 }
@@ -561,7 +540,7 @@ sub uid_fetch_m { # long_response
 
 sub cmd_status ($$$;@) {
         my ($self, $tag, $mailbox, @items) = @_;
-        my $ibx = $self->{imapd}->{groups}->{$mailbox} or
+        my $ibx = $self->{imapd}->{mailboxes}->{$mailbox} or
                 return "$tag NO Mailbox doesn't exist: $mailbox\r\n";
         return "$tag BAD no items\r\n" if !scalar(@items);
         ($items[0] !~ s/\A\(//s || $items[-1] !~ s/\)\z//s) and
@@ -577,8 +556,7 @@ sub cmd_status ($$$;@) {
                 } elsif ($it eq 'UIDNEXT') {
                         push(@it, ($max //= $mm->max // 0) + 1);
                 } elsif ($it eq 'UIDVALIDITY') {
-                        push(@it, $mm->created_at //
-                                return("$tag BAD UIDVALIDITY\r\n"));
+                        push(@it, $ibx->{uidvalidity});
                 } else {
                         return "$tag BAD invalid item\r\n";
                 }
diff --git a/lib/PublicInbox/IMAPD.pm b/lib/PublicInbox/IMAPD.pm
index a10fabff..b647c940 100644
--- a/lib/PublicInbox/IMAPD.pm
+++ b/lib/PublicInbox/IMAPD.pm
@@ -5,82 +5,75 @@
 # see script/public-inbox-imapd for how it is used
 package PublicInbox::IMAPD;
 use strict;
-use parent qw(PublicInbox::NNTPD);
+use PublicInbox::Config;
 use PublicInbox::InboxIdle;
 use PublicInbox::IMAP;
-# *UID_BLOCK = \&PublicInbox::IMAP::UID_BLOCK;
+use PublicInbox::DummyInbox;
+my $dummy = bless { uidvalidity => 0 }, 'PublicInbox::DummyInbox';
 
 sub new {
         my ($class) = @_;
         bless {
-                groups => {},
+                mailboxes => {},
                 err => \*STDERR,
                 out => \*STDOUT,
-                grouplist => [],
                 # accept_tls => { SSL_server => 1, ..., SSL_reuse_ctx => ... }
                 # pi_config => PublicInbox::Config
                 # idler => PublicInbox::InboxIdle
         }, $class;
 }
 
-sub refresh_inboxlist ($) {
-        my ($self) = @_;
-        my @names = map { $_->{newsgroup} } @{delete $self->{grouplist}};
-        my %ns; # "\Noselect \HasChildren"
-
-        if (my @uc = grep(/[A-Z]/, @names)) {
-                warn "Uppercase not allowed for IMAP newsgroup(s):\n",
-                        map { "\t$_\n" } @uc;
-                my %uc = map { $_ => 1 } @uc;
-                @names = grep { !$uc{$_} } @names;
-        }
-        for (@names) {
-                my $up = $_;
-                while ($up =~ s/\.[^\.]+\z//) {
-                        $ns{$up} = '\\Noselect \\HasChildren';
-                }
-        }
-        @names = map {;
-                my $at = delete($ns{$_}) ? '\\HasChildren' : '\\HasNoChildren';
-                qq[* LIST ($at) "." $_\r\n]
-        } @names;
-        push(@names, map { qq[* LIST ($ns{$_}) "." $_\r\n] } keys %ns);
-        @names = sort {
-                my ($xa) = ($a =~ / (\S+)\r\n/g);
-                my ($xb) = ($b =~ / (\S+)\r\n/g);
-                length($xa) <=> length($xb);
-        } @names;
-        $self->{inboxlist} = \@names;
-}
-
 sub imapd_refresh_ibx { # pi_config->each_inbox cb
         my ($ibx, $imapd) = @_;
         my $ngname = $ibx->{newsgroup} or return;
         if (ref $ngname) {
                 warn 'multiple newsgroups not supported: '.
                         join(', ', @$ngname). "\n";
+                return;
         } elsif ($ngname =~ m![^a-z0-9/_\.\-\~\@\+\=:]! ||
                  $ngname =~ /\.[0-9]+-[0-9]+\z/) {
-                warn "mailbox name invalid: `$ngname'\n";
+                warn "mailbox name invalid: newsgroup=`$ngname'\n";
+                return;
         }
-
+        $ibx->over or return;
+        $ibx->{over} = undef;
         my $mm = $ibx->mm or return;
         $ibx->{mm} = undef;
+
+        # RFC 3501 2.3.1.1 -  "A good UIDVALIDITY value to use in
+        # this case is a 32-bit representation of the creation
+        # date/time of the mailbox"
         defined($ibx->{uidvalidity} = $mm->created_at) or return;
-        $imapd->{tmp_groups}->{$ngname} = $ibx;
+        PublicInbox::IMAP::ensure_ranges_exist($imapd, $ibx, $mm->max // 1);
 
         # preload to avoid fragmentation:
         $ibx->description;
         $ibx->base_url;
-        # my $max = $mm->max // 0;
-        # my $uid_min = UID_BLOCK * int($max/UID_BLOCK) + 1;
+
+        # ensure dummies are selectable
+        my $dummies = $imapd->{dummies};
+        do {
+                $dummies->{$ngname} = $dummy;
+        } while ($ngname =~ s/\.[^\.]+\z//);
 }
 
 sub imapd_refresh_finalize {
         my ($imapd, $pi_config) = @_;
-        $imapd->{groups} = delete $imapd->{tmp_groups};
-        $imapd->{grouplist} = [ values %{$imapd->{groups}} ];
-        refresh_inboxlist($imapd);
+        my $mailboxes;
+        if (my $next = delete $imapd->{imapd_next}) {
+                $imapd->{mailboxes} = delete $next->{mailboxes};
+                $mailboxes = delete $next->{dummies};
+        } else {
+                $mailboxes = delete $imapd->{dummies};
+        }
+        %$mailboxes = (%$mailboxes, %{$imapd->{mailboxes}});
+        $imapd->{mailboxes} = $mailboxes;
+        $imapd->{inboxlist} = [
+                map {
+                        my $no = $mailboxes->{$_} == $dummy ? '' : 'No';
+                        qq[* LIST (\\Has${no}Children) "." $_\r\n]
+                } sort { length($a) <=> length($b) } keys %$mailboxes
+        ];
         $imapd->{pi_config} = $pi_config;
         if (my $idler = $imapd->{idler}) {
                 $idler->refresh($pi_config);
@@ -92,8 +85,8 @@ sub imapd_refresh_step { # pi_config->iterate_start cb
         if (defined($section)) {
                 return if $section !~ m!\Apublicinbox\.([^/]+)\z!;
                 my $ibx = $pi_config->lookup_name($1) or return;
-                imapd_refresh_ibx($ibx, $imapd);
-        } else { # "EOF"
+                imapd_refresh_ibx($ibx, $imapd->{imapd_next});
+        } else { # undef == "EOF"
                 imapd_refresh_finalize($imapd, $pi_config);
         }
 }
@@ -101,11 +94,12 @@ sub imapd_refresh_step { # pi_config->iterate_start cb
 sub refresh_groups {
         my ($self, $sig) = @_;
         my $pi_config = PublicInbox::Config->new;
-        $self->{tmp_groups} = {};
-        if (0 && $sig) { # SIGHUP
+        if ($sig) { # SIGHUP is handled through the event loop
+                $self->{imapd_next} = { dummies => {}, mailboxes => {} };
                 $pi_config->iterate_start(\&imapd_refresh_step, $self);
                 PublicInbox::DS::requeue($pi_config); # call event_step
-        } else { # initial start
+        } else { # initial start is synchronous
+                $self->{dummies} = {};
                 $pi_config->each_inbox(\&imapd_refresh_ibx, $self);
                 imapd_refresh_finalize($self, $pi_config);
         }