Skip to content

Commit 1e41f92

Browse files
authored
Merge pull request #14 from rubybui/add-email
add mailer
2 parents 58e7f81 + 87a912e commit 1e41f92

8 files changed

Lines changed: 216 additions & 2 deletions

File tree

mind_matter_api/api/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@
1212
from mind_matter_api.repositories.survey_responses import SurveyResponseRepository
1313
from mind_matter_api.services.surveys import SurveyService
1414

15+
# email
16+
from mind_matter_api.api.email import init_email_routes
17+
1518
def register_routes(app):
1619
# --- wire up your services ---
1720
app.user_service = UserService(UserRepository())
1821

1922
# --- mount your route init functions ---
2023
init_user_routes(app)
21-
24+
init_email_routes(app)
2225

2326
app.survey_service = SurveyService(SurveyRepository(),
2427
SurveyQuestionRepository(),

mind_matter_api/api/email.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from flask import Blueprint, jsonify, request
2+
from mind_matter_api.utils.email import send_email, send_welcome_email, send_password_reset_email
3+
4+
def init_email_routes(app):
5+
"""Initialize email-related API routes."""
6+
7+
@app.route('/test-email', methods=['POST'])
8+
def test_email():
9+
"""Test endpoint for sending emails."""
10+
try:
11+
data = request.get_json()
12+
email_type = data.get('type', 'general')
13+
to_email = data.get('to')
14+
15+
if not to_email:
16+
return jsonify({'error': 'Email address is required'}), 400
17+
18+
if email_type == 'welcome':
19+
success = send_welcome_email(to=to_email, name='Test User')
20+
elif email_type == 'reset':
21+
success = send_password_reset_email(to=to_email, reset_token='test-token-123')
22+
else:
23+
# Send general test email
24+
success = send_email(
25+
to=to_email,
26+
subject='Test Email from Mind Matter',
27+
html='<h1>Test Email</h1><p>This is a test email from Mind Matter API.</p>',
28+
text='Test Email\nThis is a test email from Mind Matter API.'
29+
)
30+
31+
if success:
32+
return jsonify({'message': f'Test {email_type} email sent successfully'}), 200
33+
else:
34+
return jsonify({'error': 'Failed to send email'}), 500
35+
36+
except Exception as e:
37+
return jsonify({'error': str(e)}), 500

mind_matter_api/api/routes.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from flask import Blueprint, jsonify, request
2+
from mind_matter_api.utils.email import send_email, send_welcome_email, send_password_reset_email
3+
4+
api = Blueprint('api', __name__)
5+
6+
@api.route('/test-email', methods=['POST'])
7+
def test_email():
8+
"""Test endpoint for sending emails."""
9+
try:
10+
data = request.get_json()
11+
email_type = data.get('type', 'general')
12+
to_email = data.get('to')
13+
14+
if not to_email:
15+
return jsonify({'error': 'Email address is required'}), 400
16+
17+
if email_type == 'welcome':
18+
success = send_welcome_email(to=to_email, name='Test User')
19+
elif email_type == 'reset':
20+
success = send_password_reset_email(to=to_email, reset_token='test-token-123')
21+
else:
22+
# Send general test email
23+
success = send_email(
24+
to=to_email,
25+
subject='Test Email from Mind Matter',
26+
html='<h1>Test Email</h1><p>This is a test email from Mind Matter API.</p>',
27+
text='Test Email\nThis is a test email from Mind Matter API.'
28+
)
29+
30+
if success:
31+
return jsonify({'message': f'Test {email_type} email sent successfully'}), 200
32+
else:
33+
return jsonify({'error': 'Failed to send email'}), 500
34+
35+
except Exception as e:
36+
return jsonify({'error': str(e)}), 500

mind_matter_api/app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
flask_static_digest,
2323
migrate,
2424
login_manager,
25+
mail,
2526
)
2627

2728

@@ -87,6 +88,7 @@ def register_extensions(app):
8788
migrate.init_app(app, db)
8889
flask_static_digest.init_app(app)
8990
login_manager.init_app(app)
91+
mail.init_app(app)
9092

9193

9294
def register_errorhandlers(app):

mind_matter_api/extensions.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from flask_migrate import Migrate
77
from flask_sqlalchemy import SQLAlchemy
88
from flask_marshmallow import Marshmallow
9+
from flask_mail import Mail
910

1011
from flask_static_digest import FlaskStaticDigest
1112
from flask_wtf.csrf import CSRFProtect
@@ -17,4 +18,5 @@
1718
cache = Cache()
1819
debug_toolbar = DebugToolbarExtension()
1920
flask_static_digest = FlaskStaticDigest()
20-
login_manager = LoginManager()
21+
login_manager = LoginManager()
22+
mail = Mail()

mind_matter_api/settings.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@
1717
SECRET_KEY = env.str("SECRET_KEY")
1818
SEND_FILE_MAX_AGE_DEFAULT = env.int("SEND_FILE_MAX_AGE_DEFAULT")
1919

