diff --git a/conf/dokuwiki.php b/conf/dokuwiki.php
index 74d95147e5bbb74a172589ee180d13f4f8944777..e6a19e60b690b7482b2b4f109ebcf27db2ade656 100644
--- a/conf/dokuwiki.php
+++ b/conf/dokuwiki.php
@@ -103,6 +103,8 @@ $conf['gdlib']       = 2;                //the GDlib version (0, 1 or 2) 2 tries
 $conf['im_convert']  = '';               //path to ImageMagicks convert (will be used instead of GD)
 $conf['jpg_quality'] = '70';             //quality of compression when scaling jpg images (0-100)
 $conf['subscribers'] = 0;                //enable change notice subscription support
+$conf['subscribe_time'] = 24 * 60 * 60;  //Time after which digests / lists are sent (in sec, default 1 day)
+                                         //Should be larger than the time specified in recent_days
 $conf['compress']    = 1;                //Strip whitespaces and comments from Styles and JavaScript? 1|0
 $conf['hidepages']   = '';               //Regexp for pages to be skipped from RSS, Search and Recent Changes
 $conf['send404']     = 0;                //Send a HTTP 404 status for non existing pages?
diff --git a/inc/actions.php b/inc/actions.php
index 92f8171336ad9b5cae93b6469ced21ef5afb1c5a..a856b79197e755aa954bb8d339ec4ff42b1d693e 100644
--- a/inc/actions.php
+++ b/inc/actions.php
@@ -47,12 +47,13 @@ function act_dispatch(){
         }
 
         //check if user is asking to (un)subscribe a page
-        if($ACT == 'subscribe' || $ACT == 'unsubscribe')
-            $ACT = act_subscription($ACT);
-
-        //check if user is asking to (un)subscribe a namespace
-        if($ACT == 'subscribens' || $ACT == 'unsubscribens')
-            $ACT = act_subscriptionns($ACT);
+        if($ACT == 'subscribe') {
+            try {
+                $ACT = act_subscription($ACT);
+            } catch (Exception $e) {
+                msg($e->getMessage(), -1);
+            }
+        }
 
         //check permissions
         $ACT = act_permcheck($ACT);
