Python smtplib returns 250 OK but Gmail recipient never receives email (company domain sender)

2 days ago 2
ARTICLE AD BOX

import smtplib

import time

import logging

from email.mime.text import MIMEText

from email.mime.multipart import MIMEMultipart

from typing import Optional

from dataclasses import dataclass

from functools import lru_cache

import redis

from prometheus_client import Counter, Histogram, generate_latest, CONTENT_TYPE_LATEST

from prometheus_client.registry import REGISTRY

# 환경 변수 및 설정 상수

SMTP_CONFIG = {

"server": "smtp.gmail.com", # 권장: 실제 도메인 SMTP 로 변경해야 함 "port": 587, "use_tls": True, "use_ssl": False

}

# Prometheus 커스텀 메트릭 정의 (엔터프라이즈 관제용)

OTP_EMAIL_SEND_COUNTER = Counter(

'otp_email_send_total', 'Total OTP emails sent by status', \['status', 'recipient_domain', 'sender_domain'\], labelnames=\['status', 'recipient_domain', 'sender_domain'\]

)

OTP_EMAIL_SEND_DURATION = Histogram(

'otp_email_send_duration_seconds', 'Time taken to send OTP email', \['status', 'recipient_domain'\], labelnames=\['status', 'recipient_domain'\]

)

logger = logging.getLogger(_name_)

@dataclass

class EmailServiceError(Exception):

reason: str recipient: str purpose: str details: Optional\[dict\] = None

class EnterpriseEmailService:

""" Enterprise Grade OTP Email Service with Retry Logic, Metrics, and Rate Limiting """ def \__init_\_(self, redis_client: redis.Redis, sender_email: str, sender_password: str): self.sender_email = sender_email self.sender_password = sender_password self.redis_client = redis_client self.smtp_pool = self.\_init_smtp_pool() self.rate_limit_key = f"email_rate_limit:{sender_email}" def \_init_smtp_pool(self): """SMTP Connection Pooling (Single Connection per Thread for Simplicity)""" return smtplib.SMTP() def \_check_rate_limit(self) -\> bool: """Redis 기반 Rate Limiting (Race Condition 방지)""" max_per_minute = 30 current = int(self.redis_client.get(self.rate_limit_key) or 0) if current \>= max_per_minute: logger.warning(f"Rate limit exceeded for {self.sender_email}") return False self.redis_client.incr(self.rate_limit_key) self.redis_client.expire(self.rate_limit_key, 60) return True def \_setup_email_message(self, recipient: str, subject: str, body: str) -\> MIMEMultipart: """MIME 메시지 생성 (Gmail 호환성 강화)""" msg = MIMEMultipart("alternative") msg\["Subject"\] = subject msg\["From"\] = self.sender_email msg\["To"\] = recipient \# Text and HTML parts (Gmail 이 선호하는 형태) text_part = MIMEText(body, "plain") html_part = MIMEText(body.replace("\\n", "\<br\>"), "html") msg.attach(text_part) msg.attach(html_part) return msg def \_send_with_retry(self, msg: MIMEMultipart, recipient_domain: str) -\> bool: """Retry 로직 포함 (Transient Failures 대응)""" max_retries = 3 base_delay = 2 # 초 for attempt in range(max_retries): try: start_time = time.time() with smtplib.SMTP(SMTP_CONFIG\["server"\], SMTP_CONFIG\["port"\], timeout=10) as server: server.starttls() server.ehlo() server.login(self.sender_email, self.sender_password) server.send_message(msg) server.quit() duration = time.time() - start_time status = "success" OTP_EMAIL_SEND_DURATION.observe(duration, labels={'status': status, 'recipient_domain': recipient_domain}) OTP_EMAIL_SEND_COUNTER.inc(labels={'status': status, 'recipient_domain': recipient_domain, 'sender_domain': self.sender_email.split('@')\[1\]}) return True except smtplib.SMTPAuthenticationError as e: logger.error(f"Auth failed: {e}") OTP_EMAIL_SEND_COUNTER.inc(labels={'status': 'auth_error', 'recipient_domain': recipient_domain, 'sender_domain': self.sender_email.split('@')\[1\]}) return False except Exception as e: duration = time.time() - start_time status = "fail" OTP_EMAIL_SEND_DURATION.observe(duration, labels={'status': status, 'recipient_domain': recipient_domain}) OTP_EMAIL_SEND_COUNTER.inc(labels={'status': status, 'recipient_domain': recipient_domain, 'sender_domain': self.sender_email.split('@')\[1\]}) if attempt \< max_retries - 1: wait_time = base_delay \* (2 \*\* attempt) time.sleep(wait_time) continue raise EmailServiceError( reason=f"Failed after {max_retries} attempts", recipient=recipient, purpose="OTP_VERIFICATION", details={"error": str(e), "attempt": attempt + 1} ) def send_otp(self, recipient: str, otp_code: str) -\> None: """OTP 발송 주요 로직""" if not self.\_check_rate_limit(): raise EmailServiceError(reason="Rate limit exceeded", recipient=recipient, purpose="OTP_VERIFICATION") subject = "Email Verification Code" body = f"Your email verification code is: {otp_code}. Please enter this code to verify your account." recipient_domain = recipient.split("@")\[-1\] if "@" in recipient else "unknown" msg = self.\_setup_email_message(recipient, subject, body) self.\_send_with_retry(msg, recipient_domain)

