使用 node.js 发送基于 STMP 与 MS Exchange 邮件的方法

目录
[隐藏]

本文主要介绍 node.js 发送基于 STMP 协议和 MS Exchange Web Service(EWS) 协议的邮件的方法。文中所有参考代码均以 TypeScript 编码示例。

1 基于 STMP 协议的 node.js 发送邮件方法

提到使用 node.js 发送邮件,基本都会提到大名鼎鼎的 Nodemailer 模块,它是当前使用 STMP 方式发送邮件的首选。
基于 NodeMailer 发送 STMP 协议邮件的文章网上已非常多,官方文档介绍也比较详细,在此仅列举示例代码以供对比参考:

封装一个 sendMail 邮件发送方法:

/**
 * 使用 Nodemailer 发送 STMP 邮件
 * @param {Object} opts 邮件发送配置
 * @param {Object} smtpCfg smtp 服务器配置
 */
async function sendMail(opts, smtpCfg) {
  const resultInfo = { code: 0, msg: '', result: null };
  if (!smtpCfg) {
      resultInfo.msg = '未配置邮件发送信息';
      resultInfo.code = - 1009;
      return resultInfo;
  }

  // 创建一个邮件对象
  const mailOpts = Object.assign(
    {
      // 发件人
      from: `Notify <${smtpCfg.auth.user}>`,
      // 主题
      subject: 'Notify',
      // text: opts.content,
      // html: opts.content,
      // 附件内容
      // /*attachments: [{
      //       filename: 'data1.json',
      //       path: path.resolve(__dirname, 'data1.json')
      //   }, {
      //       filename: 'pic01.jpg',
      //       path: path.resolve(__dirname, 'pic01.jpg')
      //   }, {
      //       filename: 'test.txt',
      //       path: path.resolve(__dirname, 'test.txt')
      //   }],*/
    },
    opts
  );

  if (!mailOpts.to) mailOpts.to = [];
  if (!Array.isArray(mailOpts.to)) mailOpts.to = String(mailOpts.to).split(',');
  mailOpts.to = mailOpts.to.map(m => String(m).trim()).filter(m => m.includes('@'));

  if (!mailOpts.to.length) {
      resultInfo.msg = '未配置邮件接收者';
      resultInfo.code = - 1010;
      return resultInfo;
  }

  const mailToList = mailOpts.to;
  const transporter = nodemailer.createTransport(smtpCfg);

  // to 列表分开发送
  for (const to of mailToList) {
    mailOpts.to = to.trim();
    try {
      const info = await transporter.sendMail(mailOpts);
      console.log('mail sent to:', mailOpts.to, ' response:', info.response);
      resultInfo.msg = info.response;
    } catch (error) {
      console.log(error);
      resultInfo.code = -1001;
      resultInfo.msg = error;
    }
  }

  return resultInfo;
}

使用 sendMail 方法发送邮件:

const opts = {
    subject: 'subject for test',
    /** HTML 格式邮件正文内容 */
    html: `email content for test: <a href="https://lzw.me">https://lzw.me</a>`,
    /** TEXT 文本格式邮件正文内容 */
    text: '',
    to: 'xxx@lzw.me',
    // 附件列表
    // attachments: [],
};
const smtpConfig = {
    host: 'smtp.qq.com', //QQ: smtp.qq.com; 网易: smtp.163.com
    port: 465, //端口号。QQ邮箱  465,网易邮箱 25
    secure: true,
    auth: {
      user: 'xxx@qq.com', //邮箱账号
      pass: '', //邮箱的授权码
    },
};
sendMail(opts, smtpConfig).then(result => console.log(result));

2 基于 MS Exchange 邮件服务器的 node.js 发送邮件方法

对于使用微软的 Microsoft Exchange Server 搭建的邮件服务,Nodemailer 就无能为力了。Exchange Web Service(EWS)提供了访问 Exchange 资源的接口,在微软官方文档中对其有详细的接口定义文档。针对 Exchange 邮件服务的流行第三方库主要有 node-ewsews-javascript-api

2.1 使用 node-ews 发送 MS Exchange 邮件

下面以 node-ews 模块为例,介绍使用 Exchange 邮件服务发送邮件的方法。

