]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/Mbox.pm
mbox: do not barf on queries which return no results
[public-inbox.git] / lib / PublicInbox / Mbox.pm
1 # Copyright (C) 2015-2018 all contributors <meta@public-inbox.org>
2 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
3
4 # Streaming interface for formatting messages as an mboxrd.
5 # Used by the web interface
6 package PublicInbox::Mbox;
7 use strict;
8 use warnings;
9 use PublicInbox::MID qw/mid_clean mid_escape/;
10 use PublicInbox::Hval qw/to_filename/;
11 use Email::Simple;
12 use Email::MIME::Encode;
13
14 sub subject_fn ($) {
15         my ($simple) = @_;
16         my $fn = $simple->header('Subject');
17         return 'no-subject' unless defined($fn);
18
19         # no need for full Email::MIME, here
20         if ($fn =~ /=\?/) {
21                 eval { $fn = Encode::decode('MIME-Header', $fn) };
22                 $fn = 'no-subject' if $@;
23         }
24         $fn =~ s/^re:\s+//i;
25         $fn = to_filename($fn);
26         $fn eq '' ? 'no-subject' : $fn;
27 }
28
29 sub emit1 {
30         my ($ctx, $msg) = @_;
31         $msg = Email::Simple->new($msg);
32         my $fn = subject_fn($msg);
33         my @hdr = ('Content-Type');
34         if ($ctx->{-inbox}->{obfuscate}) {
35                 # obfuscation is stupid, but maybe scrapers are, too...
36                 push @hdr, 'application/mbox';
37                 $fn .= '.mbox';
38         } else {
39                 push @hdr, 'text/plain';
40                 $fn .= '.txt';
41         }
42         push @hdr, 'Content-Disposition', "inline; filename=$fn";
43
44         # single message should be easily renderable in browsers,
45         # unless obfuscation is enabled :<
46         [ 200, \@hdr, [ msg_str($ctx, $msg) ] ]
47 }
48
49 sub msg_str {
50         my ($ctx, $simple) = @_; # Email::Simple object
51         my $header_obj = $simple->header_obj;
52
53         # drop potentially confusing headers, ssoma already should've dropped
54         # Lines and Content-Length
55         foreach my $d (qw(Lines Bytes Content-Length Status)) {
56                 $header_obj->header_set($d);
57         }
58         my $ibx = $ctx->{-inbox};
59         my $base = $ibx->base_url($ctx->{env});
60         my $mid = mid_clean($header_obj->header('Message-ID'));
61         $mid = mid_escape($mid);
62         my @append = (
63                 'Archived-At', "<$base$mid/>",
64                 'List-Archive', "<$base>",
65                 'List-Post', "<mailto:$ibx->{-primary_address}>",
66         );
67         my $crlf = $simple->crlf;
68         my $buf = "From mboxrd\@z Thu Jan  1 00:00:00 1970\n" .
69                         $header_obj->as_string;
70         for (my $i = 0; $i < @append; $i += 2) {
71                 my $k = $append[$i];
72                 my $v = $append[$i + 1];
73                 my @v = $header_obj->header($k);
74                 foreach (@v) {
75                         if ($v eq $_) {
76                                 $v = undef;
77                                 last;
78                         }
79                 }
80                 $buf .= "$k: $v$crlf" if defined $v;
81         }
82         $buf .= $crlf;
83
84         # mboxrd quoting style
85         # ref: http://www.qmail.org/man/man5/mbox.html
86         my $body = $simple->body;
87         $body =~ s/^(>*From )/>$1/gm;
88         $buf .= $body;
89         $buf .= "\n";
90 }
91
92 sub thread_mbox {
93         my ($ctx, $srch, $sfx) = @_;
94         eval { require IO::Compress::Gzip };
95         return sub { need_gzip(@_) } if $@;
96
97         my $cb = sub { $srch->get_thread($ctx->{mid}, @_) };
98         PublicInbox::MboxGz->response($ctx, $cb);
99 }
100
101 sub emit_range {
102         my ($ctx, $range) = @_;
103
104         my $query;
105         if ($range eq 'all') { # TODO: YYYY[-MM]
106                 $query = '';
107         } else {
108                 return [404, [qw(Content-Type text/plain)], []];
109         }
110         mbox_all($ctx, $query);
111 }
112
113 sub mbox_all {
114         my ($ctx, $query) = @_;
115
116         eval { require IO::Compress::Gzip };
117         return sub { need_gzip(@_) } if $@;
118         my $cb = sub { $ctx->{srch}->query($query, @_) };
119         PublicInbox::MboxGz->response($ctx, $cb, 'results-'.$query);
120 }
121
122 sub need_gzip {
123         my $fh = $_[0]->([501, ['Content-Type' => 'text/html']]);
124         my $title = 'gzipped mbox not available';
125         $fh->write(<<EOF);
126 <html><head><title>$title</title><body><pre>$title
127 The administrator needs to install the IO::Compress::Gzip Perl module
128 to support gzipped mboxes.
129 <a href="../">Return to index</a></pre></body></html>
130 EOF
131         $fh->close;
132 }
133
134 1;
135
136 package PublicInbox::MboxGz;
137 use strict;
138 use warnings;
139 use PublicInbox::Hval qw/to_filename/;
140
141 sub new {
142         my ($class, $ctx, $cb) = @_;
143         my $buf = '';
144         bless {
145                 buf => \$buf,
146                 gz => IO::Compress::Gzip->new(\$buf, Time => 0),
147                 cb => $cb,
148                 ctx => $ctx,
149                 msgs => [],
150                 opts => { offset => 0 },
151         }, $class;
152 }
153
154 sub response {
155         my ($class, $ctx, $cb, $fn) = @_;
156         my $body = $class->new($ctx, $cb);
157         # http://www.iana.org/assignments/media-types/application/gzip
158         $body->{hdr} = [ 'Content-Type', 'application/gzip' ];
159         $body->{fn} = $fn;
160         # fill in Content-Disposition filename
161         my $hdr = $body->getline;
162         if ($body->{hdr}) {
163                 return [ 404, ['Content-Type','text/plain'],
164                         [ "No results found\n" ] ];
165         }
166         [ 200, $hdr, $body ];
167 }
168
169 sub set_filename ($$) {
170         my ($fn, $msg) = @_;
171         return to_filename($fn) if defined($fn);
172
173         PublicInbox::Mbox::subject_fn($msg);
174 }
175
176 # called by Plack::Util::foreach or similar
177 sub getline {
178         my ($self) = @_;
179         my $ctx = $self->{ctx} or return;
180         my $res;
181         my $ibx = $ctx->{-inbox};
182         my $gz = $self->{gz};
183         do {
184                 # work on existing result set
185                 while (defined(my $smsg = shift @{$self->{msgs}})) {
186                         my $msg = eval { $ibx->msg_by_smsg($smsg) } or next;
187                         $msg = Email::Simple->new($msg);
188                         $gz->write(PublicInbox::Mbox::msg_str($ctx, $msg));
189
190                         # use subject of first message as subject
191                         if (my $hdr = delete $self->{hdr}) {
192                                 my $fn = set_filename($self->{fn}, $msg);
193                                 push @$hdr, 'Content-Disposition',
194                                                 "inline; filename=$fn.mbox.gz";
195                                 return $hdr;
196                         }
197                         my $bref = $self->{buf};
198                         if (length($$bref) >= 8192) {
199                                 my $ret = $$bref; # copy :<
200                                 ${$self->{buf}} = '';
201                                 return $ret;
202                         }
203
204                         # be fair to other clients on public-inbox-httpd:
205                         return '';
206                 }
207
208                 # refill result set
209                 $res = $self->{cb}->($self->{opts});
210                 $self->{msgs} = $res->{msgs};
211                 $res = scalar @{$self->{msgs}};
212                 $self->{opts}->{offset} += $res;
213         } while ($res);
214         $gz->close;
215         delete $self->{ctx};
216         ${delete $self->{buf}};
217 }
218
219 sub close {} # noop
220
221 1;