]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/Mbox.pm
mbox: do not clobber existing archive headers in WWW
[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/mid2path mid_clean/;
10 use URI::Escape qw/uri_escape_utf8/;
11 require Email::Simple;
12
13 sub thread_mbox {
14         my ($ctx, $srch, $sfx) = @_;
15         sub {
16                 my ($response) = @_; # Plack callback
17                 emit_mbox($response, $ctx, $srch, $sfx);
18         }
19 }
20
21 sub emit1 {
22         my $simple = Email::Simple->new(pop);
23         my $ctx = pop;
24         sub {
25                 my ($response) = @_;
26                 # single message should be easily renderable in browsers
27                 my $fh = $response->([200, ['Content-Type'=>'text/plain']]);
28                 emit_msg($ctx, $fh, $simple);
29                 $fh->close;
30         }
31 }
32
33 sub emit_msg {
34         my ($ctx, $fh, $simple) = @_; # Email::Simple object
35         my $header_obj = $simple->header_obj;
36
37         # drop potentially confusing headers, ssoma already should've dropped
38         # Lines and Content-Length
39         foreach my $d (qw(Lines Bytes Content-Length Status)) {
40                 $header_obj->header_set($d);
41         }
42         my $feed_opts = $ctx->{feed_opts};
43         unless ($feed_opts) {
44                 require PublicInbox::Feed; # FIXME: gross
45                 $feed_opts = PublicInbox::Feed::get_feedopts($ctx);
46                 $ctx->{feed_opts} = $feed_opts;
47         }
48         my $base = $feed_opts->{url};
49         my $mid = mid_clean($header_obj->header('Message-ID'));
50         $mid = uri_escape_utf8($mid);
51         my %append = (
52                 'Archived-At' => "<$base$mid/>",
53                 'List-Archive' => "<$base>",
54                 'List-Post' => "<mailto:$feed_opts->{id_addr}>",
55         );
56         while (my ($k, $v) = each %append) {
57                 my @v = $header_obj->header($k);
58                 foreach (@v) {
59                         if ($v eq $_) {
60                                 $v = undef;
61                                 last;
62                         }
63                 }
64                 $header_obj->header_set($k, @v, $v) if defined $v;
65         }
66
67         my $buf = $header_obj->as_string;
68         unless ($buf =~ /\AFrom /) {
69                 $fh->write("From mboxrd\@z Thu Jan  1 00:00:00 1970\n");
70         }
71         $fh->write($buf .= $simple->crlf);
72
73         $buf = $simple->body;
74         $simple->body_set('');
75
76         # mboxrd quoting style
77         # ref: http://www.qmail.org/man/man5/mbox.html
78         $buf =~ s/^(>*From )/>$1/gm;
79
80         $fh->write($buf .= "\n");
81 }
82
83 sub emit_mbox {
84         my ($response, $ctx, $srch, $sfx) = @_;
85         my $type = 'mbox';
86         if ($sfx) {
87                 eval { require IO::Compress::Gzip };
88                 return need_gzip($response) if $@;
89                 $type = 'gzip';
90         }
91
92         # http://www.iana.org/assignments/media-types/application/gzip
93         # http://www.iana.org/assignments/media-types/application/mbox
94         my $fh = $response->([200, ['Content-Type' => "application/$type"]]);
95         $fh = PublicInbox::MboxGz->new($fh) if $sfx;
96
97         require PublicInbox::Git;
98         my $mid = $ctx->{mid};
99         my $git = $ctx->{git} ||= PublicInbox::Git->new($ctx->{git_dir});
100         my %opts = (offset => 0);
101         my $nr;
102         do {
103                 my $res = $srch->get_thread($mid, \%opts);
104                 my $msgs = $res->{msgs};
105                 $nr = scalar @$msgs;
106                 while (defined(my $smsg = shift @$msgs)) {
107                         my $msg = eval {
108                                 my $p = 'HEAD:'.mid2path($smsg->mid);
109                                 Email::Simple->new($git->cat_file($p));
110                         };
111                         emit_msg($ctx, $fh, $msg) if $msg;
112                 }
113
114                 $opts{offset} += $nr;
115         } while ($nr > 0);
116
117         $fh->close;
118 }
119
120 sub need_gzip {
121         my $fh = $_[0]->([501, ['Content-Type' => 'text/html']]);
122         my $title = 'gzipped mbox not available';
123         $fh->write(<<EOF);
124 <html><head><title>$title</title><body><pre>$title
125 The administrator needs to install the IO::Compress::Gzip Perl module
126 to support gzipped mboxes.
127 <a href="../">Return to index</a></pre></body></html>
128 EOF
129         $fh->close;
130 }
131
132 1;
133
134 # fh may not be a proper IO, so we wrap the write and close methods
135 # to prevent IO::Compress::Gzip from complaining
136 package PublicInbox::MboxGz;
137 use strict;
138 use warnings;
139
140 sub new {
141         my ($class, $fh) = @_;
142         my $buf;
143         bless {
144                 buf => \$buf,
145                 gz => IO::Compress::Gzip->new(\$buf),
146                 fh => $fh,
147         }, $class;
148 }
149
150 sub _flush_buf {
151         my ($self) = @_;
152         if (defined ${$self->{buf}}) {
153                 $self->{fh}->write(${$self->{buf}});
154                 ${$self->{buf}} = undef;
155         }
156 }
157
158 sub write {
159         $_[0]->{gz}->write($_[1]);
160         _flush_buf($_[0]);
161 }
162
163 sub close {
164         my ($self) = @_;
165         $self->{gz}->close;
166         _flush_buf($self);
167         $self->{fh}->close;
168 }
169
170 1;