mineoメールを24時間自動転送してみよう

私は普段、各サービスのメールをGmailで一括管理するようにしています(複数のメーラを使うのが面倒なので)。

が、当該サービスの終了のお知らせがアナウンスされたため、今回対応することに。

大概のものはGmailのメアドに転送するようにしたのですが、なんとmineoメールには転送機能がありません

PCを立ち上げている間だけの転送で良ければ、普通にメーラで実現出来ると思います。が、24時間回すファンレスなモノにしたかったこともあり、ESP32評価ボード(アフィリエイトリンクです) で作ってみることに(最初は Raspberry Pi で実装しようと思ったのですが、4Bとか、お高いんですもの)。

久し振りにいきなりコードを書き始めるのもしんどいので、Claude君(無償!w)にあーでもないこーでもないと相談しながら作ったコードが以下のものです(Claude君には転載許可を得ています)。

#include <WiFi.h>
#include <WiFiClient.h>
#include <Preferences.h>

// =====================================================
// 設定
// =====================================================
#define WIFI_SSID      "your_ssid"
#define WIFI_PASSWORD  "your_wifi_pass"

#define IMAP_HOST      "imap.mineo.jp"
#define IMAP_PORT      143
#define IMAP_USER      "your_address@mineo.jp"
#define IMAP_PASS      "your_password"

#define SMTP_HOST      "smtpauth.mineo.jp"
#define SMTP_PORT      587
#define SMTP_USER      "your_address@mineo.jp"
#define SMTP_PASS      "your_password"
#define FORWARD_TO     "destination@example.com"

#define CHECK_INTERVAL    (30 * 60 * 1000UL)  // 30分毎にチェック
#define NVS_NAMESPACE     "mail_fwd"
#define NVS_COUNT_KEY     "uid_count"
#define MAX_STORED_IDS    100
#define CHECK_LATEST      20
#define MAX_BODY_BYTES    (20 * 1024 * 1024)  // 転送上限20MB

// =====================================================
// グローバル
// =====================================================
static WiFiClient  g_imap_tcp;
static WiFiClient  g_smtp_tcp;
static Preferences prefs;
static int         g_tagNum = 1;

static String g_storedUIDs[MAX_STORED_IDS];
static int    g_storedCount = 0;

// =====================================================
// ログ
// =====================================================
typedef enum { LOG_INFO, LOG_WARN, LOG_ERROR } LogLevel;
static void logPrint(LogLevel lv, const char* msg) {
    static const char* pfx[] = {"[INFO] ","[WARN] ","[ERROR] "};
    Serial.print(pfx[lv]); Serial.println(msg);
}
static void logPrint(LogLevel lv, const String &msg) { logPrint(lv, msg.c_str()); }

// =====================================================
// NVS
// =====================================================
static void loadStoredUIDs() {
    prefs.begin(NVS_NAMESPACE, true);
    g_storedCount = prefs.getInt(NVS_COUNT_KEY, 0);
    for (int i = 0; i < g_storedCount; i++) {
        String k = "uid_" + String(i);
        g_storedUIDs[i] = prefs.getString(k.c_str(), "");
    }
    prefs.end();
    logPrint(LOG_INFO, "NVS 保存済みUID件数: " + String(g_storedCount));
}
static bool isProcessed(const String &uid) {
    for (int i = 0; i < g_storedCount; i++)
        if (g_storedUIDs[i] == uid) return true;
    return false;
}
static void saveUID(const String &uid) {
    if (g_storedCount >= MAX_STORED_IDS) {
        for (int i = 0; i < g_storedCount - 1; i++)
            g_storedUIDs[i] = g_storedUIDs[i+1];
        g_storedCount--;
    }
    g_storedUIDs[g_storedCount++] = uid;
    prefs.begin(NVS_NAMESPACE, false);
    prefs.putInt(NVS_COUNT_KEY, g_storedCount);
    for (int i = 0; i < g_storedCount; i++) {
        String k = "uid_" + String(i);
        prefs.putString(k.c_str(), g_storedUIDs[i]);
    }
    prefs.end();
    logPrint(LOG_INFO, "NVS UID保存: " + uid);
}
// static void clearAllUIDs() {
//     prefs.begin(NVS_NAMESPACE,false); prefs.clear(); prefs.end();
//     g_storedCount=0; logPrint(LOG_WARN,"NVS全削除");
// }

