]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/SearchMsg.pm
searchmsg: remove locale-dependency for ->date
[public-inbox.git] / lib / PublicInbox / SearchMsg.pm
1 # Copyright (C) 2015-2016 all contributors <meta@public-inbox.org>
2 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
3 # based on notmuch, but with no concept of folders, files or flags
4 #
5 # Wraps a document inside our Xapian search index.
6 package PublicInbox::SearchMsg;
7 use strict;
8 use warnings;
9 use Search::Xapian;
10 use Date::Parse qw/str2time/;
11 use PublicInbox::MID qw/mid_clean/;
12 use PublicInbox::Address;
13 our $PFX2TERM_RE = undef;
14
15 sub new {
16         my ($class, $mime) = @_;
17         my $doc = Search::Xapian::Document->new;
18         $doc->add_term(PublicInbox::Search::xpfx('type') . 'mail');
19
20         bless { type => 'mail', doc => $doc, mime => $mime }, $class;
21 }
22
23 sub wrap {
24         my ($class, $doc, $mid) = @_;
25         bless { doc => $doc, mime => undef, mid => $mid }, $class;
26 }
27
28 sub get_val ($$) {
29         my ($doc, $col) = @_;
30         Search::Xapian::sortable_unserialise($doc->get_value($col));
31 }
32
33 sub load_doc {
34         my ($class, $doc) = @_;
35         my $data = $doc->get_data or return;
36         my $ts = get_val($doc, &PublicInbox::Search::TS);
37         utf8::decode($data);
38         my ($subj, $from, $refs, $to, $cc, $blob) = split(/\n/, $data);
39         bless {
40                 doc => $doc,
41                 subject => $subj,
42                 ts => $ts,
43                 from => $from,
44                 references => $refs,
45                 to => $to,
46                 cc => $cc,
47                 blob => $blob,
48         }, $class;
49 }
50
51 # :bytes and :lines metadata in RFC 3977
52 sub bytes ($) { get_val($_[0]->{doc}, &PublicInbox::Search::BYTES) }
53 sub lines ($) { get_val($_[0]->{doc}, &PublicInbox::Search::LINES) }
54 sub num ($) { get_val($_[0]->{doc}, &PublicInbox::Search::NUM) }
55
56 sub __hdr ($$) {
57         my ($self, $field) = @_;
58         my $val = $self->{$field};
59         return $val if defined $val;
60
61         my $mime = $self->{mime} or return;
62         $val = $mime->header($field);
63         $val = '' unless defined $val;
64         $val =~ tr/\n/ /;
65         $val =~ tr/\r//d;
66         $self->{$field} = $val;
67 }
68
69 sub subject ($) { __hdr($_[0], 'subject') }
70 sub to ($) { __hdr($_[0], 'to') }
71 sub cc ($) { __hdr($_[0], 'cc') }
72
73 # no strftime, that is locale-dependent
74 my @DoW = qw(Sun Mon Tue Wed Thu Fri Sat);
75 my @MoY = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
76
77 sub date ($) {
78         my ($self) = @_;
79         my $date = __hdr($self, 'date');
80         return $date if defined $date;
81         my $ts = $self->{ts};
82         return unless defined $ts;
83         my ($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime($ts);
84         $self->{date} = "$DoW[$wday], ".
85                         sprintf("%02d $MoY[$mon] %04d %02d:%02d:%02d +0000",
86                                 $mday, $year+1900, $hour, $min, $sec);
87
88 }
89
90 sub from ($) {
91         my ($self) = @_;
92         my $from = __hdr($self, 'from');
93         if (defined $from && !defined $self->{from_name}) {
94                 my @n = PublicInbox::Address::names($from);
95                 $self->{from_name} = join(', ', @n);
96         }
97         $from;
98 }
99
100 sub from_name {
101         my ($self) = @_;
102         my $from_name = $self->{from_name};
103         return $from_name if defined $from_name;
104         $self->from;
105         $self->{from_name};
106 }
107
108 sub ts {
109         my ($self) = @_;
110         $self->{ts} ||= eval { str2time($self->mime->header('Date')) } || 0;
111 }
112
113 sub to_doc_data {
114         my ($self, $blob) = @_;
115         my @rows = ($self->subject, $self->from, $self->references,
116                         $self->to, $self->cc);
117         push @rows, $blob if defined $blob;
118         join("\n", @rows);
119 }
120
121 sub references {
122         my ($self) = @_;
123         my $x = $self->{references};
124         defined $x ? $x : '';
125 }
126
127 sub ensure_metadata {
128         my ($self) = @_;
129         my $doc = $self->{doc};
130         my $end = $doc->termlist_end;
131
132         unless (defined $PFX2TERM_RE) {
133                 my $or = join('|', keys %PublicInbox::Search::PFX2TERM_RMAP);
134                 $PFX2TERM_RE = qr/\A($or)/;
135         }
136
137         while (my ($pfx, $field) = each %PublicInbox::Search::PFX2TERM_RMAP) {
138                 # ideally we'd move this out of the loop:
139                 my $i = $doc->termlist_begin;
140
141                 $i->skip_to($pfx);
142                 if ($i != $end) {
143                         my $val = $i->get_termname;
144
145                         if ($val =~ s/$PFX2TERM_RE//o) {
146                                 $self->{$field} = $val;
147                         }
148                 }
149         }
150 }
151
152 sub mid ($;$) {
153         my ($self, $mid) = @_;
154
155         if (defined $mid) {
156                 $self->{mid} = $mid;
157         } elsif (my $rv = $self->{mid}) {
158                 $rv;
159         } else {
160                 $self->ensure_metadata; # needed for ghosts
161                 $self->{mid} ||= $self->_extract_mid;
162         }
163 }
164
165 sub _extract_mid { mid_clean(mid_mime($_[0]->mime)) }
166
167 sub blob {
168         my ($self, $x40) = @_;
169         if (defined $x40) {
170                 $self->{blob} = $x40;
171         } else {
172                 $self->{blob};
173         }
174 }
175
176 sub mime {
177         my ($self, $mime) = @_;
178         if (defined $mime) {
179                 $self->{mime} = $mime;
180         } else {
181                 # TODO load from git
182                 $self->{mime};
183         }
184 }
185
186 sub doc_id {
187         my ($self, $doc_id) = @_;
188         if (defined $doc_id) {
189                 $self->{doc_id} = $doc_id;
190         } else {
191                 # TODO load from xapian
192                 $self->{doc_id};
193         }
194 }
195
196 sub thread_id {
197         my ($self) = @_;
198         my $tid = $self->{thread};
199         return $tid if defined $tid;
200         $self->ensure_metadata;
201         $self->{thread};
202 }
203
204 sub path {
205         my ($self) = @_;
206         my $path = $self->{path};
207         return $path if defined $path;
208         $self->ensure_metadata;
209         $self->{path};
210 }
211
212 1;