【办公】企业邮箱
企业邮箱,由 yudao-module-oa 后端模块的 mail 包实现,前端实现在 @/views/oa/mail/provider、@/views/oa/mail/account、@/views/oa/mail/inbox 目录。
企业邮箱通过 IMAP 读取邮件,通过 SMTP 发送邮件。管理员维护邮件服务配置,用户绑定自己的邮箱账号;文件夹、邮件索引和已读取正文分别保存,附件文件仍从远端邮件服务器读取。
本文涉及表如下图所示:
# 1. 邮箱服务配置
邮箱服务配置,由 OaMailProviderController 提供接口(/oa/mail-provider)。
# 1.1 主表表结构
省略 creator/create_time/updater/update_time/deleted/tenant_id 等通用字段
邮箱服务配置表 oa_mail_provider,保存企业统一的 IMAP、SMTP 连接参数:
CREATE TABLE `oa_mail_provider` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
`name` varchar(255) NOT NULL COMMENT '名称',
`imap` json NOT NULL COMMENT 'IMAP 连接配置',
`smtp` json NOT NULL COMMENT 'SMTP 连接配置',
`status` tinyint NOT NULL COMMENT '状态',
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='企业邮箱服务配置';
① name 是服务配置的展示名称。同一租户可以配置多套服务商参数,供个人账号绑定时选择。删除前必须先解除账号引用,由 OaMailProviderServiceImpl 的 deleteMailProvider 调用 getMailAccountCountByProviderId 校验,仍有关联账号时抛出 MAIL_PROVIDER_IN_USE 异常。
② imap、smtp 以 JSON 保存对应协议的连接配置,结构相同,对应 OaMailProviderDO.ConnectionConfig。SSL 与 STARTTLS 二选一,由 OaMailProviderSaveReqVO.ConnectionConfig.isEncryptionValid 校验(@AssertTrue):
imap / smtp(连接配置)
{
"host": "imap.example.com",
"port": 993,
"sslEnable": true,
"starttlsEnable": false
}
| 字段 | 类型 | 说明 |
|---|---|---|
host | String | 服务器域名 |
port | Integer | 服务器端口,取值 1 ~ 65535 |
sslEnable | Boolean | 是否开启 SSL |
starttlsEnable | Boolean | 是否开启 STARTTLS |
SMTP 示例通常为 host = smtp.example.com、port = 465(SSL)或 587(STARTTLS),字段结构与 IMAP 一致。
③ 枚举 status 启用状态(CommonStatusEnum),对应字典 common_status。本模块没有 OA_MAIL_* 字典,服务配置与个人账号共用框架通用状态:
| 值 | 枚举 | 说明 |
|---|---|---|
0 | ENABLE | 开启 |
1 | DISABLE | 关闭 |
绑定账号、设置默认账号、连接测试前,都会通过 validateMailProviderEnabled 校验服务配置处于开启状态,否则抛出 MAIL_PROVIDER_DISABLED。
# 1.2 管理后台
对应 [OA 办公协同 -> 企业邮箱 -> 邮箱服务配置] 菜单,对应 yudao-ui-admin-vue3 项目的 @/views/oa/mail/provider 目录。
# 列表
查看已配置的邮件服务及启用状态。

# 新增与修改
点击【新增】打开 MailProviderForm.vue,填写服务名称、IMAP 和 SMTP 配置。主机、端口及安全连接参数应与企业邮箱服务商提供的参数一致,保存后供个人账号绑定时选择。

