Skip to content

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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:

1
2
3
4
5
6
7
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:

1
2
3
4
5
6
7
8
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):

1
2
3
4
5
6
7
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:

1
2
3
4
5
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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:

1
2
3
4
5
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:

1
2
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:

1
set-mailbox-ingress(RelayIngress.new(:$router));

POSTing a raw message to the controller returns 204; with no configured ingress it returns 503.