Skip to content
Browse files

sharing

  • Loading branch information...
1 parent b448c7b commit 7ce107f2ac9e335543999c48c48a82b13948773c @KeyserSosa KeyserSosa committed
View
10 r2/example.ini
@@ -36,6 +36,11 @@ change_db_host = 127.0.0.1
change_db_user = reddit
change_db_pass = password
+email_db_name = email
+email_db_host = 127.0.0.1
+email_db_user = reddit
+email_db_pass = password
+
###
# Other magic settings
###
@@ -85,11 +90,16 @@ num_comments = 200
max_comments = 500
num_side_reddits = 20
+smtp_server = localhost
+new_link_share_delay = 5 minutes
+share_reply = noreply@yourdomain.com
+
#user-agents to limit
agents =
feedback_email = abuse@localhost
+
[server:main]
use = egg:Paste#http
host = 0.0.0.0
View
7 r2/r2/config/databases.py
@@ -53,6 +53,13 @@
pool_size = 2,
max_overflow = 2)
+email_engine = db_manager.get_engine(g.email_db_name,
+ db_host = g.email_db_host,
+ db_user = g.email_db_user,
+ db_pass = g.email_db_pass,
+ pool_size = 2,
+ max_overflow = 2)
+
dbm.type_db = main_engine
dbm.relation_type_db = main_engine
View
5 r2/r2/config/routing.py
@@ -90,10 +90,13 @@ def make_map(global_conf={}, app_conf={}):
mc('/info/:article/details', controller='front',
action = 'details')
+ mc('/mail/optout', controller='front', action = 'optout')
+ mc('/mail/optin', controller='front', action = 'optin')
+
mc('/', controller='hot', action='listing')
listing_controllers = "hot|saved|toplinks|new|recommended|normalized|randomrising"
-
+
mc('/:controller', action='listing',
requirements=dict(controller=listing_controllers))
View
38 r2/r2/controllers/api.py
@@ -615,6 +615,44 @@ def POST_comment(self, res, parent, comment, ip):
@Json
@validate(VUser(),
VModhash(),
+ VCaptcha(),
+ VRatelimit(rate_user = True, rate_ip = True,
+ prefix = "rate_share_"),
+ share_from = VLength('share_from', length = 60),
+ emails = ValidEmails("share_to"),
+ thing = VByName('id'))
+ def POST_share(self, res, emails, thing, share_from):
+
+ # remove the ratelimit error if the user's karma is high
+ sr = thing.subreddit_slow
+ should_ratelimit = sr.should_ratelimit(c.user, 'link')
+ if not should_ratelimit:
+ c.errors.remove(errors.RATELIMIT)
+
+ res._hide("status_" + thing._fullname)
+
+ if res._chk_captcha(errors.BAD_CAPTCHA, thing._fullname):
+ pass
+ elif not res._chk_errors((errors.BAD_EMAILS, errors.NO_EMAILS,
+ errors.RATELIMIT, errors.TOO_MANY_EMAILS),
+ thing._fullname):
+
+ c.user.add_share_emails(emails)
+ c.user._commit()
+
+ res._update("share_li_" + thing._fullname,
+ innerHTML=_('shared'))
+ res._hide("sharelink_" + thing._fullname)
+
+ emailer.share(thing, emails, from_name = share_from or "")
+
+ #set the ratelimiter
+ if should_ratelimit:
+ VRatelimit.ratelimit(rate_user=True, rate_ip = True, prefix = "rate_share_")
+
+ @Json
+ @validate(VUser(),
+ VModhash(),
vote_type = VVotehash(('vh', 'id')),
ip = ValidIP(),
dir = VInt('dir', min=-1, max=1),
View
3 r2/r2/controllers/errors.py
@@ -60,6 +60,9 @@
('INVALID_SUBREDDIT_TYPE', _('that option is not valid')),
('DESC_TOO_LONG', _('description is too long')),
('CHEATER', 'what do you think you\'re doing there?'),
+ ('BAD_EMAILS', _('the following emails are invalid: %(emails)s')),
+ ('NO_EMAILS', _('please enter at least one email address')),
+ ('TOO_MANY_EMAILS', _('please only share to %(num)s emails at a time.')),
))
errors = Storage([(e, e) for e in error_list.keys()])
View
23 r2/r2/controllers/front.py
@@ -27,6 +27,7 @@
from r2.models import *
from r2.lib.pages import *
from r2.lib.menus import *
+from r2.lib.emailer import opt_out, opt_in
from r2.lib.utils import to36, sanitize_url, check_cheating
from r2.lib.db.operators import desc
from r2.lib.strings import strings
@@ -534,3 +535,25 @@ def GET_submit(self, url, title):
title=title or '',
subreddits = sr_names,
captcha=captcha)).render()
+
+ @validate(msg_hash = nop('x'))
+ def GET_optout(self, msg_hash):
+ email, sent = opt_out(msg_hash)
+ if not email:
+ return self.abort404()
+ return BoringPage(_("opt out"),
+ content = OptOut(email = email, leave = True,
+ sent = sent,
+ msg_hash = msg_hash)).render()
+
+
+ @validate(msg_hash = nop('x'))
+ def GET_optin(self, msg_hash):
+ email, sent = opt_in(msg_hash)
+ if not email:
+ return self.abort404()
+ return BoringPage(_("welcome back"),
+ content = OptOut(email = email, leave = False,
+ sent = sent,
+ msg_hash = msg_hash)).render()
+
View
3 r2/r2/controllers/listingcontroller.py
@@ -409,7 +409,8 @@ def query(self):
c.user._commit()
elif self.where == 'sent':
- q = Message._query(Message.c.author_id == c.user._id)
+ q = Message._query(Message.c.author_id == c.user._id,
+ Message.c._spam == (True, False))
q._sort = desc('_date')
return q
View
22 r2/r2/controllers/validator/validator.py
@@ -632,6 +632,28 @@ def run(self, reason):
sr_onoff = dict((sr, fullnames[sr._fullname] == 1) for sr in srs)
return ('subscribe', sr_onoff)
+
+class ValidEmails(Validator):
+ separator = re.compile(r'[^\s,;]+')
+ email_re = re.compile(r'.+@.+\..+')
+
+ def __init__(self, param, num = 20, **kw):
+ self.num = num
+ Validator.__init__(self, param = param, **kw)
+
+ def run(self, emails):
+ emails = set(self.separator.findall(emails) if emails else [])
+ failures = set(e for e in emails if not self.email_re.match(e))
+ emails = emails - failures
+ if failures:
+ c.errors.add(errors.BAD_EMAILS, {'emails': ', '.join(failures)})
+ elif not emails:
+ c.errors.add(errors.NO_EMAILS)
+ elif len(emails) > self.num:
+ c.errors.add(errors.TOO_MANY_EMAILS, {'num': self.num})
+ else:
+ return emails
+
# NOTE: make sure *never* to have res check these are present
# otherwise, the response could contain reference to these errors...!
class ValidIP(Validator):
View
111 r2/r2/lib/emailer.py
@@ -20,28 +20,36 @@
# CondeNet, Inc. All Rights Reserved.
################################################################################
from email.MIMEText import MIMEText
-from pylons import c,g
-from pages import PasswordReset
-from r2.models.account import passhash
+from pylons.i18n import _
+from pylons import c, g, request
+from r2.lib.pages import PasswordReset, Share, Mail_Opt
+from r2.lib.utils import timeago
+from r2.models import passhash, Email, Default
from r2.config import cache
-import os, random
+import os, random, datetime
+import smtplib
def email_address(name, address):
return '"%s" <%s>' % (name, address) if name else address
-
feedback = email_address('reddit feedback', g.feedback_email)
-def simple_email(to, fr, subj, body):
- msg = MIMEText(body)
- msg['Subject'] = subj
- msg['From'] = fr
- msg['To'] = to
- assert not fr.startswith('-') and not to.startswith('-'), 'security'
- i, o = os.popen2(["/usr/sbin/sendmail", '-f', fr, to])
- i.write(msg.as_string())
- i.close()
- o.close()
- del i, o
+def send_mail(msg, fr, to, test = False):
+ if not test:
+ session = smtplib.SMTP(g.smtp_server)
+ session.sendmail(fr, to, msg.as_string())
+ session.quit()
+ else:
+ g.log.debug(msg.as_string())
+
+def simple_email(to, fr, subj, body, test = False):
+ def utf8(s):
+ return s.encode('utf8') if isinstance(s, unicode) else s
+ msg = MIMEText(utf8(body))
+ msg.set_charset('utf8')
+ msg['To'] = utf8(to)
+ msg['From'] = utf8(fr)
+ msg['Subject'] = utf8(subj)
+ send_mail(msg, fr, to, test = test)
def sys_email(email, body, name='', subj = lambda x: x):
fr = (c.user.name if c.user else 'Anonymous user')
@@ -70,3 +78,74 @@ def password_email(user):
'reddit.com password reset',
PasswordReset(user=user, passlink=passlink).render(style='email'))
+def share(link, emails, from_name = ""):
+ now = datetime.datetime.now(g.tz)
+ ival = now - timeago(g.new_link_share_delay)
+ date = max(now,link._date + ival)
+ Email.handler.add_to_queue(c.user, link, emails, from_name, date,
+ request.ip, Email.Kind.SHARE)
+
+def send_queued_mail():
+ now = datetime.datetime.now(g.tz)
+ if not c.site:
+ c.site = Default
+
+ clear = False
+ session = smtplib.SMTP(g.smtp_server)
+ try:
+ for email in Email.get_unsent(now):
+ clear = True
+ if not email.should_queue():
+ continue
+ elif email.kind == Email.Kind.SHARE:
+ email.fr_addr = g.share_reply
+ email.body = Share(username = email.from_name(),
+ msg_hash = email.msg_hash,
+ link = email.thing).render(style = "email")
+ email.subject = _("[reddit] %(user)s has shared a link with you") % \
+ {"user": email.from_name()}
+ session.sendmail(email.fr_addr, email.to_addr,
+ email.to_MIMEText().as_string())
+ elif email.kind == Email.Kind.OPTOUT:
+ email.fr_addr = g.share_reply
+ email.body = Mail_Opt(msg_hash = email.msg_hash,
+ leave = True).render(style = "email")
+ email.subject = _("[reddit] email removal notice")
+ session.sendmail(email.fr_addr, email.to_addr,
+ email.to_MIMEText().as_string())
+
+ elif email.kind == Email.Kind.OPTIN:
+ email.fr_addr = g.share_reply
+
+ email.body = Mail_Opt(msg_hash = email.msg_hash,
+ leave = False).render(style = "email")
+ email.subject = _("[reddit] email addition notice")
+ session.sendmail(email.fr_addr, email.to_addr,
+ email.to_MIMEText().as_string())
+
+ else:
+ # handle other types of emails here
+ pass
+ email.set_sent()
+ finally:
+ session.quit()
+ if clear:
+ Email.handler.clear_queue(now)
+
+
+
+def opt_out(msg_hash):
+ email, added = Email.handler.opt_out(msg_hash)
+ if email and added:
+ Email.handler.add_to_queue(None, None, [email], "reddit.com",
+ datetime.datetime.now(g.tz),
+ '127.0.0.1', Email.Kind.OPTOUT)
+ return email, added
+
+def opt_in(msg_hash):
+ email, removed = Email.handler.opt_in(msg_hash)
+ if email and removed:
+ Email.handler.add_to_queue(None, None, [email], "reddit.com",
+ datetime.datetime.now(g.tz),
+ '127.0.0.1', Email.Kind.OPTIN)
+ return email, removed
View
12 r2/r2/lib/jsonresponse.py
@@ -108,16 +108,18 @@ def _chk_error(self, error_name, err_on_thing = ''):
self._clear_error(error_name, err_on_thing)
return False
- def _chk_errors(self, errors):
+ def _chk_errors(self, errors, err_on_thing = ''):
if errors:
return reduce(lambda x, y: x or y,
- [self._chk_error(e) for e in errors])
+ [self._chk_error(e, err_on_thing = err_on_thing) for e in errors])
return False
- def _chk_captcha(self, err):
- if self._chk_error(err):
- self.captcha = {'iden' : get_iden(), 'refresh' : True}
+ def _chk_captcha(self, err, err_on_thing = ''):
+ if self._chk_error(err, err_on_thing):
+ self.captcha = {'iden' : get_iden(), 'refresh' : True, 'id': err_on_thing}
self._focus('captcha')
+ return True
+ return False
@property
def response(self):
View
3 r2/r2/lib/menus.py
@@ -125,6 +125,7 @@ def __getattr__(self, attr):
reports = _("reports"),
reportedauth = _("reported authors"),
info = _("info"),
+ share = _("share"),
overview = _("overview"),
submitted = _("submitted"),
@@ -488,6 +489,6 @@ class AdminKindMenu(KindMenu):
class AdminTimeMenu(TimeMenu):
get_param = 't'
default = 'day'
- options = ('hour', 'day', 'week', 'month', 'year')
+ options = ('hour', 'day', 'week')
View
27 r2/r2/lib/pages/pages.py
@@ -91,7 +91,12 @@ def __init__(self, space_compress = True, nav_menus = None, loginbox = True,
self.subreddit_sidebox = True
self.subreddit_checkboxes = c.site == Default
- self._content = content
+ if c.user_is_loggedin:
+ self._content = PaneStack([ShareLink(), content])
+ else:
+ self._content = content
+
+
self.toolbars = self.build_toolbars()
def rightbox(self):
@@ -739,6 +744,26 @@ def __init__(self, captcha = None, url = '', title= '', subreddits = ()):
Wrapped.__init__(self, captcha = captcha, url = url,
title = title, subreddits = subreddits)
+class ShareLink(Wrapped):
+ def __init__(self, link_name = "", emails = None):
+ captcha = Captcha() if c.user.needs_captcha() else None
+ Wrapped.__init__(self, link_name = link_name,
+ emails = c.user.recent_share_emails(),
+ captcha = captcha)
+
+class Share(Wrapped):
+ pass
+
+class Mail_Opt(Wrapped):
+ pass
+
+class OptOut(Wrapped):
+ pass
+
+class OptIn(Wrapped):
+ pass
+
+
class UserStats(Wrapped):
"""For drawing the stats page, which is fetched from the cache."""
def __init__(self):
View
1 r2/r2/models/__init__.py
@@ -26,6 +26,7 @@
from vote import *
from report import *
from subreddit import *
+from mail_queue import Email
from admintools import *
import thing_changes
View
25 r2/r2/models/account.py
@@ -59,6 +59,7 @@ class Account(Thing):
sort_options = {},
has_subscribed = False,
pref_media = 'off',
+ share = {},
)
def karma(self, kind, sr = None):
@@ -194,6 +195,30 @@ def subreddits(self):
from subreddit import Subreddit
return Subreddit.user_subreddits(self)
+ def recent_share_emails(self):
+ return self.share.get('recent', set([]))
+
+ def add_share_emails(self, emails):
+ if not emails:
+ return
+
+ if not isinstance(emails, set):
+ emails = set(emails)
+
+ self.share.setdefault('emails', {})
+ share = self.share.copy()
+
+ share_emails = share['emails']
+ for e in emails:
+ share_emails[e] = share_emails.get(e, 0) +1
+
+ share['recent'] = emails
+
+ self.share = share
+
+
+
+
class FakeAccount(Account):
_nodb = True
View
8 r2/r2/models/link.py
@@ -299,7 +299,9 @@ def _new(cls, author, link, parent, body, ip, spam = False):
if parent:
to = Account._byID(parent.author_id)
- i = Inbox._add(to, c, 'inbox')
+ # only global admins can be message spammed.
+ if not c._spam or to.name in g.admins:
+ i = Inbox._add(to, c, 'inbox')
#clear that chache
clear_memo('builder.link_comments2', link._id)
@@ -473,7 +475,9 @@ def _new(cls, author, to, subject, body, ip, spam = False):
#author = Author(author, m, 'author')
#author._commit()
- i = Inbox._add(to, m, 'inbox')
+ # only global admins can be message spammed.
+ if not m._spam or to.name in g.admins:
+ i = Inbox._add(to, m, 'inbox')
from admintools import admintools
utils.worker.do(lambda: admintools.add_thing(m))
View
342 r2/r2/models/mail_queue.py
@@ -0,0 +1,342 @@
+# "The contents of this file are subject to the Common Public Attribution
+# License Version 1.0. (the "License"); you may not use this file except in
+# compliance with the License. You may obtain a copy of the License at
+# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
+# License Version 1.1, but Sections 14 and 15 have been added to cover use of
+# software over a computer network and provide for limited attribution for the
+# Original Developer. In addition, Exhibit A has been modified to be consistent
+# with Exhibit B.
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
+# the specific language governing rights and limitations under the License.
+#
+# The Original Code is Reddit.
+#
+# The Original Developer is the Initial Developer. The Initial Developer of the
+# Original Code is CondeNet, Inc.
+#
+# All portions of the code written by CondeNet are Copyright (c) 2006-2008
+# CondeNet, Inc. All Rights Reserved.
+##############################################################################
+from r2.config.databases import email_engine
+from r2.lib.db.tdb_sql import make_metadata, settings
+from sqlalchemy.databases.postgres import PGInet, PGBigInteger
+from r2.models.thing_changes import changed, index_str, create_table
+import sqlalchemy as sa
+import datetime
+from r2.lib.utils import Storage, timeago
+from account import Account
+from r2.lib.db.thing import Thing
+from email.MIMEText import MIMEText
+import sha
+from r2.lib.memoize import memoize, clear_memo
+
+
+def mail_queue(metadata):
+ return sa.Table(settings.DB_APP_NAME + '_mail_queue', metadata,
+ sa.Column("uid", sa.Integer,
+ sa.Sequence('queue_id_seq'), primary_key=True),
+
+ # unique hash of the message to carry around
+ sa.Column("msg_hash", sa.String),
+
+ # the id of the account who started it
+ sa.Column('account_id', PGBigInteger),
+
+ # the name (not email) for the from
+ sa.Column('from_name', sa.String),
+
+ # the "To" address of the email
+ sa.Column('to_addr', sa.String),
+
+ # fullname of the thing
+ sa.Column('fullname', sa.String),
+
+ # when added to the queue
+ sa.Column('date',
+ sa.DateTime(timezone = True),
+ nullable = False),
+
+ # IP of original request
+ sa.Column('ip', PGInet),
+
+ # enum of kind of event
+ sa.Column('kind', sa.Integer),
+
+ # any message that may have been included
+ sa.Column('body', sa.String),
+
+ )
+
+def sent_mail_table(metadata):
+ return sa.Table(settings.DB_APP_NAME + '_sent_mail', metadata,
+ # tracking hash of the email
+ sa.Column('msg_hash', sa.String, primary_key=True),
+
+ # the account who started it
+ sa.Column('account_id', PGBigInteger),
+
+ # the "To" address of the email
+ sa.Column('to_addr', sa.String),
+
+ # IP of original request
+ sa.Column('ip', PGInet),
+
+ # fullname of the reference thing
+ sa.Column('fullname', sa.String),
+
+ # send date
+ sa.Column('date',
+ sa.DateTime(timezone = True),
+ default = sa.func.now(),
+ nullable = False),
+
+ # enum of kind of event
+ sa.Column('kind', sa.Integer),
+
+ )
+
+
+def opt_out(metadata):
+ return sa.Table(settings.DB_APP_NAME + '_opt_out', metadata,
+ sa.Column('email', sa.String, primary_key = True),
+ # when added to the list
+ sa.Column('date',
+ sa.DateTime(timezone = True),
+ default = sa.func.now(),
+ nullable = False),
+ # why did they do it!?
+ sa.Column('msg_hash', sa.String),
+ )
+
+class EmailHandler(object):
+ def __init__(self, force = False):
+ self.metadata = make_metadata(email_engine)
+ self.queue_table = mail_queue(self.metadata)
+ indices = [index_str(self.queue_table, "date", "date"),
+ index_str(self.queue_table, 'kind', 'kind')]
+ create_table(self.queue_table, indices, force = force)
+
+ self.opt_table = opt_out(self.metadata)
+ indices = [index_str(self.opt_table, 'email', 'email')]
+ create_table(self.opt_table, indices, force = force)
+
+ self.track_table = sent_mail_table(self.metadata)
+ indices = [index_str(self.track_table, 'to_addr', 'to_addr'),
+ index_str(self.track_table, 'date', 'date'),
+ index_str(self.track_table, 'ip', 'ip'),
+ index_str(self.track_table, 'kind', 'kind'),
+ index_str(self.track_table, 'fullname', 'fullname'),
+ index_str(self.track_table, 'account_id', 'account_id'),
+ index_str(self.track_table, 'msg_hash', 'msg_hash'),
+ ]
+ create_table(self.track_table, indices, force = force)
+
+ def __repr__(self):
+ return "<email-handler>"
+
+ def has_opted_out(self, email):
+ o = self.opt_table
+ s = sa.select([o.c.email], o.c.email == email, limit = 1)
+ res = s.execute()
+ return bool(res.fetchall())
+
+ def opt_out(self, msg_hash):
+ """Adds the recipient of the email to the opt-out list and returns
+ that address."""
+ email = self.get_recipient(msg_hash)
+ if email:
+ o = self.opt_table
+ try:
+ o.insert().execute({o.c.email: email, o.c.msg_hash: msg_hash})
+ clear_memo('r2.models.mail_queue.has_opted_out',
+ email)
+ return (email, True)
+ except sa.exceptions.SQLError:
+ return (email, False)
+ return (None, False)
+
+ def opt_in(self, msg_hash):
+ """Removes recipient of the email from the opt-out list"""
+ email = self.get_recipient(msg_hash)
+ if email:
+ o = self.opt_table
+ if self.has_opted_out(email):
+ sa.delete(o, o.c.email == email).execute()
+ clear_memo('r2.models.mail_queue.has_opted_out',
+ email)
+ return (email, True)
+ else:
+ return (email, False)
+ return (None, False)
+
+ def get_recipient(self, msg_hash):
+ t = self.track_table
+ s = sa.select([t.c.to_addr], t.c.msg_hash == msg_hash).execute()
+ res = s.fetchall()
+ return res[0][0] if res and res[:1] else None
+
+
+ def add_to_queue(self, user, thing, emails, from_name, date, ip,
+ kind, body = ""):
+ s = self.queue_table
+ hashes = []
+ for email in emails:
+ uid = user._id if user else 0
+ tid = thing._fullname if thing else ""
+ key = sha.new(str((email, from_name, uid, tid, ip, kind, body,
+ datetime.datetime.now()))).hexdigest()
+ s.insert().execute({s.c.to_addr : email,
+ s.c.account_id : uid,
+ s.c.from_name : from_name,
+ s.c.fullname: tid,
+ s.c.ip : ip,
+ s.c.kind: kind,
+ s.c.body: body,
+ s.c.date : date,
+ s.c.msg_hash : key})
+ hashes.append(key)
+ return hashes
+
+
+ def from_queue(self, max_date, batch_limit = 50, kind = None):
+ from r2.models import is_banned_IP, Account, Thing
+ keep_trying = True
+ min_id = None
+ s = self.queue_table
+ while keep_trying:
+ where = [s.c.date < max_date]
+ if min_id:
+ where.append(s.c.uid > min_id)
+ if kind:
+ where.append(s.c.kind == kind)
+
+ res = sa.select([s.c.to_addr, s.c.account_id,
+ s.c.from_name, s.c.fullname, s.c.body,
+ s.c.kind, s.c.ip, s.c.date, s.c.uid,
+ s.c.msg_hash],
+ sa.and_(*where),
+ order_by = s.c.uid, limit = batch_limit).execute()
+ res = res.fetchall()
+
+ if not res: break
+
+ # batch load user accounts
+ aids = [x[1] for x in res if x[1] > 0]
+ accts = Account._byID(aids, data = True,
+ return_dict = True) if aids else {}
+
+ # batch load things
+ tids = [x[3] for x in res if x[3]]
+ things = Thing._by_fullname(tids, data = True,
+ return_dict = True) if tids else {}
+
+ # make sure no IPs have been banned in the mean time
+ ips = set(x[6] for x in res)
+ ips = dict((ip, is_banned_IP(ip)) for ip in ips)
+
+ # get the lower bound date for next iteration
+ min_id = max(x[8] for x in res)
+
+ # did we not fetch them all?
+ keep_trying = (len(res) == batch_limit)
+
+ for addr, acct, fname, fulln, body, kind, ip, date, uid, msg_hash \
+ in res:
+ yield (accts.get(acct), things.get(fulln), addr,
+ fname, date, ip, ips[ip], kind, msg_hash, body)
+
+ def clear_queue(self, max_date, kind = None):
+ s = self.queue_table
+ where = [s.c.date < max_date]
+ if kind:
+ where.append([s.c.kind == kind])
+ sa.delete(s, sa.and_(*where)).execute()
+
+
+class Email(object):
+ handler = EmailHandler()
+
+ Kind = ["SHARE", "FEEDBACK", "ADVERTISE", "OPTOUT", "OPTIN"]
+ Kind = Storage((e, i) for i, e in enumerate(Kind))
+
+ def __init__(self, user, thing, email, from_name, date, ip, banned_ip,
+ kind, msg_hash, body = '', subject = "", from_addr = ''):
+ self.user = user
+ self.thing = thing
+ self.to_addr = email
+ self.fr_addr = from_addr
+ self._from_name = from_name
+ self.date = date
+ self.ip = ip
+ self.banned_ip = banned_ip
+ self.kind = kind
+ self.sent = False
+ self.body = ""
+ self.subject = subject
+ self.msg_hash = msg_hash
+
+ def from_name(self):
+ return ("%(name)s (%(uname)s)" if self._from_name != self.user.name
+ else "%(uname)s") % \
+ dict(name = self._from_name, uname = self.user.name)
+
+ @classmethod
+ def get_unsent(cls, max_date, batch_limit = 50, kind = None):
+ for e in cls.handler.from_queue(max_date, batch_limit = batch_limit,
+ kind = kind):
+ yield cls(*e)
+
+ def should_queue(self):
+ return (not self.user or not self.user._spam) and \
+ (not self.thing or not self.thing._spam) and \
+ not self.banned_ip and \
+ (self.kind == self.Kind.OPTOUT or
+ not has_opted_out(self.to_addr))
+
+ def set_sent(self, date = None):
+ if not self.sent:
+ from pylons import g
+ self.date = date or datetime.datetime.now(g.tz)
+ t = self.handler.track_table
+ t.insert().execute({t.c.account_id:
+ self.user._id if self.user else 0,
+ t.c.to_addr : self.to_addr,
+ t.c.ip : self.ip,
+ t.c.fullname:
+ self.thing._fullname if self.thing else "",
+ t.c.date: self.date,
+ t.c.kind : self.kind,
+ t.c.msg_hash : self.msg_hash,
+ })
+ self.sent = True
+
+ def to_MIMEText(self):
+ def utf8(s):
+ return s.encode('utf8') if isinstance(s, unicode) else s
+ fr = '"%s" <%s>' % (self._from_name, self.fr_addr) if self._from_name else self.fr_addr
+ if not fr.startswith('-') and not self.to_addr.startswith('-'): # security
+ msg = MIMEText(utf8(self.body))
+ msg.set_charset('utf8')
+ msg['To'] = utf8(self.to_addr)
+ msg['From'] = utf8(fr)
+ msg['Subject'] = utf8(self.subject)
+ if self.user:
+ msg['X-Reddit-username'] = utf8(self.user.name)
+ msg['X-Reddit-ID'] = self.msg_hash
+ return msg
+ return None
+
+@memoize('r2.models.mail_queue.has_opted_out')
+def has_opted_out(email):
+ o = Email.handler.opt_table
+ s = sa.select([o.c.email], o.c.email == email, limit = 1)
+ res = s.execute()
+ return bool(res.fetchall())
+
+
+
+
+
+
View
117 r2/r2/public/static/inbound-email-policy.html
@@ -0,0 +1,117 @@
+<html>
+<head>
+<title>reddit email policy</title>
+<body>
+
+<div style="width: 80ex">
+<p>
+Section 17538.45 of the California Business and Professional Code allows
+Electronic Mail Service Providers to restrict or prohibit use of their
+equipment for Unsolicited Electronic Mail Advertising (UEMA), when the
+equipment is located in the state of California. It further provides
+for liquidated damages of $50 per message received. reddit.com uses
+equipment located in California, and the reddit.com policy is that
+Unsolicited Electronic Mail Advertising is prohibited.
+</p>
+<hr>
+<p>Text of section 17538.45 of the Business and Professional Code of the
+State of California, copied on July 17th, 2008. Check
+with the <a HREF="http://www.leginfo.ca.gov/calaw.html">California
+Law</a> website for the current text (search for section 17538 or "unsolicited"
+in the Business and Professions Code).
+</p>
+</div>
+<pre>
+17538.45. (a) For purposes of this section, the following words
+have the following meanings:
+ (1) "Electronic mail advertisement" means any electronic mail
+message, the principal purpose of which is to promote, directly or
+indirectly, the sale or other distribution of goods or services to
+the recipient.
+ (2) "Unsolicited electronic mail advertisement" means any
+electronic mail advertisement that meets both of the following
+requirements:
+ (A) It is addressed to a recipient with whom the initiator does
+not have an existing business or personal relationship.
+ (B) It is not sent at the request of or with the express consent
+of the recipient.
+ (3) "Electronic mail service provider" means any business or
+organization qualified to do business in California that provides
+registered users the ability to send or receive electronic mail
+through equipment located in this state and that is an intermediary
+in sending or receiving electronic mail.
+ (4) "Initiation" of an unsolicited electronic mail advertisement
+refers to the action by the initial sender of the electronic mail
+advertisement. It does not refer to the actions of any intervening
+electronic mail service provider that may handle or retransmit the
+electronic message.
+ (5) "Registered user" means any individual, corporation, or other
+entity that maintains an electronic mail address with an electronic
+mail service provider.
+ (b) No registered user of an electronic mail service provider
+shall use or cause to be used that electronic mail service provider's
+equipment located in this state in violation of that electronic mail
+service provider's policy prohibiting or restricting the use of its
+service or equipment for the initiation of unsolicited electronic
+mail advertisements.
+ (c) No individual, corporation, or other entity shall use or cause
+to be used, by initiating an unsolicited electronic mail
+advertisement, an electronic mail service provider's equipment
+located in this state in violation of that electronic mail service
+provider's policy prohibiting or restricting the use of its equipment
+to deliver unsolicited electronic mail advertisements to its
+registered users.
+ (d) An electronic mail service provider shall not be required to
+create a policy prohibiting or restricting the use of its equipment
+for the initiation or delivery of unsolicited electronic mail
+advertisements.
+ (e) Nothing in this section shall be construed to limit or
+restrict the rights of an electronic mail service provider under
+Section 230(c)(1) of Title 47 of the United States Code, any decision
+of an electronic mail service provider to permit or to restrict
+access to or use of its system, or any exercise of its editorial
+function.
+ (f) (1) In addition to any other action available under law, any
+electronic mail service provider whose policy on unsolicited
+electronic mail advertisements is violated as provided in this
+section may bring a civil action to recover the actual monetary loss
+suffered by that provider by reason of that violation, or liquidated
+damages of fifty dollars ($50) for each electronic mail message
+initiated or delivered in violation of this section, up to a maximum
+of twenty-five thousand dollars ($25,000) per day, whichever amount
+is greater.
+ (2) In any action brought pursuant to paragraph (1), the court may
+award reasonable attorney's fees to a prevailing party.
+ (3) (A) In any action brought pursuant to paragraph (1), the
+electronic mail service provider shall be required to establish as an
+element of its cause of action that prior to the alleged violation,
+the defendant had actual notice of both of the following:
+ (i) The electronic mail service provider's policy on unsolicited
+electronic mail advertising.
+ (ii) The fact that the defendant's unsolicited electronic mail
+advertisements would use or cause to be used the electronic mail
+service provider's equipment located in this state.
+ (B) In this regard, the Legislature finds that with rapid advances
+in Internet technology, and electronic mail technology in
+particular, Internet service providers are already experimenting with
+embedding policy statements directly into the software running on
+the computers used to provide electronic mail services in a manner
+that displays the policy statements every time an electronic mail
+delivery is requested. While the state of the technology does not
+support this finding at present, the Legislature believes that, in a
+given case at some future date, a showing that notice was supplied
+via electronic means between the sending and receiving computers
+could be held to constitute actual notice to the sender for purposes
+of this paragraph.
+ (4) (A) An electronic mail service provider who has brought an
+action against a party for a violation under Section 17529.8 shall
+not bring an action against that party under this section for the
+same unsolicited commercial electronic mail advertisement.
+ (B) An electronic mail service provider who has brought an action
+against a party for a violation of this section shall not bring an
+action against that party under Section 17529.8 for the same
+unsolicited commercial electronic mail advertisement.
+</pre>
+</body>
+</html>
+
View
81 r2/r2/public/static/link.js
@@ -450,6 +450,11 @@ function Link(id) {
Link.prototype = new Thing();
+Link.prototype.share = function() {
+ var share = new ShareLink(this._id);
+ share.attach(this.$("child"));
+};
+
// Commenting on a link is handled by the Comment API so defer to it
Link.comment = Comment.comment;
@@ -480,3 +485,79 @@ function setClickCookie(id) {
createCookie("click", readCookie("click") + id + ":");
}
+
+function ThingForm(id) {
+ this.__init__(id);
+};
+
+ThingForm.prototype = {
+ __init__: function(id) {
+ this._id = id;
+ this.form = $(this.__name__ + "_" + id);
+ if (!this.form) {
+ var p = this.__prototype__();
+ if (p) {
+ this.form = re_id_node(p.cloneNode(true), id);
+ }
+ } else {
+ show(this.form);
+ }
+ },
+
+ __name__ : "",
+
+ __prototype__: function() {
+ return $(this.__name__ + '_');
+ },
+
+ $: function(name) {
+ return $(name + '_' + this._id);
+ },
+
+ cancel: function() {
+ hide(this.form);
+ return false;
+ },
+
+ ok: function() {
+ return true;
+ },
+
+ attach: function(where) {
+ if (this.form.parentNode) {
+ if(this.form.parentNode != where) {
+ // TODO
+ }
+ }
+ else {
+ where.insertBefore(this.form, where.firstChild);
+ }
+ show(this.form);
+ }
+
+};
+
+function ShareLink(id) {
+ this.__init__(id);
+};
+
+ShareLink.prototype = new ThingForm();
+ShareLink.prototype.__name__ = "sharelink";
+
+ShareLink.prototype.ok = function() {
+ var p = this.__prototype__();
+ var v = this.$("share_to").value.replace(/[\s,;]+/g, "\r\n");
+ p.firstChild.share_to.value = v;
+ p.firstChild.share_to.innerHTML = v;
+ return true;
+};
+
+function share(id) {
+ if (logged) {
+ new Link(id).share();
+ }
+ else {
+ showcover(true, 'share_' + id);
+ }
+};
+
View
4 r2/r2/public/static/reddit.css
@@ -415,7 +415,7 @@ before enabling */
.raisedbox #avatar a { padding: 0px; }
*/
-.infotable { margin-top: 5px; }
+.infotable { margin-top: 5px; margin-bottom: 10px; }
.infotable .small { font-size: smaller; }
.infotable td { padding-right: 3px; }
.infotable a:hover { text-decoration: underline }
@@ -826,6 +826,8 @@ a.star { text-decoration: none; color: #ff8b60 }
.clearleft { clear: left; height: 0px; }
.clear { clear: both; }
+.sharetable {margin-left: 20px; }
+
.sponsored .entry { margin-right: 20px;}
.sponsored .titlerow { background: #fcfcfc;
View
17 r2/r2/public/static/utils.js
@@ -1,5 +1,5 @@
function unsafe(text) {
- text = text || "";
+ text = text.replace?text:"";
return text.replace(/&gt;/g, ">").replace(/&lt;/g, "<").replace(/&amp;/g, "&");
}
@@ -206,8 +206,9 @@ function handleResponse(action) {
}
if (r.captcha) {
if (r.captcha.refresh) {
- var captcha = $("capimage");
- var capiden = $("capiden");
+ var id = r.captcha.id;
+ var captcha = $("capimage" + (id?('_'+id):''));
+ var capiden = $("capiden" + (id?('_'+id):''));
capiden.value = r.captcha.iden;
captcha.src = ("/captcha/" + r.captcha.iden + ".png?" +
Math.random())
@@ -238,7 +239,15 @@ function handleResponse(action) {
}
function re_id_node(node, id) {
- if(node.id && typeof(node.id) == "string") { node.id += id; }
+ function add_id(s) {
+ if(s && typeof(s) == "string") {
+ if(s[s.length-1] != '_') s += '_';
+ s += id;
+ }
+ return s;
+ }
+ node.id = add_id(node.id);
+ node.htmlFor = add_id(node.htmlFor);
var children = node.childNodes;
for(var i = 0; i < children.length; i++) {
re_id_node(children[i], id);
View
2 r2/r2/templates/base.html
@@ -17,7 +17,7 @@
## the Original Code is CondeNet, Inc.
##
## All portions of the code written by CondeNet are Copyright (c) 2006-2008
-## CondeNet, Inc. All Rights Reserved.
+## CondeNet, Inc. All Rights Reserved."
################################################################################
<%!
View
35 r2/r2/templates/captcha.html
@@ -17,7 +17,7 @@
## the Original Code is CondeNet, Inc.
##
## All portions of the code written by CondeNet are Copyright (c) 2006-2008
-## CondeNet, Inc. All Rights Reserved.
+## CondeNet, Inc. All Rights Reserved."
################################################################################
<%! from r2.lib.template_helpers import static %>
@@ -25,39 +25,58 @@
<%namespace file="utils.html" import="error_field"/>
${captchagen(thing.iden, thing.error)}
-<%def name="captchagen(iden, error='', tabulate=False, size=60, label=True)">
+<%def name="captchagen(iden, error='', tabulate=False, tabular = True, size=60, label=True, show_error = True)">
%if tabulate:
<table>
%endif
+ %if tabular:
<tr>
<td></td>
<td>
+ %endif
<img id="capimage"
+ class="capimage"
alt="i wonder if these things even work"
%if iden:
src="/captcha/${iden}.png"
%else:
src="${static('kill.png')}"
%endif
- /></td>
+ />
+ %if tabular:
+ </td>
</tr>
<tr>
<td align="right">
+ %else:
+ <span class="cap-reply">
+ %endif
%if label:
- <label>${_("human?")}</label>
+ <label for="captcha">${_("human?")}</label>
%endif
+ %if tabular:
</td>
<td>
+ %endif
<input id="capiden" name="iden" type="hidden" value="${iden}"></input>
<input id="captcha" name="captcha" type="text" size="${size}"
- onfocus="clearTitle(this)"/>
+ class="cap-text" onfocus="clearTitle(this)"/>
<script type="text/javascript">
setMessage($('captcha'), '${_("type the letters from the image above")}');
</script>
- </td><td>
- ${error_field("BAD_CAPTCHA", "span")}
- </td>
+ %if tabulatar:
+ </td>
+ <td>
+ %else:
+ </span>
+ %endif
+ %if show_error:
+ ${error_field("BAD_CAPTCHA", "span")}
+ %endif
+ %if tabulatar:
+ </td>
</tr>
+ %endif
%if tabulate:
</table>
%endif
View
5 r2/r2/templates/link.html
@@ -171,6 +171,11 @@
${parent.delete_or_report_buttons()}
${parent.buttons()}
${self.media_embed()}
+ %if c.user_is_loggedin:
+ <li id="share_li_${fullname}">
+ ${parent.simple_button("share", fullname, _("share"), "share")}
+ </li>
+ %endif
</%def>
View
34 r2/r2/templates/mail_opt.email
@@ -0,0 +1,34 @@
+## The contents of this file are subject to the Common Public Attribution
+## License Version 1.0. (the "License"); you may not use this file except in
+## compliance with the License. You may obtain a copy of the License at
+## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
+## License Version 1.1, but Sections 14 and 15 have been added to cover use of
+## software over a computer network and provide for limited attribution for the
+## Original Developer. In addition, Exhibit A has been modified to be consistent
+## with Exhibit B.
+##
+## Software distributed under the License is distributed on an "AS IS" basis,
+## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
+## the specific language governing rights and limitations under the License.
+##
+## The Original Code is Reddit.
+##
+## The Original Developer is the Initial Developer. The Initial Developer of
+## the Original Code is CondeNet, Inc.
+##
+## All portions of the code written by CondeNet are Copyright (c) 2006-2008
+## CondeNet, Inc. All Rights Reserved.
+################################################################################
+%if thing.leave:
+
+Your email address will no longer receive email from us. If you want to reconsider, visit:
+
+http://${g.domain}/mail/optin?x=${thing.msg_hash}
+
+We promise not to hold it against you.
+
+%else:
+
+Your email address is once again to allowed to receive messages from ${g.domain}. Welcome back.
+
+%endif
View
2 r2/r2/templates/newlink.html
@@ -17,7 +17,7 @@
## the Original Code is CondeNet, Inc.
##
## All portions of the code written by CondeNet are Copyright (c) 2006-2008
-## CondeNet, Inc. All Rights Reserved.
+## CondeNet, Inc. All Rights Reserved."
################################################################################
<%namespace file="utils.html" import="error_field, submit_form, plain_link, text_with_links"/>
View
46 r2/r2/templates/optout.html
@@ -0,0 +1,46 @@
+## "The contents of this file are subject to the Common Public Attribution
+## License Version 1.0. (the "License"); you may not use this file except in
+## compliance with the License. You may obtain a copy of the License at
+## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
+## License Version 1.1, but Sections 14 and 15 have been added to cover use of
+## software over a computer network and provide for limited attribution for the
+## Original Developer. In addition, Exhibit A has been modified to be consistent
+## with Exhibit B.
+##
+## Software distributed under the License is distributed on an "AS IS" basis,
+## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
+## the specific language governing rights and limitations under the License.
+##
+## The Original Code is Reddit.
+##
+## The Original Developer is the Initial Developer. The Initial Developer of the
+## Original Code is CondeNet, Inc.
+##
+## All portions of the code written by CondeNet are Copyright (c) 2006-2008
+## CondeNet, Inc. All Rights Reserved."
+###############################################################################
+<div style="font-size:larger">
+%if thing.leave:
+ <p>
+ ${_("The address %(email)s will no longer recieve email from us.") % dict(email=thing.email)}
+ </p>
+
+ %if thing.sent:
+ <p>
+ ${_("A confirmation email has been queued up and should be reaching you shortly. It will be the last you hear of us.")}
+ </p>
+ %else:
+ <p>
+ ${_("A confirmation email has already been queued up and/or sent.")}
+ </p>
+ %endif
+%elif thing.sent:
+ <p>
+ ${_("The address %(email)s is once again fair game to receive email from us. Welcome back. You'll be receiving a confirmation email.") % dict(email=thing.email)}
+ </p>
+%else:
+ <p>
+ ${_("%(email)s has already been removed from our block list.") % dict(email=thing.email)}
+ </p>
+%endif
+</div>
View
36 r2/r2/templates/share.email
@@ -0,0 +1,36 @@
+## The contents of this file are subject to the Common Public Attribution
+## License Version 1.0. (the "License"); you may not use this file except in
+## compliance with the License. You may obtain a copy of the License at
+## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
+## License Version 1.1, but Sections 14 and 15 have been added to cover use of
+## software over a computer network and provide for limited attribution for the
+## Original Developer. In addition, Exhibit A has been modified to be consistent
+## with Exhibit B.
+##
+## Software distributed under the License is distributed on an "AS IS" basis,
+## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
+## the specific language governing rights and limitations under the License.
+##
+## The Original Code is Reddit.
+##
+## The Original Developer is the Initial Developer. The Initial Developer of
+## the Original Code is CondeNet, Inc.
+##
+## All portions of the code written by CondeNet are Copyright (c) 2006-2008
+## CondeNet, Inc. All Rights Reserved.
+################################################################################
+<%!
+ from r2.lib.template_helpers import reddit_link
+%>
+
+A user from http://${g.domain}/ has shared a link with you.
+
+"${thing.link.title}"
+http://${g.domain}/goto?share=true&id=${thing.link._fullname}
+
+There are currently ${thing.link.num_comments} comments on this link. You can view them here:
+
+http://${g.domain}${reddit_link(thing.link.permalink, url=True)}
+
+___
+If you would not like to receive emails from reddit.com in the future, visit http://${g.domain}/mail/optout?x=${thing.msg_hash}
View
69 r2/r2/templates/sharelink.html
@@ -0,0 +1,69 @@
+<%namespace file="printable.html" import="yes_no_button"/>
+<%namespace file="utils.html" import="error_field"/>
+<%namespace file="captcha.html" import="captchagen"/>
+
+<div id="sharelink_${thing.link_name}"
+ ${"" if thing.link_name else "style='display:none'"}>
+ <form onsubmit="return post_form(this, 'share')" method="post"
+ id="shareform_${thing.link_name}" class="pretty-form"
+ action="/post/share">
+ ${error_field("RATELIMIT_"+ thing.link_name)}
+ <table class="preftable sharetable">
+ <tr>
+ <th>
+ <label class="big" for="share_to_${thing.link_name}">
+ ${_("send this link to")}
+ </label>
+ </th>
+ <td>
+ <textarea id="share_to_${thing.link_name}" name="share_to" rows="4">
+ ${unsafe('&#x0D;&#x0A;'.join(websafe(e) for e in thing.emails))}
+ </textarea>
+ </td>
+ <td>
+ ${error_field("BAD_EMAILS_" + thing.link_name)}
+ ${error_field("TOO_MANY_EMAILS_" + thing.link_name)}
+ ${error_field("NO_EMAILS_" + thing.link_name)}
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="big" for="share_from_${thing.link_name}">
+ ${_("your name")}
+ </label>&nbsp;
+ <span class="little gray">
+ ${_("(optional)")}
+ </span>
+ </th>
+ <td>
+ <input class="real-name" value="${c.user.name}"
+ type="text" id="share_from_${thing.link_name}"
+ name="share_from" />
+ </td>
+ <td>
+ </td>
+ </tr>
+ %if thing.captcha:
+ ${captchagen(thing.captcha.iden, thing.captcha.error, tabulate = False, size = 30)}
+ %endif
+ <tr>
+ <td>
+ </td>
+ <td>
+ <span id='status_${thing.link_name}' class='error'></span>
+ <button id="share_submit_${thing.link_name}"
+ onclick="return new ShareLink(_id(this)).ok();"
+ type="submit" class="btn">
+ ${_("share")}
+ </button>
+ <button id="share_${thing.link_name}" class="btn"
+ onclick="return new ShareLink(_id(this)).cancel();">
+ ${_("cancel")}
+ </button>
+ </td>
+ <td>
+ </td>
+ </tr>
+ </table>
+ </form>
+</div>
View
2 r2/r2/templates/subredditinfobar.html
@@ -17,7 +17,7 @@
## the Original Code is CondeNet, Inc.
##
## All portions of the code written by CondeNet are Copyright (c) 2006-2008
-## CondeNet, Inc. All Rights Reserved.
+## CondeNet, Inc. All Rights Reserved."
################################################################################
<%!
View
1 r2/supervise_watcher.py
@@ -233,6 +233,7 @@ def Alert(restart_list=['MEM','CPU']):
session = smtplib.SMTP(smtpserver)
smtpresult = session.sendmail(alert_sender,
alert_recipients, mesg)
+ session.quit()
#print mesg
#print "Email sent"

0 comments on commit 7ce107f

Please sign in to comment.
Something went wrong with that request. Please try again.