PR

pywin32でOutlookからメールを自動送信する方法|Excelデータ差し込みで一括送信【Python開発記③】

Python

NINJIN Mail制作記 第3回

スポンサーリンク

pywin32でOutlookからメールを自動送信する|Excelデータの差し込みと一括送信の実装

こんにちは、ニンジンです🥕

この記事では、Pythonの pywin32win32com.client)から Outlook を操作し、Excelの名簿データを1件ずつ差し込んだメールを一括送信する方法を解説します。全3回でお届けしてきた「NINJIN Mail」開発記の最終回です。

本文の差し込みには文字列メソッド .format()、名簿の1行分をまとめて扱うには row.to_dict() を使います。送信は、テスト段階では画面を開くだけの mail.Display()、本番では実送信する mail.Send() に切り替えられるようにします。

第1回(pandasでExcelを読み込む)・第2回(filedialogでファイルを選ぶ)のコードを土台にしていますが、差し込みと送信の部分だけを読みたい方でも追えるよう、要点は都度おさらいします。

この記事で分かること

  • GUIの雛形({会社名} {氏名} 様 など)にExcelのデータを差し込む .format() の使い方
  • row.to_dict() でExcelの列(役職・部署名など)が増えてもコード修正が不要になる設計
  • pywin32(win32com.client)でOutlookを起動し、メールを作成・送信するコード
  • 本番送信 mail.Send() とテスト用 mail.Display() の使い分け
  • 送信中・完了でボタンの色を変えて進捗を見せるUIの工夫
  • 第1回〜第3回を組み合わせて完成する、メール自動送信ツールの全体像

スポンサーリンク

本文へのデータ差し込み処理を実装する|.format() と row.to_dict() の活用

GUIで入力したテンプレートに、Excelのデータを差し込む処理を作ります。例えばGUI側で「{氏名} 様」という雛形を用意しておき、そこにExcelの「山田 太郎」といったデータを当てはめるイメージです。

このような文字の差し込みは、Pythonの文字列操作メソッド .format() で実現できます。テンプレートとデータを組み合わせることで、メール本文を自動生成します。

氏名・アドレス・会社名の列を持つ、第1回と同じ名簿Excelファイル

▲ 氏名・アドレス・会社名は、第1回と同じ名簿Excelを使う

名簿の列名(氏名・アドレス・会社名)が、そのままテンプレートの {氏名} などに対応します。

サンプルコード:mail-soft-test04.py(第2回のGUIに、本文差し込みのテスト出力を追加したもの)

import pandas as pd
import tkinter as tk
from tkinter import filedialog, scrolledtext, messagebox
import os
import json

CONFIG_FILE = "config.json"

# --- 設定ファイルの読み書き(前回パスの取得用) ---
def load_config():
    if os.path.exists(CONFIG_FILE):
        with open(CONFIG_FILE, "r", encoding="utf-8") as f:
            return json.load(f)
    return {"last_path": "", "last_file": "未選択"}

def save_config(file_path):
    with open(CONFIG_FILE, "w", encoding="utf-8") as f:
        json.dump({"last_path": os.path.dirname(file_path), "last_file": file_path}, f, ensure_ascii=False, indent=4)

