近开发一个项目,需要在Node.js程序里实现定期给管理员发邮件的功能。

笔者平时只会在Web界面收发邮件。对邮件的原理完全不懂(可能大学教过,然而全忘了),直到要解决这个问题。请教了几个业务的同事,得到的答复是:“你需要搭一个SMTP服务,还要装一个mail agent,巴拉巴拉……” 你们在说什么,我瞎了听不见……

听起来很复杂,有没有开箱即用的服务啊?一打听还真有。同事告知我司有提供Exchange服务。笔者的内心独白:“Exchange啊,我见过,跟outlook什么关系?”。好在最后还是在同事的帮助下,冰雪聪明的笔者实现了这个功能。踩了一些坑,记录一下,顺便复习一下基础知识。

名词解释

以上提到的那些名词我一个也没听懂,直到做完这个功能。先给大家解释一下:

  • SMTP。简单邮件传输协议。实际常用于在不同的邮件服务器之间传输邮件。

  • IMAP/POP。两者都是用于在本地查收邮件的协议。POP需要将邮件下载到本地存储。IMAP是POP的增强版,更偏云端一些,邮件存储在服务器,可以多设备访问。

  • Email Agent。在邮件各个传输链路上的具体程序。可以理解成协议的实现者。

下面是一个传统的邮件从发送到接收的过程:

(图片来源:维基百科 )

第一个教我的同事实际上是让我去安装图中各个小矩形里的具体程序(比如MUA,MSA,MTA,MRA),它们都是email agent。可以不恰当地将它们比喻成邮局的各个部门。

从图中可以看到SMTP主要用于发送邮件,而IMAP和POP主要用于本地获取查看邮件。

好,科普结束。以上内容跟本文主题无直接关系,逃……

下面是本文的主角:

Exchange Web Service

先解释一下Exchange服务,它是微软开发的一个邮件和日历服务,运行在Windows Server操作系统上。它不同于SMTP和IMAP/POP等,并不是一个简单的协议,而是微软自己实现的一套服务。

而Exchange Web Service(简称EWS),是应用跟Exchange服务器通信的一种方式。简单地说,当你使用Exchange提供的邮件服务时,可以使用EWS发送或者接收邮件等。不过微软已经在2018年7月宣布停止在产品和功能上更新EWS,它推荐使用Microsoft Graph来访问邮箱服务。这不重要,因为已经安装的EWS不受影响,可以继续使用。

看看Exchange的架构(示例为Exchange on-premise版本,除此之外它还有online版)。

(图片来源:EWS应用和Exchange的架构)

回顾一下开头提到的场景,结合实际情况(即公司已有Exchange服务),要解决发邮件的问题只需关注图中的1、2、3。

可以把Node.js程序看做图中的EWS应用,它需要调用EWS的API跟Exchange服务器通信,从而实现发邮件的功能。

我们实现一个EWS发送邮件的程序需要实现两点:

  • 鉴权。校验身份。

  • 发送。将邮件内容发出去。

下面看看具体实现。

一个基于EWS的Node.js发邮件程序

微软官网提供了一套EWS Managed API,用于调用EWS的接口。但是很遗憾,它不支持Node.js。不过github上有Node.js版本:ews-javascript-api,可以直接从npm上安装。笔者最终没有用它,因为只是一个小小的发邮件功能,不必用这么全的第三方库。(其实是懒得看文档了)。

推荐EWS的同事使用了一个叫exchange-web-service的库,非常简单。不过笔者在使用的时候踩到了坑,后来看了一遍该库的代码,改进了一版代码,最终解决了问题,顺便加深了对EWS的理解。

先说说坑:

发邮件程序被设计成了一个接口(ThinkJS程序里的一个action)。这个接口需要先查数据库,按条件筛选出收件人。然后调用exchange-web-service库的方法,将邮件发送给筛选出来的收件人。伪代码如下:

import ews from 'exchange-web-service';

// 配置邮箱帐号

ews.config('mail_account', 'mail_password', "https://mail_domain/Ews/Exchange.asmx", "mail_domain");

async checkNotifyUsersAction() {

// 只允许命令行执行

if (!this.isCli) { return this.fail("only allow invoked in cli mode"); }

// 筛选收件人

const recipients = await this.modelInstance.getRecipients();

const title = '标题';

const msg = `<![CDATA[ 你好 ]]>`;

// 发件

ews.sendMail(email, title, msg);

}

