diff --git a/feeds.module b/feeds.module
index 20ce26a76db973315eab903310b75f92f0b0680b..7875e60f3f293d7e17a969c18522725620232d33 100644
--- a/feeds.module
+++ b/feeds.module
@@ -928,3 +928,34 @@ function feeds_alter($type, &$data) {
 /**
  * @}
  */
+
+/**
+ * Copy of valid_url() that supports the webcal scheme.
+ *
+ * @see valid_url().
+ *
+ * @todo Replace with valid_url() when http://drupal.org/node/1191252 is fixed.
+ */
+function feeds_valid_url($url, $absolute = FALSE) {
+  if ($absolute) {
+    return (bool) preg_match("
+      /^                                                      # Start at the beginning of the text
+      (?:ftp|https?|feed|webcal):\/\/                         # Look for ftp, http, https, feed or webcal schemes
+      (?:                                                     # Userinfo (optional) which is typically
+        (?:(?:[\w\.\-\+!$&'\(\)*\+,;=]|%[0-9a-f]{2})+:)*      # a username or a username and password
+        (?:[\w\.\-\+%!$&'\(\)*\+,;=]|%[0-9a-f]{2})+@          # combination
+      )?
+      (?:
+        (?:[a-z0-9\-\.]|%[0-9a-f]{2})+                        # A domain name or a IPv4 address
+        |(?:\[(?:[0-9a-f]{0,4}:)*(?:[0-9a-f]{0,4})\])         # or a well formed IPv6 address
+      )
+      (?::[0-9]+)?                                            # Server port number (optional)
+      (?:[\/|\?]
+        (?:[\w#!:\.\?\+=&@$'~*,;\/\(\)\[\]\-]|%[0-9a-f]{2})   # The path and query (optional)
+      *)?
+    $/xi", $url);
+  }
+  else {
+    return (bool) preg_match("/^(?:[\w#!:\.\?\+=&@$'~*,;\/\(\)\[\]\-]|%[0-9a-f]{2})+$/i", $url);
+  }
+}
diff --git a/libraries/http_request.inc b/libraries/http_request.inc
index 24cc43191788a5f1650a19f9d11df214aea445f2..39b25c6dde65564f5fe676c19118e038d4ef7d16 100644
--- a/libraries/http_request.inc
+++ b/libraries/http_request.inc
@@ -121,20 +121,36 @@ function http_request_get($url, $username = NULL, $password = NULL, $accept_inva
     }
   }
 
+  // Support the 'feed' and 'webcal' schemes by converting them into 'http'.
+  $url = strtr($url, array('feed://' => 'http://', 'webcal://' => 'http://'));
+
   if ($curl) {
     $headers[] = 'User-Agent: Drupal (+http://drupal.org/)';
     $result = new stdClass();
 
-    // Only download via cURL if we can validate the scheme to be either http or
-    // https.
-    // Validate in PHP, CURLOPT_PROTOCOLS is only supported with cURL 7.19.4
+    // Parse the URL and make sure we can handle the schema.
+    // cURL can only support either http:// or https://.
+    // CURLOPT_PROTOCOLS is only supported with cURL 7.19.4
     $uri = parse_url($url);
-    if (isset($uri['scheme']) && $uri['scheme'] != 'http' && $uri['scheme'] != 'https') {
-      $result->error = 'invalid schema '. $uri['scheme'];
-      $result->code = -1003; // This corresponds to drupal_http_request()
+    if (!isset($uri['scheme'])) {
+      $result->error = 'missing schema';
+      $result->code = -1002;
     }
     else {
+      switch ($uri['scheme']) {
+        case 'http':
+        case 'https':
+          // Valid scheme.
+          break;
+        default:
+          $result->error = 'invalid schema ' . $uri['scheme'];
+          $result->code = -1003;
+          break;
+      }
+    }
 
+    // If the scheme was valid, continue to request the feed using cURL.
+    if (empty($result->error)) {
       $download = curl_init($url);
       curl_setopt($download, CURLOPT_FOLLOWLOCATION, TRUE);
       if (!empty($username)) {
diff --git a/plugins/FeedsHTTPFetcher.inc b/plugins/FeedsHTTPFetcher.inc
index c8fccfb6528dd6911de55259473611de9e219913..b48d8a1ef4bff4bf343ae3ef248b263619110e78 100644
--- a/plugins/FeedsHTTPFetcher.inc
+++ b/plugins/FeedsHTTPFetcher.inc
@@ -148,7 +148,11 @@ class FeedsHTTPFetcher extends FeedsFetcher {
    * Override parent::sourceFormValidate().
    */
   public function sourceFormValidate(&$values) {
-    if ($this->config['auto_detect_feeds']) {
+    if (!feeds_valid_url($values['source'], TRUE)) {
+      $form_key = 'feeds][' . get_class($this) . '][source';
+      form_set_error($form_key, t('The URL %source is invalid.', array('%source' => $values['source'])));
+    }
+    elseif ($this->config['auto_detect_feeds']) {
       feeds_include_library('http_request.inc', 'http_request');
       if ($url = http_request_get_common_syndication($values['source'])) {
         $values['source'] = $url;
diff --git a/plugins/FeedsProcessor.inc b/plugins/FeedsProcessor.inc
index f283d9712b7d9e8bdb6b59c3342525a2ed33dc8b..b1925458f1cb997eda1360ff3526df2b032e6686 100644
--- a/plugins/FeedsProcessor.inc
+++ b/plugins/FeedsProcessor.inc
@@ -163,8 +163,8 @@ abstract class FeedsProcessor extends FeedsPlugin {
       $messages[] = array(
        'message' => format_plural(
           $state->created,
-          'Created @number @entity',
-          'Created @number @entities',
+          'Created @number @entity.',
+          'Created @number @entities.',
           array('@number' => $state->created) + $tokens
         ),
       );
@@ -173,8 +173,8 @@ abstract class FeedsProcessor extends FeedsPlugin {
       $messages[] = array(
        'message' => format_plural(
           $state->updated,
-          'Updated @number @entity',
-          'Updated @number @entities',
+          'Updated @number @entity.',
+          'Updated @number @entities.',
           array('@number' => $state->updated) + $tokens
         ),
       );
@@ -183,8 +183,8 @@ abstract class FeedsProcessor extends FeedsPlugin {
       $messages[] = array(
        'message' => format_plural(
           $state->failed,
-          'Failed importing @number @entity',
-          'Failed importing @number @entities',
+          'Failed importing @number @entity.',
+          'Failed importing @number @entities.',
           array('@number' => $state->failed) + $tokens
         ),
         'level' => WATCHDOG_ERROR,
@@ -443,9 +443,9 @@ abstract class FeedsProcessor extends FeedsPlugin {
     );
     global $user;
     $formats = filter_formats($user);
-      foreach ($formats as $format) {
-        $format_options[$format->format] = check_plain($format->name);
-      }
+    foreach ($formats as $format) {
+      $format_options[$format->format] = check_plain($format->name);
+    }
     $form['input_format'] = array(
       '#type' => 'select',
       '#title' => t('Text format'),
diff --git a/tests/feeds_processor_node.test b/tests/feeds_processor_node.test
index 327bc0c9e9fe4db527ffe30c1bddf95cc4392107..476348e8cf00065a1b8b44348a96a2b01f2bae08 100644
--- a/tests/feeds_processor_node.test
+++ b/tests/feeds_processor_node.test
@@ -31,12 +31,7 @@ class FeedsRSStoNodesTest extends FeedsWebTestCase {
     // text on nodes will fail.
     $edit = array('fields[body][type]' => 'text_default');
     $this->drupalPost('admin/structure/types/manage/article/display/teaser', $edit, 'Save');
-  }
 
-  /**
-   * Test node creation, refreshing/deleting feeds and feed items.
-   */
-  public function test() {
     // Create an importer configuration.
     $this->createImporterConfiguration('Syndication', 'syndication');
     $this->addMappings('syndication',
@@ -68,7 +63,12 @@ class FeedsRSStoNodesTest extends FeedsWebTestCase {
         ),
       )
     );
+  }
 
+  /**
+   * Test node creation, refreshing/deleting feeds and feed items.
+   */
+  public function test() {
     $nid = $this->createFeedNode();
 
     // Assert 10 items aggregated after creation of the node.
@@ -81,9 +81,6 @@ class FeedsRSStoNodesTest extends FeedsWebTestCase {
 
     // Navigate to a non-feed node, there should be no Feeds tabs visible.
     $article_nid = db_query_range("SELECT nid FROM {node} WHERE type = 'article'", 0, 1)->fetchField();
-    $this->drupalGet('node/'. $article_nid);
-    $this->assertNoRaw('node/'. $article_nid .'/import');
-    $this->assertNoRaw('node/'. $article_nid .'/delete-items');
     $this->assertEqual("Created by FeedsNodeProcessor", db_query("SELECT nr.log FROM {node} n JOIN {node_revision} nr ON n.vid = nr.vid WHERE n.nid = :nid", array(':nid' => $article_nid))->fetchField());
 
     // Assert accuracy of aggregated information.
@@ -365,4 +362,48 @@ class FeedsRSStoNodesTest extends FeedsWebTestCase {
     $this->assertText('Fri, 09/18/2009');
     $this->assertText('The first major change is switching');
   }
+
+  /**
+   * Test validation of feed URLs.
+   */
+  function testFeedURLValidation() {
+    $edit['feeds[FeedsHTTPFetcher][source]'] = 'invalid://url';
+    $this->drupalPost('node/add/page', $edit, 'Save');
+    $this->assertText('The URL invalid://url is invalid.');
+  }
+
+  /**
+   * Test using non-normal URLs like feed:// and webcal://.
+   */
+  function testOddFeedSchemes() {
+    $url = $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'feeds') .'/tests/feeds/developmentseed.rss2';
+
+    $schemes = array('feed', 'webcal');
+    $item_count = 0;
+    foreach ($schemes as $scheme) {
+      $feed_url = strtr($url, array('http://' => $scheme . '://', 'https://' => $scheme . '://'));
+
+      $edit['feeds[FeedsHTTPFetcher][source]'] = $feed_url;
+      $this->drupalPost('node/add/page', $edit, 'Save');
+      $this->assertText('Basic page Development Seed - Technological Solutions for Progressive Organizations has been created.');
+      $this->assertText('Created 10 nodes.');
+      $this->assertFeedItemCount($item_count + 10);
+      $item_count += 10;
+    }
+  }
+
+  /**
+   * Test that feed elements and links are not found on non-feed nodes.
+   */
+  function testNonFeedNodeUI() {
+    // There should not be feed links on an article node.
+    $non_feed_node = $this->drupalCreateNode(array('type' => 'article'));
+    $this->drupalGet('node/'. $non_feed_node->nid);
+    $this->assertNoLink('Import');
+    $this->assertNoLink('Delete items');
+
+    // Navigat to a non-feed node form, there should be no Feed field visible.
+    $this->drupalGet('node/add/article');
+    $this->assertNoFieldByName('feeds[FeedsHTTPFetcher][source]');
+  }
 }