# --- メインGUIクラス ---
class MailerApp:
    def __init__(self, root):
        self.root = root
        self.root.title("展示会メール作成ツール Pro")
        self.root.geometry("700x600")

        # 設定読み込み
        config = load_config()
        self.current_file_path = config.get("last_file", "未選択")

        # 1. ファイル選択エリア
        file_frame = tk.LabelFrame(self.root, text=" 1. 名簿ファイルの指定 ", padx=10, pady=10)
        file_frame.pack(padx=20, pady=10, fill="x")

        self.lbl_path = tk.Label(file_frame, text=f"現在のファイル: {self.current_file_path}", fg="blue", wraplength=500, justify="left")
        self.lbl_path.pack(side="left", padx=5)

        btn_select = tk.Button(file_frame, text="ファイル変更", command=self.select_file)
        btn_select.pack(side="right")

        # 2. 本文入力エリア
        body_frame = tk.LabelFrame(self.root, text=" 2. 本文雛形の作成 ", padx=10, pady=10)
        body_frame.pack(padx=20, pady=10, fill="both", expand=True)

        tk.Label(body_frame, text="{氏名} {会社名} が自動で置換されます").pack(anchor="w")
        self.txt_body = scrolledtext.ScrolledText(body_frame, font=("MS Gothic", 10), height=15, undo=True)
        self.txt_body.pack(fill="both", expand=True, pady=5)
        
        default_text = "{会社名}\n{氏名} 様\n\n昨日はありがとうございました。"
        self.txt_body.insert(tk.END, default_text)

        # 3. 操作ボタンエリア
        btn_frame = tk.Frame(self.root)
        btn_frame.pack(pady=20)

        self.btn_exec = tk.Button(btn_frame, text=" 一括作成(テスト出力) ", command=self.execute, bg="#0078d4", fg="white", width=25, height=2)
        self.btn_exec.pack(side="left", padx=10)

        self.btn_quit = tk.Button(btn_frame, text=" 終了 ", command=self.root.destroy, width=10, height=2)
        self.btn_quit.pack(side="left", padx=10)

    def select_file(self):
        config = load_config()
        initial_dir = config.get("last_path", os.path.expanduser("~"))
        
        file_path = filedialog.askopenfilename(title="Excelを選択", initialdir=initial_dir, filetypes=[("Excel", "*.xlsx")])
        
        if file_path:
            self.current_file_path = file_path
            self.lbl_path.config(text=f"現在のファイル: {file_path}")
            save_config(file_path)

    def execute(self):
        if not os.path.exists(self.current_file_path):
            messagebox.showerror("エラー", "Excelファイルが選択されていないか、存在しません。")
            return

        template = self.txt_body.get("1.0", tk.END).strip()
        
        try:
            df = pd.read_excel(self.current_file_path)
            print("\n" + "="*50 + "\n 送信プレビュー \n" + "="*50)
            for _, row in df.iterrows():
                print(f"TO: {row['アドレス']}")
                print(template.format(**row.to_dict()))
                print("-" * 30)
            messagebox.showinfo("完了", f"{len(df)}件のプレビューをターミナルに出力しました。")
        except Exception as e:
            messagebox.showerror("エラー", f"処理に失敗しました:\n{e}")

if __name__ == "__main__":
    root = tk.Tk()
    app = MailerApp(root)
    root.mainloop()

※ VS Codeなどのエディターでサンプルコードを実行してください。

展示会メール作成ツールProのGUI画面。右上にファイル変更ボタンがある

▲ 実行するとGUIが立ち上がる。まず右上の「ファイル変更」をクリック

コードを実行すると、画像のGUIが立ち上がります。名簿を指定するため、右上の「ファイル変更」をクリックします。

Windowsのファイル選択ダイアログでExcelファイルを選んでいる画面

▲ ファイル選択ウィンドウで対象のExcelを選び「開く」

「ファイルを開く」ウィンドウが立ち上がります。対象のExcelファイルを選択し、「開く」をクリックします。

ファイル選択後のGUI画面。本文雛形に会社名と氏名の差し込み箇所がある

▲ 「一括作成(テスト出力)」を押すと {会社名} {氏名} の箇所にExcelデータが入る

ファイルが選択され、GUIに戻ります。「一括作成(テスト出力)」を押すと、本文内の {会社名} {氏名} の箇所にExcelのデータが差し込まれます。

処理完了を知らせる「完了」ダイアログ

▲ 出力に成功すると「完了」のダイアログが表示される

差し込みの出力に成功すると、「完了」のダイアログが表示されます。

VS Codeのターミナルに差し込み済みのメール本文プレビューが出力された画面

▲ VS Codeのターミナルに差し込み結果が出力されれば成功

VS Codeのターミナルに、宛先アドレスと差し込み済みの本文が出力されれば 成功です。

row.to_dict()で項目追加にも対応できる設計に

