From 272269431eb0768aef4217c6cdd3b3ac8a2ca9ef Mon Sep 17 00:00:00 2001 From: Akira Date: Fri, 5 Sep 2025 22:48:15 +0900 Subject: [PATCH] Email feature --- ...ロゲイニング2025_20250905_173618.csv | 31 ++ CPLIST/input/team_mail.csv | 2 + CPLIST/input/teams2025-errorfix.csv | 3 +- EMAIL_SENDING_MANUAL.md | 243 +++++++++++++++ config/settings.py | 2 +- rog/management/commands/send_team_emails.py | 282 ++++++++++++++++++ templates/emails/team_registration_body.txt | 21 ++ .../emails/team_registration_subject.txt | 1 + test_email.csv | 3 + メール送信マニュアル.md | 243 +++++++++++++++ 10 files changed, 828 insertions(+), 3 deletions(-) create mode 100644 CPLIST/input/import_results_岐阜ロゲイニング2025_20250905_173618.csv create mode 100644 CPLIST/input/team_mail.csv create mode 100644 EMAIL_SENDING_MANUAL.md create mode 100644 rog/management/commands/send_team_emails.py create mode 100644 templates/emails/team_registration_body.txt create mode 100644 templates/emails/team_registration_subject.txt create mode 100644 test_email.csv create mode 100644 メール送信マニュアル.md diff --git a/CPLIST/input/import_results_岐阜ロゲイニング2025_20250905_173618.csv b/CPLIST/input/import_results_岐阜ロゲイニング2025_20250905_173618.csv new file mode 100644 index 0000000..67887e3 --- /dev/null +++ b/CPLIST/input/import_results_岐阜ロゲイニング2025_20250905_173618.csv @@ -0,0 +1,31 @@ +チーム名,ゼッケン番号,カテゴリー,時間,オーナーメール,リーダー,メンバー数,メンバー一覧,参加登録状況,エントリーID,作成日時 +いなりずし,1,一般-3時間,3.0時間,いなりずし_member_1@dummy.local,児玉優美(1976-12-13),5,優美(1976-12-13); 豊久(1973-11-23); 児玉優美(1976-12-13); 児玉豊久(1973-11-23); 田中広美(1975-10-31),完了,548,2025-09-05 07:46:39 +Go to the peak!,2,一般-5時間,5.0時間,go_to_the_peak_member_1@dummy.local,柴山晋太郎(1974-12-14),3,柴山晋太郎(1974-12-14); 後藤克弘(1968-04-07); 二村修(1967-06-22),完了,549,2025-09-05 07:46:40 +きみこうじ,3,一般-3時間,3.0時間,きみこうじ_member_1@dummy.local,齋藤貴美子(1980-07-06),2,齋藤貴美子(1980-07-06); 江口浩次(1968-04-19),完了,550,2025-09-05 07:46:40 +ウエストサイド,4,一般-5時間,5.0時間,ウエストサイド_member_1@dummy.local,後藤睦子(1961-05-01),8,睦子(2000-01-01); マサトシ(2000-01-01); ショウコ(2000-01-01); ヨシミ(2000-01-01); 後藤睦子(1961-05-01); 後藤正寿(1959-07-23); 大坪照子(1958-11-11); 松村芳美(1964-04-28),完了,551,2025-09-05 07:46:40 +ベル,5,一般-3時間,3.0時間,ベル_member_1@dummy.local,川村健一(1969-10-08),7,健一(2000-01-01); ショウジ(2000-01-01); チナミ(2000-01-01); 川村健一(1969-10-08); 曽我部知奈美(1973-12-17); 伊藤徳幸(1975-02-06); 筒井勝児(1976-05-31),完了,552,2025-09-05 07:46:40 +ぐりと愉快な仲間たち,6,一般-3時間,3.0時間,ぐりと愉快な仲間たち_member_1@dummy.local,長屋香代子(1961-10-27),4,(1961-10-27); (1961-05-26); 長屋香代子(1961-10-27); 長屋宣宏(1961-05-26),完了,553,2025-09-05 07:46:40 +坂本555,7,一般-5時間,5.0時間,坂本555_member_1@dummy.local,坂本正憲(1972-05-30),3,坂本正憲(1972-05-30); 坂本彩子(1976-03-29); 坂本瑠璃子(2003-08-23),完了,554,2025-09-05 07:46:40 +M sisters with D,8,一般-5時間,5.0時間,m__sisters_with__d_member_1@dummy.local,前田貴代美(1973-01-15),2,前田貴代美(1973-01-15); 中濱智恵美(1969-06-16),完了,555,2025-09-05 07:46:41 +さなっく,9,一般-5時間,5.0時間,さなっく_member_1@dummy.local,山田朋博(1971-04-23),2,山田朋博(1971-04-23); 眞田尚亮(1982-11-30),完了,556,2025-09-05 07:46:41 +煮込みラーメン,10,一般-3時間,3.0時間,煮込みラーメン_member_1@dummy.local,西岡嵩倫(1999-01-05),4,(1999-01-05); (1971-02-02); 西岡嵩倫(1999-01-05); 西岡影忠(1971-02-02),完了,557,2025-09-05 07:46:41 +サウナとビリヤニ,11,一般-3時間,3.0時間,サウナとビリヤニ_member_1@dummy.local,坂口祐生(1992-01-07),3,坂口祐生(1992-01-07); 近藤準(1987-01-25); 圓山大貴(1993-05-10),完了,558,2025-09-05 07:46:41 +ひろ君と愉快な仲間たち,12,お試し・一般-3時間,3.0時間,ひろ君と愉快な仲間たち_member_1@dummy.local,山脇裕子(1984-01-26),5,山脇裕子(1984-01-26); 高橋美智子(1975-04-21); 樋口博久(1964-01-08); 雨宮功治(1962-05-25); 広瀬貴士(1978-08-17),完了,559,2025-09-05 07:46:41 +山下和乃,13,女性ソロ-3時間,3.0時間,山下和乃_member_1@dummy.local,山下和乃(2004-04-26),1,山下和乃(2004-04-26),完了,560,2025-09-05 07:46:42 +Best Wishes,14,女性ソロ-5時間,5.0時間,best_wishes_member_1@dummy.local,長谷川美貴(1973-05-06),2,美貴(1973-05-06); 長谷川美貴(1973-05-06),完了,561,2025-09-05 07:46:42 +しーくん,15,男性ソロ-3時間,3.0時間,しーくん_member_1@dummy.local,水門茂(1962-12-24),2,茂(1962-12-24); 水門茂(1962-12-24),完了,562,2025-09-05 07:46:42 +風呂の会,16,男性ソロ-5時間,5.0時間,風呂の会_member_1@dummy.local,浅井貴弘(1984-07-11),2,貴弘(2000-01-01); 浅井貴弘(1984-07-11),完了,563,2025-09-05 07:46:42 +近藤隆,17,男性ソロ-5時間,5.0時間,近藤隆_member_1@dummy.local,近藤隆(1962-06-28),1,近藤隆(1962-06-28),完了,564,2025-09-05 07:46:42 +日吉将大,18,男性ソロ-3時間,3.0時間,日吉将大_member_1@dummy.local,日吉将大(1995-09-14),2,(1995-09-14); 日吉将大(1995-09-14),完了,565,2025-09-05 07:46:42 +東京OLクラブ,19,男性ソロ-3時間,3.0時間,東京olクラブ_member_1@dummy.local,阿部昌隆(1956-04-20),1,阿部昌隆(1956-04-20),完了,566,2025-09-05 07:46:42 +Best Wishes,20,男性ソロ-5時間,5.0時間,best_wishes_member_1@dummy.local,長谷川美貴(1973-05-06),2,寿郎(1973-10-26); 長谷川美貴(1973-05-06),完了,567,2025-09-05 07:46:42 +脇屋貴司,21,男性ソロ-5時間,5.0時間,脇屋貴司_member_1@dummy.local,脇屋貴司(1983-10-26),1,脇屋貴司(1983-10-26),完了,568,2025-09-05 07:46:42 +うぱうぱアイランド,22,ファミリー-3時間,3.0時間,うぱうぱアイランド_member_1@dummy.local,伊藤由美子(1992-03-28),3,伊藤由美子(1992-03-28); 伊藤嘉仁(1993-08-25); 伊藤嘉利(2022-09-13),完了,569,2025-09-05 07:46:42 +Team117,23,ファミリー-3時間,3.0時間,team117_member_1@dummy.local,佐々木孝好(1970-12-20),4,佐々木孝好(1970-12-20); 佐々木享子(1977-08-25); 佐々木実希(2012-01-21); 佐々木麻妃(2016-07-01),完了,570,2025-09-05 07:46:43 +チームしぇいや,24,ファミリー-3時間,3.0時間,チームしぇいや_member_1@dummy.local,山本龍也(1976-03-14),6,聖也(2009-09-09); 輝也(2015-06-03); 龍也(1976-03-14); 山本龍也(1976-03-14); 山本聖也(2009-09-09); 山本輝也(2015-06-03),完了,571,2025-09-05 07:46:43 +まゆちー,25,お試し-3時間,3.0時間,まゆちー_member_1@dummy.local,浅田舞子(1986-02-22),4,浅田舞子(1986-02-22); 浅田真結菜(2014-03-30); 森美紀(1988-03-06); 森千晴(2017-08-04),完了,572,2025-09-05 07:56:33 +ガンバルゾー,26,お試し-3時間,3.0時間,ガンバルゾー_member_1@dummy.local,森祐貴(1985-09-26),4,森祐貴(1985-09-26); 浅田直之(1987-12-12); 浅田晃汰(2014-01-06); 森光喜(2015-04-22),完了,573,2025-09-05 07:56:33 +ランエンジョン!,27,お試し-5時間,5.0時間,ランエンジョン!_member_1@dummy.local,河合賢次(1972-12-14),2,河合賢次(1972-12-14); 中野真樹(1973-01-23),完了,574,2025-09-05 07:56:33 +fun!fun!うごchan,28,お試し-5時間,5.0時間,funfunうごchan_member_1@dummy.local,早川宏美(1975-06-15),1,早川宏美(1975-06-15),完了,575,2025-09-05 08:36:17 +ポエドリ,29,お試し-5時間,5.0時間,ポエドリ_member_1@dummy.local,高木俊裕(1984-03-09),1,高木俊裕(1984-03-09),完了,576,2025-09-05 08:36:18 +前川一彦,30,男性ソロ-5時間,5.0時間,前川一彦_member_1@dummy.local,前川一彦(1990-01-01),1,前川一彦(1990-01-01),完了,577,2025-09-05 08:36:18 diff --git a/CPLIST/input/team_mail.csv b/CPLIST/input/team_mail.csv new file mode 100644 index 0000000..b01e59e --- /dev/null +++ b/CPLIST/input/team_mail.csv @@ -0,0 +1,2 @@ +部門別数,時間,部門,チーム名,メール,パスワード,電話番号,氏名1 +2,5,一般,ウエストサイド,hannivalscipio@gmail.com,ka9749,090-4790-9749,宮田 明 diff --git a/CPLIST/input/teams2025-errorfix.csv b/CPLIST/input/teams2025-errorfix.csv index 88058ae..86cac88 100644 --- a/CPLIST/input/teams2025-errorfix.csv +++ b/CPLIST/input/teams2025-errorfix.csv @@ -1,5 +1,4 @@ -部門別数,時間,部門,"チーム名 -",メール,パスワード,電話番号,氏名1,誕生日1,氏名2,誕生日2,氏名3,誕生日3,氏名4,誕生日4,氏名5,誕生日5,氏名6,誕生日6,氏名7,誕生日7,, +部門別数,時間,部門,チーム名,メール,パスワード,電話番号,氏名1,誕生日1,氏名2,誕生日2,氏名3,誕生日3,氏名4,誕生日4,氏名5,誕生日5,氏名6,誕生日6,氏名7,誕生日7,, 2,5,お試し・ファミリー,fun!fun!うごchan,fulayota333@gmail.com,ha7384,090-6599-7384,早川宏美,1975/6/15,,,,,,,,,,,,,, 1,5,お試し・ファミリー,ポエドリ,takagitoshihiro8@yahoo.co.jp,ta4245,090-5866-4245,高木俊裕,1984/03/09,,,,,,,,,,,,,, 7,5,男性ソロ,前川一彦,yoshino-chuo@docomo.ne.jp,ma2351,090-1074-2351,前川一彦,1990/1/1,,,,,,,,,,,,,, \ No newline at end of file diff --git a/EMAIL_SENDING_MANUAL.md b/EMAIL_SENDING_MANUAL.md new file mode 100644 index 0000000..a1b33e3 --- /dev/null +++ b/EMAIL_SENDING_MANUAL.md @@ -0,0 +1,243 @@ +# チームメール送信システム操作マニュアル + +## 概要 +このシステムは、ロゲイニング大会の参加チームに対して、パスワードやイベント情報を含むメールを一括送信するためのDjango管理コマンドです。 + +## 前提条件 + +### 必要な環境 +- Docker Compose環境が稼働していること +- PostgreSQLデータベースが接続されていること +- SMTPサーバー設定が完了していること(Outlook: smtp.outlook.com:587) + +### 必要なファイル +1. **CSVファイル**: チーム情報を含むデータファイル +2. **メールテンプレートファイル**: + - `/templates/emails/team_registration_subject.txt` (件名テンプレート) + - `/templates/emails/team_registration_body.txt` (本文テンプレート) + +## CSVファイル形式 + +### ファイル配置場所 +``` +CPLIST/input/team_mail.csv +``` + +### CSVファイルの形式 +```csv +team_name,email,password,category,duration,leader_name,phone_number,member_names +チーム名,メールアドレス,パスワード,部門,時間,代表者名,電話番号,メンバー名 +``` + +### 例 +```csv +team_name,email,password,category,duration,leader_name,phone_number,member_names +ウエストサイド,hannivalscipio@gmail.com,west123,一般,3,田中太郎,090-1234-5678,田中太郎・佐藤花子 +``` + +## メールテンプレート + +### 件名テンプレート (`/templates/emails/team_registration_subject.txt`) +``` +【岐阜ロゲ in 大垣】チーム「{{ team_name }}」パスワードのご連絡 +``` + +### 本文テンプレート (`/templates/emails/team_registration_body.txt`) +``` +{{ team_name }} 代表者 {{leader_name}} 様 + +岐阜ロゲ in 大垣 へのご参加ありがとうございます。 + +ご連絡が大変遅くなり、申し訳ございません。 + +以下の内容でパスワードをお送りいたしますので、よろしくお願い申し上げます。 + +■ チーム情報 + +チーム名: {{ team_name }} +部門: {{ category }}({{ duration }}時間) + +ユーザー名: {{ email }} +パスワード: {{ password }} + + +-- +岐阜ロゲ in 大垣 +運営:NPO 岐阜aiネットワーク +``` + +### 利用可能な変数 +- `{{ team_name }}` - チーム名 +- `{{ email }}` - メールアドレス +- `{{ password }}` - パスワード +- `{{ category }}` - 部門 +- `{{ duration }}` - 時間 +- `{{ leader_name }}` - 代表者名 +- `{{ phone_number }}` - 電話番号 +- `{{ member_names }}` - メンバー名 + +## 操作手順 + +### 1. 事前準備 +1. CSVファイルを `CPLIST/input/team_mail.csv` に配置 +2. メールテンプレートファイルを確認・編集 +3. Docker環境が起動していることを確認 + +### 2. ドライラン(テスト実行) +実際にメールを送信する前に、テスト実行を行います: + +```bash +cd /path/to/rogaining_srv +docker compose exec app python manage.py send_team_emails --csv_file='CPLIST/input/team_mail.csv' --dry_run +``` + +**ドライランの確認項目:** +- CSVファイルが正常に読み込まれるか +- テンプレートが正しく適用されるか +- 送信対象の件数が正しいか +- メールの件名・本文のプレビューが正しいか + +### 3. 実際のメール送信 +ドライランで問題がないことを確認後、実際の送信を行います: + +```bash +cd /path/to/rogaining_srv +docker compose exec app python manage.py send_team_emails --csv_file='CPLIST/input/team_mail.csv' +``` + +### 4. 送信結果の確認 +コマンド実行後、以下の情報が表示されます: +- 処理行数 +- メール送信数 +- エラーがあった場合のエラー内容 + +## コマンドオプション + +### 基本コマンド +```bash +python manage.py send_team_emails --csv_file='' +``` + +### オプション一覧 +- `--csv_file`: CSVファイルのパス(必須) +- `--dry_run`: ドライラン(テスト実行)モード +- `--delay`: メール送信間隔(秒)デフォルト: 1秒 + +### 使用例 +```bash +# ドライランモード +python manage.py send_team_emails --csv_file='CPLIST/input/team_mail.csv' --dry_run + +# 実際の送信 +python manage.py send_team_emails --csv_file='CPLIST/input/team_mail.csv' + +# 送信間隔を3秒に設定 +python manage.py send_team_emails --csv_file='CPLIST/input/team_mail.csv' --delay=3 +``` + +## エラー対処法 + +### よくあるエラーと対処法 + +#### 1. CSVファイルが見つからない +``` +CommandError: CSVファイルが見つかりません: CPLIST/input/team_mail.csv +``` +**対処法:** +- ファイルパスを確認 +- ファイル名のスペルミスをチェック +- ファイルが存在することを `ls -la CPLIST/input/` で確認 + +#### 2. テンプレートファイルが見つからない +``` +TemplateDoesNotExist: emails/team_registration_subject.txt +``` +**対処法:** +- テンプレートファイルが正しい場所に配置されているか確認 +- ファイル名が正しいかチェック +- Docker容器を再起動: `docker compose restart app` + +#### 3. SMTP接続エラー +``` +SMTPException: SMTP Auth failure +``` +**対処法:** +- メールサーバー設定を確認 +- 認証情報(ユーザー名・パスワード)を確認 +- ネットワーク接続を確認 + +#### 4. CSV読み込みエラー +**対処法:** +- CSVファイルの文字エンコーディング(UTF-8 BOM)を確認 +- CSVヘッダーが正しいか確認 +- 必須フィールドが欠けていないかチェック + +## セキュリティ注意事項 + +1. **パスワード情報の取り扱い** + - CSVファイルには機密情報が含まれるため、適切なアクセス権限を設定 + - 送信完了後はCSVファイルを安全な場所に移動またはバックアップ + +2. **メール送信記録** + - 送信ログを保存し、送信状況を記録 + - 重複送信を避けるため、送信済みチームを管理 + +3. **レート制限** + - 大量送信時はレート制限を考慮し、適切な間隔を設定 + - SMTPサーバーの制限を確認 + +## トラブルシューティング + +### Docker関連 +```bash +# コンテナの状態確認 +docker compose ps + +# コンテナの再起動 +docker compose restart app + +# ログの確認 +docker compose logs app +``` + +### データベース接続確認 +```bash +# データベース接続テスト +docker compose exec app python manage.py dbshell +``` + +### CSVファイル確認 +```bash +# ファイル存在確認 +ls -la CPLIST/input/ + +# ファイル内容確認 +head -5 CPLIST/input/team_mail.csv +``` + +## 運用Tips + +1. **バッチ送信** + - 大量のメール送信時は、CSVファイルを分割して複数回に分けて送信 + - 送信間隔を適切に設定してサーバー負荷を軽減 + +2. **テスト環境での確認** + - 本番送信前に、テスト用メールアドレスでの動作確認を推奨 + - ドライランを必ず実行 + +3. **バックアップ** + - 送信前にCSVファイルとテンプレートファイルをバックアップ + - 送信ログを保存 + +## 更新履歴 + +- 2025年9月5日: 初版作成 + - 基本的なメール送信機能 + - Django テンプレートシステム統合 + - Outlook SMTP設定対応 + +## 連絡先 + +システムに関する問い合わせ: +- 運営:NPO 岐阜aiネットワーク +- メール:rogaining@gifuai.net diff --git a/config/settings.py b/config/settings.py index 2e91ee2..2b6c3b6 100644 --- a/config/settings.py +++ b/config/settings.py @@ -275,7 +275,7 @@ EMAIL_HOST = 'smtp.outlook.com' EMAIL_PORT = 587 EMAIL_USE_TLS = True EMAIL_HOST_USER = 'rogaining@gifuai.net' -EMAIL_HOST_PASSWORD = 'ctcpy9823"x~' +EMAIL_HOST_PASSWORD = 'gifuainetwork@123' DEFAULT_FROM_EMAIL = 'rogaining@gifuai.net' APP_DOWNLOAD_LINK = 'https://apps.apple.com/jp/app/%E5%B2%90%E9%98%9C%E3%83%8A%E3%83%93/id6444221792' diff --git a/rog/management/commands/send_team_emails.py b/rog/management/commands/send_team_emails.py new file mode 100644 index 0000000..3850679 --- /dev/null +++ b/rog/management/commands/send_team_emails.py @@ -0,0 +1,282 @@ +""" +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メール送信完了!')) diff --git a/templates/emails/team_registration_body.txt b/templates/emails/team_registration_body.txt new file mode 100644 index 0000000..798df0a --- /dev/null +++ b/templates/emails/team_registration_body.txt @@ -0,0 +1,21 @@ +{{ team_name }} 代表者 {{leader_name}} 様 + +岐阜ロゲ in 大垣 へのご参加ありがとうございます。 + +ご連絡が大変遅くなり、申し訳ございません。 + +以下の内容でパスワードをお送りいたしますので、よろしくお願い申し上げます。 + +■ チーム情報 + +チーム名: {{ team_name }} +部門: {{ category }}({{ duration }}時間) + +ユーザー名: {{ email }} +パスワード: {{ password }} + + +-- +岐阜ロゲ in 大垣 +運営:NPO 岐阜aiネットワーク +Email: info@gifuai.net diff --git a/templates/emails/team_registration_subject.txt b/templates/emails/team_registration_subject.txt new file mode 100644 index 0000000..342d321 --- /dev/null +++ b/templates/emails/team_registration_subject.txt @@ -0,0 +1 @@ +【岐阜ロゲ in 大垣】チーム「{{ team_name }}」パスワードのご連絡 \ No newline at end of file diff --git a/test_email.csv b/test_email.csv new file mode 100644 index 0000000..33aad28 --- /dev/null +++ b/test_email.csv @@ -0,0 +1,3 @@ +チーム名,部門,時間,氏名1,メール,電話番号 +テストチーム1,一般,4,テスト 太郎,test@example.com,090-1234-5678 +テストチーム2,ファミリー,3,テスト 花子,test2@example.com,080-9876-5432 diff --git a/メール送信マニュアル.md b/メール送信マニュアル.md new file mode 100644 index 0000000..a1b33e3 --- /dev/null +++ b/メール送信マニュアル.md @@ -0,0 +1,243 @@ +# チームメール送信システム操作マニュアル + +## 概要 +このシステムは、ロゲイニング大会の参加チームに対して、パスワードやイベント情報を含むメールを一括送信するためのDjango管理コマンドです。 + +## 前提条件 + +### 必要な環境 +- Docker Compose環境が稼働していること +- PostgreSQLデータベースが接続されていること +- SMTPサーバー設定が完了していること(Outlook: smtp.outlook.com:587) + +### 必要なファイル +1. **CSVファイル**: チーム情報を含むデータファイル +2. **メールテンプレートファイル**: + - `/templates/emails/team_registration_subject.txt` (件名テンプレート) + - `/templates/emails/team_registration_body.txt` (本文テンプレート) + +## CSVファイル形式 + +### ファイル配置場所 +``` +CPLIST/input/team_mail.csv +``` + +### CSVファイルの形式 +```csv +team_name,email,password,category,duration,leader_name,phone_number,member_names +チーム名,メールアドレス,パスワード,部門,時間,代表者名,電話番号,メンバー名 +``` + +### 例 +```csv +team_name,email,password,category,duration,leader_name,phone_number,member_names +ウエストサイド,hannivalscipio@gmail.com,west123,一般,3,田中太郎,090-1234-5678,田中太郎・佐藤花子 +``` + +## メールテンプレート + +### 件名テンプレート (`/templates/emails/team_registration_subject.txt`) +``` +【岐阜ロゲ in 大垣】チーム「{{ team_name }}」パスワードのご連絡 +``` + +### 本文テンプレート (`/templates/emails/team_registration_body.txt`) +``` +{{ team_name }} 代表者 {{leader_name}} 様 + +岐阜ロゲ in 大垣 へのご参加ありがとうございます。 + +ご連絡が大変遅くなり、申し訳ございません。 + +以下の内容でパスワードをお送りいたしますので、よろしくお願い申し上げます。 + +■ チーム情報 + +チーム名: {{ team_name }} +部門: {{ category }}({{ duration }}時間) + +ユーザー名: {{ email }} +パスワード: {{ password }} + + +-- +岐阜ロゲ in 大垣 +運営:NPO 岐阜aiネットワーク +``` + +### 利用可能な変数 +- `{{ team_name }}` - チーム名 +- `{{ email }}` - メールアドレス +- `{{ password }}` - パスワード +- `{{ category }}` - 部門 +- `{{ duration }}` - 時間 +- `{{ leader_name }}` - 代表者名 +- `{{ phone_number }}` - 電話番号 +- `{{ member_names }}` - メンバー名 + +## 操作手順 + +### 1. 事前準備 +1. CSVファイルを `CPLIST/input/team_mail.csv` に配置 +2. メールテンプレートファイルを確認・編集 +3. Docker環境が起動していることを確認 + +### 2. ドライラン(テスト実行) +実際にメールを送信する前に、テスト実行を行います: + +```bash +cd /path/to/rogaining_srv +docker compose exec app python manage.py send_team_emails --csv_file='CPLIST/input/team_mail.csv' --dry_run +``` + +**ドライランの確認項目:** +- CSVファイルが正常に読み込まれるか +- テンプレートが正しく適用されるか +- 送信対象の件数が正しいか +- メールの件名・本文のプレビューが正しいか + +### 3. 実際のメール送信 +ドライランで問題がないことを確認後、実際の送信を行います: + +```bash +cd /path/to/rogaining_srv +docker compose exec app python manage.py send_team_emails --csv_file='CPLIST/input/team_mail.csv' +``` + +### 4. 送信結果の確認 +コマンド実行後、以下の情報が表示されます: +- 処理行数 +- メール送信数 +- エラーがあった場合のエラー内容 + +## コマンドオプション + +### 基本コマンド +```bash +python manage.py send_team_emails --csv_file='' +``` + +### オプション一覧 +- `--csv_file`: CSVファイルのパス(必須) +- `--dry_run`: ドライラン(テスト実行)モード +- `--delay`: メール送信間隔(秒)デフォルト: 1秒 + +### 使用例 +```bash +# ドライランモード +python manage.py send_team_emails --csv_file='CPLIST/input/team_mail.csv' --dry_run + +# 実際の送信 +python manage.py send_team_emails --csv_file='CPLIST/input/team_mail.csv' + +# 送信間隔を3秒に設定 +python manage.py send_team_emails --csv_file='CPLIST/input/team_mail.csv' --delay=3 +``` + +## エラー対処法 + +### よくあるエラーと対処法 + +#### 1. CSVファイルが見つからない +``` +CommandError: CSVファイルが見つかりません: CPLIST/input/team_mail.csv +``` +**対処法:** +- ファイルパスを確認 +- ファイル名のスペルミスをチェック +- ファイルが存在することを `ls -la CPLIST/input/` で確認 + +#### 2. テンプレートファイルが見つからない +``` +TemplateDoesNotExist: emails/team_registration_subject.txt +``` +**対処法:** +- テンプレートファイルが正しい場所に配置されているか確認 +- ファイル名が正しいかチェック +- Docker容器を再起動: `docker compose restart app` + +#### 3. SMTP接続エラー +``` +SMTPException: SMTP Auth failure +``` +**対処法:** +- メールサーバー設定を確認 +- 認証情報(ユーザー名・パスワード)を確認 +- ネットワーク接続を確認 + +#### 4. CSV読み込みエラー +**対処法:** +- CSVファイルの文字エンコーディング(UTF-8 BOM)を確認 +- CSVヘッダーが正しいか確認 +- 必須フィールドが欠けていないかチェック + +## セキュリティ注意事項 + +1. **パスワード情報の取り扱い** + - CSVファイルには機密情報が含まれるため、適切なアクセス権限を設定 + - 送信完了後はCSVファイルを安全な場所に移動またはバックアップ + +2. **メール送信記録** + - 送信ログを保存し、送信状況を記録 + - 重複送信を避けるため、送信済みチームを管理 + +3. **レート制限** + - 大量送信時はレート制限を考慮し、適切な間隔を設定 + - SMTPサーバーの制限を確認 + +## トラブルシューティング + +### Docker関連 +```bash +# コンテナの状態確認 +docker compose ps + +# コンテナの再起動 +docker compose restart app + +# ログの確認 +docker compose logs app +``` + +### データベース接続確認 +```bash +# データベース接続テスト +docker compose exec app python manage.py dbshell +``` + +### CSVファイル確認 +```bash +# ファイル存在確認 +ls -la CPLIST/input/ + +# ファイル内容確認 +head -5 CPLIST/input/team_mail.csv +``` + +## 運用Tips + +1. **バッチ送信** + - 大量のメール送信時は、CSVファイルを分割して複数回に分けて送信 + - 送信間隔を適切に設定してサーバー負荷を軽減 + +2. **テスト環境での確認** + - 本番送信前に、テスト用メールアドレスでの動作確認を推奨 + - ドライランを必ず実行 + +3. **バックアップ** + - 送信前にCSVファイルとテンプレートファイルをバックアップ + - 送信ログを保存 + +## 更新履歴 + +- 2025年9月5日: 初版作成 + - 基本的なメール送信機能 + - Django テンプレートシステム統合 + - Outlook SMTP設定対応 + +## 連絡先 + +システムに関する問い合わせ: +- 運営:NPO 岐阜aiネットワーク +- メール:rogaining@gifuai.net