]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/Smsg.pm
smsg: introduce ->populate method
[public-inbox.git] / lib / PublicInbox / Smsg.pm
1 # Copyright (C) 2015-2020 all contributors <meta@public-inbox.org>
2 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
3 #
4 # A small/skeleton/slim representation of a message.
5
6 # This used to be "SearchMsg", but we split out overview
7 # indexing into over.sqlite3 so it's not just "search".  There
8 # may be many of these objects loaded in memory at once for
9 # large threads in our WWW UI and the NNTP range responses.
10 package PublicInbox::Smsg;
11 use strict;
12 use warnings;
13 use base qw(Exporter);
14 our @EXPORT_OK = qw(subject_normalized);
15 use PublicInbox::MID qw/mid_mime/;
16 use PublicInbox::Address;
17 use PublicInbox::MsgTime qw(msg_timestamp msg_datestamp);
18 use Time::Local qw(timegm);
19
20 sub wrap {
21         my ($class, $mid) = @_;
22         bless { mid => $mid }, $class;
23 }
24
25 sub get_val ($$) {
26         my ($doc, $col) = @_;
27         # sortable_unserialise is defined by PublicInbox::Search::load_xapian()
28         sortable_unserialise($doc->get_value($col));
29 }
30
31 sub to_doc_data {
32         my ($self) = @_;
33         join("\n",
34                 $self->{subject},
35                 $self->{from},
36                 $self->references,
37                 $self->{to},
38                 $self->{cc},
39                 $self->{blob},
40                 $self->{mid},
41                 $self->{bytes} // '',
42                 $self->{lines} // ''
43         );
44 }
45
46 sub load_from_data ($$) {
47         my ($self) = $_[0]; # data = $_[1]
48         (
49                 $self->{subject},
50                 $self->{from},
51                 $self->{references},
52
53                 # To: and Cc: are stored to optimize HDR/XHDR in NNTP since
54                 # some NNTP clients will use that for message displays.
55                 # NNTP only, and only stored in Over(view), not Xapian
56                 $self->{to},
57                 $self->{cc},
58
59                 $self->{blob},
60                 $self->{mid},
61
62                 # NNTP only
63                 $self->{bytes},
64                 $self->{lines}
65         ) = split(/\n/, $_[1]);
66 }
67
68 sub load_expand {
69         my ($self, $doc) = @_;
70         my $data = $doc->get_data or return;
71         $self->{ts} = get_val($doc, PublicInbox::Search::TS());
72         my $dt = get_val($doc, PublicInbox::Search::DT());
73         my ($yyyy, $mon, $dd, $hh, $mm, $ss) = unpack('A4A2A2A2A2A2', $dt);
74         $self->{ds} = timegm($ss, $mm, $hh, $dd, $mon - 1, $yyyy);
75         utf8::decode($data);
76         load_from_data($self, $data);
77         $self;
78 }
79
80 sub psgi_cull ($) {
81         my ($self) = @_;
82         from_name($self); # fill in {from_name} so we can delete {from}
83
84         # drop NNTP-only fields which aren't relevant to PSGI results:
85         # saves ~80K on a 200 item search result:
86         delete @$self{qw(from ts to cc bytes lines)};
87         $self;
88 }
89
90 # Only called by PSGI interface, not NNTP
91 sub from_mitem {
92         my ($mitem, $srch) = @_;
93         return $srch->retry_reopen(\&from_mitem, $mitem) if $srch;
94         my $self = bless {}, __PACKAGE__;
95         psgi_cull(load_expand($self, $mitem->get_document));
96 }
97
98 # :bytes and :lines metadata in RFC 3977
99 sub bytes ($) { $_[0]->{bytes} }
100 sub lines ($) { $_[0]->{lines} }
101
102 sub __hdr ($$) {
103         my ($self, $field) = @_;
104         $self->{lc($field)} //= do {
105                 my $mime = $self->{mime} or return;
106                 my $val = join(', ', $mime->header($field));
107                 $val =~ tr/\r//d;
108                 $val =~ tr/\t\n/  /;
109                 $val;
110         };
111 }
112
113 # for Import and v1 WWW code paths
114 sub populate {
115         my ($self, $hdr, $v2w) = @_;
116         for my $f (qw(From To Cc Subject)) {
117                 my @all = $hdr->header($f);
118                 my $val = join(', ', @all);
119                 $val =~ tr/\r//d;
120                 # MIME decoding can create NULs, replace them with spaces
121                 # to protect git and NNTP clients
122                 $val =~ tr/\0\t\n/   /;
123
124                 # lower-case fields for read-only stuff
125                 $self->{lc($f)} = $val;
126
127                 # Capitalized From/Subject for git-fast-import
128                 next if $f eq 'To' || $f eq 'Cc';
129                 if (scalar(@all) > 1) {
130                         $val = $all[0];
131                         $val =~ tr/\r//d;
132                         $val =~ tr/\0\t\n/   /;
133                 }
134                 $self->{$f} = $val if $val ne '';
135         }
136         $v2w //= {};
137         $self->{-ds} = [ my @ds = msg_datestamp($hdr, $v2w->{autime}) ];
138         $self->{-ts} = [ my @ts = msg_timestamp($hdr, $v2w->{cotime}) ];
139         $self->{ds} //= $ds[0]; # no zone
140         $self->{ts} //= $ts[0];
141 }
142
143 sub subject ($) { __hdr($_[0], 'Subject') }
144 sub to ($) { __hdr($_[0], 'To') }
145 sub cc ($) { __hdr($_[0], 'Cc') }
146
147 # no strftime, that is locale-dependent and not for RFC822
148 my @DoW = qw(Sun Mon Tue Wed Thu Fri Sat);
149 my @MoY = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
150
151 sub date ($) {
152         my ($self) = @_;
153         my $ds = $self->{ds};
154         return unless defined $ds;
155         my ($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime($ds);
156         "$DoW[$wday], " . sprintf("%02d $MoY[$mon] %04d %02d:%02d:%02d +0000",
157                                 $mday, $year+1900, $hour, $min, $sec);
158
159 }
160
161 sub from ($) {
162         my ($self) = @_;
163         my $from = __hdr($self, 'From');
164         if (defined $from && !defined $self->{from_name}) {
165                 my @n = PublicInbox::Address::names($from);
166                 $self->{from_name} = join(', ', @n);
167         }
168         $from;
169 }
170
171 sub from_name {
172         my ($self) = @_;
173         my $from_name = $self->{from_name};
174         return $from_name if defined $from_name;
175         $self->from;
176         $self->{from_name};
177 }
178
179 sub ts {
180         my ($self) = @_;
181         $self->{ts} ||= eval { msg_timestamp($self->{mime}->header_obj) } || 0;
182 }
183
184 sub ds {
185         my ($self) = @_;
186         $self->{ds} ||= eval { msg_datestamp($self->{mime}->header_obj); } || 0;
187 }
188
189 sub references {
190         my ($self) = @_;
191         my $x = $self->{references};
192         defined $x ? $x : '';
193 }
194
195 sub mid ($;$) {
196         my ($self, $mid) = @_;
197
198         if (defined $mid) {
199                 $self->{mid} = $mid;
200         } elsif (defined(my $rv = $self->{mid})) {
201                 $rv;
202         } else {
203                 die "NO {mime} for mid\n" unless $self->{mime};
204                 mid_mime($self->{mime}) # v1 w/o Xapian
205         }
206 }
207
208 our $REPLY_RE = qr/^re:\s+/i;
209
210 sub subject_normalized ($) {
211         my ($subj) = @_;
212         $subj =~ s/\A\s+//s; # no leading space
213         $subj =~ s/\s+\z//s; # no trailing space
214         $subj =~ s/\s+/ /gs; # no redundant spaces
215         $subj =~ s/\.+\z//; # no trailing '.'
216         $subj =~ s/$REPLY_RE//igo; # remove reply prefix
217         $subj;
218 }
219
220 1;