wysylka maili

This commit is contained in:
Mateusz Gruszczyński 2025-05-16 23:11:46 +02:00
parent 528f31bd2e
commit c311096e05
4 changed files with 311 additions and 21 deletions

View File

@ -1,8 +1,11 @@
# Database connection
DB_HOST=localhost DB_HOST=localhost
DB_USER=drupal DB_USER=root
DB_PASSWORD=haslo DB_PASSWORD=secret
DB_NAME=drupal6 DB_NAME=drupal
# Path to Drupal installation (used by delete_user.php) SMTP_HOST=smtp.mailserver.com
DRUPAL_PATH=/var/www/drupal SMTP_PORT=587
SMTP_USER=your@email.com
SMTP_PASSWORD=yourpassword
DRUPAL_PATH=/path/to/drupal

84
README.md Normal file
View File

@ -0,0 +1,84 @@
# 🧹 Drupal User Cleanup & Notification Tool
To narzędzie służy do:
- filtrowania nieaktywnych użytkowników w bazie Drupala 6,
- generowania raportów CSV/Excel,
- walidacji adresów e-mail,
- wysyłki maili ostrzegawczych przez SMTP,
- usuwania nieaktywnych kont.
---
## 📦 Wymagania
- Python 3.8+
- Zainstalowane pakiety z `requirements.txt`
- Plik `.env` z konfiguracją SMTP i bazy danych
- Szablon HTML: `mail_template.html`
---
## ⚙️ Przykładowy `.env`
```
DB_HOST=localhost
DB_USER=drupal_user
DB_PASSWORD=secret
DB_NAME=drupal
SMTP_HOST=smtp.yourdomain.com
SMTP_PORT=587
SMTP_USER=mailer@yourdomain.com
SMTP_PASSWORD=supersecret
DRUPAL_PATH=/var/www/html
```
---
## 🚀 Przykłady użycia
### 🔎 Tryb podglądu:
```bash
python3 app.py --days-inactive 730 --dry-run
```
### 🧹 Usuwanie użytkowników:
```bash
python3 app.py --days-inactive 730 --delete --drupal-path /var/www/html
```
### 📧 Testowy e-mail:
```bash
python3 app.py --send-test test@example.com
```
### 📤 Wysyłka maili do użytkowników:
```bash
python3 app.py --send-mails --mails-per-pack 100 --time-per-pack 60
```
### 📊 Raport domen:
```bash
python3 app.py --report-domains
```
---
## 📁 Wyniki
- `user_cleanup_results_YYYY-MM-DD_HHMM.csv` lista użytkowników.
- `user_cleanup_results_YYYY-MM-DD_HHMM.xlsx` opcjonalnie Excel.
- `user_cleanup.log` log operacji.
---
## 🛑 Uwaga
Narzędzie **nie usuwa** użytkowników z punktami lub treściami. Filtrowanie jest ostrożne i bezpieczne.
---
## ✉️ Kontakt
Jeśli masz pytania napisz do administratora systemu lub autora skryptu.

159
app.py
View File

@ -17,6 +17,11 @@ from tabulate import tabulate
import xlsxwriter import xlsxwriter
from collections import defaultdict from collections import defaultdict
import subprocess import subprocess
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import time
# Redis - baza 5 # Redis - baza 5
redis_client = redis.Redis(host='localhost', port=6379, db=5, decode_responses=True) redis_client = redis.Redis(host='localhost', port=6379, db=5, decode_responses=True)
@ -149,9 +154,89 @@ def confirm_delete():
def days_to_years(days): def days_to_years(days):
return round(days / 365, 1) return round(days / 365, 1)
def get_smtp_config():
required_keys = ["SMTP_HOST", "SMTP_PORT", "SMTP_USER", "SMTP_PASSWORD"]
config = {key: os.getenv(key) for key in required_keys}
missing = [k for k, v in config.items() if not v]
if missing:
raise ValueError(f"❌ Brakuje wymaganych zmiennych SMTP w pliku .env: {', '.join(missing)}")
return {
"host": config["SMTP_HOST"],
"port": config["SMTP_PORT"],
"user": config["SMTP_USER"],
"password": config["SMTP_PASSWORD"]
}
def send_email_batch(users, smtp_config, mails_per_pack=100, time_per_pack=60, dry_run=False):
import os
template_path = "mail_template.html"
if not os.path.exists(template_path):
print(f"❌ Brak pliku szablonu: {template_path}")
return
try:
with open(template_path, "r", encoding="utf-8") as f:
template = f.read()
except Exception as e:
print(f"❌ Błąd podczas odczytu szablonu HTML: {e}")
return
try:
smtp = smtplib.SMTP(smtp_config["host"], int(smtp_config["port"]))
smtp.starttls()
smtp.login(smtp_config["user"], smtp_config["password"])
except Exception as e:
print(f"❌ Błąd połączenia z serwerem SMTP: {e}")
return
print(f"📨 Rozpoczynam wysyłkę {len(users)} maili...")
for i in tqdm(range(0, len(users), mails_per_pack), desc="Wysyłanie emaili"):
batch = users[i:i + mails_per_pack]
for user in batch:
if dry_run:
print(f"🔔 [TRYB TESTOWY] Mail do: {user['mail']}")
continue
try:
msg = MIMEMultipart()
msg['From'] = smtp_config["user"]
msg['To'] = user['mail']
msg['Subject'] = "Twoje konto w unitraklub.pl"
body = template.replace("@user", user['name']).replace(
"@rejestracja", datetime.datetime.fromtimestamp(user['created']).strftime('%Y-%m-%d'))
msg.attach(MIMEText(body, 'html'))
smtp.send_message(msg)
time.sleep(0.5) # opcjonalne mikroopóźnienie między mailami
except Exception as e:
print(f"⚠️ Błąd wysyłki do {user['mail']}: {e}")
if i + mails_per_pack < len(users):
print(f"⏸ Przerwa {time_per_pack} sekund między paczkami...")
time.sleep(time_per_pack)
smtp.quit()
print("✅ Wysyłka zakończona.")
def validate_smtp_config(config):
required = ['host', 'port', 'user', 'password']
for key in required:
if not config.get(key):
raise ValueError(f"❌ Brakuje wartości SMTP dla: {key}")
def main(): def main():
signal.signal(signal.SIGINT, lambda s, f: sys.exit("\n🛑 Przerwano przez użytkownika.")) signal.signal(signal.SIGINT, lambda s, f: sys.exit("\n🛑 Przerwano przez użytkownika."))
load_dotenv() load_dotenv()
try:
smtp_config = get_smtp_config()
except ValueError as e:
print(str(e))
sys.exit(1)
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Drupal 6 user cleanup tool", description="Drupal 6 user cleanup tool",
@ -176,32 +261,70 @@ def main():
formatter_class=argparse.RawDescriptionHelpFormatter formatter_class=argparse.RawDescriptionHelpFormatter
) )
parser.add_argument('--host') parser.add_argument('--host', help='Adres hosta bazy danych (można ustawić w .env jako DB_HOST)')
parser.add_argument('--user') parser.add_argument('--user', help='Użytkownik bazy danych (lub DB_USER z .env)')
parser.add_argument('--password') parser.add_argument('--password', help='Hasło do bazy danych (lub DB_PASSWORD z .env)')
parser.add_argument('--database') parser.add_argument('--database', help='Nazwa bazy danych (lub DB_NAME z .env)')
parser.add_argument('--days-inactive', type=int)
parser.add_argument('--days-inactive', type=int,
help='Minimalna liczba dni nieaktywności, po której użytkownik uznawany jest za nieaktywny')
parser.add_argument('--dry-run', action='store_true', default=None, parser.add_argument('--dry-run', action='store_true', default=None,
help='Tryb podglądu (domyślny jeśli nie podano --delete)') help='Tryb podglądu: nie wykonuje żadnych zmian, tylko raportuje')
parser.add_argument('--delete', action='store_true')
parser.add_argument('--validate', action='store_true') parser.add_argument('--delete', action='store_true',
parser.add_argument('--flush-cache', action='store_true') help='Usuń użytkowników, którzy spełniają kryteria filtrowania')
parser.add_argument('--export-excel', action='store_true')
parser.add_argument('--report-domains', action='store_true') parser.add_argument('--validate', action='store_true',
help='Tylko sprawdź poprawność adresów e-mail (bez usuwania)')
parser.add_argument('--flush-cache', action='store_true',
help='Wyczyść cache rekordów MX w Redisie')
parser.add_argument('--export-excel', action='store_true',
help='Zapisz wynik filtrowania użytkowników także do pliku Excel (.xlsx)')
parser.add_argument('--report-domains', action='store_true',
help='Wygeneruj raport ilości użytkowników według domen e-mail')
parser.add_argument('--veteran-year', type=int, default=2012, parser.add_argument('--veteran-year', type=int, default=2012,
help='Minimalny rok rejestracji konta do uznania za stare (domyślnie: 2012)') help='Rok, przed którym konto uznawane jest za stare/weterana (domyślnie: 2012)')
parser.add_argument('--recent-login-days', type=int, default=1095, parser.add_argument('--recent-login-days', type=int, default=1095,
help='Ile dni wstecz ostatnie logowanie czyni konto aktywnym (domyślnie: 1095)') help='Ile dni wstecz uznaje się zalogowanego weterana za aktywnego (domyślnie: 1095)')
parser.add_argument('--show-table', action='store_true', parser.add_argument('--show-table', action='store_true',
help='Wyświetl tabelę z listą użytkowników do usunięcia') help='Wyświetl tabelę użytkowników spełniających kryteria do usunięcia')
parser.add_argument('--drupal-path', parser.add_argument('--drupal-path',
help="Ścieżka do katalogu Drupala (można też podać przez .env jako DRUPAL_PATH)") help='Ścieżka do katalogu Drupala (można ustawić przez .env jako DRUPAL_PATH)')
parser.add_argument('--send-test', metavar="EMAIL",
help='Wyślij testowego maila z szablonu na wskazany adres e-mail')
parser.add_argument('--send-mails', action='store_true',
help='Wyślij powiadomienia e-mail do użytkowników z listy kandydatów')
parser.add_argument('--mails-per-pack', type=int, default=100,
help='Ile e-maili wysyłać w jednej paczce (domyślnie: 100)')
parser.add_argument('--time-per-pack', type=int, default=60,
help='Ile sekund czekać między paczkami maili (domyślnie: 60 sek.)')
args = parser.parse_args() args = parser.parse_args()
if args.send_test:
test_user = {
'name': 'Testowy Użytkownik',
'mail': args.send_test,
'created': int(datetime.datetime.now().timestamp()) - (86400 * 365 * 2) # 2 lata temu
}
print(f"📬 Wysyłka testowego maila na: {test_user['mail']}")
send_email_batch([test_user], smtp_config, mails_per_pack=1, time_per_pack=0, dry_run=False)
return
if not args.drupal_path: if not args.drupal_path:
args.drupal_path = os.getenv("DRUPAL_PATH") args.drupal_path = os.getenv("DRUPAL_PATH")
@ -280,9 +403,15 @@ def main():
], headers=["UID", "Login", "E-mail", "Ostatnie log.", "Rejestracja", "Punkty", "Nieaktywny", "E-mail OK", "Tymczasowy"], tablefmt="fancy_grid")) ], headers=["UID", "Login", "E-mail", "Ostatnie log.", "Rejestracja", "Punkty", "Nieaktywny", "E-mail OK", "Tymczasowy"], tablefmt="fancy_grid"))
export_to_csv(final_candidates) export_to_csv(final_candidates)
if args.export_excel: if args.export_excel:
export_to_excel(final_candidates) export_to_excel(final_candidates)
if args.send_mails:
send_email_batch(final_candidates, smtp_config, args.mails_per_pack, args.time_per_pack)
print("\n📋 Parametry filtrowania:") print("\n📋 Parametry filtrowania:")
if args.days_inactive: if args.days_inactive:
print(f"- Nieaktywni: brak logowania przez ≥ {args.days_inactive} dni (~{days_to_years(args.days_inactive)} lat)") print(f"- Nieaktywni: brak logowania przez ≥ {args.days_inactive} dni (~{days_to_years(args.days_inactive)} lat)")

74
mail_template.html Normal file
View File

@ -0,0 +1,74 @@
<!-- mail_template.html -->
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Powiadomienie o nieaktywności</title>
<style>
body {
font-family: 'Segoe UI', Roboto, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 0;
color: #333333;
}
.container {
max-width: 600px;
margin: 40px auto;
background-color: #ffffff;
padding: 30px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
h1 {
font-size: 22px;
color: #222222;
}
p {
font-size: 16px;
line-height: 1.6;
}
.footer {
margin-top: 30px;
font-size: 14px;
color: #888888;
text-align: center;
}
.highlight {
background-color: #ffe9e9;
padding: 12px;
border-left: 4px solid #e74c3c;
margin: 20px 0;
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
<h1>Witaj @user,</h1>
<p>
Kontaktujemy się z Tobą w sprawie Twojego konta w serwisie <strong>unitraklub.pl</strong>.
</p>
<p>
Zarejestrowałeś/aś konto dnia <strong>@rejestracja</strong>. Niestety, od momentu rejestracji nie wykazało ono żadnej aktywności.
</p>
<div class="highlight">
Jeśli nie zalogujesz się w najbliższym czasie, konto zostanie zablokowane i następnie usunięte.
</div>
<p>
Jeśli nadal chcesz korzystać z serwisu, wystarczy się zalogować to wszystko!
</p>
<p>Pozdrawiamy,<br>Zespół administracyjny unitraklub.pl</p>
<div class="footer">
Ten e-mail został wygenerowany automatycznie. Prosimy na niego nie odpowiadać.
</div>
</div>
</body>
</html>