diff --git a/CTFd/api/v1/users.py b/CTFd/api/v1/users.py index 37e6ff02..5e37b9a8 100644 --- a/CTFd/api/v1/users.py +++ b/CTFd/api/v1/users.py @@ -4,9 +4,12 @@ from CTFd.models import db, Users, Solves, Awards, Fails, Tracking, Unlocks, Sub from CTFd.utils.decorators import ( authed_only, admins_only, - authed + authed, + ratelimit ) from CTFd.cache import cache, clear_standings +from CTFd.utils.config import get_mail_provider +from CTFd.utils.email import sendmail from CTFd.utils.user import get_current_user, is_admin from CTFd.utils.decorators.visibility import check_account_visibility, check_score_visibility @@ -280,3 +283,44 @@ class UserAwards(Resource): 'success': True, 'data': response.data } + + +@users_namespace.route('//email') +@users_namespace.param('user_id', "User ID") +class UserEmails(Resource): + @admins_only + @ratelimit(method="POST", limit=10, interval=60) + def post(self, user_id): + req = request.get_json() + text = req.get('text', '').strip() + user = Users.query.filter_by(id=user_id).first_or_404() + + if get_mail_provider() is None: + return { + 'success': False, + 'errors': { + "": [ + "Email settings not configured" + ] + } + }, 400 + + if not text: + return { + 'success': False, + 'errors': { + "text": [ + "Email text cannot be empty" + ] + } + }, 400 + + result, response = sendmail( + addr=user.email, + text=text + ) + + return { + 'success': result, + 'data': {} + } diff --git a/CTFd/themes/admin/static/js/users/actions.js b/CTFd/themes/admin/static/js/users/actions.js index 5b95b6e3..64bc612d 100644 --- a/CTFd/themes/admin/static/js/users/actions.js +++ b/CTFd/themes/admin/static/js/users/actions.js @@ -64,6 +64,46 @@ $(document).ready(function () { }); }); + $('#user-mail-form').submit(function(e){ + e.preventDefault(); + var params = $('#user-mail-form').serializeJSON(true); + CTFd.fetch('/api/v1/users/'+USER_ID+'/email', { + method: 'POST', + credentials: 'same-origin', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(params) + }).then(function (response) { + return response.json(); + }).then(function (response) { + if (response.success) { + $('#user-mail-form > #results').append( + ezbadge({ + type: 'success', + body: 'E-Mail sent successfully!' + }) + ); + $('#user-mail-form').find("input[type=text], textarea").val("") + } else { + $('#user-mail-form > #results').empty(); + Object.keys(response.errors).forEach(function (key, index) { + $('#user-mail-form > #results').append( + ezbadge({ + type: 'error', + body: response.errors[key] + }) + ); + var i = $('#user-mail-form').find('input[name={0}], textarea[name={0}]'.format(key)); + var input = $(i); + input.addClass('input-filled-invalid'); + input.removeClass('input-filled-valid'); + }); + } + }); + }); + $('.delete-submission').click(function(e){ e.preventDefault(); var submission_id = $(this).attr('submission-id'); diff --git a/CTFd/themes/admin/templates/modals/mail/send.html b/CTFd/themes/admin/templates/modals/mail/send.html index 3a1c3538..cf482937 100644 --- a/CTFd/themes/admin/templates/modals/mail/send.html +++ b/CTFd/themes/admin/templates/modals/mail/send.html @@ -1,16 +1,16 @@ -
+
- +
-
\ No newline at end of file diff --git a/tests/api/v1/test_users.py b/tests/api/v1/test_users.py index 7e5f27fb..4108bdac 100644 --- a/tests/api/v1/test_users.py +++ b/tests/api/v1/test_users.py @@ -569,3 +569,60 @@ def test_api_user_get_awards(): r = client.get('/api/v1/users/2/awards') assert r.status_code == 200 destroy_ctfd(app) + + +def test_api_user_send_email(): + """Can an admin post /api/v1/users//email""" + app = create_ctfd() + with app.app_context(): + + register_user(app) + + with login_as_user(app) as client: + r = client.post('/api/v1/users/2/email', json={ + 'text': 'email should get rejected' + }) + assert r.status_code == 403 + + with login_as_user(app, "admin") as admin: + r = admin.post('/api/v1/users/2/email', json={ + 'text': 'email should be accepted' + }) + assert r.get_json() == { + 'success': False, + 'errors': { + "": [ + "Email settings not configured" + ] + } + } + assert r.status_code == 400 + + set_config('verify_emails', True) + set_config('mail_server', 'localhost') + set_config('mail_port', 25) + set_config('mail_useauth', True) + set_config('mail_username', 'username') + set_config('mail_password', 'password') + + with login_as_user(app, "admin") as admin: + r = admin.post('/api/v1/users/2/email', json={ + 'text': '' + }) + assert r.get_json() == { + 'success': False, + 'errors': { + "text": [ + "Email text cannot be empty" + ] + } + } + assert r.status_code == 400 + + with login_as_user(app, "admin") as admin: + r = admin.post('/api/v1/users/2/email', json={ + 'text': 'email should be accepted' + }) + assert r.status_code == 200 + + destroy_ctfd(app)