$theTitle=wp_title(" - ", false); if($theTitle != "") { ?> } else { ?> } ?>
by Andrew Johnstone
In: PHP
31 May 2010A while back a colleague and myself had a two week sprint to ensure that we could deliver 20k emails with up to 5mb attachments each. We experimented with 4k emails at 5mb each sending straight through the MTA, which we found to cause excessive load on the servers.
Using exim filters we could add attachments at the delivery stage, therefore reducing overhead in constructing and injecting data into an email. The delivery of emails varied as to whether it was feasible to send as a BCC or whether there were placeholders intended for each individual recipient. As such the following addresses the worst case scenario of attachments with placeholders intended for each individual recipient.
A few things that we needed to be careful of were:
One important fact is that if the exim filter failed for any reason, the message is retained in the queue, allowing you to control sending out broken messages, I.e. missing files.
Exims configuration is located within the following folders, the important part for this is the transport that specifies the exim_filter to be used. There are varying configurations of this, which you can use for smarthosts, local delivery or remote smtp.
/etc/exim4/conf.d/ |-- acl |-- auth |-- main |-- retry |-- rewrite |-- router `-- transport
### transport/30_exim4-config_remote_smtp ################################# # This transport is used for delivering messages over SMTP connections. remote_smtp: debug_print = "T: remote_smtp for $local_part@$domain" driver = smtp transport_filter = /path/to/exim/filter/attachments.php .ifdef REMOTE_SMTP_HOSTS_AVOID_TLS hosts_avoid_tls = REMOTE_SMTP_HOSTS_AVOID_TLS .endif .ifdef REMOTE_SMTP_HEADERS_REWRITE headers_rewrite = REMOTE_SMTP_HEADERS_REWRITE .endif .ifdef REMOTE_SMTP_RETURN_PATH return_path = REMOTE_SMTP_RETURN_PATH .endif .ifdef REMOTE_SMTP_HELO_FROM_DNS helo_data=REMOTE_SMTP_HELO_DATA .endif
### transport/30_exim4-config_maildir_home ################################# # Use this instead of mail_spool if you want to to deliver to Maildir in # home-directory - change the definition of LOCAL_DELIVERY # maildir_home: debug_print = "T: maildir_home for $local_part@$domain" driver = appendfile transport_filter = /path/to/exim/filter/attachments.php .ifdef MAILDIR_HOME_MAILDIR_LOCATION directory = MAILDIR_HOME_MAILDIR_LOCATION .else directory = $home/Maildir .endif .ifdef MAILDIR_HOME_CREATE_DIRECTORY create_directory .endif .ifdef MAILDIR_HOME_CREATE_FILE create_file = MAILDIR_HOME_CREATE_FILE .endif delivery_date_add envelope_to_add return_path_add maildir_format .ifdef MAILDIR_HOME_DIRECTORY_MODE directory_mode = MAILDIR_HOME_DIRECTORY_MODE .else directory_mode = 0700 .endif .ifdef MAILDIR_HOME_MODE mode = MAILDIR_HOME_MODE .else mode = 0600 .endif mode_fail_narrower = false # This transport always chdirs to $home before trying to deliver. If # $home is not accessible, this chdir fails and prevents delivery. # If you are in a setup where home directories might not be # accessible, uncomment the current_directory line below. # current_directory = /
### transport/30_exim4-config_remote_smtp_smarthost ################################# # This transport is used for delivering messages over SMTP connections # to a smarthost. The local host tries to authenticate. # This transport is used for smarthost and satellite configurations. remote_smtp_smarthost: debug_print = "T: remote_smtp_smarthost for $local_part@$domain" driver = smtp transport_filter = /path/to/exim/filter/attachments.php hosts_try_auth = < ; ${if exists{CONFDIR/passwd.client} { ${lookup{$host}nwildlsearch{CONFDIR/passwd.client}{$host_address}} } {} } .ifdef REMOTE_SMTP_SMARTHOST_HOSTS_AVOID_TLS hosts_avoid_tls = REMOTE_SMTP_SMARTHOST_HOSTS_AVOID_TLS .endif .ifdef REMOTE_SMTP_HEADERS_REWRITE headers_rewrite = REMOTE_SMTP_HEADERS_REWRITE .endif .ifdef REMOTE_SMTP_RETURN_PATH return_path = REMOTE_SMTP_RETURN_PATH .endif .ifdef REMOTE_SMTP_HELO_FROM_DNS helo_data=REMOTE_SMTP_HELO_DATA .endif
An example of an exim filter
abstract class eximFilter extends php_user_filter { private $_stream; private $_filterAppended = FALSE; private $_replacements = array(); private function flush() { rewind($this->_stream); fpassthru($this->_stream); fclose($this->_stream); } /** * Ensure output steam available */ private function hasStream() { return is_resource($this->_stream); } private function addFilter() { } /** * Remove last attached stream */ private function removeFilter() { if ($this->hasStream()) $this->_filterAppended = !stream_filter_remove($this->_stdOut); } public function __destruct() { if (is_resource($this->_stream)) fclose($this->_stream); } /** * @param string $reason */ protected function bail($reason = false) { if(is_string($reason)) { echo $reason; } exit(1); } } class stdOutFilter extends eximFilter { public function filter($line) { echo $line; } } class eximFilters { private $_data; private $_wrapper = 'exim'; public $_filters = array(); private $_filterWrapper, $_filterName; private $_instream; private $_outstream; public function setStreams($in, $out) { $this->_instream = $in; $this->_outstream = $out; return $this; } public function addFilter(eximFilter $filter) { $this->_filters[ $f = get_class($filter) ] = $filter; return $this; } public function removeFilter(eximFilter $filter) { unset( $this->_filters[ get_class($filter) ]); return $this; } public function getFilters() { return $this->_filters; } /** * Ensure streams available */ private function hasStream() { return (is_resource($this->_instream) && is_resource($this->_outstream)); } public function execute() { while($line = fgets($this->_instream)) { foreach($this->getFilters() as $filter) { $filter->filter($line); } } } } $filters = new eximFilters(); $filters->setStreams( fopen('php://stdin', 'r+'), fopen('php://stdout', 'w') ) ->addFilter(new stdOutFilter()) ->execute();
With attachments you have to base64 encode the stream and split the data at the 76 character onto a new line. You also have to have a scheme for acquiring the header and location for the file that is to be injected into the stream and remove the line.
This reduced IO, Memory and CPU usage when dispatching bulk emails with attachments with roughly a 2000% performance increase.
I have been a developer for roughly 10 years and have worked with an extensive range of technologies. Whilst working for relatively small companies, I have worked with all aspects of the development life cycle, which has given me a broad and in-depth experience.