jmhobbs

A Walk Through Swiftmailer Transport AWS SES

A Problem

"@jmhobbs thanks for your amazonses swiftmailer integration. it works. can't understand your code tho. way over my head."

- @rrcatto

"@rrcatto no problem! Neither can I sometimes. :-)"

- @jmhobbs

After this exchange, I thought I would dig back into this project and outline how it works.

Three Core Classes

There are really only two classes in play with this project. The first is Swift_AWSInputByteStream, the second is the transport itself, Swift_AWSTransport. The third, and possibly the most confusing, is ChunkedTransferSocket.

So let's got over each of them.

Swift_AWSInputByteStream

The purpose of this class is to write out the contents of the message to the provided socket. We have this special class for AWS because the documentation specifies that the message data should be Base64 encoded. One side effect of Base64 is padding on encoding. Because of this, we buffer any excess bytes and encode only on multiples of 3 bytes received.

Here is a documented version of the core function, write:

public function write($bytes) {

  // Get the buffer size + new chunk size
  $total_size = strlen( $this->buffer ) + strlen( $bytes );
  // Size of the remainder we will need to buffer
  $excess = $total_size % 3;

  // Nothing to write? Return early.
  if( $total_size - $excess == 0 ) { return ++$this->counter; }

  // Encode and write bytes to the socket
  $this->socket->write(
    urlencode(
      base64_encode(
        substr(
          $this->buffer . $bytes, // Source is buffer + new chunk
          0,                      // Begin at the beginning
          $total_size - $excess   // Write up to the new buffer 
        )
      )
    )
  );

  // If there was excess, store it in the buffer
  if( $excess != 0 ) {
    $this->buffer = substr( $this->buffer . $bytes, -1 * $excess );
  }
  else {
    $this->buffer = '';
  }

  return ++$this->counter;
}

Swift_AWSTransport

This class provides the transport for Swiftmailer. It sets up the socket, takes a message, and sends it off to AWS. The core functionality is in _doSend. This function is documented below. I'm not detailing much here, because it's mostly glue code.

protected function _doSend( 
  Swift_Mime_Message $message, 
  &$failedRecipients = null
) {
  // Use secret key to generate HMAC used to 
  // authorize the message to AWS
  $date = date( 'D, j F Y H:i:s O' );
  // Use the native extension if available
  if( 
    function_exists( 'hash_hmac' ) and 
    in_array( 'sha1', hash_algos() )
  ) {
    $hmac = base64_encode( 
      hash_hmac( 'sha1', $date, $this->AWSSecretKey, true ) 
    );
  }
  // Otherwise, fallback to a PHP implementation
  else {
    $hmac = $this->calculate_RFC2104HMAC( $date, $this->AWSSecretKey );
  }

  // Now we use that to create the authorization header
  $auth = "AWS3-HTTPS AWSAccessKeyId=" . 
          $this->AWSAccessKeyId . 
          ", Algorithm=HmacSHA1, Signature=" . 
          $hmac;

  $host = parse_url( $this->endpoint, PHP_URL_HOST );
  $path = parse_url( $this->endpoint, PHP_URL_PATH );

  // Open up a raw SSL socket to the host 
  $fp = fsockopen( 'ssl://' . $host , 443, $errno, $errstr, 30 );

  if( ! $fp ) {
    throw new AWSConnectionError( "$errstr ($errno)" );
  }

  // Convert that into a chunked "socket"
  $socket = new ChunkedTransferSocket( $fp, $host, $path );

  // Add our date and auth headers (generated above)
  $socket->header("Date", $date);
  $socket->header("X-Amzn-Authorization", $auth);

  // Write the initial post parameters
  $socket->write("Action=SendRawEmail&RawMessage.Data=");

  // Hand it off to an Swift_AWSInputByteStream to write the message
  $ais = new Swift_AWSInputByteStream($socket);
  $message->toByteStream($ais);
  $ais->flushBuffers();

  $result = $socket->read();

  return $result;
}

Okay, not simple, but fairly straightforward.

ChunkedTransferSocket

This class makes an HTTP request direct on the socket. Since we don't know the message size before encoding, and it's memory intensive to encode, buffer, and then send, we do a chunked transfer encoding POST.

It's actually pretty easy. You send some headers, and then every time you have a chunk to write, you preface it with the number of bytes you are sending before you send them.

Here's the code for the write function:

public function write ( $chunk ) {
  if( $this->write_finished ) { throw new InvalidOperationException( "Can not write, reading has started." ); }

  if( ! $this->write_started ) {
    fwrite( $this->socket, "\r\n" ); // Start message body
    $this->write_started = true;
  }

  // Write the length of the chunk, carriage return and new line
  fwrite( $this->socket, sprintf( "%x\r\n", strlen( $chunk ) ) );
  // Write the chunk
  fwrite( $this->socket, $chunk . "\r\n" );
  // Flush the socket to send the data now, not later
  fflush( $this->socket );
}

Pretty simple once you understand how chunked transfer works. The rest of the class is just state keeping.

Conclusion

So, that's that. Nothing really deep in there, just a collection of fairly simple methods that, glued together, send email to AWS SES.

Hit me up with any questions in the comments section if you have them.