Amazonのアソシエイトとして、ラズパイダ(raspida.com)は適格販売により収入を得ています。詳しくは当サイトの プライバシーポリシーをご覧ください。

作業した元記事はこちら。

IMAPメールをモニターに表示する仕組み(Zero2/2w)

Raspberry Pi OS lite(以下、Lite)だとターミナルしかないため、日本語は表示も入力もできない。入力はしないとしても表示は困る。localeコマンドで確認すると、GB(イギリス)のまま。

LANG=en_GB.UTF-8
LANGUAGE=
LC_CTYPE="en_GB.UTF-8"
LC_NUMERIC="en_GB.UTF-8"
LC_TIME="en_GB.UTF-8"
LC_COLLATE="en_GB.UTF-8"
LC_MONETARY="en_GB.UTF-8"
LC_MESSAGES="en_GB.UTF-8"
LC_PAPER="en_GB.UTF-8"
LC_NAME="en_GB.UTF-8"
LC_ADDRESS="en_GB.UTF-8"
LC_TELEPHONE="en_GB.UTF-8"
LC_MEASUREMENT="en_GB.UTF-8"
LC_IDENTIFICATION="en_GB.UTF-8"
LC_ALL=

これをraspi-configで日本語(ja_JP.UTF-8)を選択する。

LANG=ja_JP.UTF-8
LANGUAGE=
LC_CTYPE="ja_JP.UTF-8"
LC_NUMERIC="ja_JP.UTF-8"
LC_TIME="ja_JP.UTF-8"
LC_COLLATE="ja_JP.UTF-8"
LC_MONETARY="ja_JP.UTF-8"
LC_MESSAGES="ja_JP.UTF-8"
LC_PAPER="ja_JP.UTF-8"
LC_NAME="ja_JP.UTF-8"
LC_ADDRESS="ja_JP.UTF-8"
LC_TELEPHONE="ja_JP.UTF-8"
LC_MEASUREMENT="ja_JP.UTF-8"
LC_IDENTIFICATION="ja_JP.UTF-8"
LC_ALL=

それから、日本語フォントも入っていない。後でconsole-setupをしたいが、Liteでも既に入っていた。

sudo apt install fonts-ipafont console-setup
sudo reboot

IPAフォントはfonts-ipafont-gothic fonts-ipafont-minchoの2つがインストールされる。

ついでにフォントキャッシュを削除しておくと良かった。(結構重要かも)

fc-cache -fv

再起動後に確認する。

fc-list : family file | grep -i ipa

こんな感じ。文字化けしているのは文字コードが違うからで問題無い。

/usr/share/fonts/opentype/ipafont-gothic/ipagp.ttf: IPA Pゴシック,IPAPGothic,䥐䅐䝯瑨楣
/usr/share/fonts/truetype/fonts-japanese-mincho.ttf: IPA明朝,IPAMincho
/usr/share/fonts/opentype/ipafont-mincho/ipamp.ttf: IPA P明朝,IPAPMincho,䥐䅐䵩湣桯
/usr/share/fonts/opentype/ipafont-gothic/ipag.ttf: IPAゴシック,IPAGothic
/usr/share/fonts/truetype/fonts-japanese-gothic.ttf: IPAゴシック,IPAGothic
/usr/share/fonts/opentype/ipafont-mincho/ipam.ttf: IPA明朝,IPAMincho

設定しておくと良いもの

最後でも良いけど、忘れないうちにcmdline.txtとconfig.txtに設定しておくことにします。

/boot/firmware/cmdline.txtに、次の3つを追加しました。

consoleblank=0 loglevel=3 vt.global_cursor_default=0

• consoleblank=0 → 画面が自動消灯しない • vt.global_cursor_default=0 →  カーソル非表示(見た目の問題) • loglevel=3 →  起動ログを抑制

/boot/firmware/config.txtには、最下段に次の3つを追加しました。

hdmi_force_hotplug=1
hdmi_group=2
hdmi_mode=82

• hdmi_force_hotplug=1 → モニター未検出でもHDMI有効(壁掛け用だから) • hdmi_group=2 → DMT(PCモニター系) • hdmi_mode=82 → 1920x1080 @ 60Hz

この辺は環境によって変えないとならない。

fbtermのインストールと設定

ここでfbtermをインストールしておく。恒久的に日本語フォントを使いたいから、設定ファイルにも書いておく。

