"""Email utilities for sending notifications and reports""" import os from flask import current_app, render_template, url_for from flask_mail import Mail, Message from threading import Thread from datetime import datetime, timedelta mail = Mail() def init_mail(app): """Initialize Flask-Mail with the app Checks for database settings first, then falls back to environment variables. """ # First, load defaults from environment variables app.config['MAIL_SERVER'] = os.getenv('MAIL_SERVER', 'localhost') app.config['MAIL_PORT'] = int(os.getenv('MAIL_PORT', 587)) app.config['MAIL_USE_TLS'] = os.getenv('MAIL_USE_TLS', 'true').lower() == 'true' app.config['MAIL_USE_SSL'] = os.getenv('MAIL_USE_SSL', 'false').lower() == 'true' app.config['MAIL_USERNAME'] = os.getenv('MAIL_USERNAME') app.config['MAIL_PASSWORD'] = os.getenv('MAIL_PASSWORD') app.config['MAIL_DEFAULT_SENDER'] = os.getenv('MAIL_DEFAULT_SENDER', 'noreply@timetracker.local') app.config['MAIL_MAX_EMAILS'] = int(os.getenv('MAIL_MAX_EMAILS', 100)) # Check if database settings should override environment variables try: from app.models import Settings from app import db if db.session.is_active: settings = Settings.get_settings() db_config = settings.get_mail_config() if db_config: # Database settings take precedence app.config.update(db_config) app.logger.info("Using database email configuration") else: app.logger.info("Using environment variable email configuration") except Exception as e: # If database is not available, fall back to environment variables app.logger.debug(f"Could not load email settings from database: {e}") mail.init_app(app) return mail def reload_mail_config(app): """Reload email configuration from database Call this after updating email settings in the database to apply changes. """ try: from app.models import Settings settings = Settings.get_settings() db_config = settings.get_mail_config() if db_config: # Update app configuration app.config.update(db_config) # Reinitialize mail with new config mail.init_app(app) return True return False except Exception as e: app.logger.error(f"Failed to reload email configuration: {e}") return False def send_async_email(app, msg): """Send email asynchronously in background thread""" with app.app_context(): try: mail.send(msg) except Exception as e: current_app.logger.error(f"Failed to send email: {e}") def send_email(subject, recipients, text_body, html_body=None, sender=None, attachments=None): """Send an email Args: subject: Email subject line recipients: List of recipient email addresses text_body: Plain text email body html_body: HTML email body (optional) sender: Sender email address (optional, uses default if not provided) attachments: List of (filename, content_type, data) tuples """ if not current_app.config.get('MAIL_SERVER'): current_app.logger.warning("Mail server not configured, skipping email send") return if not recipients: current_app.logger.warning("No recipients specified for email") return msg = Message( subject=subject, recipients=recipients if isinstance(recipients, list) else [recipients], body=text_body, html=html_body, sender=sender or current_app.config['MAIL_DEFAULT_SENDER'] ) # Add attachments if provided if attachments: for filename, content_type, data in attachments: msg.attach(filename, content_type, data) # Send asynchronously Thread(target=send_async_email, args=(current_app._get_current_object(), msg)).start() def send_overdue_invoice_notification(invoice, user): """Send notification about an overdue invoice Args: invoice: Invoice object user: User object (invoice creator or admin) """ if not user.email or not user.email_notifications or not user.notification_overdue_invoices: return days_overdue = (datetime.utcnow().date() - invoice.due_date).days subject = f"Invoice {invoice.invoice_number} is {days_overdue} days overdue" text_body = f""" Hello {user.display_name}, Invoice {invoice.invoice_number} for {invoice.client_name} is now {days_overdue} days overdue. Invoice Details: - Invoice Number: {invoice.invoice_number} - Client: {invoice.client_name} - Amount: {invoice.currency_code} {invoice.total_amount} - Due Date: {invoice.due_date} - Days Overdue: {days_overdue} Please follow up with the client or update the invoice status. View invoice: {url_for('invoices.view_invoice', invoice_id=invoice.id, _external=True)} --- TimeTracker - Time Tracking & Project Management """ html_body = render_template( 'email/overdue_invoice.html', user=user, invoice=invoice, days_overdue=days_overdue ) send_email(subject, user.email, text_body, html_body) def send_task_assigned_notification(task, user, assigned_by): """Send notification when a user is assigned to a task Args: task: Task object user: User who was assigned assigned_by: User who made the assignment """ if not user.email or not user.email_notifications or not user.notification_task_assigned: return subject = f"You've been assigned to task: {task.name}" text_body = f""" Hello {user.display_name}, {assigned_by.display_name} has assigned you to a task. Task Details: - Task: {task.name} - Project: {task.project.name if task.project else 'N/A'} - Priority: {task.priority or 'Normal'} - Due Date: {task.due_date if task.due_date else 'Not set'} - Status: {task.status} Description: {task.description or 'No description provided'} View task: {url_for('tasks.edit_task', task_id=task.id, _external=True)} --- TimeTracker - Time Tracking & Project Management """ html_body = render_template( 'email/task_assigned.html', user=user, task=task, assigned_by=assigned_by ) send_email(subject, user.email, text_body, html_body) def send_weekly_summary(user, start_date, end_date, hours_worked, projects_data): """Send weekly time tracking summary to user Args: user: User object start_date: Start of the week end_date: End of the week hours_worked: Total hours worked projects_data: List of dicts with project data """ if not user.email or not user.email_notifications or not user.notification_weekly_summary: return subject = f"Your Weekly Time Summary ({start_date} to {end_date})" # Build project summary text project_summary = "\n".join([ f"- {p['name']}: {p['hours']:.1f} hours" for p in projects_data ]) text_body = f""" Hello {user.display_name}, Here's your time tracking summary for the week of {start_date} to {end_date}: Total Hours: {hours_worked:.1f} Hours by Project: {project_summary} Keep up the great work! View detailed reports: {url_for('reports.reports', _external=True)} --- TimeTracker - Time Tracking & Project Management """ html_body = render_template( 'email/weekly_summary.html', user=user, start_date=start_date, end_date=end_date, hours_worked=hours_worked, projects_data=projects_data ) send_email(subject, user.email, text_body, html_body) def send_comment_notification(comment, task, mentioned_users): """Send notification about a new comment Args: comment: Comment object task: Task the comment is on mentioned_users: List of User objects mentioned in the comment """ for user in mentioned_users: if not user.email or not user.email_notifications or not user.notification_task_comments: continue subject = f"You were mentioned in a comment on: {task.name}" text_body = f""" Hello {user.display_name}, {comment.user.display_name} mentioned you in a comment on task "{task.name}". Comment: {comment.content} Task: {task.name} Project: {task.project.name if task.project else 'N/A'} View task: {url_for('tasks.edit_task', task_id=task.id, _external=True)} --- TimeTracker - Time Tracking & Project Management """ html_body = render_template( 'email/comment_mention.html', user=user, comment=comment, task=task ) send_email(subject, user.email, text_body, html_body) def test_email_configuration(): """Test email configuration and return status Returns: dict: Status information with 'configured', 'settings', 'errors', 'source' keys """ status = { 'configured': False, 'settings': {}, 'errors': [], 'warnings': [], 'source': 'environment' # or 'database' } # Check if database configuration is enabled try: from app.models import Settings settings = Settings.get_settings() if settings.mail_enabled and settings.mail_server: status['source'] = 'database' mail_server = settings.mail_server mail_port = settings.mail_port mail_username = settings.mail_username mail_password = settings.mail_password mail_use_tls = settings.mail_use_tls mail_use_ssl = settings.mail_use_ssl mail_default_sender = settings.mail_default_sender else: # Use environment/app config mail_server = current_app.config.get('MAIL_SERVER') mail_port = current_app.config.get('MAIL_PORT') mail_username = current_app.config.get('MAIL_USERNAME') mail_password = current_app.config.get('MAIL_PASSWORD') mail_use_tls = current_app.config.get('MAIL_USE_TLS') mail_use_ssl = current_app.config.get('MAIL_USE_SSL') mail_default_sender = current_app.config.get('MAIL_DEFAULT_SENDER') except Exception: # Fall back to app config if database not available mail_server = current_app.config.get('MAIL_SERVER') mail_port = current_app.config.get('MAIL_PORT') mail_username = current_app.config.get('MAIL_USERNAME') mail_password = current_app.config.get('MAIL_PASSWORD') mail_use_tls = current_app.config.get('MAIL_USE_TLS') mail_use_ssl = current_app.config.get('MAIL_USE_SSL') mail_default_sender = current_app.config.get('MAIL_DEFAULT_SENDER') status['settings'] = { 'server': mail_server or 'Not configured', 'port': mail_port or 'Not configured', 'username': mail_username or 'Not configured', 'password_set': bool(mail_password), 'use_tls': mail_use_tls, 'use_ssl': mail_use_ssl, 'default_sender': mail_default_sender or 'Not configured' } # Check for configuration issues if not mail_server or mail_server == 'localhost': status['errors'].append('Mail server not configured or set to localhost') if not mail_default_sender or mail_default_sender == 'noreply@timetracker.local': status['warnings'].append('Default sender email should be configured with a real email address') if mail_use_tls and mail_use_ssl: status['errors'].append('Cannot use both TLS and SSL. Choose one.') if not mail_username and mail_server not in ['localhost', '127.0.0.1']: status['warnings'].append('MAIL_USERNAME not set (may be required for authentication)') if not mail_password and mail_username: status['warnings'].append('MAIL_PASSWORD not set but MAIL_USERNAME is configured') # Mark as configured if minimum requirements are met status['configured'] = bool(mail_server and mail_server != 'localhost' and not status['errors']) return status def send_test_email(recipient_email, sender_name='TimeTracker Admin'): """Send a test email to verify email configuration Args: recipient_email: Email address to send test email to sender_name: Name of the sender Returns: tuple: (success: bool, message: str) """ try: current_app.logger.info(f"[EMAIL TEST] Starting test email send to: {recipient_email}") # Validate recipient email if not recipient_email or '@' not in recipient_email: current_app.logger.warning(f"[EMAIL TEST] Invalid recipient email: {recipient_email}") return False, 'Invalid recipient email address' # Check if mail is configured mail_server = current_app.config.get('MAIL_SERVER') if not mail_server: current_app.logger.error("[EMAIL TEST] Mail server not configured") return False, 'Mail server not configured. Please set MAIL_SERVER in environment variables.' # Log current configuration current_app.logger.info(f"[EMAIL TEST] Configuration:") current_app.logger.info(f" - Server: {mail_server}:{current_app.config.get('MAIL_PORT')}") current_app.logger.info(f" - TLS: {current_app.config.get('MAIL_USE_TLS')}") current_app.logger.info(f" - SSL: {current_app.config.get('MAIL_USE_SSL')}") current_app.logger.info(f" - Username: {current_app.config.get('MAIL_USERNAME')}") current_app.logger.info(f" - Sender: {current_app.config.get('MAIL_DEFAULT_SENDER')}") subject = 'TimeTracker Email Test' text_body = f""" Hello, This is a test email from TimeTracker to verify your email configuration is working correctly. If you received this email, your email settings are properly configured! Test Details: - Sent at: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC - Sent by: {sender_name} - Mail Server: {current_app.config.get('MAIL_SERVER')}:{current_app.config.get('MAIL_PORT')} - TLS Enabled: {current_app.config.get('MAIL_USE_TLS')} - SSL Enabled: {current_app.config.get('MAIL_USE_SSL')} --- TimeTracker - Time Tracking & Project Management """ try: html_body = render_template( 'email/test_email.html', sender_name=sender_name, mail_server=current_app.config.get('MAIL_SERVER'), mail_port=current_app.config.get('MAIL_PORT'), use_tls=current_app.config.get('MAIL_USE_TLS'), use_ssl=current_app.config.get('MAIL_USE_SSL') ) current_app.logger.info("[EMAIL TEST] HTML template rendered successfully") except Exception as template_error: # If template doesn't exist, use text only current_app.logger.warning(f"[EMAIL TEST] HTML template not available: {template_error}") html_body = None # Create message current_app.logger.info("[EMAIL TEST] Creating email message") msg = Message( subject=subject, recipients=[recipient_email], body=text_body, html=html_body, sender=current_app.config['MAIL_DEFAULT_SENDER'] ) # Send synchronously for testing (so we can catch errors) current_app.logger.info("[EMAIL TEST] Attempting to send email via SMTP...") mail.send(msg) current_app.logger.info(f"[EMAIL TEST] ✓ Email sent successfully to {recipient_email}") return True, f'Test email sent successfully to {recipient_email}' except Exception as e: current_app.logger.error(f"[EMAIL TEST] ✗ Failed to send test email: {type(e).__name__}: {str(e)}") current_app.logger.exception("[EMAIL TEST] Full exception trace:") return False, f'Failed to send test email: {str(e)}'