差し込み処理を作る際、「項目が増えるたびにプログラムを書き直す必要がある」という問題が出てきます。例えば「役職」や「部署名」といった列をExcelに追加するたびに、コードを修正するのは手間です。

そこで活用したのが row.to_dict() です。これを使うと、Excelの1行分のデータを「項目名=値」のセットとしてまとめて扱えます。結果として、テンプレート側に {役職}{部署名} と記述するだけで、対応するデータが自動的に差し込まれます。

つまり、Excelの列を増やしてもプログラム側の修正は不要になり、メンテナンス性の高い設計になります。


スポンサーリンク

pywin32(win32com.client)でOutlookを操作してメールを送信する

ここでは、メール送信の自動化を実装します。使用するのは pywin32 です。これにより、PythonからOutlookを直接操作できるようになります。

今回のポイントは、これまで作ってきた execute メソッドの中身を、実際のOutlook操作に置き換えることです。処理の流れ自体は変えず、中身だけを実際の送信処理に差し替えるイメージです。

Outlookの操作自体は比較的シンプルで、想定していたよりも少ないコードで実装できます。

サンプルコード:mail-soft-test05.py(件名の入力欄と、送信中・完了でボタンの色が変わる表示を追加)

import pandas as pd
import tkinter as tk
from tkinter import filedialog, scrolledtext, messagebox
import os
import json
import win32com.client  # Outlook操作用
import time

CONFIG_FILE = "config.json"

def load_config():
    if os.path.exists(CONFIG_FILE):
        with open(CONFIG_FILE, "r", encoding="utf-8") as f:
            return json.load(f)
    return {"last_path": "", "last_file": "未選択"}

def save_config(file_path):
    with open(CONFIG_FILE, "w", encoding="utf-8") as f:
        json.dump({"last_path": os.path.dirname(file_path), "last_file": file_path}, f, ensure_ascii=False, indent=4)

