Skip to content

Instantly share code, notes, and snippets.

@thomasfw
Last active March 20, 2024 23:47
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thomasfw/5df1a041fd8f9c939ef9d88d887ce023 to your computer and use it in GitHub Desktop.
Save thomasfw/5df1a041fd8f9c939ef9d88d887ce023 to your computer and use it in GitHub Desktop.
Enable non-filesystem attachments with wp_mail()
<?php
/**
* Plugin Name: Enhanced WP Mail Attachments
* Plugin URI: https://gist.github.com/thomasfw/5df1a041fd8f9c939ef9d88d887ce023/
* Version: 0.1
*/
/**
* Adds support for defining attachments as data arrays in wp_mail().
* Allows us to send string-based or binary attachments (non-filesystem)
* and gives us more control over the attachment data.
*
* @param array $atts Array of the `wp_mail()` arguments.
* - string|string[] $to Array or comma-separated list of email addresses to send message.
* - string $subject Email subject.
* - string $message Message contents.
* - string|string[] $headers Additional headers.
* - string|string[] $attachments Paths to files to attach.
*
* @see https://gist.github.com/thomasfw/5df1a041fd8f9c939ef9d88d887ce023/
*/
add_filter( 'wp_mail', function( $atts )
{
$attachment_arrays = [];
if ( array_key_exists('attachments', $atts) && isset($atts['attachments']) && $atts['attachments'] )
{
$attachments = $atts['attachments'];
if ( is_array($attachments) && ! empty($attachments) )
{
// Is the $attachments array a single array of attachment data, or an array containing multiple arrays of
// attachment data? (note that the array may also be a one-dimensional array of file paths, as-per default usage).
$is_multidimensional_array = count($attachments) == count($attachments, COUNT_RECURSIVE) ? false : true;
if ( ! $is_multidimensional_array ) $attachments = [ $attachments ];
// Work out which attachments we want to process here. If the value is an array with either
// a 'path' or 'path' key, then we'll process it separately and remove it from the
// $atts['attachments'] so that WP doesn't try to process it in wp_mail().
foreach ( $attachments as $index => $attachment )
{
if ( is_array($attachment) && (array_key_exists('path', $attachment) || array_key_exists('string', $attachment)) )
{
$attachment_arrays[] = $attachment;
if ( $is_multidimensional_array ) {
unset( $atts['attachments'][$index] );
} else {
$atts['attachments'] = [];
}
}
}
}
// Set the $wp_mail_attachments global to our attachment data.
// We'll read this later to check if any extra attachments should
// be added to the email. The value will be reset every time wp_mail()
// is called.
global $wp_mail_attachments;
$wp_mail_attachments = $attachment_arrays;
// We can't use the global $phpmailer to add our attachments directly in the 'wp_mail' filter callback because WP calls $phpmailer->clearAttachments()
// after this filter runs. Instead, we now hook into the 'phpmailer_init' action (triggered right before the email is sent), and read
// the $wp_mail_attachments global to check for any additional attachments to add.
add_action( 'phpmailer_init', function( \PHPMailer\PHPMailer\PHPMailer $phpmailer )
{
// Check the $wp_mail_attachments global for any attachment data, and reset it for good measure.
$attachment_arrays = [];
if ( array_key_exists('wp_mail_attachments', $GLOBALS) )
{
global $wp_mail_attachments;
$attachment_arrays = $wp_mail_attachments;
$wp_mail_attachments = [];
}
// Loop through our attachment arrays and attempt to add them using PHPMailer::addAttachment() or PHPMailer::addStringAttachment():
foreach ( $attachment_arrays as $attachment )
{
$is_filesystem_attachment = array_key_exists( 'path', $attachment ) ? true : false;
try
{
$encoding = $attachment['encoding'] ?? $phpmailer::ENCODING_BASE64;
$type = $attachment['type'] ?? '';
$disposition = $attachment['disposition'] ?? 'attachment';
if ( $is_filesystem_attachment )
{
$phpmailer->addAttachment( ($attachment['path'] ?? null), ($attachment['name'] ?? ''), $encoding, $type, $disposition );
}
else
{
$phpmailer->addStringAttachment( ($attachment['string'] ?? null), ($attachment['filename'] ?? ''), $encoding, $type, $disposition );
}
}
catch ( \PHPMailer\PHPMailer\Exception $e ) { continue; }
}
// var_dump( $phpmailer->getAttachments() ); // Debug the mail attachments, including those parsed by WP.
});
}
return $atts;
});
@flymikeGit
Copy link

PHP Fatal error: Uncaught ErrorException: Attempt to assign property "___wp_attachments" on null in /xxxs/enhanced-wpmail-attachments/enhanced-wpmail-attachments.php:56

At the time apply_filters for wp_mail is called, the global $phpmailer may not have been initialized, or gone missing for some reason. The wp_mail function itself checks and corrects that, if necessary, around line 257 - unfortunately after the filters have been applied.

@thomasfw
Copy link
Author

thomasfw commented Jan 3, 2022

@flymikeGit - I've updated this to set the attachment data elsewhere instead of on the global $phpmailer instance (which as you reported, may or may not be available at the time the filter is applied).

However, this change is untested for now until I get more time to check it. Feel free to try it and let me know if it's working for you.

@flymikeGit
Copy link

Yes. I can confirm that change solves the problem. Thanks!

@robwent
Copy link

robwent commented Feb 28, 2022

Thanks for this!

I think the 'name' and 'filename' parameters are wrong in the examples.

In the foreach loop, files use 'name' and strings use 'filename'.

Switching my string to use 'filename' stopped the attachment coming through as 'noname'.

@thomasfw
Copy link
Author

thomasfw commented Mar 1, 2022

@robwent good catch, examples have been updated!

@soderlind
Copy link

Really nice, this should be in core. You made my day 🥇

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment