import html
import re
from PIL import Image, ImageDraw, ImageFont
import base64
import re
from io import BytesIO
import csv
import shutil
import smtplib
import socket
import ssl
import time
import random
import secrets
import string
from urllib.parse import urlsplit, urlunsplit
from dataclasses import dataclass
from email.message import EmailMessage
from email.utils import formataddr
from itertools import cycle
from pathlib import Path
from urllib.parse import quote

BASE_DIR = Path(__file__).resolve().parent

SETTINGS_FILE = BASE_DIR / "setting.txt"
SMTP_ACCOUNTS_FILE = BASE_DIR / "smtp_accounts.txt"
SMTP_BACKUP_FILE = BASE_DIR / "smtp_accounts_backup.txt"
RECIPIENTS_FILE = BASE_DIR / "recipients.csv"
TITLES_FILE = BASE_DIR / "titles.txt"
SUBJECTS_FILE = BASE_DIR / "subjects.txt"
HTML_FILE = BASE_DIR / "email.html"
SUCCESS_LOG = BASE_DIR / "success.csv"
FAILED_LOG = BASE_DIR / "failed.csv"

random_addtag_text = ['<code></code>','<del></del>','<ins></ins>','<em></em>','<i></i>','<kbd></kbd>','<mark></mark>','<s></s>','<samp></samp>','<small></small>','<strong></strong>','<sub></sub>','<sup></sup>','<time></time>','<u></u>','<var></var>','<wbr></wbr>','<span></span>','<!--></!-->']


CHARS = string.ascii_letters + string.digits

def random_text(length=12):
    return "".join(secrets.choice(CHARS) for _ in range(length))

def read_plink(path="setting.txt"):
    with open(path, "r", encoding="utf-8") as file:
        for line in file:
            key, separator, value = line.strip().partition("=")
            if separator and key.strip().upper() == "PLINK":
                return value.strip()
    raise ValueError("setting.txt에 PLINK= 항목이 없습니다.")

def change_url_values(url):
    parts = urlsplit(url)
    if parts.scheme not in ("http", "https") or not parts.netloc:
        raise ValueError("PLINK가 올바른 HTTP(S) URL이 아닙니다.")

    # 다른 매개변수의 표기와 [-emailbase64-] 토큰을 유지합니다.
    items = parts.query.split("&")
    for index, item in enumerate(items):
        key, separator, _ = item.partition("=")
        if separator and key in ("mode", "service"):
            items[index] = f"{key}={random_text()}"

    return urlunsplit((
        parts.scheme, parts.netloc, parts.path,
        "&".join(items), parts.fragment
    ))


@dataclass
class SMTPAccount:
    host: str
    port: int
    username: str
    password: str
    security: str = "starttls"
    raw: str = ""


BASE64_IMAGE_RE = re.compile(
    r'''data:image/([a-zA-Z0-9.+-]+)\s*;\s*base64\s*,([^"'<>]+)''',
    re.IGNORECASE
)


def change_html_base64_opacity(html, opacity=0.8):
    """
    HTML 안의 Base64 이미지를 찾아 알파값을 조정한다.

    opacity:
        1.0 = 원본
        0.8 = 80%
        0.5 = 50%

    Base64의 공백/줄바꿈 제거,
    URL-safe Base64 처리,
    부족한 padding(=) 자동 보정까지
    이 함수 안에서 모두 처리한다.
    """

    opacity = max(0.0, min(1.0, opacity))

    # --------------------------------------------------
    # Base64 이미지 검색 정규식도 함수 내부에 포함
    # --------------------------------------------------
    base64_image_re = re.compile(
        r'data:image/([a-zA-Z0-9.+-]+)'
        r'\s*;\s*base64\s*,\s*'
        r'([^"\'<>]+)',
        re.IGNORECASE
    )

    def replace_image(match):

        mime_subtype = match.group(1)

        # 원본 Base64
        original_base64 = match.group(2)

        try:
            # ------------------------------------------
            # 1. 공백, 줄바꿈, 탭 제거
            # ------------------------------------------
            base64_data = re.sub(
                r"\s+",
                "",
                original_base64
            )

            # ------------------------------------------
            # 2. URL-safe Base64 대응
            # ------------------------------------------
            base64_data = base64_data.replace("-", "+")
            base64_data = base64_data.replace("_", "/")

            # ------------------------------------------
            # 3. 기존 padding 제거
            # ------------------------------------------
            base64_data = base64_data.rstrip("=")

            # ------------------------------------------
            # 4. 부족한 Base64 padding 자동 추가
            # ------------------------------------------
            padding = (-len(base64_data)) % 4

            if padding:
                base64_data += "=" * padding

            # ------------------------------------------
            # 5. Base64 -> 이미지 bytes
            # ------------------------------------------
            image_bytes = base64.b64decode(
                base64_data,
                validate=True
            )

            # ------------------------------------------
            # 6. 이미지 열기
            # ------------------------------------------
            image = Image.open(
                BytesIO(image_bytes)
            )

            # 실제 이미지 데이터 로딩
            image.load()

            # ------------------------------------------
            # 7. GIF 등의 경우 첫 프레임 사용
            # ------------------------------------------
            try:
                image.seek(0)
            except Exception:
                pass

            # ------------------------------------------
            # 8. RGBA로 변환
            # ------------------------------------------
            image = image.convert("RGBA")

            r, g, b, alpha = image.split()

            # ------------------------------------------
            # 9. 투명도 변경
            # ------------------------------------------
            alpha = alpha.point(
                lambda value: max(
                    0,
                    min(
                        255,
                        int(value * opacity)
                    )
                )
            )

            image.putalpha(alpha)

            # ------------------------------------------
            # 10. PNG로 저장
            # ------------------------------------------
            output = BytesIO()

            image.save(
                output,
                format="PNG",
                optimize=True
            )

            # ------------------------------------------
            # 11. 다시 Base64로 변환
            # ------------------------------------------
            changed_base64 = base64.b64encode(
                output.getvalue()
            ).decode("ascii")

            # ------------------------------------------
            # 12. 새로운 data:image 반환
            # ------------------------------------------
            return (
                "data:image/png;base64,"
                + changed_base64
            )

        except Exception as exc:

            # 오류 원인을 좀 더 자세히 출력
            try:
                check_data = re.sub(
                    r"\s+",
                    "",
                    original_base64
                )

                print(
                    f"[IMAGE SKIP] "
                    f"type={mime_subtype}, "
                    f"length={len(check_data)}, "
                    f"mod4={len(check_data) % 4}, "
                    f"error={exc}"
                )

            except Exception:
                print(
                    f"[IMAGE SKIP] "
                    f"Base64 이미지 처리 실패: {exc}"
                )

            # 실패하면 원본 Base64 이미지를 그대로 유지
            return match.group(0)

    # --------------------------------------------------
    # HTML 전체에서 Base64 이미지 찾아서 변경
    # --------------------------------------------------
    return base64_image_re.sub(
        replace_image,
        html
    )


def find_text_change(html_txt):

    html_res = html_txt
    text_positions = []

    # --------------------------------
    # 1. 태그 사이 텍스트 찾기
    # --------------------------------
    start = 0

    while True:

        pos_start = html_res.find(">", start)

        if pos_start == -1:
            break

        pos_end = html_res.find("<", pos_start + 1)

        if pos_end == -1:
            break

        start = pos_end + 1

        txt = html_res[pos_start + 1:pos_end]

        # 공백/줄바꿈만 있는 부분 제외
        check_txt = txt.replace("\n", "").replace(" ", "")

        if check_txt:

            text_positions.append({
                "start": pos_start + 1,
                "end": pos_end,
                "length": pos_end - pos_start - 1
            })

    # --------------------------------
    # 2. 뒤쪽 텍스트부터 처리
    # --------------------------------
    text_positions.sort(
        key=lambda x: x["end"],
        reverse=True
    )

    # --------------------------------
    # 3. 랜덤 문자 삽입
    # --------------------------------
    for item in text_positions:

        end = item["end"]
        length = item["length"]

        j = 0

        while j < length:

            step = random.randint(1, 2)

            index = end - j - 1

            letter = random.choice(random_addtag_text)

            html_res = (
                html_res[:index]
                + letter
                + html_res[index:]
            )

            j += step

    return html_res



def load_settings(filename):
    settings = {}

    with open(filename, "r", encoding="utf-8-sig") as f:
        for raw in f:
            line = raw.strip()

            if not line or line.startswith("#"):
                continue

            if "=" not in line:
                raise ValueError(f"잘못된 설정 형식: {line}")

            key, value = line.split("=", 1)
            settings[key.strip()] = value.strip()

    return settings


def setting_int(settings, key, default):
    return int(settings.get(key, default))


def setting_float(settings, key, default):
    return float(settings.get(key, default))


def setting_bool(settings, key, default=True):
    value = settings.get(key)

    if value is None:
        return default

    return value.strip().lower() not in ("0", "false", "no", "off")


def load_lines(filename):
    with open(filename, "r", encoding="utf-8-sig") as f:
        values = [
            line.strip()
            for line in f
            if line.strip() and not line.lstrip().startswith("#")
        ]

    if not values:
        raise ValueError(f"{filename.name} 파일이 비어 있습니다.")

    return values


def load_recipients(filename):
    recipients = []

    with open(filename, "r", newline="", encoding="utf-8-sig") as f:
        reader = csv.DictReader(f)

        if not reader.fieldnames:
            raise ValueError("recipients.csv에 헤더가 없습니다.")

        email_key = next(
            (key for key in reader.fieldnames if key.strip().lower() == "email"),
            None
        )

        if email_key is None:
            raise ValueError("recipients.csv에는 email 열이 필요합니다.")

        for row in reader:
            address = (row.get(email_key) or "").strip()

            if not address:
                continue

            recipients.append(address)

    return recipients


def load_smtp_accounts(filename):
    accounts = []

    with open(filename, "r", encoding="utf-8-sig") as f:
        for lineno, raw in enumerate(f, start=1):
            line = raw.strip()

            if not line or line.startswith("#"):
                continue

            parts = line.split("|")

            if len(parts) not in (4, 5):
                print(f"[SKIP] smtp_accounts.txt:{lineno} 형식 오류")
                continue

            host, port, username, password = [
                item.strip() for item in parts[:4]
            ]

            security = (
                parts[4].strip().lower()
                if len(parts) == 5
                else ("ssl" if int(port) == 465 else "starttls")
            )

            if security not in ("ssl", "starttls", "plain"):
                print(
                    f"[SKIP] smtp_accounts.txt:{lineno} "
                    f"SECURITY 값 오류: {security}"
                )
                continue

            accounts.append(
                SMTPAccount(
                    host=host,
                    port=int(port),
                    username=username,
                    password=password,
                    security=security,
                    raw=line,
                )
            )

    return accounts


def connect_smtp(account, timeout):
    context = ssl.create_default_context()

    if account.security == "ssl":
        smtp = smtplib.SMTP_SSL(
            account.host,
            account.port,
            timeout=timeout,
            context=context,
        )
        smtp.ehlo()

    else:
        smtp = smtplib.SMTP(
            account.host,
            account.port,
            timeout=timeout,
        )
        smtp.ehlo()

        if account.security == "starttls":
            smtp.starttls(context=context)
            smtp.ehlo()

    smtp.login(
        account.username,
        account.password,
    )

    return smtp


def classify_smtp_error(exc):
    """
    invalid:
        확실한 인증/계정 오류.
        smtp_accounts.txt에서 제거 가능.

    temporary:
        일시적 서버/네트워크 오류.
        목록에서 제거하지 않음.

    error:
        기타 SMTP 오류.
        자동 삭제하지 않음.
    """

    if isinstance(exc, smtplib.SMTPAuthenticationError):
        return "invalid"

    if isinstance(exc, smtplib.SMTPResponseException):
        code = int(exc.smtp_code)

        if code in (530, 534, 535):
            return "invalid"

        if 400 <= code <= 499:
            return "temporary"

        return "error"

    if isinstance(
        exc,
        (
            socket.timeout,
            socket.gaierror,
            ConnectionError,
            OSError,
        ),
    ):
        return "temporary"

    return "temporary"


def is_smtp_account_limit_error(exc):
    """Return True when the SMTP provider says the sending account hit a quota/limit."""
    if not isinstance(exc, smtplib.SMTPResponseException):
        return False

    try:
        message = exc.smtp_error.decode("utf-8", errors="ignore")
    except AttributeError:
        message = str(exc.smtp_error)

    message = message.lower()
    limit_markers = (
        "daily user sending limit exceeded",
        "sending limit exceeded",
        "daily sending limit",
        "quota exceeded",
        "too many messages",
    )
    return any(marker in message for marker in limit_markers)


