大数跨境

SpringBoot3+IMAP 企业邮箱入 OA:本地索引、三栏收件箱与业务单据挂邮件

SpringBoot3+IMAP 企业邮箱入 OA:本地索引、三栏收件箱与业务单据挂邮件 企业软件源码
2026-10-16
7
导读:系统通知发信不是员工邮箱。RuoYi Office 用 Jakarta Mail 做 IMAP/SMTP 连接器,把信封信息落本地索引,三栏收件箱翻页不打满邮件服务器;点开再拉正文和附件。再配服务模板

SpringBoot3+IMAP 企业邮箱入 OA:本地索引、三栏收件箱与业务单据挂邮件

🌐 文档地址:https://ruoyioffice.com
 📦 源码1·GitHub:https://github.com/yuqing2026/ruoyi-office
 📦 源码2·GitCode:https://gitcode.com/zhouzhongyan/ruoyi-office
 📦 源码3·Gitee:https://gitee.com/yqzy1688/ruoyi-office
 💬 微信:17156169080(备注「RuoYi Office」)
 本文由 RuoYi Office 团队整理,结合实际项目分享企业软件的设计思路与开发经验。

一句话:OA 里「发验证码 / 发待办提醒」用的是系统发信通道;员工每天要处理的是自己的企业邮箱。RuoYi Office 把 IMAP 收信、SMTP 写信做成办公菜单里的三栏客户端,列表走本地索引,正文按需回源。

▲ 连接器用 Jakarta Mail;索引表扛列表和未读;UI 是文件夹 + 邮件列表 + 读信/写信


引言:为什么「系统已经会发邮件」还不够

很多后台脚手架都有邮件模块:配一个 SMTP,就能发重置密码、流程超时提醒。上线以后行政和销售会问另一句话:

「能不能在 OA 里看我的企业邮箱?别再切网页版邮箱,待办和邮件分两处。」

这不是同一件事。


系统发信
员工邮箱客户端
账号
平台共用发件人
每人一个 IMAP/SMTP 身份
方向
几乎只出、不进
收、发、草稿、已删除
列表
发送日志
未读、主题搜索、附件标记
权限
管理员配通道
只能看自己的账号
和审批的关系
通知你去办
客户来信要转成待办或流程

如果把员工邮箱做成「每次打开都对 IMAP 全量 LIST」,页面会卡,邮件服务器也会被打满。RuoYi Office 的取舍是:信封信息同步进本地索引,翻页和搜索打数据库;点进一封再 IMAP FETCH 正文和附件。

本文按真实代码写这一层,不讲「怎么申请腾讯企业邮」,只讲接到 OA 之后怎么分层。


一、业务设计:三层拆开

1.1 连接层:只懂协议

ImapSmtpConnector 用 Jakarta Mail 做四件事:测连、列出规范文件夹、增量拉新信、SMTP 发送。它不认识租户套餐,也不渲染 HTML。

测连必须 IMAP、SMTP 都过。只通收信、发信 465 被防火墙拦,上线当天就会被销售投诉「能看不能回」。

连接器内部自己组 Session / Store / Transport,业务服务只传 MailServerConfig(主机、端口、SSL、用户名、密码)。不要在 Controller 里拼 Properties,否则每个接口一套超时和协议开关,排障时对不上。

文件夹别名是连接层的一部分:Sent、Sent Items、Sent Mail 都归到规范名 Sent;Trash、Deleted Items、Bin 归到 Trash。中文文件夹名若没进别名表,同步会跳过该夹——这是配置问题,不是「同步坏了」。

1.2 索引层:列表不回源

表 oa_mail_message_index 存 account、规范文件夹、IMAP UID、Message-ID、主题、发件人、收件人、时间、已读、是否有附件、摘要。草稿可以额外存 body_html,因为草稿往往还没上服务器,或 UID 是本地占位。

文件夹水位在 oa_mail_folder_state:规范名、真实 IMAP 名、last_uid。下次同步只 FETCH 大于 last_uid 的新信。

同步结果写 oa_mail_sync_log,账号上记 last_sync_time / last_error,排障不用翻应用日志。