// =====================================================
// WiFi
// =====================================================
static void connectWiFi() {
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    logPrint(LOG_INFO, "WiFi接続中...");
    int retry = 0;
    while (WiFi.status() != WL_CONNECTED) {
        delay(500); Serial.print(".");
        if (++retry > 40) { Serial.println(); logPrint(LOG_ERROR,"WiFi失敗。再起動"); ESP.restart(); }
    }
    Serial.println();
    logPrint(LOG_INFO, "WiFi接続完了: " + WiFi.localIP().toString());
}

// =====================================================
// Base64
// =====================================================
static String b64enc(const String &s) {
    static const char* t = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    String r;
    const int len = s.length();
    r.reserve(((len + 2) / 3) * 4 + 2);
    for (int i = 0; i < len; i += 3) {
        uint8_t a = (uint8_t)s[i];
        uint8_t b = (i+1 < len) ? (uint8_t)s[i+1] : 0;
        uint8_t c = (i+2 < len) ? (uint8_t)s[i+2] : 0;
        r += t[a >> 2];
        r += t[((a & 3) << 4) | (b >> 4)];
        r += (i+1 < len) ? t[((b & 0xF) << 2) | (c >> 6)] : '=';
        r += (i+2 < len) ? t[c & 0x3F]                    : '=';
    }
    return r;
}
static int b64val(char c) {
    if (c>='A'&&c<='Z') return c-'A';
    if (c>='a'&&c<='z') return c-'a'+26;
    if (c>='0'&&c<='9') return c-'0'+52;
    if (c=='+') return 62; if (c=='/') return 63;
    return -1;
}
static String b64dec(const String &src) {
    String r; r.reserve(src.length()*3/4+4);
    int len=src.length();
    for(int i=0;i+3<len;i+=4){
        int v0=b64val(src[i]),v1=b64val(src[i+1]);
        int v2=b64val(src[i+2]),v3=b64val(src[i+3]);
        if(v0<0||v1<0) break;
        r+=(char)((v0<<2)|(v1>>4));
        if(src[i+2]!='='&&v2>=0) r+=(char)(((v1&0xF)<<4)|(v2>>2));
        if(src[i+3]!='='&&v3>=0) r+=(char)(((v2&0x3)<<6)|v3);
    }
    return r;
}

// =====================================================
// MIMEヘッダーデコード(件名・From表示用)
// =====================================================
static String jisToUtf8(const String &jis) {
    String u; u.reserve(jis.length()*2);
    int len=jis.length(),i=0; bool k=false;
    while(i<len){
        uint8_t c=(uint8_t)jis[i];
        if(c==0x1B&&i+2<len){
            uint8_t c1=jis[i+1],c2=jis[i+2];
            if(c1==0x24&&(c2==0x42||c2==0x40)){k=true;i+=3;continue;}
            if(c1==0x28){k=false;i+=3;continue;}
        }
        if(k&&i+1<len){
            uint8_t b1=c,b2=jis[i+1];i+=2;
            if(b1==0x24&&b2>=0x21&&b2<=0x73){
                uint32_t x=0x3040+(b2-0x20);
                u+=(char)(0xE0|(x>>12));u+=(char)(0x80|((x>>6)&0x3F));u+=(char)(0x80|(x&0x3F));continue;}
            if(b1==0x25&&b2>=0x21&&b2<=0x76){
                uint32_t x=0x30A0+(b2-0x20);
                u+=(char)(0xE0|(x>>12));u+=(char)(0x80|((x>>6)&0x3F));u+=(char)(0x80|(x&0x3F));continue;}
            int row=b1-0x21,col=b2-0x21;
            uint8_t s1=(row>>1)+(row<62?0x71:0xB1);if(s1>=0xA0)s1+=0x40;
            uint8_t s2=(row&1)?col+0x9F+(col>=0x3F?1:0):col+0x40+(col>=0x3F?1:0);
            u+=(char)s1;u+=(char)s2;continue;
        }
        if(c>=0x20&&c<0x7F)u+=(char)c;
        i++;
    }
    return u;
}
static String decodeMimeHeader(const String &input) {
    String result; result.reserve(input.length());
    int len=input.length(),i=0;
    while(i<len){
        int start=input.indexOf("=?",i);
        if(start<0){result+=input.substring(i);break;}
        if(start>i){String b=input.substring(i,start);if(result.length()>0)b.trim();result+=b;}
        int ce=input.indexOf('?',start+2);if(ce<0){result+=input.substring(start);break;}
        String charset=input.substring(start+2,ce);charset.toUpperCase();
        if(ce+1>=len)break;char enc=input[ce+1];
        int ee=input.indexOf('?',ce+2);if(ee<0){result+=input.substring(start);break;}
        int te=input.indexOf("?=",ee);if(te<0){result+=input.substring(start);break;}
        String et=input.substring(ee+1,te),decoded;
        if(enc=='B'||enc=='b') decoded=b64dec(et);
        else if(enc=='Q'||enc=='q'){
            decoded.reserve(et.length());
            for(int j=0;j<(int)et.length();j++){
                if(et[j]=='_')decoded+=' ';
                else if(et[j]=='='&&j+2<(int)et.length()){char hex[3]={et[j+1],et[j+2],0};decoded+=(char)strtol(hex,nullptr,16);j+=2;}
                else decoded+=et[j];
            }
        } else decoded=et;
        if(charset=="ISO-2022-JP"||charset=="ISO-2022-JP-2")result+=jisToUtf8(decoded);
        else result+=decoded;
        i=te+2;
        while(i<len&&(input[i]==' '||input[i]=='\t')&&i+1<len&&input[i+1]=='=')i++;
    }
    return result;
}