注意,这里ews.sendMail是没有被“await”的,因为这个库不支持promise,那么它能不能将邮件发送成功呢?当然不能。并且在调试的时候发现,先调用ews.sendMail,再执行筛选收件人的操作,就能收到邮件。为什么放在最后不行?

经过分析,发现ews.sendMail本身是异步操作,而筛选收件人的await能够hold住整个action的执行,为ews.sendMail的异步操作争取了时间,所以能发送成功。如果将sendMail放到最后执行,进程结束了,发送邮件的操作就终止了。

解决办法很简单,将这个库的接口都改为async/await方式。并将原先的调用方法改为await ews.sendMail(email, title, msg);即可。

根据这个库梳理了调用EWS的流程。

Node.js调用EWS的原理

再解释两个名词:

  • SOAP。是一种消息格式(本质是XML)。用特定的结构和标签约定了消息的格式,比如<soap:Envelope>、<soap:Header>、<soap: Body>。EWS使用SOAP来传递消息和指令。

  • NTML。一种认证方式。用于鉴别访问者具有系统访问权限。EWS不止有一种认证方式,NTLM只是其中一种。

下面动手实现调用EWS的Node.js程序。

首先,构造SOAP格式的邮件内容。一个发送邮件的SOAP如下所示:

<?xml version="1.0" encoding="UTF-8"?>

<soap:Envelope

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages"

xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"

xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">

<soap:Header>

<t:RequestServerVersion Version="Exchange2010" />

</soap:Header>

<soap:Body>

<m:CreateItem MessageDisposition="SendAndSaveCopy">

<m:SavedItemFolderId>

<t:DistinguishedFolderId Id="sentitems" />

</m:SavedItemFolderId>

<m:Items>

<t:Message>

<t:Subject>测试</t:Subject>

<t:Body BodyType="HTML">你好</t:Body>

<t:ToRecipients>

<t:Mailbox>

<t:EmailAddress>recipient@example.com</t:EmailAddress>

</t:Mailbox>

</t:ToRecipients>

</t:Message>

</m:Items>

</m:CreateItem>

</soap:Body>

</soap:Envelope>

当然,EWS提供的远不止发邮件这么简单的功能。更多操作请参考官方文档。

构造完邮件内容之后,需要借助一个npm包:httpntlm。它实现了鉴权并发送邮件内容的功能。调用方法如下所示:

httpntlm.post({

username: 'xxx',

password: 'xxx',

domain: 'xxx',

url: 'xxx',

content: '' // SOAP邮件内容

})

ThinkJS实现定时任务

发邮件的功能完成了,需要实现定时功能。定时功能当然要借助crontab啦。ThinkJS只需要几行配置就能搞定crontab,不用开发者多操心。参考文档:https://thinkjs.org/zh-cn/doc/3.0/crontab.html

问题来了,如何保证邮件不重复发送(即定时任务不重复执行)。

首先,config/crontab.js(ts)要配置type: 'one'。这样能保证在开多个worker的时候只有一个worker会执行定时任务。

其次,如果项目部署在多台机器,要保证只有一个机器能执行定时任务,这个可以通过环境变量来实现,比如当process.env.CRONTAB为1的时候才开启。可以在将代码部署到线上的时候匹配特定的机器名,并在这台机器的部署命令中设置参数CRONTAB=1。

config/crontab.js代码如下:

module.exports = [{

immediate: false,

cron: '0 14 * * 4,5',

handle: 'api/crontab/xxx',

type: 'one',

enable: process.env.CRONTAB == '1'

}];

总结

本文介绍了邮件的基本原理和流程,EWS的用法,以及ThinkJS开发定时任务的注意事项。在开发过程中,顺带理解了SOAP、NTLM等协议。邮件功能还有其他开源的解决办法,比如基于SMTP等协议的开源项目nodemailer。希望本文能给大家带来启发,祝写码开心~

参考

  • 阮一峰:如何验证 Email 地址:SMTP 协议入门教程

  • JavaMail学习笔记(一)、理解邮件传输协议(SMTP、POP3、IMAP、MIME)

  • Microsoft NTLM文档