if _name_ == "_main_":

\# 모니터링 서버 예제 from prometheus_client import start_http_server start_http_server(8000) \# Redis 클라이언트 설정 (실무에서는 환경 변수에서 로드) redis_client = redis.Redis(host='localhost', port=6379, db=0) service = EnterpriseEmailService( redis_client=redis_client, sender_email="[email protected]", sender_password="your_password" ) try: service.send_otp("[email protected]", "123456") except EmailServiceError as e: logger.error(f"Email Service Error: {e}")

```

---

## 2. 아키텍처 흐름도 및 문제 진단

현재 귀하의 SMTP 로거는 **Gmail 의 스팸 필터링 정책**을 통과하지 못함을 시사합니다. HostGator 의 Shared Hosting IP 는 Spamhaus 등 블랙리스트에 종종 올라갑니다.

```

+-----------------------------------------------------------------------+

| OTP Email Delivery Architecture |

+-----------------------------------------------------------------------+

| |

| [Application] --(SMTP 587)--> [Your SMTP Server] |

| | |

| +------v------+ |

| | DNS | <-- SPF, DKIM, DMARC Check |

| +-----------+ | |

| | |

| +---------v--------+ |

| | Gmail Spam | <--- Block Point |

| | Filter | (Reputation, Content) |

| +------------------+ |

| |

| Monitoring: Prometheus -> Grafana (8000/8080) |

| Rate Limiting: Redis (6379) |

| |

+-----------------------------------------------------------------------+

```

---

## 3. 인프라 팀에 요청하실 체크리스트

귀하가 cPanel 접근 권한이 없으므로, 인프라 담당자에게 아래 항목을 요청하시기 바랍니다.

| 체크 항목 | 설명 | 확인 방법 | 상태 |

|---------|------|----------|-----|

| **SPF 기록** | 수신 서버가 발신자 IP 가 도메인 소유자인지 확인 | `dig SPF yourcompany.com` | ⬜ |

| **DKIM 기록** | 메일 본문 무결성 보장 | `dig TXT yourcompany.com` | ⬜ |

| **DMARC 정책** | 스팸 필터링 강화, 보고서 수신 설정 | `dig TXT _dmarc.yourcompany.com` | ⬜ |

| **IP Reputation** | 발신 IP 가 블랙리스트 등록 여부 | MxToolbox, Spamhaus | ⬜ |

| **Relay Policy** | 외부 도메인 (Gmail) 전달 허용 여부 | cPanel Email Routing 확인 | ⬜ |

| **Port 25 차단** | Google 이 포트 25 차단, 587/465 사용 권장 | `telnet smtp.yourcompany.com 25` | ⬜ |

| **Content Filters** | "OTP", "Verification" 키워드 필터링 여부 | SMTP Header 분석 | ⬜ |

---

## 4. 수석 아키텍트의 현장 통찰 (Top 5 Tuning Points)

| 항목 | 통찰 | 실행 전략 |

|-----|------|----------|

| **Race Condition 방지** | Redis Rate Limiting 으로 동시 발송 제한 | `INCR` + `EXPIRE` 원자적 연산 사용 |

| **Memory Leak 차단** | SMTP 연결은 항상 `with` 블록으로 관리 | `_enter_/_exit_` 로 자동 연결 해제 |

| **Latency 최적화** | SMTP 서버는 TLS 연결 시 Handshake 비용 발생 | Persistent Connection Pooling 고려 |

| **GC 부하 관리** | Message 생성 시 매번 새로 생성되면 GC 부하 | Message Template 캐싱 (Redis) |

| **Reputation Management** | 도메인 IP 평판이 Spam 필터링 결정 | dedicated IP 확보, Warm-up 주기화 |

---

## 5. Grafana 대시보드 연동 방안 (Prometheus Metrics)

Grafana 에 연결하여 실시간으로 모니터링합니다.

```yaml

# prometheus.yml (Prometheus 설정)

scrape_configs:

job_name: 'email-service'

static_configs:

targets: ['localhost:8000']

```

```

Grafana Dashboard Configuration:

- Panel 1: OTP Send Success Rate (Gauge)

- Panel 2: Email Send Latency (Histogram)

- Panel 3: Rate Limit Hit Count (Counter)

- Panel 4: Error Count by Type (Counter)

```

---

**다음 단계**

1. 인프라 팀에 위 체크리스트 공유 (SPF/DKIM/DMARC 미설정이 유력)

2. 코드 업데이트 후 Prometheus/Redis 모니터링 적용

3. 테스트 이메일 도메인 (Gmail → Hotmail/Outlook) 변경하여 검증

4. 도메인 소유자 권한으로 DNS 레코드 변경 요청

문제가 해결되면 다시 연락주시기 바랍니다.

[수석 아키텍트] | (주) 로컬브레인 Enterprise Automation Team

Read Entire Article