// =====================================================
// 生TCP 共通ヘルパー
// =====================================================
static String tcpReadLine(WiFiClient &tcp, uint32_t timeout_ms = 10000) {
    String line; line.reserve(256);
    uint32_t t = millis();
    while (millis() - t < timeout_ms) {
        while (tcp.available()) {
            char c = tcp.read();
            if (c == '\n') return line;
            if (c != '\r') line += c;
            t = millis();
        }
        delay(5);
    }
    return line;
}

// =====================================================
// IMAP(生TCP)
// =====================================================
static String imapTag() {
    char buf[8]; snprintf(buf, sizeof(buf), "T%03d", g_tagNum++);
    return String(buf);
}
static String imapCmd(const String &cmd, String *lines = nullptr) {
    String tag = imapTag();
    g_imap_tcp.print(tag + " " + cmd + "\r\n");
    uint32_t t = millis();
    while (millis() - t < 15000) {
        if (!g_imap_tcp.available()) { delay(10); continue; }
        String line = tcpReadLine(g_imap_tcp);
        if (line.startsWith(tag)) {
            if (line.indexOf(" OK")  > 0) return "OK";
            if (line.indexOf(" NO")  > 0) return "NO";
            if (line.indexOf(" BAD") > 0) return "BAD";
            return "ERR";
        }
        if (lines) { *lines += line; *lines += '\n'; }
        t = millis();
    }
    return "TIMEOUT";
}

static String imapFetchRaw(int msgNo, const char* section) {
    String cmd;
    if (strlen(section) == 0)
        cmd = "FETCH " + String(msgNo) + " (BODY.PEEK[])";
    else
        cmd = "FETCH " + String(msgNo) + " (BODY.PEEK[" + section + "])";

    String tag = imapTag();
    g_imap_tcp.print(tag + " " + cmd + "\r\n");

    String body;
    uint32_t t = millis();
    while (millis() - t < 30000) {
        if (!g_imap_tcp.available()) { delay(10); continue; }
        String line = tcpReadLine(g_imap_tcp);
        t = millis();
        if (line.startsWith(tag)) break;

        int lb = line.indexOf('{'), rb = line.indexOf('}');
        if (lb >= 0 && rb > lb) {
            int byteCount = line.substring(lb+1, rb).toInt();
            if (byteCount <= 0 || byteCount > MAX_BODY_BYTES) {
                logPrint(LOG_WARN, "FETCHサイズ異常: " + String(byteCount));
                break;
            }
            body.reserve(byteCount + 4);
            int rd = 0;
            uint32_t bt = millis();
            while (rd < byteCount && millis() - bt < 120000) {
                if (g_imap_tcp.available()) {
                    body += (char)g_imap_tcp.read();
                    rd++;
                    bt = millis();
                } else delay(1);
            }
            logPrint(LOG_INFO, "FETCH受信: " + String(rd) + "/" + String(byteCount) + " bytes");
            uint32_t et2 = millis();
            while (millis() - et2 < 5000) {
                if (!g_imap_tcp.available()) { delay(5); continue; }
                String tail = tcpReadLine(g_imap_tcp);
                et2 = millis();
                if (tail.startsWith(tag)) break;
            }
            return body;
        }
    }
    return body;
}

