Is PHP's mail() function attachment support a bit broken on Windows?
Asked Answered
T

1

6

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.

Triceps answered 23/9, 2011 at 1:31 Comment(9)
It's not often you see a question from a) an admin, and b) someone with a lot of rep. Must be NP-Hard... :PAffirm
I wonder if a bounty is in order in this case.Affirm
mail() is very underpowered, never use it for anything that's not super basic. I think the manual hints at thisMedlock
I think the difference here is that when mail() uses sendmail then PHP passes message locally to sendmail without actually waiting for this message to be delivered(sendmail sends message after mail() returns) so it is fast as local connections are fast, while if mail() uses sockets then it actually waits for this message to be sent, so mail() speed depends on external connection speed. About buffer size - I don't think it matters as maximum TCP packet payload size is usually about 1.5k, so 8k buffer should be ok - even if there is no internal optimization only 1 packet in 6 will be undersized.Vary
@Vary - I am guessing that's the reason too, though I didn't expect sending over a socket to be that slow. Anyway, why not make that an answer and I'll mark you as accepted.Triceps
@Kev♦ - Before I make an answer could you please check if this is really true? I think it can be easily done with tcpdump or whatever alternative there is for Windows - just look if you send packets slowly(big delay between consecutive outgoing packets) or remote server is slow(packets arrive with big delay). There is also a possibility that Windows implementation messed up your TCP window settings and delay may be caused by a remote server with high RTT(ping) that is forced to send ACK's after each received data packet.Vary
@Vary this was tried on three different windows server and three different delivery paths (see the fourth paragraph). SwiftMail on the other hand, sends via a TCP socket as well but doesn't have this issue at all).Triceps
Kev♦ - All mail protocols I know(pop3,imap, etc..) run over TCP, so any mail client/server will send/receive via TCP sockets. The problem can be with TCP implementation, for example if something messes up TCP settings and TCP window size becomes for example 1000, then it will be (500KB/1KB)*10ms = 5 seconds of pure waiting when senging 500KB data over connection with RTT=10ms. TCP packet window size can show any decent packet sniffer(tcpdump, etc.).Vary
@Vary - oh I know that. SwiftMail uses a direct TCP socket to the mailserver but it seems to be much more efficient. I am guessing its buffering and internal stream constructs are better thought out. The point being that there's nothing wrong with my windows server(s) and its TCP/IP stack if SwiftMail and Pear Mail are fine (as are CDOSYS and .NET's System.Net,Mail API's which all talk SMTP over TCP). i.e. everything else that talks SMTP is fine, PHP's mail() function is the odd man out.Triceps
C
1

My thoughts would use http://glob.com.au/sendmail/ on Windows with the PEAR Mail class: http://pear.php.net/package/Mail. This could possibly be a workaround for the latency you're experiencing. I'm thinking this won't be able to bypass the buffer, but I think it would be worth a shot.

Also, I have never used it, but I've heard good things about SwiftMailer: http://swiftmailer.org/

Climactic answered 23/9, 2011 at 2:41 Comment(1)
It's ok, I've already recommended SwiftMailer which is pretty darned good.Triceps

© 2022 - 2024 — McMap. All rights reserved.