electron-vue邮件客户端总结

关于项目

这是我的毕业设计(2018),邮件客户端

包含收发邮件、通讯录、多账户登录、本地数据保存等功能

github:github.com/ooooevan/Vm…

使用的相关模块

  • 用vue-cli构建electron-vue项目
  • 用node-imap模块接收邮件
  • 用nodemailer发送邮件
  • 用element-ui做样式框架
  • 用lowdb做本地数据存储
  • 用iconv-lite、quoted-printable、utf8等处理编码
  • 用vue-quill-editor做富文本编辑器

调试运行

npm run dev    # 调试运行,localhost:9080

npm run build  # 打包,安装包在build目录
复制代码

页面截图

项目目录

最外层结构是由electron-vue创建,主要看src的结构

─ src
 ├── main
 │  ├── index.js                         #主进程,创建渲染进程
 ├── models                              #定义模型,用于封装对象
 ├── renrender                           #渲染进程,里面就是一个vue项目目录
 │  ├── common                           #一些重要的js函数与公共样式
 │      ├── javascript
 │          ├── cache.js                 #硬盘存取相关函数
 │          ├── config.js                #存放配置及正则表达式
 │          ├── getEmail.js              #获取email的函数
 │          ├── parseEmail.js            #解析email的函数
 │          ├── sendEmail.js             #发送email的函数
 │      ├── style
 │  ├── components                       #存放组件
 │  ├── pages                            #存放页面
 │  ├── router                           #路由
 │  ├── store                            #vuex的store相关文件
 │  ├── app.vue                          #vue页面最外层结构
 │  ├── main.js                          #vue项目入口
 ├── index.ejs                           #electron页面入口
复制代码

开发过程

关于electron和vue

electron将chromium和nodejs合并到同一运行时环境中,可以用html、css、javascript来构建跨平台的桌面应用。说白了就是我们写网页的同时还可以调用nodejs的api(如调用fs模块存储数据到电脑),然后electron帮我们打包成一个跨平台的桌面应用。 vue是当前主流mvvm框架之一,这里就不多介绍了,用到了vuex、router等,不懂的话需要先去了解一下才能看懂项目

本项目用vue-vli初始化electron-vue,开发方便

vue init simulatedgreg/electron-vue Vmail
复制代码

打包选的是electron-builder,这个工具可以直接打包安装包,而electron-packager打包成可执行文件

项目思路分解

项目主体是邮件,比较重要的有四步:获取解析邮件、存储邮件、显示邮件和发送邮件 获取和发送邮件要根据邮件协议来分析

获取与解析邮件

读取邮件的协议有pop3(Post Office Protocol)、imap(Internet Message Access Protocol)。pop3简单但交互性较弱。imap较复杂,可交互性强,是一个联机协议,如可以获取邮件后将邮件置为已读,而pop3协议是只读的。 如果要自己实现获取协议会比较麻烦,去github逛了一圈,发现node-imap这个库挺不错的,就用了它

关于密码要先去邮件服务器开通获取,如qq:邮箱->设置->账号->imap服务,开启(需要自己手动保存密码)

项目中获取邮件有两个方法:一个是获取一个完整的邮件(getEmailDetail),一个获取一组邮件头(getEmailList)。如在登陆成功后,会自动获取邮件列表显示出来,此时是调用getEmailList。当点击某一个邮件时,会自动获取一个完整的邮件,调用getEmailDetail。node-imap这个库包含着两个功能,还支持很多不同参数,可自行去github熟悉。

下面重点讲解析一封邮件

邮件有邮件头和邮件体两部分。邮件头的格式基本都是一样的,而邮件体格式就多种多样了,因为有很多类型如:纯文本,html页面,包含附件等等 第一步是看Content-Type

  1. text,主要有text/html和text/plain,内容需要用Content-Transfer-Encoding解码,常见传输编码为base64和quoted-printable
  2. multipart,又分为mixed、alternative和related。multipart有boundary分割符,将邮件体分割成不同段
    • mixed是有附件的类型
    • alternative是纯文本和超文本同时存在的类型
    • related是资源内嵌类型,如内容为html,但html里有图片,把图片提取出来以附件形式发送
  3. image、application,一般是出现在附件中的格式

