Hi,亲爱的用户:

最近我们正以「LeanCloud Web 应用开发实践」为主题,进行一系列的直播及文章分享。并将重写后的 LeanTicket (工单)作为范例,介绍使用 LeanCloud 开发 Web 应用的一些最佳实践,内容包括且不限于:

  1. 使用 LeanCloud 账号系统以及三方 OAuth 授权。
  2. 使用 ACL 保障数据安全。
  3. 使用云引擎的云函数和 hook 函数简化客户端逻辑。
  4. LiveQuery 的使用。
  5. 与微信或其他外部系统对接。
  6. 邮件的发送即接收。
  7. 批量处理/订正数据的实践。
  8. 使用数据缓存提高响应速度,减少存储服务查询次数。

欢迎您加入微信群(文末扫码)进行交流。

第七期课程:如何通过邮件处理工单

很多用户习惯使用 Email 来处理工作,所以工单系统也接入了邮件功能,不光可以用邮件接收工单提醒,还可以直接通过邮件来回复工单。

本期我们来介绍邮件的接入:

  • 通过工单系统的事件自动发送邮件。
  • 通过回复邮件来回复工单。

讲师

LeanCloud 工程师 陈伟

直播时间

2017 年 9 月 12 日(周二) 20:00
课程地址:https://www.douyu.com/15879691634

扫描下方二维码即可加入「LeanCloud Web 应用开发实践」,如有疑问也可加微信「 wangxiaoneng123 」咨询。

本主题已置顶,它将始终显示在它所属分类的顶部。可由职员对所有人解除置顶,或者由用户自己取消置顶。

视频回放: https://www.bilibili.com/video/av14410658/25

使用场景 00:00

很多用户习惯使用 Email 来处理工作,所以工单系统也接入了邮件功能,不光可以用邮件接收工单提醒,还可以直接通过邮件来回复工单。

选择邮件服务提供商 01:40

如果要在应用中接入邮件功能,首先需要选择一个邮件服务提供商。对于服务商的选择不是本文的重点,我们就以 Mailgun9 来举例,其他的邮件服务使用起来也大同小异。

预先准备:

  • 注册邮件服务商的账号。
  • 添加需要的 domain,根据需要修改相关 DNS 的信息,或者使用默认提供的 domain。
  • 从相关配置页面获取一些 key 等信息,以备程序中使用。比如 mailgun 就需要 apiKeydomain 两个信息。
  • 查找是否有相关的 SDK,能极大的提高程序开发效率。比如 mailgun-js1

提示:mailgun 的 apiKey 等信息不要写死在代码里,建议配置在云引擎的环境变量中,设置方法:LeanCloud 控制台 -> 云引擎 -> 设置 -> 自定义环境变量 中配置。

Ticket 中发送邮件 05:37

本文使用的 LeanTicket2 版本 6b962f2

以「新工单产生」事件为例:新的工单产生会触发 TicketafterSave hook1,会调用 api/notify.jsnewTicket1 方法:

// file: api/Ticket.js
// ...
AV.Cloud.afterSave('Ticket', (req) => {
  // ...
      return notify.newTicket(req.object, req.currentUser, assignee)
  // ...
})
// ...

// file: api/notify.js
// ...
exports.newTicket = (ticket, author, assignee) => {
  // ...
    mail.newTicket(ticket, author, assignee),
  // ...
}
// ...

最终调用到 api/mail.jsnewTicket 方法:

// ...
exports.mailgun = require('mailgun-js')({apiKey: config.mailgunKey, domain: config.mailgunDomain})
// ...
exports.newTicket = (ticket, from, to) => {
  // ...
  return send({
    from: `${from.get('username')} <ticket@leancloud.cn>`,
    to: to.get('email'),
    subject: `[LeanTicket] ${ticket.get('title')} (#${ticket.get('nid')})`,
    'h:Reply-To': `ticket-${to.id}@leancloud.cn`,
    text: ticket.get('content'),
    url: common.getTicketUrl(ticket),
  })
}
// ...
const send = (params) => {
  // ...
  return exports.mailgun.messages().send({
    from: params.from,
    to: params.to,
    subject: params.subject,
    'h:Reply-To': params['h:Reply-To'],
    text: `${params.text},
--
您能收到邮件是因为该工单与您相关。
可以直接回复邮件,或者点击 ${params.url} 查看。`,
  })
  .catch((err) => {
    // ...
  })
}