1.3 产品层:Outlook 式三栏

收件箱页把侧栏收起,主区三列:

  1. FolderPane
    :账号切换、文件夹、同步、去设置;
  2. MessageList
    :关键字、已读筛选、分页;
  3. ReadingPane
     或内嵌 ComposePane:读信 / 回复 / 写信。

独立「写信」菜单走 /oa/mail/compose,给习惯全屏编辑的人。附件、富文本、选账号,和内嵌写信共用组件。

发送成功后:SMTP 把信交出去,同时尽量在 SENT 索引里看到自己。失败要保留草稿,不要把编辑器里的 HTML 清掉。附件先上传到文件存储,再在 MIME 里挂 ByteArrayDataSource,避免把大文件读进邮件索引字段。


二、配置:模板、策略、账号,不要写死主机名

2.1 服务模板

腾讯企业邮、阿里云邮箱、自建 Docker 邮服,IMAP 主机和端口不一样,但对产品都是「一种 Profile」。oa_mail_provider_profile 存 IMAP/SMTP 主机、端口、SSL、显示名。账号绑定可以引用模板,也可以临时填自定义主机(租户策略允许时)。

▲ 效果:模板是表单不是空表格。演示环境可挂内网 Docker 邮服,生产换成企业邮主机即可

2.2 租户策略

oa_mail_tenant_policy 控制本租户能不能让员工自己绑外部邮箱、分配方式(管理员指定 / 按规则开通)等。私有化场景经常是:IT 统一开通,员工不能随便填 QQ 邮箱当工作邮箱。

2.3 用户账号

oa_mail_account 绑定登录用户与邮箱地址、密码(或授权码)、引用的模板、开通状态。绑定后提供「测试连接」。失败原因写回账号,避免只弹一个「同步失败」。

管理员分配走单独权限 oa:mail:assign,和「自己改密码」分开,避免全员都能给别人开箱。

测连代码把 IMAP 开 INBOX 和 SMTP connect 做成一次事务式检查,缺一不可:

public void testConnection(MailServerConfig config) throws MessagingException {    Store store = null;    try {        store = openStore(config);        Folder folder = store.getFolder("INBOX");        folder.open(Folder.READ_ONLY);        folder.close(false);    } finally {        closeQuietly(store);    }    Transport transport = null;    try {        Session session = createSmtpSession(config);        String protocol = Boolean.TRUE.equals(config.getSmtpSsl()) ? "smtps" : "smtp";        transport = session.getTransport(protocol);        transport.connect(config.getSmtpHost(),                config.getSmtpPort() != null ? config.getSmtpPort() : 25,                config.getUsername(), config.getPassword());    } finally {        if (transport != null) {            transport.close();        }    }}

只测 IMAP 的结果是:收件箱能刷,点回复才发现 465 出不去。


三、PC 端:先看三栏读信,再看写信

3.1 收件箱:文件夹 + 列表 + 正文(效果页)

路径:办公 → 邮箱 → 收件箱(/oa/mail/inbox)。左侧是 INBOX / 未读 / 已发送 / 草稿 / 已删除。点一封邮件,右侧读信区出主题、收发件人、HTML 正文、附件下载。读信时会尝试把 IMAP 已读状态写回索引。

▲ 效果:点开一封信后右侧出主题、收发件人、正文,以及回复/转发/转流程

未读是虚拟文件夹:查询仍走 INBOX,只加 seen=false。IMAP 侧「已删除」名称五花八门(Trash / Deleted Items / Bin),连接器用别名映射到规范名 Trash。

3.2 写信:独立页或三栏内嵌

点「写邮件」可在三栏右侧切到 ComposePane,也可进独立写信页。回复会带上 Re:,转发带 Fwd:,并引用原文 HTML。草稿可以先落本地索引(本地 UID),同步后再与服务器草稿夹对齐。

▲ 效果:SMTP 发送前仍是 OA 登录态;附件走同一套文件存储,不把二进制塞进索引表

3.3 账号页(本文最多一张「表」)

邮箱账号设置列出当前用户已绑定的地址、最近同步时间、错误信息,可测连、改授权码。

