關於專案
這是我的畢業設計(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
- 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 +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--
複製程式碼
分析:
- 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: 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等知識不是重點,所以我一筆帶過
以上就是對專案的總結,如果有錯,望指正