Support » Plugins » Hacks » using wp_mail with php-generated attachments

  • hello,

    TLDR version – I had some problem sending e-mails with on-the-fly generated attachments, so here’s what I was trying to do and how I fixed it (end of TLDR)

    I am currently working for non-profit organization and I had to deploy simple form on one of our websites running WP.

    The funny thing to do was generating PDF of what the user posted in the form and sending this PDF as an e-mail attachment to him.

    long story short – as a form input we use si-contact-form (http://wordpress.org/extend/plugins/si-contact-form/)
    and to generate PDFs I chose FPDF (http://www.fpdf.org/).

    FPDF allows me to pass the pdf as a string with
    $string = $pdf->Output("filename-this_is_ignored_when_i_want_string", "S");
    and then I wanted to add that as an attachment.
    unfortunatelly wp_mail doesn’t support adding string attachments, although it uses phpmailer, which itself has this feature
    AddStringAttachment($string,$filename,$encoding,$type);

    So I had to add it manually – in a body part of the message, and of course set the content-type header to multipart/mixed…

    $semirandom = md5(time());
            $header .= 'Content-Type: multipart/mixed; boundary=' . $semirandom . "\r\n";
    
            $message = "--".$semirandom."\r\n";
    
    	$message .= 'Content-type: text/html; charset='. get_option('blog_charset') . $php_eol;
    	//$msg is previously generated message from what admin of the site specifies
            $message .= "\r\n".$msg."\r\n\r\n";
    
            $message .= "--".$semirandom."\r\n";
    
            $message .= "Content-Type: application/octet-stream; name=\"form.pdf\"\r\n";
            $message .= "Content-Disposition: attachment; filename=\"form.pdf\"\r\n";
            $message .= "Content-Transfer-Encoding: base64\r\n\r\n";
            $message .= chunk_split(base64_encode($pdf->Output("","S")));
    
            $message .= "--".$semirandom."--";
    
            @wp_mail($email, $subj, $message, $header);

    which worked fine in almost everything (gmail and yahoo etc. throung webbrowsers, some company mail servers with thunderbird/outlook…)
    until my friend tried it with his university e-mail account forwarded to gmail, which instead of text displayed the actual stuff I put in $message

    --8d5ae687d4dacb540c7bde17ab5e7fef
    Content-type: text/html; charset=UTF-8
    
    hello,
    blah blah text of the e-mail
    
    --8d5ae687d4dacb540c7bde17ab5e7fef
    Content-Type: application/octet-stream; name="form.pdf"
    Content-Disposition: attachment; filename="form.pdf"
    Content-Transfer-Encoding: base64
    
    go8PC9UeXBlIC9QYWdlCi9QYXJlbnQgMSAwIFIKL1Jlc291cmNlcyAy
    IDAgUgovQ29udGVudHMgNCAwIFI+PgplbmRvYmoKNCA... (encoded PDF)
    --8d5ae687d4dacb540c7bde17ab5e7fef--

    So I had to dig. As it turns out, the message had two “content-type: multipart/mixed” headers like this:

    Content-Type: multipart/mixed;
    	 boundary="5b34d68abe22d374e2af04c6783370f2"
    MIME-Version: 1.0
    Content-Transfer-Encoding: 8bit
    Content-Type: multipart/mixed; charset=""

    the first one (that one with boundary, which is !before! MIME-Version: 1.0) is the one I inserted, the second one was inserted by wp_mail().
    And this is not what we want, even if most of mailservers and clients did not have problem with it…

    I found the problem in pluggable.php somewhere around line 410:

    // Set Content-Type and charset
    	// If we don't have a content-type from the input headers
    	if ( !isset( $content_type ) )
    		$content_type = 'text/plain';
    
    	$content_type = apply_filters( 'wp_mail_content_type', $content_type );
    
    	$phpmailer->ContentType = $content_type;
    
    	// Set whether it's plaintext, depending on $content_type
    	if ( 'text/html' == $content_type )
    		$phpmailer->IsHTML( true );
    	// If we don't have a charset from the input headers
    	if ( !isset( $charset ) )
    		$charset = get_bloginfo( 'charset' );
    
    	// Set the content-type and charset
    	$phpmailer->CharSet = apply_filters( 'wp_mail_charset', $charset );
    
    	// Set custom headers
    	if ( !empty( $headers ) ) {
    		foreach( (array) $headers as $name => $content ) {
    			$phpmailer->AddCustomHeader( sprintf( '%1$s: %2$s', $name, $content ) );
    		}
    	if ( false !== stripos( $content_type, 'multipart' ) && ! empty($boundary) )
    			$phpmailer->AddCustomHeader( sprintf( "Content-Type: %s;\n\t boundary=\"%s\"", $content_type, $boundary ) );
    	}

    this code sets the multipart/mixed twice – first one is the $phpmailer->ContentType = $content_type; line, which makes that header below “MIME-Version: 1.0”, but without boundary

    and the second one is the last line of this snippet, which adds it with boundary, but as a custom header which is placed before “MIME-Version: 1.0”

    so a quick fix for me looks like this – it sets the boundary right to to $phpmailer->ContentType and I deleted the second part which adds it as custom header

    //fixed by Martin Svetlik
    	if ( false !== stripos( $content_type, 'multipart' ) && ! empty($boundary) ) {
    		$phpmailer->ContentType = $content_type . "; boundary=" . $boundary;
    	}
    	else {
    		$phpmailer->ContentType = $content_type;
    
    		// Set whether it's plaintext, depending on $content_type
    		if ( 'text/html' == $content_type )
    			$phpmailer->IsHTML( true );
    
    		// If we don't have a charset from the input headers
    		if ( !isset( $charset ) )
    			$charset = get_bloginfo( 'charset' );
    	}
    //end of the fix
    
    	// Set the content-type and charset
    	$phpmailer->CharSet = apply_filters( 'wp_mail_charset', $charset );
    
    	// Set custom headers
    	if ( !empty( $headers ) ) {
    		foreach( (array) $headers as $name => $content ) {
    			$phpmailer->AddCustomHeader( sprintf( '%1$s: %2$s', $name, $content ) );
    		}
    /*		//M.S. deteled this - it's set it several rows up
    		if ( false !== stripos( $content_type, 'multipart' ) && ! empty($boundary) )
    			$phpmailer->AddCustomHeader( sprintf( "Content-Type: %s;\n\t boundary=%s", $content_type, $boundary ) );
    */	}

    this works pretty good, but produces header with “charset” even for multipart messages like

    MIME-Version: 1.0
    Content-Transfer-Encoding: 8bit
    Content-Type: multipart/mixed; boundary=3432f0f547aafa6045243aba478151d3; charset=""

    and it’s not much user-friendly.

    so the ultimate sollution was to add function to work with string attachments –
    the function wp_mail in pluggable.php is now declared as
    function wp_mail( $to, $subject, $message, $headers = '', $attachments = array(), $string_attachments = array() )

    and at the end, where normal attachments are added is my new part which adds string attachments, so it looks like this

    if ( !empty( $attachments ) ) {
    		foreach ( $attachments as $attachment ) {
    			try {
    				$phpmailer->AddAttachment($attachment);
    			} catch ( phpmailerException $e ) {
    				continue;
    			}
    		}
    	}
    
    //added this. Martin Svetlik
    	if ( !empty( $string_attachments ) ) {
    		foreach ( $string_attachments as $str_attachment ) {
    			try {
    				$phpmailer->AddStringAttachment($str_attachment[0], $str_attachment[1], $str_attachment[2], $str_attachment[3]);
    			} catch ( phpmailerException $e ) {
    				continue;
    			}
    		}
    	}

    and it is used like this

    $str_att1 = array();
    	$str_att1[0] = $pdf->Output("","S");
    	$str_att1[1] = "F3_registracia.pdf";
    	$str_att1[2] = "base64";
    	$str_att1[3] = "application/octet-stream";
    
    	$string_att_array = array();
    	$string_att_array[] = $str_att1;
    
    	@wp_mail($email, $subj, $msg, $header, $normal_attachments, $string_att_array);

    hope this helps if somebody has the same problem, and maybe even maked into next version of WP 🙂

  • The topic ‘using wp_mail with php-generated attachments’ is closed to new replies.