TiledViz
Loading...
Searching...
No Matches
email_utils.py
1# -*- coding: utf-8 -*-
2
3import os
4import jwt
5import random
6import datetime
7from flask import current_app, flash, redirect
8from flask_mail import Mail, Message
9
10MAIL_SERVER='SMTP_SERVER'
11MAIL_PORT=0
12MAIL_USE_SSL=False
13MAIL_USE_TLS=False
14MAIL_USERNAME='SMTP_USERNAME'
15MAIL_PASSWORD='SMTP_PASSWORD'
16MAIL_DEFAULT_SENDER='FROM_EMAIL'
17IMAP_SERVER='IMAP_SERVER'
18IMAP_PORT=0
19
20App=""
21mail=""
22
23# Flask-Mail Configuration
24def init_mail(app):
25 """
26 Initialize Flask-Mail with SMTP parameters from environment variables
27 """
28 global App, mail, \
29 MAIL_SERVER, MAIL_PORT, \
30 MAIL_USE_SSL, MAIL_USE_TLS, \
31 MAIL_USERNAME, MAIL_PASSWORD, MAIL_DEFAULT_SENDER, \
32 IMAP_SERVER, IMAP_PORT
33
34
35 # Read MAIL environment variables
36 MAIL_SERVER=os.getenv('SMTP_SERVER')
37 MAIL_PORT=int(os.getenv('SMTP_PORT'))
38 MAIL_USE_SSL=os.getenv('SMTP_USE_SSL').lower() == 'true'
39 MAIL_USE_TLS=os.getenv('SMTP_USE_TLS').lower() == 'true'
40 MAIL_USERNAME=os.getenv('SMTP_USERNAME')
41 MAIL_PASSWORD=os.getenv('SMTP_PASSWORD')
42 MAIL_DEFAULT_SENDER=os.getenv('FROM_EMAIL')
43 IMAP_SERVER=os.getenv('IMAP_SERVER')
44 IMAP_PORT=int(os.getenv('IMAP_PORT'))
45
46 app.config['MAIL_SERVER'] = MAIL_SERVER
47 app.config['MAIL_PORT'] = MAIL_PORT
48 app.config['MAIL_USE_SSL'] = MAIL_USE_SSL
49 app.config['MAIL_USE_TLS'] = MAIL_USE_TLS
50 app.config['MAIL_USERNAME'] = MAIL_USERNAME
51 app.config['MAIL_PASSWORD'] = MAIL_PASSWORD
52 app.config['MAIL_DEFAULT_SENDER'] = MAIL_DEFAULT_SENDER
53 App=app
54
55 # Initialize Flask-Mail
56 mail = Mail(app)
57 return mail
58
59# Global variable to store mail instance
60mail_instance = None
61
62def get_mail():
63 """
64 Return Flask-Mail instance
65 """
66 global mail
67
68 if mail is None:
69 # Throw an error
70 flash("Error with SMTP mail server not configured.")
71 return redirect(url_for("home"))
72
73 return mail
74
75# Token Generation and Verification Functions
76def generate_verification_token(user_id, secret_key=None, expiration_sec=3600):
77 """
78 Generate a JWT token for email verification
79 Args:
80 user_id: The user's ID
81 secret_key: Secret key for JWT signing (if None, will try to get from current_app)
82 expiration_sec: Token expiration time in seconds (default: 1 hour)
83 Returns:
84 JWT token string
85 """
86 # Create expiration datetime (UTC)
87 expiration_time = datetime.datetime.utcnow() + datetime.timedelta(seconds=expiration_sec)
88
89 # Package data to be tokenized
90 data = {
91 "exp": expiration_time,
92 "confirm_id": user_id
93 }
94
95 # Get secret key
96 if secret_key is None:
97 try:
98 secret_key = current_app.secret_key
99 except RuntimeError:
100 # Fallback if not in app context
101 secret_key = os.getenv('SECRET_KEY', 'I-am-a-funny-unicorn')
102
103 # Generate token using HS512 algorithm
104 token = jwt.encode(data, secret_key, algorithm="HS512")
105 return token
106
107def verify_token(token, secret_key=None):
108 """
109 Verify and decode a JWT token
110 Args:
111 token: JWT token string
112 secret_key: Secret key for JWT verification (if None, will try to get from current_app)
113 Returns:
114 dict with user_id if valid, None if invalid
115 """
116 try:
117 # Get secret key
118 if secret_key is None:
119 try:
120 secret_key = current_app.secret_key
121 except RuntimeError:
122 # Fallback if not in app context
123 secret_key = os.getenv('SECRET_KEY', 'I-am-a-funny-unicorn')
124
125 # Decode token and verify signature
126 data = jwt.decode(token, secret_key, algorithms=["HS512"])
127 return data
128 except jwt.ExpiredSignatureError:
129 # Token expired
130 return None
131 except jwt.InvalidSignatureError:
132 # Invalid signature
133 return None
134 except jwt.InvalidTokenError:
135 # Invalid token format
136 return None
137
138# Email Sending Functions
139def send_verification_email(user_email, username, token):
140 """
141 Send verification email with JWT token
142 Args:
143 user_email: Recipient email address
144 username: Username for personalization
145 token: JWT verification token
146 Returns:
147 True if email sent successfully, False otherwise
148 """
149 try:
150 from flask import current_app
151
152 # Get SMTP configuration with fallback to hardcoded values
153 smtp_server = MAIL_SERVER
154 smtp_port = MAIL_PORT
155 smtp_username = MAIL_USERNAME
156 smtp_password = MAIL_PASSWORD
157 from_email = MAIL_DEFAULT_SENDER
158
159 # Create a fresh Mail instance
160 from flask_mail import Mail
161 mail = Mail(App)
162
163 # Create verification URL
164 server_name = os.getenv('SERVER_NAME')
165 domain = os.getenv('DOMAIN')
166 verification_url = f"https://{server_name}.{domain}/verify-email/{token}"
167
168 # Create email message
169 msg = Message(
170 subject="TiledViz - Email Verification",
171 recipients=[user_email],
172 sender=from_email
173 )
174
175 # HTML content
176 html_content = f"""
177 <html>
178 <body>
179 <h2>Welcome to TiledViz, {username}!</h2>
180 <p>Thank you for registering. Please verify your email address by clicking the link below:</p>
181 <p><a href="{verification_url}" style="background-color: #4CAF50; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">Verify Email Address</a></p>
182 <p>Or copy and paste this link in your browser:</p>
183 <p>{verification_url}</p>
184 <p>This link will expire in 1 hour.</p>
185 <p>If you didn't create an account, please ignore this email.</p>
186 <br>
187 <p>Best regards,<br>The TiledViz Team</p>
188 </body>
189 </html>
190 """
191
192 # Plain text content
193 text_content = f"""
194 Welcome to TiledViz, {username}!
195
196 Thank you for registering. Please verify your email address by visiting the link below:
197
198 {verification_url}
199
200 This link will expire in 1 hour.
201
202 If you didn't create an account, please ignore this email.
203
204 Best regards,
205 The TiledViz Team
206 """
207
208 # Set both HTML and text content
209 msg.html = html_content
210 msg.body = text_content
211
212 # Send email
213 mail.send(msg)
214
215 print(" Email sent successfully")
216 return True
217
218 except Exception as e:
219 print(f" Error sending verification email: {e}")
220 print(f" Error type: {type(e)}")
221 import traceback
222 print(f" Traceback: {traceback.format_exc()}")
223 return False
224
225def send_new_register_email(admin_emails,username,creation_date,user_email,user_company,user_manager):
226 """
227 Send email to an admin when a new user register
228 Args:
229 admin_emails : All admins emails
230 username : To see if admin knows it
231 user_email: User email for verification
232 user_company: User company for verification
233 user_manager: User manager for verification
234 Returns:
235 True if email sent successfully, False otherwise
236 """
237 try:
238 from flask import current_app
239
240 # Get SMTP configuration with fallback to hardcoded values
241 smtp_server = MAIL_SERVER
242 smtp_port = MAIL_PORT
243 smtp_username = MAIL_USERNAME
244 smtp_password = MAIL_PASSWORD
245 from_email = MAIL_DEFAULT_SENDER
246
247 # Create a fresh Mail instance
248 from flask_mail import Mail
249 mail = Mail(App)
250
251 # Create email message
252 msg = Message(
253 subject="TiledViz - New Register",
254 bcc=admin_emails,
255 sender=from_email
256 )
257
258 # HTML content
259 html_content = f"""
260 <html>
261 <body>
262 <h2>{username} just registered in TiledViz.</h2>
263 <p>Here are the different information entered by the user :</p>
264 <p>- Username : {username}</p>
265 <p>- Email : {user_email}</p>
266 <p>- Company : {user_company}</p>
267 <p>- Manager : {user_manager}</p>
268 <p>If you did not authorize this registration, you can review or delete this account from your admin dashboard.</p>
269 <br>
270 <p>Best regards,<br>The TiledViz Team</p>
271 </body>
272 </html>
273 """
274
275 # Plain text content
276 text_content = f"""
277 {username} just registered in TiledViz.
278
279 Here are the different information entered by the user :
280
281 - Username : {username}
282 - Email : {user_email}
283 - Company : {user_company}
284 - Manager : {user_manager}
285
286 If you did not authorize this registration, you can review or delete this account from your admin dashboard.
287
288 Best regards,
289 The TiledViz Team
290 """
291
292 # Set both HTML and text content
293 msg.html = html_content
294 msg.body = text_content
295
296 # Send email
297 mail.send(msg)
298
299 print(" Email sent successfully")
300 return True
301
302 except Exception as e:
303 print(f" Error sending verification email: {e}")
304 print(f" Error type: {type(e)}")
305 import traceback
306 print(f" Traceback: {traceback.format_exc()}")
307 return False
308
309# IMAP Functions for Email Management
310def delete_sent_email(subject, recipient):
311 """
312 Delete sent email from IMAP Sent folder
313 Args:
314 subject: Email subject to search for
315 recipient: Recipient email address
316 Returns:
317 True if email deleted successfully, False otherwise
318 """
319 try:
320 # Validate parameters
321 if not subject or not recipient:
322 print(f"Error deleting sent email: Missing subject or recipient (subject={subject}, recipient={recipient})")
323 return False
324
325 # IMAP configuration from environment variables
326 imap_server = IMAP_SERVER
327 imap_port = IMAP_PORT
328 username = MAIL_USERNAME
329 password = MAIL_PASSWORD
330
331 # Check if password is available (required for IMAP)
332 if not password:
333 print(f"Error deleting sent email: SMTP_PASSWORD not configured")
334 return False
335
336 # Connect to IMAP server
337 import imaplib
338 try:
339 mail = imaplib.IMAP4_SSL(imap_server, imap_port)
340 mail.login(username, password)
341 except Exception as e:
342 print(f"Error connecting to IMAP server: {e}")
343 return False
344
345 # Select Sent folder
346 mail.select('INBOX.Sent') # OVH uses INBOX.Sent for sent emails
347
348 # Search for email with specific subject and recipient
349 search_criteria = f'(SUBJECT "{subject}" TO "{recipient}")'
350 status, messages = mail.search(None, search_criteria)
351
352 if status == 'OK' and messages and messages[0]:
353 # Get message IDs - messages[0] might be bytes or string
354 message_bytes = messages[0]
355 if isinstance(message_bytes, bytes):
356 message_str = message_bytes.decode('utf-8')
357 else:
358 message_str = str(message_bytes) if message_bytes else ''
359
360 if message_str.strip():
361 message_ids = message_str.split()
362
363 # Delete each matching email
364 for msg_id in message_ids:
365 mail.store(msg_id, '+FLAGS', '\\Deleted')
366
367 # Expunge deleted emails
368 mail.expunge()
369 mail.close()
370 mail.logout()
371 return True
372
373 mail.close()
374 mail.logout()
375 return False
376
377 except Exception as e:
378 print(f"Error deleting sent email: {e}")
379 return False
380
381
382# 2FA login
383def generate_verification_code():
384 """
385 Generate a 6 digits code for 2FA login verification
386 Args:
387 user_id: The user's ID
388 expiration_sec: Token expiration time in seconds (default: 6 minutes)
389 Returns:
390 6 digits code
391 """
392
393 # Generate code
394 code = random.randrange(100000, 999999)
395 return code
396
397
398def send_2FAcode_email(user_email, username, code):
399 """
400 Send 2FAcode email with JWT token
401 Args:
402 user_email: Recipient email address
403 username: Username for personalization
404 code: 6 digit
405 Returns:
406 True if email sent successfully, False otherwise
407 """
408 try:
409 from flask import current_app
410
411 # Get SMTP configuration with fallback to hardcoded values
412 smtp_server = MAIL_SERVER
413 smtp_port = MAIL_PORT
414 smtp_username = MAIL_USERNAME
415 smtp_password = MAIL_PASSWORD
416 from_email = MAIL_DEFAULT_SENDER
417
418 # Create a fresh Mail instance
419 from flask_mail import Mail
420 mail = Mail(App)
421
422 # Create verification URL
423 server_name = os.getenv('SERVER_NAME')
424 domain = os.getenv('DOMAIN')
425
426 # Create email message
427 msg = Message(
428 subject="TiledViz - 2FA code",
429 recipients=[user_email],
430 sender=from_email
431 )
432
433 # HTML content
434 html_content = f"""
435 <html>
436 <body>
437 <h2>Welcome to TiledViz, {username}!</h2>
438 <p>You tried to login to TiledViz. Use this code in login page to validate 2FA verification.
439 <p><a style="background-color: #4CAF50; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">{code}</a></p>
440 <p>This link will expire in 5 minutes.</p>
441 <br>
442 <p>Best regards,<br>The TiledViz Team</p>
443 </body>
444 </html>
445 """
446
447 # Plain text content
448 text_content = f"""
449 Welcome to TiledViz, {username}!
450
451 Please use this code to validate your 2FA security check:
452
453 {code}
454
455 This code will expire in 5 minutes.
456
457 If you didn't create an account, please ignore this email.
458
459 Best regards,
460 The TiledViz Team
461 """
462
463 # Set both HTML and text content
464 msg.html = html_content
465 msg.body = text_content
466
467 # Send email
468 mail.send(msg)
469
470 print(" Email sent successfully")
471 return True
472
473 except Exception as e:
474 print(f" Error sending verification email: {e}")
475 print(f" Error type: {type(e)}")
476 import traceback
477 print(f" Traceback: {traceback.format_exc()}")
478 return False