// =====================================================
// SMTP(生TCP)
// =====================================================
static String smtpRead(uint32_t timeout_ms = 10000) {
    String last;
    uint32_t t = millis();
    while (millis() - t < timeout_ms) {
        if (!g_smtp_tcp.available()) { delay(10); continue; }
        String line = tcpReadLine(g_smtp_tcp);
        t = millis();
        last = line;
        if (line.length() >= 4 && line[3] == ' ') break;
    }
    return last;
}
static bool smtpExpect(int code) {
    String r = smtpRead();
    Serial.println("SMTP<< " + r);
    return r.startsWith(String(code));
}
static void smtpSend(const String &s) {
    Serial.println("SMTP>> " + s);
    g_smtp_tcp.print(s + "\r\n");
}
static void smtpWriteStuffed(const String &s) {
    int len = s.length();
    bool lineStart = true;
    for (int i = 0; i < len; i++) {
        char c = s[i];
        if (lineStart && c == '.') g_smtp_tcp.write('.');
        g_smtp_tcp.write(c);
        lineStart = (c == '\n');
    }
}

// =====================================================
// 転送送信
// fullMail = BODY.PEEK[]で取得したRFC822全体
// =====================================================
static bool sendForwardMail(
    const String &subject,
    const String &from,
    const String &date,
    const String &fullMail)
{
    if (!g_smtp_tcp.connect(SMTP_HOST, SMTP_PORT)) {
        logPrint(LOG_ERROR, "SMTP TCP接続失敗"); return false;
    }
    if (!smtpExpect(220)) {
        logPrint(LOG_ERROR, "SMTPグリーティング失敗"); g_smtp_tcp.stop(); return false;
    }

    smtpSend("EHLO esp32.local");
    {
        uint32_t t = millis();
        while (millis() - t < 5000) {
            if (!g_smtp_tcp.available()) { delay(10); continue; }
            String line = tcpReadLine(g_smtp_tcp);
            Serial.println("SMTP<< " + line);
            t = millis();
            if (line.length() >= 4 && line[3] == ' ') break;
        }
    }

    smtpSend("AUTH LOGIN");
    if (!smtpExpect(334)) { logPrint(LOG_ERROR,"AUTH LOGIN失敗"); g_smtp_tcp.stop(); return false; }
    smtpSend(b64enc(String(SMTP_USER)));
    if (!smtpExpect(334)) { logPrint(LOG_ERROR,"USERNAME失敗"); g_smtp_tcp.stop(); return false; }
    smtpSend(b64enc(String(SMTP_PASS)));
    if (!smtpExpect(235)) { logPrint(LOG_ERROR,"SMTP認証失敗"); g_smtp_tcp.stop(); return false; }
    logPrint(LOG_INFO, "SMTP認証成功");

    smtpSend("MAIL FROM:<" + String(SMTP_USER) + ">");
    if (!smtpExpect(250)) { logPrint(LOG_ERROR,"MAIL FROM失敗"); g_smtp_tcp.stop(); return false; }

    smtpSend("RCPT TO:<" + String(FORWARD_TO) + ">");
    if (!smtpExpect(250)) { logPrint(LOG_ERROR,"RCPT TO失敗"); g_smtp_tcp.stop(); return false; }

    smtpSend("DATA");
    if (!smtpExpect(354)) { logPrint(LOG_ERROR,"DATA失敗"); g_smtp_tcp.stop(); return false; }

    // RFC822のヘッダー部とボディ部を分割
    int sep = fullMail.indexOf("\r\n\r\n");
    bool crlf = true;
    if (sep < 0) { sep = fullMail.indexOf("\n\n"); crlf = false; }
    String hdrPart = (sep >= 0) ? fullMail.substring(0, sep) : fullMail;
    String bdyPart = (sep >= 0) ? fullMail.substring(sep + (crlf ? 4 : 2)) : "";

    // Resent-*ヘッダーを先頭に追加(RFC5322準拠の転送形式)
    g_smtp_tcp.print("Resent-From: " + String(SMTP_USER) + "\r\n");
    g_smtp_tcp.print("Resent-To: " + String(FORWARD_TO) + "\r\n");
    g_smtp_tcp.print("Resent-Date: " + date + "\r\n");
    g_smtp_tcp.print("X-Forwarded-To: " + String(FORWARD_TO) + "\r\n");

    // 元ヘッダーを1行ずつ処理
    // To:のみ転送先に差し替え
    // Subject:に[転送]プレフィックス付加
    // Return-Path:/Delivered-To:/Received:/DKIM-Signature:は除去
    bool skipFolded = false;
    int hpos = 0, hlen = hdrPart.length();
    while (hpos < hlen) {
        int nl = hdrPart.indexOf('\n', hpos);
        int end = (nl < 0) ? hlen : nl + 1;
        String hline = hdrPart.substring(hpos, end);
        hpos = end;

        // 折り畳み継続行
        if (hline.length() > 0 && (hline[0] == ' ' || hline[0] == '\t')) {
            if (!skipFolded) smtpWriteStuffed(hline);
            continue;
        }

        // 新しいヘッダーフィールド
        String hlineLower = hline; hlineLower.toLowerCase();

        // To: のみ転送先に差し替え
        if (hlineLower.startsWith("to:")) {
            g_smtp_tcp.print("To: " + String(FORWARD_TO) + "\r\n");
            skipFolded = true;
            continue;
        }
        // Subject: に[転送]プレフィックス付加
        if (hlineLower.startsWith("subject:")) {
            g_smtp_tcp.print("Subject: =?UTF-8?B?" + b64enc("[転送] " + subject) + "?=\r\n");
            skipFolded = true;
            continue;
        }
        // 除去するヘッダー
        if (hlineLower.startsWith("return-path:") ||
            hlineLower.startsWith("delivered-to:") ||
            hlineLower.startsWith("received:") ||
            hlineLower.startsWith("dkim-signature:")) {
            skipFolded = true;
            continue;
        }

        skipFolded = false;
        smtpWriteStuffed(hline);
    }

    // ヘッダーとボディの区切り空行
    g_smtp_tcp.print("\r\n");

    // ボディをそのまま送信
    smtpWriteStuffed(bdyPart);
    if (bdyPart.length() == 0 || bdyPart[bdyPart.length()-1] != '\n')
        g_smtp_tcp.print("\r\n");

    g_smtp_tcp.print(".\r\n");
    if (!smtpExpect(250)) { logPrint(LOG_ERROR,"DATA終端失敗"); g_smtp_tcp.stop(); return false; }

    smtpSend("QUIT");
    smtpExpect(221);
    g_smtp_tcp.stop();
    return true;
}