▲ 唯一偏列表的图:用来证明「一人一账号、失败可回看」,读信效果以上面三栏为准


四、同步引擎:增量 UID,不要每次全量

4.1 一次同步扫四个规范夹

syncAccount 先 listCanonicalFolders,把服务器上的 Sent/Drafts/Trash 别名对上规范名。某个规范夹在服务器不存在就记状态、跳过,不把整个同步判失败。

每个夹调用 syncOneFolder:读 last_uid,只拉新 UID,已存在的 (account, folder, uid) 跳过。成功数和失败信息进同步日志。

private int syncOneFolder(OaMailAccountDO account, MailServerConfig config,                          MailFolderInfo folderInfo) throws Exception {    OaMailFolderStateDO state = folderStateMapper            .selectByAccountFolder(account.getId(), folderInfo.getCanonical());    long lastUid = state != null && state.getLastUid() != null ? state.getLastUid() : 0L;    List<MailFetchedMessage> fetched = imapSmtpConnector            .fetchNewMessages(config, folderInfo.getImapName(), lastUid);    long maxUid = lastUid;    int success = 0;    for (MailFetchedMessage msg : fetched) {        if (msg.getUid() == null) continue;        if (messageIndexMapper.selectByAccountFolderUid(                account.getId(), folderInfo.getCanonical(), msg.getUid()) != null) {            maxUid = Math.max(maxUid, msg.getUid());            continue;        }        messageIndexMapper.insert(OaMailMessageIndexDO.builder()                .accountId(account.getId())                .folder(folderInfo.getCanonical())                .uid(msg.getUid())                .subject(msg.getSubject())                .fromAddr(msg.getFromAddr())                .snippet(msg.getSnippet())                .hasAttach(msg.getHasAttach())                .seen(msg.getSeen())                .build());        success++;        maxUid = Math.max(maxUid, msg.getUid());    }    upsertFolderState(account.getId(), folderInfo.getCanonical(),            folderInfo.getImapName(), maxUid);    return success;}

增量同步的代价是:服务器上被改已读、被移动的旧信,不会在下一次增量里自动追上。产品上用「点开详情回写 seen」补一刀;彻底对齐需要后续 UIDVALIDITY / FLAGS 同步,那是下一阶段的事,本文不装已经做完。

4.2 列表走索引,详情再 FETCH

分页接口直接 selectPage 索引表。详情里:若是本地草稿 UID,用表里的 body_html;否则按账号配置打开 IMAP,按文件夹真实名 + UID 拉正文,附件落 oa_mail_attachment。拉失败时仍返回信封,避免整页白屏。

public OaMailMessageDetailRespVO getMessageDetail(Long id) {    OaMailMessageIndexDO index = getOwnedMessage(id);    OaMailMessageDetailRespVO resp = BeanUtils.toBean(index, OaMailMessageDetailRespVO.class);    if (OaMailFolderNames.isLocalDraftUid(index.getUid())) {        resp.setBodyHtml(index.getBodyHtml());        resp.setAttachments(toAttachmentVos(attachmentMapper.selectByMessageIndexId(id)));        return resp;    }    MailMessageDetail detail = imapSmtpConnector.fetchMessageDetail(            accountService.resolveConfig(account),            resolveImapFolder(account.getId(), index.getFolder()),            index.getUid());    if (detail != null) {        resp.setBodyHtml(detail.getBodyHtml());        saveAttachments(id, detail.getAttachments());        if (Boolean.TRUE.equals(detail.getSeen()) && !Boolean.TRUE.equals(index.getSeen())) {            OaMailMessageIndexDO update = new OaMailMessageIndexDO();            update.setId(id);            update.setSeen(true);            messageIndexMapper.updateById(update);        }    }    resp.setAttachments(toAttachmentVos(attachmentMapper.selectByMessageIndexId(id)));    return resp;}

4.3 前端三栏如何拼

收件箱挂载时把侧栏折叠,离开再恢复,避免菜单把三栏挤成窄条。选中邮件、点回复,只换右栏组件,不整页跳转。