第二步看boundary

只有multipart类型才有boundary,因为这种类型比较复杂,需要用boundary分段解析

Content-Type: multipart/mixed;
  boundary="----=_NextPart_5A640E3E_0AF97620_651579F6"
复制代码

这里的boundary是一串字符,但是分割不是直接用boundary,而是用父段和子段来分割

父段: '--' + boundary + '--'

子段: '--' + boundary

据我观察,父段只出现0次或1次并且在最后的位置(可能我遇到的邮件类型有限),所以内容就是分割后的数组的第一个元素:emailText = emailText.split(fatherBoundary)[0].trim() 子段将内容分为不同的段,每个子段需要单独重新解析,因为里面也有自己的Content-typeboundary,所以一封邮件可能出现两个不同的boundary

第三步解析 若分割后的段是html或附件等,直接根据charset和encoding等解析;若仍是multipart类型,则用同样的思路再次解析(关于编码解析下面有说)

下面举个例子(已删除部分不必要的):

From: "=?gb18030?B?amlhbmJvKw==?=" <490549111@qq.com>
To: "=?gb18030?B?amlhbmJvKw==?=" <635638508@qq.com>
Subject: 123
Content-Type: multipart/mixed;
  boundary="----=_NextPart_5A5F05FC_0A84FD10_42CF1A15"
Content-Transfer-Encoding: 8Bit
Date: Wed, 17 Jan 2018 16:14:52 +0800

This is a multi-part message in MIME format.

------=_NextPart_5A5F05FC_0A84FD10_42CF1A15
Content-Type: multipart/alternative;
  boundary="----=_NextPart_5A5F05FC_0A84FD10_3247BB56";

------=_NextPart_5A5F05FC_0A84FD10_3247BB56
Content-Type: text/plain;
  charset="gb18030"
Content-Transfer-Encoding: base64

MTINCjM=

------=_NextPart_5A5F05FC_0A84FD10_3247BB56
Content-Type: text/html;
  charset="gb18030"
Content-Transfer-Encoding: base64

PGRpdj4xMjwvZGl2PjxkaXY+MzwvZGl2Pg==

------=_NextPart_5A5F05FC_0A84FD10_3247BB56--

------=_NextPart_5A5F05FC_0A84FD10_42CF1A15
Content-Type: application/octet-stream;
  charset="gb18030";
  name="=?gb18030?B?08q8/s/qx+kucG5n?="
Content-Disposition: attachment; filename="=?gb18030?B?08q8/s/qx+kucG5n?="
Content-Transfer-Encoding: base64

iVBORw0KGgoAAAANSUhEUgAAA2QAAAHRCAIAAACKEu1wAAAAAXNSR0IArs4c6QAAA...(很长,省略)

------=_NextPart_5A5F05FC_0A84FD10_42CF1A15--

复制代码

分析:

  1. Content-type是multipart/mixed,说明是包含附件类型
  2. 父段出现在最后,分割后的数组取第一项即可
  3. 根据子段分割,段内有各自的boundary、Content-type和charset等。
    1. 第一段是一个alternative的小邮件,包含纯文本和超文本。再根据boundary分割即可
      1. 解析后可的到纯文本内容为12\r\n3
      2. 解析后可的到超文本内容为<div>12</div><div>3</div>
    2. 第二段是application类型附件,根据charset和encoding解析得到文件名是邮件详情.png

编写时要注意的问题

  1. 遇到过Content-type为multipart/related;type="multipart/alternative";boundary="----=_NextPart_5A6951CD_6F185580_3879981A,这样要算related,不能算alternative,按复杂的那个算
  2. 观察发现related类型和mixed类型的解析规则一样
  3. 有些邮件一些值不全(如没有charset),需要设置默认值
  4. base64值解析错误,是因为base64有换行符,需要去掉

存储邮件

分析了邮件的类型,那存储邮件就不难了。下面是刚解析完的邮件对象格式