基本上就是封装发送邮件所需要的参数,然后调用 mailgun SDK API 即可。replyTicketchangeAssignee 方法类似。

关于 h:Reply-To 部分在后面的「确定『回复人』」部分提到。

通过邮件回复工单 12:06

通过邮件回复工单,邮件先发送到 mailgun 服务器,通过 mailgun 配置的 route 就可以把邮件转发到 LeanTicket 中。所以我们需要先在 mailgun 中配置一个 route:

  • Expression Type: Match Recipient (匹配收件人)
  • Recipient:ticket(.*)@leancloudcn(收件人是 ticket 开头 @leancloud.cn 结尾)
  • Actions: Store and notify: https://leanticket.cn/webhooks/mailgun/catchall (邮件发送到指定的 URL)

也需要在应用中配置相关的路由来接受邮件:

// file: api/index.js
router.use('/webhooks/mailgun', require('./mailgun'))

对于接受邮件的 webHook 路由,需要确认发起请求的是合法的(是 mailgun 服务器发出,而不是第三方伪造),需要一个鉴权的过程(代码):

  router.post('/*', function (req, res, next) {
    const body = req.body
    if (!mailgun.validateWebhook(body.timestamp, body.token, body.signature)) {
      console.error('Request came, but not from Mailgun')
      res.send({ error: { message: 'Invalid signature. Are you even Mailgun?' } })
      return
    }
    next()
  })

鉴权通过后就可以从邮件中提取相关的信息,来生成工单的回复信息:

router.post('/catchall', function (req, res, next) {
  Promise.all([
    getFromUser(req.body),
    getTicket(req.body),
  ]).spread((fromUser, ticket) => {
    return new AV.Object('Reply').save({
      ticket,
      content: req.body['stripped-text'].replace(/\r\n/g, '\n')
    }, {user: fromUser})
  }).then(() => {
    res.send('OK')
  }).catch(next)
})

确定「回复人」18:42

邮件回复工单时,确定回复人可能是一个问题,假设场景:

  • 有一个工单,通过邮件发送到客服张三的 zhangsan@lc.cn 邮箱。
  • 张三习惯使用个人邮箱 zhangsan@gmail.com 抓取自己其他邮箱邮件统一浏览,所以工单邮件被抓取到 zhangsan@gmail.com 中。
  • 张三回复邮件,此时邮件的「回复人」是 zhangsan@gmail.com 。
  • 邮件经过 mailgun 到达 Ticket 应用,Ticket 根据邮件的回复者 zhangsan@gmail.com 到 _User 表查询不到任何信息(因为这不是张三在 Ticket 中注册的邮箱)。

为了解决上面的问题,Ticket 在发送邮件时设置了 特殊的头信息

// file: api/mail.js
'h:Reply-To': `ticket-${to.id}@leancloud.cn`

邮件客户端在回复邮件时会将该头信息的值作为邮件的「收件人」,我们在其中拼接了 userId 信息,通过该信息就能定位到 邮件的回复人在 LeanTicket 中具体是哪个用户,而不用关心其 email 信息:

// file: api/mailgun.js
const getFromUser = (mail) => {
  const match = mail.To.match(/^.*<ticket-(.*)@leancloud.cn>.*$/)
  if (match) {
    return new AV.Query('_User').get(match[1], {useMasterKey: true})
    .then((user) => {
      if (!user) {
        throw new Error('user not found, objectId=' + match[1])
      }
      return user
    })
  }
  throw new Error('user objectId mismatch:' + mail.To)
}

提示:因为此流程是 mailgun 触发,调用该方法的上下文没有 currentUser 信息,所以查询 _User 时使用 masterKey 权限。

另外通过邮件标题中的工单编号,很容易定位到具体的工单:

const getTicket = (mail) => {
  const match = mail.Subject.match(/.*\s\(#(\d+)\)$/)
  if (match) {
    return new AV.Query('Ticket').equalTo('nid', parseInt(match[1])).first({useMasterKey: true})
  }
  return Promise.resolve()
}

This topic is now unpinned. It will no longer appear at the top of its category.