looking for hints, attaching images to message

Swift Mailer is a fantastic library for sending email with php. Discuss this library or ask any questions about it here.

Moderators: Chris Corbyn, General Moderators

Post Reply
garethjax
Forum Newbie
Posts: 18
Joined: Mon Apr 23, 2007 7:16 am

looking for hints, attaching images to message

Post by garethjax »

hello :)
I've just subscribed to the forum but i've already been reading it for weeks in order to enhance my understanding of swiftmailer.
(which by the way, is truly wonderful! KUDOS!).

Now i've got a problem: i need to send an html-based newsletter, which is usually done by a collegue. I would like to create a "method" such as he can simply fill some files and start the sending without my intervention. At the moment, when i need to attach images, i get his html code and insert manually the php code needed to attach images. Which is ok when you have 3 images but it's kinda boring when you have 20... or 50. :wink:

So i'm wondering how can i make the code to dinamically load and attach all the images !
If the argument has already been discussed in the past, please point me to it :)

thanks in advance!
User avatar
Oren
DevNet Resident
Posts: 1640
Joined: Fri Apr 07, 2006 5:13 am
Location: Israel

Post by Oren »

Your question is not clear to me. What do you mean by attaching images dynamically to the html? Where are the images coming from?

Anyways, this might help a bit: http://www.swiftmailer.org/wikidocs/v3/ ... /form2mail
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

I know what you're asking but it's tricky. I'm going to be generous and write a class right now in this thread to deal with this since it's cleaner than working with $GLOBALS directly.

Maybe something like this, assuming PHP5.

Message body:

Code: Select all

Here's a <img src="/actual/path/on/server.jpeg" alt="foo" /> image.

And another <img src="/another/path/on/server.jpeg" alt="bar" />
Snippet (untested). It could be more basic than this but I'm taking into account duplicates:

Code: Select all

<?php

/**
 * Untested - Should turn <img /> tags into embedded images.
 * @author Chris Corbyn
 * @license Free for all
 */
class ImageEmbedder
{
  /**
   * The message object
   * @var Swift_Message
   */
  protected $message;
  /**
   * The mime part
   * @var Swift_Message_Part
   */
  protected $part;
  /**
   * A little registry of images
   * @var array
   */
  protected $images = array();
  
  /**
   * Ctor.
   * Creates the converter.
   * @param Swift_Message
   */
  public function __construct(Swift_Message $message)
  {
    $this->setMessage($message);
  }
  /**
   * Set the message object.
   * @param Swift_Message
   */
  public function setMessage(Swift_Message $message)
  {
    $this->message = $message;
  }
  /**
   * Get the message object.
   * @return Swift_Message
   */
  public function getMessage()
  {
    return $this->message;
  }
  /**
   * Set the mime part.
   * @param Swift_Message_Part
   */
  public function setPart(Swift_Message_Part $part)
  {
    $this->part = $part;
  }
  /**
   * Get the mime part.
   * @return Swift_Message_Part
   */
  public function getPart()
  {
    return $this->part;
  }
  /**
   * Get, or lazy load an image (factory/registry method).
   * @param string Path to file on disk
   * @return Swift_Message_Image
   */
  public function getImage($path)
  {
    //If one is already created, use it
    if (array_key_exists($path, $this->images)) {
      return $this->images[$path];
    }
    //Otherwise make one and register it
    else {
        $image = new Swift_Message_Image(new Swift_File($path));
        $this->registerImage($path, $image);
        return $image;
    }
  }
  /**
   * Register a newly created image against it's path on disk.
   * @param string Path to file on disk
   * @param Swift_Message_Image The image object
   */
  public function registerImage($path, Swift_Message_Image $image)
  {
    $this->images[$path] = $image;
    $message = $this->getMessage();
    $message->attach($image);
  }
  /**
   * Callback for replacements.
   * Attaches real images if it can.
   * @param array PCRE backreferences.
   * @return string
   */
  protected function embedImage($matches)
  {
    $ret = $matches[1];
    $path = realpath($matches[2]);
    if (file_exists($path)) {
      $image = $this->getImage($path);
      $src = "cid:" . $image->getContentId();
      $ret .= $src;
    }
    else $ret .= $matches[2];
    $ret .= $matches[3];
    return $ret;
  }
  /**
   * Run the converter on the current mime part.
   */
  public function execute()
  {
    if (!$this->getPart()) {
      throw new Exception("You must provide the part to scan using setPart() before invoking execute()");
    }
    $part = $this->getPart();
    $body = $part->getBody();
    $bodyConverted = preg_replace_callback('~(<img\s+[^>]*src=")([^"]+)("[^>]*>)~is', array($this, 'embedImage'), $body);
    $part->setBody($bodyConverted);
  }
}
Usage:

