diff --git a/docs/customizing.rst b/docs/customizing.rst index ef7119fb..58abf746 100644 --- a/docs/customizing.rst +++ b/docs/customizing.rst @@ -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 @@ -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 @@ -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 ------------------------- diff --git a/flask_security/changeable.py b/flask_security/changeable.py index 36c56e29..466b979f 100644 --- a/flask_security/changeable.py +++ b/flask_security/changeable.py @@ -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']) @@ -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): diff --git a/flask_security/confirmable.py b/flask_security/confirmable.py index b72596b4..fb84d726 100644 --- a/flask_security/confirmable.py +++ b/flask_security/confirmable.py @@ -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 @@ -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) diff --git a/flask_security/core.py b/flask_security/core.py index 69c8c78e..549e8bc7 100644 --- a/flask_security/core.py +++ b/flask_security/core.py @@ -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 @@ -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, @@ -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 @@ -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'): @@ -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) diff --git a/flask_security/passwordless.py b/flask_security/passwordless.py index fef70ad7..d9c125a3 100644 --- a/flask_security/passwordless.py +++ b/flask_security/passwordless.py @@ -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']) @@ -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) diff --git a/flask_security/recoverable.py b/flask_security/recoverable.py index a130103e..f783ca2a 100644 --- a/flask_security/recoverable.py +++ b/flask_security/recoverable.py @@ -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']) @@ -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 @@ -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): diff --git a/flask_security/registerable.py b/flask_security/registerable.py index 2ce6de9f..a23c6410 100644 --- a/flask_security/registerable.py +++ b/flask_security/registerable.py @@ -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']) @@ -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 diff --git a/tests/test_misc.py b/tests/test_misc.py index 65568a37..5aae5109 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -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()