Tag: email

python email header 인코딩 오류 수정l

postfix mail archiving을 진행하면서 메시지 헤더를 처리할 일이 생겼지요. 보내는 사람에 따라서 archiving을 하지 않으려구오. 뭐... cron에서 보내는거 daily 체크 메일등등 저장할 필요 없잖아요.

그런데 이거 하다보니 헤더 디코딩이 안된 것들이 간간히 들어옵니다. 언듯 봐서는 잘된것 처럼 보이는데 그래서 좀 찾아보니 python의 오류가 아니고, 아예 잘못된 헤더랍니다.

=?UTF-8?B?6rmA7ZiV7ISd?=<XXXX@naver.com>'
=?UTF-8?B?6rmA7ZiV7ISd?= <XXXX@naver.com>'

첫번째 것은 잘못 인코딩 된 것이고, 두번째 것은 제대로 인코딩 된 것입니다. RFC에 따르면 "?=" 다음에 스페이스를 넣게 되어있다는군요.

그래서 이거 해결하는 코드 간략하게 만들어 봤습니다. 아주 간단!!

def decode_header(s):
    ret = []
    for header, encoding in email.header.decode_header(s):
        if (encoding is None) and re.search(r'\?=.+', header):
            header = re.sub(r'([^\?.]{2})(\?=)(.{1})', r'\1\2 \3', header)
            ret.extend(email.header.decode_header(header))
            continue
        ret.append((header, encoding))
    return ret

근데.. 이런 오류는 꼭 NAVER에서 온 메일만 그래요... ㅡㅡ

ps. re.sub에서 [a-zA-Z0-9]는 \w로 바꿀 수 있다구요? 예 유니코드 환경이 아니면 맞는 이야기인데, 유니코드면 약간 다르더군요. Python 메뉴얼에는 이렇게 나와있습니다.

\w
When the LOCALE and UNICODE flags are not specified, matches any alphanumeric character and the underscore; this is equivalent to the set [a-zA-Z0-9_]. With LOCALE, it will match the set [0-9_] plus whatever characters are defined as alphanumeric for the current locale. If UNICODE is set, this will match the characters [0-9_] plus whatever is classified as alphanumeric in the Unicode character properties database.


postfix mail archiving

메일을 보낼때 검토받아서 보내는 부분은 보류되었습니다. 역시나 하나가 끝나면 하나가 다시 오는법... 검토는 하지않지만 주고받은 메일을 archiving하는 것으로 선회했습니다. moderation은 영 정말로 껄그럽지만, archiving은 그보다는 좀 낫네요.

가장 간단한 방법은 모든 always_bcc으로 모든 메일 메시지를 특정 계정으로 보내면 됩니다. archiving용 계정 하나 만들고 모두 보내면 되니깐 편하겠지요.

그런데 모든 메일입니다. archiving에 필요없는 메시지도 가는 것이지요. 뭐 메일 root@로 날아오는 관리일이나 각종 보안 검사메일, cron 명령 메일등등 이런것을 굿이 archiving할 필요가 없는데 말입니다. 그래서 가장 간단하지만 always_bcc를 사용하지 않기로하고, 그런 다음에 눈에 들어온게 content_filter입니다.

content_filter를 만들어서 거기서 메일을 저장하면 되겠지요. 간단한 내용이지만 이거 메커니즘 이해하느라 상당히 고생했습니다. postfix 샘플대로 하니 안됩니다. 계속 loop를 돌아서 에러를... ㅡㅜ;

설정

archiver 필터를 archiver라고 하면

main.cf:

content_filter = archiver

이제 모든 메시지는 archiver 서비스를 통해서 전달됩니다.

master.cf:

archiver   unix -       n       n       -       10      pipe
   flags=Rq user=whitekid null_sender=
   argv=/usr/local/bin/python /home/whitekid/work/postfix-archiver/archiver.py 127.0.0.1 20026 ${sender} ${recipient}

