aboutsummaryrefslogtreecommitdiff
path: root/lib/Sietima.pm
blob: 98859abd017105cf9aee7fe5a6723ab5d34e2b46 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
package Sietima; 
use Moo;
use Sietima::Policy;
use Types::Standard qw(ArrayRef Object FileHandle Maybe);
use Type::Params qw(compile);
use Sietima::Types qw(Address AddressFromStr
                      EmailMIME Message
                      Subscriber SubscriberFromAddress SubscriberFromStr SubscriberFromHashRef
                      Transport);
use Sietima::Message;
use Sietima::Subscriber;
use Email::Sender::Simple qw();
use Email::Sender;
use Email::Address;
use namespace::clean;
 
with 'MooX::Traits';
# VERSION 
# ABSTRACT: minimal mailing list manager 
 
=head1 SYNOPSIS
 
  use Sietima;
 
  Sietima->new({
    return_path => 'the-list@the-domain.tld',
    subscribers => [ 'person@some.were', @etc ],
  })->handle_mail_from_stdin;
 
=head1 DESCRIPTION
 
Sietima is a minimal mailing list manager written in modern Perl. It
aims to be the spiritual successor of L<Siesta>.
 
The base C<Sietima> class does very little: it just puts the email
message from C<STDIN> into a new envelope using L<< /C<return_path> >>
as sender and all the L<< /C<subscribers> >> addresses as recipients,
and sends it.
 
Additional behaviour is provided via traits / roles. This class
consumes L<< C<MooX::Traits> >> to simplify composing roles:
 
  Sietima->with_traits(qw(AvoidDups NoMail))->new(\%args);
 
These are the traits provided with the default distribution:
 
=for :list
= L<< C<AvoidDups>|Sietima::Role::AvoidDups >>
prevents the sender from receiving copies of their own messages
= L<< C<Debounce>|Sietima::Role::Debounce >>
avoids mail-loops using a C<X-Been-There> header
= L<< C<Headers>|Sietima::Role::Headers >>
adds C<List-*> headers to all outgoing messages
= L<< C<ManualSubscription>|Sietima::Role::ManualSubscription >>
specifies that to (un)subscribe, people should write to the list owner
= L<< C<NoMail>|Sietima::Role::NoMail >>
avoids sending messages to subscribers who don't want them
= L<< C<ReplyTo>|Sietima::Role::ReplyTo >>
optionally sets the C<Reply-To> header to the mailing list address
= L<< C<SubjectTag>|Sietima::Role::SubjectTag >>
prepends a C<[tag]> to the subject header of outgoing messages that
aren't already tagged
= L<< C<SubscriberOnly::Drop>|Sietima::Role::SubscriberOnly::Drop >>
silently drops all messages coming from addresses not subscribed to
the list
= L<< C<SubscriberOnly::Moderate>|Sietima::Role::SubscriberOnly::Moderate >>
holds messages coming from addresses not subscribed to the list for
moderation, and provides commands to manage the moderation queue
 
The only "configuration mechanism" currently supported is to
initialise a C<Sietima> object in your driver script, passing all the
needed values to the constructor. L<< C<Sietima::CmdLine> >> is the
recommended way of doing that: it adds command-line parsing capability
to Sietima.
 
=attr C<return_path>
 
A L<< C<Email::Address> >> instance, coerced from string if
necessary. This is the address that Sietima will send messages
I<from>.
 
=cut
 
has return_path => (
    isa => Address,
    is => 'ro',
    required => 1,
    coerce => AddressFromStr,
);
 
=attr C<subscribers>
 
An array-ref of L<< C<Sietima::Subscriber> >> objects, defaults to the
empty array.
 
Each item can be coerced from a string or a L<< C<Email::Address> >>
instance, or a hashref of the form
 
  { address => $string, %other_attributes }
 
The base Sietima class only uses the address of subscribers, but some
roles use the other attributes (L<< C<NoMail>|Sietima::Role::NoMail
>>, for example, uses the C<prefs> attribute, and L<<
C<SubscriberOnly> >> uses C<aliases> via L<<
C<match>|Sietima::Subscriber/match >>)
 
=cut
 
my $subscribers_array = ArrayRef[
    Subscriber->plus_coercions(
        SubscriberFromAddress,
        SubscriberFromStr,
        SubscriberFromHashRef,
    )
];
has subscribers => (
    isa => $subscribers_array,
    is => 'lazy',
    coerce => $subscribers_array->coercion,
);
sub _build_subscribers { +[] }
 
=attr C<transport>
 
A L<< C<Email::Sender::Transport> >> instance, which will be used to
send messages. If not passed in, Sietima uses L<<
C<Email::Sender::Simple> >>'s L<<
C<default_transport>|Email::Sender::Simple/default_transport >>.
 
=cut
 