sudo atp install fbterm
nano ~/.fbtermrc

元々あった設定をコメントアウトして、その下に追記した。 フォントをIPAゴシックにし、フォントサイズは最終的に44ポイントでちょうど良かった。

# font family names/pixelsize used by fbterm, multiple font family names must be seperated by ','
# and using a fixed width font as the first is strongly recommended
##font-names=mono
##font-size=12
font-names=IPAGothic
font-size=44

サイズを計算するのに次のコマンドで

stty size

仮に22 80と返ってきたら、縦22行、横80文字ということで、これはフォントサイズ48pt相当になる計算です。

一時的にサイズを試すなら、fbterm内ではなくTTY1に戻ってから次のコマンド。 fbtermは仮想端末だから、実行中に一時的に変えられない。

fbterm --font-names=IPAGothic --font-size=40

■Pi 5は8GBモデルがオススメ

IMAPの接続確認

例は独自ドメインIMAPなので誰でもというわけにはいきませんが、gmailであれば先にアプリパスワードも発行しておいてから、読み替えてください。 ターミナルで実行して、先に確認しておきます。

単に接続の確認だけ:

python3 - << 'EOF'
import imaplib
m = imaplib.IMAP4_SSL("xxxxxx.xserver.jp", 993)
print(m.noop())
m.logout()
EOF

OKと返ってくれば問題無い。('OK', [b'...'])

パスワードを用いたログイン確認:

python3 - << 'EOF'
import imaplib

HOST = "xxxxxx.xserver.jp"
PORT = 993
USER = "アカウント@メールドメイン"
PASS = "パスワード"

m = imaplib.IMAP4_SSL(HOST, PORT)
print(m.login(USER, PASS))
m.logout()
EOF

('OK', [b'NOOP completed'])と返れば良い。 CERTIFICATE_VERIFY_FAILEDと出た場合、自己署名の可能性があるので、それはPythonコード内で処理するから、テストでは問題無い。

成功した最終的なPythonコード

Python内で、パスワードなどは外に出した。 新規作成したconfigファイルに、次のように記述。

# ---- 設定 ----
IMAP_HOST=imap.domain.jp
IMAP_PORT=993
IMAP_USER=user@domail.com
IMAP_PASS=password

肝心のPythonコード

#!/usr/bin/env python3
import imaplib
import email
from email import policy
import time
import re
import sys

# ---- 設定 ----
from pathlib import Path

def load_config():
    config = {}
    for line in Path.home().joinpath(".imap_config").read_text().splitlines():
        if "=" in line:
            k, v = line.strip().split("=", 1)
            config[k] = v
    return config

cfg = load_config()

IMAP_HOST = cfg["IMAP_HOST"]
IMAP_PORT = int(cfg["IMAP_PORT"])
IMAP_USER = cfg["IMAP_USER"]
IMAP_PASS = cfg["IMAP_PASS"]
MAILBOX = "INBOX"
CHECK_INTERVAL = 60
MAX_LINES = 20

sys.stdout.reconfigure(encoding='utf-8')

def decode_body(msg):
    for part in msg.walk():
        if part.get_content_type() == "text/plain":
            charset = part.get_content_charset() or "utf-8"
            payload = part.get_payload(decode=True)
            try:
                return payload.decode(charset, errors="replace")
            except:
                return payload.decode("utf-8", errors="replace")
    return ""

def clean_text(text):

    lines = []

    for line in text.splitlines():
        stripped = line.strip()

        # 行頭が TEL のときだけ停止
        if re.match(r'^TEL[::]', stripped, re.IGNORECASE):
            break

        # URLは除外
        if re.search(r'https?://', line):
            continue

        line = line.strip()
        if line:
            lines.append(line)

    return lines[:MAX_LINES]

def clear_screen():
    print("\033[2J\033[H", end="")

def display_two(messages):
    clear_screen()

    for mail_id, subject, lines in messages:
        print("=" * 60)
        print(f"[件名] {subject}")
        print("-" * 60)
        for line in lines:
            print(line)
        print("=" * 60)
        print()

