메일 프로그램을 하나 짜야 하는데 클래스 상속과 재정의 문제로 반나절 삽질을 했습니다.
현재 서버에 설치된 SMTPd가 localhost조차도 인증을 해야만 메일을 보낼 수 있게 설정되어 있습니다. smtplib 모듈의 login(user, password) 메쏘드를 이용하면 된다는 말을 듣고 그대로 해보았는데 계속 실패하는 것입니다.
문제의 원인을 찾아본 결과, SMTPd가 제공(한다고 표시)하는 AUTH 방식이 제대로 작동하지 않는 것이었습니다. 위 SMTPd의 ehlo 메시지는 다음과 같습니다.
LOGIN, CRAM-MD5, PLAIN 이 3가지의 인증방식을 제공한다고 나와 있지만, 실제로 제가 테스트해본 결과 CRAM-MD5 인증은 작동하지 않습니다. 그동안 몰랐던 이유는 아웃룩이나 모질라에서는 LOGIN 방식만을 지원하기 때문에 메일 클라이언트에서 메일 보내고 받는 데 여태 문제가 없었기 때문입니다. 물론 CRAM-MD5 인증방식이 제대로 작동하도록 재설치하면 아무 문제가 없지만, 그럴 수 없는 사정이었습니다.
smtplib 모듈의 login() 메쏘드 소스를 보니, 서버의 AUTH 응답 코드를 리스트로 가지고 있다가, CRAM-MD5, LOGIN, PLAIN 순서대로 인증 시도를 하더군요.
여기서 문제가 생기는 것입니다. 실제로 서버에서는 CRAM-MD5 인증방식이 제대로 작동하지 않는데도 불구하고 Response 메시지에 표시가 되는 문제가 있는데, smtplib 모듈에서는 Response 메시지만을 읽고 CRAM-MD5 방식의 인증을 먼저 시도한다는 거죠. 참고로 CRAM-MD5 인코딩은 서버측 세션값과 timestamp, 클라이언트의 MAC 어드레스 등을 base64로 인코딩합니다. 이에 비하자면 LOGIN은
어렵지 않게 풀립니다. 아웃룩, 모질라 등의 메이저 메일 클라이언트들에서 CRAM-MD5 방식의 SMTP 인증을 빨리 지원해주기를 바랍니다. (사실 위 클라이언트들에서 CRAM-MD5 방식을 (smtplib 모듈과 같은 방식으로) 지원했더라면, 서버 관리자가 진작에 문제를 알아차렸을 것이므로 이렇게 허망한 삽질을 할 이유도 없었을 것입니다. -_-)
아뭏든 smtplib.py 소스에 직접 손을 대면 어렵지 않게 해결할 수 있었겠지만--if/elif 순서를 바꿔 LOGIN을 먼저 처리하도록--, 표준 라이브러리에 손을 대는 것이 영 찜찜하여, 관계된 모듈의 클래스를 상속받아 문제가 되는 login() 메쏘드만 재정의했습니다.
아래와 같이 사용합니다.
혹시 SMTP 인증 문제로 어려움을 겪고 계시다면, 을 참조하여 서버와 주고받는 메시지를 확인해보시고, smtplib 모듈에서 encode_base64, encode_cram_md5, encode_plain 등을 각각 추출하여 직접 서버와 통신해보십시요.
현재 서버에 설치된 SMTPd가 localhost조차도 인증을 해야만 메일을 보낼 수 있게 설정되어 있습니다. smtplib 모듈의 login(user, password) 메쏘드를 이용하면 된다는 말을 듣고 그대로 해보았는데 계속 실패하는 것입니다.
문제의 원인을 찾아본 결과, SMTPd가 제공(한다고 표시)하는 AUTH 방식이 제대로 작동하지 않는 것이었습니다. 위 SMTPd의 ehlo 메시지는 다음과 같습니다.
코드: |
ehlo localhost 250-xxx.xxx.co.kr 250-PIPELINING 250-AUTH LOGIN CRAM-MD5 PLAIN 250 8BITMIME |
LOGIN, CRAM-MD5, PLAIN 이 3가지의 인증방식을 제공한다고 나와 있지만, 실제로 제가 테스트해본 결과 CRAM-MD5 인증은 작동하지 않습니다. 그동안 몰랐던 이유는 아웃룩이나 모질라에서는 LOGIN 방식만을 지원하기 때문에 메일 클라이언트에서 메일 보내고 받는 데 여태 문제가 없었기 때문입니다. 물론 CRAM-MD5 인증방식이 제대로 작동하도록 재설치하면 아무 문제가 없지만, 그럴 수 없는 사정이었습니다.
smtplib 모듈의 login() 메쏘드 소스를 보니, 서버의 AUTH 응답 코드를 리스트로 가지고 있다가, CRAM-MD5, LOGIN, PLAIN 순서대로 인증 시도를 하더군요.
코드: |
if authmethod==AUTH_CRAM_MD5: ... elif authmethod==AUTH_LOGIN: ... elif authmethod==AUTH_PLAIN: ... |
여기서 문제가 생기는 것입니다. 실제로 서버에서는 CRAM-MD5 인증방식이 제대로 작동하지 않는데도 불구하고 Response 메시지에 표시가 되는 문제가 있는데, smtplib 모듈에서는 Response 메시지만을 읽고 CRAM-MD5 방식의 인증을 먼저 시도한다는 거죠. 참고로 CRAM-MD5 인코딩은 서버측 세션값과 timestamp, 클라이언트의 MAC 어드레스 등을 base64로 인코딩합니다. 이에 비하자면 LOGIN은
코드: |
"%s %s" % ('LOGIN', encode_base64(user, eol="")) |
어렵지 않게 풀립니다. 아웃룩, 모질라 등의 메이저 메일 클라이언트들에서 CRAM-MD5 방식의 SMTP 인증을 빨리 지원해주기를 바랍니다. (사실 위 클라이언트들에서 CRAM-MD5 방식을 (smtplib 모듈과 같은 방식으로) 지원했더라면, 서버 관리자가 진작에 문제를 알아차렸을 것이므로 이렇게 허망한 삽질을 할 이유도 없었을 것입니다. -_-)
아뭏든 smtplib.py 소스에 직접 손을 대면 어렵지 않게 해결할 수 있었겠지만--if/elif 순서를 바꿔 LOGIN을 먼저 처리하도록--, 표준 라이브러리에 손을 대는 것이 영 찜찜하여, 관계된 모듈의 클래스를 상속받아 문제가 되는 login() 메쏘드만 재정의했습니다.
코드: |
import smtplib class SimpleSMTP(smtplib.SMTP): def login(self, user, password): (code, resp) = self.docmd("AUTH", "%s %s" % (AUTH_LOGIN, encode_base64(user, eol=""))) if code != 334: raise SMTPAuthenticationError(code, resp) (code, resp) = self.docmd(encode_base64(password, eol="")) if code not in [235, 503]: # 235 == 'Authentication successful' # 503 == 'Error: already authenticated' raise SMTPAuthenticationError(code, resp) return (code, resp) |
아래와 같이 사용합니다.
코드: |
smtp = SimpleSMTP(smtphost) smtp.login(user, password) smtp.sendmail(mailfrom, mailto, subject+'\r\n\r\n'+body) smtp.quit() |
혹시 SMTP 인증 문제로 어려움을 겪고 계시다면, 을 참조하여 서버와 주고받는 메시지를 확인해보시고, smtplib 모듈에서 encode_base64, encode_cram_md5, encode_plain 등을 각각 추출하여 직접 서버와 통신해보십시요.
코드: |
def encode_base64(s, eol=None): return "".join(base64.encodestring(s).split("\n")) def encode_cram_md5(challenge, user, password): challenge = base64.decodestring(challenge) response = user + " " + hmac.HMAC(password, challenge).hexdigest() return encode_base64(response, eol="") def encode_plain(user, password): return encode_base64("%s\0%s\0%s" % (user, user, password), eol="") |
'programming > python' 카테고리의 다른 글
파이썬 코딩 스타일 가이드 (0) | 2004.01.27 |
---|---|
파이썬 스타일 지도서 (0) | 2004.01.27 |
특정 달의 날짜 수 구하기 (4) | 2004.01.27 |