2.1.1 封装一个基于 node-ews 发送邮件的方法

封装一个 sendMailByNodeEws 方法:

import EWS from 'node-ews';

export interface IEwsSendOptions {
    auth: {
        user: string;
        pass?: string;
        /** 密码加密后的秘钥(NTLMAuth.nt_password)。为字符串时,应为 hex 编码结果 */
        nt_password?: string | Buffer;
        /** 密码加密后的秘钥(NTLMAuth.lm_password)。为字符串时,应为 hex 编码结果 */
        lm_password?: string | Buffer;
    };
    /** Exchange 地址 */
    host?: string;
    /** 邮件主题 */
    subject?: string;
    /** HTML 格式邮件正文内容 */
    html?: string;
    /** TEXT 文本格式邮件正文内容(优先级低于 html 参数) */
    text?: string;
    to?: string;
}

/**
 * 使用 Exchange(EWS) 发送邮件
 */
export async function sendMailByNodeEws(options: IEwsSendOptions) {
    const resultInfo = { code: 0, msg: '', result: null };

    if (!options) {
        resultInfo.code = -1001;
        resultInfo.msg = 'Options can not be null';
    } else if (!options.auth) {
        resultInfo.code = -1002;
        resultInfo.msg = 'Options.auth{user,pass} can not be null';
    } else if (!options.auth.user || (!options.auth.pass && !options.auth.lm_password)) {
        resultInfo.code = -1003;
        resultInfo.msg = 'Options.auth.user or Options.auth.password can not be null';
    }

    if (resultInfo.code) return resultInfo;

    const ewsConfig = {
        username: options.auth.user,
        password: options.auth.pass,
        nt_password: options.auth.nt_password,
        lm_password: options.auth.lm_password,
        host: options.host,
        // auth: 'basic',
    };

    if (ewsConfig.nt_password && typeof ewsConfig.nt_password === 'string') {
        ewsConfig.nt_password = Buffer.from(ewsConfig.nt_password, 'hex');
    }

    if (ewsConfig.lm_password && typeof ewsConfig.lm_password === 'string') {
        ewsConfig.lm_password = Buffer.from(ewsConfig.lm_password, 'hex');
    }

    Object.keys(ewsConfig).forEach(key => {
        if (!ewsConfig[key]) delete ewsConfig[key];
    });

    // initialize node-ews
    const ews = new EWS(ewsConfig);
    // define ews api function
    const ewsFunction = 'CreateItem';
    // define ews api function args
    const ewsArgs = {
        attributes: {
            MessageDisposition: 'SendAndSaveCopy',
        },
        SavedItemFolderId: {
            DistinguishedFolderId: {
                attributes: {
                    Id: 'sentitems',
                },
            },
        },
        Items: {
            Message: {
                ItemClass: 'IPM.Note',
                Subject: options.subject,
                Body: {
                    attributes: {
                        BodyType: options.html ? 'HTML' : 'Text',
                    },
                    $value: options.html || options.text,
                },
                ToRecipients: {
                    Mailbox: {
                        EmailAddress: options.to,
                    },
                },
                IsRead: 'false',
            },
        },
    };

    try {
        const result = await ews.run(ewsFunction, ewsArgs);
        // console.log('mail sent to:', options.to, ' response:', result);
        resultInfo.result = result;
        if (result.ResponseMessages.MessageText) resultInfo.msg = result.ResponseMessages.MessageText;
    } catch (err) {
        console.log(err.stack);
        resultInfo.code = 1001;
        resultInfo.msg = err.stack;
    }

    return resultInfo;
}

使用 sendMailByNodeEws 方法发送邮件:

sendMailByNodeEws({
       auth: {
        user: 'abc@xxx.com',
        pass: '123456',
        /** 密码加密后的秘钥(NTLMAuth.nt_password)。为字符串时,应为 hex 编码结果 */
        nt_password: '',
        /** 密码加密后的秘钥(NTLMAuth.lm_password)。为字符串时,应为 hex 编码结果 */
        lm_password: '',
    },
    /** Exchange 地址 */
    host: 'https://ews.xxx.com',
    /** 邮件主题 */
    subject: 'subject for test',
    /** HTML 格式邮件正文内容 */
    html: `email content for test: <a href="https://lzw.me">https://lzw.me</a>`,
    /** TEXT 文本格式邮件正文内容(优先级低于 html 参数) */
    text: '',
    to: 'xxx@lzw.me',
})

