Pythonで請求書メールを一括送信!smtplibでPDF添付・宛名差し込み・二重送信防止まで【フリーランス向け】
「月末になると、取引先ごとに請求書PDFを添付してメールを書くのが地味にしんどい…」
「宛名や金額をコピペし間違えて、別の会社の請求書を送ってしまったらと思うとこわい…」
フリーランスや個人事業をしていると、こんな悩みありますよね。3社くらいなら手作業でもなんとかなりますが、毎月くり返すとなると、うっかりミスがいちばんの心配です。
この記事では、Python の標準ライブラリだけ(smtplib と email.message.EmailMessage)を使って、CSVの一覧から宛名・金額を差し込んだ請求書メールをPDF添付つきで一括送信する仕組みを作ります。
- 対象:Pythonの基本(for文・関数・辞書)がわかる初〜中級者
- 使うもの:Python 3(標準ライブラリのみ)+練習用に
aiosmtpd - ゴール:「送る前に確認できる」「PDFが無ければ止まる」「2回実行しても二重送信しない」送信スクリプト
いきなり本物のメールを送るのはこわいので、自分のPCの中だけで動くテスト用メールサーバーで試してから本番に切り替える流れにしています。一緒に見ていきましょう!🚀
全体の流れ:一覧 → 下書き確認 → 送信

