HarmonyOS开发:邮件应用——企业邮箱

举报
Jack20 发表于 2026/06/26 16:51:12 2026/06/26
【摘要】 HarmonyOS开发:邮件应用——企业邮箱核心要点:企业邮箱不是"收发邮件"那么简单——IMAP/SMTP协议对接、增量同步省流量、附件预览与安全、邮件搜索与归档,每个环节都有坑。 背景与动机你打开邮箱App,等了10秒邮件列表才出来。点开一封邮件,又等了5秒。附件?点一下下载,又等了30秒。你搜索一封上个月的邮件,转了半天圈告诉你"未找到"。用户对邮箱的忍耐度极低——它就是个工具,打开...

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)是发信协议,核心流程:

  1. 建立TCP连接
  2. EHLO握手,协商认证方式和加密
  3. STARTTLS升级为加密连接
  4. AUTH LOGIN认证
  5. MAIL FROM指定发件人
  6. RCPT TO指定收件人
  7. DATA发送邮件内容
  8. 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对企业邮箱相关能力的增强:

  1. 统一邮件服务:新增@kit.MailKit(假设),提供统一的邮件收发API,封装IMAP/SMTP/Exchange协议细节,开发者不需要直接操作Socket。

  2. 推送增强:Push Kit新增邮件推送类型,新邮件到达时系统自动拉起App同步,不需要App自己维护长连接。

  3. 安全邮件:新增S/MIME加密邮件支持,邮件端到端加密,只有收件人能解密。企业机密邮件必备。

  4. 智能分类:AI自动分类邮件——重要邮件、普通邮件、订阅邮件、垃圾邮件,减少信息过载。

  5. 附件预览增强:系统级附件预览能力,支持Office文档、PDF、压缩包直接预览,不需要第三方App。

总结

企业邮箱的核心是协议对接和增量同步。IMAP收信、SMTP发信,协议本身不难,但MIME解析和安全认证的坑很多。增量同步用UID比对,推送驱动触发,省电省流量。

核心记住三点:

  • 不要自己写MIME解析器,用服务端解析或成熟库,自己写一个月都搞不定
  • 增量同步用UID+Flag比对,全量拉取几千封邮件谁也受不了
  • 附件缓存要管理,LRU策略+容量上限,否则缓存目录无限膨胀
评估维度 说明
学习难度 ⭐⭐⭐⭐ 协议对接不难,但MIME解析和安全认证很复杂
使用频率 ⭐⭐⭐⭐⭐ 企业办公必备,每天都要用
重要程度 ⭐⭐⭐⭐ 邮箱是基础功能,做不好用户直接用系统邮箱

邮箱是OA的"基础设施"。做不好没关系——用户会用系统邮箱或第三方邮箱替代。做好了,用户才愿意留在你的OA里。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。