Action Mailbox
Action Mailbox routes inbound email into mailbox classes. A raw message enters through an ingress, becomes an inbound-email record with a processing state machine, is matched to a mailbox by recipient, sender, or subject, and is processed with callbacks.
Inbound email#
parse-message turns a raw RFC 822 string into a Message with unfolded
headers and a body:
use MVC::Keayl::Mailbox;
my $message = parse-message($raw);
$message.from; # the sender address
$message.to; # the recipient addresses
$message.cc;
$message.recipients; # to + cc
$message.subject;
$message.message-id;
$message.header('x-spam-score');
$message.body;
An InboundEmail wraps the raw message and tracks its state. It starts
pending and transitions through processing, then delivered, bounced, or
failed:
my $email = InboundEmail.new(:$raw);
$email.is-pending; # True
$email.processing-bang;
$email.delivered-bang; # or bounced-bang / failed-bang
$email.message; # the parsed Message
Mailboxes#
A mailbox subclasses Mailbox and implements process. Callbacks run around
processing, and the inbound email is marked delivered when process returns:
class SupportMailbox is Mailbox {
method process {
# self.message is the parsed inbound message
}
}
SupportMailbox.before-processing(-> $mailbox { ... });
SupportMailbox.after-processing(-> $mailbox { ... });
perform-processing drives the lifecycle: it marks the email processing, runs
the before callbacks, calls process, runs the after callbacks, and marks the
email delivered.
Bouncing#
Calling bounce inside processing marks the email bounced, runs the
on-bounce callbacks, and halts the rest of processing (the after callbacks are
skipped):
class RepliesMailbox is Mailbox {
method process {
self.bounce unless self.message.from;
}
}
RepliesMailbox.on-bounce(-> $mailbox { ... });
Failures#
An exception raised during processing marks the email failed. A matching
rescue-from handler runs; with no handler, the exception is re-raised:
class ImportMailbox is Mailbox {
method process { ... }
}
ImportMailbox.rescue-from(X::Parse, -> $mailbox, $error { ... });
Routing#
A Router maps inbound email to a mailbox. Each routing rule matches on
recipient, to, from, or subject, takes a matching predicate over the
inbound email, or all for a catch-all. Conditions accept an exact string or a
regex (subject and a string match as a substring); every condition in a rule
must match. The first matching rule wins:
use MVC::Keayl::Mailbox::Router;
my $router = Router.new;
$router.routing(to => 'support@example.com', mailbox => SupportMailbox);
$router.routing(from => rx:i/ '@billing.example.com' $/, mailbox => BillingMailbox);
$router.routing(subject => 'invoice', mailbox => InvoiceMailbox);
$router.routing(matching => -> $email { $email.message.header('x-spam').defined }, mailbox => SpamMailbox);
$router.routing(all => True, mailbox => DefaultMailbox);
$router.route($email); # finds the mailbox and processes the email
$router.mailbox-for($email); # the matched mailbox class, or Nil
Ingress#
An ingress receives raw email, records an InboundEmail, and hands it to the
router. RelayIngress takes one message at a time, as from a webhook relay:
use MVC::Keayl::Mailbox::Ingress;
my $ingress = RelayIngress.new(:$router);
$ingress.receive($raw); # records and routes the message
$ingress.repository.all; # every recorded inbound email
SourceIngress pulls from a generic source (an SMTP or POP mailbox), processing
every fetched message:
my $ingress = SourceIngress.new(:$router, :$source);
$ingress.poll; # fetches from source.fetch and processes each
Records persist through a repository.
MVC::Keayl::Mailbox::Ingress::Repository defines the interface and
MemoryRepository is the in-process implementation.
Relay endpoint#
RelayController is the HTTP relay endpoint. Configure the ingress once, and the
controller hands each posted raw email to it:
set-mailbox-ingress(RelayIngress.new(:$router));
POSTing a raw message to the controller returns 204; with no configured
ingress it returns 503.