File Coverage

blib/lib/Sietima/MailStore/FS.pm
Criterion Covered Total %
statement 109 109 100.0
branch 8 10 80.0
condition n/a
subroutine 19 19 100.0
pod 6 7 85.7
total 142 145 97.9


line stmt bran cond sub pod time code
1             package Sietima::MailStore::FS;
2 1     1   27112 use Moo;
  1         3  
  1         9  
3 1     1   378 use Sietima::Policy;
  1         2  
  1         11  
4 1     1   534 use Types::Path::Tiny qw(Dir);
  1         17610  
  1         8  
5 1     1   473 use Types::Standard qw(Object ArrayRef Str slurpy);
  1         3  
  1         6  
6 1     1   1066 use Type::Params qw(compile);
  1         2  
  1         10  
7 1     1   241 use Sietima::Types qw(EmailMIME TagName);
  1         5  
  1         12  
8 1     1   641 use Digest::SHA qw(sha1_hex);
  1         3  
  1         50  
9 1     1   6 use namespace::clean;
  1         2  
  1         12  
10              
11             our $VERSION = '1.0.4'; # VERSION
12             # ABSTRACT: filesystem-backed email store
13              
14              
15             with 'Sietima::MailStore';
16              
17              
18             has root => (
19             is => 'ro',
20             required => 1,
21             isa => Dir,
22             coerce => 1,
23             );
24              
25             has [qw(_tagdir _msgdir)] => ( is => 'lazy' );
26 2     2   20 sub _build__tagdir($self) { $self->root->child('tags') }
  2         6  
  2         4  
  2         16  
27 2     2   750 sub _build__msgdir($self) { $self->root->child('msgs') }
  2         5  
  2         6  
  2         11  