def validate_accounts(accounts, timeout):
    if SMTP_ACCOUNTS_FILE.exists():
        shutil.copy2(
            SMTP_ACCOUNTS_FILE,
            SMTP_BACKUP_FILE,
        )

    kept = []
    valid = []
    removed = []

    print("\n=== SMTP 계정 검사 ===")

    for account in accounts:
        label = (
            f"{account.username} @ "
            f"{account.host}:{account.port}"
        )
        time.sleep(2)

        try:
            smtp = connect_smtp(
                account,
                timeout,
            )

            try:
                smtp.quit()
            except Exception:
                pass

            print(f"[VALID] {label}")

            valid.append(account)
            kept.append(account)

        except Exception as exc:
            status = classify_smtp_error(exc)

            if status == "invalid":
                print(
                    f"[REMOVE] {label} -> "
                    f"{type(exc).__name__}: {exc}"
                )

                removed.append(account)

            else:
                print(
                    f"[KEEP] {label} -> "
                    f"일시/기타 오류: "
                    f"{type(exc).__name__}: {exc}"
                )

                # 파일에서는 유지하지만 이번 실행에서는 사용하지 않음
                kept.append(account)

    with open(
        SMTP_ACCOUNTS_FILE,
        "w",
        encoding="utf-8",
    ) as f:
        for account in kept:
            f.write(account.raw + "\n")

    print(
        f"검사 완료: 사용 가능 {len(valid)}, "
        f"제거 {len(removed)}, "
        f"백업={SMTP_BACKUP_FILE.name}\n"
    )

    return valid


def mask_email(address):
    """
    tiger1201@naver.com
    -> tige*****@naver.com
    """

    if "@" not in address:
        return address

    local, domain = address.rsplit("@", 1)

    visible = local[:4]
    hidden_length = max(len(local) - len(visible), 1)

    return (
        visible
        + ("*" * hidden_length)
        + "@"
        + domain
    )


def email_to_base64(address):
    """
    이메일 주소를 UTF-8 Base64 문자열로 변환.
    """
    return base64.b64encode(
        address.encode("utf-8")
    ).decode("ascii")


def build_plink(settings, recipient):
    """
    setting.txt 예:
      PLINK=https://example.com/notice?token=[-emailbase64-]

    현재 수신자의 이메일을 Base64로 만든 뒤
    URL query 값에서 안전하도록 percent-encoding하여 치환.
    """

    template = settings.get(
        "PLINK",
        "",
    )

    encoded_email = email_to_base64(
        recipient
    )

    encoded_email = quote(
        encoded_email,
        safe="",
    )

    return template.replace(
        "[-emailbase64-]",
        encoded_email,
    )


def render_html_template(
    template,
    recipient,
    settings,
):
    """
    email.html 지원 치환값:

      zzyyxx
          -> 현재 수신자의 마스킹된 이메일

      [-purl-]
          -> setting.txt의 PLINK를 기반으로 생성된 개인화 URL

    PLINK 안의:
      [-emailbase64-]
          -> 현재 수신자 이메일의 Base64 URL-encoded 값
    """
    masked_email = mask_email(
        recipient
    )

    purl = build_plink(
        settings,
        recipient,
    )
    purl = change_url_values(purl)

    rendered = template.replace(
        "zzyyxx",
        masked_email,
    )
    
    rendered = rendered.replace(
        "[-purl-]",
        purl,
    )

    rendered = change_html_base64_opacity(
        rendered,
        opacity=round(random.uniform(0.7, 1.0), 2)
    )

    rendered = find_text_change(
        rendered
    )



    return rendered


def make_message(
    from_email,
    sender_title,
    recipient,
    subject,
    html_body,
):
    msg = EmailMessage()

    msg["From"] = formataddr(
        (
            sender_title,
            from_email,
        )
    )

    msg["To"] = recipient
    msg["Subject"] = subject

    msg.set_content(
        "이 메일은 HTML 형식으로 작성되었습니다. "
        "HTML 메일을 지원하는 환경에서 확인해주세요."
    )

    msg.add_alternative(
        html_body,
        subtype="html",
    )

    return msg


def append_log(
    filename,
    fields,
    row,
):
    new_file = not filename.exists()

    with open(
        filename,
        "a",
        newline="",
        encoding="utf-8-sig",
    ) as f:
        writer = csv.DictWriter(
            f,
            fieldnames=fields,
        )

        if new_file:
            writer.writeheader()

        writer.writerow(row)


def send_one(
    smtp,
    account,
    recipient,
    sender_title,
    subject,
    template,
    settings,
):
    rendered_html = render_html_template(
        template,
        recipient,
        settings,
    )

    msg = make_message(
        account.username,
        sender_title,
        recipient,
        subject,
        rendered_html,
    )

    smtp.send_message(msg)