# 删除
点击【删除】并确认。服务配置存在关联账号时,应先处理账号绑定关系,再删除服务配置。
# 2. 我的邮箱账号
我的邮箱账号,由 OaMailAccountController 提供接口(/oa/mail-account)。
# 2.1 主表表结构
省略 creator/create_time/updater/update_time/deleted/tenant_id 等通用字段
个人账号表 oa_mail_account,保存用户绑定的邮箱地址和登录凭据:
CREATE TABLE `oa_mail_account` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
`provider_id` bigint NOT NULL COMMENT '服务配置编号',
`mail` varchar(255) NOT NULL COMMENT '邮箱地址',
`username` varchar(255) NOT NULL COMMENT '登录用户名',
`password` text NOT NULL COMMENT '密码或授权码',
`default_status` bit(1) NOT NULL COMMENT '是否默认发件账号',
`status` tinyint NOT NULL COMMENT '状态',
`default_creator` varchar(64) GENERATED ALWAYS AS (CASE WHEN default_status = b'1' AND deleted = b'0' THEN creator ELSE NULL END) STORED,
PRIMARY KEY (`id`),
KEY `idx_creator` (`tenant_id`, `creator`, `deleted`),
KEY `idx_provider` (`tenant_id`, `provider_id`, `deleted`),
UNIQUE KEY `uk_default_creator` (`tenant_id`, `default_creator`)
) ENGINE=InnoDB COMMENT='企业邮箱个人账号';
① provider_id 关联 oa_mail_provider 表的 id 字段。账号没有独立的所属人字段,通用字段 creator 即绑定该账号的后台用户(关联 system_users 表的 id 字段),由 OaMailAccountServiceImpl 的 validateMailAccount 校验归属。idx_creator 支撑【我的邮箱账号】按用户查询。
② mail 是邮箱地址,username 是服务端登录名,password 保存加密后的密码或授权码。mail / username / provider_id 绑定后不可修改,修改时若三者任一变化,updateMailAccount 抛出 MAIL_ACCOUNT_IDENTITY_IMMUTABLE 异常——换邮箱需新增绑定,避免旧邮件索引归到新账号。
③ default_status 标记是否为当前用户的默认发件账号,不是全局默认邮箱。生成列 default_creator + 唯一索引 uk_default_creator 保证每个用户至多一个默认账号;设置默认时由 updateMailAccountDefault / createMailAccount 先 clearDefaultAccounts 再写入。关闭状态的账号不能设为默认,否则抛出 MAIL_ACCOUNT_DISABLED。
④ 枚举 status 启用状态同样使用 CommonStatusEnum,对应字典 common_status(0 开启 / 1 关闭),与服务配置一致。
⑤ 删除账号只移除本地绑定(deleteMailAccount):不解绑远端邮箱服务商账号,也不级联清理 oa_mail_folder / oa_mail_message。
# 2.2 管理后台
对应 [OA 办公协同 -> 企业邮箱 -> 我的邮箱账号] 菜单,对应 yudao-ui-admin-vue3 项目的 @/views/oa/mail/account 目录。
# 绑定与修改
点击【新增】打开 MailAccountForm.vue,选择邮箱服务,填写邮箱地址、登录名和密码或授权码,设置启用状态后保存。

