diff --git a/isso/db/comments.py b/isso/db/comments.py index 0ba3964..d2181f3 100644 --- a/isso/db/comments.py +++ b/isso/db/comments.py @@ -81,14 +81,14 @@ class Comments: ' mode=1', 'WHERE id=? AND mode=2'], (id, )) - def unsubscribe(self, id): + def unsubscribe(self, email, id): """ Turn off email notifications for replies to this comment. """ self.db.execute([ 'UPDATE comments SET', ' notification=0', - 'WHERE id=?'], (id, )) + 'WHERE email=? AND (id=? OR parent=?);'], (email, id, id)) def update(self, id, data): """ diff --git a/isso/ext/notifications.py b/isso/ext/notifications.py index ccea39b..00f0a02 100644 --- a/isso/ext/notifications.py +++ b/isso/ext/notifications.py @@ -14,6 +14,11 @@ from email.utils import formatdate from email.header import Header from email.mime.text import MIMEText +try: + from urllib.parse import quote +except ImportError: + from urllib import quote + import logging logger = logging.getLogger("isso") @@ -99,7 +104,7 @@ class SMTP(object): def __iter__(self): yield "comments.new:after-save", self.notify - def format(self, thread, comment, comment_parent, admin=False): + def format(self, thread, comment, parent_comment, recipient=None, admin=False): rv = io.StringIO() @@ -133,10 +138,10 @@ class SMTP(object): rv.write("Activate comment: %s\n" % (uri + "/activate/" + key)) else: - uri = self.general_host + "/id/%i" % comment_parent["id"] - key = self.isso.sign(('unsubscribe', comment_parent["id"])) + uri = self.general_host + "/id/%i" % parent_comment["id"] + key = self.isso.sign(('unsubscribe', recipient)) - rv.write("Unsubscribe from this conversation: %s\n" % (uri + "/unsubscribe/" + key)) + rv.write("Unsubscribe from this conversation: %s\n" % (uri + "/unsubscribe/" + quote(recipient) + "/" + key)) rv.seek(0) return rv.read() @@ -152,10 +157,10 @@ class SMTP(object): email = comment_to_notify["email"] if "email" in comment_to_notify and comment_to_notify["notification"] and email not in notified \ and comment_to_notify["id"] != comment["id"] and email != comment["email"]: - body = self.format(thread, comment, comment_to_notify, admin=False) + body = self.format(thread, comment, parent_comment, email, admin=False) subject = "Re: New comment posted on %s" % thread["title"] self.sendmail(subject, body, thread, comment, to=email) - notified += email + notified.append(email) body = self.format(thread, comment, None, admin=True) self.sendmail(thread["title"], body, thread, comment) diff --git a/isso/views/comments.py b/isso/views/comments.py index 4c12820..175c346 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -32,6 +32,10 @@ try: from urlparse import urlparse except ImportError: from urllib.parse import urlparse +try: + from urllib import unquote +except ImportError: + from urllib.parse import unquote try: from StringIO import StringIO except ImportError: @@ -107,7 +111,7 @@ class API(object): ('view', ('GET', '/id/')), ('edit', ('PUT', '/id/')), ('delete', ('DELETE', '/id/')), - ('unsubscribe', ('GET', '/id//unsubscribe/')), + ('unsubscribe', ('GET', '/id//unsubscribe//')), ('moderate', ('GET', '/id///')), ('moderate', ('POST', '/id///')), ('like', ('POST', '/id//like')), @@ -494,18 +498,20 @@ class API(object): return resp """ - @api {get} /id/:id/key unsubscribe + @api {get} /id/:id/:email/key unsubscribe @apiGroup Comment @apiDescription Opt out from getting any further email notifications about replies to a particular comment. In order to use this endpoint, the requestor needs a `key` that is usually obtained from an email sent out by isso. @apiParam {number} id The id of the comment to unsubscribe from replies to. + @apiParam {string} email + The email address of the subscriber. @apiParam {string} key The key to authenticate the subscriber. - @apiExample {curl} Unsubscribe from replies to comment with id 13: - curl -X GET 'https://comments.example.com/id/13/unsubscribe/TODO-COMPUTE-HASH' + @apiExample {curl} Unsubscribe Alice from replies to comment with id 13: + curl -X GET 'https://comments.example.com/id/13/unsubscribe/alice@example.com/TODO-COMPUTE-HASH' @apiSuccessExample {html} Using GET: <!DOCTYPE html> @@ -523,13 +529,15 @@ class API(object): Yo """ - def unsubscribe(self, environ, request, id, key): + def unsubscribe(self, environ, request, id, email, key): + email = unquote(email) + try: rv = self.isso.unsign(key, max_age=2**32) except (BadSignature, SignatureExpired): raise Forbidden - if rv[0] != 'unsubscribe' or rv[1] != id: + if rv[0] != 'unsubscribe' or rv[1] != email: raise Forbidden item = self.comments.get(id) @@ -538,7 +546,7 @@ class API(object): raise NotFound with self.isso.lock: - self.comments.unsubscribe(id) + self.comments.unsubscribe(email, id) modal = ( ""