I had a ticket logged by a customer today reporting that PHP's mail()
function was timing out on him on one of our Windows 2003 Server boxes when trying to send attachments.
Upon investigating I was able to reproduce his problem. Messages containing smallish attachments at around the 30-60Kb size were taking 15-20 seconds to be processed by the mail()
function. Larger attachments of around 360-500Kb were taking longer than the maximum script execution time permitted (90 seconds).
I was able to reproduce the problem on two different Windows 2003 servers and a Windows 2008R2 server. I also tried three different versions of PHP (5.2.14, 5.2.17 and 5.3.6 - all 32 bit and all non-threadsafe builds as per Microsoft's recommendations for running PHP on Windows).
In all cases mail was being sent via SMTP (i.e. not using a sendmail implementation). I tried three different SMTP scenarios:
- Delivery directly to our SMTP smarthost cluster (running exim)
- Delivery via local IIS SMTP Service which relays to our smarthosts
- Delivery via local IIS SMTP Service but with MX lookup and direct delivery
Regardless of the above, sending attachments was still suboptimal which means the problem can't be pinned on a slow relay.
I then ran the same code on our CentOS servers which didn't exhibit any of these issues, the mail()
function returned almost immediately. However PHP on these servers are configured to use sendmail
.
I then decided to spelunk the PHP source code to find out what the implementation of the mail()
function looked like and discovered this code in ext/standard/mail.c
:
if (!sendmail_path) {
#if (defined PHP_WIN32 || defined NETWARE)
/* handle old style win smtp sending */
if (TSendMail(INI_STR("SMTP"), &tsm_err, &tsm_errmsg, headers, subject, to, message, NULL, NULL, NULL TSRMLS_CC) == FAILURE) {
if (tsm_errmsg) {
php_error_docref(NULL TSRMLS_CC, E_WARNING, "%s", tsm_errmsg);
efree(tsm_errmsg);
} else {
php_error_docref(NULL TSRMLS_CC, E_WARNING, "%s", GetSMErrorText(tsm_err));
}
return 0;
}
return 1;
#else
return 0;
#endif
TSendMail()
is implemented in another source file (win32/sendmail.c
). Ultimately all data sent to a SMTP server seems to be passed synchronously via a function called Post()
in sendmail.c
which looks like:
static int Post(LPCSTR msg)
{
int len = strlen(msg);
int slen;
int index = 0;
while (len > 0) {
if ((slen = send(sc, msg + index, len, 0)) < 1)
return (FAILED_TO_SEND);
len -= slen;
index += slen;
}
return (SUCCESS);
}
The send()
function is a winsock2
function.
I am wondering if buffer size (8K is the default according to the KB article below) or lack of tuning is having some impact here for larger amounts of data. There are no calls to setsockopt()
to specify a buffer size or any other options to optimise calls to send()
.
Perhaps the mail()
function on Windows using SMTP delivery is not intended to be used for sending large emails?
I'd be interested to know if anyone else has looked at this code or experienced the same thing.
Design issues - Sending small data segments over TCP with Winsock
Just to be clear, we already have an alternative solution for the customer (SwiftMailer) in place now so this isn't about getting recommendations for alternatives.
System.Net,Mail
API's which all talk SMTP over TCP). i.e. everything else that talks SMTP is fine, PHP'smail()
function is the odd man out. – Triceps