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

3 weeks ago 21
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