一、發送郵件
單一的node后台其實本身並沒有發送郵件的功能,要想實現發送郵件的效果,還是需要借助一個郵箱來實現郵件的發送。
流程:前端提出發送需求 —— Node后台收集需要發送的信息 —— 發送給郵箱服務器來進行發送
在node后台 —— 郵箱后台的這個過程中,遵循了SMTP協議(SMTP(Simple Mail Transfer Protocol)簡單郵件傳輸協議)。這個協議來控制郵件的中轉方式,用於因特網中郵件服務器之間交換郵件。
二、接收郵件
后台接收郵件其實就是把郵箱已經接受的郵件獲取到node后台。
流程:node后台請求郵件 —— 郵箱根據請求的情況返回對應的郵件
在郵箱收到郵件 —— 將信息獲取到后台的過程中,遵循pop3或imap協議。
1、pop3協議
POP3協議允許電子郵件客戶端下載服務器上的郵件,但是在客戶端的操作(如移動郵件、刪除郵件、標記已讀等),不會反饋到服務器上。比如通過客戶端收取了郵箱中的3封郵件並移動到其他文件夾,郵箱服務器上的這些郵件是沒有同時被移動的。也就是說POP3協議實際上是下載了一份郵件的副本到本地郵件客戶端,而且對本地郵件副本的操作只會影響本地數據。多個郵件客戶端里面的郵件的狀態可能會不一致。
2、imap協議
IMAP也是提供面向用戶的郵件收取服務。與POP3不同的是,您在電子郵件客戶端收取的郵件仍然保留在服務器上,同時在客戶端上的操作都會反饋到服務器上。
比如:刪除郵件,標記已讀等,服務器上的郵件也會做相應的動作。換句話說,IMAP把遠程文件夾當成本地文件夾來操作,它們之間類似於雙向同步。這樣的好處是,當你在多個郵件客戶端看見的郵件的狀態是一致的。
三、實現詳解
1、第三方模塊
在node的生態中,實現這樣的功能有很多的第三方模塊。比如:
- nodemailer
- node-red
- node-imap
大家可以在npm上搜索並查看文檔進行開發。
下面我們使用nodemailer、imap這兩個模塊進行講解,使用的郵箱是qq郵箱。
2、發送郵件(需要使用nodemailer模塊)
一共分為三步:創建連接 - 消息體 - 發送消息對象
- 使用SMTP或其他傳輸機制創建Nodemailer傳輸器
- 設置消息選項(誰向誰發送消息)
- 使用先前創建的傳輸程序的sendMail()方法傳遞消息對象
let transporter = nodemailer.createTransport({ host: 'smtp.qq.com', port: 465, secure: true, // true for 465, false for other ports auth: { user: "xxx@qq.com", // generated ethereal user pass: '郵箱的驗證碼'// generated ethereal password } }); //創建Nodemailer傳輸器 let mailOptions = { from: '"khy" <xxx@qq.com>', // sender address to: 'xxx@qq.com', // list of receivers subject: 'Hello', // Subject line text: 'Hello world?', // plain text body html: '<b>Hello world?</b>' // html body }; //設置消息選項 transporter.sendMail(mailOptions, (error, info) => { if (error) { return console.log(error); } console.log('Message sent: %s', info.messageId); // Preview only available when sending through an Ethereal account console.log('Preview URL: %s', nodemailer.getTestMessageUrl(info)); // Message sent: <b658f8ca-6296-ccf4-8306-87d57a0b4321@example.com> // Preview URL: https://ethereal.email/message/WaQKMgKddxQDoou... }); //sendMail()方法傳遞消息對象
(1)創建Nodemailer傳輸器
這個部分是設置node后台需要向哪一個郵箱后台,提交郵件請求
nodemailer.createTransport(options[, defaults]) options – 設置鏈接郵箱SMTP數據 host:SMTP地址,從郵件服務提供商獲取 port: SMTP端口號,從郵件服務提供商獲取 secure: true, //465端口為true,其他接口為false auth: { user: "xxx@qq.com", 請求代理的郵箱地址 pass: 'jfxksdbztpcohjff’郵箱邀請碼,從郵件服務提供商獲取 } defaults – 共享數據對象
transporter.verify(callback);
這個是后台驗證是否授權成功,一般用於后台鏈接測試,實際項目中,不需要寫!
let transporter = nodemailer.createTransport({ host: 'smtp.qq.com', port: 465, secure: true, auth: { user: "xxx@qq.com", pass: '郵箱的驗證碼' } }); transporter.verify(function(error, success) { if (error) { console.log(error); } else { console.log('授權成功!'); } });
(2)設置消息選項
這個部分是編輯郵件的內容部分,這一步其實就是建立一個對象,把相關信息作為對象的屬性進行存儲。
屬性列表:(表中尖括號括起來的內容表示數據類型)
from - 郵件發出的地址
to - 郵件送到的地址
cc - 抄送的地址
bcc - 加密抄送的地址
subject - 郵件的標題
text - 發送的文本 , ,
html - 發送html頁, ,
attachments - 添加附件對應的屬性:
filename - 文件名
content - 傳輸內容, , 可以將數據導入文件,作為附件發送。
path - 本地路徑如果希望流式傳輸文件而不是包含該文件,則使用該文件的路徑(對於較大的附件更好)
href – 網絡數據信息路徑。
contentType - 規定附件的格式,如果未設置,將從文件名生成
contentDisposition - optional content disposition type for the attachment, defaults to ‘attachment’
cid - optional content id for using inline images in HTML message source
encoding - If set and content is string, then encodes the content to a Buffer using the specified encoding. Example values: ‘base64’, ‘hex’, ‘binary’ etc. Useful if you want to use binary attachments in a JSON formatted email object.
headers - custom headers for the attachment node. Same usage as with message headers
raw - is an optional special value that overrides entire contents of current mime node including mime headers. Useful if you want to prepare node contents yourself
上邊部分不太常用的的內容沒有翻譯,一般在郵件發送時也不需要編寫,英文內容來源於nodemailer官方文檔。
//address實例---符合address格式的例子 1.字符串:'foobar@example.com' 2.字符串嵌套:'"Майлер, Ноде" <foobar@example.com>' 3.對象:{ name: 'Майлер, Ноде', address: 'foobar@example.com' } 4.數組[ '"Ноде Майлер" <bar@example.com>, '"Name, User" <bar@example.com>' ] 5.混合數組[ 'foobar@example.com', { name: 'Майлер, Ноде', address: 'foobar@example.com' } ] //attachments實例 attachments: [ { //字符串生成附件 filename: 'text1.txt', content: 'hello world!' }, { // 字符流生成附件 filename: 'text2.txt', content: new Buffer('hello world!','utf-8') }, { // 磁盤文件生成附件 filename: 'text3.txt', path: '/path/to/file.txt' }, { //文件名及類型會自動從路徑獲取 path: '/path/to/file.txt' }, { // 字符流生成附件 filename: 'text4.txt', content: fs.createReadStream('file.txt') }, { // 為附件定義自定義內容類型 filename: 'text.bin', content: 'hello world!', contentType: 'text/plain' }, { // 使用URL添加附件 filename: 'license.txt', path: 'https://raw.github.com/nodemailer/nodemailer/master/LICENSE' } ]
注意:
在屬性中“from”必填,且必須是Nodemailer傳輸器中已經授權的郵箱。否則會出現錯誤
在屬性中“to,cc,bcc”三者都表示發送到郵箱的地址。可以同時選則多個屬性,每個屬性也可以同時添加多個目標地址,但是不能三個屬性一個收件地址都不填。
subject,text,html,attachments可以為空。當他們為空時會發送空郵件。
磁盤文件只能使用path路徑,網絡連接可以使用path或href。
附件名稱可以修改,可以與文件名稱不一致。
在設置對象中有兩個屬性分別是text、和html。這兩個對象都是郵件需要發送的內容,當html為空時,郵件會發送空的文本。當html不為空時,html會覆蓋text內容(郵件主體呈現出html內容,而不顯示text內容)。
所有文本字段(電子郵件地址、明文主體、html主體、附件文件名)都使用UTF-8作為編碼。附件是以二進制方式傳輸的。
(3)sendMail()方法傳遞消息對象
使用了這個方法之后,郵件請求就發送到了SMTP服務器中了。
nodemailer.sendMail(object,callback(err,info))
- object就是第二步創建的郵件信息對象對象
- 回調函數有兩個參數,一個是錯誤信息,一個是發送的信息
三,獲取郵件
IMAP:https://www.npmjs.com/package/imap,好好看文檔
在獲取郵件的時候,無非就是以下幾個步驟:
連接到服務器 —— 打開指定郵箱 —— 篩選(操作)郵件 —— 解析郵件 —— 查看結果
var Imap = require('imap'), inspect = require('util').inspect; var imap = new Imap({ user: 'mygmailname@gmail.com', password: 'mygmailpassword', host: 'imap.gmail.com', port: 993, tls: true }); function openInbox(cb) { imap.openBox('INBOX', true, cb); } imap.once('ready', function() { openInbox(function(err, box) { if (err) throw err; var f = imap.seq.fetch('1:3', { bodies: 'HEADER.FIELDS (FROM TO SUBJECT DATE)', struct: true }); f.on('message', function(msg, seqno) { console.log('Message #%d', seqno); var prefix = '(#' + seqno + ') '; msg.on('body', function(stream, info) { var buffer = ''; stream.on('data', function(chunk) { buffer += chunk.toString('utf8'); }); stream.once('end', function() { console.log(prefix + 'Parsed header: %s', inspect(Imap.parseHeader(buffer))); }); }); msg.once('attributes', function(attrs) { console.log(prefix + 'Attributes: %s', inspect(attrs, false, 8)); }); msg.once('end', function() { console.log(prefix + 'Finished'); }); }); f.once('error', function(err) { console.log('Fetch error: ' + err); }); f.once('end', function() { console.log('Done fetching all messages!'); imap.end(); }); }); }); imap.once('error', function(err) { console.log(err); }); imap.once('end', function() { console.log('Connection ended'); }); imap.connect();
(1)連接郵箱
構造器:使用指定的配置對象創建並返回Connection的新實例。設置需要連接到哪一個服務器
var imap = new Imap({ user: 'xxx@qq.com', //你的郵箱賬號 password: 'xxx', //你的郵箱授權碼 host: 'imap.qq.com', //郵箱服務器的主機地址 port: 993, //郵箱服務器的端口地址 tls: true, //使用安全傳輸協議 tlsOptions: { rejectUnauthorized: false } //禁用對證書有效性的檢查 connTimeout: 10000,//鏈接超時等待數,默認10000毫秒 authTimeout: 5000,//身份驗證的毫秒數 });
與服務器構建鏈接:connect()
imap.connect(); //鏈接郵箱,並身份驗證
與服務器斷開鏈接:end(),destroy()
imap.end(); //當所有的請求結束后,斷開鏈接 imap.destroy(); //忽略正在傳輸的內容,立即斷開鏈接
(2)打開指定郵箱
獲取所有郵箱列表:getBoxes
imap.getBoxes(function(err,box){ //box就是郵箱的列表 //box具體內容如下: //INBOX:收件箱 //Sent Messages:發送的郵件 //Drafts:草稿箱 //Deleted:刪除郵件 //Junk:垃圾郵件 })
打開郵箱:openBox(< string >mailboxName[, < boolean >openReadOnly=false], < function >callback)
imap.openBox('INBOX',true,function(err,box){ //打開郵箱后的操作 })
關閉郵箱:closeBox([< boolean >autoExpunge=true, ]< function >callback)
imap.closeBox(true,function(err){ //如果關閉失敗,觸發回調函數 })
注意:這里的第一個參數可選,默認是true。如果autoExpunge為真,則在當前打開的郵箱中標記為Deleted的任何消息都將被刪除,如果郵箱未在只讀模式下打開。如果autoExpunge為false,則斷開連接或打開另一個郵箱,標記為Deleted的消息將不會從當前打開的郵箱中刪除。
addBox、delBox、renameBox等一些不常用的方法參考npm-imap文檔:https://www.npmjs.com/package/imap
(3)篩選(操作)郵件
搜索郵件:search(< array >criteria, < function >callback)
-
callback函數有兩個參數(err,list)list是符合要求的UID數組(UID是郵箱用來標識你這個賬戶的每一封郵件的編號)
criteria:設置篩選條件
- UID:通過郵箱標識符查找
//常用實例 imap.search( [['UID', '491']],function(err,list){ console.log(box); })//搜索UID為491的郵件 imap.search( [['UID', '430:450']],function(err,list){ console.log(box); })//搜索UID為430到450的郵件 imap.search( [['UID', '430:*']],function(err,list){ console.log(box); })//搜索UID為430及以上的郵件 imap.search( [['UID', '430:450','491']],function(err,list){ console.log(box); })//搜索UID為430到450或491的郵件
- ALL:查找所有郵件
- UNSEEN:查找未讀信息
- UNSEEN:查找已讀信息
- ANSWERED:已經回復后的消息
imap.search( ['ALL'],function(err,list){ console.log(box); })//搜索所有郵件 //其他功能替換ALL即可
- ‘BEFORE’ - 在指定日期之前的所有郵件
- ‘ON’ - 在指定日期當天的郵件
- ‘SINCE’ - 在指定日期及之后的郵件
imap.search( [['BEFORE','2018/12/7']],function(err,list){ console.log(box); })//搜索所有郵件 //第二個參數為,js可以解析的日期字符串或日期對象
- LARGER’ :郵件的大小大於指定字節數。
- ‘SMALLER’:郵件的 大小小於指定字節數。
imap.search( [['LARGER',1280]],function(err,list){ console.log(box); })//搜索所有郵件 //第二個參數為int類型的數字
- 數組嵌套表示且
- 數組結合‘OR’表示或
[ 'UNSEEN', ['SINCE', 'April 20, 2010'] ] [ ['OR', 'UNSEEN', ['SINCE', 'April 20, 2010'] ] ]
注意:
- 國內的郵件服務商,並沒有對所有的search語句進行實現。建議大家使用Gmail。
- 以上只列出了一些常用方法,全部方法請參考:https://www.npmjs.com/package/imap
(4)抓取郵件內容
fetch(< MessageSource >source, < object >options)
source表示UID數組,options表示可以添加的屬性,return
options屬性:
- bodies 選擇抓取的部分
- ‘HEADER’ - 頭部信息
- ‘TEXT’ - 郵件主體
- ‘’ -全部信息(頭部+主體)
- 其他屬性參考:https://www.npmjs.com/package/imap
var f = imap.fetch([491,496,493],{bodies:''}); f.on('message',function(msg, seqno){ //msg是抓取對應郵件的事件觸發器 //seqno是郵件在郵箱的編號(不是UID) msg.on('body',function(stream,info){ //stream表示可讀流,是郵件內容的流 //info郵件的基礎信息,包括大小編號 })//當對應郵件接收流觸發 msg.on('end',function(){ })//當對應郵件所有內容接收完后觸發 }) //抓取完一封郵件后觸發 f.once('error',function(err){}) //抓去錯誤后觸發 f.once('end',function(){}) //所有郵件抓取結束后觸發
注意:
-
on,once都是添加事件,只不過once為一次性事件,on為多次事件
-
f本身就是一個事件的觸發器,每抓取一封郵件,message的回調函數會執行,生成對應的那一封郵件的觸發器msg
-
msg也是一個觸發器,當抓取對應郵件下載到本地后觸發
(5)郵件內容解析–使用mailparser
f.on('message', function(msg, seqno) { var mailparser = new MailParser(); //每封郵件添加一個mailparser msg.on('body', function(stream, info) { stream.pipe(mailparser); //將為解析的數據流pipe到mailparser mailparser.on("headers", function(headers) { //headers是Map類型 console.log("郵件主題: " + headers.get('subject')); console.log("發件人: " + headers.get('from').text); console.log("收件人: " + headers.get('to').text); }); //當郵件頭部流全部傳入mailparser后觸發 mailparser.on("data", function(data) { //data是對象 if (data.type === 'text') { console.log("郵件內容: " + data.html); } if (data.type === 'attachment') { console.log("附件名稱:"+data.filename); data.content.pipe(fs.createWriteStream(data.filename)); //保存附件到當前目錄下 data.release(); } }); });
在這次的郵件服務器研究過程中,有一些問題沒有深入分析,只是學習了基本使用方法。比如:在抓取郵件的過程中,涉及到了流式傳輸,這里沒有再深入分析。