Node.js定时邮件的那些事儿的更多相关文章

  1. node.js爱心邮件

    一.用的软件是VsCode:下载地址:https://code.visualstudio.com/ 二.用的是node.js完成:下载地址:http://nodejs.cn/download/ 无脑下 ...

  2. node.js发邮件

    在node上使用第三方类库(nodemailer)发邮件是一件很esay的事情:) app.js   以QQ邮箱为例 var nodemailer = require('nodemailer'); v ...

  3. PHP vs Node.js

    网络正在处于一个日新月异的发展时代.服务器端开发人员在选择语言的时候非常困惑,有长期占主导地位的语言,例如C.Java和Perl,也有专注于web开发的语言,例如Ruby.Clojure和Go.只要你 ...

  4. [SD喜爱语言PK大赛]001.PHP vs Node.js

    引言:近日,两大编程飓风之战已经愈演愈烈.在程序员社区,一些争端因PHP与Node.js而起. 观点:其实就本人及团队而言,Language just a language!不存在高低之分,而侧重的原 ...

  5. node.js爬取数据并定时发送HTML邮件

    node.js是前端程序员不可不学的一个框架,我们可以通过它来爬取数据.发送邮件.存取数据等等.下面我们通过koa2框架简单的只有一个小爬虫并使用定时任务来发送小邮件! 首先我们先来看一下效果图 差不 ...

  6. 转:Node.js邮件发送组件- Nodemailer 1.0发布

    原文来自于http://www.infoq.com/cn/news/2014/07/node.js-nodemailer1.0-publish Nodemailer是一个简单易用的Node.js邮件发 ...

  7. 使用Node.js还可以发邮件

    前言 今天,我们给大家开发一个小效果.篇幅比较短,主要给大家展示效果.实战 首先我们初始化一个Node项目 npm init -y 创建一个app.js文件 'use strict'; const n ...

  8. Understanding node.js

    Node.js has generally caused two reactions in people I've introduced it to. Basically people either ...

  9. Node.js快速入门

    Node.js是什么? Node.js是建立在谷歌Chrome的JavaScript引擎(V8引擎)的Web应用程序框架. 它的最新版本是:v0.12.7(在编写本教程时的版本).Node.js在官方 ...

随机推荐

  1. vsftpd配置详解

    匿名用户权限控制: anonymous_enable=YES #是否启用匿名用户 no_anon_password=YES #匿名用户login时不询问口令 anon_upload_enable=(y ...

  2. uwsgi部署django项目

    一.更新系统软件包 yum update -y 二.安装软件管理包及依赖 yum -y groupinstall "Development tools" yum install o ...

  3. Vue学习笔记-组件通信-父传子(props中的驼峰标识)

    在组件中,使用选项props来声明需要从父级接收到的数据.props的值有两种方式:方式一:字符串数组,数组中的字符串就是传递时的名称.方式二:对象,对象可以设置传递时的类型,也可以设置默认值等. & ...

  4. 用Redis进行实时数据排名

    1先生成一个Redis对象 2实例化一个对象.zscore有序集合中进行排序 3 Redis Zscore命令返回有序集合中,成员的分数值.如果成员元素不是有序集合 key的成员,则key不存在,返回 ...

  5. Optional常用操作

    1. 常见操作 @Test public void test1() { F f = new F(); // of(非null对象) Optional<F> fOptional = Opti ...

  6. mock.js模拟生成假数据

    mock使用方法很简单, 下面是简单的用法, 详细的用法可以看官方文档, 写的很清楚, 下面的代码直接拷贝到本地html文件, 双击打开即可生成你想要的数据 <!DOCTYPE html> ...

  7. set实现数组去重后是对象,这里转化为数组

    ES6中新增了Set数据结构,类似于数组,但是 它的成员都是唯一的 ,其构造函数可以接受一个数组作为参数,如: let array = [1, 1, 1, 1, 2, 3, 4, 4, 5, 3]; ...

  8. linux下不同服务器间数据传输(rcp,scp,rsync,ftp,sftp,lftp,wget,curl)

    因为工作原因,需要经常在不同的服务器见进行文件传输,特别是大文件的传输,因此对linux下不同服务器间数据传输命令和工具进行了研究和总结.主要是rcp,scp,rsync,ftp,sftp,lftp, ...

  9. shell 截取变量的字符串

    假设有变量 var=http://www.linuxidc.com/test.htm一 # 号截取,删除左边字符,保留右边字符.echo ${var#*//}其中 var 是变量名,# 号是运算符,* ...

  10. 1.Jmeter 快速入门教程(一) - 认识jmeter和google插件

    Jmeter是免费开源的性能测试工具( 同时也可以用作功能测试,http协议debug工具 ).  在如今越来越注重知识产权的今天, 公司越来越不愿意冒着巨大的风险去使用盗版的商业性能测试工具. 但如 ...