class MailerApp:
    def __init__(self, root):
        self.root = root
        self.root.title("展示会メール作成ツール Pro")
        self.root.geometry("700x600")

        config = load_config()
        self.current_file_path = config.get("last_file", "未選択")

        # 1. ファイル選択
        file_frame = tk.LabelFrame(self.root, text=" 1. 名簿ファイルの指定 ", padx=10, pady=10)
        file_frame.pack(padx=20, pady=10, fill="x")
        self.lbl_path = tk.Label(file_frame, text=f"現在のファイル: {self.current_file_path}", fg="blue", wraplength=500, justify="left")
        self.lbl_path.pack(side="left", padx=5)
        btn_select = tk.Button(file_frame, text="ファイル変更", command=self.select_file)
        btn_select.pack(side="right")
        
        # 2. 件名入力エリア
        subject_frame = tk.LabelFrame(self.root, text=" 2. 件名の入力 ", padx=10, pady=10)
        subject_frame.pack(padx=20, pady=10, fill="x")

        tk.Label(subject_frame, text="件名を入力してください").pack(anchor="w")
        self.txt_subject = tk.Entry(subject_frame, font=("MS Gothic", 10), width=50)
        self.txt_subject.pack(side="left", padx=5)

        # 3. 本文入力エリア
        body_frame = tk.LabelFrame(self.root, text=" 3. 本文雛形の作成 ", padx=10, pady=10)
        body_frame.pack(padx=20, pady=10, fill="both", expand=True)
        tk.Label(body_frame, text="{氏名} {会社名} が自動で置換されます").pack(anchor="w")
        self.txt_body = scrolledtext.ScrolledText(body_frame, font=("MS Gothic", 10), height=15, undo=True)
        self.txt_body.pack(fill="both", expand=True, pady=5)
        
        default_text = "{会社名}\n{氏名} 様\n\n昨日はありがとうございました。"
        self.txt_body.insert(tk.END, default_text)

        # 4. 操作ボタン
        btn_frame = tk.Frame(self.root)
        btn_frame.pack(pady=20)
        # 初期状態は青
        self.btn_exec = tk.Button(btn_frame, text=" 一括作成(実行) ", command=self.execute, bg="#0078d4", fg="white", width=25, height=2)
        self.btn_exec.pack(side="left", padx=10)
        self.btn_quit = tk.Button(btn_frame, text=" 終了 ", command=self.root.destroy, width=10, height=2)
        self.btn_quit.pack(side="left", padx=10)

    def select_file(self):
        config = load_config()
        initial_dir = config.get("last_path", os.path.expanduser("~"))
        file_path = filedialog.askopenfilename(title="Excelを選択", initialdir=initial_dir, filetypes=[("Excel", "*.xlsx")])
        if file_path:
            self.current_file_path = file_path
            self.lbl_path.config(text=f"現在のファイル: {file_path}")
            save_config(file_path)
            # ファイルを変えたらボタンの色を戻す
            self.reset_button()

    def reset_button(self):
        self.btn_exec.config(text=" 一括作成(実行) ", state="normal", bg="#0078d4", fg="white")

    def execute(self):
        if not os.path.exists(self.current_file_path):
            messagebox.showerror("エラー", "Excelファイルが選択されていません。")
            return

        # ボタンを「送信中」状態にする(黄色・無効化)
        self.btn_exec.config(text="送信中...", state="disabled", bg="#ffc107", fg="black")
        self.root.update()

        template = self.txt_body.get("1.0", tk.END).strip()
        
        try:
            # Outlookの起動
            outlook = win32com.client.Dispatch("Outlook.Application")
            df = pd.read_excel(self.current_file_path)
            
            count = 0
            for _, row in df.iterrows():
                # メール作成
                mail = outlook.CreateItem(0)
                mail.to = row['アドレス']
                mail.Subject = self.txt_subject.get().strip() or "(件名なし)"
                
                # 差し込み処理
                mail.Body = template.format(**row.to_dict())
                
                # 送信(テスト時は .Display() に変えると画面が立ち上がるだけで止まる)
                mail.Send() 
                count += 1
                time.sleep(0.5) # 連続送信による負荷軽減

            # 完了状態にする(緑色)
            self.btn_exec.config(text=f"送信完了({count}件)", state="normal", bg="#28a745", fg="white")
            messagebox.showinfo("完了", f"全 {count} 件の送信を完了しました。\nOutlookの送信済みアイテムを確認してください。")

        except Exception as e:
            self.reset_button()
            messagebox.showerror("エラー", f"処理に失敗しました:\n{e}")

if __name__ == "__main__":
    root = tk.Tk()
    app = MailerApp(root)
    root.mainloop()

※ VS Codeなどのエディターでサンプルコードを実行してください。

件名入力欄が追加された展示会メール作成ツールProのGUI画面

▲ 実行するとGUIが立ち上がる。件名の入力欄が追加されている

コードを実行すると、画像のGUIが立ち上がります。件名の入力欄が追加されています。まず右上の「ファイル変更」をクリックします。

Windowsのファイル選択ダイアログでExcelファイルを選んでいる画面

▲ ファイル選択ウィンドウで対象のExcelを選び「開く」

「ファイルを開く」ウィンドウが立ち上がります。対象のExcelファイルを選択して、「開く」をクリックします。

ファイルと件名を入力し、一括作成(実行)ボタンを押す直前のGUI画面

▲ 件名を入力し「一括作成(実行)」をクリックして送信

選択されたExcelファイルが表示されます。件名に文字を入力し、「一括作成(実行)」ボタンをクリックして送信します。

送信完了を知らせる「完了」ダイアログ

▲ 送信に成功すると「完了」のダイアログが表示される

メール送信に成功すると、「完了」のダイアログが表示されます。

Outlookの送信済みアイテムフォルダに送信したメールが並んでいる画面

▲ Outlookの送信済みアイテムに送信記録が残る

Outlookを開いて「送信済みアイテム」を確認すると、送信記録が残っています。

実装のポイント

今回の処理の中で、押さえておきたいポイントをいくつか紹介します。

Outlook操作の準備

import win32com.client

これが、Outlookを操作するためのライブラリです。いわば「PythonからOutlookを動かすための入り口」になります。

