File Coverage

blib/lib/Sietima/MailStore/FS.pm
Criterion Covered Total %
statement 115 115 100.0
branch 24 42 57.1
condition n/a
subroutine 19 19 100.0
pod 6 7 85.7
total 164 183 89.6


line stmt bran cond sub pod time code
1             package Sietima::MailStore::FS;
2 1     1   27534 use Moo;
  1         5  
  1         11  
3 1     1   383 use Sietima::Policy;
  1         24  
  1         8  
4 1     1   412 use Types::Path::Tiny qw(Dir);
  1         16207  
  1         9  
5 1     1   511 use Types::Standard qw(Object ArrayRef Str slurpy);
  1         2  
  1         6  
6 1     1   1103 use Type::Params qw(compile);
  1         2  
  1         7  
7 1     1   200 use Sietima::Types qw(EmailMIME TagName);
  1         2  
  1         6  
8 1     1   613 use Digest::SHA qw(sha1_hex);
  1         3  
  1         69  
9 1     1   7 use namespace::clean;
  1         2  
  1         8  
10              
11             our $VERSION = '1.0.3'; # 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 50   2   24 sub _build__tagdir($self) { $self->root->child('tags') }
  2 50       8  
  2         4  
  2         3  
  2         13  
27 2 50   2   635 sub _build__msgdir($self) { $self->root->child('msgs') }
  2 50       7  
  2         5  
  2         3  
  2         10  
28              
29 2 50   2 0 80089 sub BUILD($self,@) {
  2         6  
  2         5  
30 2         39 $self->$_->mkpath for qw(_tagdir _msgdir);
31 2         325 return;
32             }
33              
34              
35 3 50   3 1 9782 sub store($self,$mail,@tags) {
  3         7  
  3         6  
  3         15  
  3         7  
36 3         9 state $check = compile(Object,EmailMIME,slurpy ArrayRef[TagName]);$check->(@_);
  3         6891  
37              
38 3         170 my $str = $mail->as_string;
39 3         405 my $id = sha1_hex($str);
40              
41 3         81 $self->_msgdir->child($id)->spew_raw($str);
42              
43 3         1536 $self->_tagdir->child($_)->append("$id\n") for @tags;
44              
45 3         915 return $id;
46             }
47              
48              
49 12 50   12 1 6112 sub retrieve_by_id($self,$id) {
  12 50       34  
  12         22  
  12         25  
  12         15  
50 12         27 state $check = compile(Object,Str);$check->(@_);
  12         1108  
51              
52 12         345 my $msg_path = $self->_msgdir->child($id);
53 12 50       564 return unless -e $msg_path;
54 12         256 return Email::MIME->new($msg_path->slurp_raw);
55             }
56              
57              
58 9 50   9   30 sub _tagged_by($self,$tag) {
  9 50       21  
  9         20  
  9         15  
  9         13  
59 9         214 my $tag_file = $self->_tagdir->child($tag);
60 9 50       494 return unless -e $tag_file;
61 9         216 return $tag_file->lines({chomp=>1});
62             }
63              
64 10 50   10 1 12839 sub retrieve_ids_by_tags($self,@tags) {
  10         18  
  10         30  
  10         20  
65 10         22 state $check = compile(Object,slurpy ArrayRef[TagName]);$check->(@_);
  10         5762  
66              
67             # this maps: id -> how many of the given @tags it has
68 10         356 my %msgs;
69 10 100       25 if (@tags) {
70 7         15 for my $tag (@tags) {
71 9         409 $_++ for @msgs{$self->_tagged_by($tag)};
72             }
73             }
74             else {
75 3         65 $msgs{$_->basename}=0 for $self->_msgdir->children;
76             }
77              
78 10         1686 my @ret;
79 10         31 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       60 next unless $msgs{$id} == @tags;
83 17         33 push @ret, $id;
84             }
85 10         37 return \@ret;
86             }
87              
88              
89 6 50   6 1 18014 sub retrieve_by_tags($self,@tags) {
  6         13  
  6         20  
  6         12  
90 6         14 state $check = compile(Object,slurpy ArrayRef[TagName]);$check->(@_);
  6         6031  
91              
92 6         261 my @ret;
93 6         23 for my $id ($self->retrieve_ids_by_tags(@tags)->@*) {
94 9         2553 push @ret, {
95             id => $id,
96             mail => $self->retrieve_by_id($id),
97             };
98             }
99              
100 6         2807 return \@ret;
101             }
102              
103              
104 1 50   1 1 1887 sub remove($self,$id) {
  1 50       4  
  1         2  
  1         3  
  1         2  
105 1         5 state $check = compile(Object,Str);$check->(@_);
  1         1020  
106              
107 1         30 for my $tag_file ($self->_tagdir->children) {
108 2 100   4   296 $tag_file->edit_lines( sub { $_='' if /\A\Q$id\E\n?\z/ } );
  4         771  
109             }
110 1         119 $self->_msgdir->child($id)->remove;
111              
112 1         194 return;
113             }
114              
115              
116 1 50   1 1 3120 sub clear($self) {
  1 50       6  
  1         4  
  1         3  
117 1         5 do { $self->$_->remove_tree;$self->$_->mkpath } for qw(_tagdir _msgdir);
  2         216  
  2         817  
118 1         131 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.3
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