先週のブログGmailのPOPサポート終了に対応する為のツールをAIに作ってもらったの続きです。
先週のブログで書いたように、昨日に受信したメールの一覧のメールを送信するツールをPythonで作りましたが不満があり通常のテキストメールではなくHTMLメールを送りたいと思いました。
Nano Banana Proが生成した画像を使っています
テキストメールは読みにくいね
先週作ったツールから送られてくるメールは以下のようになっています(画像はぼかしています)。メールは時間 送信者 件名の一覧(表)で、縦方向もキレイに並ぶように少しだけ努力していますが、正確には揃っていません。
また、送信者や件名は短く切っていますがスクロールして後半を見たりはできません。

そんな要望にはHTMLメールですね。以下の画像は今回作ったツールの送信してくるHTMLメールです。テーブルで縦方向も揃っていますし、送信者や件名はスクロールして後半も見れます。

PyJSX
さてHTMLを生成するにはテンプレートエンジンを使う事が多いと思います、Pythonでは以前取り上げた有名フレームワークDjyangoのDTL(Django Template Language)やJinja(Jinja2)が良く使われているようです。
しかし、最近はReactばかり書いてきた私はJSXが使いたいなぁ〜と思って検索したところPyJSXを発見しました❗
PyJSXの簡単な解説
PyJSXはPythonの中にHTMLを書けるJSX同様のライブラリーです。PyJSXのGitHubにあるExampleを使って簡単に解説しますね。
main.py
main.pyはJSXを使ったコードを呼出すコードです。
- ① このインポートを行うと以降でimportされるモジュール読み込み時にJSXをPythonに変換する処理が組み込まれます
import pyjsx.auto_setup # ← ①
from hello import hello
hello()
hello.py
hello.pyはJSXが書かれているPythonモジュールです。
- ② 先頭の
# coding: jsxはPythonのコーディング指定機能で通常は文字コードを指定しますが、ここではJSXライブラリーのTokenizerが動作しJSX文法を解釈し、さらにTranspilerが動作しJSXがPythonコードに変換されます - ③ ReactのJSX同様にPythonコード内に直接HTMLが書けます
- ④ ReactのJSX同様に
{}内にはPythonの式が書けます
# coding: jsx # ← ②
from pyjsx import jsx
def hello():
to = "world"
print(<h1>Hello, {to}!</h1>) # ← ③、④
したがって、繰り返しは以下のようにリスト内包表記を使って書けます。
<div>
{[<p>Row: {i}</p> for i in range(3)]}
</div>
↓ 結果
<div>
<p>Row: 0</p> <p>Row: 1</p> <p>Row: 2</p>
</div>
今回のコード
先週のコードは色々な部分を改良したので、軽く解説します。
メイン yesterday_mails.py
- ① このインポートは重要です
#!/usr/bin/env python3
import pyjsx.auto_setup # ← ①
from datetime import date, timedelta
from mail_utils import fetch_latest_emails, send_html_email
from jsx_render import make_html_mail
READ_MAIL_URL = "https://my-server.com/read_mail"
def main():
# 昨日のメール一覧を取得
yesterday = (date.today() - timedelta(days=1))
yesterday_mails = fetch_latest_emails(date=yesterday, num_emails=100)
yesterday_str = yesterday.isoformat()
# HTMLメールを生成
html = make_html_mail(yesterday, yesterday_mails, READ_MAIL_URL)
# print(html)
# メール一覧をメール送信
send_html_email(f"メール一覧 {yesterday_str}", html)
if __name__ == "__main__":
main()
HTMLメール作成 jsx_render.py
- ① このコーディング指定は重要です!
- ② 今回はスタイル情報はstyle属性で指定しています
- ここはReactと違い通常のCSSと同じ小文字・ハイフンで指定します
- ③ テーブルの幅指定には表の列グループ要素を使っています。初めて使いました😅
- ④ 将来は日付をクリックするとメール内容が読めるようにリンクを追加しました。まだサーバー側は実装されていません
- ⑤ リスト内包表記を使いテーブルの行を表示しています
- ⑥ JSX内にはスタイル情報
<style>を書けないので、文字列に変換した後でスタイル情報を埋め込んでいます - ⑦ JSXオブジェクトをHTML文字列に変換しています
⑥のスタイル指定ですが、tr:hoverはこのメールを確認する際のユーザビリティを格段に上げます。これは絶対に入れたかったのですがstyle属性では指定出来ないのでのでこのような形で実装しました。
# coding: jsx # ← ①
from pyjsx import jsx, JSX
def make_table(emails, read_mail_url):
table_style = { # ← ②
"width": "100%",
"border-collapse": "collapse",
"border": "1px solid #e0e0e0",
"table-layout": "fixed"
}
th_style = {
"padding": "6px 12px",
"text-align": "left",
"background-color": "#e0e0e0",
"border-bottom": "1px solid #e0e0e0"
}
td_style = {
"padding": "6px 12px",
"border-bottom": "1px solid #e0e0e0",
"color": "#555",
"overflow-x": "scroll",
"white-space": "nowrap"
}
a_style = {
"color": "#555",
"text-decoration": "none"
}
return (
<table style={table_style}>
<colgroup> { # ← ③ }
<col style={{"width": "10%"}} />
<col style={{"width": "30%"}} />
<col style={{"width": "55%"}} />
</colgroup>
<thead>
<tr>
<th style={th_style}>時刻</th>
<th style={th_style}>送信者</th>
<th style={th_style}>件名</th>
</tr>
</thead>
<tbody>
{
<tr>
<td style={td_style}>
<a href={f"{read_mail_url}/{email['uid']}"} style={a_style}>
{email['date'][11:]} # ← ④
</a>
</td>
<td style={td_style}>{email['from']}</td>
<td style={td_style}>{email['subject']}</td>
</tr>
for email in emails # ← ⑤
}
</tbody>
</table>
)
def make_html_mail(date_str, emails, read_mail_url):
html = (
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>メール一覧</title>
STYLE { # ← ⑥ }
</head>
<body style={{"margin": "20px"}}>
<h1>Date: {date_str}</h1>
{make_table(emails, read_mail_url)}
</body>
</html>
)
# JSXを文字列に変換
html_str = str(html) # ← ⑦
# <style>を差し込む # ← ⑥
html_with_style = html_str.replace("STYLE", """
<style>
tr:hover {background-color: #f8f9fa;}
</style>
""")
return html_with_style
メール受信、送信 mail_util.py
Pythonのimaplibは機能が豊富なわりに情報が少なく苦労しました。
- ① imaplibでは取得するメールの条件が指定できます、ここでは
ON 23-Dec-2025で昨日の日付を指定してメールを取得しています- 先週のコードでは最新の100件のメールを取得しプログラムで日付を比較して取得していましたね
- ② 将来の機能拡張に備え、メールのユーニークIDを取得するようにしました
import imaplib
import smtplib
import email
import os
from datetime import timedelta, timezone
from email.header import decode_header
from email.utils import parsedate_to_datetime
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
# IMAP/SMTP接続設定
IMAP_SERVER = os.getenv("IMAP_SERVER")
SMTP_SERVER = os.getenv("SMTP_SERVER")
IMAP_PORT = int(os.getenv("IMAP_PORT"))
SMTP_PORT = int(os.getenv("SMTP_PORT"))
EMAIL_ACCOUNT = os.getenv("EMAIL_ACCOUNT")
EMAIL_PASSWORD = os.getenv("EMAIL_PASSWORD")
def connect_to_imap():
"""IMAPサーバーに接続"""
mail = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT)
mail.login(EMAIL_ACCOUNT, EMAIL_PASSWORD)
return mail
def decode_mime_words(s):
"""MIMEエンコードされた文字列をデコード"""
if s is None:
return ''
decoded_fragments = decode_header(s)
fragments = []
for fragment, encoding in decoded_fragments:
if isinstance(fragment, bytes):
# エンコーディングを明示的に処理
if encoding:
try:
fragment = fragment.decode(encoding, errors='replace')
except:
fragment = fragment.decode('utf-8', errors='replace')
else:
# エンコーディングが不明な場合、複数試行
for enc in ['iso-2022-jp', 'utf-8', 'shift_jis', 'euc-jp']:
try:
fragment = fragment.decode(enc)
break
except:
continue
else:
fragment = fragment.decode('utf-8', errors='replace')
fragments.append(str(fragment))
return ''.join(fragments)
def convert_to_jst(date_str):
"""メールの日付を日本時間(JST)に変換"""
try:
# メールの日付文字列をdatetimeオブジェクトに変換
dt = parsedate_to_datetime(date_str)
# 日本時間(JST = UTC+9)に変換
jst = timezone(timedelta(hours=9))
dt_jst = dt.astimezone(jst)
# yyyy-mm-dd HH:MM:SS 形式で返す
return dt_jst.strftime('%Y-%m-%d %H:%M:%S')
except:
return date_str # 変換できない場合は元の文字列を返す
def fetch_latest_emails(mailbox='INBOX', date=None, num_emails=10):
"""
最新のメールを取得
Args:
mailbox: メールボックス名
date: 取得するメールの日付(datetime.dateオブジェクト)。Noneの場合はすべてのメールを対象
num_emails: 取得する最新メールの数
Returns:
メールのリスト(各メールは辞書形式)
"""
try:
# IMAPに接続
mail = connect_to_imap()
# メールボックスを選択
mail.select(mailbox)
# すべてのメールIDを検索 # ↓ ①
criterion = date and f'ON {date.strftime("%d-%b-%Y")}' or 'ALL'
status, messages = mail.uid('search', None, criterion) # ← ②
uids = messages[0].split()
# 最新のメールIDを取得(昇順で取得)
latest_uids = uids[-num_emails:] # [::-1]
emails = []
for uid in latest_uids:
# メールを取得
status, msg_data = mail.uid('fetch', uid, '(RFC822)')
for response_part in msg_data:
if isinstance(response_part, tuple):
# メールメッセージをパース
msg = email.message_from_bytes(response_part[1])
# 件名を取得
subject = decode_mime_words(msg['Subject']) if msg['Subject'] else 'No Subject'
# 送信者を取得
from_ = decode_mime_words(msg['From']) if msg['From'] else 'Unknown'
# 日付を取得して日本時間に変換
date = convert_to_jst(msg['Date']) if msg['Date'] else 'Unknown'
# 本文を取得
body = ""
if msg.is_multipart():
for part in msg.walk():
content_type = part.get_content_type()
content_disposition = str(part.get("Content-Disposition"))
if content_type == "text/plain" and "attachment" not in content_disposition:
payload = part.get_payload(decode=True)
if payload:
# 文字エンコーディングを取得
charset = part.get_content_charset()
if charset:
try:
body = payload.decode(charset, errors='replace')
except:
body = payload.decode('utf-8', errors='replace')
else:
# charsetが指定されていない場合、複数試行
for enc in ['iso-2022-jp', 'utf-8', 'shift_jis', 'euc-jp']:
try:
body = payload.decode(enc)
break
except:
continue
else:
body = payload.decode('utf-8', errors='replace')
break
else:
payload = msg.get_payload(decode=True)
if payload:
charset = msg.get_content_charset()
if charset:
try:
body = payload.decode(charset, errors='replace')
except:
body = payload.decode('utf-8', errors='replace')
else:
# charsetが指定されていない場合、複数試行
for enc in ['iso-2022-jp', 'utf-8', 'shift_jis', 'euc-jp']:
try:
body = payload.decode(enc)
break
except:
continue
else:
body = payload.decode('utf-8', errors='replace')
emails.append({
'uid': uid.decode(),
'subject': subject,
'from': from_,
'date': date,
'body': body[:200] # 本文の最初の200文字のみ
})
print(f"-- Get email: {len(emails)}")
return emails
except imaplib.IMAP4.error as e:
print(f"IMAP Error : {e}")
return []
except Exception as e:
print(f"Error : {e}")
return []
finally:
mail.close() # IMAPに接続を閉じる
mail.logout()
def send_html_email(subject, body):
"""
メールを送信する
Args:
to_email: 送信先メールアドレス(文字列またはリスト)
subject: 件名
body: 本文(html)
Returns:
成功した場合はTrue、失敗した場合はFalse
"""
try:
to_email = EMAIL_ACCOUNT
msg = MIMEMultipart('alternative')
msg['From'] = EMAIL_ACCOUNT
msg['To'] = to_email
msg['Subject'] = subject
# HTMLパートを追加
html_part = MIMEText(body, 'html', 'utf-8')
msg.attach(html_part)
# SMTPサーバーに接続
server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
server.starttls() # TLS暗号化を開始
# ログイン
server.login(EMAIL_ACCOUNT, EMAIL_PASSWORD)
# メールを送信
server.sendmail(EMAIL_ACCOUNT, to_email, msg.as_string())
print(f"-- Send email: {to_email}")
return True
except smtplib.SMTPException as e:
print(f"SMTP Error : {e}")
return False
except Exception as e:
print(f"Error : {e}")
return False
finally:
server.quit() # 接続を閉じる
まとめ
PyJSXを使う事で、無事にPythonでもJSXを使う事ができました。
JSXは現在ではReact以外 Solidや、Qwik、Inferno等のフロントエンド・フレームワークでも採用されています、Reactの次に使われているVue.jsでもプラグインをインストールするとJSXが使えます。
JavaScrip言語以外でも、今回のPython言語のPyJSXや、Ruby言語(Ruby on Rails)にもruxというJSXライブラリーがあります。
色々な言語・フレームワークでJSXが使えるようになるのは良いですね。😄











