]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/Mbox.pm
2ea326a5a87fbf8788064e0be61819c8fe79b8da
[public-inbox.git] / lib / PublicInbox / Mbox.pm
1 # Copyright (C) 2015 all contributors <meta@public-inbox.org>
2 # License: AGPLv3 or later (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         eval { require IO::Compress::Gzip };
105         return sub { need_gzip(@_) } if $@;
106         my $query;
107         if ($range eq 'all') { # TODO: YYYY[-MM]
108                 $query = '';
109         } else {
110                 return [404, [qw(Content-Type text/plain)], []];
111         }
112
113         my $cb = sub { $ctx->{srch}->query($query, @_) };
114         PublicInbox::MboxGz->response($ctx, $cb);
115 }
116
117 sub need_gzip {
118         my $fh = $_[0]->([501, ['Content-Type' => 'text/html']]);
119         my $title = 'gzipped mbox not available';
120         $fh->write(<<EOF);
121 <html><head><title>$title</title><body><pre>$title
122 The administrator needs to install the IO::Compress::Gzip Perl module
123 to support gzipped mboxes.
124 <a href="../">Return to index</a></pre></body></html>
125 EOF
126         $fh->close;
127 }
128
129 1;
130
131 package PublicInbox::MboxGz;
132 use strict;
133 use warnings;
134
135 sub new {
136         my ($class, $ctx, $cb) = @_;
137         my $buf = '';
138         bless {
139                 buf => \$buf,
140                 gz => IO::Compress::Gzip->new(\$buf, Time => 0),
141                 cb => $cb,
142                 ctx => $ctx,
143                 msgs => [],
144                 opts => { offset => 0 },
145         }, $class;
146 }
147
148 sub response {
149         my ($class, $ctx, $cb) = @_;
150         my $body = $class->new($ctx, $cb);
151         # http://www.iana.org/assignments/media-types/application/gzip
152         $body->{hdr} = [ 'Content-Type', 'application/gzip' ];
153         my $hdr = $body->getline; # fill in Content-Disposition filename
154         [ 200, $hdr, $body ];
155 }
156
157 # called by Plack::Util::foreach or similar
158 sub getline {
159         my ($self) = @_;
160         my $ctx = $self->{ctx} or return;
161         my $res;
162         my $ibx = $ctx->{-inbox};
163         my $gz = $self->{gz};
164         do {
165                 # work on existing result set
166                 while (defined(my $smsg = shift @{$self->{msgs}})) {
167                         my $msg = eval { $ibx->msg_by_smsg($smsg) } or next;
168                         $msg = Email::Simple->new($msg);
169                         $gz->write(PublicInbox::Mbox::msg_str($ctx, $msg));
170
171                         # use subject of first message as subject
172                         if (my $hdr = delete $self->{hdr}) {
173                                 my $fn = PublicInbox::Mbox::subject_fn($msg);
174                                 push @$hdr, 'Content-Disposition',
175                                                 "inline; filename=$fn.mbox.gz";
176                                 return $hdr;
177                         }
178                         my $bref = $self->{buf};
179                         if (length($$bref) >= 8192) {
180                                 my $ret = $$bref; # copy :<
181                                 ${$self->{buf}} = '';
182                                 return $ret;
183                         }
184
185                         # be fair to other clients on public-inbox-httpd:
186                         return '';
187                 }
188
189                 # refill result set
190                 $res = $self->{cb}->($self->{opts});
191                 $self->{msgs} = $res->{msgs};
192                 $res = scalar @{$self->{msgs}};
193                 $self->{opts}->{offset} += $res;
194         } while ($res);
195         $gz->close;
196         delete $self->{ctx};
197         ${delete $self->{buf}};
198 }
199
200 sub close {} # noop
201
202 1;