Python でメールの添付ファイルを zip して送る

Python スクリプトからメールを送るのは smtplib があるおかげで、わりと簡単にできます。ですが、添付ファイルを付けようと思うとちょっと手間でした。

今回は簡潔に書くため、全て localhost の SMTP サーバを利用しています。Gmail の SMTP サーバで送るときは TLS を使う必要があるのでご注意ください。

テキストだけのメール

import smtplib
from email.mime.text import MIMEText
from email.header import Header
from email.utils import formatdate

def mime_message(from_addr, to_addr, subject, body):
	encoding = 'utf-8'

	m = MIMEText(body, 'plain', encoding)
	m['Subject'] = Header(subject, encoding)
	m['To'] = to_addr
	m['From'] = from_addr
	m['Date'] = formatdate()
	
	return m

def send_message(m):
	smtp_host = 'localhost'
	
	s = smtplib.SMTP(smtp_host)
	s.sendmail(m['From'], m['To'], m.as_string())
	s.quit()

to_addr = 'postmaster@example.com'
from_addr = 'atmark@example.com'
subject = 'メールのテスト'
body = 'このメールはテストメールです。'

send_message(mime_message(from_addr, to_addr, subject, body))

smtplib.sendmail() の from と to は RFC を確認せず書いているので、もしかしたら引っかかるかもしれません("atmark <atmark@example.com>" みたいな形式)。

ファイルを zip して添付

変更するのはメッセージの作成部分だけです。

import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from email.header import Header
from email.utils import formatdate
from email import encoders

import tempfile, zipfile
import os.path

def mime_email(to, subject, body, attachment_path = None):
	encoding = 'utf-8'

	m = MIMEMultipart()
	m['Subject'] = Header(subject, encoding)
	m['To'] = to
	m['Date'] = formatdate()

	# 本文
	m.attach(MIMEText(body, 'plain', encoding))

	if attachment_path:
		# 添付ファイルを zip でまとめる
		temp = tempfile.TemporaryFile()
		with zipfile.ZipFile(temp, 'w', compression = zipfile.ZIP_DEFLATED) as zip:
			zip.write(attachment_path, arcname = os.path.basename(attachment_path))
		temp.seek(0)

		# attach file
		a = MIMEBase('application', 'zip')
		a.set_payload(temp.read())
		encoders.encode_base64(a)
		m.attach(a)
		a.add_header('Content-Disposition', 'attachment', filename = date.today().strftime('attachment.zip'))
	
	return m

zipfile.write(filename) は与えられたパスのファイルをアーカイブします。このとき引数 arcname でアーカイブ時のファイル名を指定できます。指定しないとファイル名がそのまま arcname になりますが、無駄なディレクトリ構造を排除したいので os.path.basename() でファイル名を取得して指定しています。同じファイル名が存在する場合は適宜変更してください。

複数のファイルを zip するときも、次のように zip.write() を続け呼び出すだけです。

attachment_files = ['path_to_file.txt', 'long_path_to_file.txt']
for p in attachment_files:
	zip.write(p, arcname = os.path.basename(p))

compression = zipfile.ZIP_DEFLATED を指定することで zip 全体を圧縮しています。compression には以下の値を指定できます。

compression説明依存
zipfile.ZIP_STORED無圧縮
zipfile.DEFLATED通常の圧縮zlib
zipfile.BZIP2BZIP2 圧縮bz2, Python 3.3 以降
zipfile.LZMALZMA 圧縮lzma, Python 3.3 以降

compression は zipfile.write() で個別に指定することもできます。

参考