── attr   #有邮件uid等简单信息
── body
   ├──attachment   #附件
   ├──bodyHtml      #超文本
   ├──bodyText      #纯文本
── emailText        #完整的邮件文本
── header           #头部信息
复制代码

我们需要根据邮件Content-type进行转换再存储,不然的话就要在显示时在判断不同类型不同处理。显然存储前处理更好

  1. 若是html类型,则将bodyHtml单独存入一个文件,bodyHtml的值为文件路径。这里需要考虑有的html并不是完整页面而是一个片段
  2. 若是mixed类型,将attachment存入单独文件,同样存为文件路径
  3. 对于alternative和related,到这里已经不用单独考虑。因为alternative是纯文本和超文本共存,也就是重复的,超文本是html片段,包含格式,而纯文本只有文字,直保留超文本即可;related是和mixed解析规则一样,并需要将资源拼合成完整html,将html单独存储即可。
if (contentType.match(htmlTypeReg)) {...}  //单独存储html
else if (contentType.match(mixedMultipart)) {...} // 单独存储附件
if (!contentType.match(htmlTypeReg)) {...}  //非htmlTypeReg也进行单独存储html
复制代码

显示邮件

进行了很规则的存储,所以显示时逻辑就很清晰了

  1. bodyHtml以.html结尾,则是html路径,用webview的src引入
  2. bodyHtml不是路径,则将html片段插入
  3. 没有bodyHtml,则将bodyText插入
  4. 有attachment则显示,没有就不显示
if (bodyHtml.indexOf(HTML) && bodyHtml.indexOf(HTML) + HTML.length === bodyHtml.length) {...} //html是路径
else if (bodyHtml) {...} //html不是路径
else {...} //没有html,只能取bodyText
if (detail.body.attachment && detail.body.attachment.length) {...} //显示附件
复制代码

发送邮件

发送邮件有stmp(Simple Mail Transfer Protocol),github有现成较成熟的nodemailer,无论发送html还是附件,都非常简单。

开发遇到的问题

编码

邮件最开始获取的是流,需要一个编码转为最初的字符串。我用的是gb18030解码 关于gb系列编码可以自行了解,简单提一下:最初只有ascii,中国想显示中文,就有了gb2312、gbk等,从简体中文慢慢加入繁体字等,最后更新的版本是gb18030,所以是gb系列直接用gb18030即可,因为它向下兼容。

解析最初的流好像用gb18030或utf-8都可以,因为各个部分都已经用base64或其他编码转为ascii码了。

From: "=?gb18030?B?amlhbmJvKw==?=" <490549111@qq.com>
复制代码

上面的字符串反应了两个事: 1、字符集为gb18030,即本邮件由gb18030编码 2、B代表base64,后面的字符用base64编码

解析的思路是先用base64转为buffer,在用gb18030字符集转为字符串 解析的方法是iconv.decode(iconv.encode('amlhbmJvKw==?=','base64'),'gb18030')

From: =?UTF-8?B?6Zi/6YeM5LqR?= <system@notice.aliyun.com>
复制代码

同样的道理,这是utf-8字符集的base64编码 解析的方法是iconv.decode(iconv.encode('B?6Zi/6YeM5LqR?=','base64'),'utf-8')

iconv.decode(iconv.encode('阿里郎阿里云','gb18030'),'utf-8')
iconv.decode(iconv.encode('6Zi/6YeM6YOO6Zi/6YeM5LqR','base64'),'gb18030')
复制代码

如果gb18030和utf-8混用了,那就出现乱码了,因为他们字符集不一样,同一个编码代表的文字不一样。

上面的第一行输出�����ɰ�����。第二行输出闃块噷閮庨樋閲屼簯

对于boundary段内的内容如:

------=_NextPart_5A640E3E_0AF97620_02509F49
Content-Type: text/plain;
  charset="gb18030"
Content-Transfer-Encoding: base64

cXdlenhj
复制代码

里面清楚写了字符集和传输编码,按它规则解析即可得到纯文本qwezxc

