]> Sergey Matveev's repositories - public-inbox.git/blobdiff - lib/PublicInbox/Mbox.pm
mbox: support inline filename via Content-Disposition header
[public-inbox.git] / lib / PublicInbox / Mbox.pm
index 0258d8c72a5fb2b33e1cb85b3352432b9b2b1aed..2ea326a5a87fbf8788064e0be61819c8fe79b8da 100644 (file)
@@ -6,16 +6,44 @@
 package PublicInbox::Mbox;
 use strict;
 use warnings;
-use PublicInbox::MID qw/mid_clean/;
-use URI::Escape qw/uri_escape_utf8/;
-use Plack::Util;
-require Email::Simple;
+use PublicInbox::MID qw/mid_clean mid_escape/;
+use PublicInbox::Hval qw/to_filename/;
+use Email::Simple;
+use Email::MIME::Encode;
+
+sub subject_fn ($) {
+       my ($simple) = @_;
+       my $fn = $simple->header('Subject');
+       return 'no-subject' unless defined($fn);
+
+       # no need for full Email::MIME, here
+       if ($fn =~ /=\?/) {
+               eval { $fn = Encode::decode('MIME-Header', $fn) };
+               $fn = 'no-subject' if $@;
+       }
+       $fn =~ s/^re:\s+//i;
+       $fn = to_filename($fn);
+       $fn eq '' ? 'no-subject' : $fn;
+}
 
 sub emit1 {
        my ($ctx, $msg) = @_;
        $msg = Email::Simple->new($msg);
-       # single message should be easily renderable in browsers
-       [200, ['Content-Type', 'text/plain'], [ msg_str($ctx, $msg)] ]
+       my $fn = subject_fn($msg);
+       my @hdr = ('Content-Type');
+       if ($ctx->{-inbox}->{obfuscate}) {
+               # obfuscation is stupid, but maybe scrapers are, too...
+               push @hdr, 'application/mbox';
+               $fn .= '.mbox';
+       } else {
+               push @hdr, 'text/plain';
+               $fn .= '.txt';
+       }
+       push @hdr, 'Content-Disposition', "inline; filename=$fn";
+
+       # single message should be easily renderable in browsers,
+       # unless obfuscation is enabled :<
+       [ 200, \@hdr, [ msg_str($ctx, $msg) ] ]
 }
 
 sub msg_str {
@@ -28,9 +56,9 @@ sub msg_str {
                $header_obj->header_set($d);
        }
        my $ibx = $ctx->{-inbox};
-       my $base = $ibx->base_url($ctx->{cgi});
+       my $base = $ibx->base_url($ctx->{env});
        my $mid = mid_clean($header_obj->header('Message-ID'));
-       $mid = uri_escape_utf8($mid);
+       $mid = mid_escape($mid);
        my @append = (
                'Archived-At', "<$base$mid/>",
                'List-Archive', "<$base>",
@@ -67,9 +95,7 @@ sub thread_mbox {
        return sub { need_gzip(@_) } if $@;
 
        my $cb = sub { $srch->get_thread($ctx->{mid}, @_) };
-       # http://www.iana.org/assignments/media-types/application/gzip
-       [200, ['Content-Type' => 'application/gzip'],
-               PublicInbox::MboxGz->new($ctx, $cb) ];
+       PublicInbox::MboxGz->response($ctx, $cb);
 }
 
 sub emit_range {
@@ -83,11 +109,9 @@ sub emit_range {
        } else {
                return [404, [qw(Content-Type text/plain)], []];
        }
-       my $cb = sub { $ctx->{srch}->query($query, @_) };
 
-       # http://www.iana.org/assignments/media-types/application/gzip
-       [200, [qw(Content-Type application/gzip)],
-               PublicInbox::MboxGz->new($ctx, $cb) ];
+       my $cb = sub { $ctx->{srch}->query($query, @_) };
+       PublicInbox::MboxGz->response($ctx, $cb);
 }
 
 sub need_gzip {
@@ -107,54 +131,70 @@ EOF
 package PublicInbox::MboxGz;
 use strict;
 use warnings;
-use PublicInbox::MID qw(mid2path);
 
 sub new {
        my ($class, $ctx, $cb) = @_;
-       my $buf;
+       my $buf = '';
        bless {
                buf => \$buf,
                gz => IO::Compress::Gzip->new(\$buf, Time => 0),
                cb => $cb,
                ctx => $ctx,
                msgs => [],
-               opts => { asc => 1, offset => 0 },
+               opts => { offset => 0 },
        }, $class;
 }
 
-sub _flush_buf {
-       my ($self) = @_;
-       my $ret = $self->{buf};
-       $ret = $$ret;
-       ${$self->{buf}} = undef;
-       $ret;
+sub response {
+       my ($class, $ctx, $cb) = @_;
+       my $body = $class->new($ctx, $cb);
+       # http://www.iana.org/assignments/media-types/application/gzip
+       $body->{hdr} = [ 'Content-Type', 'application/gzip' ];
+       my $hdr = $body->getline; # fill in Content-Disposition filename
+       [ 200, $hdr, $body ];
 }
 
 # called by Plack::Util::foreach or similar
 sub getline {
        my ($self) = @_;
+       my $ctx = $self->{ctx} or return;
        my $res;
-       my $ctx = $self->{ctx};
-       my $git = $ctx->{git};
+       my $ibx = $ctx->{-inbox};
        my $gz = $self->{gz};
        do {
+               # work on existing result set
                while (defined(my $smsg = shift @{$self->{msgs}})) {
-                       my $msg = eval {
-                               my $p = 'HEAD:'.mid2path($smsg->mid);
-                               Email::Simple->new($git->cat_file($p));
-                       };
-                       $msg or next;
+                       my $msg = eval { $ibx->msg_by_smsg($smsg) } or next;
+                       $msg = Email::Simple->new($msg);
                        $gz->write(PublicInbox::Mbox::msg_str($ctx, $msg));
-                       my $ret = _flush_buf($self);
-                       return $ret if $ret;
+
+                       # use subject of first message as subject
+                       if (my $hdr = delete $self->{hdr}) {
+                               my $fn = PublicInbox::Mbox::subject_fn($msg);
+                               push @$hdr, 'Content-Disposition',
+                                               "inline; filename=$fn.mbox.gz";
+                               return $hdr;
+                       }
+                       my $bref = $self->{buf};
+                       if (length($$bref) >= 8192) {
+                               my $ret = $$bref; # copy :<
+                               ${$self->{buf}} = '';
+                               return $ret;
+                       }
+
+                       # be fair to other clients on public-inbox-httpd:
+                       return '';
                }
+
+               # refill result set
                $res = $self->{cb}->($self->{opts});
                $self->{msgs} = $res->{msgs};
                $res = scalar @{$self->{msgs}};
                $self->{opts}->{offset} += $res;
        } while ($res);
        $gz->close;
-       _flush_buf($self);
+       delete $self->{ctx};
+       ${delete $self->{buf}};
 }
 
 sub close {} # noop