// =====================================================
// メイン転送処理
// =====================================================
static void forwardMails() {
    logPrint(LOG_INFO, "====== メール転送処理 開始 ======");
    g_tagNum = 1;

    if (!g_imap_tcp.connect(IMAP_HOST, IMAP_PORT)) {
        logPrint(LOG_ERROR, "IMAP TCP接続失敗"); return;
    }
    tcpReadLine(g_imap_tcp);

    if (imapCmd(String("LOGIN ") + IMAP_USER + " " + IMAP_PASS) != "OK") {
        logPrint(LOG_ERROR, "IMAPログイン失敗"); g_imap_tcp.stop(); return;
    }
    logPrint(LOG_INFO, "IMAPログイン成功");

    String selLines;
    if (imapCmd("SELECT INBOX", &selLines) != "OK") {
        logPrint(LOG_ERROR, "INBOX選択失敗"); g_imap_tcp.stop(); return;
    }

    int totalMsg = 0;
    {
        int idx = selLines.indexOf(" EXISTS");
        if (idx > 0) {
            int nl = selLines.lastIndexOf('\n', idx);
            String numStr = selLines.substring(nl+1, idx);
            numStr.trim();
            if (numStr.startsWith("* ")) numStr = numStr.substring(2);
            totalMsg = numStr.toInt();
        }
    }
    logPrint(LOG_INFO, "総件数: " + String(totalMsg));

    if (totalMsg == 0) {
        logPrint(LOG_INFO, "メールなし");
        imapCmd("LOGOUT"); g_imap_tcp.stop();
        logPrint(LOG_INFO, "====== メール転送処理 終了 ======");
        return;
    }

    int startNum = max(1, totalMsg - CHECK_LATEST + 1);
    logPrint(LOG_INFO, "処理範囲: " + String(startNum) + "~" + String(totalMsg));

    for (int msgNo = totalMsg; msgNo >= startNum; msgNo--) {

        // UID取得
        String flagLines;
        imapCmd("FETCH " + String(msgNo) + " (UID FLAGS)", &flagLines);
        String uid;
        {
            int ui = flagLines.indexOf("UID ");
            if (ui >= 0) {
                int ue = flagLines.indexOf(' ', ui+4);
                if (ue < 0) ue = flagLines.indexOf(')', ui+4);
                if (ue > ui+4) uid = flagLines.substring(ui+4, ue);
            }
            uid.trim();
            if (uid.isEmpty()) uid = "seq_" + String(msgNo);
        }

        logPrint(LOG_INFO, "--- msgNo:" + String(msgNo) + " UID:" + uid + " ---");
        if (isProcessed(uid)) { logPrint(LOG_INFO, "スキップ(処理済)"); continue; }

        // ENVELOPE取得(件名・From・Date)
        String envLines;
        imapCmd("FETCH " + String(msgNo) + " (ENVELOPE)", &envLines);
        String subject, from, date;
        {
            int ei = envLines.indexOf("ENVELOPE (");
            if (ei >= 0) {
                int pos = ei + 10;
                auto nextQuoted = [&](int &p) -> String {
                    int s = envLines.indexOf('"', p); if (s < 0) return "";
                    int e = envLines.indexOf('"', s+1);
                    while (e > 0 && envLines[e-1] == '\\') e = envLines.indexOf('"', e+1);
                    if (e < 0) return ""; p = e+1;
                    return envLines.substring(s+1, e);
                };
                date    = nextQuoted(pos);
                subject = decodeMimeHeader(nextQuoted(pos));
                int fb = envLines.indexOf("((\"", pos);
                if (fb < 0) fb = envLines.indexOf("((NIL", pos);
                if (fb >= 0) {
                    int fe = envLines.indexOf("))", fb);
                    if (fe > fb) {
                        String fromBlock = envLines.substring(fb+2, fe);
                        int fp = 0;
                        String fname = nextQuoted(fp);
                        int nilPos = fromBlock.indexOf("NIL");
                        int uq = fromBlock.indexOf('"', nilPos < 0 ? 0 : nilPos+3);
                        String fuser, fhost;
                        if (uq >= 0) {
                            int ue2 = fromBlock.indexOf('"', uq+1);
                            fuser = fromBlock.substring(uq+1, ue2);
                            int hq = fromBlock.indexOf('"', ue2+1);
                            if (hq >= 0) {
                                int he = fromBlock.indexOf('"', hq+1);
                                fhost = fromBlock.substring(hq+1, he);
                            }
                        }
                        from = decodeMimeHeader(fname);
                        if (fuser.length() > 0 && fhost.length() > 0)
                            from += " <" + fuser + "@" + fhost + ">";
                    }
                }
            }
        }
        logPrint(LOG_INFO, "件名: " + subject);
        logPrint(LOG_INFO, "From: " + from);

        // RFC822全体を取得
        logPrint(LOG_INFO, "メール取得中...");
        String fullMail = imapFetchRaw(msgNo, "");

        logPrint(LOG_INFO, "メール全体長: " + String(fullMail.length()) + " bytes");

        // 転送
        logPrint(LOG_INFO, "転送中...");
        bool fwd = sendForwardMail(subject, from, date, fullMail);
        if (fwd) {
            logPrint(LOG_INFO, "転送成功");
            saveUID(uid);
        } else {
            logPrint(LOG_ERROR, "転送失敗");
        }
        delay(300);
    }

    imapCmd("LOGOUT");
    g_imap_tcp.stop();
    logPrint(LOG_INFO, "====== メール転送処理 終了 ======");
}