Code: Select all

$message = new Swift_Message("Your subject");
$part = new Swift_Message_Part($html_body_here, "text/html");
$message->attach($part);

//Now get ready for the conversion

$converter = new ImageEmbedder($message);
$converter->setPart($part);
$converter->execute();

//Optionally process more parts
//$converter->setPart($some_other_part);
//$converer->execute();

$swift->send($message, .... );
I just wrote that code in the thread in the last few mins before I leave work so expect it to not work until I get chance to fix any issues with it.

I could actually write a plugin to do this.

EDIT | I had forgotten to actually attach the image. Fixed that.
EDIT 2 | Changed something else to avoid duplicates.
garethjax
Forum Newbie
Posts: 18
Joined: Mon Apr 23, 2007 7:16 am

Post by garethjax »

Oren wrote:Your question is not clear to me. What do you mean by attaching images dynamically to the html? Where are the images coming from?

Anyways, this might help a bit: http://www.swiftmailer.org/wikidocs/v3/ ... /form2mail
d11wtq did the unthinkable and is writing a class, i was expecting only some guidelines :D

My problem is that my collegue usually load those images on a remote webserver and send the mail with another system written in perl (years ago).These emails have the images linked remotely so it's quite light, yet some email clients block it.

We would like to try html emailing with attached images and i was wondering how to do that work when you have many images (buttons, rounded corners, etc.) that are of small size but they need to be attached too.
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

With a little modification, this script could scan for all "src" and "href" attributes in any element and attach the files. The first snippet I posted actually does this just for <img> tags.

If you need the files to be attached from a remote location, the getImage() method will need to be modified to first download the files with file_get_contents() and then written to disk with file_put_contents() before being attached. If the files are already on the same server then it should just work.

I can write a plugin for this later but I don't have time just yet. Too busy drinking tea after my bike ride home in the rain :P I find this regex type stuff pretty trivial so a plugin is certainly doable without much hassle.
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

Ok, I've gone whole-hog and have written a full-blown plugin to do this same job, except that it also supports remote files now.

If anyone wants to test before I release, be my guest, you'll be doing me a favour since I got halfway in without unit testing it and have completed the whole ~500 line plugin with nothing but acceptance testing. I'm sure it's fine though.

For PHP5:

Code: Select all

<?php

/**
 * A Swift Mailer plugin to download remote images and stylesheets then embed them.
 * This also embeds local files from disk.
 * Please read the LICENSE file
 * @package Swift_Plugin
 * @author Chris Corbyn <chris@w3style.co.uk>
 * @license GNU Lesser General Public License
 */

require_once dirname(__FILE__) . "/../ClassLoader.php";
Swift_ClassLoader::load("Swift_Events_BeforeSendListener");

/**
 * Swift FileEmbedder Plugin to embed remote files.
 * Scans a Swift_Message instance for remote files and then embeds them before sending.
 * This also embeds local files from disk.
 * @package Swift_Plugin
 * @author Chris Corbyn <chris@w3style.co.uk>
 */
