The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

Mail::SRS - OO interface to Sender Rewriting Scheme

SYNOPSIS

  http://spf.pobox.com/srs.html

  use Mail::SRS;
  my $srs = Mail::SRS->new
    (bounce_delimiter  => '+',
     sender_delimiter  => '-',
     cookie_delimiter  => '-',
     alias_delimiter   => '=',
     address_delimiter => '#',
     secret            => [ 'my secret', 'older secrets', ... ],
     format            => 'bounce[% $bounce_delimiter . $sender . $cookie_delimiter . $cookie . $alias_delimiter . $alias_user %]@[% $alias_host %]';
     max_age           => 30, # days
     validator         => sub {
       my %o = @_; # cookie, sender, alias
       ...
       return; # valid.
       return "550 No more bounces accepted to that address.";
       return "550 Bounces not accepted to that address.";
     },
     extractor         => sub {
       my ($self, $address) = @_;
       ...
       return ($sender, $cookie, $alias_user, $alias_host);
     },
    );

  $srs->set_secret('new secret');
  $srs->set_secret('newer secret', $srs->get_secret);

  my ($new_sender, $cookie) = $srs->forward(sender => 'sender@example.com',
                                            alias  => 'alias@forwarder.com',
                                            rcpts  => [ 'rcpt@example.net' ]);

  # $new_sender is your new return-path.
  # when you get mail to that return-path, you can recover the original data with:

  my ($sender, $alias, $response) = $srs->reverse(address => $new_sender);

DESCRIPTION

The Sender Rewriting Scheme preserves .forward functionality in an SPF-compliant world.

This module should be considered alpha at this time. Documentation is incomplete. Pobox.com decided to publish Mail::SRS to CPAN anyway because there seems to be a fair amount of interest out there in implementing SRS.

SPF requires an SMTP client IP to match the envelope sender (return-path). When a message is forwarded through an intermediate server, that intermediate server may need to rewrite the return-path to remain SPF compliant. If the message bounces, that intermediate server needs to validate the bounce and forward the bounce to the original sender.

SRS provides a convention for return-path rewriting which allows multiple forwarding servers to compact the return-path. SRS also provides an authentication mechanism to ensure that purported bounces are not arbitrarily forwarded.

SRS is documented at http://spf.pobox.com/srs.html

A given SRS address is valid for one month by default.

Cookies are relatively unique.

You may wish to limit the number of bounces you will convey to a given SRS sender. The rcpts argument to forward lets you encode the approximate number of recipients into the cookie; you can thus limit a given SRS address to a specified number of uses by passing reverse() a validator callback which performs a local database lookup against the cookie,sender,alias tuple.

METHODS

new

  my $srs = Mail::SRS->new
    (sender_delimiter  => '-',
     cookie_delimiter  => '-',
     alias_delimiter   => '=',
     address_delimiter => '#',
     secret            => [ 'my secret', 'older secrets', ... ],
     format            => 'bounce+[% $sender . $cookie_delimiter . $cookie . $alias_delimiter . $alias_user %]@[% $alias_host %]';
     max_age           => 30, # days
     validator         => sub {
       my %o = @_; # cookie, sender, alias
       ...
       return; # valid.
       return "550 No more bounces accepted to that address.";
       return "550 Bounces not accepted to that address.";
     },
     extractor         => sub {
       my ($self, $address) = @_;
       ...
       return ($sender, $cookie, $alias_user, $alias_host);
     },
    );

forward

  my ($new_sender, $cookie) = $srs->forward(sender => 'sender@example.com',
                                            alias  => 'alias@forwarder.com',
                                            rcpts  => [ 'rcpt@example.net' ]);

  # $new_sender is your new return-path.

reverse

  # $new_sender is the return-path produced by ->forward().
  # when you get mail to that return-path, you can recover the original data with:

  my ($sender, $alias, $response) = $srs->reverse(address => $new_sender);

set_secret, get_secret

  $srs->set_secret('new secret');
  $srs->set_secret('newer secret', $srs->get_secret);

ALGORITHM

Cookies are needed so a reversing host doesn't become an open relay.

We are concerned that an attacker will try to forge or replay cookies.

We approach the replay problem by limiting the validity of a cookie in time and in the number of punches permitted that cookie.

We approach the forgery problem by using a secret string in the creation and validation of the cookie.

Punches: When we create a cookie, we do so knowing how many recipients are being used for that cookie; and we multiply that number by a modest ratio which allows for downstream .forwarding to multiple accounts. We encode that recipient count into the cookie and expose it in the salt.

Time: When we create a cookie, we do so knowing the current time. We encode the current time, with limited precision, into the cookie and expose it in the salt.

The salt of a standard crypt cookie can represent 12 bits of data, being m([a-zA-Z0-9./]{2}): each character is one of 64 bytes; two characters afford 4096 or 2**12 combinations.

* Let us specify that an SRS cookie may expect as few as 2 and as many as 8 discrete punches. More punches than 8 shall be considered "infinite".

  Using 2 bits, an SRS cookie can specify a maximum punch count of 2, 4, 8, or infinite.

    0 = 2
    1 = 4
    2 = 8
    3 = infinite

That leaves 10 bits.

* Let us specify that an SRS cookie shall expire after 1 month.

  Day precision is sufficient.  To store 256 days, we need 8 bits.

* Reserved: That leaves 2 bits reserved for future use.

* SRS cookie:

       8      2   2
  [  day   ][ p][rr]

We test an SRS cookie for time-validity by decoding the salt to reveal the time slot and the punch count; we then confirm that the time slot and punch count were not forged by recrypting the cookie against the asserted data plus the secret.