20+
# Mail settings
21+
MAIL_SERVER = env.str("MAIL_SERVER", default="smtp.gmail.com")
22+
MAIL_PORT = env.int("MAIL_PORT", default=587)
23+
MAIL_USE_TLS = env.bool("MAIL_USE_TLS", default=True)
24+
MAIL_USERNAME = env.str("MAIL_USERNAME", default="")
25+
MAIL_PASSWORD = env.str("MAIL_PASSWORD", default="")
26+
MAIL_DEFAULT_SENDER = env.str("MAIL_DEFAULT_SENDER", default="")
27+
MAIL_MAX_EMAILS = env.int("MAIL_MAX_EMAILS", default=None)
28+
MAIL_ASCII_ATTACHMENTS = env.bool("MAIL_ASCII_ATTACHMENTS", default=False)
29+
MAIL_SUPPRESS_SEND = env.bool("MAIL_SUPPRESS_SEND", default=False)
30+
2031
DEBUG_TB_ENABLED = DEBUG
2132
DEBUG_TB_INTERCEPT_REDIRECTS = False
2233
CACHE_TYPE = (

mind_matter_api/utils/email.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# -*- coding: utf-8 -*-
2+
"""Email utility functions."""
3+
import logging
4+
from typing import List, Optional, Union
5+
from flask import current_app
6+
from flask_mail import Message
7+
from mind_matter_api.extensions import mail
8+
9+
logger = logging.getLogger(__name__)
10+
11+
def send_email(
12+
to: Union[str, List[str]],
13+
subject: str,
14+
html: str,
15+
text: Optional[str] = None,
16+
cc: Optional[Union[str, List[str]]] = None,
17+
bcc: Optional[Union[str, List[str]]] = None,
18+
attachments: Optional[List[tuple]] = None
19+
) -> bool:
20+
"""Send an email using Flask-Mail.
21+
22+
Args:
23+
to: Email address(es) of the recipient(s)
24+
subject: Subject of the email
25+
html: HTML content of the email
26+
text: Plain text content of the email (optional)
27+
cc: Email address(es) to CC (optional)
28+
bcc: Email address(es) to BCC (optional)
29+
attachments: List of attachments as (filename, content_type, data) tuples (optional)
30+
31+
Returns:
32+
bool: True if email was sent successfully, False otherwise
33+
"""
34+
try:
35+
msg = Message(
36+
subject=subject,
37+
recipients=[to] if isinstance(to, str) else to,
38+
html=html,
39+
body=text,
40+
cc=[cc] if isinstance(cc, str) else cc,
41+
bcc=[bcc] if isinstance(bcc, str) else bcc,
42+
attachments=attachments,
43+
sender=current_app.config['MAIL_DEFAULT_SENDER']
44+
)
45+
46+
mail.send(msg)
47+
logger.info(f"Email sent successfully to {to}")
48+
return True
49+
50+
except Exception as e:
51+
logger.error(f"Failed to send email to {to}: {str(e)}")
52+
return False
53+
54+
def send_password_reset_email(to: str, reset_token: str) -> bool:
55+
"""Send a password reset email.
56+
57+
Args:
58+
to: Email address of the recipient
59+
reset_token: Password reset token
60+
61+
Returns:
62+
bool: True if email was sent successfully, False otherwise
63+
"""
64+
reset_url = f"{current_app.config.get('FRONTEND_URL', '')}/reset-password?token={reset_token}"
65+
66+
html = f"""
67+
<h1>Password Reset Request</h1>
68+
<p>You have requested to reset your password. Click the link below to proceed:</p>
69+
<p><a href="{reset_url}">Reset Password</a></p>
70+
<p>If you did not request this, please ignore this email.</p>
71+
<p>This link will expire in 1 hour.</p>
72+
"""
73+
74+
text = f"""
75+
Password Reset Request
76+
77+
You have requested to reset your password. Visit the link below to proceed:
78+
{reset_url}
79+
80+
If you did not request this, please ignore this email.
81+
This link will expire in 1 hour.
82+
"""
83+
84+
return send_email(
85+
to=to,
86+
subject="Password Reset Request",
87+
html=html,
88+
text=text
89+
)
90+
91+
def send_welcome_email(to: str, name: str) -> bool:
92+
"""Send a welcome email to new users.
93+
94+
Args:
95+
to: Email address of the recipient
96+
name: Name of the recipient
97+
98+
Returns:
99+
bool: True if email was sent successfully, False otherwise
100+
"""
101+
html = f"""
102+
<h1>Welcome to Mind Matter!</h1>
103+
<p>Hello {name},</p>
104+
<p>Thank you for joining Mind Matter. We're excited to have you on board!</p>
105+
<p>You can now log in to your account and start exploring our features.</p>
106+
"""
107+
108+
text = f"""
109+
Welcome to Mind Matter!
110+
111+
Hello {name},
112+
113+
Thank you for joining Mind Matter. We're excited to have you on board!
114+
You can now log in to your account and start exploring our features.
115+
"""
116+
117+
return send_email(
118+
to=to,
119+
subject="Welcome to Mind Matter!",
120+
html=html,
121+
text=text
122+
)

requirements/prod.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,4 @@ pytest-cov==6.0.0
5151
WebTest==3.0.2
5252
flasgger
5353

54+
Flask-Mail

0 commit comments

Comments
 (0)