- 物联网
- 嵌入式
【免费下载链接】nodemcu-firmware
Lua based interactive firmware for ESP8266, ESP8285 and ESP32
导读
imap.lua是 NodeMCU Firmware 仓库中一个以纯 Lua 实现的 IMAP 4rev1(RFC 2060)协议客户端模块,它让 ESP8266 / ESP8285 设备能够通过 TCP 连接到邮件服务器,完成登录、选定邮箱文件夹、读取最新邮件头部(SUBJECT、FROM、DATE)与纯文本正文等操作。本文以官方文档 docs/lua-modules/imap.md 为骨架,结合模块源码 lua_modules/email/imap.lua 与完整示例 lua_examples/email/read_email_imap.lua,逐项讲解全部 API 的语法、参数与返回值,并剖析其基于response_processed标志与receive回调的状态机原理,最终给出可在真实设备上运行的完整邮件读取程序。读完本文,你将能在 NodeMCU 固件上独立实现"读取邮箱最新一封邮件并通过串口展示"的完整链路。
一、模块概览:在 ESP8266 上实现 IMAP 4rev1 客户端
imap模块由 AllAboutEE 于 2015 年 3 月 12 日贡献并维护,源码位于 lua_modules/email/imap.lua,配套的模块说明文档与示例存放在仓库的docs/lua-modules/与lua_examples/email/目录下:
| 项目 | 说明 |
|---|---|
| 起始版本日期 | 2015-03-12 |
| 协议 | IMAP 4rev1(即 RFC 2060) |
| 源码文件 | lua_modules/email/imap.lua |
| 示例文件 | lua_examples/email/read_email_imap.lua |
| 模块说明 | lua_modules/email/README.md |
该模块仅提供"读取邮件"能力(IMAP 的 EXAMINE / FETCH / LOGOUT 等无副作用命令),不包含发送邮件功能。模块源码头部注释标明其最初在 NodeMCU 0.9.5 build 20150213 上测试通过,整体思路是:把每个 IMAP 命令封装为"向服务器发送一行命令文本 + 注册对应的receive回调",并在回调中累积响应数据、识别命令完成的标志,从而把异步的 TCP 数据流转译为可轮询的同步状态。
二、加载与释放:require 与 release
模块以源码形式存放在文件系统中,加载方式与普通 Lua 模块一致:
imap = require("imap.lua")若希望将脚本预编译为字节码以节省内存,lua_modules/email/imap.lua 源码头部注释给出了推荐流程:
node.compile("imap.lua") -- 上传脚本后先编译生成 imap.lc file.remove("imap.lua") -- 编译成功后删除原始脚本不再使用模块时,按 NodeMCU 惯例清除全局引用与package.loaded缓存即可彻底释放:
imap = nil package.loaded["imap"] = nil三、API 全参考:九个函数的语法、参数与返回
以下内容完整对应官方文档 docs/lua-modules/imap.md 的 API 说明,并补充了源码级细节。
3.1imap.response_processed()
用于检查上一条 IMAP 命令是否已被服务器处理完毕。
- 语法:
imap.response_processed() - 参数:无
- 返回值:布尔值。返回
true表示上一条命令已处理完成,false表示仍在等待服务器响应。
这是整个模块的"心跳"函数。在源码 lua_modules/email/imap.lua 中,模块用一个模块级变量response_processed记录状态,每次发出新命令前(如login、examine、fetch_header等)都会先将其重置为false,等收到服务器以"complete"结尾的应答时再由回调置回true。
3.2imap.config(username, password, tag, [debug])
初始化 IMAP 会话配置,登录前必须调用。
- 语法:
imap.config(username, password, tag, [debug]) - 参数:
username:IMAP 用户名。对大多数邮箱服务商来说,用户名就是完整邮箱地址。password:IMAP 密码。tag:IMAP 命令标签。当前实现下任意简单字符串(如"t1")均可正常工作。debug:(布尔值,可选)设为true时,ESP8266 与 IMAP 服务器之间的完整对话将被打印到串口,便于排查问题;默认值为false。
- 返回值:
nil
该函数在源码中只是把四个参数存入模块级变量USERNAME、PASSWORD、TAG、DEBUG(见 lua_modules/email/imap.lua)。其中TAG会作为后续所有 IMAP 命令文本的前缀,服务器对每条命令的完成应答也会带上同样的标签,这是 IMAP 协议区分命令与响应的标准机制。
3.3imap.login(socket)
登录一个新的邮件会话,向服务器发送LOGIN命令。
- 语法:
imap.login(socket) - 参数:
socket—— 由net.createConnection创建的 IMAP TCP socket 对象。 - 返回值:
nil
实现细节(lua_modules/email/imap.lua):
socket:send(TAG .. " LOGIN " .. USERNAME .. " " .. PASSWORD .. "\r\n") socket:on("receive", display)即把config阶段配置好的用户名密码拼成标准的 IMAP 命令行发送,同时把receive回调切换为通用的display处理器。
3.4imap.get_most_recent_num()
获取邮箱中最新的邮件编号。必须在调用examine之后才能调用本函数,因为最新编号是从EXAMINE的应答中解析出来的。
- 语法:
imap.get_most_recent_num() - 参数:无
- 返回值:最新一封邮件的编号(数字)。
3.5imap.examine(socket, mailbox)
检查(IMAP 术语为 examine)给定的邮箱/文件夹,向服务器发送EXAMINE命令。
- 语法:
imap.examine(socket, mailbox) - 参数:
socket:IMAP TCP socket 对象。mailbox:要检查的文件夹名称,例如"INBOX"。
- 返回值:
nil
实现细节(lua_modules/email/imap.lua):
socket:send(TAG .. " EXAMINE " .. mailbox .. "\r\n") socket:on("receive", set_most_recent_num)服务器对EXAMINE会返回* <n> EXISTS之类的应答,表示该文件夹当前共有n封邮件。模块在set_most_recent_num回调中用模式([0-9]+) EXISTS提取这个数字存入most_recent_num(见 lua_modules/email/imap.lua),供后续fetch_header/fetch_body_plain_text使用。
3.6imap.get_header()
获取最近一次抓取的邮件头部字段内容。
- 语法:
imap.get_header() - 参数:无
- 返回值:最近一次
fetch_header抓取的头部字段原始内容(字符串)。
3.7imap.fetch_header(socket, msg_number, field)
抓取某一封邮件的指定头部字段,例如SUBJECT、FROM、DATE。
- 语法:
imap.fetch_header(socket, msg_number, field) - 参数:
socket:IMAP TCP socket 对象。msg_number:要读取的邮件编号;注意1表示最新/最近的一封邮件(与 IMAP 的递增编号语义一致)。field:头部字段名,如SUBJECT、FROM、DATE。
- 返回值:
nil
实现细节(lua_modules/email/imap.lua):
header = "" -- 抓取新头部前先清空 socket:send(TAG .. " FETCH " .. msg_number .. " BODY[HEADER.FIELDS (" .. field .. ")]\r\n") socket:on("receive", set_header)命令文本使用了 IMAP 的BODY[HEADER.FIELDS (...)]部分获取语法,服务器只会返回指定的头部字段。set_header回调会把收到的每个网络分片追加到header变量,直到识别到"complete"(见 lua_modules/email/imap.lua)。
3.8imap.get_body()
获取最近一次读取的邮件正文内容。
- 语法:
imap.get_body() - 参数:无
- 返回值:最近一次
fetch_body_plain_text获取的邮件正文(字符串)。
3.9imap.fetch_body_plain_text(socket, msg_number)
抓取指定邮件的纯文本正文版本,向服务器发送FETCH ... BODY[1]命令。
- 语法:
imap.fetch_body_plain_text(socket, msg_number) - 参数:
socket:IMAP TCP socket 对象。msg_number:要获取正文的邮件编号;1表示最新邮件。
- 返回值:
nil
实现细节(lua_modules/email/imap.lua):
body = "" -- 抓取新邮件前先清空 socket:send(TAG .. " FETCH " .. msg_number .. " BODY[1]\r\n") socket:on("receive", set_body)3.10imap.logout(socket)
向服务器发送LOGOUT命令,结束邮件会话。
- 语法:
imap.logout(socket) - 参数:
socket:IMAP TCP socket 对象。 - 返回值:
nil
实现细节(lua_modules/email/imap.lua):
socket:send(TAG .. " LOGOUT\r\n") socket:on("receive", display)LOGOUT命令用于结束 IMAP 会话。模块本身不负责关闭 TCP socket,示例代码会在处理完邮件后自行调用imap_socket:close()。
四、源码级原理剖析:异步 TCP 上的状态机设计
4.1response_processed标志与回调切换
NodeMCU 的 TCP socket 是异步的(相关 API 见 net 模块文档 中的net.socket:on()与net.socket:send())。imap.lua的核心设计是用一个模块级布尔变量response_processed(lua_modules/email/imap.lua)作为"命令是否完成"的语义标志:
- 每次发送新命令前,先置
response_processed = false; - 通过
socket:on("receive", 对应回调)把接收回调切换到该命令专用的处理器; - 回调持续把收到的数据分片追加进
header/body等累积变量; - 当某个分片里匹配到
"complete"字样时,置response_processed = true。
这一机制的逻辑依据写在display回调的注释里:某些 IMAP 响应较长,receive回调会被触发多次,但可以确定的是——IMAP 服务器在数据发送完毕时必定回复<tag> OK <command> complete(见 lua_modules/email/imap.lua)。因此"complete"是判断命令完成的可靠锚点,而不必依赖具体的分片边界。
4.2 四个内部回调的分工
模块源码中共有四个内部local function,分别对应四类命令响应:
| 内部函数 | 服务命令 | 职责 | 源码位置 |
|---|---|---|---|
display | LOGIN、LOGOUT | 调试打印响应、识别"complete" | lua_modules/email/imap.lua |
set_most_recent_num | EXAMINE | 解析* <n> EXISTS得到最新编号 | lua_modules/email/imap.lua |
set_header | FETCH ... BODY[HEADER.FIELDS (...)] | 累积头部字段文本 | lua_modules/email/imap.lua |
set_body | FETCH ... BODY[1] | 累积邮件正文文本 | lua_modules/email/imap.lua |
除set_most_recent_num用string.find(response, "([0-9]+) EXISTS")提取编号外,其余回调均采用"字符串拼接 + 等待complete"的通用模式,与 net 模块文档 中关于receive事件按网络帧多次触发的说明完全契合——数据超过约 1460 字节(以太网帧上限)时会分成多个receive回调,所以模块用累积拼接而非单次赋值来收拢响应。
4.3 从源码可推断的局限
- 模块只监听
receive事件,未处理连接中断/超时等异常分支,若服务器无响应,response_processed将一直保持false,需要由应用层自行兜底; get_most_recent_num()依赖EXAMINE应答中的EXISTS计数,因此文档明确要求它只能在examine之后调用;- 由于是逐段拼接字符串,超大邮件的正文会持续占用内存,在内存紧张的 ESP8266 上读取大邮件时需要留意剩余可用内存。
五、实战:完整读取最新一封邮件
官方文档给出的参考示例是 lua_examples/email/read_email_imap.lua。该示例演示了完整流程:连接 WiFi → 建立 TCP 连接 → 配置并登录 → 检查 INBOX → 抓取 SUBJECT / FROM → 抓取正文 → 清理协议文本后通过串口打印 → 关闭连接。下面保留示例全貌并逐段加注说明。
5.1 完整示例代码
local imap = require("imap") local IMAP_USERNAME = "email@domain.com" local IMAP_PASSWORD = "password" -- 向你的邮箱服务商确认其"无加密 IMAP 服务器地址与端口"(例如搜索 -- "[邮箱服务名] imap settings"),填入下面的服务端信息 local IMAP_SERVER = "imap.service.com" local IMAP_PORT = "143" local IMAP_TAG = "t1" -- IMAP 命令标签,通常无需修改 local IMAP_DEBUG = true -- 设为 true 可在串口查看 ESP8266 与 IMAP 服务器的完整对话 local SSID = "ssid" local SSID_PASSWORD = "password" local count = 0 -- 记录当前处于第几步,用于依次发送多条 IMAP 命令 local imap_socket, timer -- 建立 TCP 连接成功后触发:配置邮箱账号并发送 LOGIN local function setup(sck) imap.config(IMAP_USERNAME, IMAP_PASSWORD, IMAP_TAG, IMAP_DEBUG) imap.login(sck) end local subject = "" local from = "" local body = "" -- 定时器回调:检查上一条 IMAP 命令是否已处理完毕, -- 处理完则发送下一条命令,逐步推进整个读取流程 local function do_next() if(imap.response_processed() == true) then if (count == 0) then -- 登录完成后,选定要读取的邮箱文件夹(此处为 INBOX) imap.examine(imap_socket,"INBOX") count = count + 1 elseif (count == 1) then -- 选定文件夹后,抓取最新邮件的 SUBJECT imap.fetch_header(imap_socket,imap.get_most_recent_num(),"SUBJECT") count = count + 1 elseif (count == 2) then subject = imap.get_header() -- 保存 SUBJECT -- 抓取最新邮件的 FROM imap.fetch_header(imap_socket,imap.get_most_recent_num(),"FROM") count = count + 1 elseif (count == 3) then from = imap.get_header() -- 保存 FROM -- 抓取最新邮件的纯文本正文 imap.fetch_body_plain_text(imap_socket,imap.get_most_recent_num()) count = count + 1 elseif (count == 4) then body = imap.get_body() -- 保存正文 imap.logout(imap_socket) -- 退出邮件会话 count = count + 1 else -- 用模式匹配剥离 IMAP 协议文本,只保留邮件实际内容 local pattern1 = "%*.*}\n" -- 去除 "* n command (BODY[n] {n}" 前缀 local pattern2 = "%)\n.+" -- 去除 ") t1 OK command completed" 结尾 from = string.gsub(from,pattern1,"") from = string.gsub(from,pattern2,"") print(from) subject = string.gsub(subject,pattern1,"") subject = string.gsub(subject,pattern2,"") print(subject) body = string.gsub(body,pattern1,"") body = string.gsub(body,pattern2,"") print("Message: " .. body) timer:stop() -- 停止定时器 imap_socket:close() -- 关闭 IMAP socket collectgarbage() -- 回收内存 end end end do -- 将 ESP8266 配置为 Wi-Fi 工作站(STA)模式 wifi.setmode(wifi.STATION) wifi.sta.config(SSID,SSID_PASSWORD) wifi.sta.autoconnect(1) -- 创建明文 TCP 连接(net 模块见 docs/modules/net.md) imap_socket = net.createConnection(net.TCP,0) imap_socket:on("connection",setup) -- 连接成功后调用 setup() imap_socket:connect(IMAP_PORT,IMAP_SERVER) -- 连接 IMAP 服务器 -- 用 1 秒周期定时器轮询 response_processed(),驱动命令逐条推进 timer = tmr.create() timer:alarm(1000, tmr.ALARM_AUTO, do_next) end5.2 执行流程拆解
- Wi-Fi 连接:调用
wifi.setmode(wifi.STATION)与wifi.sta.config(...)接入路由器(Wi-Fi API 详见 wifi 模块文档); - 建立 TCP 连接:
net.createConnection(net.TCP, 0)创建 socket,端口143是 IMAP 明文端口;连接成功后触发setup回调; - 登录:
setup中先imap.config(...)再imap.login(sck); - 轮询驱动:
tmr.create()创建定时器,以 1 秒周期调用do_next(定时器 API 详见 tmr 模块文档)。do_next每轮先检查imap.response_processed(),为true才推进下一步命令,从而保证 IMAP 命令严格串行; - 读取与清理:依次 EXAMINE INBOX → 抓 SUBJECT → 抓 FROM → 抓正文 → LOGOUT;最后用两个 Lua 模式(
pattern1、pattern2)剥掉* n ... (BODY[n] {n}与) t1 OK ... completed之类的协议封装文本,串口输出的就是从from、subject、body打印出的纯邮件内容; - 善后:停表、关 socket、
collectgarbage()释放内存。
5.3 运行前提与限制(重要)
以下限制来自示例源码头部的作者说明,实际部署前必须确认:
- 服务器必须提供无加密(明文)的 IMAP 访问,即使用端口 143。该示例最初是在 AOL 与 Time Warner Cable 邮箱账号上测试通过的;Gmail 等不支持无 SSL 访问的邮件服务无法使用本示例;
- 不是所有邮箱服务商都开放明文 IMAP,部署前应先向服务商确认
imap.<service>.com:143是否可用;若不可用,需要自行改造为加密连接方案; msg_number传1表示最新/最新的一封邮件,若要读取更早的邮件可改用更大的编号(在EXAMINE返回的EXISTS计数范围内);- 本模块为同步语义封装,示例依赖定时器轮询
response_processed(),因此两次命令之间天然存在约 1 秒的间隔;若需加快节奏,可适当缩短timer:alarm的周期。
六、小结与上手清单
综合官方文档 docs/lua-modules/imap.md、模块源码 lua_modules/email/imap.lua 与示例 lua_examples/email/read_email_imap.lua,上手该模块只需五步:
- 将
imap.lua上传到设备文件系统(可选:用node.compile("imap.lua")预编译以省内存); - 在代码中
require("imap.lua"),确认邮件服务商提供明文 IMAP(端口 143); - 用
imap.config(用户名, 密码, "t1", true)配置会话,debug=true可先观察协议对话; - 建立
net.createConnection(net.TCP, 0),连接成功后依次login→examine("INBOX")→fetch_header(最新编号, "SUBJECT"/"FROM")→fetch_body_plain_text(最新编号)→logout,每一步之间用imap.response_processed()判断完成; - 读取
imap.get_header()/imap.get_body(),用模式匹配剥离协议文本后即得到邮件内容。
通过这套 API,NodeMCU 设备便可在纯 Lua 层面完成邮箱读取——无论是做邮件触发式的智能家居联动,还是低成本的邮件通知终端,这个模块都给出了一个直接可用的起点。
- 物联网
- 嵌入式
【免费下载链接】nodemcu-firmware
Lua based interactive firmware for ESP8266, ESP8285 and ESP32
相关推荐
Seelen UI:彻底改变你的Windows桌面体验,打造完全个性化的数字工作空间
Seelen UI:彻底改变你的Windows桌面体验,打造完全个性化的数字工作空间 你是否曾经对Windows千篇一律的桌面界面感到厌倦?是否梦想过能够按照自
桌面应用前端插件系统如何用Electron-React Boilerplate构建强大的跨平台邮件客户端:SMTP/IMAP集成完整指南
如何用Electron React Boilerplate构建强大的跨平台邮件客户端:SMTP/IMAP集成完整指南 Electron React Boiler
示例工程前端Klavis 本地邮件 MCP 服务器实战指南:基于 FastMCP 的 IMAP/SMTP 邮箱管理
Klavis 本地邮件 MCP 服务器实战指南:基于 FastMCP 的 IMAP/SMTP 邮箱管理 本文围绕仓库中 mcp_servers/local/Po
AI 应用LLM 网关MCP 服务工具调用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考