2.1.2 基于 NTLMAuth 的认证配置方式

直接配置 pass 密码可能会导致明文密码泄露,我们可以将 pass 字段留空,配置 nt_passwordlm_password 字段,使用 NTLMAuth 认证模式。此二字段基于 pass 明文生成,其 nodejs 生成方式可借助 httpntlm 模块完成,具体参考如下:

import { ntlm as NTLMAuth } from 'httpntlm';

/** 将输入的邮箱账号密码转换为 NTLMAuth 秘钥(hex)格式并输出 */
const getHashedPwd = () => {
  const passwordPlainText = process.argv.slice(2)[0];

  if (!passwordPlainText) {
    console.log('USEAGE: \n\tnode get-hashed-pwd.js [password]');
    return;
  }

  const nt_password = NTLMAuth.create_NT_hashed_password(passwordPlainText.trim());
  const lm_password = NTLMAuth.create_LM_hashed_password(passwordPlainText.trim());

  // console.log('\n password:', passwordPlainText);
  console.log(` nt_password:`, nt_password.toString('hex'));
  console.log(` lm_password:`, lm_password.toString('hex'));

  return {
    nt_password,
    lm_password,
  };
};

getHashedPwd();

2.2 使用 ews-javascript-api 发送 MS Exchange 邮件

基于 ews-javascript-api 发送邮件的方式,在其官方 wiki 有相关示例,但本人在测试过程中未能成功,具体为无法取得服务器认证,也未能查证具体原因,故以下代码仅作参考:

/**
 * 使用 `ews-javascript-api` 发送(MS Exchange)邮件
 */
export async function sendMailByEwsJApi(options: IEwsSendOptions) {
    const resultInfo = { code: 0, msg: '', result: null };

    if (!options) {
        resultInfo.code = -1001;
        resultInfo.msg = 'Options can not be null';
    } else if (!options.auth) {
        resultInfo.code = -1002;
        resultInfo.msg = 'Options.auth{user,pass} can not be null';
    } else if (!options.auth.user || (!options.auth.pass && !options.auth.lm_password)) {
        resultInfo.code = -1003;
        resultInfo.msg = 'Options.auth.user or Options.auth.password can not be null';
    }

    const ews = require('ews-javascript-api');
    const exch = new ews.ExchangeService(ews.ExchangeVersion.Exchange2010);
    exch.Credentials = new ews.WebCredentials(options.auth.user, options.auth.pass);
    exch.Url = new ews.Uri(options.host);
    ews.EwsLogging.DebugLogEnabled = true; // false to turnoff debugging.

    const msgattach = new ews.EmailMessage(exch);
    msgattach.Subject = options.subject;
    msgattach.Body = new ews.MessageBody(ews.BodyType.HTML, escape(options.html || options.text));
    if (!Array.isArray(options.to)) options.to = [options.to];
    options.to.forEach(to => msgattach.ToRecipients.Add(to));
    // msgattach.Importance = ews.Importance.High;

    // 发送附件
    // msgattach.Attachments.AddFileAttachment('filename to attach.txt', 'c29tZSB0ZXh0');

    try {
        const result = await msgattach.SendAndSaveCopy(); // .Send();
        console.log('DONE!', result);
        resultInfo.result = result;
    } catch (err) {
        console.log('ERROR:', err);
        resultInfo.code = 1001;
        resultInfo.msg = err;
    }
    return resultInfo;
}

3 扩展参考

点赞 (1)
  1. 王光卫博客说道:
    Google Chrome 87.0.4280.141 Google Chrome 87.0.4280.141 Mac OS X  11.1.0 Mac OS X 11.1.0

    我要去GitHub clone一份 :razz:

发表评论

邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

543 151 237 517 004 423 407 909 973 369 525 191 420 211 101 358 161 565 576 730 059 271 819 065 197 935 364 702 435 690 466 112 462 505 672 102 969 576 792 339 125 817 314 998