<div class="flex h-full min-h-0 overflow-hidden rounded-md border">  <FolderPane :accounts="accounts" :folder="folder"              @sync="handleSync" @compose="startCompose()" />  <MessageList :messages="messages" :selected-id="selectedId"               @select="openMessage" />  <ComposePane v-if="composing" :model-value="composeModel" @sent="afterSent" />  <ReadingPane v-else :detail="detail" @reply="startReply('reply')" @bpm="toBpm" /></div>

五、邮件挂业务:先有关联表,再接待办和流程

读信区提供「转待办 / 发起流程」入口,写入 oa_mail_biz_link:biz_type 目前是 TODO 或 BPM,带标题、流程 key 等。

必须说清楚边界:一期先落关联记录,真正创建个人待办、真正 submitProcessInstance 还是扩展点(代码里对两类 type 打了 stub 日志)。这样设计的好处是产品入口和表结构先稳定,不会等 BPM 联调完才让用户看见按钮。

不要把「能点按钮」宣传成「邮件一键变审批单已经闭环」。闭环要等 bizId 回填、从待办能跳回这封邮件。

读信区 @bpm="toBpm" 目前是把关联请求打到 createMailBizLink。产品验收时分两档:

  • P0
    :关联行能查到,类型、标题、邮件 ID 正确;
  • P1
    :TODO 真插入待办、BPM 真发起实例并回写 bizId。

把 P1 当成 P0,会在客户演示里当场翻车。


六、和系统邮件模块怎么共存

平台仍需要「忘记密码」「流程超时」这种系统 SMTP。那是基础设施/系统管理里的发信账号和模板,发件人往往是 noreply@。

OA 邮箱客户端是另一条线:员工自己的收件箱。两套配置不要共用一张账号表,否则管理员改系统发信密码,会误伤全员 IMAP。RuoYi Office 把它们分在不同菜单:系统「邮箱管理」vs 办公「邮箱 / 收件箱」。

验收时用两封信区分:

  1. 走流程超时提醒——收件人邮箱里能看到,但 OA 收件箱不一定同步得到(取决于那封信是否进了员工 IMAP);
  2. 用 OA 客户端写给同事——对方 OA 收件箱同步后应出现,已发送里也应有。

如果 2 失败、1 成功,说明系统 SMTP 通、员工 IMAP/SMTP 不通,去查账号模板而不是「邮件模块坏了」。


七、数据结构

表
作用
oa_mail_provider_profile
IMAP/SMTP 模板
oa_mail_tenant_policy
租户是否允许自绑、分配模式
oa_mail_account
用户邮箱身份、测连、最近同步
oa_mail_folder_state
规范夹 ↔ IMAP 名、last_uid
oa_mail_message_index
列表索引、草稿 HTML
oa_mail_attachment
附件元数据与存储 URL
oa_mail_sync_log
每次同步计数与失败原因
oa_mail_biz_link
邮件与待办/流程的挂接

索引表不存完整 MIME。附件走文件存储,和云盘一样避免把邮件库撑成对象存储。


八、技术亮点

要点
实现
价值
协议收敛
一个 Connector 测连/拉信/发信
业务服务不直接 new Session
规范文件夹
别名映射 Sent/Trash
兼容腾讯邮与自建 Dovecot
增量 UID
folder_state.last_uid
同步可秒级,不打满 IMAP
列表本地化
message_index 分页
搜索主题不经过公网邮服
正文延迟拉
详情才 FETCH
索引表保持瘦
三栏 UI
文件夹/列表/读信可替换右栏
回复不用丢上下文
账号与系统发信分离
两套菜单两套表
改 noreply 不影响员工邮箱
业务挂接预埋
biz_link + TODO/BPM 类型
入口先在,闭环可迭代

九、快速体验

在线演示:https://ruoyioffice.com/web/(账号 admin / admin123)

  1. 打开 办公 → 邮箱 → 邮箱服务模板,看 IMAP/SMTP 是否已有演示模板;
  2. 邮箱账号设置
     里确认当前用户已绑定账号,必要时点测试连接;
  3. 进 收件箱,点同步,再点一封有正文的信;
  4. 点写邮件或回复,观察右栏切到写信而不整页刷新;
  5. 若演示邮服未启动,列表可能为空——这恰好说明客户端依赖真实 IMAP,不是假数据。