has transport => (
    isa => Transport,
    is => 'lazy',
);
sub _build_transport { Email::Sender::Simple->default_transport }
 
=method C<handle_mail_from_stdin>
 
  $sietima->handle_mail_from_stdin();
 
This is the main entry-point when Sietima is invoked from a MTA. It
will parse a L<< C<Email::MIME> >> object out of the standard input,
then pass it to L<< /C<handle_mail> >> for processing.
 
=cut
 
sub handle_mail_from_stdin($self,@) {
    my $mail_text = do { local $/; <> };
    # we're hoping that, since we probably got called from an MTA/MDA, 
    # STDIN contains a well-formed email message, addressed to us 
    my $incoming_mail = Email::MIME->new(\$mail_text);
    return $self->handle_mail($incoming_mail);
}
 
=method C<handle_mail>
 
  $sietima->handle_mail($email_mime);
 
Main driver method: converts the given email message into a list of
L<< C<Sietima::Message> >> objects by calling L<< /C<munge_mail> >>,
then sends each of them by calling L<< /C<send_message> >>.
 
=cut
 
sub handle_mail($self,$incoming_mail) {
    state $check = compile(Object,EmailMIME); $check->(@_);
 
    my (@outgoing_messages) = $self->munge_mail($incoming_mail);
    for my $outgoing_message (@outgoing_messages) {
        $self->send_message($outgoing_message);
    }
    return;
}
 
=method C<subscribers_to_send_to>
 
  my $subscribers_aref = $sietima->subscribers_to_send_to($email_mime);
 
Returns an array-ref of L<< C<Sietima::Subscriber> >> objects that
should receive copies of the given email message.
 
In this base class, it just returns the value of the L<<
/C<subscribers> >> attribute. Roles such as L<<
C<AvoidDups>|Sietima::Role::AvoidDups >> modify this method to exclude
some subscribers.
 
=cut
 
sub subscribers_to_send_to($self,$incoming_mail) {
    state $check = compile(Object,EmailMIME); $check->(@_);
 
    return $self->subscribers;
}
 
=method C<munge_mail>
 
  my @messages = $sietima->munge_mail($email_mime);
 
Returns a list of L<< C<Sietima::Message> >> objects representing the
messages to send to subscribers, based on the given email message.
 
In this base class, this method returns a single instance to send to
all L<< /C<subscribers_to_send_to> >>, containing exactly the given
email message.
 
Roles such as L<< C<SubjectTag>|Sietima::Role::SubjectTag >> modify
this method to alter the message.
 
=cut
 
sub munge_mail($self,$incoming_mail) {
    state $check = compile(Object,EmailMIME); $check->(@_);
 
    return Sietima::Message->new({
        mail => $incoming_mail,
        from => $self->return_path,
        to => $self->subscribers_to_send_to($incoming_mail),
    });
}
 
=method C<send_message>
 
  $sietima->send_message($sietima_message);
 
Sends the given L<< C<Sietima::Message> >> object via the L<<
/C<transport> >>, but only if the message's
L<envelope|Sietima::Message/envelope> specifies some recipients.
 
=cut
 
sub send_message($self,$outgoing_message) {
    state $check = compile(Object,Message); $check->(@_);
 
    my $envelope = $outgoing_message->envelope;
    if ($envelope->{to} && $envelope->{to}->@*) {
        $self->transport->send(
            $outgoing_message->mail,
            $envelope,
        );
    }
 
    return;
}
 
sub _trait_namespace 'Sietima::Role' } ## no critic(ProhibitUnusedPrivateSubroutines) 
 
=method C<list_addresses>
 
  my $addresses_href = $sietima->list_addresses;
 
Returns a hashref of L<< C<Email::Address> >> instances or strings,
that declare various addresses related to this list.
 
This base class declares only the L<< /C<return_path> >>, and does not
use this method at all.
 
The L<< C<Headers>|Sietima::Role::Headers >> role uses this to
populate the various C<List-*> headers.
 
=cut
 
sub list_addresses($self) {
    return +{
        return_path => $self->return_path,
    };
}
 
=method C<command_line_spec>
 
  my $app_spec_data = $sietima->command_line_spec;
 
Returns a hashref describing the command line processing for L<<
C<App::Spec> >>. L<< C<Sietima::CmdLine> >> uses this to build the
command line parser.
 
This base class declares a single sub-command:
 
=over
 
=item C<send>
 
Invokes the L<< /C<handle_mail_from_stdin> >> method.
 
For example, in a C<.qmail> file:
 
  |/path/to/sietima send
 
=back
 
Roles can extend this to provide additional sub-commands and options.
 
=cut
 
sub command_line_spec($self) {
    return {
        name => 'sietima',
        title => 'a simple mailing list manager',
        subcommands => {
            send => {
                op => 'handle_mail_from_stdin',
                summary => 'send email from STDIN',
            },
        },
    };
}
 
1;