""" 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(), 'category': row.get('部門', '').strip(), 'duration': row.get('時間', '').strip(), 'member_name': row.get('氏名1', '').strip(), 'phone': 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メール送信完了!'))