28              
29 2     2 0 92406 sub BUILD($self,@) {
  2         6  
  2         4  
30 2         45 $self->$_->mkpath for qw(_tagdir _msgdir);
31 2         423 return;
32             }
33              
34              
35 3     3 1 12386 sub store($self,$mail,@tags) {
  3         6  
  3         6  
  3         7  
  3         9  
36 3         11 state $check = compile(Object,EmailMIME,slurpy ArrayRef[TagName]);$check->(@_);
  3         7003  
37              
38 3         188 my $str = $mail->as_string;
39 3         467 my $id = sha1_hex($str);
40              
41 3         77 $self->_msgdir->child($id)->spew_raw($str);
42              
43 3         1728 $self->_tagdir->child($_)->append("$id\n") for @tags;
44              
45 3         1103 return $id;
46             }
47              
48              
49 12     12 1 7422 sub retrieve_by_id($self,$id) {
  12         18  
  12         19  
  12         19  
50 12         26 state $check = compile(Object,Str);$check->(@_);
  12         1273  
51              
52 12         430 my $msg_path = $self->_msgdir->child($id);
53 12 50       699 return unless -e $msg_path;
54 12         310 return Email::MIME->new($msg_path->slurp_raw);
55             }
56              
57              
58 9     9   15 sub _tagged_by($self,$tag) {
  9         16  
  9         16  
  9         15  
59 9         231 my $tag_file = $self->_tagdir->child($tag);
60 9 50       504 return unless -e $tag_file;
61 9         277 return $tag_file->lines({chomp=>1});
62             }
63              
64 10     10 1 14459 sub retrieve_ids_by_tags($self,@tags) {
  10         15  
  10         22  
  10         14  
65 10         19 state $check = compile(Object,slurpy ArrayRef[TagName]);$check->(@_);
  10         5604  
66              
67             # this maps: id -> how many of the given @tags it has
68 10         336 my %msgs;
69 10 100       30 if (@tags) {
70 7         19 for my $tag (@tags) {
71 9         459 $_++ for @msgs{$self->_tagged_by($tag)};
72             }
73             }
74             else {
75 3         80 $msgs{$_->basename}=0 for $self->_msgdir->children;
76             }
77              
78 10         2251 my @ret;
79 10         32 for my $id (keys %msgs) {
80             # if this message id does not have all the required tags, we
81             # won't return it
82 21 100       57 next unless $msgs{$id} == @tags;
83 17         38 push @ret, $id;
84             }
85 10         42 return \@ret;
86             }
87              
88              
89 6     6 1 18329 sub retrieve_by_tags($self,@tags) {
  6         15  
  6         36  
  6         11  
90 6         14 state $check = compile(Object,slurpy ArrayRef[TagName]);$check->(@_);
  6         5979  
91              
92 6         305 my @ret;
93 6         22 for my $id ($self->retrieve_ids_by_tags(@tags)->@*) {
94 9         2802 push @ret, {
95             id => $id,
96             mail => $self->retrieve_by_id($id),
97             };
98             }
99              
100 6         3364 return \@ret;
101             }
102              
103              
104 1     1 1 2695 sub remove($self,$id) {
  1         2  
  1         3  
  1         2  
105 1         7 state $check = compile(Object,Str);$check->(@_);
  1         1375  
106              
107 1         37 for my $tag_file ($self->_tagdir->children) {
108 2 100   4   426 $tag_file->edit_lines( sub { $_='' if /\A\Q$id\E\n?\z/ } );
  4         982  
109             }
110 1         177 $self->_msgdir->child($id)->remove;
111              
112 1         152 return;
113             }
114              
115              
116 1     1 1 4039 sub clear($self) {
  1         2  
  1         2  
117 1         4 do { $self->$_->remove_tree;$self->$_->mkpath } for qw(_tagdir _msgdir);
  2         282  
  2         1085  
118 1         153 return;
119             }
120              
121             1;
122              
123             __END__
124              
125             =pod
126              
127             =encoding UTF-8
128              
129             =head1 NAME
130              
131             Sietima::MailStore::FS - filesystem-backed email store
132              
133             =head1 VERSION
134              
135             version 1.0.4
136              
137             =head1 SYNOPSIS
138              
139             my $store = Sietima::MailStore::FS->new({ root => '/tmp/my-store' });
140              
141             =head1 DESCRIPTION
142              
143             This class implements the L<< C<Sietima::MailStore> >> interface,
144             storing emails as files on disk.
145              
146             =head1 ATTRIBUTES
147              
148             =head2 C<root>
149              
150             Required, a L<< C<Path::Tiny> >> object that points to an existing
151             directory. Coercible from a string.
152              
153             It's a good idea for the directory to be readable and writable by the
154             user who will run the mailing list, and also by all users who will run
155             administrative commands (like those provided by L<<
156             C<Sietima::Role::SubscriberOnly::Moderate> >>). A way to achieve that
157             is to have a group dedicated to list owners, and set the directory
158             group-writable and group-sticky, and owned by that group:
159              
160             # chgrp -R mailinglists /tmp/my-store
161             # chmod -R g+rwXs /tmp/my-store
162              
163             =head1 METHODS
164              
165             =head2 C<store>
166              
167             my $id = $store->store($email_mime_object,@tags);
168              
169             Stores the given email message inside the L<store root|/root>, and
170             associates with the given tags.
171              
172             Returns a unique identifier for the stored message. If you store twice
173             the same message (or two messages that stringify identically), you'll
174             get the same identifier.
175              
176             =head2 C<retrieve_by_id>
177              
178             my $email_mime_object = $store->retrieve_by_id($id);
179              
180             Given an identifier returned by L<< /C<store> >>, this method returns
181             the email message.
182              
183             If the message has been deleted, or the identifier is not recognised,
184             this method returns C<undef> in scalar context, or an empty list in
185             list context.
186              
187             =head2 C<retrieve_ids_by_tags>
188              
189             my @ids = $store->retrieve_ids_by_tags(@tags)->@*;
190              
191             Given a list of tags, this method returns an arrayref containing the
192             identifiers of all (and only) the messages that were stored associated
193             with (at least) all those tags. The order of the returned identifiers
194             is essentially random.
195              
196             If there are no messages associated with the given tags, this method
197             returns an empty arrayref.
198              
199             =head2 C<retrieve_by_tags>
200              
201             my @email_mime_objects = $store->retrieve_by_tags(@tags)->@*;
202              
203             This method is similar to L<< /C<retrieve_ids_by_tags> >>, but it
204             returns an arrayref of hashrefs like:
205              
206             $store->retrieve_ids_by_tags('t1') ==> [
207             { id => $id1, mail => $msg1 },
208             { id => $id2, mail => $msg2 },
209             ]
210              
211             =head2 C<remove>
212              
213             $store->remove($id);
214              
215             This method removes the message corresponding to the given identifier
216             from disk. Removing a non-existent message does nothing.
217              
218             =head2 C<clear>
219              
220             $store->clear();
221              
222             This method removes all messages from disk. Clearing as empty store
223             does nothing.
224              
225             =for Pod::Coverage BUILD
226              
227             =head1 AUTHOR
228              
229             Gianni Ceccarelli <dakkar@thenautilus.net>
230              
231             =head1 COPYRIGHT AND LICENSE
232              
233             This software is copyright (c) 2017 by Gianni Ceccarelli <dakkar@thenautilus.net>.
234              
235             This is free software; you can redistribute it and/or modify it under
236             the same terms as the Perl 5 programming language system itself.
237              
238             =cut