@@ -550,81 +551,68 @@ function act_export($act){
 }
 
 /**
- * Handle page 'subscribe', 'unsubscribe'
+ * Handle page 'subscribe'
+ *
+ * Throws exception on error.
  *
- * @author Steven Danz <steven-danz@kc.rr.com>
- * @todo   localize
+ * @author Adrian Lang <lang@cosmocode.de>
  */
 function act_subscription($act){
-    global $ID;
-    global $INFO;
     global $lang;
-
-    $file=metaFN($ID,'.mlist');
-    if ($act=='subscribe' && !$INFO['subscribed']){
-        if ($INFO['userinfo']['mail']){
-            if (io_saveFile($file,$_SERVER['REMOTE_USER']."\n",true)) {
-                $INFO['subscribed'] = true;
-                msg(sprintf($lang[$act.'_success'], $INFO['userinfo']['name'], $ID),1);
-            } else {
-                msg(sprintf($lang[$act.'_error'], $INFO['userinfo']['name'], $ID),1);
-            }
-        } else {
-            msg($lang['subscribe_noaddress']);
-        }
-    } elseif ($act=='unsubscribe' && $INFO['subscribed']){
-        if (io_deleteFromFile($file,$_SERVER['REMOTE_USER']."\n")) {
-            $INFO['subscribed'] = false;
-            msg(sprintf($lang[$act.'_success'], $INFO['userinfo']['name'], $ID),1);
-        } else {
-            msg(sprintf($lang[$act.'_error'], $INFO['userinfo']['name'], $ID),1);
-        }
-    }
-
-    return 'show';
-}
-
-/**
- * Handle namespace 'subscribe', 'unsubscribe'
- *
- */
-function act_subscriptionns($act){
-    global $ID;
     global $INFO;
-    global $lang;
 
-    if(!getNS($ID)) {
-        $file = metaFN(getNS($ID),'.mlist');
-        $ns = "root";
-    } else {
-        $file = metaFN(getNS($ID),'/.mlist');
-        $ns = getNS($ID);
+    if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
+        // No post to handle, let tpl_subscribe manage the request.
+        return $act;
     }
 
-    // reuse strings used to display the status of the subscribe action
-    $act_msg = rtrim($act, 'ns');
-
-    if ($act=='subscribens' && !$INFO['subscribedns']){
-        if ($INFO['userinfo']['mail']){
-            if (io_saveFile($file,$_SERVER['REMOTE_USER']."\n",true)) {
-                $INFO['subscribedns'] = true;
-                msg(sprintf($lang[$act_msg.'_success'], $INFO['userinfo']['name'], $ns),1);
-            } else {
-                msg(sprintf($lang[$act_msg.'_error'], $INFO['userinfo']['name'], $ns),1);
+    // Get and validate parameters.
+    if (!isset($_POST['subscribe_target'])) {
+        throw new Exception($lang['subscr_no_target']);
+    }
+    $target = $_POST['subscribe_target'];
+    $valid_styles = array('every', 'digest');
+    if (substr($target, -1, 1) === ':') {
+        // Allow “list” subscribe style since the target is a namespace.
+        $valid_styles[] = 'list';
+    }
+    $style  = valid_input_set('subscribe_style', $valid_styles, $_POST,
+                              $lang['subscr_invalid_style']);
+    $action = valid_input_set('subscribe_action', array('subscribe',
+                                                        'unsubscribe'),
+                              $_POST, $lang['subscr_invalid_action']);
+
+    // Check other conditions.
+    if ($action === 'subscribe') {
+        if ($INFO['userinfo']['mail'] === '') {
+            throw new Exception($lang['subscr_subscribe_noaddress']);
+        }
+    } elseif ($action === 'unsubscribe') {
+        $is = false;
+        foreach($INFO['subscribed'] as $subscr) {
+            if ($subscr['target'] === $target) {
+                $is = true;
             }
-        } else {
-            msg($lang['subscribe_noaddress']);
         }
-    } elseif ($act=='unsubscribens' && $INFO['subscribedns']){
-        if (io_deleteFromFile($file,$_SERVER['REMOTE_USER']."\n")) {
-            $INFO['subscribedns'] = false;
-            msg(sprintf($lang[$act_msg.'_success'], $INFO['userinfo']['name'], $ns),1);
-        } else {
-            msg(sprintf($lang[$act_msg.'_error'], $INFO['userinfo']['name'], $ns),1);
+        if ($is === false) {
+            throw new Exception(sprintf($lang['subscr_not_subscribed_you'],
+                                        prettyprint_id($target)));
         }
+        // subscription_set deletes a subscription if style = null.
+        $style = null;
     }
 
-    return 'show';
+    // Perform action.
+    require_once DOKU_INC . 'inc/subscription.php';
+    if (!subscription_set($target, $_SERVER['REMOTE_USER'], $style)) {
+        throw new Exception(sprintf($lang["subscr_{$action}_error"],
+                                    hsc($INFO['userinfo']['name']),
+                                    prettyprint_id($target)));
+    }
+    $INFO['subscribed'] = get_info_subscribed();
+    msg(sprintf($lang["subscr_{$action}_success"], hsc($INFO['userinfo']['name']),
+                prettyprint_id($target)), 1);
+    return $act;
 }
 
 //Setup VIM: ex: et ts=2 enc=utf-8 :
diff --git a/inc/common.php b/inc/common.php
index 85187f16d74c24ee42d714dca2793e209cb8d305..2cc279844b37629ab341fa2b035fab199fefaffb 100644
--- a/inc/common.php
+++ b/inc/common.php
@@ -13,6 +13,7 @@ require_once(DOKU_INC.'inc/utf8.php');
 require_once(DOKU_INC.'inc/mail.php');
 require_once(DOKU_INC.'inc/parserutils.php');
 require_once(DOKU_INC.'inc/infoutils.php');
+require_once DOKU_INC.'inc/subscription.php';
 
 /**
  * These constants are used with the recents function
@@ -117,8 +118,7 @@ function pageinfo(){
     if(isset($_SERVER['REMOTE_USER'])){
         $info['userinfo']     = $USERINFO;
         $info['perm']         = auth_quickaclcheck($ID);
-        $info['subscribed']   = is_subscribed($ID,$_SERVER['REMOTE_USER'],false);
-        $info['subscribedns'] = is_subscribed($ID,$_SERVER['REMOTE_USER'],true);
+        $info['subscribed']   = get_info_subscribed();
         $info['client']       = $_SERVER['REMOTE_USER'];
 
         if($info['perm'] == AUTH_ADMIN){
@@ -1061,10 +1061,10 @@ function notify($id,$who,$rev='',$summary='',$minor=false,$replace=array()){
     }elseif($who == 'subscribers'){
         if(!$conf['subscribers']) return; //subscribers enabled?
         if($conf['useacl'] && $_SERVER['REMOTE_USER'] && $minor) return; //skip minors
-        $bcc  = subscriber_addresslist($id,false);
+        $bcc  = subscription_addresslist($id,false);
         if(empty($bcc)) return;
         $to   = '';
-        $text = rawLocale('subscribermail');
+        $text = rawLocale('subscr_single');
     }elseif($who == 'register'){
         if(empty($conf['registernotify'])) return;
         $text = rawLocale('registermail');
@@ -1097,7 +1097,7 @@ function notify($id,$who,$rev='',$summary='',$minor=false,$replace=array()){
         $text = str_replace('@OLDPAGE@',wl($id,"rev=$rev",true,'&'),$text);
         require_once(DOKU_INC.'inc/DifferenceEngine.php');
         $df  = new Diff(explode("\n",rawWiki($id,$rev)),
-                explode("\n",rawWiki($id)));
+                        explode("\n",rawWiki($id)));
         $dformat = new UnifiedDiffFormatter();
         $diff    = $dformat->format($df);
     }else{
@@ -1272,97 +1272,6 @@ function obfuscate($email) {
     }
 }
 
-/**
- * Let us know if a user is tracking a page or a namespace
- *
- * @author Andreas Gohr <andi@splitbrain.org>
- */
-function is_subscribed($id,$uid,$ns=false){
-    if(!$ns) {
-        $file=metaFN($id,'.mlist');
-    } else {
-        if(!getNS($id)) {
-            $file = metaFN(getNS($id),'.mlist');
-        } else {
-            $file = metaFN(getNS($id),'/.mlist');
-        }
-    }
-    if (@file_exists($file)) {
-        $mlist = file($file);
-        $pos = array_search($uid."\n",$mlist);
-        return is_int($pos);
-    }
-
-    return false;
-}
-
-/**
- * Return a string with the email addresses of all the
- * users subscribed to a page
- *
- * @author Steven Danz <steven-danz@kc.rr.com>
- */
-function subscriber_addresslist($id,$self=true){
-    global $conf;
-    global $auth;
-
-    if (!$conf['subscribers']) return '';
-
-    $users = array();
-    $emails = array();
-
-    // load the page mlist file content
-    $mlist = array();
-    $file=metaFN($id,'.mlist');
-    if (@file_exists($file)) {
-        $mlist = file($file);
-        foreach ($mlist as $who) {
-            $who = rtrim($who);
-            if(!$self && $who == $_SERVER['REMOTE_USER']) continue;
-            $users[$who] = true;
-        }
-    }
-
-    // load also the namespace mlist file content
-    $ns = getNS($id);
-    while ($ns) {
-        $nsfile = metaFN($ns,'/.mlist');
-        if (@file_exists($nsfile)) {
-            $mlist = file($nsfile);
-            foreach ($mlist as $who) {
-                $who = rtrim($who);
-                if(!$self && $who == $_SERVER['REMOTE_USER']) continue;
-                $users[$who] = true;
-            }
-        }
-        $ns = getNS($ns);
-    }
-    // root namespace
-    $nsfile = metaFN('','.mlist');
-    if (@file_exists($nsfile)) {
-        $mlist = file($nsfile);
-        foreach ($mlist as $who) {
-            $who = rtrim($who);
-            if(!$self && $who == $_SERVER['REMOTE_USER']) continue;
-            $users[$who] = true;
-        }
-    }
-    if(!empty($users)) {
-        foreach (array_keys($users) as $who) {
-            $info = $auth->getUserData($who);
-            if($info === false) continue;
-            $level = auth_aclcheck($id,$who,$info['grps']);
-            if ($level >= AUTH_READ) {
-                if (strcasecmp($info['mail'],$conf['notify']) != 0) {
-                    $emails[] = $info['mail'];
-                }
-            }
-        }
-    }
-
-    return implode(',',$emails);
-}
-
 /**
  * Removes quoting backslashes
  *
@@ -1545,4 +1454,30 @@ function send_redirect($url){
     exit;
 }
 
-//Setup VIM: ex: et ts=4 enc=utf-8 :
+/**
+ * Validate a value using a set of valid values
+ *
+ * This function checks whether a specified value is set and in the array
+ * $valid_values. If not, the function returns a default value or, if no
+ * default is specified, throws an exception.
+ *
+ * @param string $param        The name of the parameter
+ * @param array  $valid_values A set of valid values; Optionally a default may
+ *                             be marked by the key “default”.
+ * @param array  $array        The array containing the value (typically $_POST
+ *                             or $_GET)
+ * @param string $exc          The text of the raised exception
+ *
+ * @author Adrian Lang <lang@cosmocode.de>
+ */
+function valid_input_set($param, $valid_values, $array, $exc = '') {
+    if (isset($array[$param]) && in_array($array[$param], $valid_values)) {
+        return $array[$param];
+    } elseif (isset($valid_values['default'])) {
+        return $valid_values['default'];
+    } else {
+        throw new Exception($exc);
+    }
+}
+
+//Setup VIM: ex: et ts=2 enc=utf-8 :
diff --git a/inc/confutils.php b/inc/confutils.php
index abfde8a808850636c5b8f84502e00aa022a6383f..de63846de8fb70a5259450951538c434b844178d 100644
--- a/inc/confutils.php
+++ b/inc/confutils.php
@@ -248,7 +248,6 @@ function actionOK($action){
         if(isset($conf['resendpasswd']) && !$conf['resendpasswd']) $disabled[] = 'resendpwd';
         if(isset($conf['subscribers']) && !$conf['subscribers']) {
             $disabled[] = 'subscribe';
-            $disabled[] = 'subscribens';
         }
         $disabled = array_unique($disabled);
     }
diff --git a/inc/form.php b/inc/form.php
index 6d496f4144cb5a4f072fc7fedb66e000898a7be4..0a6bc2bba2958c8e266eecfebc27ffb08b10b6a8 100644
--- a/inc/form.php
+++ b/inc/form.php
@@ -283,6 +283,27 @@ class Doku_Form {
         echo $this->getForm();
     }
 
+    /**
+     * Add a radio set
+     *
+     * This function adds a set of radio buttons to the form. If $_POST[$name]
+     * is set, this radio is preselected, else the first radio button.
+     *
+     * @param string    $name    The HTML field name
+     * @param array     $entries An array of entries $value => $caption
+     *
+     * @author Adrian Lang <lang@cosmocode.de>
+     */
+
+    function addRadioSet($name, $entries) {
+        $value = (isset($_POST[$name]) && isset($entries[$_POST[$name]])) ?
+                 $_POST[$name] : key($entries);
+        foreach($entries as $val => $cap) {
+            $data = ($value === $val) ? array('checked' => 'checked') : array();
+            $this->addElement(form_makeRadioField($name, $val, $cap, '', '', $data));
+        }
+    }
+
 }
 
 /**
diff --git a/inc/lang/en/lang.php b/inc/lang/en/lang.php
index cf5173d05f50aeb0ea772ac2a4be83534886506d..728d7823bf341585f3bff545c375e44c3518e79c 100644
--- a/inc/lang/en/lang.php
+++ b/inc/lang/en/lang.php
@@ -39,10 +39,7 @@ $lang['btn_delete'] = 'Delete';
 $lang['btn_back']   = 'Back';
 $lang['btn_backlink']    = "Backlinks";
 $lang['btn_backtomedia'] = 'Back to Mediafile Selection';
-$lang['btn_subscribe']   = 'Subscribe Page Changes';
-$lang['btn_unsubscribe'] = 'Unsubscribe Page Changes';
-$lang['btn_subscribens']   = 'Subscribe Namespace Changes';
-$lang['btn_unsubscribens'] = 'Unsubscribe Namespace Changes';
+$lang['btn_subscribe']   = 'Manage Subscriptions';
 $lang['btn_profile']     = 'Update Profile';
 $lang['btn_reset']       = 'Reset';
 $lang['btn_resendpwd']   = 'Send new password';
@@ -158,6 +155,7 @@ $lang['download']   = 'Download Snippet';
 
 $lang['mail_newpage']  = 'page added:';
 $lang['mail_changed']  = 'page changed:';
+$lang['mail_subscribe_list'] = 'pages changed in namespace:';
 $lang['mail_new_user'] = 'new user:';
 $lang['mail_upload']   = 'file uploaded:';
 
@@ -212,11 +210,26 @@ $lang['img_format']  = 'Format';
 $lang['img_camera']  = 'Camera';
 $lang['img_keywords']= 'Keywords';
 
-$lang['subscribe_success']  = 'Added %s to subscription list for %s';
-$lang['subscribe_error']    = 'Error adding %s to subscription list for %s';
-$lang['subscribe_noaddress']= 'There is no address associated with your login, you cannot be added to the subscription list';
-$lang['unsubscribe_success']= 'Removed %s from subscription list for %s';
-$lang['unsubscribe_error']  = 'Error removing %s from subscription list for %s';
+$lang['subscr_subscribe_success']  = 'Added %s to subscription list for %s';
+$lang['subscr_subscribe_error']    = 'Error adding %s to subscription list for %s';
+$lang['subscr_subscribe_noaddress']= 'There is no address associated with your login, you cannot be added to the subscription list';
+$lang['subscr_unsubscribe_success']= 'Removed %s from subscription list for %s';
+$lang['subscr_unsubscribe_error']  = 'Error removing %s from subscription list for %s';
+$lang['subscr_no_target'] = 'No subscription target';
+$lang['subscr_invalid_style'] = 'Invalid subscription style';
+$lang['subscr_invalid_action'] = 'Invalid subscription action';
+$lang['subscr_already_subscribed'] = '%s is already subscribed to %s';
+$lang['subscr_not_subscribed'] = '%s is not subscribed to %s';
+$lang['subscr_not_subscribed_you'] = 'You are not subscribed to %s';
+// Manage page for subscriptions
+$lang['subscr_m_current_header'] = 'Current subscriptions';
+$lang['subscr_m_current'] = 'Your current subscriptions:';
+$lang['subscr_m_entry'] = 'Subscribed to %s receiving %s.';
+$lang['subscr_m_delete'] = 'Delete';
+$lang['subscr_m_not_subscribed'] = 'You are currently not subscribed to this page.';
+$lang['subscr_m_new_header'] = 'Add subscription';
+$lang['subscr_m_noemail'] =  'You did not set an email address.';
+$lang['subscr_m_subscribe'] = 'Subscribe';
 
 /* auth.class language support */
 $lang['authmodfailed']   = 'Bad user authentication configuration. Please inform your Wiki Admin.';
diff --git a/inc/lang/en/subscr_digest.txt b/inc/lang/en/subscr_digest.txt
new file mode 100644
index 0000000000000000000000000000000000000000..35011b6e61d76b5341a8a73ea5484099dd26b7a6
--- /dev/null
+++ b/inc/lang/en/subscr_digest.txt
@@ -0,0 +1,20 @@
+Hello!
+
+The page @PAGE@ in the @TITLE@ wiki changed.
+Here are the changes:
+
+--------------------------------------------------------
+@DIFF@
+--------------------------------------------------------
+
+Old Revision: @OLDPAGE@
+New Revision: @NEWPAGE@
+
+To cancel the page notifications, log into the wiki at
+@DOKUWIKIURL@ then visit
+@SUBSCRIBE@
+and unsubscribe page and/or namespace changes.
+
+-- 
+This mail was generated by DokuWiki at
+@DOKUWIKIURL@
diff --git a/inc/lang/en/subscr_form.txt b/inc/lang/en/subscr_form.txt
new file mode 100644
index 0000000000000000000000000000000000000000..94b75258c3f32276b3980ec289524d14abdf3ffe
--- /dev/null
+++ b/inc/lang/en/subscr_form.txt
@@ -0,0 +1,3 @@
+====== Manage subscriptions ======
+
+This form allows you to manage your subscriptions for the current page.
diff --git a/inc/lang/en/subscr_list.txt b/inc/lang/en/subscr_list.txt
new file mode 100644
index 0000000000000000000000000000000000000000..efe27d866ec0f758a63cf513d78471e5d323b551
--- /dev/null
+++ b/inc/lang/en/subscr_list.txt
@@ -0,0 +1,17 @@
+Hello!
+
+Pages in the namespace @PAGE@ of the @TITLE@ wiki changed.
+Here are the changed pages:
+
+--------------------------------------------------------
+@DIFF@
+--------------------------------------------------------
+
+To cancel the page notifications, log into the wiki at
+@DOKUWIKIURL@ then visit
+@SUBSCRIBE@
+and unsubscribe page and/or namespace changes.
+
+--
+This mail was generated by DokuWiki at
+@DOKUWIKIURL@
diff --git a/inc/lang/en/subscribermail.txt b/inc/lang/en/subscr_single.txt
similarity index 100%
rename from inc/lang/en/subscribermail.txt
rename to inc/lang/en/subscr_single.txt
diff --git a/inc/pageutils.php b/inc/pageutils.php
index 9c192e5e6bafb5586d1ea1bfe97b0b2767012591..239ff41c57ee65ab7494db2ab87b6791fc7d508d 100644
--- a/inc/pageutils.php
+++ b/inc/pageutils.php
@@ -534,4 +534,21 @@ function isVisiblePage($id){
     return !isHiddenPage($id);
 }
 
+/**
+ * Format an id for output to a user
+ *
+ * Namespaces are denoted by a trailing “:*”. The root namespace is
+ * “*”. Output is escaped.
+ *
+ * @author Adrian Lang <lang@cosmocode.de>
+ */
 
+function prettyprint_id($id) {
+    if (!$id || $id === ':') {
+        return '*';
+    }
+    if ((substr($id, -1, 1) === ':')) {
+        $id .= '*';
+    }
+    return hsc($id);
+}
diff --git a/inc/subscription.php b/inc/subscription.php
new file mode 100644
index 0000000000000000000000000000000000000000..1dcecf6f56a8f71e5e4e80477eac8d37ce53effb
--- /dev/null
+++ b/inc/subscription.php
@@ -0,0 +1,342 @@
+<?php
+/**
+ * Utilities for handling (email) subscriptions
+ *
+ * The public interface of this file consists of the functions
+ * - subscription_find
+ * - subscription_send_digest
+ * - subscription_send_list
+ * - subscription_set
+ * - get_info_subscribed
+ * - subscription_addresslist
+ *
+ * @author  Adrian Lang <lang@cosmocode.de>
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ */
+
+require_once DOKU_INC.'/inc/pageutils.php';
+
+/**
+ * Get the name of the metafile tracking subscriptions to target page or
+ * namespace
+ *
+ * @param string $id The target page or namespace, specified by id; Namespaces
+ *                   are identified by appending a colon.
+ *
+ * @author Adrian Lang <lang@cosmocode.de>
+ */
+function subscription_filename($id) {
+    $meta_fname = '.mlist';
+    if ((substr($id, -1, 1) === ':')) {
+        $meta_froot = getNS($id);
+        if ($meta_froot === false) {
+            $meta_fname = '/' . $meta_fname;
+        }
+    } else {
+        $meta_froot = $id;
+    }
+    return metaFN($meta_froot, $meta_fname);
+}
+
+/**
+ * Set subscription information
+ *
+ * Allows to set subscription informations for permanent storage in meta files.
+ * Subscriptions consist of a target object, a subscribing user, a subscribe
+ * style and optional data.
+ * A subscription may be deleted by specifying an empty subscribe style.
+ * Only one subscription per target and user is allowed.
+ * The function returns false on error, otherwise true. Note that no error is
+ * returned if a subscription should be deleted but the user is not subscribed
+ * and the subscription meta file exists.
+ *
+ * @param string $page      The target object (page or namespace), specified by
+ *                          id; Namespaces are identified by a trailing colon.
+ * @param string $user      The user
+ * @param string $style     The subscribe style; DokuWiki currently implements
+ *                          “every”, “digest”, and “list”.
+ * @param bool   $overwrite Whether an existing subscription may be overwritten
+ * @param string $data      An optional data blob; For list and digest, this
+ *                          defaults to time().
+ *
+ * @author Adrian Lang <lang@cosmocode.de>
+ */
+function subscription_set($page, $user, $style, $overwrite = false, $data = null) {
+    global $lang;
+    if (is_null($style)) {
+        // Delete subscription.
+        $file = subscription_filename($page);
+        if (!@file_exists($file)) {
+            msg(sprintf($lang['subscr_not_subscribed'], $user,
+                        prettyprint_id($page)), -1);
+            return false;
+        }
+
+        // io_deleteFromFile does not return false if no line matched.
+        return io_deleteFromFile($file,
+                                 subscription_regex(array('user' => $user)),
+                                 true);
+    }
+
+    // Delete subscription if one exists and $overwrite is true. If $overwrite
+    // is false, fail.
+    $subs = subscription_find($page, array('user' => $user));
+    if (count($subs) > 0 && array_pop(array_keys($subs)) === $page) {
+        if (!$overwrite) {
+            msg(sprintf($lang['subscr_already_subscribed'], $user,
+                        prettyprint_id($page)), -1);
+            return false;
+        }
+        // Fail if deletion failed, else continue.
+        if (!subscription_set($page, $user, null)) {
+            return false;
+        }
+    }
+
+    $file = subscription_filename($page);
+    $content = auth_nameencode($user) . ' ' . $style;
+    if (in_array($style, array('list', 'digest'))) {
+        $content .= ' ' . (!is_null($data) ? $data : time());
+    }
+    return io_saveFile($file, $content . "\n", true);
+}
+
+/**
+ * Recursively search for matching subscriptions
+ *
+ * This function searches all relevant subscription files for a page or
+ * namespace.
+ *
+ * @param string $page The target object’s (namespace or page) id
+ * @param array  $pre  A hash of predefined values
+ *
+ * @see function subscription_regex for $pre documentation
+ *
+ * @author Adrian Lang <lang@cosmocode.de>
+ */
+function subscription_find($page, $pre) {
+    // Construct list of files which may contain relevant subscriptions.
+    $filenames = array();
+    do {
+        $filenames[$page] = subscription_filename($page);
+        $page = getNS(rtrim($page, ':')) . ':';
+    } while ($page !== ':');
+
+    // Handle files.
+    $matches = array();
+    foreach ($filenames as $cur_page => $filename) {
+        if (!@file_exists($filename)) {
+            continue;
+        }
+        $subscriptions = file($filename);
+        foreach ($subscriptions as $subscription) {
+            if (strpos($subscription, ' ') === false) {
+                // This is an old subscription file.
+                $subscription = trim($subscription) . " every\n";
+            }
+            if (preg_match(subscription_regex($pre), $subscription,
+                           &$line_matches) === 0) {
+                continue;
+            }
+            $match = array_slice($line_matches, 1);
+            if (!isset($matches[$cur_page])) {
+                $matches[$cur_page] = array();
+            }
+            $matches[$cur_page][] = $match;
+        }
+    }
+    return array_reverse($matches);
+}
+
+/**
+ * Get data for $INFO['subscribed']
+ *
+ * $INFO['subscribed'] is either false if no subscription for the current page
+ * and user is in effect. Else it contains an array of arrays with the fields
+ * “target”, “style”, and optionally “data”.
+ *
+ * @author Adrian Lang <lang@cosmocode.de>
+ */
+function get_info_subscribed() {
+    global $ID;
+    global $conf;
+    if (!$conf['subscribers']) {
+        return false;
+    }
+
+    $subs = subscription_find($ID, array('user' => $_SERVER['REMOTE_USER']));
+    if (count($subs) === 0) {
+        return false;
+    }
+
+    $_ret = array();
+    foreach ($subs as $target => $subs_data) {
+        $new = array('target' => $target,
+                     'style'  => $subs_data[0][0]);
+        if (count($subs_data[0]) > 1) {
+            $new['data'] = $subs_data[0][1];
+        }
+        $_ret[] = $new;
+    }
+
+    return $_ret;
+}
+
+/**
+ * Construct a regular expression parsing a subscription definition line
+ *
+ * @param array $pre A hash of predefined values; “user”, “style”, and
+ *                   “data” may be set to limit the results to
+ *                   subscriptions matching these parameters. If
+ *                   “escaped” is true, these fields are inserted into the
+ *                   regular expression without escaping.
+ *
+ * @author Adrian Lang <lang@cosmocode.de>
+ */
+function subscription_regex($pre = array()) {
+    if (!isset($pre['escaped']) || $pre['escaped'] === false) {
+        $pre = array_map('preg_quote_cb', $pre);
+    }
+    foreach (array('user', 'style', 'data') as $key) {
+        if (!isset($pre[$key])) {
+            $pre[$key] = '(\S+)';
+        }
+    }
+    return '/^' . $pre['user'] . '(?: ' . $pre['style'] .
+           '(?: ' . $pre['data'] . ')?)?$/';
+}
+
+/**
+ * Return a string with the email addresses of all the
+ * users subscribed to a page
+ *
+ * @param string $id   The id of the changed page
+ * @param bool   $self Whether a notice should be sent to the editor if he is
+ *                     subscribed
+ *
+ * @author Steven Danz <steven-danz@kc.rr.com>
+ */
+function subscription_addresslist($id, $self=true){
+    global $conf;
+    global $auth;
+
+    if (!$conf['subscribers']) {
+        return '';
+    }
+    $pres = array('style' => 'every', 'escaped' => true);
+    if (!$self && isset($_SERVER['REMOTE_USER'])) {
+        $pres['user'] = '((?:(?!' . preg_quote_cb($_SERVER['REMOTE_USER']) .
+                        ')\S?)+)';
+    }
+    $subs = subscription_find($id, $pres);
+    $emails = array();
+    foreach ($subs as $by_targets) {
+        foreach ($by_targets as $sub) {
+            $info = $auth->getUserData($sub[0]);
+            if ($info === false) continue;
+            $level = auth_aclcheck($id, $sub[0], $info['grps']);
+            if ($level >= AUTH_READ) {
+                if (strcasecmp($info['mail'], $conf['notify']) != 0) {
+                    $emails[$sub[0]] =  $info['mail'];
+                }
+            }
+        }
+    }
+    return implode(',', $emails);
+}
+
+/**
+ * Send a digest mail
+ *
+ * Sends a digest mail showing a bunch of changes.
+ *
+ * @param string $subscriber_mail The target mail address
+ * @param array  $change          The newest change
+ * @param int    $lastupdate      Time of the last notification
+ *
+ * @author Adrian Lang <lang@cosmocode.de>
+ */
+function subscription_send_digest($subscriber_mail, $change, $lastupdate) {
+    $id = $change['id'];
+    $n = 0;
+    do {
+        $rev = getRevisions($id, $n++, 1);
+        $rev = (count($rev) > 0) ? $rev[0] : null;
+    } while (!is_null($rev) && $rev > $lastupdate);
+
+    $ip = $change['ip'];
+    $replaces = array('NEWPAGE'   => wl($id, '', true, '&'),
+                      'SUBSCRIBE' => wl($id, array('do' => 'subscribe'), true, '&'));
+    if (!is_null($rev)) {
+        $subject = 'changed';
+        $replaces['OLDPAGE'] = wl($id, "rev=$rev", true, '&');
+        require_once DOKU_INC.'inc/DifferenceEngine.php';
+        $df = new Diff(explode("\n", rawWiki($id, $rev)),
+                        explode("\n", rawWiki($id)));
+        $dformat = new UnifiedDiffFormatter();
+        $replaces['DIFF'] = $dformat->format($df);
+    } else {
+        $subject = 'newpage';
+        $replaces['OLDPAGE'] = 'none';
+        $replaces['DIFF'] = rawWiki($id);
+    }
+    subscription_send($subscriber_mail, $replaces, $subject, $id,
+                      'subscr_digest');
+}
+
+/**
+ * Send a list mail
+ *
+ * Sends a list mail showing a list of changed pages.
+ *
+ * @param string $subscriber_mail The target mail address
+ * @param array  $changes         Array of changes
+ * @param string $id              The id of the namespace
+ *
+ * @author Adrian Lang <lang@cosmocode.de>
+ */
+function subscription_send_list($subscriber_mail, $changes, $id) {
+    $list = '';
+    foreach ($changes as $change) {
+        $list .= '* ' . $change['id'] . NL;
+    }
+    subscription_send($subscriber_mail,
+                      array('DIFF'      => rtrim($list),
+                            'SUBSCRIBE' => wl($changes[0]['id'],
+                                              array('do' => 'subscribe'),
+                                              true, '&')),
+                      'subscribe_list',
+                      prettyprint_id($id),
+                      'subscr_list');
+}
+
+/**
+ * Helper function for sending a mail
+ *
+ * @param string $subscriber_mail The target mail address
+ * @param array  $replaces        Predefined parameters used to parse the
+ *                                template
+ * @param string $subject         The lang id of the mail subject (without the
+ *                                prefix “mail_”)
+ * @param string $id              The page or namespace id
+ * @param string $template        The name of the mail template
+ *
+ * @author Adrian Lang <lang@cosmocode.de>
+ */
+function subscription_send($subscriber_mail, $replaces, $subject, $id, $template) {
+    global $conf;
+
+    $text = rawLocale($template);
+    $replaces = array_merge($replaces, array('TITLE'       => $conf['title'],
+                                             'DOKUWIKIURL' => DOKU_URL,
+                                             'PAGE'        => $id));
+
+    foreach ($replaces as $key => $substitution) {
+        $text = str_replace('@'.strtoupper($key).'@', $substitution, $text);
+    }
+
+    global $lang;
+    $subject = $lang['mail_' . $subject] . ' ' . $id;
+    mail_send('', '['.$conf['title'].'] '. $subject, $text,
+              $conf['mailfrom'], '', $subscriber_mail);
+}
diff --git a/inc/template.php b/inc/template.php
index 4681300ebaee0b8e900da37a440adf05b5ae50be..9b738bf8f3be32e63fe895581cffd04dcfd0e1c4 100644
--- a/inc/template.php
+++ b/inc/template.php
@@ -126,6 +126,9 @@ function tpl_content_core(){
     case 'admin':
       tpl_admin();
       break;
+    case 'subscribe':
+      tpl_subscribe();
+      break;
     default:
       $evt = new Doku_Event('TPL_ACT_UNKNOWN',$ACT);
       if ($evt->advise_before())
@@ -540,31 +543,10 @@ function tpl_button($type,$return=false){
       }
       break;
     case 'subscribe':
-    case 'subscription':
-      if($conf['useacl'] && $auth && $ACT == 'show' && $conf['subscribers'] == 1){
-        if($_SERVER['REMOTE_USER']){
-          if($INFO['subscribed']){
-            if(actionOK('unsubscribe'))
-              $out .= html_btn('unsubscribe',$ID,'',array('do' => 'unsubscribe',));
-          } else {
-            if(actionOK('subscribe'))
-              $out .= html_btn('subscribe',$ID,'',array('do' => 'subscribe',));
-          }
-        }
-      }
-      if($type == 'subscribe') break;
-      // else: fall through for backward compatibility
-    case 'subscribens':
-      if($conf['useacl'] && $auth && $ACT == 'show' && $conf['subscribers'] == 1){
-        if($_SERVER['REMOTE_USER']){
-          if($INFO['subscribedns']){
-            if(actionOK('unsubscribens'))
-              $out .= html_btn('unsubscribens',$ID,'',array('do' => 'unsubscribens',));
-          } else {
-            if(actionOK('subscribens'))
-              $out .= html_btn('subscribens',$ID,'',array('do' => 'subscribens',));
-          }
-        }
+      if ($conf['useacl'] && $auth && $ACT == 'show' &&
+          $conf['subscribers'] && isset($_SERVER['REMOTE_USER']) &&
+          actionOK('subscribe')) {
+        $out .= html_btn('subscribe',$ID,'',array('do' => 'subscribe',));
       }
       break;
     case 'backlink':
@@ -712,37 +694,12 @@ function tpl_actionlink($type,$pre='',$suf='',$inner='',$return=false){
       break;
    case 'subscribe':
    case 'subscription':
-      if($conf['useacl'] && $auth && $ACT == 'show' && $conf['subscribers'] == 1){
+      if($conf['useacl'] && $auth && $ACT == 'show' && $conf['subscribers']) {
         if($_SERVER['REMOTE_USER']){
-          if($INFO['subscribed']) {
-            if(actionOK('unsubscribe'))
-              $out .= tpl_link(wl($ID,'do=unsubscribe'),
-                     $pre.(($inner)?$inner:$lang['btn_unsubscribe']).$suf,
-                     'class="action unsubscribe" rel="nofollow"',1);
-          } else {
             if(actionOK('subscribe'))
               $out .= tpl_link(wl($ID,'do=subscribe'),
                      $pre.(($inner)?$inner:$lang['btn_subscribe']).$suf,
                      'class="action subscribe" rel="nofollow"',1);
-          }
-        }
-      }
-      if($type == 'subscribe') break;
-      // else: fall through for backward compatibility
-    case 'subscribens':
-      if($conf['useacl'] && $auth && $ACT == 'show' && $conf['subscribers'] == 1){
-        if($_SERVER['REMOTE_USER']){
-          if($INFO['subscribedns']) {
-            if(actionOK('unsubscribens'))
-              $out .= tpl_link(wl($ID,'do=unsubscribens'),
-                     $pre.(($inner)?$inner:$lang['btn_unsubscribens']).$suf,
-                     'class="action unsubscribens" rel="nofollow"',1);
-          } else {
-            if(actionOK('subscribens'))
-              $out .= tpl_link(wl($ID,'do=subscribens'),
-                     $pre.(($inner)?$inner:$lang['btn_subscribens']).$suf,
-                     'class="action subscribens" rel="nofollow"',1);
-          }
         }
       }
       break;
@@ -1323,23 +1280,9 @@ function tpl_actiondropdown($empty='',$button='&gt;'){
             echo '<option value="profile">'.$lang['btn_profile'].'</option>';
         }
 
-        if($conf['useacl'] && $auth && $ACT == 'show' && $conf['subscribers'] == 1){
+        if($conf['useacl'] && $auth && $ACT == 'show' && $conf['subscribers']){
             if($_SERVER['REMOTE_USER']){
-                if($INFO['subscribed']) {
-                    echo '<option value="unsubscribe">'.$lang['btn_unsubscribe'].'</option>';
-                } else {
                     echo '<option value="subscribe">'.$lang['btn_subscribe'].'</option>';
-                }
-            }
-        }
-
-        if($conf['useacl'] && $auth && $ACT == 'show' && $conf['subscribers'] == 1){
-            if($_SERVER['REMOTE_USER']){
-                if($INFO['subscribedns']) {
-                    echo '<option value="unsubscribens">'.$lang['btn_unsubscribens'].'</option>';
-                } else {
-                    echo '<option value="subscribens">'.$lang['btn_subscribens'].'</option>';
-                }
             }
         }
 
@@ -1406,5 +1349,68 @@ function tpl_include_page($pageid,$print=true){
     echo $html;
 }
 
+/**
+ * Display the subscribe form
+ *
+ * @author Adrian Lang <lang@cosmocode.de>
+ */
+
+function tpl_subscribe() {
+    global $INFO;
+    global $ID;
+    global $lang;
+    $targets = array($ID         => 'the current page',
+                     'namespace' => 'the namespace “%s”');
+    $styles  = array('every'  => 'a notice on every change',
+                     'digest' => 'a digest for each changed page*',
+                     'list'   => 'a list of changed pages*');
+
+    echo p_locale_xhtml('subscr_form');
+
+    echo '<h2>' . $lang['subscr_m_current_header'] . '</h2>';
+    if ($INFO['subscribed'] === false) {
+        echo '<p>' . $lang['subscr_m_not_subscribed'] . '</p>';
+    } else {
+        echo '<p>' . $lang['subscr_m_current'] . '</p>';
+        echo '<ul>';
+        foreach($INFO['subscribed'] as $sub) {
+            $form = new Doku_Form(array('class' => 'unsubscribe'));
+            if ($sub['target'] !== $ID) {
+                $str = sprintf($targets['namespace'], prettyprint_id($sub['target']));
+            } else {
+                $str = $targets[$ID];
+            }
+            $form->addElement('<li><div class="li">' .
+                              sprintf($lang['subscr_m_entry'], $str,
+                                      $styles[$sub['style']]));
+            $form->addElement(form_makeButton('submit', 'subscribe', $lang['subscr_m_delete']));
+            $form->addHidden('subscribe_target', $sub['target']);
+            $form->addHidden('subscribe_style', $sub['style']);
+            $form->addHidden('subscribe_action', 'unsubscribe');
+            $form->addElement('</div></li>');
+            html_form('UNSUBSCRIBE', $form);
+        }
+        echo '</ul>';
+    }
+
+    echo '<h2>' . $lang['subscr_m_new_header'] . '</h2>';
+    if ($INFO['userinfo']['mail'] === '') {
+        echo $lang['subscr_m_noemail'];
+        return;
+    }
+    $styles['list'] = $styles['list'] . ' (Not allowed for single pages)';
+    $form = new Doku_Form(array('id' => 'subscribe'));
+    $ns = getNS($ID). ':';
+    $targets[$ns] = sprintf($targets['namespace'], prettyprint_id($ns));
+    unset($targets['namespace']);
+    $form->addElement('<p>' . 'Subscribe to' . '</p>');
+    $form->addRadioSet('subscribe_target', $targets);
+    $form->addElement('<p>' . 'Receive' . '</p>');
+    $form->addRadioSet('subscribe_style', $styles);
+    $form->addHidden('subscribe_action', 'subscribe');
+    $form->addElement(form_makeButton('submit', 'subscribe', $lang['subscr_m_subscribe']));
+    html_form('SUBSCRIBE', $form);
+}
+
 //Setup VIM: ex: et ts=4 enc=utf-8 :
 
diff --git a/lib/exe/indexer.php b/lib/exe/indexer.php
index 1c4128eb74a5689f10e5d2c8dc39a2468c911439..eb1556e1d3266c635ba14ea6f610a260883ddaab 100644
--- a/lib/exe/indexer.php
+++ b/lib/exe/indexer.php
@@ -37,6 +37,7 @@ if ($evt->advise_before()) {
   runIndexer() or
   metaUpdate() or
   runSitemapper() or
+  sendDigest() or
   runTrimRecentChanges() or
   runTrimRecentChanges(true) or
   $evt->advise_after();
@@ -334,6 +335,75 @@ function runSitemapper(){
     return true;
 }
 
+/**
+ * Send digest and list mails for all subscriptions which are in effect for the
+ * current page
+ *
+ * @author Adrian Lang <lang@cosmocode.de>
+ */
+function sendDigest() {
+    require_once DOKU_INC . 'inc/subscription.php';
+    echo 'sendDigest(): start'.NL;
+    global $ID;
+    global $conf;
+    if (!$conf['subscribers']) {
+        return;
+    }
+
+    $subscriptions = subscription_find($ID, array('style' => '(digest|list)',
+                                                  'escaped' => true));
+    global $auth;
+    global $lang;
+    global $conf;
+    foreach($subscriptions as $id => $users) {
+        foreach($users as $data) {
+            list($user, $style, $lastupdate) = $data;
+            $lastupdate = (int) $lastupdate;
+            if ($lastupdate + $conf['subscribe_interval'] > time()) {
+                // Less than a day passed since last update.
+                continue;
+            }
+            // TODO: Does that suffice for namespaces?
+            $info = $auth->getUserData($user);
+            if ($info === false) {
+                continue;
+            }
+            $level = auth_aclcheck($id, $user, $info['grps']);
+            if ($level < AUTH_READ) {
+                continue;
+            }
+
+            if (substr($id, -1, 1) === ':') {
+                // The subscription target is a namespace
+                $changes = getRecentsSince($lastupdate, null, getNS($id));
+                if (count($changes) === 0) {
+                    continue;
+                }
+                if ($style === 'digest') {
+                    foreach($changes as $change) {
+                        subscription_send_digest($info['mail'], $change,
+                                                 $lastupdate);
+                    }
+                } elseif ($style === 'list') {
+                    subscription_send_list($info['mail'], $changes, $id);
+                }
+                // TODO: Handle duplicate subscriptions.
+            } else {
+                $meta = p_get_metadata($id);
+                $rev = $meta['last_change']['date'];
+                if ($rev < $lastupdate) {
+                    // There is no new revision.
+                    continue;
+                }
+                subscription_send_digest($info['mail'], $meta['last_change'],
+                                         $lastupdate);
+            }
+            // Update notification time.
+            subscription_set($id, $user, $style, true);
+        }
+    }
+}
+
 /**
  * Formats a timestamp as ISO 8601 date
  *
diff --git a/lib/exe/js.php b/lib/exe/js.php
index 38fda178988b44f43f9997a7ffa69d4a8efbcc83..8648bf18f71630503cb2507d8474c7f9250dba9a 100644
--- a/lib/exe/js.php
+++ b/lib/exe/js.php
@@ -53,6 +53,7 @@ function js_out(){
                 DOKU_INC.'lib/scripts/edit.js',
                 DOKU_INC.'lib/scripts/linkwiz.js',
                 DOKU_INC.'lib/scripts/media.js',
+                DOKU_INC.'lib/scripts/subscriptions.js',
                 DOKU_TPLINC.'script.js',
             );
 
diff --git a/lib/scripts/subscriptions.js b/lib/scripts/subscriptions.js
new file mode 100644
index 0000000000000000000000000000000000000000..9f602dde8bf63c9fad11da3f70ed630f001bcb72
--- /dev/null
+++ b/lib/scripts/subscriptions.js
@@ -0,0 +1,46 @@
+/**
+ * Hide list subscription style if target is a page
+ *
+ * @author Adrian Lang <lang@cosmocode.de>
+ */
+
+addInitEvent(function () {
+    var form = $('subscribe');
+    if (!form) {
+        return;
+    }
+
+    var styleradios = {};
+
+    function update_state() {
+        if (!this.checked) {
+            return;
+        }
+        if (this.value.match(/:$/)) {
+            styleradios.list.parentNode.style.display = '';
+        } else {
+            styleradios.list.parentNode.style.display = 'none';
+            if (styleradios.list.checked) {
+                styleradios.digest.checked = 'checked';
+            }
+        }
+    }
+
+    var cur_sel = null;
+
+    var inputs = form.getElementsByTagName('input');
+    for (var i = 0; i < inputs.length ; ++i) {
+        switch (inputs[i].name) {
+        case 'subscribe_target':
+            inputs[i].addEventListener('change', update_state, false);
+            if (inputs[i].checked) {
+                cur_sel = inputs[i];
+            }
+            break;
+        case 'subscribe_style':
+            styleradios[inputs[i].value] = inputs[i];
+            break;
+        }
+    }
+    update_state.call(cur_sel);
+});
diff --git a/lib/tpl/default/design.css b/lib/tpl/default/design.css
index 4830a9e2cb3c6be7e5a45f63ee1319d7f18f2f81..02804256cfd5c638cc6e27f3841071d347a964c3 100644
--- a/lib/tpl/default/design.css
+++ b/lib/tpl/default/design.css
@@ -833,3 +833,17 @@ div.dokuwiki div.imagemeta img.thumb {
   float: left;
   margin-right: 0.1em;
 }
+
+form#subscribe p {
+    margin: 0.5em 0;
+}
+
+form#subscribe label {
+    display:block;
+    margin: 0 0.5em 0.5em;
+}
+
+form#subscribe fieldset, form#unsubscribe fieldset {
+    text-align:inherit;
+    margin: 0;
+}
diff --git a/lib/tpl/default/main.php b/lib/tpl/default/main.php
index 67c3a00ba9db4baf31a64f261b8deed478109695..b5717c00972d99022ae1a349f00b14ec441b5831 100644
--- a/lib/tpl/default/main.php
+++ b/lib/tpl/default/main.php
@@ -117,7 +117,6 @@ if (!defined('DOKU_INC')) die();
       </div>
       <div class="bar-right" id="bar__bottomright">
         <?php tpl_button('subscribe')?>
-        <?php tpl_button('subscribens')?>
         <?php tpl_button('admin')?>
         <?php tpl_button('profile')?>
         <?php tpl_button('login')?>