# 连接测试与默认账号
通过连接测试确认账号可以访问邮箱服务,由 POST /oa/mail-account/test-connection 提供接口(testMailAccountConnection),分别探测 IMAP 与 SMTP,不发送真实邮件。设置默认账号时调用 PUT /oa/mail-account/update-default,写信时使用当前用户的默认发件账号。
# 移除绑定
删除账号用于移除本人邮箱绑定;企业邮箱中的远端账号仍由邮件服务商管理。
# 3. 收件箱与邮件
收件箱与邮件,由 OaMailMessageController 提供接口(/oa/mail-message);文件夹列表由 OaMailFolderController 提供接口(/oa/mail-folder)。
# 3.1 文件夹表结构
省略 creator/create_time/updater/update_time/deleted/tenant_id 等通用字段
文件夹表 oa_mail_folder,保存远端邮箱的文件夹及其同步进度:
CREATE TABLE `oa_mail_folder` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
`account_id` bigint NOT NULL COMMENT '邮箱账号编号',
`name` varchar(255) NOT NULL COMMENT '远端文件夹完整名称',
`type` varchar(20) NOT NULL COMMENT '文件夹类型',
`uid_validity` bigint DEFAULT NULL COMMENT '远端文件夹 UID 有效期',
`sync_time` datetime DEFAULT NULL COMMENT '最近完整同步时间',
`available` bit(1) NOT NULL DEFAULT b'1' COMMENT '远端文件夹是否可用',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_account_folder` (`tenant_id`, `account_id`, `name`)
) ENGINE=InnoDB COMMENT='企业邮箱文件夹';
① account_id 关联 oa_mail_account 表的 id 字段。name 是远端文件夹的完整路径名,uk_account_folder 唯一索引保证同一账号下远端文件夹名不重复。
② 枚举 type 文件夹类型(OaMailFolderTypeEnum)。本模块没有对应字典,类型值直接使用字符串常量:
| 值 | 枚举 | 说明 |
|---|---|---|
INBOX | INBOX | 收件箱 |
SENT | SENT | 已发送 |
DRAFTS | DRAFTS | 草稿箱 |
TRASH | TRASH | 已删除 |
CUSTOM | CUSTOM | 自定义文件夹 |
③ uid_validity 记录远端文件夹的 UIDVALIDITY,与邮件的 uid 一起定位远端邮件。available 标记远端是否还存在该文件夹:同步时若远端已删除,本地只把 available 置为 false,保留旧索引但不再展示。由 OaMailFolderServiceImpl 的 validateMailFolder 校验文件夹归属账号。
④ 同步不会在远端创建目录,只映射服务商已有的标准 / 自定义文件夹(syncMailMessageList → getFolders)。
该功能还包含邮件索引表:
oa_mail_message(邮件索引):在同步、查看、写信时维护,保存主题、收发人和阅读状态;正文按需缓存。
# 3.2 邮件索引表结构
省略 creator/create_time/updater/update_time/deleted/tenant_id 等通用字段
邮件索引表 oa_mail_message,保存邮件的主题、收发人和阅读状态;附件文件仍从远端读取:
CREATE TABLE `oa_mail_message` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
`account_id` bigint NOT NULL COMMENT '邮箱账号编号',
`folder_id` bigint NOT NULL COMMENT '文件夹编号',
`uid_validity` bigint NOT NULL COMMENT '远端文件夹 UID 有效期',
`uid` bigint NOT NULL COMMENT '远端邮件 UID',
`subject` longtext NOT NULL COMMENT '邮件主题',
`content` longtext DEFAULT NULL COMMENT '安全正文缓存,NULL 表示未读取',
`sender` text COMMENT '发件人',
`recipients` json COMMENT '收件人',
`ccs` json COMMENT '抄送人',
`reply_tos` json DEFAULT NULL COMMENT '回复地址',
`receive_time` datetime DEFAULT NULL COMMENT '接收时间',
`read_status` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否已读',
`has_attach` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否有附件',
`attachments` json DEFAULT NULL COMMENT '附件目录缓存,附件文件仍从远端读取',
`size` int DEFAULT NULL COMMENT '邮件大小,单位字节',
PRIMARY KEY (`id`),
KEY `idx_folder_uid` (`tenant_id`, `folder_id`, `uid_validity`, `uid`, `deleted`),
KEY `idx_account_folder_time` (`tenant_id`, `account_id`, `folder_id`, `deleted`, `receive_time`)
) ENGINE=InnoDB COMMENT='企业邮箱邮件索引';
① account_id 关联 oa_mail_account 表的 id 字段,folder_id 关联 oa_mail_folder 表的 id 字段。远端主键是 (uid_validity, uid),idx_folder_uid 支撑按文件夹定位远端邮件;idx_account_folder_time 支撑收件箱按接收时间分页。归属由 validateMessage → validateMailAccount 校验。
② content 是经安全过滤后的正文缓存:null 表示尚未拉取,空字符串表示正文为空。首次打开详情(getMailMessage)才从远端读取并回写;列表同步一般只写索引字段。
③ recipients、ccs、reply_tos、attachments 均为 JSON。附件只缓存目录元数据(部件路径、名称、大小),文件内容仍按需从远端读取(getMailMessageAttachment):
recipients / ccs / reply_tos(地址列表)与 attachments(附件目录)
地址类字段均为字符串数组,例如:
["alice@example.com", "bob@example.com"]
attachments 对应 OaMailMessageDO.Attachment:
[
{
"part": "1.2",
"name": "需求说明.pdf",
"size": 204800
}
]
| 字段 | 类型 | 说明 |
|---|---|---|
part | String | MIME 部件路径 |
name | String | 附件名称 |
size | Integer | 附件大小,单位字节 |
④ read_status 是否已读。更新时(updateMailMessageRead)会同步远端 SEEN 标志。前端虚拟入口 UNREAD = 收件箱(INBOX)+ readStatus = false,由 OaMailFolderKeyEnum.UNREAD 标识,不是真实远端文件夹。
# 3.3 同步与删除流转
邮件同步与删除由 OaMailMessageServiceImpl / OaMailMessageClientImpl 控制。文件夹类型见 §3.1,阅读状态见上表 read_status。
由于各家邮件服务商支持的 IMAP 扩展不同,写操作前会先用 hasCapability 探测远端能力(OaMailCapabilityEnum),缺失时直接抛异常而不降级:
| 值 | 枚举 | 说明 |
|---|---|---|
UIDPLUS | UID_PLUS | 按 UID 操作,彻底删除与草稿替换必需 |
MOVE | MOVE | 移动邮件,删除到已删除文件夹必需 |
ID | ID | 客户端身份,部分服务商(如网易)要求上报后才允许收发 |
| 场景 | 关键能力 | 说明 | 可执行操作 |
|---|---|---|---|
| 同步索引 | 账号行锁 | lockMailAccount 串行化同一账号的同步与写操作 | 同步、查看、已读、删除、草稿、发送 |
| 非回收站删除 | IMAP MOVE | 移到远端 TRASH | 删除(软删远端) |
| 回收站删除 | IMAP UIDPLUS | 按 UID expunge 指定邮件 | 彻底删除 |
| 草稿 / 发送 | IMAP UIDPLUS | 草稿替换、发送后清理依赖 UIDPLUS | 保存草稿、发送 |
状态流转说明
同步(syncMailMessageList)
└─ 映射远端已有文件夹 → 写入/更新 oa_mail_folder + oa_mail_message 索引
(不在远端新建目录;远端已消失的文件夹 → available = false)
删除(deleteMailMessage)
普通文件夹 ──IMAP MOVE──→ TRASH(远端移入已删除,本地删索引)
已在 TRASH ──UIDPLUS expunge──→ 远端物理清除指定 UID(本地删索引)
草稿(saveMailMessageDraft) / 发送(sendMailMessage)
└─ 依赖 UIDPLUS;草稿先追加再按 UID 清理旧草稿,不允许全文件夹 expunge
- 同步(
syncMailMessageList):先lockMailAccount加账号锁,再打开 IMAP Store,按远端已有文件夹逐个syncFolder。不会创建远端目录;远端已不存在的文件夹只把本地available置为false。 - 删除(
deleteMailMessage→deleteMessage):当前不在TRASH时,要求服务商支持MOVE,把邮件移到已删除文件夹;已在TRASH时要求UIDPLUS,对该 UID 执行expunge。远端成功后再删本地索引。 - 草稿 / 发送:
saveMailMessageDraft、发送流程依赖UIDPLUS做按 UID 的安全替换;缺少能力时分别抛出MAIL_DRAFT_REPLACE_UNSUPPORTED、MAIL_PERMANENT_DELETE_UNSUPPORTED等异常。 - 未读入口:
getMailFolderList在收件箱后插入虚拟UNREAD;getMailMessagePage把folderKey = UNREAD转为INBOX+readStatus = false。
# 3.4 管理后台
对应 [OA 办公协同 -> 企业邮箱 -> 收件箱] 菜单,对应 yudao-ui-admin-vue3 项目的 @/views/oa/mail/inbox 目录。
# 同步与查询
选择本人邮箱账号和文件夹后查看邮件;同步通过 POST /oa/mail-message/sync 拉取邮件索引,列表由 MailMessageList.vue 展示。侧边栏可切换收件箱、未读、已发送、草稿箱、已删除及自定义文件夹。

# 查看邮件
点击邮件打开 MailMessageDetail.vue,查看发件人、收件人、抄送人、正文和附件。邮件详情由 /oa/mail-message/get 提供,阅读状态通过 /oa/mail-message/update-read 更新。

# 写信与保存草稿
打开 MailMessageForm.vue,选择发件账号,填写收件人、抄送人、主题、正文和附件。保存草稿调用 POST /oa/mail-message/save-draft,发送调用 POST /oa/mail-message/send。

# 回复与转发
在邮件详情中进入回复、回复全部或转发,前端调用 GET /oa/mail-message/compose 获得预填信息,再打开写信表单。确认收件人和正文后发送。
mode 写信方式(OaMailComposeModeEnum)决定预填内容,接口用 @InEnum 限定取值:
| 值 | 枚举 | 说明 |
|---|---|---|
new | NEW | 新邮件,不预填;不作为 compose 的入参,仅作为草稿回显时返回给前端的模式 |
draft | DRAFT | 编辑草稿,原样回填收件人、抄送人、主题、正文和附件,并带上 draftId |
reply | REPLY | 回复,收件人取 reply_tos,为空时回落到 sender;主题加 Re: 前缀 |
replyAll | REPLY_ALL | 回复全部,在回复基础上把原收件人并入收件人、原抄送人并入抄送人 |
forward | FORWARD | 转发,主题加 Fwd: 前缀,收件人留空,并携带原附件 |
draft要求原邮件确实位于草稿箱,否则抛出MAIL_NOT_DRAFT;回显时接口把mode改写为new,前端按新邮件处理。- 只有
draft和forward会带上原附件;reply/replyAll不携带。 - 所有地址都会剔除本人邮箱并去重,且收件人中已出现的地址会从抄送人中移除。
reply/replyAll/forward的正文以blockquote引用原文,原始邮件头做 HTML 转义,并移除邮件自带 CSS 避免样式泄漏。
# 删除邮件
选择邮件执行删除,调用 DELETE /oa/mail-message/delete。该操作涉及远端邮箱,确认所选账号和文件夹后再执行;非已删除文件夹会先移到回收站,已在回收站则彻底清除。