Files
rogaining_srv/rog/management/commands/send_team_emails.py
2025-09-05 23:04:40 +09:00

284 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
CSVファイルからチーム情報を読み込んでメール送信するDjango管理コマンド
Outlook SMTP (rogaining@gifuai.net) での送信に対応
Usage:
# ドライラン(実際には送信しない)
docker compose exec app python manage.py send_team_emails --csv_file="CPLIST/input/team_mail.csv" --dry_run
# 実際のメール送信送信間隔1秒
docker compose exec app python manage.py send_team_emails --csv_file="CPLIST/input/team_mail.csv"
# カスタム送信間隔3秒間隔
docker compose exec app python manage.py send_team_emails --csv_file="CPLIST/input/team_mail.csv" --delay=3
Author: システム開発チーム
Date: 2025-09-05
"""
import csv
import os
import time
from django.core.management.base import BaseCommand, CommandError
from django.core.mail import send_mail
from django.conf import settings
from django.template.loader import render_to_string
from datetime import datetime
class Command(BaseCommand):
help = 'CSVファイルからチーム情報を読み込んでOutlook SMTPでメール送信'
def add_arguments(self, parser):
parser.add_argument(
'--csv_file',
type=str,
required=True,
help='CSVファイルのパス (例: CPLIST/input/team_mail.csv)'
)
parser.add_argument(
'--dry_run',
action='store_true',
help='ドライラン(実際にはメール送信しない)'
)
parser.add_argument(
'--delay',
type=int,
default=1,
help='メール送信間隔(秒)デフォルト: 1秒'
)
parser.add_argument(
'--test_email',
type=str,
help='テスト用メールアドレス(指定した場合、全てのメールをこのアドレスに送信)'
)
def handle(self, *args, **options):
csv_file = options['csv_file']
dry_run = options['dry_run']
delay = options['delay']
test_email = options.get('test_email')
mode = '[DRY RUN] ' if dry_run else ''
self.stdout.write(f'{mode}CSVメール送信開始: csv_file={csv_file}')
if test_email:
self.stdout.write(f'🧪 テストモード: 全メールを {test_email} に送信')
# CSVファイル存在確認
if not os.path.exists(csv_file):
raise CommandError(f'CSVファイルが見つかりません: {csv_file}')
# SMTP設定確認
self.verify_email_settings()
# 統計情報
stats = {
'total_rows': 0,
'emails_sent': 0,
'errors': []
}
# CSVファイル読み込み・メール送信
try:
with open(csv_file, 'r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f)
for row_num, row in enumerate(reader, start=2):
stats['total_rows'] += 1
try:
# CSVデータ取得
team_data = self.extract_team_data(row)
# メール内容生成
email_content = self.generate_email_content(team_data)
subject = email_content['subject']
body = email_content['body']
# 送信先決定テストモードならtest_emailに
recipient = test_email if test_email else team_data['email']
self.stdout.write(f'{mode}{row_num}: {team_data["team_name"]}{recipient}')
if dry_run:
self.show_email_preview(subject, body)
else:
# 実際のメール送信
self.send_email(subject, body, recipient, team_data)
self.stdout.write(f' ✅ メール送信完了')
# 送信間隔
if delay > 0:
time.sleep(delay)
stats['emails_sent'] += 1
except Exception as e:
error_msg = f'{row_num}: {str(e)}'
stats['errors'].append(error_msg)
self.stdout.write(self.style.ERROR(error_msg))
continue
except Exception as e:
raise CommandError(f'CSVファイル読み込みエラー: {str(e)}')
# 結果レポート
self.print_stats(stats, dry_run)
def verify_email_settings(self):
"""SMTP設定確認"""
required_settings = [
'EMAIL_HOST', 'EMAIL_PORT', 'EMAIL_HOST_USER',
'EMAIL_HOST_PASSWORD', 'DEFAULT_FROM_EMAIL'
]
for setting in required_settings:
if not hasattr(settings, setting) or not getattr(settings, setting):
raise CommandError(f'メール設定が不完全です: {setting}')
self.stdout.write(f'📧 SMTP設定確認完了: {settings.EMAIL_HOST}:{settings.EMAIL_PORT}')
self.stdout.write(f'📧 送信者: {settings.DEFAULT_FROM_EMAIL}')
def extract_team_data(self, row):
"""CSVデータからチーム情報を抽出"""
team_data = {
'team_name': row.get('チーム名', '').strip(),
'email': row.get('メール', '').strip(),
'password': row.get('password', '').strip(),
'category': row.get('部門', '').strip(),
'duration': row.get('時間', '').strip(),
'leader_name': row.get('氏名1', '').strip(),
'phone_number': row.get('電話番号', '').strip(),
}
# 必須項目チェック
if not team_data['team_name']:
raise ValueError('チーム名が必要です')
if not team_data['email']:
raise ValueError('メールアドレスが必要です')
return team_data
def generate_email_content(self, team_data):
"""メール内容生成(外部テンプレートファイル使用)"""
# テンプレートファイルから件名を読み込み
subject = render_to_string(
'emails/team_registration_subject.txt',
team_data
).strip()
# テンプレートファイルから本文を読み込み
body = render_to_string(
'emails/team_registration_body.txt',
team_data
).strip()
return {
'subject': subject,
'body': body
}
def show_email_preview(self, subject, body):
"""メールプレビュー表示(ドライラン用)"""
self.stdout.write(f' 件名: {subject}')
self.stdout.write(f' 本文プレビュー: {body[:100]}...')
def send_email(self, subject, body, recipient, team_data):
"""実際のメール送信"""
try:
send_mail(
subject=subject,
message=body,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[recipient],
fail_silently=False
)
except Exception as e:
raise ValueError(f'メール送信エラー ({recipient}): {str(e)}')
def print_stats(self, stats, dry_run):
"""統計情報の表示"""
mode = '[DRY RUN] ' if dry_run else ''
self.stdout.write(self.style.SUCCESS('\n' + '='*50))
self.stdout.write(self.style.SUCCESS(f'{mode}メール送信結果'))
self.stdout.write(self.style.SUCCESS('='*50))
self.stdout.write(f'処理行数: {stats["total_rows"]}')
self.stdout.write(f'メール送信数: {stats["emails_sent"]}')
if stats['errors']:
self.stdout.write(f'エラー数: {len(stats["errors"])}')
for error in stats['errors']:
self.stdout.write(f' {error}')
if not dry_run:
self.stdout.write(self.style.SUCCESS('\nメール送信完了!'))
else:
self.stdout.write(self.style.WARNING('\n※ ドライランのため、実際のメール送信は行われていません'))
def send_email_to_team(self, row, template_content, template_name, row_num, stats):
"""実際のメール送信"""
team_name = row.get('チーム名', '').strip()
email = row.get('メール', '').strip()
category = row.get('部門', '').strip()
duration = row.get('時間', '').strip()
leader_name = row.get('氏名1', '').strip()
phone = row.get('電話番号', '').strip()
if not email:
raise ValueError('メールアドレスが必要です')
if not team_name:
raise ValueError('チーム名が必要です')
# テンプレート処理
context_data = {
'event_name': '岐阜ロゲイニング2025',
'team_name': team_name,
'category': category,
'duration': duration,
'leader_name': leader_name,
'email': email,
'phone': phone,
'password': row.get('パスワード', '').strip()
}
# テンプレートファイルから件名・本文を生成
subject = render_to_string('emails/team_registration_subject.txt', context_data).strip()
body = render_to_string('emails/team_registration_body.txt', context_data).strip()
# メール送信
try:
send_mail(
subject=subject,
message=body,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[email],
fail_silently=False,
)
self.stdout.write(f'{row_num}: メール送信完了 {team_name} ({email})')
stats['emails_sent'] += 1
except Exception as e:
raise ValueError(f'メール送信エラー: {str(e)}')
def print_stats(self, stats, dry_run):
"""統計情報の表示"""
mode = '[DRY RUN] ' if dry_run else ''
self.stdout.write(self.style.SUCCESS('\n' + '='*50))
self.stdout.write(self.style.SUCCESS(f'{mode}メール送信結果'))
self.stdout.write(self.style.SUCCESS('='*50))
self.stdout.write(f'処理行数: {stats["total_rows"]}')
self.stdout.write(f'メール送信数: {stats["emails_sent"]}')
if stats['errors']:
self.stdout.write(f'\nエラー数: {len(stats["errors"])}')
for error in stats['errors']:
self.stdout.write(f' {error}')
if dry_run:
self.stdout.write(self.style.WARNING('\n※ ドライランのため、実際にはメールは送信されていません'))
else:
self.stdout.write(self.style.SUCCESS('\nメール送信完了!'))