diff --git a/config.example.py b/config.example.py index 122fb6d..c046bcb 100644 --- a/config.example.py +++ b/config.example.py @@ -19,6 +19,8 @@ MAINTENANCE_MODE_LOGINS = True # What the site identifies itself as. This affects templates, not database stuff. SITE_NAME = 'Nyaa' +# What the both sites are labeled under (used for eg. email subjects) +GLOBAL_SITE_NAME = 'Nyaa.si' # General prefix for running multiple sites, eg. most database tables are site-prefixed SITE_FLAVOR = 'nyaa' # 'nyaa' or 'sukebei' @@ -49,13 +51,25 @@ else: SQLALCHEMY_DATABASE_URI = ( 'sqlite:///' + os.path.join(BASE_DIR, 'test.db') + '?check_same_thread=False') -# Email server settings +########### +## EMAIL ## +########### + +# 'smtp' or 'mailgun' +MAIL_BACKEND = 'mailgun' +MAIL_FROM_ADDRESS = 'Sender Name ' + +# Mailgun settings +MAILGUN_API_BASE = 'https://api.mailgun.net/v3/YOUR_DOMAIN_NAME' +MAILGUN_API_KEY = 'YOUR_API_KEY' + +# SMTP settings SMTP_SERVER = '***' SMTP_PORT = 587 -MAIL_FROM_ADDRESS = '***' SMTP_USERNAME = '***' SMTP_PASSWORD = '***' + # The maximum number of files a torrent can contain # until the site says "Too many files to display." MAX_FILES_VIEW = 1000 diff --git a/nyaa/email.py b/nyaa/email.py new file mode 100644 index 0000000..a14ebae --- /dev/null +++ b/nyaa/email.py @@ -0,0 +1,83 @@ +import smtplib +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText + +from flask import current_app as app + +import requests + +from nyaa import models + + +class EmailHolder(object): + ''' Holds email subject, recipient and content, so we have a general class for + all mail backends. ''' + def __init__(self, subject=None, recipient=None, text=None, html=None): + self.subject = subject + self.recipient = recipient # models.User or string + self.text = text + self.html = html + + def format_recipient(self): + if isinstance(self.recipient, models.User): + return '{} <{}>'.format(self.recipient.username, self.recipient.email) + else: + return self.recipient + + def recipient_email(self): + if isinstance(self.recipient, models.User): + return self.recipient.email + else: + return self.recipient.email + + def as_mimemultipart(self): + msg = MIMEMultipart() + msg['Subject'] = self.subject + msg['From'] = app.config['MAIL_FROM_ADDRESS'] + msg['To'] = self.format_recipient() + + msg.attach(MIMEText(self.text, 'plain')) + if self.html: + msg.attach(MIMEText(self.html, 'html')) + + return msg + + +def send_email(email_holder): + mail_backend = app.config.get('MAIL_BACKEND') + if mail_backend == 'mailgun': + _send_mailgun(email_holder) + elif mail_backend == 'smtp': + _send_smtp(email_holder) + elif mail_backend: + # TODO: Do this in logging.error when we have that set up + print('Unknown mail backend:', mail_backend) + + +def _send_mailgun(email_holder): + mailgun_endpoint = app.config['MAILGUN_API_BASE'] + '/messages' + auth = ('api', app.config['MAILGUN_API_KEY']) + data = { + 'from': app.config['MAIL_FROM_ADDRESS'], + 'to': email_holder.format_recipient(), + 'subject': email_holder.subject, + 'text': email_holder.text, + 'html': email_holder.html + } + r = requests.post(mailgun_endpoint, data=data, auth=auth) + # TODO real error handling? + assert r.status_code == 200 + + +def _send_smtp(email_holder): + # NOTE: Unused, most likely untested! Should work, however. + msg = email_holder.as_mimemultipart() + + server = smtplib.SMTP(app.config['SMTP_SERVER'], app.config['SMTP_PORT']) + server.set_debuglevel(1) + server.ehlo() + server.starttls() + server.ehlo() + server.login(app.config['SMTP_USERNAME'], app.config['SMTP_PASSWORD']) + server.sendmail(app.config['SMTP_USERNAME'], email_holder.recipient_email(), msg.as_string()) + server.quit() diff --git a/nyaa/templates/email/verify.html b/nyaa/templates/email/verify.html new file mode 100644 index 0000000..099f831 --- /dev/null +++ b/nyaa/templates/email/verify.html @@ -0,0 +1,24 @@ + + + Verify your {{ config.GLOBAL_SITE_NAME }} account + + + +
+ {{ user.username }}, please verify your email by clicking the link below: +
+
+ {{ activation_link }} +
+
+ If you did not sign up for {{ config.GLOBAL_SITE_NAME }}, feel free to ignore this email. +
+ + diff --git a/nyaa/templates/email/verify.txt b/nyaa/templates/email/verify.txt new file mode 100644 index 0000000..05ee2e0 --- /dev/null +++ b/nyaa/templates/email/verify.txt @@ -0,0 +1,6 @@ +{{ user.username }}, please verify your email by clicking the link below: + +{{ activation_link }} +(if you can't click on the link, copy and paste it to your browser's address bar) + +If you did not sign up for {{ config.GLOBAL_SITE_NAME }}, feel free to ignore this email. diff --git a/nyaa/views/account.py b/nyaa/views/account.py index 003f061..7fe5b28 100644 --- a/nyaa/views/account.py +++ b/nyaa/views/account.py @@ -1,12 +1,9 @@ -import smtplib from datetime import datetime -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText from ipaddress import ip_address import flask -from nyaa import forms, models +from nyaa import email, forms, models from nyaa.extensions import db from nyaa.views.users import get_activation_link @@ -83,8 +80,7 @@ def register(): db.session.commit() if app.config['USE_EMAIL_VERIFICATION']: # force verification, enable email - activ_link = get_activation_link(user) - send_verification_email(user.email, activ_link) + send_verification_email(user) return flask.render_template('waiting.html') else: # disable verification, set user as active and auto log in user.status = models.UserStatusType.ACTIVE @@ -150,25 +146,19 @@ def redirect_url(): return url -def send_verification_email(to_address, activ_link): - ''' this is until we have our own mail server, obviously. - This can be greatly cut down if on same machine. - probably can get rid of all but msg formatting/building, - init line and sendmail line if local SMTP server ''' +def send_verification_email(user): + activation_link = get_activation_link(user) - msg_body = 'Please click on: ' + activ_link + ' to activate your account.\n\n\nUnsubscribe:' + tmpl_context = { + 'activation_link': activation_link, + 'user': user + } - msg = MIMEMultipart() - msg['Subject'] = 'Verification Link' - msg['From'] = app.config['MAIL_FROM_ADDRESS'] - msg['To'] = to_address - msg.attach(MIMEText(msg_body, 'plain')) + email_msg = email.EmailHolder( + subject='Verify your {} account'.format(app.config['GLOBAL_SITE_NAME']), + recipient=user, + text=flask.render_template('email/verify.txt', **tmpl_context), + html=flask.render_template('email/verify.html', **tmpl_context), + ) - server = smtplib.SMTP(app.config['SMTP_SERVER'], app.config['SMTP_PORT']) - server.set_debuglevel(1) - server.ehlo() - server.starttls() - server.ehlo() - server.login(app.config['SMTP_USERNAME'], app.config['SMTP_PASSWORD']) - server.sendmail(app.config['SMTP_USERNAME'], to_address, msg.as_string()) - server.quit() + email.send_email(email_msg) diff --git a/nyaa/views/users.py b/nyaa/views/users.py index 48cf861..71b53ff 100644 --- a/nyaa/views/users.py +++ b/nyaa/views/users.py @@ -202,15 +202,23 @@ def activate_user(payload): user = models.User.by_id(user_id) - if not user: + # Only allow activating inactive users + if not user or user.status != models.UserStatusType.INACTIVE: flask.abort(404) + # Set user active user.status = models.UserStatusType.ACTIVE - db.session.add(user) db.session.commit() - return flask.redirect(flask.url_for('account.login')) + # Log user in + flask.g.user = user + flask.session['user_id'] = user.id + flask.session.permanent = True + flask.session.modified = True + + flask.flash(flask.Markup("You've successfully verified your account!"), 'success') + return flask.redirect(flask.url_for('main.home')) def _create_user_class_choices(user): diff --git a/requirements.txt b/requirements.txt index 007fced..4893830 100644 --- a/requirements.txt +++ b/requirements.txt @@ -41,6 +41,7 @@ pytest==3.1.1 python-dateutil==2.6.0 python-editor==1.0.3 python-utils==2.1.0 +requests==2.18.4 six==1.10.0 SQLAlchemy==1.1.10 SQLAlchemy-FullText-Search==0.2.3