HarmonyOS开发:邮件应用——企业邮箱
HarmonyOS开发:邮件应用——企业邮箱
核心要点:企业邮箱不是"收发邮件"那么简单——IMAP/SMTP协议对接、增量同步省流量、附件预览与安全、邮件搜索与归档,每个环节都有坑。
背景与动机
你打开邮箱App,等了10秒邮件列表才出来。点开一封邮件,又等了5秒。附件?点一下下载,又等了30秒。你搜索一封上个月的邮件,转了半天圈告诉你"未找到"。
用户对邮箱的忍耐度极低——它就是个工具,打开就要能用,慢一秒都嫌多。
企业邮箱比个人邮箱更复杂:
- 协议对接:IMAP收信、SMTP发信、Exchange同步,三种协议各有各的坑
- 增量同步:不能每次打开都全量拉取,几千封邮件拉一次得多久?
- 附件处理:50MB的PPT附件,下载还是预览?安全扫描怎么做?
- 邮件搜索:按发件人、主题、时间、附件类型搜索,本地搜索还是服务端搜索?
- 安全合规:邮件可能包含机密信息,截屏要管、转发要管、离职要清
鸿蒙做邮箱有个天然优势——后台推送。新邮件到达时通过Push Kit推送通知,不需要App轮询,省电省流量。
核心原理
企业邮箱的架构核心:协议适配 + 增量同步 + 本地索引 + 安全管控。
flowchart TD
subgraph 协议层
A[IMAP4<br/>收信协议]
B[SMTP<br/>发信协议]
C[Exchange<br/>企业同步]
end
subgraph 同步层
D[增量同步引擎<br/>UID+Flag比对]
E[推送通知<br/>Push Kit]
F[离线队列<br/>断网缓存]
end
subgraph 存储层
G[邮件RDB<br/>本地数据库]
H[全文索引<br/>邮件搜索]
I[附件缓存<br/>LRU策略]
end
subgraph 安全层
J[SSL/TLS加密]
K[附件安全扫描]
L[防截屏/防转发]
end
A --> D
B --> F
C --> D
E --> D
D --> G
D --> H
D --> I
A --> J
B --> J
D --> K
D --> L
classDef protocol fill:#1565C0,color:#fff,stroke:#0D47A1
classDef sync fill:#2E7D32,color:#fff,stroke:#1B5E20
classDef storage fill:#E65100,color:#fff,stroke:#BF360C
classDef security fill:#C62828,color:#fff,stroke:#B71C1C
class A,B,C protocol
class D,E,F sync
class G,H,I storage
class J,K,L security
IMAP协议要点
IMAP(Internet Message Access Protocol)是收信协议,核心概念:
- Folder:邮件文件夹,如INBOX、Sent、Drafts、Trash
- UID:邮件唯一标识,全局唯一,不会变
- Flag:邮件标记,如\Seen(已读)、\Flagged(星标)、\Deleted(已删除)
- UIDVALIDITY:文件夹UID有效性标识,变了说明UID重新分配了
增量同步的核心:用UID和Flag比对,只拉取新增和变化的邮件。
SMTP协议要点
SMTP(Simple Mail Transfer Protocol)是发信协议,核心流程:
- 建立TCP连接
- EHLO握手,协商认证方式和加密
- STARTTLS升级为加密连接
- AUTH LOGIN认证
- MAIL FROM指定发件人
- RCPT TO指定收件人
- DATA发送邮件内容
- QUIT断开连接
增量同步策略
| 策略 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| UID比对 | 拉取服务端UID列表,与本地比对 | 精确,不遗漏 | 需要拉取完整UID列表 |
| Flag比对 | 只拉取Flag变化的邮件 | 高效 | 可能遗漏新邮件 |
| 时间窗口 | 只拉取最近N天的邮件 | 简单 | 历史邮件不同步 |
| 推送驱动 | 收到推送后触发同步 | 最省流量 | 依赖推送服务 |
推荐组合:推送驱动 + UID比对 + Flag比对。收到推送后同步,同步时先比对Flag(快速),再比对UID(精确)。
代码实战
基础用法:IMAP收信与SMTP发信
先实现最核心的功能——连接邮件服务器、收邮件、发邮件。
// EmailClient.ets - 邮件客户端核心
import { socket } from '@kit.NetworkKit';
import { relationalStore } from '@kit.ArkData';
import { BusinessError } from '@kit.BasicServicesKit';
// 邮件账户配置
export interface EmailAccount {
accountId: string; // 账户ID
email: string; // 邮箱地址
displayName: string; // 显示名称
imapServer: string; // IMAP服务器
imapPort: number; // IMAP端口
smtpServer: string; // SMTP服务器
smtpPort: number; // SMTP端口
username: string; // 用户名
password: string; // 密码(加密存储)
useSSL: boolean; // 是否使用SSL
}
// 邮件信息
export interface EmailMessage {
messageId: string; // 邮件ID
uid: number; // IMAP UID
folder: string; // 所属文件夹
from: EmailAddress; // 发件人
to: EmailAddress[]; // 收件人
cc: EmailAddress[]; // 抄送
bcc: EmailAddress[]; // 密送
subject: string; // 主题
body: string; // 正文(纯文本)
htmlBody: string; // 正文(HTML)
date: number; // 日期
flags: string[]; // 标记
attachments: Attachment[]; // 附件列表
isRead: boolean; // 已读
isStarred: boolean; // 星标
isEncrypted: boolean; // 是否加密邮件
}
// 邮件地址
export interface EmailAddress {
name: string; // 显示名称
address: string; // 邮箱地址
}
// 附件
export interface Attachment {
fileName: string; // 文件名
fileSize: number; // 文件大小
contentType: string; // MIME类型
contentId?: string; // 内嵌资源ID
downloadUrl?: string; // 下载URL
localPath?: string; // 本地路径
}
// 同步状态
export interface SyncStatus {
folder: string;
lastSyncTime: number;
lastUid: number;
uidValidity: string;
totalMessages: number;
syncedMessages: number;
}
export class EmailClient {
private static instance: EmailClient;
private account: EmailAccount | null = null;
private imapConnection: socket.TLSSocket | null = null;
private smtpConnection: socket.TLSSocket | null = null;
private isConnected: boolean = false;
private constructor() {}
static getInstance(): EmailClient {
if (!EmailClient.instance) {
EmailClient.instance = new EmailClient();
}
return EmailClient.instance;
}
// 配置邮件账户
setAccount(account: EmailAccount): void {
this.account = account;
console.info(`[Email] 账户已配置: ${account.email}`);
}
// 连接IMAP服务器
async connectIMAP(): Promise<boolean> {
if (!this.account) {
console.error('[Email] 未配置账户');
return false;
}
try {
// 创建TLS连接
this.imapConnection = socket.constructTLSSocket();
await this.imapConnection.connect({
address: {
address: this.account.imapServer,
port: this.account.imapPort,
family: 1, // IPv4
},
secureOptions: {
checkServerIdentity: (hostname: string, cert: string) => {
// 验证服务器证书
if (hostname !== this.account!.imapServer) {
return new Error('证书主机名不匹配');
}
return undefined;
},
},
});
// 等待服务器问候
const greeting = await this.readIMAPResponse();
console.info(`[Email] IMAP连接成功: ${greeting}`);
// 登录认证
await this.sendIMAPCommand(`LOGIN ${this.account.username} ${this.account.password}`);
const loginResult = await this.readIMAPResponse();
if (loginResult.includes('OK')) {
this.isConnected = true;
console.info('[Email] IMAP登录成功');
return true;
} else {
console.error('[Email] IMAP登录失败');
return false;
}
} catch (error) {
console.error(`[Email] IMAP连接失败: ${JSON.stringify(error)}`);
return false;
}
}
// 选择文件夹
async selectFolder(folder: string): Promise<SyncStatus | null> {
if (!this.isConnected) return null;
try {
await this.sendIMAPCommand(`SELECT ${folder}`);
const response = await this.readIMAPResponse();
// 解析响应,获取邮件数量和UIDVALIDITY
const existsMatch = response.match(/(\d+) EXISTS/);
const uidValidityMatch = response.match(/UIDVALIDITY (\d+)/);
return {
folder,
lastSyncTime: Date.now(),
lastUid: 0,
uidValidity: uidValidityMatch?.[1] || '',
totalMessages: parseInt(existsMatch?.[1] || '0'),
syncedMessages: 0,
};
} catch (error) {
console.error(`[Email] 选择文件夹失败: ${JSON.stringify(error)}`);
return null;
}
}
// 增量同步——拉取新邮件
async syncNewEmails(folder: string, lastUid: number): Promise<EmailMessage[]> {
if (!this.isConnected) return [];
try {
// 搜索大于lastUid的邮件UID
await this.sendIMAPCommand(`UID SEARCH UID ${lastUid + 1}:*`);
const searchResult = await this.readIMAPResponse();
// 解析UID列表
const uidPattern = /\d+/g;
const uids: number[] = [];
let match;
while ((match = uidPattern.exec(searchResult)) !== null) {
const uid = parseInt(match[0]);
if (uid > lastUid) uids.push(uid);
}
if (uids.length === 0) {
console.info('[Email] 没有新邮件');
return [];
}
// 批量拉取邮件内容
const emails: EmailMessage[] = [];
for (const uid of uids) {
const email = await this.fetchEmail(uid, folder);
if (email) emails.push(email);
}
console.info(`[Email] 同步完成: ${emails.length}封新邮件`);
return emails;
} catch (error) {
console.error(`[Email] 同步失败: ${JSON.stringify(error)}`);
return [];
}
}
// 拉取单封邮件
private async fetchEmail(uid: number, folder: string): Promise<EmailMessage | null> {
try {
// 拉取邮件头
await this.sendIMAPCommand(`UID FETCH ${uid} (FLAGS BODY[HEADER.FIELDS (FROM TO CC SUBJECT DATE)])`);
const headerResponse = await this.readIMAPResponse();
// 拉取邮件正文
await this.sendIMAPCommand(`UID FETCH ${uid} BODY[TEXT]`);
const bodyResponse = await this.readIMAPResponse();
// 解析邮件(简化实现)
return this.parseEmail(uid, folder, headerResponse, bodyResponse);
} catch (error) {
console.error(`[Email] 拉取邮件失败: uid=${uid}`);
return null;
}
}
// 发送邮件
async sendEmail(
to: EmailAddress[],
cc: EmailAddress[],
subject: string,
body: string,
attachments?: Attachment[]
): Promise<boolean> {
if (!this.account) return false;
try {
// 构建SMTP连接
this.smtpConnection = socket.constructTLSSocket();
await this.smtpConnection.connect({
address: {
address: this.account.smtpServer,
port: this.account.smtpPort,
family: 1,
},
secureOptions: {},
});
// SMTP握手和认证(简化)
await this.readSMTPResponse(); // 服务器问候
await this.sendSMTPCommand('EHLO localhost');
await this.readSMTPResponse();
await this.sendSMTPCommand('STARTTLS');
await this.readSMTPResponse();
await this.sendSMTPCommand(`AUTH LOGIN`);
await this.readSMTPResponse();
await this.sendSMTPCommand(Buffer.from(this.account.username).toString('base64'));
await this.readSMTPResponse();
await this.sendSMTPCommand(Buffer.from(this.account.password).toString('base64'));
const authResult = await this.readSMTPResponse();
if (!authResult.includes('235')) {
console.error('[Email] SMTP认证失败');
return false;
}
// 发送邮件内容
await this.sendSMTPCommand(`MAIL FROM:<${this.account.email}>`);
await this.readSMTPResponse();
for (const addr of to) {
await this.sendSMTPCommand(`RCPT TO:<${addr.address}>`);
await this.readSMTPResponse();
}
await this.sendSMTPCommand('DATA');
await this.readSMTPResponse();
// 构建邮件内容
const emailContent = this.buildEmailContent(to, cc, subject, body, attachments);
await this.sendSMTPCommand(emailContent + '\r\n.');
const sendResult = await this.readSMTPResponse();
if (sendResult.includes('250')) {
console.info('[Email] 邮件发送成功');
return true;
}
return false;
} catch (error) {
console.error(`[Email] 发送失败: ${JSON.stringify(error)}`);
return false;
}
}
// 构建邮件内容
private buildEmailContent(
to: EmailAddress[],
cc: EmailAddress[],
subject: string,
body: string,
attachments?: Attachment[]
): string {
const lines: string[] = [];
lines.push(`From: ${this.account!.displayName} <${this.account!.email}>`);
lines.push(`To: ${to.map(a => `${a.name} <${a.address}>`).join(', ')}`);
if (cc.length > 0) {
lines.push(`Cc: ${cc.map(a => `${a.name} <${a.address}>`).join(', ')}`);
}
lines.push(`Subject: =?UTF-8?B?${Buffer.from(subject).toString('base64')}?=`);
lines.push(`Date: ${new Date().toUTCString()}`);
lines.push('MIME-Version: 1.0');
if (attachments && attachments.length > 0) {
// 多部分邮件
const boundary = `----=_Part_${Date.now()}`;
lines.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
lines.push('');
lines.push(`--${boundary}`);
lines.push('Content-Type: text/plain; charset=UTF-8');
lines.push('Content-Transfer-Encoding: base64');
lines.push('');
lines.push(Buffer.from(body).toString('base64'));
lines.push('');
for (const att of attachments) {
lines.push(`--${boundary}`);
lines.push(`Content-Type: ${att.contentType}; name="${att.fileName}"`);
lines.push('Content-Transfer-Encoding: base64');
lines.push(`Content-Disposition: attachment; filename="${att.fileName}"`);
lines.push('');
// 附件内容(实际需要读取文件并编码)
lines.push('[附件内容]');
lines.push('');
}
lines.push(`--${boundary}--`);
} else {
lines.push('Content-Type: text/plain; charset=UTF-8');
lines.push('Content-Transfer-Encoding: base64');
lines.push('');
lines.push(Buffer.from(body).toString('base64'));
}
return lines.join('\r\n');
}
// 解析邮件(简化实现)
private parseEmail(
uid: number,
folder: string,
headerResponse: string,
bodyResponse: string
): EmailMessage {
// 实际实现需要完整的MIME解析器
return {
messageId: `msg_${uid}`,
uid,
folder,
from: { name: '发件人', address: 'sender@example.com' },
to: [{ name: '我', address: this.account?.email || '' }],
cc: [],
bcc: [],
subject: '邮件主题',
body: '邮件正文',
htmlBody: '',
date: Date.now(),
flags: [],
attachments: [],
isRead: false,
isStarred: false,
isEncrypted: false,
};
}
// 发送IMAP命令
private async sendIMAPCommand(command: string): Promise<void> {
if (!this.imapConnection) return;
await this.imapConnection.send(`${command}\r\n`);
}
// 读取IMAP响应
private async readIMAPResponse(): Promise<string> {
// 简化实现,实际需要处理多行响应和标签匹配
return 'OK';
}
// 发送SMTP命令
private async sendSMTPCommand(command: string): Promise<void> {
if (!this.smtpConnection) return;
await this.smtpConnection.send(`${command}\r\n`);
}
// 读取SMTP响应
private async readSMTPResponse(): Promise<string> {
return '250 OK';
}
// 断开连接
async disconnect(): Promise<void> {
if (this.imapConnection) {
await this.sendIMAPCommand('LOGOUT');
this.imapConnection.close();
this.imapConnection = null;
}
if (this.smtpConnection) {
await this.sendSMTPCommand('QUIT');
this.smtpConnection.close();
this.smtpConnection = null;
}
this.isConnected = false;
console.info('[Email] 已断开连接');
}
}
进阶用法:附件处理与邮件搜索
附件预览和安全扫描,邮件全文搜索。
// EmailAttachmentManager.ets - 附件管理
import { fileIo as fs } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
// 附件安全扫描结果
export interface ScanResult {
isSafe: boolean;
threatType?: 'virus' | 'macro' | 'suspicious';
threatName?: string;
}
export class EmailAttachmentManager {
private static instance: EmailAttachmentManager;
private context: common.UIAbilityContext | null = null;
private cacheDir: string = '';
private maxCacheSize: number = 200 * 1024 * 1024; // 200MB缓存上限
// 下载中的任务
private downloadingTasks: Map<string, number> = new Map(); // url -> progress
private constructor() {}
static getInstance(): EmailAttachmentManager {
if (!EmailAttachmentManager.instance) {
EmailAttachmentManager.instance = new EmailAttachmentManager();
}
return EmailAttachmentManager.instance;
}
init(context: common.UIAbilityContext): void {
this.context = context;
this.cacheDir = context.cacheDir + '/email_attachments';
if (!fs.accessSync(this.cacheDir)) {
fs.mkdirSync(this.cacheDir);
}
}
// 下载附件
async downloadAttachment(
messageId: string,
attachmentIndex: number,
downloadUrl: string,
fileName: string,
onProgress?: (percent: number) => void
): Promise<string | null> {
const localPath = `${this.cacheDir}/${messageId}_${fileName}`;
// 检查是否已下载
if (fs.accessSync(localPath)) {
console.info(`[Attachment] 使用缓存: ${fileName}`);
return localPath;
}
try {
// 检查缓存空间
await this.ensureCacheSpace();
// 下载文件
const http = await import('@kit.NetworkKit').then(m => m.http.createHttp());
const response = await http.requestInStream(downloadUrl, {
method: 'GET',
expectDataType: 'arraybuffer',
});
// 写入本地
const file = fs.openSync(localPath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
fs.writeSync(file.fd, response.result as ArrayBuffer);
fs.closeSync(file);
// 安全扫描
const scanResult = await this.scanAttachment(localPath);
if (!scanResult.isSafe) {
// 不安全的附件,删除并通知用户
fs.unlinkSync(localPath);
console.warn(`[Attachment] 附件不安全: ${scanResult.threatName}`);
return null;
}
console.info(`[Attachment] 下载完成: ${fileName}`);
return localPath;
} catch (error) {
console.error(`[Attachment] 下载失败: ${JSON.stringify(error)}`);
return null;
}
}
// 安全扫描(简化实现)
private async scanAttachment(filePath: string): Promise<ScanResult> {
const fileName = filePath.split('/').pop() || '';
// 检查危险文件类型
const dangerousExts = ['.exe', '.bat', '.cmd', '.vbs', '.js', '.scr'];
const ext = '.' + fileName.split('.').pop()?.toLowerCase();
if (dangerousExts.includes(ext)) {
return {
isSafe: false,
threatType: 'suspicious',
threatName: `危险文件类型: ${ext}`,
};
}
// 检查文件大小(超过50MB的文件需要额外确认)
const stat = fs.statSync(filePath);
if (stat.size > 50 * 1024 * 1024) {
console.warn('[Attachment] 大文件附件,建议在WiFi环境下载');
}
// 实际项目中需要接入病毒扫描服务
return { isSafe: true };
}
// 确保缓存空间
private async ensureCacheSpace(): Promise<void> {
let totalSize = 0;
const files = fs.listFileSync(this.cacheDir);
for (const file of files) {
const stat = fs.statSync(`${this.cacheDir}/${file}`);
totalSize += stat.size;
}
// 缓存超过上限,清理最旧的文件
if (totalSize > this.maxCacheSize) {
const sortedFiles = files
.map(f => ({
name: f,
path: `${this.cacheDir}/${f}`,
mtime: fs.statSync(`${this.cacheDir}/${f}`).mtime,
}))
.sort((a, b) => a.mtime - b.mtime); // 按修改时间排序
for (const file of sortedFiles) {
fs.unlinkSync(file.path);
totalSize -= fs.statSync(file.path)?.size || 0;
if (totalSize < this.maxCacheSize * 0.7) break; // 清理到70%
}
}
}
// 获取附件预览信息
getAttachmentPreview(filePath: string): { type: string; canPreview: boolean } {
const ext = filePath.split('.').pop()?.toLowerCase() || '';
const previewableTypes = ['pdf', 'jpg', 'jpeg', 'png', 'gif', 'txt', 'md'];
return {
type: ext,
canPreview: previewableTypes.includes(ext),
};
}
}
完整示例:企业邮箱页面
把收信、发信、附件、搜索串起来,做一个完整的邮箱页面。
// EmailPage.ets - 企业邮箱页面
import { EmailClient, EmailMessage, EmailAddress, Attachment } from '../email/EmailClient';
import { EmailAttachmentManager } from '../email/EmailAttachmentManager';
@Entry
@Component
struct EmailPage {
@State emailList: EmailMessage[] = [];
@State selectedEmail: EmailMessage | null = null;
@State currentFolder: string = 'INBOX';
@State isComposing: boolean = false;
@State searchText: string = '';
@State isLoading: boolean = false;
@State unreadCount: number = 0;
// 写邮件表单
@State composeTo: string = '';
@State composeCc: string = '';
@State composeSubject: string = '';
@State composeBody: string = '';
private emailClient: EmailClient = EmailClient.getInstance();
private attachmentManager: EmailAttachmentManager = EmailAttachmentManager.getInstance();
aboutToAppear(): void {
this.loadEmails();
}
build() {
Column() {
// 顶部导航
this.TopBar()
if (this.isComposing) {
// 写邮件
this.ComposeView()
} else if (this.selectedEmail) {
// 邮件详情
this.EmailDetailView()
} else {
// 邮件列表
this.EmailListView()
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
// 顶部导航
@Builder
TopBar() {
Row() {
if (this.selectedEmail || this.isComposing) {
Image($r('app.media.ic_back'))
.width(24).height(24)
.fillColor('#333333')
.onClick(() => {
this.selectedEmail = null;
this.isComposing = false;
})
.margin({ right: 12 })
}
if (this.isComposing) {
Text('写邮件')
.fontSize(18)
.fontWeight(FontWeight.Bold)
} else if (this.selectedEmail) {
Text('邮件详情')
.fontSize(18)
.fontWeight(FontWeight.Bold)
} else {
// 文件夹切换
Row() {
ForEach(['INBOX', 'Sent', 'Drafts', 'Trash'], (folder: string) => {
Text(this.getFolderName(folder))
.fontSize(14)
.fontColor(this.currentFolder === folder ? '#1565C0' : '#666666')
.fontWeight(this.currentFolder === folder ? FontWeight.Bold : FontWeight.Normal)
.padding({ left: 8, right: 8, top: 6, bottom: 6 })
.borderRadius(16)
.backgroundColor(this.currentFolder === folder ? '#E3F2FD' : Color.Transparent)
.onClick(() => {
this.currentFolder = folder;
this.loadEmails();
})
})
}
Blank()
// 搜索
Image($r('app.media.ic_search'))
.width(24).height(24)
.fillColor('#666666')
.margin({ right: 12 })
// 写邮件
Image($r('app.media.ic_edit'))
.width(24).height(24)
.fillColor('#1565C0')
.onClick(() => {
this.isComposing = true;
this.composeTo = '';
this.composeCc = '';
this.composeSubject = '';
this.composeBody = '';
})
}
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.backgroundColor(Color.White)
}
// 邮件列表
@Builder
EmailListView() {
List({ space: 1 }) {
ForEach(this.emailList, (email: EmailMessage) => {
ListItem() {
this.EmailListItem(email)
}
})
}
.layoutWeight(1)
}
// 邮件列表项
@Builder
EmailListItem(email: EmailMessage) {
Row() {
// 头像
Text(email.from.name.charAt(0))
.width(40).height(40)
.fontSize(18)
.fontColor('#FFFFFF')
.textAlign(TextAlign.Center)
.borderRadius(20)
.backgroundColor(this.getAvatarColor(email.from.address))
// 邮件信息
Column() {
Row() {
Text(email.from.name)
.fontSize(15)
.fontWeight(email.isRead ? FontWeight.Normal : FontWeight.Bold)
.layoutWeight(1)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(this.formatEmailTime(email.date))
.fontSize(12)
.fontColor('#999999')
if (email.isStarred) {
Text('⭐').fontSize(12).margin({ left: 4 })
}
}
.width('100%')
Text(email.subject)
.fontSize(14)
.fontColor(email.isRead ? '#666666' : '#333333')
.fontWeight(email.isRead ? FontWeight.Normal : FontWeight.Medium)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 2 })
Row() {
Text(email.body)
.fontSize(13)
.fontColor('#999999')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.layoutWeight(1)
if (email.attachments.length > 0) {
Text('📎')
.fontSize(14)
.margin({ left: 4 })
}
if (!email.isRead) {
Circle()
.width(8).height(8)
.fill('#1565C0')
.margin({ left: 4 })
}
}
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.margin({ left: 12 })
}
.width('100%')
.padding(12)
.backgroundColor(Color.White)
.onClick(() => {
this.selectedEmail = email;
email.isRead = true;
})
}
// 邮件详情
@Builder
EmailDetailView() {
Scroll() {
Column() {
// 主题
Text(this.selectedEmail!.subject)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.width('100%')
// 发件人信息
Row() {
Text(this.selectedEmail!.from.name)
.fontSize(15)
.fontWeight(FontWeight.Medium)
Text(`<${this.selectedEmail!.from.address}>`)
.fontSize(13)
.fontColor('#999999')
.margin({ left: 4 })
}
.width('100%')
.margin({ top: 12 })
// 收件人
Text(`收件人: ${this.selectedEmail!.to.map(t => t.address).join(', ')}`)
.fontSize(13)
.fontColor('#999999')
.width('100%')
.margin({ top: 4 })
// 时间
Text(this.formatDate(this.selectedEmail!.date))
.fontSize(13)
.fontColor('#999999')
.margin({ top: 4 })
Divider().margin({ top: 16, bottom: 16 })
// 正文
Text(this.selectedEmail!.body)
.fontSize(15)
.lineHeight(24)
.width('100%')
// 附件
if (this.selectedEmail!.attachments.length > 0) {
Divider().margin({ top: 16, bottom: 16 })
Text(`附件 (${this.selectedEmail!.attachments.length})`)
.fontSize(14)
.fontWeight(FontWeight.Medium)
.width('100%')
.margin({ bottom: 8 })
ForEach(this.selectedEmail!.attachments, (att: Attachment) => {
Row() {
Text('📎')
.fontSize(20)
.margin({ right: 8 })
Column() {
Text(att.fileName)
.fontSize(14)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(this.formatFileSize(att.fileSize))
.fontSize(12)
.fontColor('#999999')
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Text('下载')
.fontSize(13)
.fontColor('#1565C0')
.onClick(() => this.downloadAttachment(att))
}
.width('100%')
.padding(12)
.borderRadius(8)
.backgroundColor('#F5F5F5')
.margin({ bottom: 8 })
})
}
// 操作按钮
Row() {
Button('回复')
.fontSize(14)
.backgroundColor('#1565C0')
.borderRadius(8)
.onClick(() => this.replyEmail())
Button('转发')
.fontSize(14)
.backgroundColor('#F5F5F5')
.fontColor('#333333')
.borderRadius(8)
.margin({ left: 12 })
.onClick(() => this.forwardEmail())
Button('删除')
.fontSize(14)
.backgroundColor('#FFEBEE')
.fontColor('#C62828')
.borderRadius(8)
.margin({ left: 12 })
}
.margin({ top: 24 })
}
.padding(16)
}
.layoutWeight(1)
.backgroundColor(Color.White)
}
// 写邮件
@Builder
ComposeView() {
Scroll() {
Column() {
// 收件人
Row() {
Text('收件人')
.fontSize(14)
.fontColor('#666666')
.width(60)
TextInput({ placeholder: '输入邮箱地址' })
.layoutWeight(1)
.fontSize(14)
.onChange((value: string) => { this.composeTo = value; })
}
.width('100%')
.padding({ top: 8, bottom: 8 })
Divider()
// 抄送
Row() {
Text('抄送')
.fontSize(14)
.fontColor('#666666')
.width(60)
TextInput({ placeholder: '输入邮箱地址' })
.layoutWeight(1)
.fontSize(14)
.onChange((value: string) => { this.composeCc = value; })
}
.width('100%')
.padding({ top: 8, bottom: 8 })
Divider()
// 主题
Row() {
Text('主题')
.fontSize(14)
.fontColor('#666666')
.width(60)
TextInput({ placeholder: '输入邮件主题' })
.layoutWeight(1)
.fontSize(14)
.onChange((value: string) => { this.composeSubject = value; })
}
.width('100%')
.padding({ top: 8, bottom: 8 })
Divider()
// 正文
TextArea({ placeholder: '输入邮件正文...' })
.width('100%')
.height(300)
.fontSize(15)
.padding(0)
.onChange((value: string) => { this.composeBody = value; })
// 发送按钮
Button('发送')
.width('100%')
.height(44)
.fontSize(16)
.backgroundColor('#1565C0')
.borderRadius(8)
.margin({ top: 16 })
.onClick(() => this.sendEmail())
}
.padding(16)
}
.layoutWeight(1)
.backgroundColor(Color.White)
}
// ========== 业务方法 ==========
// 加载邮件列表
private loadEmails(): void {
this.isLoading = true;
// 模拟数据
this.emailList = [
this.createMockEmail('张经理', 'zhang@company.com', '关于Q3项目进度', '请查收Q3项目进度报告...', false, true),
this.createMockEmail('李总', 'li@company.com', '明天的会议安排', '明天上午10点开会...', true, false),
this.createMockEmail('HR部门', 'hr@company.com', '6月工资条', '您好,6月工资条已发放...', true, false),
this.createMockEmail('系统通知', 'system@company.com', '密码即将过期', '您的企业邮箱密码将在7天后过期...', true, false),
];
this.unreadCount = this.emailList.filter(e => !e.isRead).length;
this.isLoading = false;
}
// 发送邮件
private async sendEmail(): Promise<void> {
if (!this.composeTo || !this.composeSubject) {
console.warn('[Email] 收件人和主题不能为空');
return;
}
const toAddresses: EmailAddress[] = this.composeTo.split(',').map(addr => ({
name: addr.trim(), address: addr.trim()
}));
const ccAddresses: EmailAddress[] = this.composeCc
? this.composeCc.split(',').map(addr => ({ name: addr.trim(), address: addr.trim() }))
: [];
const success = await this.emailClient.sendEmail(
toAddresses, ccAddresses, this.composeSubject, this.composeBody
);
if (success) {
this.isComposing = false;
console.info('[Email] 邮件已发送');
}
}
// 回复邮件
private replyEmail(): void {
if (!this.selectedEmail) return;
this.isComposing = true;
this.composeTo = this.selectedEmail.from.address;
this.composeCc = '';
this.composeSubject = `Re: ${this.selectedEmail.subject}`;
this.composeBody = `\n\n--- 原始邮件 ---\n${this.selectedEmail.body}`;
}
// 转发邮件
private forwardEmail(): void {
if (!this.selectedEmail) return;
this.isComposing = true;
this.composeTo = '';
this.composeCc = '';
this.composeSubject = `Fwd: ${this.selectedEmail.subject}`;
this.composeBody = `\n\n--- 转发邮件 ---\n来自: ${this.selectedEmail.from.address}\n${this.selectedEmail.body}`;
}
// 下载附件
private async downloadAttachment(attachment: Attachment): Promise<void> {
if (!attachment.downloadUrl) return;
const path = await this.attachmentManager.downloadAttachment(
'msg_001', 0, attachment.downloadUrl, attachment.fileName
);
if (path) {
console.info(`[Email] 附件已下载: ${path}`);
}
}
// ========== 辅助方法 ==========
private createMockEmail(
fromName: string, fromAddr: string, subject: string, body: string,
isRead: boolean, isStarred: boolean
): EmailMessage {
return {
messageId: `msg_${Date.now()}_${Math.random().toString(36).substring(2, 6)}`,
uid: Math.floor(Math.random() * 10000),
folder: 'INBOX',
from: { name: fromName, address: fromAddr },
to: [{ name: '我', address: 'me@company.com' }],
cc: [],
bcc: [],
subject, body, htmlBody: '',
date: Date.now() - Math.floor(Math.random() * 86400000),
flags: [],
attachments: [],
isRead, isStarred, isEncrypted: false,
};
}
private getFolderName(folder: string): string {
const map: Record<string, string> = {
'INBOX': '收件箱', 'Sent': '已发送', 'Drafts': '草稿', 'Trash': '已删除',
};
return map[folder] || folder;
}
private getAvatarColor(email: string): string {
const colors = ['#1565C0', '#2E7D32', '#E65100', '#6A1B9A', '#C62828', '#00897B'];
let hash = 0;
for (let i = 0; i < email.length; i++) hash = email.charCodeAt(i) + ((hash << 5) - hash);
return colors[Math.abs(hash) % colors.length];
}
private formatEmailTime(timestamp: number): string {
const diff = Date.now() - timestamp;
if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前`;
if (diff < 86400000) return `${Math.floor(diff / 3600000)}小时前`;
return `${Math.floor(diff / 86400000)}天前`;
}
private formatDate(timestamp: number): string {
const date = new Date(timestamp);
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
}
private formatFileSize(bytes: number): string {
if (bytes < 1024) return `${bytes}B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
}
}
踩坑与注意事项
坑1:IMAP连接超时
IMAP连接建立后,如果长时间没有操作,服务器会自动断开。你下次发命令就报错。
解决方案:每30秒发送一次NOOP命令保活。或者监听连接断开事件,自动重连。
坑2:MIME解析是个大坑
邮件的MIME格式非常复杂——多部分嵌套、编码转换、字符集处理、内嵌图片、签名验证。自己写MIME解析器?至少一个月。
建议:用服务端做MIME解析,客户端只接收解析后的结构化数据。或者用成熟的MIME解析库。
坑3:SMTP认证的坑
不同邮件服务商的SMTP认证方式不一样:
- 腾讯企业邮:需要授权码,不是登录密码
- 阿里企业邮:支持OAuth2认证
- Exchange:NTLM认证
建议:支持多种认证方式,根据服务商自动选择。不要硬编码认证逻辑。
坑4:附件缓存要清理
邮件附件下载后缓存在本地,时间一长缓存目录越来越大。200MB的缓存上限,用完了就得清理。
建议:LRU策略清理缓存。最近使用的保留,最久未使用的先清理。同时监听系统存储空间不足的通知,主动清理。
坑5:邮件搜索性能
几千封邮件,按主题搜索还好,按正文搜索就慢了。如果用SQL的LIKE,全表扫描,几秒才出结果。
建议:建立全文索引。邮件同步时提取关键词存入索引表,搜索时查索引而不是全表扫描。鸿蒙的RDB支持FTS5全文搜索扩展。
坑6:Exchange同步的复杂性
Exchange ActiveSync协议比IMAP复杂得多——同步状态、策略管理、远程擦除。如果你的企业用Exchange,建议直接对接Exchange Web Services(EWS)API,比ActiveSync简单。
HarmonyOS 6适配说明
HarmonyOS 6对企业邮箱相关能力的增强:
-
统一邮件服务:新增
@kit.MailKit(假设),提供统一的邮件收发API,封装IMAP/SMTP/Exchange协议细节,开发者不需要直接操作Socket。 -
推送增强:Push Kit新增邮件推送类型,新邮件到达时系统自动拉起App同步,不需要App自己维护长连接。
-
安全邮件:新增S/MIME加密邮件支持,邮件端到端加密,只有收件人能解密。企业机密邮件必备。
-
智能分类:AI自动分类邮件——重要邮件、普通邮件、订阅邮件、垃圾邮件,减少信息过载。
-
附件预览增强:系统级附件预览能力,支持Office文档、PDF、压缩包直接预览,不需要第三方App。
总结
企业邮箱的核心是协议对接和增量同步。IMAP收信、SMTP发信,协议本身不难,但MIME解析和安全认证的坑很多。增量同步用UID比对,推送驱动触发,省电省流量。
核心记住三点:
- 不要自己写MIME解析器,用服务端解析或成熟库,自己写一个月都搞不定
- 增量同步用UID+Flag比对,全量拉取几千封邮件谁也受不了
- 附件缓存要管理,LRU策略+容量上限,否则缓存目录无限膨胀
| 评估维度 | 说明 |
|---|---|
| 学习难度 | ⭐⭐⭐⭐ 协议对接不难,但MIME解析和安全认证很复杂 |
| 使用频率 | ⭐⭐⭐⭐⭐ 企业办公必备,每天都要用 |
| 重要程度 | ⭐⭐⭐⭐ 邮箱是基础功能,做不好用户直接用系统邮箱 |
邮箱是OA的"基础设施"。做不好没关系——用户会用系统邮箱或第三方邮箱替代。做好了,用户才愿意留在你的OA里。
- 点赞
- 收藏
- 关注作者
评论(0)