Python으로 이메일 주소를 검증하는 방법
Python으로 이메일 주소를 검증하는 것은 쉬운 부분은 간단하지만, 가장 중요한 부분은 의외로 어려워요. 이 가이드는 로컬에서 실행할 수 있는 구문 검사와 DNS 조회를 살펴보고, 원시 SMTP 프로브가 실제로 왜 실패하는지 설명하며, REST API로 메일함 수준의 결과를 얻는 방법을 보여줘요.
Python으로는 잘못된 형식의 입력을 걸러내고, 필요하면 DNS/MX 확인 결과를 캐시하세요. 메일함 수준의 결과는 검증 API를 사용하세요. 원시 smtplib 프로브는 쉽게 제한되고 모호한 응답을 돌려주는 경우가 많기 때문이에요.
이메일 "검증"의 실제 의미
이메일 검증 은 메일을 보내기 전에 주소가 실제로 존재하고 메일을 받을 수 있는지 확인하는 과정이에요. 갈수록 어려워지는 일련의 확인으로 나뉘어요. 주소 형식이 올바른지, 도메인이 메일을 받는지, 그리고 그 메일함이 실제로 존재하는지예요. Python에서는 앞의 두 가지는 쉽게 잘할 수 있어요. 세 번째가 직접 만든 솔루션 대부분이 조용히 실패하는 지점이고, 그 이유를 이해하는 것이 이 글에서 가장 유용한 부분이에요.
아래 계획은 단계별로 구성돼요. 빠르고 저렴한 확인은 로컬에서 실행해 명백히 잘못된 입력을 걸러내고, 주소를 사용할 수 있는지에 대한 메일함 수준의 답은 검증 API를 호출해 얻으세요. 이렇게 하면 쓸모없는 입력은 API 호출 없이 바로 거부하고, 통과한 주소에는 더 명확한 결과를 얻을 수 있어요.
1단계: 구문 검증
첫 번째 확인은 주소 형식이 올바른지 보는 거예요. 정규식을 직접 작성하고 싶겠지만, 이메일 구문은 보기보다 훨씬 복잡하고 직접 만든 정규식은 거의 항상 너무 엄격하거나 너무 느슨해요. 이 작업의 표준 라이브러리는 email-validator예요. 관련 표준을 제대로 구현했고 실제 서비스에서 널리 쓰여요.
pip로 설치하세요.
pip install email-validator
그런 다음 주소를 검증하세요. 이 라이브러리는 잘못된 형식이면 EmailNotValidError 예외를 발생시키고, 유효한 입력이면 정규화된 객체를 반환해요.
from email_validator import validate_email, EmailNotValidError
def check_syntax(address):
try:
result = validate_email(address, check_deliverability=False)
return True, result.normalized
except EmailNotValidError as error:
return False, str(error)
ok, value = check_syntax("jane.doe@example.com")
print(ok, value) # True jane.doe@example.com
ok, value = check_syntax("jane.doe@@example")
print(ok, value) # False, with a description of the problem
이 단계는 check_deliverability=False 설정 덕분에 네트워크 호출 없이 순수하게 구문만 확인하므로, 키를 입력할 때마다 또는 폼을 제출할 때마다 실행할 만큼 빨라요. 항상 원시 사용자 입력 대신 라이브러리가 반환하는 normalized 값을 저장하세요. 일관된 저장을 위해 주소를 표준 형태로 바꿔주기 때문이에요.
2단계: 도메인과 MX 확인
형식이 올바른 주소라도 도메인이 이메일을 받을 수 없다면 쓸모가 없어요. 다음 확인은 도메인의 DNS 레코드를 조회해 해당 도메인을 담당하는 메일 서버를 지정하는 MX 레코드를 찾는 거예요. MX 레코드가 없는 도메인은 메일을 받을 수 없으므로, 그 도메인의 주소는 바로 거부할 수 있어요.
반면 email-validator 라이브러리에 check_deliverability=True옵션을 전달하면 검증 과정에서 MX 조회까지 대신 수행해 줘요.
from email_validator import validate_email, EmailNotValidError
def check_domain(address):
try:
result = validate_email(address, check_deliverability=True)
return True, result.domain
except EmailNotValidError as error:
return False, str(error)
여러 주소에서 같은 도메인의 결과를 캐시하려는 등 DNS 쿼리를 직접 실행하고 싶다면 dnspython 라이브러리를 직접 사용하세요.
pip install dnspython
import dns.exception
import dns.resolver
def has_mx_record(domain):
try:
records = dns.resolver.resolve(domain, "MX")
return len(records) > 0
except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN):
return False
except dns.exception.DNSException:
return False # Treat resolver errors as inconclusive.
print(has_mx_record("gmail.com")) # True
print(has_mx_record("no-such-domain-xyz.invalid")) # False
이 방법은 정말 유용해요. 만료됐거나, 존재한 적이 없거나, 메일을 처리하지 않는 도메인의 주소를 걸러내 주니까요. 하지만 아직 알려주지 않은 것이 있어요. someone@gmail.com 이 실제 메일함인지 여부예요. Gmail에는 MX 레코드가 있으므로, 모든 Gmail 주소는 유효하든 아니든 이 확인을 통과해요.
3단계: 메일함 확인, 그리고 원시 SMTP가 부족한 이유
메일함 자체를 확인하려면 수신 메일 서버와 통신해야 해요. 원리상으로는 SMTP로 연결하고, 자신을 소개한 다음, 대상 주소로 RCPT TO 명령을 보내고 응답 코드를 읽으면 돼요. 긍정 코드는 메일함이 존재한다는 뜻이고, 거부는 존재하지 않는다는 뜻이에요. Python에 내장된 smtplib 으로 이 작업을 할 수 있고, 코드는 놀라울 만큼 단순해 보여요.
import smtplib
import dns.resolver
def naive_smtp_check(address):
domain = address.split("@")[1]
mx_host = str(dns.resolver.resolve(domain, "MX")[0].exchange)
server = smtplib.SMTP(timeout=10)
server.connect(mx_host)
server.helo("example.com")
server.mail("probe@example.com")
code, message = server.rcpt(address)
server.quit()
# 250 and 251 generally mean the mailbox was accepted.
return code in (250, 251)
튜토리얼에서는 잘 작동하지만 실제 서비스에서는 실패해요. 그 이유를 자세히 알아둘 가치가 있어요. 제대로 된 검증 도구가 이런 스크립트에 의존하지 않는 이유를 설명해 주거든요.
IP 평판과 속도 제한
메일 제공업체는 검증 프로브를 적극적으로 감지해요. 한 서버에서 RCPT TO 확인을 몇 번만 실행해도 Gmail이나 Outlook 같은 제공업체는 몇 번의 시도 만에 그 IP를 제한하거나 차단해요. 일단 제한되면 서버는 메일함 존재 여부와 상관없이 모호한 응답을 돌려주므로 결과가 무의미해져요. 실제 검증이 충분히 워밍업되고 잘 관리되는 대규모 IP 풀에서 이루어지는 이유가 바로 이거예요.
그레이리스팅
많은 서버는 그레이리스팅을 사용해서, 모르는 발신자가 처음 연결하면 일부러 일시적인 4xx 실패를 돌려줘요. 정상적인 메일 서버는 나중에 재시도하지만 스패머는 대개 그러지 않아요. 재시도하지 않는 단순한 스크립트는 이 일시적 실패를 거부로 읽고, 멀쩡한 메일함을 유효하지 않다고 표시해요. 순전히 조급함 때문에 생기는 거짓 음성이에요.
캐치올 도메인
캐치올 도메인은 가능한 모든 주소로 오는 메일을 받으므로, RCPT TO 명령은 realperson@domain.com 와 random-string-9f3a@domain.com 모두에 긍정 코드를 돌려줘요. 이런 도메인에서는 원시 SMTP 확인으로 실제 메일함과 존재하지 않는 메일함을 구분할 수 없어요. 캐치올을 감지하려면 확실히 존재하지 않는 주소로 두 번째 프로브를 보내 그것도 수락되는지 봐야 하는데, 이는 단순한 예/아니요가 아니라 캐치올 감지예요.
주요 제공업체의 모호한 응답
일부 대형 제공업체는 일부러 RCPT TO 동작을 모호하게 만들었어요. 어떤 곳은 핸드셰이크 단계에서 모든 주소를 수락하고 나중에야 반송하기 때문에, SMTP 응답에는 여러분이 원하는 정보가 담겨 있지 않아요.
그래서 email-validator 의 관리자들은 일부러 라이브러리에서 SMTP 확인을 제외했어요. 문서에는 서버가 주소의 전달 가능 여부를 드러내지 않는 데 능숙하고, 메일을 받는 것처럼 보이는 주소도 나중에 반송될 수 있으므로 SMTP 서버에 직접 연결해서 얻을 것이 없다고 분명히 적혀 있어요. 솔직한 결론은 이거예요. 구문과 MX 확인은 직접 코드로 잘할 수 있지만, 믿을 수 있는 메일함 확인은 그렇지 않아요.
4단계: VeriMails API로 검증하기
현실적인 해결책은 메일함 확인을 이를 위해 구축된 인프라를 가진 검증 서비스에 맡기고, Python에서 HTTP 요청 한 번으로 그 서비스를 호출하는 거예요. VeriMails는 REST API 를 제공하며, 구문, MX, DNS, 실시간 SMTP 핸드셰이크, 캐치올 감지, 일회용 주소 감지, 역할 기반 주소 감지까지 전체 과정을 실행하고 하나의 구조화된 결과를 반환해요.
표준 requests 라이브러리를 사용하면 검증 호출은 다음과 같아요. 엔드포인트에 대한 자세한 내용은 다음 문서를 참고하세요. VeriMails API 문서:
import requests
def verify_email(address, api_key):
response = requests.get(
"https://api.verimails.com/v1/verify",
params={"email": address},
headers={"Authorization": f"Bearer {api_key}"},
timeout=10,
)
response.raise_for_status()
return response.json()
result = verify_email("jane.doe@example.com", api_key="YOUR_API_KEY")
print(result["status"]) # e.g. "deliverable", "undeliverable", "catch_all"
print(result["disposable"]) # True if a throwaway address
print(result["role_based"]) # True for addresses such as info@ or support@
그런 다음 결과에 따라 처리하면 돼요. 가입 폼이라면 deliverable 주소는 받고, undeliverable 와 disposable 주소는 거부하고, 다음 결과에 대한 정책은 직접 정할 수 있어요. catch_all:
def is_acceptable_for_signup(address, api_key):
result = verify_email(address, api_key)
if result["disposable"]:
return False, "Disposable addresses are not allowed."
if result["status"] == "undeliverable":
return False, "That address does not appear to exist."
return True, "Address verified."
호출마다 명확한 결과를 반환하므로, 사용자를 기다리게 하지 않고 폼 제출 과정 안에서 실행할 만큼 빨라요. 이미 데이터베이스에 있는 기존 주소 리스트라면 대량 CSV 검증 이 더 잘 맞아요. 단일 주소 엔드포인트를 수천 번 반복 호출하는 것보다 파일 하나를 업로드하는 편이 훨씬 효율적이기 때문이에요.
Python 애플리케이션에 권장하는 방식은 두 계층을 결합하는 거예요. 로컬 email-validator 구문 검사로 크레딧을 쓰기 전에 쓸모없는 입력을 즉시 거부하고, 통과한 모든 주소는 API를 호출해 신뢰할 수 있는 도달률 결과를 받으세요. 명백한 실수에는 빠른 피드백을, 실제 주소에는 믿을 수 있는 판정을 얻을 수 있어요.
애플리케이션 정책 표
| 결과 | 가입 정책 | 가져오기 또는 CRM 정책 |
|---|---|---|
deliverable | 정규화된 주소를 받아서 저장 | 활성 세그먼트에 유지 |
undeliverable | 명확한 수정 안내 메시지와 함께 거부 | 캠페인 전에 차단 |
catch_all | 계정 위험도에 따라 수락하거나 검토 | 별도 세그먼트로 나누고 보수적으로 발송 |
disposable | 대부분의 제품에서 거부 | 아웃리치 리스트에서 차단 |
role_based | 공유 받은편지함이 허용될 때만 허용 | 콜드 아웃리치 전에 검토 |
API 호출과 리스트 작업
가입, 결제, 리드 폼, 초대, 계정 변경처럼 즉시 결정이 필요한 곳에는 API를 사용하세요. 수천 개의 주소를 같은 기준으로 검토해야 하는 CSV 내보내기, CRM 정리, 캠페인 대상에는 대량 검증을 사용하세요. 두 워크플로 모두 같은 차단 규칙으로 연결해야, 한 경로에서 거부된 주소가 나중에 다른 도구에서 발송되는 일을 막을 수 있어요.
한데 모으기
깔끔한 구현은 세 계층으로 이루어져요. 첫째, email-validator 구문 검사를 check_deliverability=False설정으로 로컬에서 즉시 실행해 잘못된 형식의 입력을 거부해요. 둘째, API를 호출하기 전에 죽은 도메인을 걸러내고 싶다면 선택적으로 dnspython 으로 MX 확인을 해요. 물론 API도 이 확인을 포함해요. 셋째, 혼자서는 안정적으로 재현할 수 없는 캐치올, 일회용, 역할 기반 감지를 포함한 메일함 수준의 결과를 얻기 위해 VeriMails API를 호출해요.
기억할 점은 역할 분담이에요. 구문과 DNS는 Python에서 충분히 직접 처리할 수 있어요. 실시간 메일함 확인은 IP 평판, 그레이리스팅 동작, 캐치올 감지에 좌우되는데 이는 스크립트 하나로 감당할 수 없고, 표준 검증 라이브러리가 일부러 SMTP 프로브를 제외한 이유이기도 해요. 속도는 로컬 확인으로, 메일함 수준 검증은 API로 해결하고, 요금제 페이지 에서 예상 사용량에 맞는 플랜을 고르세요.
자주 묻는 질문
관련 글
깨끗한 데이터로 시작하세요
가입 시 무료 크레딧 100개. 신용카드 없이 시작할 수 있어요. 오늘 바로 실천해 보세요.
무료로 시작하기