実行中の状態を分かりやすくする

self.btn_exec.config(...)

execute が呼ばれたタイミングで、ボタンの表示を変更しています。実行中は黄色(送信中)、完了後は緑(完了)です。処理の進行状況が一目で分かるので、使う人にとっても安心感があります。

実際のメール送信:mail.Send() と mail.Display()

mail.Send()

ここが実際にメールを送信する本番処理です。ただし、いきなり送信するのが不安な場合は、テスト方法も用意できます。

mail.Display()

こちらに書き換えると、メールは送信されず、Outlookの新規メール画面が開くだけになります。差し込み内容の確認など、テスト段階ではこちらを使うのがおすすめです。


スポンサーリンク

第1回〜第3回を組み合わせて完成する自動化フロー

第1回から第3回までのコードを組み合わせると、以下の流れが実現できます。

  1. Excelファイルからデータを読み込む(第1回:pandas)
  2. GUIでメール本文の雛形を作成する(第2回:tkinter)
  3. データを差し込んで本文を自動生成する(.format() / row.to_dict()
  4. Outlookを操作してメールを送信する(pywin32:mail.Send()

ここまでで、メール自動送信ツールの基本部分は完成しています。

実務で使うために追加した機能|ブログ版と販売版(NINJIN Mail)の違い

ここまでの機能でも一定の効果はありますが、実際の業務で使うには、もう一歩踏み込んだ使いやすさと安全性が必要です。そこで、販売しているNINJIN Mailでは、以下の機能を追加・強化しています。

① 機能面のアップグレード

  • UIのモダン化:tkinter から customtkinter に刷新し、ダークモードにも対応。より直感的で使いやすいデザインに改善
  • テンプレート保存(最大3パターン):タブで切り替えながら複数の本文を管理可能。終了時も自動保存
  • 署名管理機能:本文とは別に署名を管理でき、すべてのメールに自動で付与
  • 共通添付ファイル機能:複数ファイルをまとめて添付し、全宛先に一括送信
  • 送信前プレビュー:「最初の1通のみ確認」機能により、実際の差し込み結果を事前チェック
  • 送信ログの記録(目玉機能):Excelに「送信日」「結果(成功/エラー)」「使用テンプレート」などを自動で書き戻し

② エラー対策・安全性の強化

  • メールアドレスの形式チェック:不正なアドレスは送信前に検知して警告
  • 空欄データへの対応:氏名未入力の警告や、空欄項目を安全に処理する仕組みを追加
  • 二重送信の防止:ファイルが開かれている場合でも「RECOVERY」として保存し、データ消失を防止
  • 送信間隔の調整:time.sleep(0.5) により負荷を分散し、フリーズやエラーを防止
  • 設定ファイルの耐障害性向上:config.json の不足項目を自動補完し、エラーを防止

PR

今回紹介したコードを含む開発の詳細は、noteでフルコード付きで販売しています。コードに関心がある方も、「コードは難しそう」と感じる方も、内容だけでも覗いてみてください。


スポンサーリンク

まとめ

pywin32(win32com.client)でOutlookを操作し、Excel名簿から差し込みメールを一括送信するツールを、第1回〜第3回で完成させました。差し込みは .format()row.to_dict()、送信はテスト用の mail.Display() と本番の mail.Send() を切り替える構成です。

大手企業なら専用システムがありますが、中小企業の営業現場では、今も手作業で1件ずつコピペしている方がたくさんいます。「そんな方々の苦労を、自分の技術で少しでも減らしたい」——そんな想いでこのツールを作りました。販売実績については、また期間を置いて報告する予定です。

PR

販売版NINJIN Mailのメイン画面。customtkinterでモダンにしたGUI

▲ 販売版 NINJIN Mail(customtkinterでUIを刷新)

実際にnoteで販売中です。フルコード付きで、環境構築なしですぐ使えるパッケージ版も用意しています。コードに興味がある方も、コードは面倒だと感じる方も、よければ覗いてみてください。

👉 noteでチェックする

次に読む記事


スポンサーリンク

よくある質問(pywin32・Outlook・差し込みメール)

Qwin32com.client を import しようとすると ModuleNotFoundError になります

Apywin32 が未インストールです。ターミナルで pip install pywin32 を実行してください。Anaconda環境の場合は conda install pywin32 でも入ります。インストール後もエラーが出るときは、python -m pywin32_postinstall -install を一度実行すると解決することがあります。

Qmail.Send() を実行してもメールが送られず、送信トレイに溜まります

AOutlookが起動していない、またはオフライン作業モードになっている可能性があります。Outlookを起動し、「送受信」タブでオフライン作業が解除されているか確認してください。企業環境ではセキュリティ設定でプログラムからの送信がブロックされ、確認ダイアログが出ることもあります。

Q送信したメール本文の日本語が文字化けします

Amail.Body ではなくプレーンテキストで渡していれば通常は文字化けしません。HTMLで送りたい場合は mail.HTMLBody を使い、テンプレート文字列がUTF-8で扱われているか確認します。Excelの読み込み時に想定と違う文字コードになっていないかも合わせてチェックしてください。

Q.format() で KeyError が出ます

Aテンプレートに書いた {部署名} などの項目名が、Excelの列名(見出し行)と1文字でも違うと KeyError になります。全角スペースや末尾の空白も原因になります。row.to_dict() の中身を print で確認し、列名とテンプレートの表記を完全に一致させてください。

QOutlookを使っていません。GmailやThunderbirdでも送れますか

Aこの方法はOutlook専用です(win32com.client でOutlookアプリを操作しているため)。Gmailなどで送る場合は、Python標準の smtplib と email ライブラリ、またはGmail APIを使う実装に置き換える必要があります。


ニンジン🥕アイコン

Follow me on X

本ブログの中の人「ニンジン🥕」はXやってます。新着記事はいち早くポストでお知らせしているので、見逃したくない方は @ninjin_py_vba をフォローしてみてください🥕

@ninjin_py_vba をフォロー

この記事が役に立ったら、シェアしてもらえると嬉しいです🥕

Xでポスト はてブ LINE Pocket

PROJECT LOG — SERIES INDEX
スポンサーリンク

シリーズ記事一覧

電子工作・自動化ツールの製作記録をシリーズごとにまとめています。気になるテーマからお読みください。


超音波距離センサーHC-SR04をRaspberry Pi Picoで使う|仕組み・配線・精度検証
.uds1-block { position: relative; overflow: hidden; border-radius: 12px; border: 1px solid #f0cba8; background: linear-g…
今週の注目ガジェット〜09/07週〜|電子工作・自作向け新製品まとめ(TickrCast/Radxa Linkr ほか)
ニンジンのガジェット通信 09/07週。ESP32-P4の自作気象モニター、極小IP-KVM「Radxa Linkr」、ESP32のDINレール型PLC、LTE開発キットなど、電子工作で使えそうな新製品5つを解説。
サーボモータの角度をタクトスイッチで可変|OLED(SSD1306)に現在角度をリアルタイム表示
Raspberry Pi Pico 2W×MG996RにタクトスイッチとOLED(SSD1306)を追加。ボタンで角度を可変しながら現在角度をリアルタイム表示するMicroPython実装を、framebufの文字拡大やサーボの遊び対策まで実機ベースで解説します。
MG996RサーボモーターをRaspberry Pi Pico 2Wで動かす:PWM制御とキャリブレーション実践
MG996RサーボモーターをRaspberry Pi Pico 2Wで動かす方法を解説。公称180度のはずが実測198度だった個体差の原因究明から、ホーンのズレ補正、タクトスイッチでの角度制御まで実機検証の様子を紹介します。
Tapo P110Mの消費電力をExcelレポート化する【年次集計・電気代試算・セキュリティ判断編】
FastAPI+openpyxlでTapo P110Mの消費電力をExcelレポート化。15秒間隔の生ログを日次集計する設計や、31円/kWhでの電気代試算、外部公開を見送った理由まで解説します。

コメント