Gmail资料自动储存并分类至Drive

前阵子朋友跟我说了一个需求,思考了一下想说花的时间不多顺手就做了

主功能

自动将Gmail信件的附档分类并储存至Google Drive指定的资料夹
朋友说以往都是每个信件手动下载再逐一上传,他称之为归档的动作

功能演示

这是主要也是唯一的页面,首先要进行登入,就如同一般使用Google登入会询问权限相关的描述,反正就同意下一步就对了
http://img2.58codes.com/2024/20126774TchiJ1SHdj.png

接下来建置一个资料夹,準备归档用,我就建了一个Test
http://img2.58codes.com/2024/20126774zPHE7aggXZ.png

然后必须将资料夹权限分享给Service account,这个Service account后续技术部份会再说明,如果没分享的话帐号没有权限操作Google Drive是没办法正常运作的
http://img2.58codes.com/2024/20126774gzlHFMBxwk.png

最后再回到主画面,找到资料夹,确认自动对应上的ID是否正确,然后就开始归档啦
http://img2.58codes.com/2024/20126774vNnCNB4WC5.png


技术部分

前端Html, css, javascript
后端Nodejs

运作流程

前端Google Login,透过帐号oauth呼叫Gmail API取得信件列表
-> 透过信件列表的ID逐一取得每个信件的详细资料(附加档案为Base64格式)
-> 将附加档案透过内部API传送至Nodejs后端
-> 后端将档案暂存至Server
-> 呼叫Drive上传档案API
-> 删除暂存在Server的档案

前置作业

相关的权限、帐号需要申请
http://img2.58codes.com/2024/20126774UYgGkVAWbg.png

红色框的OAuth是给前端Google登入 access gmail使用的
http://img2.58codes.com/2024/20126774bRKz1gm53m.png

蓝色框则Service account则是Drive需要的
这边需要自行新增金钥,这个金钥会是Nodejs后端需要用到的身份验证
http://img2.58codes.com/2024/201267743V6MQwnxDb.png

实作

这边就是基本的初始化设定
"google-signin-client_id" = 前置作业红框的 OAuth用户端编号

<meta name="google-signin-client_id" content="YOUR_CLIENT_ID.apps.googleusercontent.com"><script src="https://apis.google.com/js/client.js?onload=loadAPIs" async defer></script><script src="https://apis.google.com/js/platform.js?onload=renderButton" async defer></script>function loadAPIs() {    gapi.client.load('gmail', 'v1');}function renderButton() {    var gmailScope = 'https://www.googleapis.com/auth/gmail.readonly';    gapi.signin2.render('signin', {        'scope': ['profile', gmailScope].join(' '),        'onsuccess': onSigninSuccess,        'onfailure': onSigninFailure    });}

读取Gmail信件,大致上是使用SDK呼叫,比较麻烦的是query部分的语法,虽然官网上都有各自的教学,但是在多条件合併使用的时候还是要注意一下,印象中连has:attachment摆前或后都有影响到搜寻结果(导致错误),可能我英文不太好所以没有非常清晰的了解,各位看官可以亲自去体验一下