class Swift_Plugin_FileEmbedder implements Swift_Events_BeforeSendListener
{
  /**
   * True if remote files will be embedded.
   * @var boolean
   */
  protected $embedRemoteFiles = true;
  /**
   * True if local files will be embedded.
   * @var boolean
   */
  protected $embedLocalFiles = true;
  /**
   * (X)HTML tag defintions listing allowed attributes and extensions.
   * @var array
   */
  protected $definitions = array(
    "img" => array(
      "attributes" => array("src"),
      "extensions" => array("gif", "png", "jpg",  "jpeg", "pjpeg")
    ),
    "link" => array(
      "attributes" => array("href"),
      "extensions" => array("css")
    ),
    "script" => array(
      "attributes" => array("src"),
      "extensions" => array("js")
    ));
  /**
   * Protocols which may be used to download a remote file.
   * @var array
   */
  protected $protocols = array(
    "http" => "http",
    "https" => "https",
    "ftp" => "ftp"
  );
  /**
   * A PCRE regexp which will be passed via sprintf() to produce a complete pattern.
   * @var string
   */
  protected $remoteFilePatternFormat = "~
    (<(?:%s)\\s+[^>]*?                   #Opening tag followed by (possible) attributes
    (?:%s)=((?:\"|')?))                  #Permitted attributes followed by (possible) quotation marks
    ((?:%s)://[\\x01-\\x7F]*?(?:%s)?)    #Remote URL (matching a permitted protocol)
    (\\2[^>]*>)                          #Remaining attributes followed by end of tag
    ~isx";
  /**
   * A PCRE regexp which will be passed via sprintf() to produce a complete pattern.
   * @var string
   */
  protected $localFilePatternFormat = "~
    (<(?:%s)\\s+[^>]*?                   #Opening tag followed by (possible) attributes
    (?:%s)=((?:\"|')?))                  #Permitted attributes followed by (possible) quotation marks
    (/[\\x01-\\x7F]*?(?:%s)?)            #Local, absolute path
    (\\2[^>]*>)                          #Remaining attributes followed by end of tag
    ~isx";
  /**
   * A list of extensions mapping to their usual MIME types.
   * @var array
   */
  protected $mimeTypes = array(
    "gif" => "image/gif",
    "png" => "image/png",
    "jpeg" => "image/jpeg",
    "jpg" => "image/jpeg",
    "pjpeg" => "image/pjpeg",
    "js" => "text/javascript",
    "css" => "text/css");
  /**
   * Child IDs of files already embedded.
   * @var array
   */
  protected $registeredFiles = array();
  
  /**
   * Get the MIME type based upon the extension.
   * @param string The extension (sans the dot).
   * @return string
   */
  public function getType($ext)
  {
    $ext = strtolower($ext);
    if (isset($this->mimeTypes[$ext]))
    {
      return $this->mimeTypes[$ext];
    }
    else return null;
  }
  /**
   * Add a new MIME type defintion (or overwrite an existing one).
   * @param string The extension (sans the dot)
   * @param string The MIME type (e.g. image/jpeg)
   */
  public function addType($ext, $type)
  {
    $this->mimeTypes[strtolower($ext)] = strtolower($type);
  }
  /**
   * Set the PCRE pattern which finds -full- HTML tags and copies the path for a local file into a backreference.
   * The pattern contains three %s replacements for sprintf().
   * First replacement is the tag name (e.g. img)
   * Second replacement is the attribute name (e.g. src)
   * Third replacement is the file extension (e.g. jpg)
   * This pattern should contain the full URL in backreference index 3.
   * @param string sprintf() format string containing a PCRE regexp.
   */
  public function setLocalFilePatternFormat($format)
  {
    $this->localFilePatternFormat = $format;
  }
  /**
   * Gets the sprintf() format string for the PCRE pattern to scan for remote files.
   * @return string
   */
  public function getLocalFilePatternFormat()
  {
    return $this->localFilePatternFormat;
  }
  /**
   * Set the PCRE pattern which finds -full- HTML tags and copies the URL for the remote file into a backreference.
   * The pattern contains four %s replacements for sprintf().
   * First replacement is the tag name (e.g. img)
   * Second replacement is the attribute name (e.g. src)
   * Third replacement is the protocol (e.g. http)
   * Fourth replacement is the file extension (e.g. jpg)
   * This pattern should contain the full URL in backreference index 3.
   * @param string sprintf() format string containing a PCRE regexp.
   */
  public function setRemoteFilePatternFormat($format)
  {
    $this->remoteFilePatternFormat = $format;
  }
  /**
   * Gets the sprintf() format string for the PCRE pattern to scan for remote files.
   * @return string
   */
  public function getRemoteFilePatternFormat()
  {
    return $this->remoteFilePatternFormat;
  }
  /**
   * Add a new protocol which can be used to download files.
   * Protocols should not include the "://" portion.  This method expects alphanumeric characters only.
   * @param string The protocol name (e.g. http or ftp)
   */
  public function addProtocol($prot)
  {
    $prot = strtolower($prot);
    $this->protocols[$prot] = $prot;
  }
  /**
   * Remove a protocol from the list of allowed protocols once added.
   * @param string The name of the protocol (e.g. http)
   */
  public function removeProtocol($prot)
  {
    unset($this->protocols[strtolower($prot)]);
  }
  /**
   * Get a list of all registered protocols.
   * @return array
   */
  public function getProtocols()
  {
    return array_values($this->protocols);
  }
  /**
   * Add, or modify a tag definition.
   * This affects how the plugins scans for files to download.
   * @param string The name of a tag to search for (e.g. img)
   * @param string The name of attributes to look for (e.g. src).  You can pass an array if there are multiple possibilities.
   * @param array A list of extensions to allow (sans dot). If there's only one you can just pass a string.
   */
  public function setTagDefinition($tag, $attributes, $extensions)
  {
    $tag = strtolower($tag);
    $attributes = (array)$attributes;
    $extensions = (array)$extensions;
    
    if (empty($tag) || empty($attributes) || empty($extensions))
    {
      return null;
    }
    
    $this->definitions[$tag] = array("attributes" => $attributes, "extensions" => $extensions);
    return true;
  }
  /**
   * Remove a tag definition for remote files.
   * @param string The name of the tag
   */
  public function removeTagDefinition($tag)
  {
    unset($this->definitions[strtolower($tag)]);
  }
  /**
   * Get a tag definition.
   * Returns an array with indexes "attributes" and "extensions".
   * Each element is an array listing the values within it.
   * @param string The name of the tag
   * @return array
   */
  public function getTagDefinition($tag)
  {
    $tag = strtolower($tag);
    if (isset($this->definitions[$tag])) return $this->definitions[$tag];
    else return null;
  }
  /**
   * Get the PCRE pattern for a remote file based on the tag name.
   * @param string The name of the tag
   * @return string
   */
  public function getRemoteFilePattern($tag_name)
  {
    $tag_name = strtolower($tag_name);
    $pattern_format = $this->getRemoteFilePatternFormat();
    if ($def = $this->getTagDefinition($tag_name))
    {
      $pattern = sprintf($pattern_format, $tag_name, implode("|", $def["attributes"]),
        implode("|", $this->getProtocols()), implode("|", $def["extensions"]));
      return $pattern;
    }
    else return null;
  }
  /**
   * Get the PCRE pattern for a local file based on the tag name.
   * @param string The name of the tag
   * @return string
   */
  public function getLocalFilePattern($tag_name)
  {
    $tag_name = strtolower($tag_name);
    $pattern_format = $this->getLocalFilePatternFormat();
    if ($def = $this->getTagDefinition($tag_name))
    {
      $pattern = sprintf($pattern_format, $tag_name, implode("|", $def["attributes"]),
        implode("|", $def["extensions"]));
      return $pattern;
    }
    else return null;
  }
  /**
   * Register a file which has been downloaded so it doesn't need to be downloaded twice.
   * @param string The remote URL
   * @param string The ID as attached in the message
   * @param Swift_Message_EmbeddedFile The file object itself
   */
  public function registerFile($url, $cid, $file)
  {
    $url = strtolower($url);
    if (!isset($this->registeredFiles[$url])) $this->registeredFiles[$url] = array("cids" => array(), "obj" => null);
    $this->registeredFiles[$url]["cids"][] = $cid;
    if (empty($this->registeredFiles[$url]["obj"])) $this->registeredFiles[$url]["obj"] = $file;
  }
  /**
   * Turn on or off remote file embedding.
   * @param boolean
   */
  public function setEmbedRemoteFiles($set)
  {
    $this->embedRemoteFiles = (bool)$set;
  }
  /**
   * Returns true if remote files can be embedded, or false if not.
   * @return boolean
   */
  public function getEmbedRemoteFiles()
  {
    return $this->embedRemoteFiles;
  }
  /**
   * Turn on or off local file embedding.
   * @param boolean
   */
  public function setEmbedLocalFiles($set)
  {
    $this->embedLocalFiles = (bool)$set;
  }
  /**
   * Returns true if local files can be embedded, or false if not.
   * @return boolean
   */
  public function getEmbedLocalFiles()
  {
    return $this->embedLocalFiles;
  }
  /**
   * Callback method for preg_replace().
   * Embeds files which have been found during scanning.
   * @param array Backreferences from preg_replace()
   * @return string The tag with it's URL replaced with a CID
   */
  protected function embedRemoteFile($matches)
  {
    $url = preg_replace("~^([^#]+)#.*\$~s", "\$1", $matches[3]);
    $bits = parse_url($url);
    $ext = preg_replace("~^.*?\\.([^\\.]+)\$~s", "\$1", $bits["path"]);
    
    $lower_url = strtolower($url);
    if (array_key_exists($lower_url, $this->registeredFiles))
    {
      $registered = $this->registeredFiles[$lower_url];
      foreach ($registered["cids"] as $cid)
      {
        if ($this->message->hasChild($cid))
        {
          return $matches[1] . $cid . $matches[4];
        }
      }
      //If we get here the file is downloaded, but not embedded
      $cid = $this->message->attach($registered["obj"]);
      $this->registerFile($url, $cid, $registered["obj"]);
      return $matches[1] . $cid . $matches[4];
    }
    $magic_quotes = get_magic_quotes_runtime();
    set_magic_quotes_runtime(0);
    $filedata = @file_get_contents($url);
    if (!$filedata)
    {
      return $matches[1] . $matches[3] . $matches[4];
    }
    set_magic_quotes_runtime($magic_quotes);
    $filename = preg_replace("~^.*/([^/]+)\$~s", "\$1", $url);
    $att = new Swift_Message_EmbeddedFile($filedata, $filename, $this->getType($ext));
    $id = $this->message->attach($att);
    $this->registerFile($url, $id, $att);
    return $matches[1] . $id . $matches[4];
  }
  /**
   * Callback method for preg_replace().
   * Embeds files which have been found during scanning.
   * @param array Backreferences from preg_replace()
   * @return string The tag with it's path replaced with a CID
   */
  protected function embedLocalFile($matches)
  {
    $path = realpath($matches[3]);
    if (!$path)
    {
      return $matches[1] . $matches[3] . $matches[4];
    }
    $ext = preg_replace("~^.*?\\.([^\\.]+)\$~s", "\$1", $path);
    
    $lower_path = strtolower($path);
    if (array_key_exists($lower_path, $this->registeredFiles))
    {
      $registered = $this->registeredFiles[$lower_path];
      foreach ($registered["cids"] as $cid)
      {
        if ($this->message->hasChild($cid))
        {
          return $matches[1] . $cid . $matches[4];
        }
      }
      //If we get here the file is downloaded, but not embedded
      $cid = $this->message->attach($registered["obj"]);
      $this->registerFile($path, $cid, $registered["obj"]);
      return $matches[1] . $cid . $matches[4];
    }
    $filename = basename($path);
    $att = new Swift_Message_EmbeddedFile(new Swift_File($path), $filename, $this->getType($ext));
    $id = $this->message->attach($att);
    $this->registerFile($path, $id, $att);
    return $matches[1] . $id . $matches[4];
  }
  /**
   * Empty out the cache of registered files.
   */
  public function clearCache()
  {
    $this->registeredFiles = null;
    $this->registeredFiles = array();
  }
  /**
   * Swift's BeforeSendListener required method.
   * Runs just before Swift sends a message.  Here is where we do all the replacements.
   * @param Swift_Events_SendEvent
   */
  public function beforeSendPerformed(Swift_Events_SendEvent $e)
  {
    $this->message = $e->getMessage();
    
    foreach ($this->message->listChildren() as $id)
    {
      $part = $this->message->getChild($id);
      $body = $part->getData();
      if (!is_string($body) || substr(strtolower($part->getContentType()), 0, 5) != "text/") continue;
      
      foreach ($this->definitions as $tag_name => $def)
      {
        if ($this->getEmbedRemoteFiles())
        {
          $re = $this->getRemoteFilePattern($tag_name);
          $body = preg_replace_callback($re, array($this, "embedRemoteFile"), $body);
        }
        
        if ($this->getEmbedLocalFiles())
        {
          $re = $this->getLocalFilePattern($tag_name);
          $body = preg_replace_callback($re, array($this, "embedLocalFile"), $body);
        }
      }
      
      $part->setData($body);
    }
  }
}
For PHP4:

Code: Select all

<?php

/**
 * A Swift Mailer plugin to download remote images and stylesheets then embed them.
 * This also embeds local files from disk.
 * Please read the LICENSE file
 * @package Swift_Plugin
 * @author Chris Corbyn <chris@w3style.co.uk>
 * @license GNU Lesser General Public License
 */


/**
 * Swift FileEmbedder Plugin to embed remote files.
 * Scans a Swift_Message instance for remote files and then embeds them before sending.
 * This also embeds local files from disk.
 * @package Swift_Plugin
 * @author Chris Corbyn <chris@w3style.co.uk>
 */
class Swift_Plugin_FileEmbedder extends Swift_Events_Listener
{
  /**
   * True if remote files will be embedded.
   * @var boolean
   */
  var $embedRemoteFiles = true;
  /**
   * True if local files will be embedded.
   * @var boolean
   */
  var $embedLocalFiles = true;
  /**
   * (X)HTML tag defintions listing allowed attributes and extensions.
   * @var array
   */
  var $definitions = array(
    "img" => array(
      "attributes" => array("src"),
      "extensions" => array("gif", "png", "jpg",  "jpeg", "pjpeg")
    ),
    "link" => array(
      "attributes" => array("href"),
      "extensions" => array("css")
    ),
    "script" => array(
      "attributes" => array("src"),
      "extensions" => array("js")
    ));
  /**
   * Protocols which may be used to download a remote file.
   * @var array
   */
  var $protocols = array(
    "http" => "http",
    "https" => "https",
    "ftp" => "ftp"
  );
  /**
   * A PCRE regexp which will be passed via sprintf() to produce a complete pattern.
   * @var string
   */
  var $remoteFilePatternFormat = "~
    (<(?:%s)\\s+[^>]*?                   #Opening tag followed by (possible) attributes
    (?:%s)=((?:\"|')?))                  #Permitted attributes followed by (possible) quotation marks
    ((?:%s)://[\\x01-\\x7F]*?(?:%s)?)    #Remote URL (matching a permitted protocol)
    (\\2[^>]*>)                          #Remaining attributes followed by end of tag
    ~isx";
  /**
   * A PCRE regexp which will be passed via sprintf() to produce a complete pattern.
   * @var string
   */
  var $localFilePatternFormat = "~
    (<(?:%s)\\s+[^>]*?                   #Opening tag followed by (possible) attributes
    (?:%s)=((?:\"|')?))                  #Permitted attributes followed by (possible) quotation marks
    (/[\\x01-\\x7F]*?(?:%s)?)            #Local, absolute path
    (\\2[^>]*>)                          #Remaining attributes followed by end of tag
    ~isx";
  /**
   * A list of extensions mapping to their usual MIME types.
   * @var array
   */
  var $mimeTypes = array(
    "gif" => "image/gif",
    "png" => "image/png",
    "jpeg" => "image/jpeg",
    "jpg" => "image/jpeg",
    "pjpeg" => "image/pjpeg",
    "js" => "text/javascript",
    "css" => "text/css");
  /**
   * Child IDs of files already embedded.
   * @var array
   */
  var $registeredFiles = array();
  
  /**
   * Get the MIME type based upon the extension.
   * @param string The extension (sans the dot).
   * @return string
   */
  function getType($ext)
  {
    $ext = strtolower($ext);
    if (isset($this->mimeTypes[$ext]))
    {
      return $this->mimeTypes[$ext];
    }
    else return null;
  }
  /**
   * Add a new MIME type defintion (or overwrite an existing one).
   * @param string The extension (sans the dot)
   * @param string The MIME type (e.g. image/jpeg)
   */
  function addType($ext, $type)
  {
    $this->mimeTypes[strtolower($ext)] = strtolower($type);
  }
  /**
   * Set the PCRE pattern which finds -full- HTML tags and copies the path for a local file into a backreference.
   * The pattern contains three %s replacements for sprintf().
   * First replacement is the tag name (e.g. img)
   * Second replacement is the attribute name (e.g. src)
   * Third replacement is the file extension (e.g. jpg)
   * This pattern should contain the full URL in backreference index 3.
   * @param string sprintf() format string containing a PCRE regexp.
   */
  function setLocalFilePatternFormat($format)
  {
    $this->localFilePatternFormat = $format;
  }
  /**
   * Gets the sprintf() format string for the PCRE pattern to scan for remote files.
   * @return string
   */
  function getLocalFilePatternFormat()
  {
    return $this->localFilePatternFormat;
  }
  /**
   * Set the PCRE pattern which finds -full- HTML tags and copies the URL for the remote file into a backreference.
   * The pattern contains four %s replacements for sprintf().
   * First replacement is the tag name (e.g. img)
   * Second replacement is the attribute name (e.g. src)
   * Third replacement is the protocol (e.g. http)
   * Fourth replacement is the file extension (e.g. jpg)
   * This pattern should contain the full URL in backreference index 3.
   * @param string sprintf() format string containing a PCRE regexp.
   */
  function setRemoteFilePatternFormat($format)
  {
    $this->remoteFilePatternFormat = $format;
  }
  /**
   * Gets the sprintf() format string for the PCRE pattern to scan for remote files.
   * @return string
   */
  function getRemoteFilePatternFormat()
  {
    return $this->remoteFilePatternFormat;
  }
  /**
   * Add a new protocol which can be used to download files.
   * Protocols should not include the "://" portion.  This method expects alphanumeric characters only.
   * @param string The protocol name (e.g. http or ftp)
   */
  function addProtocol($prot)
  {
    $prot = strtolower($prot);
    $this->protocols[$prot] = $prot;
  }
  /**
   * Remove a protocol from the list of allowed protocols once added.
   * @param string The name of the protocol (e.g. http)
   */
  function removeProtocol($prot)
  {
    unset($this->protocols[strtolower($prot)]);
  }
  /**
   * Get a list of all registered protocols.
   * @return array
   */
  function getProtocols()
  {
    return array_values($this->protocols);
  }
  /**
   * Add, or modify a tag definition.
   * This affects how the plugins scans for files to download.
   * @param string The name of a tag to search for (e.g. img)
   * @param string The name of attributes to look for (e.g. src).  You can pass an array if there are multiple possibilities.
   * @param array A list of extensions to allow (sans dot). If there's only one you can just pass a string.
   */
  function setTagDefinition($tag, $attributes, $extensions)
  {
    $tag = strtolower($tag);
    $attributes = (array)$attributes;
    $extensions = (array)$extensions;
    
    if (empty($tag) || empty($attributes) || empty($extensions))
    {
      return null;
    }
    
    $this->definitions[$tag] = array("attributes" => $attributes, "extensions" => $extensions);
    return true;
  }
  /**
   * Remove a tag definition for remote files.
   * @param string The name of the tag
   */
  function removeTagDefinition($tag)
  {
    unset($this->definitions[strtolower($tag)]);
  }
  /**
   * Get a tag definition.
   * Returns an array with indexes "attributes" and "extensions".
   * Each element is an array listing the values within it.
   * @param string The name of the tag
   * @return array
   */
  function getTagDefinition($tag)
  {
    $tag = strtolower($tag);
    if (isset($this->definitions[$tag])) return $this->definitions[$tag];
    else return null;
  }
  /**
   * Get the PCRE pattern for a remote file based on the tag name.
   * @param string The name of the tag
   * @return string
   */
  function getRemoteFilePattern($tag_name)
  {
    $tag_name = strtolower($tag_name);
    $pattern_format = $this->getRemoteFilePatternFormat();
    if ($def = $this->getTagDefinition($tag_name))
    {
      $pattern = sprintf($pattern_format, $tag_name, implode("|", $def["attributes"]),
        implode("|", $this->getProtocols()), implode("|", $def["extensions"]));
      return $pattern;
    }
    else return null;
  }
  /**
   * Get the PCRE pattern for a local file based on the tag name.
   * @param string The name of the tag
   * @return string
   */
  function getLocalFilePattern($tag_name)
  {
    $tag_name = strtolower($tag_name);
    $pattern_format = $this->getLocalFilePatternFormat();
    if ($def = $this->getTagDefinition($tag_name))
    {
      $pattern = sprintf($pattern_format, $tag_name, implode("|", $def["attributes"]),
        implode("|", $def["extensions"]));
      return $pattern;
    }
    else return null;
  }
  /**
   * Register a file which has been downloaded so it doesn't need to be downloaded twice.
   * @param string The remote URL
   * @param string The ID as attached in the message
   * @param Swift_Message_EmbeddedFile The file object itself
   */
  function registerFile($url, $cid, &$file)
  {
    $url = strtolower($url);
    if (!isset($this->registeredFiles[$url])) $this->registeredFiles[$url] = array("cids" => array(), "obj" => null);
    $this->registeredFiles[$url]["cids"][] = $cid;
    if (empty($this->registeredFiles[$url]["obj"])) $this->registeredFiles[$url]["obj"] =& $file;
  }
  /**
   * Turn on or off remote file embedding.
   * @param boolean
   */
  function setEmbedRemoteFiles($set)
  {
    $this->embedRemoteFiles = (bool)$set;
  }
  /**
   * Returns true if remote files can be embedded, or false if not.
   * @return boolean
   */
  function getEmbedRemoteFiles()
  {
    return $this->embedRemoteFiles;
  }
  /**
   * Turn on or off local file embedding.
   * @param boolean
   */
  function setEmbedLocalFiles($set)
  {
    $this->embedLocalFiles = (bool)$set;
  }
  /**
   * Returns true if local files can be embedded, or false if not.
   * @return boolean
   */
  function getEmbedLocalFiles()
  {
    return $this->embedLocalFiles;
  }
  /**
   * Callback method for preg_replace().
   * Embeds files which have been found during scanning.
   * @param array Backreferences from preg_replace()
   * @return string The tag with it's URL replaced with a CID
   */
  function embedRemoteFile($matches)
  {
    $url = preg_replace("~^([^#]+)#.*\$~s", "\$1", $matches[3]);
    $bits = parse_url($url);
    $ext = preg_replace("~^.*?\\.([^\\.]+)\$~s", "\$1", $bits["path"]);
    
    $lower_url = strtolower($url);
    if (array_key_exists($lower_url, $this->registeredFiles))
    {
      $registered =& $this->registeredFiles[$lower_url];
      foreach ($registered["cids"] as $cid)
      {
        if ($this->message->hasChild($cid))
        {
          return $matches[1] . $cid . $matches[4];
        }
      }
      //If we get here the file is downloaded, but not embedded
      $cid = $this->message->attach($registered["obj"]);
      $this->registerFile($url, $cid, $registered["obj"]);
      return $matches[1] . $cid . $matches[4];
    }
    $magic_quotes = get_magic_quotes_runtime();
    set_magic_quotes_runtime(0);
    $filedata = @file_get_contents($url);
    if (!$filedata)
    {
      return $matches[1] . $matches[3] . $matches[4];
    }
    set_magic_quotes_runtime($magic_quotes);
    $filename = preg_replace("~^.*/([^/]+)\$~s", "\$1", $url);
    $att =& new Swift_Message_EmbeddedFile($filedata, $filename, $this->getType($ext));
    $id = $this->message->attach($att);
    $this->registerFile($url, $id, $att);
    return $matches[1] . $id . $matches[4];
  }
  /**
   * Callback method for preg_replace().
   * Embeds files which have been found during scanning.
   * @param array Backreferences from preg_replace()
   * @return string The tag with it's path replaced with a CID
   */
  function embedLocalFile($matches)
  {
    $path = realpath($matches[3]);
    if (!$path)
    {
      return $matches[1] . $matches[3] . $matches[4];
    }
    $ext = preg_replace("~^.*?\\.([^\\.]+)\$~s", "\$1", $path);
    
    $lower_path = strtolower($path);
    if (array_key_exists($lower_path, $this->registeredFiles))
    {
      $registered =& $this->registeredFiles[$lower_path];
      foreach ($registered["cids"] as $cid)
      {
        if ($this->message->hasChild($cid))
        {
          return $matches[1] . $cid . $matches[4];
        }
      }
      //If we get here the file is downloaded, but not embedded
      $cid = $this->message->attach($registered["obj"]);
      $this->registerFile($path, $cid, $registered["obj"]);
      return $matches[1] . $cid . $matches[4];
    }
    $filename = basename($path);
    $att =& new Swift_Message_EmbeddedFile(new Swift_File($path), $filename, $this->getType($ext));
    $id = $this->message->attach($att);
    $this->registerFile($path, $id, $att);
    return $matches[1] . $id . $matches[4];
  }
  /**
   * Empty out the cache of registered files.
   */
  function clearCache()
  {
    $this->registeredFiles = null;
    $this->registeredFiles = array();
  }
  /**
   * Swift's BeforeSendListener required method.
   * Runs just before Swift sends a message.  Here is where we do all the replacements.
   * @param Swift_Events_SendEvent
   */
  function beforeSendPerformed(&$e)
  {
    $this->message =& $e->getMessage();
    
    foreach ($this->message->listChildren() as $id)
    {
      $part =& $this->message->getChild($id);
      $body =& $part->getData();
      if (!is_string($body) || substr(strtolower($part->getContentType()), 0, 5) != "text/") continue;
      
      foreach ($this->definitions as $tag_name => $def)
      {
        if ($this->getEmbedRemoteFiles())
        {
          $re = $this->getRemoteFilePattern($tag_name);
          $body = preg_replace_callback($re, array(&$this, "embedRemoteFile"), $body);
        }
        
        if ($this->getEmbedLocalFiles())
        {
          $re = $this->getLocalFilePattern($tag_name);
          $body = preg_replace_callback($re, array(&$this, "embedLocalFile"), $body);
        }
      }
      
      $part->setData($body);
    }
  }
}
Example use case (in the most basic scenario -- I'll document the advanced stuff in the wiki):

Code: Select all

<?php

error_reporting(E_ALL); ini_set("display_errors", true);

require_once "lib/Swift.php";
require_once "lib/Swift/Connection/SMTP.php";
require_once "lib/Swift/Plugin/FileEmbedder.php";

$swift =& new Swift(new Swift_Connection_SMTP("smtp.server.tld"));

//Just attach the plugin... simple
$plugin =& new Swift_Plugin_FileEmbedder();
//You can add your own definitions if needed (but the most common ones are already added)
$plugin->setTagDefinition("td", "background", array("gif", "png"));

$swift->attachPlugin($plugin, "file_embedder");

$message =& new Swift_Message("Example again");

$part =& new Swift_Message_Part("This is a remote file

Here: <img title=\"foo\" src=\"http://www.google.co.uk/intl/en_uk/images/logo.gif\" alt=\"foobar\" />

and this one here too <img src=\"http://www.swiftmailer.org/images/swift_logo2.gif\" /> but I'm not sure
if it actually works fully yet :-\\", "text/html");

$message->attach($part);

$swift->send($message, "user@address.tld", "user@address.tld");
Post Reply