イメージとしては、「宛名ラベルの一覧表を作って、封筒に詰めて、中身を確認してからポストに入れる」流れをPythonにやってもらう感じです。
- invoices.csv に宛先・宛名・金額・PDFファイル名を書く
- make_mails.py でメールを組み立て、送る前に
.emlファイルとして保存して確認する - send_mails.py で送信する(送信済みは記録して、2回目はスキップ)
フォルダ構成はこんな感じです。
請求メール/
├─ invoices.csv
├─ make_mails.py
├─ send_mails.py
└─ invoices/
├─ invoice_tanaka_202610.pdf
├─ invoice_suzuki_202610.pdf
└─ invoice_sato_202610.pdf
一覧のCSVは次のとおりです(UTF-8で保存します)。
宛先メール,宛名,件名の月,金額,PDFファイル
tanaka@example.com,株式会社タナカ商事 田中様,2026年10月,165000,invoice_tanaka_202610.pdf
suzuki@example.com,スズキデザイン 鈴木様,2026年10月,55000,invoice_suzuki_202610.pdf
sato@example.com,佐藤工務店 佐藤様,2026年10月,88000,invoice_sato_202610.pdf
ステップ1:メールを組み立てて、送る前に確認する
まずは送らずに作るだけのスクリプトです。ここが重要です👇
EmailMessageのset_content()で本文、add_attachment()でPDFを添付- 本文はテンプレート文字列に
format()で宛名・月・金額を差し込む({amount:,}で3桁区切り) - PDFが見つからない行はメールを作らず、エラーとして別に集める
- 作ったメールは
outbox/に.emlで保存。ダブルクリックするとメールソフトで「届いたときの見た目」を確認できます
import csv
from email.message import EmailMessage
from pathlib import Path
FROM_ADDR = "yamada@example.com" # 自分の送信元アドレス
PDF_DIR = Path("invoices") # 請求書PDFを置いたフォルダ
OUTBOX = Path("outbox") # 送信前の確認用フォルダ
BODY = """{name}
いつもお世話になっております。山田です。
{month}分の請求書をお送りします。
ご請求金額:{amount:,}円(税込)
PDFを添付しておりますので、ご確認をお願いいたします。
"""
def build_message(row):
"""CSVの1行から、PDF添付つきのメールを1通つくる"""
msg = EmailMessage()
msg["From"] = FROM_ADDR
msg["To"] = row["宛先メール"]
msg["Subject"] = f"【請求書】{row['件名の月']}分のご請求"
msg.set_content(BODY.format(name=row["宛名"], month=row["件名の月"],
amount=int(row["金額"])))
pdf = PDF_DIR / row["PDFファイル"]
msg.add_attachment(pdf.read_bytes(), maintype="application",
subtype="pdf", filename=pdf.name)
return msg
def load_messages(csv_path="invoices.csv"):
"""全行を読み、PDFが無い行はエラーとして別に返す"""
messages, errors = [], []
with open(csv_path, encoding="utf-8", newline="") as f:
for row in csv.DictReader(f):
if not (PDF_DIR / row["PDFファイル"]).exists():
errors.append(f"PDFが見つかりません: {row['PDFファイル']}({row['宛名']})")
continue
messages.append(build_message(row))
return messages, errors
if __name__ == "__main__":
messages, errors = load_messages()
OUTBOX.mkdir(exist_ok=True)
for i, msg in enumerate(messages, 1):
# 送る前に .eml で保存(ダブルクリックでメールソフトで開ける)
eml = OUTBOX / f"{i:02d}_{msg['To']}.eml"
eml.write_bytes(bytes(msg))
print(f"[{i}] {msg['To']} / {msg['Subject']} / 添付: {msg.get_payload()[1].get_filename()}")
for e in errors:
print("[注意]", e)
print(f"作成 {len(messages)}通・エラー {len(errors)}件 → {OUTBOX}/ を確認してください")
![実行結果:PDFが無い1件を[注意]として表示](https://python-beginner.blog/wp-content/uploads/2026/10/terminal_PDF1_20261015_090442822307.png)
わざと佐藤工務店さんのPDFを置かずに実行してみました。
2通だけ作られて、PDFが無い1件は[注意]として表示されました。ここで気づければ、PDFを置き忘れたまま送ってしまう事故を防げます😊
outbox/01_tanaka@example.com.eml を開くと、件名「【請求書】2026年10月分のご請求」、本文の宛名「株式会社タナカ商事 田中様」と「ご請求金額:165,000円(税込)」、添付の invoice_tanaka_202610.pdf がちゃんと入っていました。
ちなみに日本語の件名は、メールの中では =?utf-8?b?44CQ6KuL...?= のような形に自動で変換(エンコード)されています。EmailMessage を使うと、この文字化け対策を自分で書かなくてよいのがうれしいポイントです。
ステップ2:テスト用サーバーに送ってみる
次は送信です。ただ、いきなり取引先に届くと困るので、まずは練習用のメールサーバーを自分のPCで動かします。aiosmtpd を入れて、ターミナルをもう1つ開いて次を実行してください。
pip install aiosmtpd
python -m aiosmtpd -n -l localhost:8025
これで localhost:8025 に「受け取ったメールを画面に表示するだけ」のサーバーが立ちます。外には一切送られません。
送信スクリプトはこちらです。ポイントをまとめるとこんな感じです👇
connect()で、テスト用(localhost)と本番(Gmailなど)を環境変数で切り替える- パスワードはコードに書かず、環境変数
SMTP_USER/SMTP_PASSから読む - PDFが足りない行が1件でもあれば、1通も送らずに中止
- 送れたら
sent_log.txtに「宛先|件名」を1行追記。次に実行したとき、記録にあるものはスキップ
import os
import smtplib
import ssl
import time
from pathlib import Path
from make_mails import load_messages
SENT_LOG = Path("sent_log.txt") # 送信済みの宛先を1行ずつ記録
def connect():
host = os.environ.get("SMTP_HOST", "smtp.gmail.com")
port = int(os.environ.get("SMTP_PORT", "465"))
if host == "localhost": # 練習用のテストサーバー(ログイン不要)
return smtplib.SMTP(host, port)
context = ssl.create_default_context()
server = smtplib.SMTP_SSL(host, port, context=context)
# パスワードはコードに書かず、環境変数から読む
server.login(os.environ["SMTP_USER"], os.environ["SMTP_PASS"])
return server
def main():
sent = set(SENT_LOG.read_text(encoding="utf-8").splitlines()) if SENT_LOG.exists() else set()
messages, errors = load_messages()
if errors:
print("PDFが足りない行があるので、送信を中止します")
for e in errors:
print(" ", e)
return
with connect() as server:
for msg in messages:
key = f"{msg['To']}|{msg['Subject']}"
if key in sent:
print(f"スキップ(送信済み): {msg['To']}")
continue
server.send_message(msg)
with SENT_LOG.open("a", encoding="utf-8") as f:
f.write(key + "\n")
print(f"送信しました: {msg['To']}")
time.sleep(1) # 連続送信を避けて1秒あける
if __name__ == "__main__":
main()
環境変数をセットして実行します。
# Windows(PowerShell)
$env:SMTP_HOST = "localhost"
$env:SMTP_PORT = "8025"
python send_mails.py
# Mac / Linux
SMTP_HOST=localhost SMTP_PORT=8025 python send_mails.py

先ほどのPDFが足りない状態で実行すると、「PDFが足りない行があるので、送信を中止します」と表示されて、1通も送られませんでした。佐藤工務店さんのPDFを置いてから実行し直した結果がこちらです。
3通とも送れて、テスト用サーバーの画面には3通分のメールと、それぞれの Content-Disposition: attachment; filename="invoice_tanaka_202610.pdf" のような添付の情報が表示されました。
では、もう一度同じコマンドを実行したらどうなるでしょう? 実行せずに答えられますか?
スキップ(送信済み): tanaka@example.com
スキップ(送信済み): suzuki@example.com
スキップ(送信済み): sato@example.com
正解は「全部スキップ」です。sent_log.txt にはこう記録されています。
tanaka@example.com|【請求書】2026年10月分のご請求
suzuki@example.com|【請求書】2026年10月分のご請求
sato@example.com|【請求書】2026年10月分のご請求
途中でエラーが出て止まったときも、送れたところまでは記録されているので、直して再実行すれば残りだけが送られます。「さっきの1通目、送れたっけ…?」と不安にならずに済むのが、この記録の役割です。
ステップ3:本番(Gmail)に切り替える
テストで問題なければ、本番のメールサーバーに切り替えます。Gmailの場合はこうです。
- サーバー:
smtp.gmail.com、ポート:465(SSL)。コードの初期値がこれになっています SMTP_USERは自分のGmailアドレス、SMTP_PASSはアプリパスワード(ふだんのログインパスワードではありません)- アプリパスワードは、Googleアカウントで2段階認証プロセスをオンにしてから作成します。会社・学校の管理アカウントなどでは、設定によって作れないこともあります
FROM_ADDRも自分のGmailアドレスに書き換えます
⚠️ 本番の前に、CSVの宛先を全部自分のアドレスにして1回送ってみるのがおすすめです。届いたメールの見た目と添付を確認してから、本物の一覧に戻しましょう。そのときは、テストで作った sent_log.txt を消してから本番を実行してくださいね(残っていると全部スキップされます)。
アプリパスワードの作り方は、Googleの公式ヘルプ「アプリ パスワードでログインする」にまとまっています。
まとめ
EmailMessageを使うと、日本語の件名・本文やPDF添付を短いコードで組み立てられる- 送る前に
.emlで保存しておけば、届いたときの見た目をメールソフトで確認できる - PDFが1件でも足りなければ送信を中止し、送れたものは記録して二重送信を防ぐ
- 練習は
aiosmtpdのテスト用サーバーで。本番は環境変数を切り替えるだけ
毎月の請求メールは、1回仕組みを作れば「CSVを更新して実行するだけ」になります。手作業のコピペミスが心配な方は、まずテスト用サーバーで動かすところから、ぜひ試してみてください😊
こちらも読まれています
📚 関連商品・おすすめ書籍
※本記事にはアフィリエイトリンクが含まれます。




