mirror of
https://github.com/reddit-archive/reddit.git
synced 2026-01-28 00:07:57 -05:00
extended sharing to allow reply-to address and message. Also rolled feedback emails into mail_queue
This commit is contained in:
@@ -123,7 +123,7 @@ def make_map(global_conf={}, app_conf={}):
|
||||
action='resetpassword')
|
||||
|
||||
mc('/post/:action', controller='post',
|
||||
requirements=dict(action="options|over18|unlogged_options"))
|
||||
requirements=dict(action="options|over18|unlogged_options|optout|optin"))
|
||||
|
||||
mc('/api/:action', controller='api')
|
||||
mc('/d/:what', controller='api', action='bookmarklet')
|
||||
|
||||
@@ -124,19 +124,18 @@ class ApiController(RedditController):
|
||||
|
||||
if not res.error:
|
||||
if reason != 'ad_inq':
|
||||
emailer.feedback_email(email, message, name = name or '')
|
||||
emailer.feedback_email(email, message, name = name or '',
|
||||
reply_to = replyto or '')
|
||||
else:
|
||||
emailer.ad_inq_email(email, message, name = name or '')
|
||||
emailer.ad_inq_email(email, message, name = name or '',
|
||||
reply_to = replyto or '')
|
||||
res._update('success',
|
||||
innerHTML=_("thanks for your message! you should hear back from us shortly."))
|
||||
res._update("personal", value='')
|
||||
res._update("captcha", value='')
|
||||
res._hide("wtf")
|
||||
|
||||
|
||||
POST_ad_inq = POST_feedback
|
||||
|
||||
|
||||
@Json
|
||||
@validate(VUser(),
|
||||
VModhash(),
|
||||
@@ -439,7 +438,7 @@ class ApiController(RedditController):
|
||||
@validate(VUser('curpass', default = ''),
|
||||
VModhash(),
|
||||
curpass = nop('curpass'),
|
||||
email = nop("email"),
|
||||
email = ValidEmails("email", num = 1),
|
||||
newpass = nop("newpass"),
|
||||
verpass = nop("verpass"),
|
||||
password = VPassword(['newpass', 'verpass']))
|
||||
@@ -450,8 +449,10 @@ class ApiController(RedditController):
|
||||
res._update('curpass', value='')
|
||||
return
|
||||
updated = False
|
||||
if email and (not hasattr(c.user,'email')
|
||||
or c.user.email != email):
|
||||
if res._chk_error(errors.BAD_EMAILS):
|
||||
res._focus('email')
|
||||
elif email and (not hasattr(c.user,'email')
|
||||
or c.user.email != email):
|
||||
c.user.email = email
|
||||
c.user._commit()
|
||||
res._update('status',
|
||||
@@ -612,16 +613,20 @@ class ApiController(RedditController):
|
||||
if should_ratelimit:
|
||||
VRatelimit.ratelimit(rate_user=True, rate_ip = True, prefix = "rate_comment_")
|
||||
|
||||
|
||||
@Json
|
||||
@validate(VUser(),
|
||||
VModhash(),
|
||||
VCaptcha(),
|
||||
VRatelimit(rate_user = True, rate_ip = True,
|
||||
prefix = "rate_share_"),
|
||||
share_from = VLength('share_from', length = 60),
|
||||
share_from = VLength('share_from', length = 100),
|
||||
emails = ValidEmails("share_to"),
|
||||
reply_to = ValidEmails("replyto", num = 1),
|
||||
message = VLength("message", length = 1000),
|
||||
thing = VByName('id'))
|
||||
def POST_share(self, res, emails, thing, share_from):
|
||||
def POST_share(self, res, emails, thing, share_from, reply_to,
|
||||
message):
|
||||
|
||||
# remove the ratelimit error if the user's karma is high
|
||||
sr = thing.subreddit_slow
|
||||
@@ -633,18 +638,37 @@ class ApiController(RedditController):
|
||||
|
||||
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):
|
||||
|
||||
elif res._chk_error(errors.RATELIMIT, thing._fullname):
|
||||
pass
|
||||
elif (share_from is None and
|
||||
res._chk_error(errors.COMMENT_TOO_LONG,
|
||||
'share_from_' + thing._fullname)):
|
||||
res._focus('share_from_' + thing._fullname)
|
||||
elif (message is None and
|
||||
res._chk_error(errors.COMMENT_TOO_LONG,
|
||||
'message_' + thing._fullname)):
|
||||
res._focus('message_' + thing._fullname)
|
||||
elif not emails and res._chk_errors((errors.BAD_EMAILS,
|
||||
errors.NO_EMAILS,
|
||||
errors.TOO_MANY_EMAILS),
|
||||
"emails_" + thing._fullname):
|
||||
res._focus("emails_" + thing._fullname)
|
||||
elif not reply_to and res._chk_error(errors.BAD_EMAILS,
|
||||
"replyto_" + thing._fullname):
|
||||
res._focus("replyto_" + thing._fullname)
|
||||
else:
|
||||
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 "")
|
||||
res._update("sharelink_" + thing._fullname,
|
||||
innerHTML=("<div class='clearleft'></div><p class='error'>%s</p>" %
|
||||
_("your link has been shared.")))
|
||||
|
||||
emailer.share(thing, emails, from_name = share_from or "",
|
||||
body = message or "", reply_to = reply_to or "")
|
||||
|
||||
#set the ratelimiter
|
||||
if should_ratelimit:
|
||||
|
||||
@@ -28,7 +28,7 @@ from r2.models import *
|
||||
from r2.lib.pages import *
|
||||
from r2.lib.menus import *
|
||||
from r2.lib.utils import to36, sanitize_url, check_cheating, title_to_url, query_string
|
||||
from r2.lib.emailer import opt_out, opt_in
|
||||
from r2.lib.emailer import has_opted_out, Email
|
||||
from r2.lib.db.operators import desc
|
||||
from r2.lib.strings import strings
|
||||
import r2.lib.db.thing as thing
|
||||
@@ -564,24 +564,30 @@ class FrontController(RedditController):
|
||||
subreddits = sr_names,
|
||||
captcha=captcha)).render()
|
||||
|
||||
@validate(msg_hash = nop('x'))
|
||||
def GET_optout(self, msg_hash):
|
||||
email, sent = opt_out(msg_hash)
|
||||
def _render_opt_in_out(self, msg_hash, leave):
|
||||
"""Generates the form for an optin/optout page"""
|
||||
email = Email.handler.get_recipient(msg_hash)
|
||||
if not email:
|
||||
return self.abort404()
|
||||
return BoringPage(_("opt out"),
|
||||
content = OptOut(email = email, leave = True,
|
||||
sent = (has_opted_out(email) == leave)
|
||||
return BoringPage(_("opt out") if leave else _("welcome back"),
|
||||
content = OptOut(email = email, leave = leave,
|
||||
sent = sent,
|
||||
msg_hash = msg_hash)).render()
|
||||
|
||||
@validate(msg_hash = nop('x'))
|
||||
def GET_optout(self, msg_hash):
|
||||
"""handles /mail/optout to add an email to the optout mailing
|
||||
list. The actual email addition comes from the user posting
|
||||
the subsequently rendered form and is handled in
|
||||
ApiController.POST_optout."""
|
||||
return self._render_opt_in_out(msg_hash, True)
|
||||
|
||||
@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()
|
||||
|
||||
"""handles /mail/optin to remove an email address from the
|
||||
optout list. The actual email removal comes from the user
|
||||
posting the subsequently rendered form and is handled in
|
||||
ApiController.POST_optin."""
|
||||
return self._render_opt_in_out(msg_hash, False)
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
from r2.lib.pages import *
|
||||
from api import ApiController
|
||||
from r2.lib.utils import Storage, query_string
|
||||
from r2.lib.emailer import opt_in, opt_out
|
||||
from pylons import request, c, g
|
||||
from validator import *
|
||||
from pylons.i18n import _
|
||||
@@ -125,3 +126,26 @@ class PostController(ApiController):
|
||||
return self.redirect(dest)
|
||||
else:
|
||||
return self.redirect('/')
|
||||
|
||||
|
||||
@validate(msg_hash = nop('x'))
|
||||
def POST_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 = True,
|
||||
msg_hash = msg_hash)).render()
|
||||
|
||||
@validate(msg_hash = nop('x'))
|
||||
def POST_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 = True,
|
||||
msg_hash = msg_hash)).render()
|
||||
|
||||
|
||||
|
||||
@@ -291,7 +291,8 @@ class RedditController(BaseController):
|
||||
kw = {}
|
||||
argspec = inspect.getargspec(fn)
|
||||
|
||||
#if there is a **kw argument in the fn definition, just pass along the environment
|
||||
# if there is a **kw argument in the fn definition,
|
||||
# just pass along the environment
|
||||
if argspec[2]:
|
||||
kw = env
|
||||
#else for each entry in the arglist set the value from the environment
|
||||
|
||||
@@ -641,6 +641,11 @@ class VReason(Validator):
|
||||
|
||||
|
||||
class ValidEmails(Validator):
|
||||
"""Validates a list of email addresses passed in as a string and
|
||||
delineated by whitespace, ',' or ';'. Also validates quantity of
|
||||
provided emails. Returns a list of valid email addresses on
|
||||
success"""
|
||||
|
||||
separator = re.compile(r'[^\s,;]+')
|
||||
email_re = re.compile(r'.+@.+\..+')
|
||||
|
||||
@@ -648,18 +653,32 @@ class ValidEmails(Validator):
|
||||
self.num = num
|
||||
Validator.__init__(self, param = param, **kw)
|
||||
|
||||
def run(self, emails):
|
||||
emails = set(self.separator.findall(emails) if emails else [])
|
||||
def run(self, emails0):
|
||||
emails = set(self.separator.findall(emails0) if emails0 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)})
|
||||
|
||||
# make sure the number of addresses does not exceed the max
|
||||
if self.num > 0 and len(emails) + len(failures) > self.num:
|
||||
# special case for 1: there should be no delineators at all, so
|
||||
# send back original string to the user
|
||||
if self.num == 1:
|
||||
c.errors.add(errors.BAD_EMAILS,
|
||||
{'emails': '"%s"' % emails0})
|
||||
# else report the number expected
|
||||
else:
|
||||
c.errors.add(errors.TOO_MANY_EMAILS,
|
||||
{'num': self.num})
|
||||
# correct number, but invalid formatting
|
||||
elif failures:
|
||||
c.errors.add(errors.BAD_EMAILS,
|
||||
{'emails': ', '.join(failures)})
|
||||
# no emails
|
||||
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
|
||||
# return single email if one is expected, list otherwise
|
||||
return list(emails)[0] if self.num == 1 else emails
|
||||
|
||||
# NOTE: make sure *never* to have res check these are present
|
||||
# otherwise, the response could contain reference to these errors...!
|
||||
|
||||
@@ -24,7 +24,7 @@ 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.models import passhash, Email, Default, has_opted_out
|
||||
from r2.config import cache
|
||||
import os, random, datetime
|
||||
import smtplib, traceback, sys
|
||||
@@ -48,25 +48,6 @@ def simple_email(to, fr, subj, body):
|
||||
msg['Subject'] = utf8(subj)
|
||||
send_mail(msg, fr, to)
|
||||
|
||||
def sys_email(email, body, name='', subj = lambda x: x):
|
||||
fr = (c.user.name if c.user else 'Anonymous user')
|
||||
if name and name != fr:
|
||||
fr = "%s [%s]" % (name, fr)
|
||||
|
||||
fr = email_address(fr, email)
|
||||
subj = subj(fr)
|
||||
simple_email(feedback, fr, subj, body)
|
||||
|
||||
|
||||
def feedback_email(email, body, name=''):
|
||||
sys_email(email, body, name=name,
|
||||
subj = lambda fr: "[feedback] feedback from %s" % fr)
|
||||
|
||||
def ad_inq_email(email, body, name=''):
|
||||
sys_email(email, body, name=name,
|
||||
subj = lambda fr: "[ad_inq] ad inquiry from %s" % fr)
|
||||
|
||||
|
||||
def password_email(user):
|
||||
key = passhash(random.randint(0, 1000), user.email)
|
||||
passlink = 'http://' + c.domain + '/resetpassword/' + key
|
||||
@@ -75,69 +56,110 @@ def password_email(user):
|
||||
'reddit.com password reset',
|
||||
PasswordReset(user=user, passlink=passlink).render(style='email'))
|
||||
|
||||
def share(link, emails, from_name = ""):
|
||||
|
||||
def _feedback_email(email, body, kind, name='', reply_to = ''):
|
||||
"""Function for handling feedback and ad_inq emails. Adds an
|
||||
email to the mail queue to the feedback email account."""
|
||||
Email.handler.add_to_queue(c.user if c.user_is_loggedin else None,
|
||||
None, [feedback], name, email,
|
||||
datetime.datetime.now(),
|
||||
request.ip, kind, body = body,
|
||||
reply_to = reply_to)
|
||||
|
||||
def feedback_email(email, body, name='', reply_to = ''):
|
||||
"""Queues a feedback email to the feedback account."""
|
||||
return _feedback_email(email, body, Email.Kind.FEEDBACK, name = name,
|
||||
reply_to = reply_to)
|
||||
|
||||
def ad_inq_email(email, body, name='', reply_to = ''):
|
||||
"""Queues a ad_inq email to the feedback account."""
|
||||
return _feedback_email(email, body, Email.Kind.ADVERTISE, name = name,
|
||||
reply_to = reply_to)
|
||||
|
||||
|
||||
def share(link, emails, from_name = "", reply_to = "", body = ""):
|
||||
"""Queues a 'share link' email."""
|
||||
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)
|
||||
Email.handler.add_to_queue(c.user, link, emails, from_name, g.share_reply,
|
||||
date, request.ip, Email.Kind.SHARE,
|
||||
body = body, reply_to = reply_to)
|
||||
|
||||
def send_queued_mail():
|
||||
"""sends mail from the mail queue to smtplib for delivery. Also,
|
||||
on successes, empties the mail queue and adds all emails to the
|
||||
sent_mail list."""
|
||||
now = datetime.datetime.now(g.tz)
|
||||
if not c.site:
|
||||
c.site = Default
|
||||
|
||||
clear = False
|
||||
session = smtplib.SMTP(g.smtp_server)
|
||||
# convienence funciton for sending the mail to the singly-defined session and
|
||||
# marking the mail as read.
|
||||
def sendmail(email):
|
||||
try:
|
||||
session.sendmail(email.fr_addr, email.to_addr,
|
||||
email.to_MIMEText().as_string())
|
||||
email.set_sent()
|
||||
# exception happens only for local recipient that doesn't exist
|
||||
except smtplib.SMTPRecipientsRefused:
|
||||
# handle error and print, but don't stall the rest of the queue
|
||||
print "Handled error sending mail (traceback to follow)"
|
||||
traceback.print_exc(file = sys.stdout)
|
||||
|
||||
|
||||
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
|
||||
|
||||
should_queue = email.should_queue()
|
||||
|
||||
# check only on sharing that the mail is invalid
|
||||
if email.kind == Email.Kind.SHARE and should_queue:
|
||||
email.body = Share(username = email.from_name(),
|
||||
msg_hash = email.msg_hash,
|
||||
link = email.thing).render(style = "email")
|
||||
link = email.thing,
|
||||
body = email.body).render(style = "email")
|
||||
email.subject = _("[reddit] %(user)s has shared a link with you") % \
|
||||
{"user": email.from_name()}
|
||||
try:
|
||||
session.sendmail(email.fr_addr, email.to_addr,
|
||||
email.to_MIMEText().as_string())
|
||||
except smtplib.SMTPRecipientsRefused:
|
||||
# handle error but don't stop the queue
|
||||
print "Handled error sending mail (traceback to follow)"
|
||||
traceback.print_exc(file = sys.stdout)
|
||||
|
||||
sendmail(email)
|
||||
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())
|
||||
sendmail(email)
|
||||
|
||||
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())
|
||||
sendmail(email)
|
||||
|
||||
elif email.kind in (Email.Kind.FEEDBACK, Email.Kind.ADVERTISE):
|
||||
if email.kind == Email.Kind.FEEDBACK:
|
||||
email.subject = "[feedback] feedback from '%s'" % \
|
||||
email.from_name()
|
||||
else:
|
||||
email.subject = "[ad_inq] feedback from '%s'" % \
|
||||
email.from_name()
|
||||
sendmail(email)
|
||||
# handle other types of emails here
|
||||
else:
|
||||
# handle other types of emails here
|
||||
pass
|
||||
email.set_sent()
|
||||
finally:
|
||||
session.quit()
|
||||
|
||||
# clear is true if anything was found and processed above
|
||||
if clear:
|
||||
Email.handler.clear_queue(now)
|
||||
|
||||
|
||||
|
||||
def opt_out(msg_hash):
|
||||
"""Queues an opt-out email (i.e., a confirmation that the email
|
||||
address has been opted out of receiving any future mail)"""
|
||||
email, added = Email.handler.opt_out(msg_hash)
|
||||
if email and added:
|
||||
Email.handler.add_to_queue(None, None, [email], "reddit.com",
|
||||
@@ -146,6 +168,8 @@ def opt_out(msg_hash):
|
||||
return email, added
|
||||
|
||||
def opt_in(msg_hash):
|
||||
"""Queues an opt-in email (i.e., that the email has been removed
|
||||
from our opt out list)"""
|
||||
email, removed = Email.handler.opt_in(msg_hash)
|
||||
if email and removed:
|
||||
Email.handler.add_to_queue(None, None, [email], "reddit.com",
|
||||
|
||||
@@ -26,7 +26,7 @@ from builder import *
|
||||
from vote import *
|
||||
from report import *
|
||||
from subreddit import *
|
||||
from mail_queue import Email
|
||||
from mail_queue import Email, has_opted_out, opt_count
|
||||
from admintools import *
|
||||
import thing_changes
|
||||
|
||||
|
||||
@@ -50,6 +50,12 @@ def mail_queue(metadata):
|
||||
# the "To" address of the email
|
||||
sa.Column('to_addr', sa.String),
|
||||
|
||||
# the "From" address of the email
|
||||
sa.Column('fr_addr', sa.String),
|
||||
|
||||
# the "Reply-To" address of the email
|
||||
sa.Column('reply_to', sa.String),
|
||||
|
||||
# fullname of the thing
|
||||
sa.Column('fullname', sa.String),
|
||||
|
||||
@@ -80,6 +86,12 @@ def sent_mail_table(metadata):
|
||||
# the "To" address of the email
|
||||
sa.Column('to_addr', sa.String),
|
||||
|
||||
# the "From" address of the email
|
||||
sa.Column('fr_addr', sa.String),
|
||||
|
||||
# the "reply-to" address of the email
|
||||
sa.Column('reply_to', sa.String),
|
||||
|
||||
# IP of original request
|
||||
sa.Column('ip', PGInet),
|
||||
|
||||
@@ -142,6 +154,7 @@ class EmailHandler(object):
|
||||
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."""
|
||||
@@ -152,6 +165,7 @@ class EmailHandler(object):
|
||||
o.insert().execute({o.c.email: email, o.c.msg_hash: msg_hash})
|
||||
clear_memo('r2.models.mail_queue.has_opted_out',
|
||||
email)
|
||||
clear_memo('r2.models.mail_queue.opt_count')
|
||||
return (email, True)
|
||||
except sa.exceptions.SQLError:
|
||||
return (email, False)
|
||||
@@ -166,6 +180,7 @@ class EmailHandler(object):
|
||||
sa.delete(o, o.c.email == email).execute()
|
||||
clear_memo('r2.models.mail_queue.has_opted_out',
|
||||
email)
|
||||
clear_memo('r2.models.mail_queue.opt_count')
|
||||
return (email, True)
|
||||
else:
|
||||
return (email, False)
|
||||
@@ -178,8 +193,8 @@ class EmailHandler(object):
|
||||
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 = ""):
|
||||
def add_to_queue(self, user, thing, emails, from_name, fr_addr, date, ip,
|
||||
kind, body = "", reply_to = ""):
|
||||
s = self.queue_table
|
||||
hashes = []
|
||||
for email in emails:
|
||||
@@ -190,6 +205,8 @@ class EmailHandler(object):
|
||||
s.insert().execute({s.c.to_addr : email,
|
||||
s.c.account_id : uid,
|
||||
s.c.from_name : from_name,
|
||||
s.c.fr_addr : fr_addr,
|
||||
s.c.reply_to : reply_to,
|
||||
s.c.fullname: tid,
|
||||
s.c.ip : ip,
|
||||
s.c.kind: kind,
|
||||
@@ -215,7 +232,7 @@ class EmailHandler(object):
|
||||
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],
|
||||
s.c.msg_hash, s.c.fr_addr, s.c.reply_to],
|
||||
sa.and_(*where),
|
||||
order_by = s.c.uid, limit = batch_limit).execute()
|
||||
res = res.fetchall()
|
||||
@@ -242,10 +259,11 @@ class EmailHandler(object):
|
||||
# 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:
|
||||
for (addr, acct, fname, fulln, body, kind, ip, date, uid,
|
||||
msg_hash, fr_addr, reply_to) in res:
|
||||
yield (accts.get(acct), things.get(fulln), addr,
|
||||
fname, date, ip, ips[ip], kind, msg_hash, body)
|
||||
fname, date, ip, ips[ip], kind, msg_hash, body,
|
||||
fr_addr, reply_to)
|
||||
|
||||
def clear_queue(self, max_date, kind = None):
|
||||
s = self.queue_table
|
||||
@@ -262,7 +280,8 @@ class Email(object):
|
||||
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 = ''):
|
||||
kind, msg_hash, body = '', from_addr = '',
|
||||
reply_to = ''):
|
||||
self.user = user
|
||||
self.thing = thing
|
||||
self.to_addr = email
|
||||
@@ -273,14 +292,20 @@ class Email(object):
|
||||
self.banned_ip = banned_ip
|
||||
self.kind = kind
|
||||
self.sent = False
|
||||
self.body = ""
|
||||
self.subject = subject
|
||||
self.body = body
|
||||
self.subject = ''
|
||||
self.msg_hash = msg_hash
|
||||
self.reply_to = reply_to
|
||||
|
||||
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)
|
||||
if not self.user:
|
||||
name = "%(name)s"
|
||||
elif self._from_name != self.user.name:
|
||||
name = "%(name)s (%(uname)s)"
|
||||
else:
|
||||
name = "%(uname)s"
|
||||
return name % dict(name = self._from_name,
|
||||
uname = self.user.name if self.user else '')
|
||||
|
||||
@classmethod
|
||||
def get_unsent(cls, max_date, batch_limit = 50, kind = None):
|
||||
@@ -303,6 +328,8 @@ class Email(object):
|
||||
t.insert().execute({t.c.account_id:
|
||||
self.user._id if self.user else 0,
|
||||
t.c.to_addr : self.to_addr,
|
||||
t.c.fr_addr : self.fr_addr,
|
||||
t.c.reply_to : self.reply_to,
|
||||
t.c.ip : self.ip,
|
||||
t.c.fullname:
|
||||
self.thing._fullname if self.thing else "",
|
||||
@@ -315,7 +342,7 @@ class Email(object):
|
||||
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
|
||||
fr = '"%s" <%s>' % (self.from_name(), self.fr_addr)
|
||||
if not fr.startswith('-') and not self.to_addr.startswith('-'): # security
|
||||
msg = MIMEText(utf8(self.body))
|
||||
msg.set_charset('utf8')
|
||||
@@ -325,6 +352,8 @@ class Email(object):
|
||||
if self.user:
|
||||
msg['X-Reddit-username'] = utf8(self.user.name)
|
||||
msg['X-Reddit-ID'] = self.msg_hash
|
||||
if self.reply_to:
|
||||
msg['Reply-To'] = utf8(self.reply_to)
|
||||
return msg
|
||||
return None
|
||||
|
||||
@@ -336,6 +365,12 @@ def has_opted_out(email):
|
||||
return bool(res.fetchall())
|
||||
|
||||
|
||||
@memoize('r2.models.mail_queue.opt_count')
|
||||
def opt_count():
|
||||
o = Email.handler.opt_table
|
||||
s = sa.select([sa.func.count(o.c.email)])
|
||||
res = s.execute().fetchone()
|
||||
return int(res[0])
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -826,7 +826,9 @@ a.star { text-decoration: none; color: #ff8b60 }
|
||||
.clearleft { clear: left; height: 0px; }
|
||||
.clear { clear: both; }
|
||||
|
||||
.sharetable {margin-left: 20px; }
|
||||
.sharetable.preftable {margin-left: 20px; }
|
||||
.sharetable.preftable th { padding-bottom: 5px; padding-top: 5px; }
|
||||
.sharetable.preftable button { margin-top: 10px }
|
||||
|
||||
.sponsored .entry { margin-right: 20px;}
|
||||
|
||||
@@ -1229,6 +1231,10 @@ a.star { text-decoration: none; color: #ff8b60 }
|
||||
#passform.pretty-form button { padding: 0px 1px; }
|
||||
|
||||
|
||||
/*opt-out/in form*/
|
||||
.opt-form { font-size: larger }
|
||||
.opt-form form { display: inline; }
|
||||
|
||||
.preftable th {
|
||||
padding: 10px;
|
||||
font-weight: bold;
|
||||
|
||||
@@ -68,9 +68,10 @@
|
||||
<td>
|
||||
%if thing.replyto:
|
||||
<input id="replyto" name="Reply-to" type="text"
|
||||
value="${thing.email}" onfocus="clearTitle(this)" size="30"/>
|
||||
value="${thing.email}" onfocus="clearTitle(this)"
|
||||
size="30"/>
|
||||
%else:
|
||||
<input id="replyto" name="email" style="color: gray"
|
||||
<input id="replyto" name="replyto" style="color: gray"
|
||||
value="${_('optional')}" type="text"
|
||||
onfocus="clearTitle(this)" size="30"/>
|
||||
%endif
|
||||
|
||||
@@ -19,28 +19,41 @@
|
||||
## All portions of the code written by CondeNet are Copyright (c) 2006-2008
|
||||
## CondeNet, Inc. All Rights Reserved."
|
||||
###############################################################################
|
||||
<div style="font-size:larger">
|
||||
<div class="opt-form">
|
||||
%if thing.leave:
|
||||
<p>
|
||||
${_("The address %(email)s will no longer recieve email from us.") % dict(email=thing.email)}
|
||||
</p>
|
||||
|
||||
%if thing.sent:
|
||||
<p>
|
||||
<p>
|
||||
${_("The address '%(email)s' will no longer receive email from us.") % dict(email=thing.email)}
|
||||
</p>
|
||||
<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.")}
|
||||
${_("Would you like the address %(email)s to no longer receive email from us?") % dict(email=thing.email)}
|
||||
<form method="post" action="/post/optout">
|
||||
<input type="hidden" name="x" value="${thing.msg_hash}" />
|
||||
<input type="submit" name="accept" value="${_('yes')}" />
|
||||
</form>
|
||||
<form method="get" action="/">
|
||||
<input type="submit" name="decline" value="${_('no')}" />
|
||||
</form>
|
||||
</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)}
|
||||
${_("'%(email)s' has been removed from our block list.") % dict(email=thing.email)}
|
||||
</p>
|
||||
%else:
|
||||
<p>
|
||||
${_("%(email)s has already been removed from our block list.") % dict(email=thing.email)}
|
||||
<p>
|
||||
${_("Allow '%(email)s' to receive email from us?") % dict(email=thing.email)}
|
||||
<form method="post" action="/post/optin">
|
||||
<input type="hidden" name="x" value="${thing.msg_hash}" />
|
||||
<input type="submit" name="accept" value="${_('yes')}" />
|
||||
</form>
|
||||
<form method="get" action="/">
|
||||
<input type="submit" name="decline" value="${_('no')}" />
|
||||
</form>
|
||||
</p>
|
||||
%endif
|
||||
</div>
|
||||
|
||||
@@ -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"/>
|
||||
@@ -33,7 +33,9 @@
|
||||
<tr>
|
||||
<td align="right">${_("email")}:</td>
|
||||
<td><input name="email" type="text" value="${hasattr(c.user,'email') and c.user.email or ''}"/></td>
|
||||
<td></td>
|
||||
<td>
|
||||
${error_field("BAD_EMAILS")}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">${_("new password")}:</td>
|
||||
|
||||
@@ -19,14 +19,18 @@
|
||||
## 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.
|
||||
%if not thing.body:
|
||||
${thing.username} from http://${g.domain}/ has shared a link with you.
|
||||
%else:
|
||||
${thing.body}
|
||||
%endif
|
||||
|
||||
"${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:
|
||||
<% from r2.lib.strings import strings, plurals %>${_("There are currently %(num_comments)s on this link. You can view them here:") % dict(num_comments = strings.number_label % (thing.link.num_comments, plurals.N_comments(thing.link.num_comments)))}
|
||||
|
||||
http://${g.domain}${reddit_link(thing.link.permalink, url=True)}
|
||||
<% from r2.lib.template_helpers import reddit_link %>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}
|
||||
@@ -2,16 +2,17 @@
|
||||
<%namespace file="utils.html" import="error_field"/>
|
||||
<%namespace file="captcha.html" import="captchagen"/>
|
||||
|
||||
<div id="sharelink_${thing.link_name}"
|
||||
<div id="sharelink_${thing.link_name}"
|
||||
${"" if thing.link_name else "style='display:none'"}>
|
||||
<div class="clearleft"><!--IEsux--></div>
|
||||
<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">
|
||||
<table class="sharetable preftable">
|
||||
<tr>
|
||||
<th>
|
||||
<label class="big" for="share_to_${thing.link_name}">
|
||||
<label for="share_to_${thing.link_name}">
|
||||
${_("send this link to")}
|
||||
</label>
|
||||
</th>
|
||||
@@ -21,14 +22,14 @@
|
||||
</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)}
|
||||
${error_field("BAD_EMAILS_emails_" + thing.link_name)}
|
||||
${error_field("TOO_MANY_EMAILS_emails_" + thing.link_name)}
|
||||
${error_field("NO_EMAILS_emails_" + thing.link_name)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
<label class="big" for="share_from_${thing.link_name}">
|
||||
<label for="share_from_${thing.link_name}">
|
||||
${_("your name")}
|
||||
</label>
|
||||
<span class="little gray">
|
||||
@@ -41,6 +42,44 @@
|
||||
name="share_from" />
|
||||
</td>
|
||||
<td>
|
||||
${error_field("COMMENT_TOO_LONG_share_from_" + thing.link_name)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
<label for="replyto_${thing.link_name}">
|
||||
${_("your email")}
|
||||
</label>
|
||||
<span class="little gray">
|
||||
${_("(optional)")}
|
||||
</span>
|
||||
</th>
|
||||
<td>
|
||||
<input name="replyto" type="text" size="30"
|
||||
value="${c.user.email if hasattr(c.user, 'email') else ''}"/>
|
||||
</td>
|
||||
<td>
|
||||
${error_field("BAD_EMAILS_replyto_" + thing.link_name)}
|
||||
${error_field("TOO_MANY_EMAILS_replyto_" + thing.link_name)}
|
||||
${error_field("NO_EMAILS_replyto_" + thing.link_name)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
<label for="message_${thing.link_name}">
|
||||
${_("message")}
|
||||
</label>
|
||||
<span class="little gray">
|
||||
${_("(optional)")}
|
||||
</span>
|
||||
</th>
|
||||
<td>
|
||||
<textarea id="message_${thing.link_name}" name="message" rows="4">
|
||||
${c.user.name} from http://${g.domain}/ has shared a link with you.
|
||||
</textarea>
|
||||
</td>
|
||||
<td>
|
||||
${error_field("COMMENT_TOO_LONG_message_" + thing.link_name)}
|
||||
</td>
|
||||
</tr>
|
||||
%if thing.captcha:
|
||||
|
||||
Reference in New Issue
Block a user