This commit is contained in:
beabigegg
2025-08-29 16:25:46 +08:00
commit b0c86302ff
65 changed files with 19786 additions and 0 deletions

View File

@@ -0,0 +1,319 @@
"""
Email Service
處理所有郵件相關功能,包括通知、提醒和摘要郵件
"""
import os
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.application import MIMEApplication
from datetime import datetime, date
from flask import current_app
from jinja2 import Environment, FileSystemLoader, select_autoescape
from utils.logger import get_logger
from utils.ldap_utils import get_user_info
logger = get_logger(__name__)
class EmailService:
"""郵件服務類別"""
def __init__(self):
self.smtp_server = os.getenv('SMTP_SERVER')
self.smtp_port = int(os.getenv('SMTP_PORT', 587))
self.use_tls = os.getenv('SMTP_USE_TLS', 'false').lower() == 'true'
self.use_ssl = os.getenv('SMTP_USE_SSL', 'false').lower() == 'true'
self.auth_required = os.getenv('SMTP_AUTH_REQUIRED', 'false').lower() == 'true'
self.sender_email = os.getenv('SMTP_SENDER_EMAIL')
self.sender_password = os.getenv('SMTP_SENDER_PASSWORD', '')
# 設定 Jinja2 模板環境
template_dir = os.path.join(os.path.dirname(__file__), '..', 'templates', 'emails')
self.jinja_env = Environment(
loader=FileSystemLoader(template_dir),
autoescape=select_autoescape(['html', 'xml'])
)
def _create_smtp_connection(self):
"""建立 SMTP 連線"""
try:
if self.use_ssl:
server = smtplib.SMTP_SSL(self.smtp_server, self.smtp_port)
else:
server = smtplib.SMTP(self.smtp_server, self.smtp_port)
if self.use_tls:
server.starttls()
if self.auth_required and self.sender_password:
server.login(self.sender_email, self.sender_password)
return server
except Exception as e:
logger.error(f"SMTP connection failed: {str(e)}")
return None
def _send_email(self, to_email, subject, html_content, text_content=None):
"""發送郵件的基礎方法"""
try:
if not self.smtp_server or not self.sender_email:
logger.error("SMTP configuration incomplete")
return False
# 建立郵件
msg = MIMEMultipart('alternative')
msg['From'] = self.sender_email
msg['To'] = to_email
msg['Subject'] = subject
# 添加文本內容
if text_content:
text_part = MIMEText(text_content, 'plain', 'utf-8')
msg.attach(text_part)
# 添加 HTML 內容
html_part = MIMEText(html_content, 'html', 'utf-8')
msg.attach(html_part)
# 發送郵件
server = self._create_smtp_connection()
if not server:
return False
server.send_message(msg)
server.quit()
logger.info(f"Email sent successfully to {to_email}")
return True
except Exception as e:
logger.error(f"Failed to send email to {to_email}: {str(e)}")
return False
def _get_user_email(self, ad_account):
"""取得使用者郵件地址"""
user_info = get_user_info(ad_account)
if user_info and user_info.get('email'):
return user_info['email']
# 如果無法從 LDAP 取得,嘗試組合郵件地址
domain = os.getenv('LDAP_DOMAIN', 'panjit.com.tw')
return f"{ad_account}@{domain}"
def send_fire_email(self, todo, recipient, sender, custom_message=''):
"""發送緊急通知郵件"""
try:
recipient_email = self._get_user_email(recipient)
sender_info = get_user_info(sender)
sender_name = sender_info.get('displayName', sender) if sender_info else sender
# 準備模板資料
template_data = {
'todo': todo,
'recipient': recipient,
'sender': sender,
'sender_name': sender_name,
'custom_message': custom_message,
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'app_name': current_app.config.get('APP_NAME', 'PANJIT Todo List')
}
# 渲染模板
template = self.jinja_env.get_template('fire_email.html')
html_content = template.render(**template_data)
# 主題
subject = f"🚨 緊急通知 - {todo.title}"
return self._send_email(recipient_email, subject, html_content)
except Exception as e:
logger.error(f"Fire email failed for {recipient}: {str(e)}")
return False
def send_reminder_email(self, todo, recipient, reminder_type):
"""發送提醒郵件"""
try:
recipient_email = self._get_user_email(recipient)
# 根據提醒類型設定主題和模板
if reminder_type == 'due_tomorrow':
subject = f"📅 明日到期提醒 - {todo.title}"
template_name = 'reminder_due_tomorrow.html'
elif reminder_type == 'overdue':
subject = f"⚠️ 逾期提醒 - {todo.title}"
template_name = 'reminder_overdue.html'
else:
subject = f"📋 待辦提醒 - {todo.title}"
template_name = 'reminder_general.html'
# 準備模板資料
template_data = {
'todo': todo,
'recipient': recipient,
'reminder_type': reminder_type,
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'app_name': current_app.config.get('APP_NAME', 'PANJIT Todo List')
}
# 渲染模板
template = self.jinja_env.get_template(template_name)
html_content = template.render(**template_data)
return self._send_email(recipient_email, subject, html_content)
except Exception as e:
logger.error(f"Reminder email failed for {recipient}: {str(e)}")
return False
def send_digest_email(self, recipient, digest_data):
"""發送摘要郵件"""
try:
recipient_email = self._get_user_email(recipient)
# 根據摘要類型設定主題
digest_type = digest_data.get('type', 'weekly')
type_names = {
'daily': '每日',
'weekly': '每週',
'monthly': '每月'
}
subject = f"📊 {type_names.get(digest_type, '定期')}摘要報告"
# 準備模板資料
template_data = {
'recipient': recipient,
'digest_data': digest_data,
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'app_name': current_app.config.get('APP_NAME', 'PANJIT Todo List')
}
# 渲染模板
template = self.jinja_env.get_template('digest.html')
html_content = template.render(**template_data)
return self._send_email(recipient_email, subject, html_content)
except Exception as e:
logger.error(f"Digest email failed for {recipient}: {str(e)}")
return False
def send_todo_notification(self, todo, recipients, action, actor):
"""發送待辦事項變更通知"""
try:
success_count = 0
for recipient in recipients:
try:
recipient_email = self._get_user_email(recipient)
actor_info = get_user_info(actor)
actor_name = actor_info.get('displayName', actor) if actor_info else actor
# 根據動作類型設定主題和模板
action_names = {
'CREATE': '建立',
'UPDATE': '更新',
'DELETE': '刪除',
'ASSIGN': '指派',
'COMPLETE': '完成'
}
action_name = action_names.get(action, action)
subject = f"📋 待辦事項{action_name} - {todo.title}"
# 準備模板資料
template_data = {
'todo': todo,
'recipient': recipient,
'action': action,
'action_name': action_name,
'actor': actor,
'actor_name': actor_name,
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'app_name': current_app.config.get('APP_NAME', 'PANJIT Todo List')
}
# 渲染模板
template = self.jinja_env.get_template('todo_notification.html')
html_content = template.render(**template_data)
if self._send_email(recipient_email, subject, html_content):
success_count += 1
except Exception as e:
logger.error(f"Todo notification failed for {recipient}: {str(e)}")
return success_count
except Exception as e:
logger.error(f"Todo notification batch failed: {str(e)}")
return 0
def send_test_email(self, recipient):
"""發送測試郵件"""
try:
recipient_email = self._get_user_email(recipient)
subject = "✅ 郵件服務測試"
html_content = f"""
<html>
<body>
<h2>郵件服務測試</h2>
<p>您好 {recipient}</p>
<p>這是一封測試郵件,用於驗證 PANJIT Todo List 系統的郵件功能是否正常運作。</p>
<p>如果您收到這封郵件,表示郵件服務配置正確。</p>
<br>
<p>測試時間: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
<p>此郵件由系統自動發送,請勿回覆。</p>
</body>
</html>
"""
return self._send_email(recipient_email, subject, html_content)
except Exception as e:
logger.error(f"Test email failed for {recipient}: {str(e)}")
return False
def send_test_email_direct(self, recipient_email):
"""直接發送測試郵件到指定郵件地址"""
try:
subject = "✅ PANJIT Todo List 郵件服務測試"
html_content = f"""
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<h2 style="color: #2563eb; border-bottom: 2px solid #e5e7eb; padding-bottom: 10px;">📧 郵件服務測試</h2>
<p>您好!</p>
<p>這是一封來自 <strong>PANJIT Todo List 系統</strong> 的測試郵件,用於驗證郵件服務功能是否正常運作。</p>
<div style="background-color: #f0f9ff; border-left: 4px solid #2563eb; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>✅ 如果您收到這封郵件,表示:</strong></p>
<ul style="margin: 10px 0; padding-left: 20px;">
<li>SMTP 服務器連線正常</li>
<li>郵件發送功能運作良好</li>
<li>您的郵件地址設定正確</li>
</ul>
</div>
<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 30px 0;">
<p style="font-size: 14px; color: #6b7280;">
<strong>測試詳細資訊:</strong><br>
📅 測試時間: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}<br>
📧 收件人: {recipient_email}<br>
🏢 發件人: PANJIT Todo List 系統
</p>
<p style="font-size: 12px; color: #9ca3af; margin-top: 30px;">
此郵件由系統自動發送,請勿回覆。如有任何問題,請聯繫系統管理員。
</p>
</div>
</body>
</html>
"""
return self._send_email(recipient_email, subject, html_content)
except Exception as e:
logger.error(f"Direct test email failed for {recipient_email}: {str(e)}")
return False