源码仓库:GitHub:https://github.com/yuqing2026/ruoyi-office | GitCode:https://gitcode.com/zhouzhongyan/ruoyi-office | Gitee:https://gitee.com/yqzy1688/ruoyi-office

本地排障顺序:

  1. 账号页点测试连接——IMAP、SMTP 是否都过;
  2. 同步日志有没有 successCount,last_error 是否端口/认证;
  3. 索引表有信封但详情无正文——FETCH 文件夹名是否映射错;
  4. 能收不能发——查 465/587 与 SSL 开关,而不是再同步一次。

相关阅读:系统通知与站内信、Flowable 待办工作台。邮箱客户端补的是「外部世界写进来的信」,待办补的是「系统里待你处理的任务」。


常见问题(FAQ)

OA 里已经有邮件通知,为什么还要 IMAP 客户端?

通知是系统推给你的事件。客户询价、供应商对账仍走企业邮箱。两套入口可以同时存在,不要用通知模块冒充收件箱。

为什么不每次打开都实时 LIST 全部邮件?

可以,但翻页会慢,而且容易触发邮服限流。信封进本地索引后,搜索和未读计数都是数据库查询。正文仍实时 FETCH,保证看到的是服务器最新内容。

授权码和登录密码有什么区别?

多数企业邮要求 IMAP 使用客户端专用授权码,不能用网页登录密码。测连失败时先核对该项,再查 993/465 端口和 SSL 开关。

邮件能直接变成审批单吗?

现在能把邮件和 TODO/BPM 类型写进关联表,并从读信区点入口。真正创建待办、发起流程实例仍是下一阶段。先不要按「已经闭环」去验收。

移动端有邮箱吗?

当前以 PC 三栏为主,UniApp 尚未做对等页。和云盘相反:云盘移动端先做浏览;邮箱移动端要处理 HTML 正文、附件、回复引用,工作量更大,所以后做。


结语

把邮箱塞进 OA,关键不是再画一个「发邮件」按钮,而是承认 系统 SMTP 和员工 IMAP 是两条产品线。RuoYi Office 用连接器管协议,用索引表管列表,用三栏页管阅读和回复,用模板/策略/账号管租户差异,再用 oa_mail_biz_link 把「这封信」和未来的待办、流程钉在一起。

如果你正在给现有 SpringBoot3 后台加企业邮箱,建议按这个顺序验收:测连双通 → 增量同步有 last_uid → 列表不打 IMAP → 点开有正文 → 发送能在已发送里看到。这五步过了,再谈规则引擎和一键转审批。

你们现在邮件是网页版企业邮、还是全员 POP 到 Outlook?评论区说说痛点。

如果这篇对你有用,欢迎点赞支持。
 我们会持续分享企业软件开发实践与 RuoYi Office 产品进展。


💡 想要体验 RuoYi Office 的强大功能?

🌐 在线演示:https://ruoyioffice.com/web/(账号 admin / admin123)

📦 源码仓库:GitHub:https://github.com/yuqing2026/ruoyi-office | GitCode:https://gitcode.com/zhouzhongyan/ruoyi-office | Gitee:https://gitee.com/yqzy1688/ruoyi-office

💬 技术咨询:添加微信 17156169080,备注「RuoYi Office」

⭐ 如果觉得不错,请给个 Star 支持一下!

【声明】内容源于网络
0
0
企业软件源码
RuoyiOffice 是一套基于 Spring Boot + Vue3 +Uniapp 的企业一体化管理平台,集 OA、CRM、ERP、工作流、HR、资产、合同、项目、AI应用等业务于一体,帮助企业用一个系统协同管理多类核心业务。
内容 155
粉丝 0
企业软件源码 RuoyiOffice 是一套基于 Spring Boot + Vue3 +Uniapp 的企业一体化管理平台,集 OA、CRM、ERP、工作流、HR、资产、合同、项目、AI应用等业务于一体,帮助企业用一个系统协同管理多类核心业务。
总阅读3.1k
粉丝0
内容155