]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/Mbox.pm
0c3e52fe42367382d922b01c0e2f77f6b0ee84d1
[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 (via getline) interface for formatting messages as an mboxrd.
5 # Used by the PSGI web interface.
6 #
7 # public-inbox-httpd favors "getline" response bodies to take a
8 # "pull"-based approach to feeding slow clients (as opposed to a
9 # more common "push" model)
10 package PublicInbox::Mbox;
11 use strict;
12 use warnings;
13 use PublicInbox::MID qw/mid_clean mid_escape/;
14 use PublicInbox::Hval qw/to_filename/;
15 use Email::Simple;
16 use Email::MIME::Encode;
17
18 sub subject_fn ($) {
19         my ($hdr) = @_;
20         my $fn = $hdr->header('Subject');
21         return 'no-subject' unless defined($fn);
22
23         # no need for full Email::MIME, here
24         if ($fn =~ /=\?/) {
25                 eval { $fn = Encode::decode('MIME-Header', $fn) };
26                 $fn = 'no-subject' if $@;
27         }
28         $fn =~ s/^re:\s+//i;
29         $fn = to_filename($fn);
30         $fn eq '' ? 'no-subject' : $fn;
31 }
32
33 sub mb_stream {
34         my ($more) = @_;
35         bless $more, 'PublicInbox::Mbox';
36 }
37
38 # called by PSGI server as body response
39 # this gets called twice for every message, once to return the header,
40 # once to retrieve the body
41 sub getline {
42         my ($more) = @_; # self
43         my ($ctx, $id, $prev, $next, $mref, $hdr) = @$more;
44         if ($hdr) { # first message hits this, only
45                 pop @$more; # $hdr
46                 return msg_hdr($ctx, $hdr);
47         }
48         if ($mref) { # all messages hit this
49                 pop @$more; # $mref
50                 return msg_body($$mref);
51         }
52         my $cur = $next or return;
53         my $ibx = $ctx->{-inbox};
54         $next = $ibx->over->next_by_mid($ctx->{mid}, \$id, \$prev);
55         $mref = $ibx->msg_by_smsg($cur) or return;
56         $hdr = Email::Simple->new($mref)->header_obj;
57         @$more = ($ctx, $id, $prev, $next, $mref); # $next may be undef, here
58         msg_hdr($ctx, $hdr); # all but first message hits this
59 }
60
61 sub close {} # noop
62
63 sub emit_raw {
64         my ($ctx) = @_;
65         my $mid = $ctx->{mid};
66         my $ibx = $ctx->{-inbox};
67         my ($mref, $more, $id, $prev, $next);
68         if (my $over = $ibx->over) {
69                 my $smsg = $over->next_by_mid($mid, \$id, \$prev) or return;
70                 $mref = $ibx->msg_by_smsg($smsg) or return;
71                 $next = $over->next_by_mid($mid, \$id, \$prev);
72         } else {
73                 $mref = $ibx->msg_by_mid($mid) or return;
74         }
75         my $hdr = Email::Simple->new($mref)->header_obj;
76         $more = [ $ctx, $id, $prev, $next, $mref, $hdr ]; # for ->getline
77         my $fn = subject_fn($hdr);
78         my @hdr = ('Content-Type');
79         if ($ibx->{obfuscate}) {
80                 # obfuscation is stupid, but maybe scrapers are, too...
81                 push @hdr, 'application/mbox';
82                 $fn .= '.mbox';
83         } else {
84                 push @hdr, 'text/plain';
85                 $fn .= '.txt';
86         }
87         push @hdr, 'Content-Disposition', "inline; filename=$fn";
88         [ 200, \@hdr, mb_stream($more) ];
89 }
90
91 sub msg_hdr ($$;$) {
92         my ($ctx, $header_obj, $mid) = @_;
93
94         # drop potentially confusing headers, ssoma already should've dropped
95         # Lines and Content-Length
96         foreach my $d (qw(Lines Bytes Content-Length Status)) {
97                 $header_obj->header_set($d);
98         }
99         my $ibx = $ctx->{-inbox};
100         my $base = $ibx->base_url($ctx->{env});
101         $mid = $ctx->{mid} unless defined $mid;
102         $mid = mid_escape($mid);
103         my @append = (
104                 'Archived-At', "<$base$mid/>",
105                 'List-Archive', "<$base>",
106                 'List-Post', "<mailto:$ibx->{-primary_address}>",
107         );
108         my $crlf = $header_obj->crlf;
109         my $buf = "From mboxrd\@z Thu Jan  1 00:00:00 1970\n" .
110                         $header_obj->as_string;
111         for (my $i = 0; $i < @append; $i += 2) {
112                 my $k = $append[$i];
113                 my $v = $append[$i + 1];
114                 my @v = $header_obj->header($k);
115                 foreach (@v) {
116                         if ($v eq $_) {
117                                 $v = undef;
118                                 last;
119                         }
120                 }
121                 $buf .= "$k: $v$crlf" if defined $v;
122         }
123         $buf .= $crlf;
124 }
125
126 sub msg_body ($) {
127         # mboxrd quoting style
128         # ref: http://www.qmail.org/man/man5/mbox.html
129         $_[0] =~ s/^(>*From )/>$1/gm;
130         $_[0] .= "\n";
131 }
132
133 sub thread_mbox {
134         my ($ctx, $over, $sfx) = @_;
135         eval { require IO::Compress::Gzip };
136         return sub { need_gzip(@_) } if $@;
137         my $mid = $ctx->{mid};
138         my $msgs = $over->get_thread($mid, {});
139         return [404, [qw(Content-Type text/plain)], []] if !@$msgs;
140         my $prev = $msgs->[-1];
141         my $i = 0;
142         my $cb = sub {
143                 while (1) {
144                         if (my $smsg = $msgs->[$i++]) {
145                                 return $smsg;
146                         }
147                         # refill result set
148                         $msgs = $over->get_thread($mid, $prev);
149                         return unless @$msgs;
150                         $prev = $msgs->[-1];
151                         $i = 0;
152                 }
153         };
154         PublicInbox::MboxGz->response($ctx, $cb, $msgs->[0]->subject);
155 }
156
157 sub emit_range {
158         my ($ctx, $range) = @_;
159
160         my $query;
161         if ($range eq 'all') { # TODO: YYYY[-MM]
162                 $query = '';
163         } else {
164                 return [404, [qw(Content-Type text/plain)], []];
165         }
166         mbox_all($ctx, $query);
167 }
168
169 sub mbox_all_ids {
170         my ($ctx) = @_;
171         my $prev = 0;
172         my $ibx = $ctx->{-inbox};
173         my $ids = $ibx->mm->ids_after(\$prev) or return
174                 [404, [qw(Content-Type text/plain)], ["No results found\n"]];
175         my $i = 0;
176         my $over = $ibx->over or
177                 return PublicInbox::WWW::need($ctx, 'Overview');
178         my $cb = sub {
179                 do {
180                         while ((my $num = $ids->[$i++])) {
181                                 my $smsg = $over->get_art($num) or next;
182                                 return $smsg;
183                         }
184                         $ids = $ibx->mm->ids_after(\$prev);
185                         $i = 0;
186                 } while (@$ids);
187                 undef;
188         };
189         return PublicInbox::MboxGz->response($ctx, $cb, 'all');
190 }
191
192 sub mbox_all {
193         my ($ctx, $query) = @_;
194
195         eval { require IO::Compress::Gzip };
196         return sub { need_gzip(@_) } if $@;
197         return mbox_all_ids($ctx) if $query eq '';
198         my $opts = { mset => 2 };
199         my $srch = $ctx->{-inbox}->search or
200                 return PublicInbox::WWW::need($ctx, 'Search');;
201         my $mset = $srch->query($query, $opts);
202         $opts->{offset} = $mset->size or
203                         return [404, [qw(Content-Type text/plain)],
204                                 ["No results found\n"]];
205         my $i = 0;
206         my $cb = sub { # called by MboxGz->getline
207                 while (1) {
208                         while (my $mi = (($mset->items)[$i++])) {
209                                 my $doc = $mi->get_document;
210                                 my $smsg = $srch->retry_reopen(sub {
211                                         PublicInbox::SearchMsg->load_doc($doc);
212                                 }) or next;
213                                 return $smsg;
214                         }
215                         # refill result set
216                         $mset = $srch->query($query, $opts);
217                         my $size = $mset->size or return;
218                         $opts->{offset} += $size;
219                         $i = 0;
220                 }
221         };
222         PublicInbox::MboxGz->response($ctx, $cb, 'results-'.$query);
223 }
224
225 sub need_gzip {
226         my $fh = $_[0]->([501, ['Content-Type' => 'text/html']]);
227         my $title = 'gzipped mbox not available';
228         $fh->write(<<EOF);
229 <html><head><title>$title</title><body><pre>$title
230 The administrator needs to install the IO::Compress::Gzip Perl module
231 to support gzipped mboxes.
232 <a href="../">Return to index</a></pre></body></html>
233 EOF
234         $fh->close;
235 }
236
237 1;
238
239 package PublicInbox::MboxGz;
240 use strict;
241 use warnings;
242 use PublicInbox::Hval qw/to_filename/;
243
244 sub new {
245         my ($class, $ctx, $cb) = @_;
246         my $buf = '';
247         bless {
248                 buf => \$buf,
249                 gz => IO::Compress::Gzip->new(\$buf, Time => 0),
250                 cb => $cb,
251                 ctx => $ctx,
252         }, $class;
253 }
254
255 sub response {
256         my ($class, $ctx, $cb, $fn) = @_;
257         my $body = $class->new($ctx, $cb);
258         # http://www.iana.org/assignments/media-types/application/gzip
259         my @h = qw(Content-Type application/gzip);
260         if ($fn) {
261                 $fn = to_filename($fn);
262                 push @h, 'Content-Disposition', "inline; filename=$fn.mbox.gz";
263         }
264         [ 200, \@h, $body ];
265 }
266
267 # called by Plack::Util::foreach or similar
268 sub getline {
269         my ($self) = @_;
270         my $ctx = $self->{ctx} or return;
271         my $gz = $self->{gz};
272         while (my $smsg = $self->{cb}->()) {
273                 my $mref = $ctx->{-inbox}->msg_by_smsg($smsg) or next;
274                 my $h = Email::Simple->new($mref)->header_obj;
275                 $gz->write(PublicInbox::Mbox::msg_hdr($ctx, $h, $smsg->{mid}));
276                 $gz->write(PublicInbox::Mbox::msg_body($$mref));
277
278                 my $bref = $self->{buf};
279                 if (length($$bref) >= 8192) {
280                         my $ret = $$bref; # copy :<
281                         ${$self->{buf}} = '';
282                         return $ret;
283                 }
284
285                 # be fair to other clients on public-inbox-httpd:
286                 return '';
287         }
288         delete($self->{gz})->close;
289         # signal that we're done and can return undef next call:
290         delete $self->{ctx};
291         ${delete $self->{buf}};
292 }
293
294 sub close {} # noop
295
296 1;