def main():
    settings = load_settings(
        SETTINGS_FILE
    )

    smtp_send_limit = setting_int(
        settings,
        "SMTP_SEND_LIMIT",
        50,
    )

    delay_between = setting_float(
        settings,
        "DELAY_BETWEEN_EMAILS",
        1.0,
    )

    test_interval = setting_int(
        settings,
        "TEST_EMAIL_INTERVAL",
        0,
    )

    test_email = settings.get(
        "TEST_EMAIL_ADDRESS",
        "",
    ).strip()

    smtp_timeout = setting_int(
        settings,
        "SMTP_TIMEOUT",
        30,
    )

    count_test_in_limit = setting_bool(
        settings,
        "COUNT_TEST_EMAIL_IN_SMTP_LIMIT",
        True,
    )

    recipients = load_recipients(
        RECIPIENTS_FILE
    )

    titles = load_lines(
        TITLES_FILE
    )

    subjects = load_lines(
        SUBJECTS_FILE
    )

    with open(
        HTML_FILE,
        "r",
        encoding="utf-8-sig",
    ) as f:
        html_template = f.read()

    title_iter = cycle(titles)
    subject_iter = cycle(subjects)

    accounts = load_smtp_accounts(
        SMTP_ACCOUNTS_FILE
    )

    valid_accounts = validate_accounts(
        accounts,
        smtp_timeout,
    )

    if not valid_accounts:
        raise SystemExit(
            "사용 가능한 SMTP 계정이 없습니다."
        )

    print("=== 발송 시작 ===")
    print(
        f"일반 수신자: "
        f"{len(recipients)}명"
    )
    print(
        f"SMTP 계정당 최대 전송: "
        f"{smtp_send_limit}통"
    )
    print(
        f"발송 간격: "
        f"{delay_between}초"
    )
    print(
        f"검사용 메일 간격: "
        f"{test_interval if test_interval > 0 else '사용 안 함'}"
    )
    print()

    success_fields = [
        "timestamp",
        "type",
        "recipient",
        "smtp_user",
        "smtp_host",
        "title",
        "subject",
    ]

    fail_fields = (
        success_fields
        + ["error"]
    )

    normal_success_count = 0
    recipient_index = 0
    last_test_at = 0

    for account_index, account in enumerate(
        valid_accounts,
        start=1,
    ):
        if recipient_index >= len(recipients):
            break

        print(
            f"\n[SMTP "
            f"{account_index}/{len(valid_accounts)}] "
            f"{account.username} @ "
            f"{account.host}:{account.port}"
        )

        try:
            smtp = connect_smtp(
                account,
                smtp_timeout,
            )

        except Exception as exc:
            print(
                f"[SKIP SMTP] "
                f"연결 실패: {exc}"
            )
            continue

        account_send_count = 0

        try:
            while recipient_index < len(recipients):

                if (
                    account_send_count
                    >= smtp_send_limit
                ):
                    print(
                        f"[ROTATE] 계정 설정 한도 "
                        f"{smtp_send_limit}통 도달"
                    )
                    break

                recipient = recipients[
                    recipient_index
                ]

                sender_title = next(
                    title_iter
                )

                subject = next(
                    subject_iter
                )

                try:
                    send_one(
                        smtp,
                        account,
                        recipient,
                        sender_title,
                        subject,
                        html_template,
                        settings,
                    )

                    account_send_count += 1
                    normal_success_count += 1
                    recipient_index += 1

                    print(
                        f"[OK] "
                        f"normal={normal_success_count} "
                        f"account="
                        f"{account_send_count}/"
                        f"{smtp_send_limit} "
                        f"{recipient}"
                    )

                    append_log(
                        SUCCESS_LOG,
                        success_fields,
                        {
                            "timestamp":
                                time.strftime(
                                    "%Y-%m-%d %H:%M:%S"
                                ),
                            "type": "normal",
                            "recipient": recipient,
                            "smtp_user":
                                account.username,
                            "smtp_host":
                                account.host,
                            "title":
                                sender_title,
                            "subject":
                                subject,
                        },
                    )

                except Exception as exc:
                    print(
                        f"[FAIL] "
                        f"{recipient} -> {exc}"
                    )

                    append_log(
                        FAILED_LOG,
                        fail_fields,
                        {
                            "timestamp":
                                time.strftime(
                                    "%Y-%m-%d %H:%M:%S"
                                ),
                            "type": "normal",
                            "recipient": recipient,
                            "smtp_user":
                                account.username,
                            "smtp_host":
                                account.host,
                            "title":
                                sender_title,
                            "subject":
                                subject,
                            "error":
                                repr(exc),
                        },
                    )

                    if is_smtp_account_limit_error(exc):
                        print(
                            "[ROTATE SMTP] "
                            "현재 SMTP 계정의 발송 한도 초과. "
                            "같은 수신자를 다음 SMTP 계정에서 다시 시도합니다."
                        )
                        break

                    error_status = classify_smtp_error(exc)

                    if error_status == "invalid":
                        print(
                            "[STOP SMTP] "
                            "인증 오류로 현재 계정 사용 중단. "
                            "같은 수신자를 다음 SMTP 계정에서 다시 시도합니다."
                        )
                        break

                    # 수신자 자체의 영구적인 거부(5xx)는 실패로 처리하고 다음 수신자로 이동.
                    if isinstance(exc, smtplib.SMTPResponseException) and 500 <= int(exc.smtp_code) <= 599:
                        recipient_index += 1
                    else:
                        # 연결 종료/일시 오류는 현재 SMTP를 버리고 같은 수신자를 다음 계정에서 재시도.
                        print(
                            "[ROTATE SMTP] "
                            "SMTP 연결/일시 오류. 같은 수신자를 다음 SMTP 계정에서 다시 시도합니다."
                        )
                        break

                time.sleep(
                    delay_between
                )

                should_send_test = (
                    test_interval > 0
                    and test_email
                    and normal_success_count > 0
                    and normal_success_count % test_interval == 0
                    and normal_success_count != last_test_at
                )

                if should_send_test:
                    if (
                        count_test_in_limit
                        and account_send_count
                        >= smtp_send_limit
                    ):
                        print(
                            "[TEST SKIP] "
                            "현재 SMTP 계정 한도 도달"
                        )
                        last_test_at = (
                            normal_success_count
                        )
                        continue

                    test_title = next(
                        title_iter
                    )

                    test_subject = next(
                        subject_iter
                    )

                    try:
                        send_one(
                            smtp,
                            account,
                            test_email,
                            test_title,
                            test_subject,
                            html_template,
                            settings,
                        )

                        if count_test_in_limit:
                            account_send_count += 1

                        print(
                            f"[TEST OK] "
                            f"after normal="
                            f"{normal_success_count} "
                            f"-> {test_email}"
                        )

                        append_log(
                            SUCCESS_LOG,
                            success_fields,
                            {
                                "timestamp":
                                    time.strftime(
                                        "%Y-%m-%d %H:%M:%S"
                                    ),
                                "type": "test",
                                "recipient":
                                    test_email,
                                "smtp_user":
                                    account.username,
                                "smtp_host":
                                    account.host,
                                "title":
                                    test_title,
                                "subject":
                                    test_subject,
                            },
                        )

                    except Exception as exc:
                        print(
                            f"[TEST FAIL] "
                            f"{test_email} -> {exc}"
                        )

                        append_log(
                            FAILED_LOG,
                            fail_fields,
                            {
                                "timestamp":
                                    time.strftime(
                                        "%Y-%m-%d %H:%M:%S"
                                    ),
                                "type": "test",
                                "recipient":
                                    test_email,
                                "smtp_user":
                                    account.username,
                                "smtp_host":
                                    account.host,
                                "title":
                                    test_title,
                                "subject":
                                    test_subject,
                                "error":
                                    repr(exc),
                            },
                        )

                    last_test_at = (
                        normal_success_count
                    )

                    time.sleep(
                        delay_between
                    )

        finally:
            try:
                smtp.quit()
            except Exception:
                pass

    print("\n=== 발송 종료 ===")

    print(
        f"처리된 일반 수신자: "
        f"{recipient_index}/"
        f"{len(recipients)}"
    )

    print(
        f"성공한 일반메일: "
        f"{normal_success_count}"
    )

    print(
        f"성공 로그: "
        f"{SUCCESS_LOG.name}"
    )

    print(
        f"실패 로그: "
        f"{FAILED_LOG.name}"
    )

    if recipient_index < len(recipients):
        print(
            f"남은 수신자: "
            f"{len(recipients) - recipient_index}명 "
            "(사용 가능한 SMTP 계정 또는 "
            "설정된 계정별 한도가 모두 소진됨)"
        )


if __name__ == "__main__":
    main()