def fetch_latest_two():
    import ssl
    context = ssl.create_default_context()

    mail = imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT, ssl_context=context)
    mail.login(IMAP_USER, IMAP_PASS)
    mail.select(MAILBOX)

    status, data = mail.search(None, "ALL")
    if status != "OK":
        mail.logout()
        return None

    ids = data[0].split()
    if len(ids) < 1:
        mail.logout()
        return None

    latest_two = ids[-1:]  # ← ここ

    results = []

    for mail_id in reversed(latest_two):  # 最新を上に
        status, msg_data = mail.fetch(mail_id, "(RFC822)")
        if status != "OK":
            continue

        raw = msg_data[0][1]
        msg = email.message_from_bytes(raw, policy=policy.default)

        subject = msg.get("Subject", "")
        body = decode_body(msg)
        lines = clean_text(body)[:22]  # ← 22行制限

        results.append((mail_id, subject, lines))

    mail.logout()
    return results

def main():
    last_top_id = None

    while True:
        try:
            results = fetch_latest_two()
            if results:
                top_id = results[0][0]

                if top_id != last_top_id:
                    display_two(results)
                    last_top_id = top_id

        except Exception as e:
            clear_screen()
            print("IMAP ERROR:", e)

        time.sleep(CHECK_INTERVAL)

if __name__ == "__main__":
    main()

中でtwoとしているのは、2通のメールを上下に表示させるように改編したため。 実際に、次の箇所を変更すれあ2通表示にもできる。

    latest_two = ids[-1:]  # ← ここを-2で2通表示へ

自動起動させる

取りあえず問題無く動くだけで終了した。 あとは、これをサービスにして自動起動させるだけです。 最初にsystemdでサービスを作成したけど、結果的に上手くいかなかった。 fbtermが落ちてしまう。ttyの奪い合い? これじゃない。

上手くできたやり方: 自動ログインをraspi-configで有効にした。 ~/.bash_profileを新たに作成してループするようにした。

nano ~/.bash_profile

中身はこれだけでOKだった。

# ~/.bash_profile

# 壁掛けメール端末用
while true; do
    fbterm --font-names=IPAGothic --font-size=44 -- python3 /home/raspida/imap_display_single.py
    sleep 5
done

もしもこれでもエラーが出るなら、エラーをmail.logとして出力するのを追加して、あとで確認してみよう。

while true; do
    fbterm --font-names=IPAGothic --font-size=44 -- python3 /home/raspida/imap_display_single.py 2>&1 | tee ~/mail.log
    sleep 5
done

おっと、まだダメだ。 画面は出た。問題無い。だけど、SSH接続が接続できるがおかしい。

SSH接続すると、 stdin isn't a interactive tty!と出てCtrl+Cでやっとログインできた。

# ~/.bash_profile

# tty1 以外(SSHなど)では fbterm を起動しない
if [ "$(tty)" = "/dev/tty1" ]; then
    while true; do
        fbterm --font-names=IPAGothic --font-size=44 -- python3 /home/raspida/imap_display_single.py 2>&1 | tee ~/mail.log
        sleep 5
    done
fi

再び、画面は表示されプログラムは動いた。一応、エラー無くSSH接続もできました。 ただ、macOS側のターミナルで、プロンプトraspida@zero2:~$の表示が色分けされなくなった。 これはfbtermで入っているってこと?

AIに聞いてみた。

.bash_profile 内で 無限ループ + fbterm 起動 を書いている場合、
非ログインや SSH でログインしたシェルでも ループ前に PS1 や .bashrc の色設定がスキップされる ことがあります。
特に、SSH では .bashrc が読み込まれるのですが、.bash_profile の条件によっては .bashrc の設定が適用されなくなる場合があります。

なるほど。 最終的には次に変更して、プロンプトの色やエイリアス設定を維持、stdin isn’t a interactive tty! エラーを回避できた。

.bash_profileの最終形:

# ~/.bash_profile
# ----------------------------------------
# 壁掛けメール端末用 (tty1 専用)
# SSH 等の非 tty1 では通常シェルを維持

# 現在の端末を確認
current_tty=$(tty)

if [ "$current_tty" = "/dev/tty1" ]; then
    # fbterm + Python 無限ループで自動起動
    while true; do
        fbterm --font-names=IPAGothic --font-size=44 -- python3 /home/raspida/imap_display_single.py 2>&1 | tee ~/mail.log
        sleep 5
    done
else
    # 非 tty1(SSH 等)の場合は ~/.bashrc を読み込む
    [ -f ~/.bashrc ] && . ~/.bashrc
fi

以上、思いのほかスムーズに完成しました。