127.0.0.1:20026 inet n - - - - smtpd
        -o content_filter=
        -o receive_override_options=no_unknown_recipient_checks,no_header_body_checks,no_milters
        -o smtpd_helo_restrictions=
        -o smtpd_client_restrictions=
        -o smtpd_sender_restrictions=
        -o smtpd_recipient_restrictions=permit_mynetworks,reject
        -o mynetworks=127.0.0.0/8
        -o smtpd_authorized_xforward_hosts=127.0.0.0/8
  • archiver 서비스에서 필터를 실행합니다.
  • 그리고 파라미터들은 필터를 처리하고 난 다음의 action들을 지정하는데 archiver 필터를 처리한 후 127.0.0.1:20026 서비스를 호출하기 위해 파라미터를 전달해줍니다. 이 파라미터는 archiver.py에서 받아서 다음 서비스에 전달합니다. 아래 서비스가 smtpd이므로 smtp로 메일을 보내는 것 처럼 합니다.

content_filter --(pipe)--> archiver --(smtp)--> 127.0.0.1:20026 의 순서로 메시지가 전달됩니다.

archiver.py

이제 archiver.py의 대략적인 소스를 보면...

import sys
import smtplib
import email

EX_OK   = 0
EX_TEMPFAIL = 75
EX_UNAVAILABLE = 69

def main():
    host, port, sender, recipient  = sys.argv[1], int(sys.argv[2]), sys.argv[3], sys.argv[4:]
    msg = email.message_from_string(sys.stdin.read())
    msg['X-Archive-Status'] = 'Archived'

    server = smtplib.SMTP(host, port)
    for r in recipient:
        server.sendmail(sender, r, msg.as_string())
    server.quit()

if __name__ == '__main__':
    try:
        main()
        sys.exit(EX_OK)
    except Exception, e:
        print e
        sys.exit(EX_TEMPFAIL)

내용은 아주 간단합니다. stdin에서 받은 내용에서 약간의 메시지 조작을 하고, 그 메시지를 다시 파라미터로 받은 smtp로 연결해서 넘김니다. 자 이렇게고 maillog를 보면

Aug 24 14:23:39 freebsd postfix/qmgr[22880]: 537F51705A: from=<root@freebsd.woosum.net>, size=1013, nrcpt=1 (queue active)
Aug 24 14:23:39 freebsd postfix/pipe[22890]: 3D3E217030: to=<whitekid@freebsd.woosum.net>, relay=archiver, delay=0.1, delays=0.01/0.01/0/0.09, dsn=2.0.0, status=sent (delivered via archiver service)
Aug 24 14:23:39 freebsd postfix/qmgr[22880]: 3D3E217030: removed
Aug 24 14:23:39 freebsd postfix/local[22894]: 537F51705A: to=<whitekid@freebsd.woosum.net>, relay=local, delay=0.01, delays=0.01/0/0/0, dsn=2.0.0, status=sent (delivered to mailbox)
Aug 24 14:23:39 freebsd postfix/qmgr[22880]: 537F51705A: removed

음 archiver 서비스를 이용해서 mailbox가지 간게 보이나요? 보이면 OK!

amavisd-new와 공존

amavisd도 content_filter를 사용합니다. 그런데 이게 2개가 적용되지 않습니다. content_filter는 1개인것이고, 이게 chain으로 역여서 돌아가는 것이지요. 이렇게 chain에 넣어야하는데, 그건 간단하게...

amavis unix - - - - 2 smtp
   -o smtp_data_done_timeout=1200
   -o smtp_send_xforward_command=yes

127.0.0.1:10025 inet n - - - - smtpd
-o content_filter=archiver

처럼 content_filter에 archiver를 넣어주면 됩니다.


postfix outgoing mail moderation

이메일 서버를 postfix를 사용하고 있습니다. 이런 중에 외부로 나가는 메일을 허락받고 보내는 솔루션을 검토할 필요가 생겼습니다. MS Outtlook에서는 email modeeration이라는 기능으로 이무 구현이 되어있더군요. 그런데 postfix용으로 이런게 있는지 뒤져보는데, 역시나 없습니다. 물론 하고자하는 일이 영 껄그런기능이라 그런지 그런 질문에 부정적인 답볃도 많구요. CCTV를 설치하고 감시하는게 어떠냐는 등등.. ^^;

어찌되었던간 postfix로 간단하게 구현했습니다.

