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:9080npm 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
- text,主要有text/html和text/plain,内容需要用Content-Transfer-Encoding解码,常见传输编码为base64和quoted-printable
- multipart,又分为mixed、alternative和related。multipart有boundary分割符,将邮件体分割成不同段
- mixed是有附件的类型
- alternative是纯文本和超文本同时存在的类型
- related是资源内嵌类型,如内容为html,但html里有图片,把图片提取出来以附件形式发送
- 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-type
和boundary
,所以一封邮件可能出现两个不同的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 +0800This 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: base64MTINCjM=------=_NextPart_5A5F05FC_0A84FD10_3247BB56
Content-Type: text/html;charset="gb18030"
Content-Transfer-Encoding: base64PGRpdj4xMjwvZGl2PjxkaXY+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: base64iVBORw0KGgoAAAANSUhEUgAAA2QAAAHRCAIAAACKEu1wAAAAAXNSR0IArs4c6QAAA...(很长,省略)------=_NextPart_5A5F05FC_0A84FD10_42CF1A15--复制代码
分析:
- Content-type是
multipart/mixed
,说明是包含附件类型 - 父段出现在最后,分割后的数组取第一项即可
- 根据子段分割,段内有各自的boundary、Content-type和charset等。
- 第一段是一个alternative的小邮件,包含纯文本和超文本。再根据boundary分割即可
- 解析后可的到纯文本内容为
12\r\n3
- 解析后可的到超文本内容为
<div>12</div><div>3</div>
- 解析后可的到纯文本内容为
- 第二段是application类型附件,根据charset和encoding解析得到文件名是
邮件详情.png
- 第一段是一个alternative的小邮件,包含纯文本和超文本。再根据boundary分割即可
编写时要注意的问题
- 遇到过Content-type为
multipart/related;type="multipart/alternative";boundary="----=_NextPart_5A6951CD_6F185580_3879981A
,这样要算related,不能算alternative,按复杂的那个算 - 观察发现related类型和mixed类型的解析规则一样
- 有些邮件一些值不全(如没有charset),需要设置默认值
- base64值解析错误,是因为base64有换行符,需要去掉
存储邮件
分析了邮件的类型,那存储邮件就不难了。下面是刚解析完的邮件对象格式
── attr #有邮件uid等简单信息
── body├──attachment #附件├──bodyHtml #超文本├──bodyText #纯文本
── emailText #完整的邮件文本
── header #头部信息
复制代码
我们需要根据邮件Content-type
进行转换再存储,不然的话就要在显示时在判断不同类型不同处理。显然存储前处理更好
- 若是html类型,则将bodyHtml单独存入一个文件,bodyHtml的值为文件路径。这里需要考虑有的html并不是完整页面而是一个片段
- 若是mixed类型,将attachment存入单独文件,同样存为文件路径
- 对于alternative和related,到这里已经不用单独考虑。因为alternative是纯文本和超文本共存,也就是重复的,超文本是html片段,包含格式,而纯文本只有文字,直保留超文本即可;related是和mixed解析规则一样,并需要将资源拼合成完整html,将html单独存储即可。
if (contentType.match(htmlTypeReg)) {...} //单独存储html
else if (contentType.match(mixedMultipart)) {...} // 单独存储附件
if (!contentType.match(htmlTypeReg)) {...} //非htmlTypeReg也进行单独存储html
复制代码
显示邮件
进行了很规则的存储,所以显示时逻辑就很清晰了
- bodyHtml以
.html
结尾,则是html路径,用webview的src引入 - bodyHtml不是路径,则将html片段插入
- 没有bodyHtml,则将bodyText插入
- 有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: base64cXdlenhj
复制代码
里面清楚写了字符集和传输编码,按它规则解析即可得到纯文本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() //全屏toggleif (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的各种形式
electron-vue邮件客户端总结相关推荐
- mail linux 客户端,Nylas Mail: 一个 Linux 的免费邮件客户端
Nylas Mail An Amazing Free Email Client For Linux 有一个经常被提及的问题是 Ubuntu 是否还应该提供默认的电子邮件客户端.就个人而言,我已经很长时 ...
- android仿微信图片编辑器,electron/vue可编辑框contenteditable|仿微信截图
基于Electron+vue实现div可编辑contenteditable插入表情|electron-vue截图功能 为了避免使用 vue 手动建立起 electron 应用程序.electron-v ...
- 记一次Electron+vue实现动态打印小票
1.业务场景 公司有做一个Electron+Vue的收银产品,收银以后需要将订单信息通过小票打印出来,记录一下做这个需求遇到的一些问题跟bug 2.解决思路 通过webview或者新窗口将需要打印的内 ...
- Nylas Mail: 一个Linux的免费邮件客户端
有一个经常被提及的问题是 Ubuntu 是否还应该提供默认的电子邮件客户端.就个人而言,我已经很长时间没有使用 Thunderbird 了.我相信这不是一个第一次被问到的问题,但我相信这是一个把它解决 ...
- 一款 Linux 邮件客户端—Nylas Mail
导读 Linux 上面有许多邮件客户端,Geary.Empathy.Evolution 和 Thunderbird 本身已经为很多用户提供了很好的服务,但是我发现了值得一试的软件: Nylas Mai ...
- 邮件客户端WebMail Pro v7.7.5发布,在线订购限时75折优惠!
2019独角兽企业重金招聘Python工程师标准>>> AfterLogic WebMail Pro是基于网页并以脚本开发的邮件客户端.能以前端模式与现有的邮件服务器或内置邮件服务器 ...
- 如何在 Mutt 邮件客户端中使用密文密码
如何在 Mutt 邮件客户端中使用密文密码 Mutt 是一个 Linux/UNIX 终端环境下的开源的邮件客户端.Mutt 以及 Alpine 在 Linux 命令行爱好者中有着最忠诚的追随者,这不是 ...
- linux 邮件客户端 n1,N1:下一代开源邮件客户端
N1 Open Source email client 当我们谈论到Linux中的邮件客户端,通常 Thunderbird.Geary 和 Evolution 就会出现在我们的脑海.作为对这些大咖们的 ...
- C#获取邮件客户端保存的邮箱密码
有时候邮件客户端记录了邮箱密码,但自己却忘记了,此时可以使用C#建立一个临时的"邮件服务器",截取密码: IPEndPoint ipEndPoint = new IPEndPoin ...
最新文章
- 魅族手机使用应用沙盒一键修改位置数据
- ThinkSNS+ 是如何计算字符显示长度的
- linux桌面文件夹改图标,Linux 给桌面程序设置个性化图标
- boost::log::sinks::simple_event_log_backend用法的测试程序
- Linux与Windows比较出的20个优势
- [leetcode] 105.从前序与中序遍历构造二叉树
- 2021年全国大学生电子设计竞赛重新启动通知及进度安排
- OPENSSL ENGINE机制
- CUDA:在GPU上实现核函数的嵌套以及编译运行
- 深入解析Mysql 主从同步延迟原理及解决方案
- git学习 远程仓库02
- mysql在windows配置多节点_Windows环境配置MySQL集群
- a 标签 jq js 打开新页面跳转
- thingworx- 安装thingworx120天试用版步骤
- 新买的硬盘接在计算机上,电脑如何对刚买来的新硬盘分区
- 手把手教你使用Python批量创建复工证明
- 量化投资学习——关于XTP交易柜台
- Optional orElseThrow 错误: 未报告的异常错误X; 必须对其进行捕获或声明以便抛出...
- 带着老娘和女儿看《孔子》
- 英文中 vi和vt的区别
热门文章
- 基于stm32移植uC/OS-III以及使用Keil仿真和逻辑分析仪抓取波形
- unity3d软阴影和硬阴影的原理_在广告摄影中阴影和高光的重要作用和控制技巧...
- java setattribute_java – HttpSession setAttribute并不总是插入新对象
- denied 登陆后access_Linux SSH登陆出现Access Denied错误的解决方法
- 汽车交通主题的微信公众号图文这样排版阅读量翻十倍
- EC-PCA: 利润中心分配分摊流程-3KE5 / 4KE5
- 一根网线同时走宽带和iptv(单线复用)
- LOL无限火力是哪个服务器先上线,LOL无限火力什么时候上线 2020年无限火力上线时间...
- 2023软件测试行情不行了?
- iOS密码查找工具:4uKey Password Manager for Mac