使用imap的node-imap相关

使用node-imap模块,一方面是较灵活,另一方面是可以同步状态。最大的问题是发现同步状态失败,根据文档下面这样就可以标记邮件已读,但是怎么都失败。。可能是支持性不够

imap.openBox('INBOX', false, cb);       //openBox不能是readOnly,置false
let f = imap.fetch(results, { bodies: '', markSeen: true }); //markSeen为true
复制代码

文档api比较多,参数也多,但是很多得到的结果不一致,要多观察多测试,查出哪个是要用的api。

electron最小化,全屏按钮和无边框

默认的窗口是有系统边框的,我不喜欢,只要再创建渲染进程是配置去掉即可

  mainWindow = new BrowserWindow({
    height: 563,
    width: 1000,
    useContentSize: true,
    autoHideMenuBar: true,
    title: 'Vmail',
    disableAutoHideCursor: true,
    frame: false // 没有边框
  })
复制代码

没有了边框,就要手动添加界面拖动。

<header style="-webkit-app-region: drag">
复制代码

这样,header就可以拖动了。但要注意,只有写行内样式才起效。同时会导致里面标签的hover不触发,要想触发,就要将这个标签设置不可拖动

<div class="refresh fl" style="-webkit-app-region: no-drag">
复制代码

下面是最小化和全屏的部分代码

const { remote } = require('electron')
...
data () {
  return {
    isFullScreen: false  //当前是否全屏状态
  }
},
mounted () {
  window.addEventListener('resize', () => {  //当resize时检测是否全屏
    this.isFullScreen = remote.getCurrentWindow().isMaximized()
  })
},
methods: {
  close () {
    remote.getCurrentWindow().close()  //点击关闭,停止渲染进程
  },
  minimize () {
    remote.getCurrentWindow().minimize()  //窗口最小化
  },
  full () {
    const browserWindow = remote.getCurrentWindow()  //全屏toggle
    if (browserWindow.isMaximized()) {
      browserWindow.unmaximize()
      this.isFullScreen = false
    } else {
      browserWindow.maximize()
      this.isFullScreen = true
    }
  },
复制代码

判断全屏还有browserWindow.isFullScreen(),发现双击拖动栏全屏不能正确返回,browserWindow.isMaximized()可以正确判断。

关于事件监听,要用addEventListener,不能用onresize,因为多个地方用到了resize,用onresize会相互覆盖

打包

项目是用electron-builder打包,第一次build要下载依赖。因为资源在墙外,要翻墙,否则报错。也可以尝试手动下载,根据命令行的提示下载对应的文件

npm应该设置镜像,配置文件为~/.npmrc

registry=https://registry.npm.taobao.org
sass_binary_site=https://npm.taobao.org/mirrors/node-sass/
phantomjs_cdnurl=http://npm.taobao.org/mirrors/phantomjs
ELECTRON_MIRROR=http://npm.taobao.org/mirrors/electron/
复制代码

www.cnblogs.com/chenweixuan… blog.csdn.net/bailong1/ar…

其他

下图是硬盘存储结构

其中config.js存储所有用户和当前用户

每个邮箱目录都有一个index文件,存着各种邮件列表,具体html或附件都单独提取出来了

开始是用qq邮箱测试,之后用其他邮箱测试,基本是没问题的,因为大多数都是根据标准来收发邮件。测试了qq、163、aliyun等都基本没问题。(163需要多一个授权步骤)

项目缺点

项目做的比较粗糙,有些功能时不完善的。比如草稿箱,写邮件时的右侧快捷选择收件人,webview不能自适应高度等。功能也不多,如没有快捷回复邮件功能等。

总结

此次项目,业务不难,主要是邮件解析部分比较绕。我没有查阅权威的文档,所以可能有缺陷。

匹配字符串用到了大量正则表达式,我写的正则也不是很好

对于vue项目结构,vuex等知识不是重点,所以我一笔带过

以上就是对项目的总结,如果有错,望指正

参考: MIME---multipart类型

邮件编码Content-Transfer-Encoding的各种形式

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值