smtpd_data_restriction에서 check_policy_service를 이용해서 처리하고, 검토되어야하는 메일일 경우에는 메시지를 HOLD하고 moderator에게 확인 메시지를 보내고 그 결과에 따라서 처리합니다.

main.cf:

smtpd_data_restrictions = check_policy_service unix:private/moderation

smtp_data_restriction을 이용한건 다른 곳에서는 해당 메시지의 queue id가 넘어오지 않아서 입니다.

master.cf:

moderation         unix  -      n       n       -       0       spawn
 user=nobody argv=/usr/local/bin/python /home/whitekid/work/postfix-moderation/postfix-moderation.py

이렇게 주면 smtp에서 데이터가 넘어올때 master.cf에 설정된 moderation 서버스에 의해서 메시지가 처리됩니다.

posfix-moderation.py

postfix-moderation.py는 smtpd policy 서비스를 하는 것으로 자세한 프로토콜은 Postfix 웹사이트에 있습니다. sender, recipient를 읽어서 외부로 나가는지 확인하고 정상적이면 action=dunno를 아니면 action=hold를 리턴하면 됩니다. 소스는 아주 간단합니다. 약 주석을 포함해서도 100줄 정도 되는군요

여기서 hold된 상태들은 PostgreSQL에 관리된 DB에 들어갑니다(간단한 것이라 SQLite등 보다 가벼운 것으로 할 수 있지만, PostgreSQL이 설치되 사용중이라 그걸로 했습니다.).

cron.py:

이렇게 DB에 메시지들이 설정되면 cron.py에서 메일 보내가나 큐를 조작하는 일들을 합니다. 1분 간격으로 실행되면서 DB를 검토해서 새로운 검토가 필요한 메시지면 moderator에 메시지를 보내고, moderator가 allow 또는 deny한 것들을 처리힙니다.

큐에서 postcat -bh [queue_file]을 이용해서 큐의 메시지를 이메일 메시지로 얻어오고 이 정보로 확인하죠.

email.message_from_file(os.popen('/usr/local/sbin/postcat -bh /var/spool/postfix/hold/%s' % queue_id))

또 문제되었던게 원본 메시지를 포함하여 moderator에게 메일을 보내는 건데..

    def attach_message(msg, attach):
        base = MIMEBase('message', 'rfc822')
        base.attach(attach)
        msg.attach(base)

    msg = MIMEMultipart()
    msg['From'] = 'moderation@' + MY_DOMAIN
    msg['To'] = moderator
    msg['Subject'] = u'[메일 전송 확인] %s' % subject
    msg.attach(MIMEText(message.encode('utf-8'), _subtype='html', _charset='utf-8'))

    attach_message(msg, orig_message)
    server = smtplib.SMTP()
    server.connect('localhost')
    server.sendmail('moderation@' + MY_DOMAIN, moderator, msg.as_string())
    server.close()

이런식으로 보냅니다.

메일 큐 조작은 postsuper를 이용하면 됩니다. -H 옵션은 홀드된 메시지를 다시 보내는 것이고, -d 옵션은 큐를 지웁니다. 따라서 allow된 것은 release하고 postqueue -i [queue_id]로 바로 보내도록 설정합니다.

cgi/moderate.py

cron에서 moderator에게 메일을 보내면 여기에는 allow/deny 링크가 포함됩니다. moderator는 이 링크를 이용해서 메일을 허락 또는 거부하죠.

물론 여기에서는 DB를 조작하는 것 빼고는 아무것도 없습니다. 당연히 모든 작업은 cron.py에서 하니깐요.

Alias /moderate "/home/whitekid/work/postfix-moderation/cgi/moderate.cgi"

<Directory "/home/whitekid/work/postfix-moderation/cgi">
 AddHandler cgi-script .cgi
 Options NONE +ExecCGI
 Order allow,deny
 Allow from all
</Directory>

정신없이 대충 적어서 무슨 말인지 도저히 저도 모르겠군요. ^^; 소스요? 그건 좀 소스정리좀 하구요...


  • Copyright © 1996-2010 Your wish is my command. All rights reserved.
    iDream theme by Templates Next | Powered by WordPress