Repository: CTFd/CTFd

Issue:
Send email from user management screen
<!--
If this is a bug report please fill out the template below.

If this is a feature request please describe the behavior that you'd like to see.
-->

**Environment**:

  - CTFd Version/Commit: 2.0.4
  - Operating System: Ubuntu
  - Web Browser and Version: Same in any (chrome, FF)

**What happened?**

In users management screen, I click on a user and click on the "Send message" button. It does nothing.

**What did you expect to happen?**

Send an email to that user.

**How to reproduce your issue**

**Any associated stack traces or error logs**




Discussion:

Resolving patch:
diff --git a/CTFd/api/v1/users.py b/CTFd/api/v1/users.py
--- a/CTFd/api/v1/users.py
+++ b/CTFd/api/v1/users.py
@@ -4,9 +4,12 @@
 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 @@ def get(self, user_id):
             'success': True,
             'data': response.data
         }
+
+
+@users_namespace.route('/<int:user_id>/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': {}
+        }


Test patch:
diff --git a/tests/api/v1/test_users.py b/tests/api/v1/test_users.py
--- 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/<user_id>/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)