Skip to content

Commit

Permalink
Customizable send_mail (opr #730) (#64)
Browse files Browse the repository at this point in the history
* Customizable send_mail (#730)

* Make send_mail function customizable.

* Modified original commit to set send_mail and render_template as init configurations.

This is easier than subclassing Security and is the same as other overrides such as login_manager.

Updated docs.
  • Loading branch information
jwag956 committed May 7, 2019
1 parent 8a8fc64 commit b18de84
Show file tree
Hide file tree
Showing 8 changed files with 84 additions and 24 deletions.
40 changes: 38 additions & 2 deletions docs/customizing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ decorator like so::
send_security_email.delay(msg)

If factory method is going to be used for initialization, use ``_SecurityState``
object returned by ``init_app`` method to initialize Celery tasks intead of using
object returned by ``init_app`` method to initialize Celery tasks instead of using
``security.send_mail_task`` directly like so::

from flask import Flask
Expand Down Expand Up @@ -194,7 +194,7 @@ object returned by ``init_app`` method to initialize Celery tasks intead of usin
def delay_flask_security_mail(msg):
send_flask_mail.delay(msg)

# A shortcurt.
# A shortcut.
security_ctx.send_mail_task(send_flask_mail.delay)

return app
Expand All @@ -215,6 +215,42 @@ Celery. The practical way with custom serialization may look like so::
.. _Celery: http://www.celeryproject.org/


Custom send_mail method
-----------------------

It's also possible to completely override the ``security.send_mail`` method to
implement your own logic.

For example, you might want to use an alternative email library like `Flask-Emails`::

from flask import Flask
from flask_security import Security, SQLAlchemyUserDatastore
from flask_emails import Message

def create_app(config):
"""Initialize Flask instance."""

app = Flask(__name__)
app.config.from_object(config)

def custom_send_mail(subject, recipient, template, **context):
ctx = ('security/email', template)
message = Message(
subject=subject,
html=_security.render_template('%s/%s.html' % ctx, **context))
message.send(mail_to=[recipient])

datastore = SQLAlchemyUserDatastore(db, User, Role)
Security(app, datastore, send_mail=custom_send_mail)

return app

.. note::

The above ``security.send_mail_task`` override will be useless if you
override the entire ``send_mail`` method.


Authorization with OAuth2
-------------------------

Expand Down
4 changes: 2 additions & 2 deletions flask_security/changeable.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from werkzeug.local import LocalProxy

from .signals import password_changed
from .utils import config_value, hash_password, send_mail
from .utils import config_value, hash_password

# Convenient references
_security = LocalProxy(lambda: current_app.extensions['security'])
Expand All @@ -29,7 +29,7 @@ def send_password_changed_notice(user):
"""
if config_value('SEND_PASSWORD_CHANGE_EMAIL'):
subject = config_value('EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE')
send_mail(subject, user.email, 'change_notice', user=user)
_security.send_mail(subject, user.email, 'change_notice', user=user)


def change_user_password(user, password):
Expand Down
8 changes: 4 additions & 4 deletions flask_security/confirmable.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from werkzeug.local import LocalProxy

from .signals import confirm_instructions_sent, user_confirmed
from .utils import config_value, get_token_status, hash_data, send_mail, \
from .utils import config_value, get_token_status, hash_data, \
url_for_security, verify_hash

# Convenient references
Expand All @@ -39,9 +39,9 @@ def send_confirmation_instructions(user):

confirmation_link, token = generate_confirmation_link(user)

send_mail(config_value('EMAIL_SUBJECT_CONFIRM'), user.email,
'confirmation_instructions', user=user,
confirmation_link=confirmation_link)
_security.send_mail(config_value('EMAIL_SUBJECT_CONFIRM'), user.email,
'confirmation_instructions', user=user,
confirmation_link=confirmation_link)

confirm_instructions_sent.send(app._get_current_object(), user=user,
token=token)
Expand Down
13 changes: 10 additions & 3 deletions flask_security/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@
ResetPasswordForm, SendConfirmationForm
from .utils import _
from .utils import config_value as cv
from .utils import get_config, hash_data, localize_callback, \
from .utils import get_config, hash_data, localize_callback, send_mail,\
string_types, url_for_security, verify_and_update_password, verify_hash

from .views import create_blueprint

# Convenient references
Expand Down Expand Up @@ -491,6 +492,8 @@ class Security(object):
:param send_confirmation_form: set form for the send confirmation view
:param passwordless_login_form: set form for the passwordless login view
:param anonymous_user: class to use for anonymous user
:param render_template: function to use to render templates
:param send_mail: function to use to send email
"""

def __init__(self, app=None, datastore=None, register_blueprint=True,
Expand All @@ -506,7 +509,9 @@ def __init__(self, app=None, datastore=None, register_blueprint=True,
change_password_form=None,
send_confirmation_form=None,
passwordless_login_form=None,
anonymous_user=None)
anonymous_user=None,
render_template=self.render_template,
send_mail=self.send_mail)
self._kwargs.update(kwargs)

self._state = None # set by init_app
Expand Down Expand Up @@ -547,7 +552,6 @@ def _register_i18n():
if '_' not in app.jinja_env.globals:
app.jinja_env.globals['_'] = state.i18n_domain.gettext

state.render_template = self.render_template
app.extensions['security'] = state

if hasattr(app, 'cli'):
Expand All @@ -562,5 +566,8 @@ def _register_i18n():
def render_template(self, *args, **kwargs):
return render_template(*args, **kwargs)

def send_mail(self, *args, **kwargs):
return send_mail(*args, **kwargs)

def __getattr__(self, name):
return getattr(self._state, name, None)
6 changes: 3 additions & 3 deletions flask_security/passwordless.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from werkzeug.local import LocalProxy

from .signals import login_instructions_sent
from .utils import config_value, get_token_status, send_mail, url_for_security
from .utils import config_value, get_token_status, url_for_security

# Convenient references
_security = LocalProxy(lambda: app.extensions['security'])
Expand All @@ -30,8 +30,8 @@ def send_login_instructions(user):
token = generate_login_token(user)
login_link = url_for_security('token_login', token=token, _external=True)

send_mail(config_value('EMAIL_SUBJECT_PASSWORDLESS'), user.email,
'login_instructions', user=user, login_link=login_link)
_security.send_mail(config_value('EMAIL_SUBJECT_PASSWORDLESS'), user.email,
'login_instructions', user=user, login_link=login_link)

login_instructions_sent.send(app._get_current_object(), user=user,
login_token=token)
Expand Down
12 changes: 6 additions & 6 deletions flask_security/recoverable.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from .signals import password_reset, reset_password_instructions_sent
from .utils import config_value, get_token_status, hash_data, hash_password, \
send_mail, url_for_security, verify_hash
url_for_security, verify_hash

# Convenient references
_security = LocalProxy(lambda: app.extensions['security'])
Expand All @@ -33,9 +33,9 @@ def send_reset_password_instructions(user):
)

if config_value('SEND_PASSWORD_RESET_EMAIL'):
send_mail(config_value('EMAIL_SUBJECT_PASSWORD_RESET'), user.email,
'reset_instructions',
user=user, reset_link=reset_link)
_security.send_mail(config_value('EMAIL_SUBJECT_PASSWORD_RESET'),
user.email, 'reset_instructions',
user=user, reset_link=reset_link)

reset_password_instructions_sent.send(
app._get_current_object(), user=user, token=token
Expand All @@ -48,8 +48,8 @@ def send_password_reset_notice(user):
:param user: The user to send the notice to
"""
if config_value('SEND_PASSWORD_RESET_NOTICE_EMAIL'):
send_mail(config_value('EMAIL_SUBJECT_PASSWORD_NOTICE'), user.email,
'reset_notice', user=user)
_security.send_mail(config_value('EMAIL_SUBJECT_PASSWORD_NOTICE'),
user.email, 'reset_notice', user=user)


def generate_reset_password_token(user):
Expand Down
8 changes: 4 additions & 4 deletions flask_security/registerable.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@

from .confirmable import generate_confirmation_link
from .signals import user_registered
from .utils import config_value, do_flash, get_message, hash_password, \
send_mail
from .utils import config_value, do_flash, get_message, hash_password

# Convenient references
_security = LocalProxy(lambda: app.extensions['security'])
Expand All @@ -37,7 +36,8 @@ def register_user(**kwargs):
user=user, confirm_token=token)

if config_value('SEND_REGISTER_EMAIL'):
send_mail(config_value('EMAIL_SUBJECT_REGISTER'), user.email,
'welcome', user=user, confirmation_link=confirmation_link)
_security.send_mail(config_value('EMAIL_SUBJECT_REGISTER'), user.email,
'welcome', user=user,
confirmation_link=confirmation_link)

return user
17 changes: 17 additions & 0 deletions tests/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,23 @@ def send_email(msg):
assert app.mail_sent is True


@pytest.mark.recoverable()
def test_alt_send_mail(app, sqlalchemy_datastore):
""" Verify that can override the send_mail method. """
app.mail_sent = False

def send_email(subject, email, template, **kwargs):
app.mail_sent = True

init_app_with_options(app, sqlalchemy_datastore, **{
'security_args': {'send_mail': send_email}
})
client = app.test_client()

client.post('/reset', data=dict(email='matt@lp.com'))
assert app.mail_sent is True


def test_register_blueprint_flag(app, sqlalchemy_datastore):
app.security = Security(app, datastore=Security, register_blueprint=False)
client = app.test_client()
Expand Down

0 comments on commit b18de84

Please sign in to comment.