]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/View.pm
view: use URI::Escape to escape URIs
[public-inbox.git] / lib / PublicInbox / View.pm
1 # Copyright (C) 2014, Eric Wong <normalperson@yhbt.net> and all contributors
2 # License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
3 package PublicInbox::View;
4 use strict;
5 use warnings;
6 use URI::Escape qw/uri_escape/;
7 use CGI qw/escapeHTML/;
8 use Encode qw/decode encode/;
9 use Encode::MIME::Header;
10
11 # only one public function:
12 sub as_html {
13         my ($class, $mime, $full_pfx) = @_;
14
15         headers_to_html_header($mime) .
16                 multipart_text_as_html($mime, $full_pfx) .
17                 "</pre>\n";
18 }
19
20 # only private functions below.
21
22 sub multipart_text_as_html {
23         my ($mime, $full_pfx) = @_;
24         my $rv = "";
25         my $part_nr = 0;
26
27         # scan through all parts, looking for displayable text
28         $mime->walk_parts(sub {
29                 my ($part) = @_;
30                 return if $part->subparts; # walk_parts already recurses
31
32                 my $part_type = $part->content_type;
33                 if ($part_type =~ m!\btext/[a-z0-9\+\._-]+\b!i) {
34                         my $fn = $part->filename;
35
36                         if ($part_nr > 0) {
37                                 defined($fn) or $fn = "part #" . ($part_nr + 1);
38                                 $rv .= add_filename_line($fn);
39                         }
40
41                         if (defined $full_pfx) {
42                                 $rv .= add_text_body_short($part, $part_nr,
43                                                         $full_pfx);
44                         } else {
45                                 $rv .= add_text_body_full($part, $part_nr);
46                         }
47                         $rv .= "\n" unless $rv =~ /\n\z/s;
48                 } else {
49                         $rv .= "-- part #" . ($part_nr + 1) . " ";
50                         $rv .= escapeHTML($part_type);
51                         $rv .= " skipped\n";
52                 }
53                 ++$part_nr;
54         });
55         $rv;
56 }
57
58 sub add_filename_line {
59         my ($fn) = @_;
60         my $len = 72;
61         my $pad = "-";
62
63         $len -= length($fn);
64         $pad x= ($len/2) if ($len > 0);
65         "$pad " . escapeHTML($fn) . " $pad\n";
66 }
67
68 sub add_text_body_short {
69         my ($part, $part_nr, $full_pfx) = @_;
70         my $n = 0;
71         my $s = escapeHTML($part->body);
72         $s =~ s!^((?:(?:&gt;[^\n]+)\n)+)!
73                 my $cur = $1;
74                 my @lines = split(/\n/, $cur);
75                 if (@lines > 1) {
76                         # show a short snippet of quoted text
77                         $cur = join(' ', @lines);
78                         $cur =~ s/&gt; ?//g;
79
80                         my @sum = split(/\s+/, $cur);
81                         $cur = '';
82                         do {
83                                 $cur .= shift(@sum) . ' ';
84                         } while (@sum && length($cur) < 68);
85                         $cur=~ s/ \z/ .../;
86                         "&gt; &lt;<a href=${full_pfx}#q${part_nr}_" . $n++ .
87                                 ">$cur<\/a>&gt;";
88                 } else {
89                         $cur;
90                 }
91         !emg;
92         $s;
93 }
94
95 sub add_text_body_full {
96         my ($part, $part_nr) = @_;
97         my $n = 0;
98         my $s = escapeHTML($part->body);
99         $s =~ s!^((?:(?:&gt;[^\n]+)\n)+)!
100                 my $cur = $1;
101                 my @lines = split(/\n/, $cur);
102                 if (@lines > 1) {
103                         "<a name=q${part_nr}_" . $n++ . ">$cur</a>";
104                 } else {
105                         $cur;
106                 }
107         !emg;
108         $s;
109 }
110
111 sub trim_message_id {
112         my ($mid) = @_;
113         $mid =~ tr/<>//d;
114         my $html = escapeHTML($mid);
115         my $href = escapeHTML(uri_escape($mid));
116
117         ($html, $href);
118 }
119
120 sub headers_to_html_header {
121         my ($simple) = @_;
122
123         my $rv = "";
124         my @title;
125         foreach my $h (qw(From To Cc Subject Date)) {
126                 my $v = $simple->header($h);
127                 defined $v or next;
128                 $v = decode("MIME-Header", $v);
129                 $v = encode("utf8", $v);
130                 $v = escapeHTML($v);
131                 $v =~ tr/\n/ /;
132                 $rv .= "$h: $v\n";
133
134                 if ($h eq "From" || $h eq "Subject") {
135                         push @title, $v;
136                 }
137         }
138
139         my $mid = $simple->header('Message-ID');
140         if (defined $mid) {
141                 my ($html, $href) = trim_message_id($mid);
142                 $rv .= "Message-ID: <a href=$href.html>$html</a> ";
143                 $rv .= "(<a href=$href.txt>raw message</a>)\n";
144         }
145
146         my $irp = $simple->header('In-Reply-To');
147         if (defined $irp) {
148                 my ($html, $href) = trim_message_id($irp);
149                 $rv .= "In-Reply-To: <a href=$href.html>$html</a>\n";
150         }
151         $rv .= "\n";
152
153         return ("<html><head><title>".
154                 join(' - ', @title) .
155                 '</title></head><body><pre style="white-space:pre-wrap">' .
156                 $rv);
157 }
158
159 1;