diff --git a/fillpdf.module b/fillpdf.module
index 0e2c334a95c7ffd0beedd185395408552e28ce7d..4eb2bacc443f5a1b96f422b3efd4ff0fafde5860 100644
--- a/fillpdf.module
+++ b/fillpdf.module
@@ -227,28 +227,34 @@ function fillpdf_parse_uri() {
  * @param $flatten
  *   Boolean. If TRUE, flatten the PDF so that fields cannot be edited.
  *   Otherwise leave fields editable.
+ * @param $handle
+ *   Boolean. If TRUE, handle the PDF, which usually consists of sending it to
+ *   the users's browser or saving it as a file.
  *
  * @return
- *   Nothing.
+ *   When $handle is FALSE, this function returns the variable it would have
+ *   used to invoke hook_fillpdf_merge_pre_handle().
+ *
+ *   When $handle is TRUE, it returns nothing.
  *
  * @see fillpdf_pdf_link()
  * for $_GET params
  */
-function fillpdf_merge_pdf($fid, $nids = NULL, $webform_arr = NULL, $sample = NULL, $force_download = FALSE, $skip_access_check = FALSE, $flatten = TRUE) {
+function fillpdf_merge_pdf($fid, $nids = NULL, $webform_arr = NULL, $sample = NULL, $force_download = FALSE, $skip_access_check = FALSE, $flatten = TRUE, $handle = TRUE) {
   // Case 1: No $fid
   if (is_null($fid)) {
     drupal_set_message(t('Fill PDF Form ID required to print a PDF.'), 'warning');
     drupal_goto();
   }
 
-  $fillpdf_info = db_query("SELECT title, default_nid, url, destination_path, replacements, destination_redirect FROM {fillpdf_forms} WHERE fid = :fid", array(':fid' => $fid))->fetch();
+  $fillpdf_info = fillpdf_load($fid);
+
   // Case 1.5: $fid is not valid.
   if ($fillpdf_info === FALSE) {
     drupal_set_message(t('Non-existent Fill PDF Form ID.'), 'error');
     drupal_not_found();
     drupal_exit();
   }
-  $fillpdf_info->replacements = _fillpdf_replacements_to_array($fillpdf_info->replacements);
 
   global $user;
 
@@ -462,7 +468,7 @@ function fillpdf_merge_pdf($fid, $nids = NULL, $webform_arr = NULL, $sample = NU
     ));
   }
 
-  // Allow modules to step in here and change the way the PDF is handled
+  // Assemble some metadata that will be useful for the handling phase.
   // @todo: Convert function parameters to use $options
   // and add those into $fillpdf_info.
 
@@ -474,72 +480,144 @@ function fillpdf_merge_pdf($fid, $nids = NULL, $webform_arr = NULL, $sample = NU
     'nodes' => $nodes,
     'webforms' => $webforms,
   );
+  $fillpdf_object->token_objects = $token_objects;
   $fillpdf_object->options = array(
     'download' => $force_download,
     'flatten' => $flatten,
   );
-  module_invoke_all('fillpdf_merge_pre_handle', $fillpdf);
 
+  if ($handle === TRUE) {
+    // Allow modules to step in here and change the way the PDF is handled
+    module_invoke_all('fillpdf_merge_pre_handle', $fillpdf_object);
+
+    // Perform the default action on the PDF - in other words, the one it was
+    // configured to do in the administrative area.
+    fillpdf_merge_handle_pdf($fillpdf_object->info, $fillpdf_object->data, $fillpdf_object->token_objects, 'default', $force_download);
+  }
+
+  // If not handling, then send back all the metadata to the caller.
+  else {
+    return $fillpdf_object;
+  }
+}
+
+/**
+ * Figure out what to do with the PDF and do it.
+ *
+ * @return Nothing.
+ * @param $pdf_info An object containing the loaded record from {fillpdf_forms}.
+ * @param $pdf_data A string containing the content of the merged PDF.
+ * @param $token_objects An array of objects to be used in replacing tokens.
+ * Here, specifically, it's for generating the filename of the handled PDF.
+ * @param $action One of the following keywords: default, download, save,
+ * redirect. These correspond to performing the configured action (from
+ * admin/structure/fillpdf/%), sending the PDF to the user's browser, saving it
+ * to a file, and saving it to a file and then redirecting the user's browser to
+ * the saved file.
+ * @param $force_download If set, this function will always end the request by
+ * sending the filled PDF to the user's browser.
+ */
+function fillpdf_merge_handle_pdf($pdf_info, $pdf_data, $token_objects, $action = 'download', $force_download = FALSE) {
+  if (in_array($action, array('default', 'download', 'save', 'redirect')) === FALSE) {
+    // Do nothing if the function is called with an invalid action.
+    return;
+  }
   // Generate the filename of downloaded PDF from title of the PDF set in
   // admin/structure/fillpdf/%fid
-  $output_name = _fillpdf_process_filename($fillpdf_info->title, $token_objects);
-
-  if (!empty($fillpdf_info->destination_path)) {
-    $destination_path = _fillpdf_process_destination_path($fillpdf_info->destination_path, $token_objects);
-    $path_exists = file_prepare_directory($destination_path, FILE_CREATE_DIRECTORY + FILE_MODIFY_PERMISSIONS);
-    if ($path_exists === FALSE) {
-      watchdog('fillpdf', "The path %destination_path does not exist and could not be
-        automatically created. Therefore, the previous submission was not saved. If
-        the URL contained download=1, then the PDF was still sent to the user's browser.
-        If you were redirecting them to the PDF, they were sent to the homepage instead.
-        If the destination path looks wrong and you have used tokens, check that you have
-        used the correct token and that it is available to Fill PDF at the time of PDF
-        generation.",
-        array('%destination_path' => $destination_path));
+  $output_name = _fillpdf_process_filename($pdf_info->title, $token_objects);
+
+  if ($action == 'default') {
+    // Determine the default action, then re-set $action to that.
+    if (empty($pdf_info->destination_path) === FALSE) {
+      $action = 'save';
     }
     else {
-      // Full steam ahead!
-      $saved_file_path = file_unmanaged_save_data($data, $destination_path . "/$output_name", FILE_EXISTS_RENAME);
-      if ($force_download === FALSE) {
-        if (isset($_GET['destination']) === FALSE) {
-          // Should we send the user directly to the saved PDF? If so, do that.
-          if ($fillpdf_info->destination_redirect) {
-            drupal_goto(file_create_url($saved_file_path));
-          }
+      $action = 'download';
+    }
+  }
+
+  // Initialize variable containing whether or not we send the user's browser to
+  // the saved PDF after saving it (if we are)
+  $redirect_to_file = FALSE;
+
+  // Get a load of this switch...they all just fall through!
+  switch ($action) {
+    case 'redirect':
+      $redirect_to_file = $pdf_info->destination_redirect;
+    case 'save':
+      fillpdf_save_to_file($pdf_info, $pdf_data, $token_objects, $output_name, !$force_download, $redirect_to_file);
+    // Fill PDF classic!
+    case 'download':
+      drupal_add_http_header("Pragma", "public");
+      drupal_add_http_header('Expires', 0);
+      drupal_add_http_header('Cache-Control', 'must-revalidate, post-check=0, pre-check=0');
+      drupal_add_http_header('Content-type', 'application-download');
+      // This must be strlen(), not drupal_strlen() because the length in bytes,
+      // not in characters, is what is needed here.
+      drupal_add_http_header('Content-Length', strlen($pdf_data));
+      drupal_add_http_header('Content-disposition', 'attachment; filename="' . $output_name . '"');
+      drupal_add_http_header('Content-Transfer-Encoding', 'binary');
+      echo $pdf_data;
+      drupal_exit();
+      break;
+  }
+}
+
+function fillpdf_save_to_file($pdf_info, $pdf_data, $token_objects, $output_name, $redirect = TRUE, $redirect_to_file = FALSE, $destination_path_override = NULL) {
+  if (isset($destination_path_override) && empty($destination_path_override) !== FALSE) {
+    $destination_path = $destination_path_override;
+  }
+  if (empty($pdf_info->destination_path) && empty($destination_path_override)) {
+    // If this function is called and the PDF isn't set up with a destination
+    // path, give it one.
+    $destination_path = 'fillpdf';
+  }
+  else {
+    $destination_path = $pdf_info->destination_path;
+  }
+  $destination_path = _fillpdf_process_destination_path($pdf_info->destination_path, $token_objects);
+  $path_exists = file_prepare_directory($destination_path, FILE_CREATE_DIRECTORY + FILE_MODIFY_PERMISSIONS);
+  if ($path_exists === FALSE) {
+    watchdog('fillpdf', "The path %destination_path does not exist and could not be
+      automatically created. Therefore, the previous submission was not saved. If
+      the URL contained download=1, then the PDF was still sent to the user's browser.
+      If you were redirecting them to the PDF, they were sent to the homepage instead.
+      If the destination path looks wrong and you have used tokens, check that you have
+      used the correct token and that it is available to Fill PDF at the time of PDF
+      generation.",
+      array('%destination_path' => $destination_path));
+  }
+  else {
+    // Full steam ahead!
+    $saved_file_path = file_unmanaged_save_data($pdf_data, $destination_path . "/$output_name", FILE_EXISTS_RENAME);
+    if ($redirect === TRUE) {
+      if (isset($_GET['destination']) === FALSE) {
+        // Should we send the user directly to the saved PDF? If so, do that.
+        if ($redirect_to_file) {
+          drupal_goto(file_create_url($saved_file_path));
         }
       }
     }
+  }
 
-    if ($force_download === FALSE) {
-      // Allow the "destination" query string parameter to be used
-      // e.g. fillpdf?nid=1&fid=1&destination=node/1
-      // If no destination is provided, drupal_goto() will send the
-      // user to the front page.
-      drupal_goto();
-    }
+  if ($redirect === TRUE) {
+    // Allow the "destination" query string parameter to be used
+    // e.g. fillpdf?nid=1&fid=1&destination=node/1
+    // If no destination is provided, drupal_goto() will send the
+    // user to the front page.
+    drupal_goto();
   }
 
-  drupal_add_http_header("Pragma", "public");
-  drupal_add_http_header('Expires', 0);
-  drupal_add_http_header('Cache-Control', 'must-revalidate, post-check=0, pre-check=0');
-  drupal_add_http_header('Content-type', 'application-download');
-  // This must be strlen(), not drupal_strlen() because the length in bytes,
-  // not in characters, is what is needed here.
-  drupal_add_http_header('Content-Length', strlen($data));
-  drupal_add_http_header('Content-disposition', 'attachment; filename="' . $output_name . '"');
-  drupal_add_http_header('Content-Transfer-Encoding', 'binary');
-  echo $data;
-  drupal_exit();
+  return $saved_file_path;
 }
 
+// @todo: Put the hooks together
+// @todo: Document hooks
 /**
  * Implementation of fillpdf_merge_pre_handle().
  * Set up the data then invoke the Rules event.
  */
 function fillpdf_fillpdf_merge_pre_handle($fillpdf) {
-  // @todo: Implement module_invoke_all() call in fillpdf_merge_pdf at
-  // appropriate place and implement Rules event invocation here.
-
   rules_invoke_event('fillpdf_merge_pre_handle', $fillpdf);
 }
 
@@ -597,6 +675,10 @@ function _fillpdf_process_filename($original, $token_objects) {
   return $output_name;
 }
 
+function fillpdf_build_filename($original, $token_objects) {
+  return _fillpdf_process_filename($original, $token_objects);
+}
+
 /**
  * Utility function to allow other functions to merge PDFs with the various methods in a consistent way.
  * @param string $method The service or program being used. Possible values: local, remote, pdftk. Currently, only pdftk is supported.
@@ -845,19 +927,24 @@ function _fillpdf_process_destination_path($destination_path, $token_objects) {
 }
 
 function _fillpdf_replacements_to_array($replacements) {
-  $standardized_replacements = str_replace(array("\r\n", "\r"), "\n", $replacements);
-  $lines = explode("\n", $standardized_replacements);
-  $return = array();
-  foreach ($lines as $replacement) {
-    if (!empty($replacement)) {
-      $split = explode('|', $replacement);
-      if (count($split) == 2) { // Sometimes it isn't; don't know why.
-        $return[$split[0]] = preg_replace('|<br />|', '
+  if (empty($replacements) !== TRUE) {
+    $standardized_replacements = str_replace(array("\r\n", "\r"), "\n", $replacements);
+    $lines = explode("\n", $standardized_replacements);
+    $return = array();
+    foreach ($lines as $replacement) {
+      if (!empty($replacement)) {
+        $split = explode('|', $replacement);
+        if (count($split) == 2) { // Sometimes it isn't; don't know why.
+          $return[$split[0]] = preg_replace('|<br />|', '
 ', $split[1]);
+        }
       }
     }
+    return $return;
+  }
+  else {
+    return array();
   }
-  return $return;
 }
 
 /**
@@ -881,3 +968,26 @@ function _fillpdf_transform_field_value($value, $pdf_replacements, $field_replac
   }
 }
 
+/**
+ * Whoa, a load function! Fill PDF is growing up!
+ */
+function fillpdf_load($fid, $reset = FALSE) {
+  static $fillpdf = array();
+  if (isset($fillpdf[$fid]) && $reset === FALSE) {
+    // I'm a placeholder if statement!
+  }
+  else {
+    $fillpdf[$fid] = db_query("SELECT * FROM {fillpdf_forms} WHERE fid = :fid", array(':fid' => $fid))->fetch();
+  }
+  if ($fillpdf[$fid]) {
+    // Turn replacements (textarea content) into an array.
+    $fillpdf[$fid]->replacements = _fillpdf_replacements_to_array($fillpdf[$fid]->replacements);
+  }
+  if ($fillpdf[$fid]) {
+    return $fillpdf[$fid];
+  }
+  else {
+    return FALSE;
+  }
+}
+
diff --git a/fillpdf.rules.inc b/fillpdf.rules.inc
index 5e788878bbb990ee2d95e8dad3802140cfc04ecb..88ac4a4239b4ca4bdddc29f4a3ea2b0545dde082 100644
--- a/fillpdf.rules.inc
+++ b/fillpdf.rules.inc
@@ -19,7 +19,13 @@
  *   See http://drupal.org/node/905632
  */
 function fillpdf_rules_data_info() {
-  return array();
+  return array(
+    'fillpdf' => array(
+      'label' => t('Fill PDF metadata'),
+      'group' => t('Fill PDF'),
+      'property info' => _fillpdf_rules_metadata_info(),
+    ),
+  );
 }
 
 /**
@@ -35,7 +41,7 @@ function fillpdf_rules_event_info() {
     'module' => 'fillpdf',
   );
   return array(
-    'fillpdf_pre_handle' => $defaults + array(
+    'fillpdf_merge_pre_handle' => $defaults + array(
       'label' => t('Filled PDF is about to be handled'),
       'variables' => array(
         'fillpdf' => array('type' => 'fillpdf', 'label' => 'Fill PDF metadata'),
@@ -47,14 +53,95 @@ function fillpdf_rules_event_info() {
 /**
  * Implements hook_rules_action_info().
  * @todo: Define the following actions:
- *   - Fill a PDF
+ *   - Fill a PDF with data from content
+ *   - Fill a PDF with data from Webform submissions
  *   - Send PDF to user's browser
- *   - Save PDF to a file
- *   - Perform the default action on the PDF
+ *   - Generate a Fill PDF link (saves new variable - could be useful for e-mail
+ *   templates and such)
  */
 function fillpdf_rules_action_info() {
-  // @todo: Define the "Save PDF to a file" rule
-  return array();
+  $defaults = array(
+    'group' => t('Fill PDF'),
+  );
+  return array(
+    'fillpdf_load' => $defaults + array(
+      'label' => t('Load a Fill PDF configuration'),
+      'base' => 'fillpdf_rules_action_load_fillpdf',
+      'provides' => array(
+        'fillpdf' => array(
+          'type' => 'fillpdf',
+          'label' => t('Fill PDF metadata'),
+        ),
+      ),
+      'parameter' => array(
+        'fid' => array(
+          'type' => 'integer',
+          'label' => t('Fill PDF Form ID'),
+        )
+      ),
+    ),
+    'fillpdf_merge_webform' => $defaults + array(
+      'label' => t('Fill a PDF with webform data'),
+      'base' => 'fillpdf_rules_action_merge_webform',
+      'description' => t('Populates the PDF with webform data and updates the
+        Rules variable with all information necessary to handle it.'),
+      'parameter' => array(
+        'fillpdf' => array(
+          'type' => 'fillpdf',
+          'label' => t('Fill PDF metadata'),
+        ),
+        'webform_nid' => array(
+          'type' => 'integer',
+          'label' => t('Webform Node ID'),
+          'optional' => TRUE,
+          'description' => t('If you leave this blank, the <em>Default Node ID</em> from the Fill PDF configuration will be used.'),
+        ),
+        'webform_sids' => array(
+          'type' => 'list<integer>',
+          'label' => t('Webform Submission ID(s)'),
+          'optional' => TRUE,
+          'description' => t('If you leave this blank, the most recent submission will be used. The last ID you specify will be checked first.'),
+        ),
+      ),
+    ),
+    'fillpdf_handle_default' => $defaults + array(
+      'label' => t('Perform the default action on the PDF'),
+      'description' => t('Handle the PDF according to its Fill PDF configuration.'),
+      'base' => 'fillpdf_rules_action_handle_default',
+      'parameter' => array(
+        'fillpdf' => array(
+          'type' => 'fillpdf',
+          'label' => t('Fill PDF metadata'),
+        )
+      ),
+    ),
+    'fillpdf_save_to_file' => $defaults + array(
+      'label' => t('Save PDF to a file'),
+      'base' => 'fillpdf_rules_action_save_to_file',
+      'provides' => array(
+        'fillpdf_saved_file_path' => array(
+          'type' => 'text',
+          'label' => t('Path to saved PDF'),
+        ),
+      ),
+      'parameter' => array(
+        'fillpdf' => array(
+          'type' => 'fillpdf',
+          'label' => t('Fill PDF metadata'),
+        )
+      ),
+    ),
+    'fillpdf_delete_saved_file' => $defaults + array(
+      'label' => t('Delete saved PDF'),
+      'base' => 'fillpdf_rules_action_delete_file',
+      'parameter' => array(
+        'filename' => array(
+          'type' => 'text',
+          'label' => t('Filename of PDF to delete'),
+        ),
+      ),
+    )
+  );
 }
 
 /**
@@ -68,25 +155,68 @@ function fillpdf_rules_condition_info() {
 }
 
 /**
- * Implements hook_default_rules_configuration().
+ * *****************
+ * *Rules callbacks*
+ * *****************
  */
-function fillpdf_rules_default_rules_configuration() {
-  $default_rules = array();
-  // @todo: Export the default rule once I create it via the UI and copy the
-  // code here.
 
-  // For attachment-based rules
-  if (module_exists('mimemail')) {
+// Action callbacks //
+
+function fillpdf_rules_action_load_fillpdf($fid) {
+  $fillpdf = new stdClass;
+  $fillpdf->info = fillpdf_load($fid);
+  return array(
+    'fillpdf' => $fillpdf,
+  );
+}
+
+/**
+ * Populates a loaded Fill PDF configuration's PDF with data.
+ */
+function fillpdf_rules_action_merge_webform($fillpdf, $webform_nid = '', $webform_sids = array()) {
+  $skip_access_check = FALSE;
+  $flatten = TRUE;
+  $webforms = array();
+  foreach ($webform_sids as $sid) {
+    $webforms[] = array('nid' => $webform_nid,
+      'sid' => $sid,
+    );
+  }
 
+  if (empty($webforms) && empty($webform_nid) !== TRUE) {
+    $webforms[0]['nid'] = $webform_nid;
   }
-  return $default_rules;
+
+  // @todo: Parameterize $skip_access_check and $flatten in Rules.
+  $fillpdf = fillpdf_merge_pdf($fillpdf->info->fid, NULL, $webforms, NULL, FALSE, $skip_access_check, $flatten, FALSE);
+  return array('fillpdf' => $fillpdf);
 }
 
 /**
- * *****************
- * *Rules callbacks*
- * *****************
+ * Save the PDF to a file and return the file path.
+ */
+function fillpdf_rules_action_save_to_file($fillpdf) {
+  $saved_file_path = fillpdf_save_to_file($fillpdf->info, $fillpdf->data, $fillpdf->token_objects, _fillpdf_process_filename($fillpdf->info->title, $fillpdf->token_objects), FALSE);
+  return array(
+    'fillpdf_saved_file_path' => $saved_file_path,
+  );
+}
+
+/**
+ * Perform the default action on the PDF. This always ends in a drupal_goto() or a drupal_exit().
  */
+function fillpdf_rules_action_handle_default($fillpdf) {
+  fillpdf_merge_handle_pdf($fillpdf->info, $fillpdf->data, $fillpdf->token_objects, 'default');
+}
 
-// @todo: Write these.
+function fillpdf_rules_action_delete_file($filename) {
+  file_unmanaged_delete($filename);
+}
+
+// Condition callbacks //
+
+// Metadata callbacks //
+function _fillpdf_rules_metadata_info() {
+  return array();
+}
 
diff --git a/fillpdf.rules_defaults.inc b/fillpdf.rules_defaults.inc
new file mode 100644
index 0000000000000000000000000000000000000000..e4a00409eedf4c30e89c7159022658daf98b5806
--- /dev/null
+++ b/fillpdf.rules_defaults.inc
@@ -0,0 +1,48 @@
+<?php
+/**
+ * Implements hook_default_rules_configuration().
+ */
+function fillpdf_default_rules_configuration() {
+  $default_rules = array();
+  // Attach the filled PDF to an e-mail.
+  if (module_exists('mimemail') && module_exists('webform_rules') && module_exists('webform')) {
+    $default_rules['rules_webform_fillpdf_attachment'] = rules_import('{ "rules_send_webform_submission_confirmation_with_filled_pdf_as_attachment" : {
+        "LABEL" : "Send Webform submission confirmation e-mail with filled PDF as attachment",
+        "PLUGIN" : "reaction rule",
+        "ACTIVE" : false,
+        "REQUIRES" : [ "rules", "fillpdf", "mimemail", "webform_rules" ],
+        "ON" : [ "webform_rules_submit" ],
+        "IF" : [ { "data_is" : { "data" : [ "node:nid" ], "value" : "2" } } ],
+        "DO" : [
+          { "fillpdf_load" : {
+              "USING" : { "fid" : "6" },
+              "PROVIDE" : { "fillpdf" : { "fillpdf" : "Fill PDF metadata" } }
+            }
+          },
+          { "fillpdf_merge_webform" : {
+              "fillpdf" : [ "fillpdf" ],
+              "webform_nid" : "2",
+              "webform_sids" : { "value" : [] }
+            }
+          },
+          { "fillpdf_save_to_file" : {
+              "USING" : { "fillpdf" : [ "fillpdf" ] },
+              "PROVIDE" : { "fillpdf_saved_file_path" : { "fillpdf_saved_file_path" : "Path to saved PDF" } }
+            }
+          },
+          { "mimemail" : {
+              "to" : [ "site:mail" ],
+              "subject" : "Copy of completed PDF for your records",
+              "body" : "Thank you for filling out the form on our website. A copy of your completed PDF is attached.",
+              "plaintext" : " Thank you for filling out the form on our website. A copy of your completed PDF is attached.",
+              "attachments" : [ "fillpdf-saved-file-path" ]
+            }
+          },
+          { "fillpdf_delete_saved_file" : { "filename" : [ "fillpdf-saved-file-path" ] } }
+        ]
+      }
+    }');
+  }
+  return $default_rules;
+}
+