// =====================================================
// Setup / Loop
// =====================================================
void setup() {
    Serial.begin(115200); delay(1000);
    Serial.println("\n=== ESP32 メール自動転送システム 起動 ===");
    // clearAllUIDs();
    loadStoredUIDs();
    connectWiFi();
    forwardMails();
}

void loop() {
    if (WiFi.status() != WL_CONNECTED) { logPrint(LOG_WARN,"WiFi切断。再接続"); connectWiFi(); }
    logPrint(LOG_INFO, "次回まで " + String(CHECK_INTERVAL/60000) + " 分待機");
    delay(CHECK_INTERVAL);
    forwardMails();
}

仕様と補足

  • mineoに届いたメールをIMAPで受け、mineoのSMTPサーバを使って転送します
  • 8行目から20行目の定数に、ご自分のパラメータを嵌め込んでください
  • 30分毎に未読チェックに行きます
  • 転送最大サイズは20MBにしています
  • 既読フラグをNVSに置いています
  • スタックが8KBしか無い環境なので、なるべく関数内部にはモノを溜め込まないようにしました
  • mineoのIMAP実装が特殊?だったため、ESP32 Mail Clientが使えませんでした(ここが一番の苦労点)
  • 一部のメーラで作られたメールだと、本文転送が出来ないようです orz…

何かの役に立てば…。