메일 프로그램을 하나 짜야 하는데 클래스 상속과 재정의 문제로 반나절 삽질을 했습니다.

현재 서버에 설치된 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="")

 

 

출처 : http://bbs.python.or.kr/viewtopic.php?t=19659

'programming > python' 카테고리의 다른 글

파이썬 코딩 스타일 가이드  (0) 2004.01.27
파이썬 스타일 지도서  (0) 2004.01.27
특정 달의 날짜 수 구하기  (4) 2004.01.27

+ Recent posts