gapi.load('client:auth2', () => {    gapi.client.load('gmail', 'v1', () => {        var query = ''        //after:2020-08-01 before:2020-08-10        if($('#startDate').val() != '') {            query = 'after:' + $('#startDate').val()        }        if($('#endDate').val() != '') {            query += ' before:' + $('#endDate').val()        }        query += ' has:attachment'        if($('#email').val() != '') {            query += ' from:' + $('#email').val()        }        else {            query += ' from:-' + profile.bu        }        // q=in:sent after:2014/01/01 before:2014/02/01        var userId = profile.getId()        var request = gapi.client.gmail.users.messages.list({            userId: userId,            q: query,            maxResults: 20        });        request.execute(function(resp) {            var messages = resp.messages;            if(messages && messages.length > 0) {                requestFullEmailMessage(userId, messages, 0, function() {                })            }        });    });})

用递迴的方式逐一读取每一封Email的内容

function requestFullEmailMessage(userId, messages, index, callback) {    var message = messages[index]    var fullMessageRequest = gapi.client.gmail.users.messages.get({        userId: userId,        id: message.id    });    fullMessageRequest.execute(function(fullMessage) {        var from = getFromEmail(fullMessage.payload.headers)        var subject = getSubject(fullMessage.payload.headers)        if(fullMessage.payload.parts && fullMessage.payload.parts.length > 0) {            startCheckAndCreateFolder(fullMessage.id, fullMessage.payload.parts, 0, from, subject, function() {                if(index + 1 < parseInt(messages.length)) {                    requestFullEmailMessage(userId, messages, index + 1, callback)                } else {                    callback()                }            })        }    });}

同样是使用递迴的方式,呼叫SDK的方法attachments.get另外取得附件资料
取得附件Base64资料后,呼叫本地的checkAndCreateFolder建置归档的资料夹
确认目的地存在后在呼叫saveAndUploadFile将Base64附件资料上传至后端

function startCheckAndCreateFolder(messageId, payloadParts, index, folderName, subject, callback){    var part = payloadParts[index]    if(part.body.attachmentId && part.filename) {        var attachmentId = part.body.attachmentId;        var request = gapi.client.gmail.users.messages.attachments.get({            'id': attachmentId,            'messageId': messageId,            'userId': 'me'        });        request.execute(function (attachment) {            $.ajax({                type: 'POST',                url: '/api/gsuite/checkAndCreateFolder',                data: {                    "parentId": "Google Drive资料夹ID",                    "folderName": folderName                },                success: function(result) {                    if(result.status == true) {                        var requestFileData = {                            mime: part.mimeType,                            filename: part.filename,                            folderId: result.data.id,                            data: attachment.data                        }                        $.ajax({                            type: 'POST',                            url: '/api/gsuite/saveAndUploadFile',                            contentType: "application/json; charset=utf-8",                            dataType: "json",                            data: JSON.stringify(requestFileData),                            success: function(uploadResult) {                                if(index + 1 < payloadParts.length) {                                    startCheckAndCreateFolder(messageId, payloadParts, index + 1, folderName, subject, callback)                                } else {                                    callback()                                }                            },                            error:function(XMLHttpRequest, textStatus){                                startCheckAndCreateFolder(messageId, payloadParts, index + 1, folderName, subject, callback)                            }                        });                    } else {                        callback()                    }                },                error:function(XMLHttpRequest, textStatus){                    startCheckAndCreateFolder(messageId, payloadParts, index + 1, folderName, subject, callback)                }            });        });    } else if(index + 1 < payloadParts.length){        startCheckAndCreateFolder(messageId, payloadParts, index + 1, folderName, subject, callback)    } else {        callback()    }}

接下来来到后端的部分
首先第一个API是checkAndCreateFolder,功能是建置当下档案要存放的资料夹
那这边客户的需求是只要按执行当下月份分类就可以,所以基本上只需要执行一次

这边使用到的gsuiteHelper drive.files是googleapis sdk可以直接到npm这边查看
https://www.npmjs.com/package/googleapis

其他参数我就不细讲,这边完全是根据客户的需求去调整设定,如果方法执行没有达到预想的效果,可以到文档查询一下有哪些设定值可以修改,调整一下或许就没问题了
https://developers.google.com/drive/api/v3/enable-shareddrives

exports.checkAndCreateFolder = function(req, res) {  var parentId = req.body.parentId  var folderName = new Date().toISOString().substring(0, 7)  folderName = folderName.split("-").join("");  gsuiteHelper.listByParentId(parentId, function(err, folders) {    var targetFolder = null    folders.forEach(folder => {      if(folder.name == folderName) {        targetFolder = folder      }    });    if(targetFolder == null) {      gsuiteHelper.createFolder(parentId, folderName, function(err, folder) {        //response      })    } else {      // response    }  })}//gsuiteHelperexports.listByParentId = function(parentId, callback) {  drive.files.list({      q: "trashed=false and '" + parentId + "' in parents",      includeItemsFromAllDrives: true,      supportsAllDrives: true    }, (err, driveRes) => {      if (err) {          callback(err, null)      }      else {          const files = driveRes.data.files;          callback(null, files)      }  });}//gsuiteHelperexports.createFolder = function(parentId, folderName, callback) {  var fileMetadata = {    'name': folderName,    'mimeType': 'application/vnd.google-apps.folder',    'parents': [parentId]  };  drive.files.create({    resource: fileMetadata,    fields: 'id',    supportsAllDrives: true,    supportsTeamDrives: true  }, function (err, file) {    if (err) {      callback(err, null)    } else {      callback(null, file)    }  });}

到了最后一步saveAndUploadFile API
这边是用fs套件做本地的档案读写操作
首先要确认资料夹是否可以读写(通常我都会先手动chmod),再将Base64资料转为档案存在Server指定的资料夹
存下后在呼叫drive.files.create API上传档案到Drive,确认完成后再删除Server暂存档,避免佔空间

exports.saveAndUploadFile = function(req, res) {  var tmpFolder = './privateFile/uploads'  var tmpFile = './privateFile/uploads/' + req.body.filename;  fs.access(tmpFolder, fs.constants.F_OK | fs.constants.W_OK, (err) => {    if (err) {      //response err    } else {      fs.writeFile(tmpFile, req.body.data, { encoding: 'base64' }, function(err) {        if(err) {          //response err        } else {          gsuiteHelper.createFile({            'name': req.body.filename,            'parents': [req.body.folderId]          }, {            mimeType: req.body.mime,            body: fs.createReadStream(tmpFile)          }, function(err, result) {            if(err) {              //response err            } else {              if(result.status == 200) {                fs.unlink(tmpFile, function() {                  resultHelper.responseSuccess(res, result.data);                });              } else {                //response err              }            }          })        }      });    }  });}//gsuiteHelperexports.createFile = function(fileMetadata, media, callback) {  drive.files.create({    resource: fileMetadata,    media: media,    fields: 'id',    supportsAllDrives: true,    supportsTeamDrives: true  }, function (err, file) {    if (err) {      callback(err, null)    } else {      callback(null, file)    }  });}

整个功能到这边是算完成了,上面这些是局部的程式,有些地方我手动删除了,不过重点都有在

检讨

其实这次开发满多地方写的满髒的,有些方法也用得不好,例如递迴、或重複检查资料夹是否存在,都可以再进行优化。
不过使用的人很少,量很小,也没收钱,做事也是要考量一下成本,就算啦


有任何开发外包工作,或者产品开发都欢迎找我讨论
产品部分可以视情况接受事后分润的方式共同创业

Email: softicecan@gmail.com


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章