第9章、Lift发送电子邮件,呼叫URL或调度任务

本章讨论与Lift内的其他系统进行交互,如发送电子邮件,呼叫URL或调度任务。

本章中的许多配方都有一个项目中的代码示例,网址为https://github.com/LiftCookbook/cookbook_around

发送纯文本电子邮件

问题

您要从Lift应用程序发送纯文本电子邮件。

使用Mailer

import net.liftweb.util.Mailer
import net.liftweb.util.Mailer._

Mailer.sendMail(
  From("you@example.org"),
  Subject("Hello"),
  To("other@example.org"),
  PlainMailBodyType("Hello from Lift") )

讨论

Mailer以异步方式发送消息,意思sendMail将立即返回,因此您不必担心与SMTP服务器进行协商的时间成本。但是,blockingSendMail 如果您需要等待,还有一种方法。

默认情况下,使用的SMTP服务器将是localhost您可以通过设置mail.smtp.host属性来更改此值例如,编辑src / mail / resources / props / default.props并添加行:

mail.smtp.host=smtp.example.org

签名sendMail需要一个FromSubject然后任意数量MailTypes

ToCCBCC
收件人电子邮件地址
ReplyTo
邮件客户端应该用于回复的地址
MessageHeader
键/值对以在消息中包括为头
PlainMailBodyType
以UTF-8编码发送的纯文本电子邮件
PlainPlusBodyType
一个纯文本电子邮件,您可以在其中指定编码
XHTMLMailBodyType
对于HTML电子邮件( “HTML电子邮件”
XHTMLPlusImages
对于附件( “使用附件发送电子邮件”

在前面的例子中,我们增加了两个类型:PlainMailBodyType 和To添加更多是你所期望的:

Mailer.sendMail(
  From("you@example.org"),
  Subject("Hello"),
  To("other@example.org"),
  To("someone@example.org"),
  MessageHeader("X-Ignore-This", "true"),
  PlainMailBodyType("Hello from Lift") )

地址类MailTypesToCCBCCReplyTo)可以给出一个可选的“人名”:

From("you@example.org", Full("Example Corporation"))

这将显示在您的邮箱中:

来自:Example Corporation <you@example.org>

默认字符集为UTF-8。如果您需要进行更改,替换使用的PlainMailBodyType用 PlainPlusBodyType("Hello from Lift", "ISO8859_1")

也可以看看

“使用附件发送电子邮件”描述具有附件的电子邮件。

对于HTML电子邮件,请参阅“HTML电子邮件”

记录电子邮件而不是发送

问题

在本地开发升降机应用程序时不需要发送电子邮件,但您确实想要看到发送的内容。

Mailer.devModeSendBoot.scala中分配日志功能

import net.liftweb.util.Mailer._
import javax.mail.internet.{MimeMessage,MimeMultipart}

Mailer.devModeSend.default.set( (m: MimeMessage) =>
  logger.info("Would have sent: "+m.getContent)
)

当您发送电子邮件时Mailer,不会联系到SMTP服务器,相反,您会看到输出到您的日志中:

会发送:Lift你好

讨论

这个配方的关键部分是设置一个 MimeMessage => Unit功能Mailer.devModeSend我们恰好是日志记录,但您可以使用此功能以任何方式处理电子邮件。

该LiftMailer允许您控制电子邮件的方式在每次运行模式发送:默认情况下,电子邮件已发送devModeSendprofileModeSendpilotModeSendstagingModeSend,和productionModeSend而默认情况下,testModeSend仅记录消息将被发送的日志。

testModeSend日志的一个参考MimeMessage,这意味着你的日志会显示这样的消息:

发送javax.mail.internet.MimeMessage@4a91a883

此配方已更改了MailerLift应用程序处于开发人员模式(默认情况下)的行为。我们正在记录消息的正文部分。

Java Mail不包含用于显示电子邮件的所有部分的实用程序,因此如果需要更多信息,则需要滚动自己的功能。例如

def display(m: MimeMessage) : String = {

  val nl = System.getProperty("line.separator")

  val from = "From: "+m.getFrom.map(_.toString).mkString(",")

  val subj = "Subject: "+m.getSubject

  def parts(mm: MimeMultipart) = (0 until mm.getCount).map(mm.getBodyPart)

  val body = m.getContent match {
    case mm: MimeMultipart =>
      val bodyParts = for (part <- parts(mm)) yield part.getContent.toString
      bodyParts.mkString(nl)

    case otherwise => otherwise.toString
  }

  val to = for {
    rt <- List(RecipientType.TO, RecipientType.CC, RecipientType.BCC)
    address <- Option(m.getRecipients(rt)) getOrElse Array()
  } yield rt.toString + ": " + address.toString

  List(from, to.mkString(nl), subj, body) mkString nl
}

Mailer.devModeSend.default.set( (m: MimeMessage) =>
  logger.info("Would have sent: "+display(m))
)

这将产生以下形式的输出:

将发送:From:you@example.org
至:other@example.org
至:someone@example.org
主题:你好
Lift你好

此示例display函数很长,但最简单。body值通过提取每个主体部分来处理多部分消息。发送更多结构化的电子邮件(例如HTML电子邮件)时触发“HTML电子邮件”中描述

如果要在实际发送邮件时调试邮件系统,请启用Java Mail调试模式。default.props中添加:

mail.debug=true

当发送电子邮件时,这会产生Java Mail系统的低级别输出:

DEBUG:JavaMail版本1.4.4
DEBUG:成功加载资源:/META-INF/javamail.default.providers
DEBUG SMTP:useEhlo true,useAuth false
DEBUG SMTP:尝试连接到主机“localhost”,端口25,isSSL为false
...

也可以看看

运行模式在Lift wiki上描述

HTML电子邮件

问题

您要从Lift应用程序发送HTML电子邮件。

Mailer一个NodeSeq包含你的HTML消息:

import net.liftweb.util.Mailer
import net.liftweb.util.Mailer._

val msg = <html>
   <head>
     <title>Hello</title>
   </head>
   <body>
    <h1>Hello</h1>
   </body>
  </html>

Mailer.sendMail(
  From("me@example.org"),
  Subject("Hello"),
  To("you@example.org"),
  msg)

讨论

一个隐含的转换NodeSeq成一个XHTMLMailBodyType这确保电子邮件的MIME类型text/html尽管名称为“XHTML”,但是使用HTML5语义将消息转换为传输。

可以通过mail.charset在Lift属性文件中进行设置来更改HTML电子邮件(UTF-8)的字符编码 

如果要同时设置消息的文本和HTML版本,请提供包含在相应BodyType中的每个正文

val html = <html>
  <head>
    <title>Hello</title>
  </head>
  <body>
    <h1>Hello!</h1>
  </body>
</html>

var text = "Hello!"

Mailer.sendMail(
  From("me@example.org"),
  Subject("Hello"),
  To("you@example.org"),
  PlainMailBodyType(text),
  XHTMLMailBodyType(html)
)

此消息将作为multipart/alternative

Content-Type:multipart / alternative;
  boundary =“---- = _ Part_1_1197390963.1360226660982”
日期:2013年2月7日,02:44:22 -0600(CST)

------ = _ Part_1_1197390963.1360226660982
Content-Type:text / plain; charset = UTF-8
内容传输编码:7bit

你好!
------ = _ Part_1_1197390963.1360226660982
Content-Type:text / html; charset = UTF-8
内容传输编码:7bit

<html>
      <head>
        <title> Hello </ title>
      </ head>
      <body>
        <h1>你好!</ h1>
      </ body>
    </ html>
------ = _ Part_1_1197390963.1360226660982--

当收到含有此内容的邮件时,由邮件客户端决定要显示哪个版本(文本或HTML)。

也可以看看

要发送附件,请参阅“使用附件发送电子邮件”

发送认证电子邮件

问题

您需要使用SMTP服务器进行身份验证才能发送电子邮件。

设置Mailer.authenticatorBoot与您的SMTP服务器的凭据,并启用mail.smtp.auth标志在提升属性文件。

修改Boot.scala以包括:

import net.liftweb.util.{Props, Mailer}
import javax.mail.{Authenticator,PasswordAuthentication}

Mailer.authenticator = for {
  user <- Props.get("mail.user")
  pass <- Props.get("mail.password")
} yield new Authenticator {
  override def getPasswordAuthentication =
    new PasswordAuthentication(user,pass)
}

在这个例子中,我们期望用户名和密码来自Lift属性,所以我们需要修改 src / main / resources / props / default.props来包含它们:

mail.smtp.auth=true
mail.user=me@example.org
mail.password=correct horse battery staple
mail.smtp.host=smtp.sendgrid.net

当您发送电子邮件,在凭证default.props将用于与SMTP服务器进行身份验证。

讨论

我们已经使用Lift属性来配置SMTP身份验证。这有利于允许我们为一些运行模式启用身份验证。例如,如果我们的default.props不包含身份验证设置,但是我们的production.default.props没有,那么在开发模式下不会发生身份验证,确保我们无法在生产环境之外不小心发送电子邮件。

您不必为此使用属性文件:Lift Mailer 还支持JNDI,或者您可以使用其他方式查找用户名和密码,并Mailer.authenticator在有值时设置

但是,一些邮件服务(如SendGrid)需要mail.smtp.auth=true设置,并且应该进入Lift属性文件或设置为JVM参数:-Dmail.smtp.auth=true

也可以看看

除此之外mail.smtp.auth,还有一系列设置来控制Java Mail API示例包括控制端口号和超时。

使用附件发送电子邮件

问题

您想发送一封包含一个或多个附件的电子邮件。

使用Mailer XHTMLPlusImages包装附件的邮件。

假设我们要构建一个CSV文件并通过电子邮件发送:

val content = "Planet,Discoverer\r\n" +
  "HR 8799 c, Marois et al\r\n" +
  "Kepler-22b, Kepler Science Team\r\n"

case class CSVFile(bytes: Array[Byte],
  filename: String = "file.csv",
  mime: String = "text/csv; charset=utf8; header=present" )

val attach = CSVFile(content.mkString.getBytes("utf8"))

val body = <p>Please research the enclosed.</p>

val msg = XHTMLPlusImages(body,
  PlusImageHolder(attach.filename, attach.mime, attach.bytes))

Mailer.sendMail(
  From("me@example.org",
  Subject("Planets"),
  To("you@example.org"),
  msg)

这里发生的是我们的消息是一个XHTMLPlusImages实例,它接受一个正文消息和附件。附件,the PlusImageHolder,是一个Array[Byte],mime类型和一个文件名。

讨论

XHTMLPlusImagesPlusImageHolder如果您有多个文件要附加,也可以接受多个虽然名称PlusImageHolder可能表明它是附件图像,您可以附加任何类型的数据作为Array[Byte]适当的MIME类型。

默认情况下,附件以配置方式发送inline这将控制Content-Disposition消息中标题,并且inline意味着当显示消息时,内容将自动显示。替代方案是attachment,并且可以使用可选的最终参数来指示PlusImageHolder

PlusImageHolder(attach.filename, attach.mime, attach.bytes, attachment=true)

实际上,邮件客户端将显示消息的方式,但这个额外的参数可能会给你一些更多的控制权。

要附加预制文件,可以使用LiftRules.loadResource从类路径中获取内容。如果我们的项目src / main / resources /文件夹中包含一个名为Kepler-22b_System_Diagram.jpg文件,我们可以加载并附加它:

val filename = "Kepler-22b_System_Diagram.jpg"

val msg =
  for ( bytes <- LiftRules.loadResource("/"+filename) )
  yield XHTMLPlusImages(
    <p>Please research this planet.</p>,
    PlusImageHolder(filename, "image/jpg", bytes) )

msg match {
  case Full(m) =>
    Mailer.sendMail(
      From("me@example.org"),
      Subject("Planet attachment"),
      To("you@example.org"),
      m)

  case _ =>
    logger.error("Planet file not found")
}

由于src / main / resources的内容包含在类路径中,所以我们将文件名传递给loadResource一个前导/字符,以便该文件可以在类路径的正确位置找到。

loadResource回报Box[Array[Byte]],因为我们不能保证该文件将存在。我们将其映射到Box[XHTMLPlusImages]该结果并匹配,以发送电子邮件或记录该文件未找到。

也可以看看

消息是使用multipart/relatedmime标题发送的,具有inline配置。电话机#1197链接到有关这方面的讨论multipart/mixed可能更适用于解决Microsoft Exchange的问题。

RFC 2183描述了Content-Disposition标题。

稍后执行任务

问题

您希望计划在将来某个时间运行的代码。

使用net.liftweb.util.Schedule

import net.liftweb.util.Schedule
import net.liftweb.util.Helpers._

Schedule(() => println("doing it"), 30 seconds)

这将导致“做”在30秒后从控制台上打印出来。

讨论

以前Schedule使用的签名预期是类型的功能() => Unit,这是我们以后想要发生的事情,而TimeSpan来自Lift的TimeHelpers,这是当我们想要发生的时候。30 secondsTimeSpan通过Helpers._导入给我们,但是如果您喜欢,则会有一个称为perform接受Long毫秒值的变体:

Schedule.perform(() => println("doing it"), 30*1000L)

在幕后,Lift正在利用ScheduledExecutorService来自java.util.concurrent和,因此返回a ScheduledFuture[Unit]cancel在运行之前,可以使用这个未来的操作。

可能会发现您只能Schedule使用函数作为参数调用,而不是延迟值。此版本立即运行该功能,但在工作线程上运行。这是一种方便地异步运行其他任务的方法,而不用为此目的创建一个演员的麻烦。

还有一种Schedule.schedule方法可以在给定的延迟之后向演员发送指定的消息。这需要一个TimeSpan延迟,但是也有一个Schedule.perform版本接受Long一个延迟。

Helpers._进口与它带来了一些隐式转换TimeSpan例如,Period可以给出JodaTime schedule并在执行函数之前将其用作延迟。在这种情况下不要试图使用JodaTime DateTime这将被转换为一个TimeSpan,但没有任何延迟的意义。

也可以看看

“定期运行任务”包括与演员进行调度的示例。

ScheduledFuture通过Java文档记录Future如果您正在构建复杂的,低级别的可撤销并发函数,建议您将Java并发实践的副本(Goetz 等人,Addison-Wesley Professional)撰写。

定期运行任务

问题

您希望计划任务定期运行(重复)。

使用net.liftweb.util.Schedule确保schedule在您的任务期间再次呼叫重新安排。例如,使用演员:

import net.liftweb.util.Schedule
import net.liftweb.actor.LiftActor
import net.liftweb.util.Helpers._

object MyScheduledTask extends LiftActor {

  case class DoIt()
  case class Stop()

  private var stopped = false

   def messageHandler = {
     case DoIt if !stopped =>
        Schedule.schedule(this, DoIt, 10 minutes)
       // ... do useful work here

     case Stop =>
       stopped = true
   }
}

该示例LiftActor为要完成的工作创建一个在接收到DoIt消息之后,演员在做任何有用的工作需要完成之前重新调整自己。以这种方式,演员将每10分钟一次。

讨论

Schedule.schedule呼叫确保this演员在发送 DoIt10分钟后消息。

要启动此过程,可能在Boot.scala中,只需将 DoIt消息发送给演员:

MyScheduledTask ! MyScheduledTask.DoIt

为了确保在Lift关闭时进程正确停止,我们在Boot.scala注册一个关机挂钩来发送Stop消息以防止将来重新安排:

LiftRules.unloadHooks.append( () => MyScheduledTask ! MyScheduledTask.Stop )

没有Stop消息,演员将继续重新安排,直到JVM退出。这可能是可以接受的,但请注意,在使用SBT开发过程中,没有Stop消息,您将在发出container:stop命令后继续安排任务

计划ScheduledFuture[Unit]从Java并发库中返回一个,它允许您进行cancel活动。

也可以看看

“ 提升行动”(Perrett,Manning Publications,Co.)的第1章包括使用的彗星演员时钟示例Schedule

获取网址

问题

您希望Lift应用程序获取URL并将其处理为文本,JSON,XML或HTML。

使用Dispatch “,用于异步HTTP交互的库”。

在开始之前,在build.sbt中包含Dispatch依赖关系

libraryDependencies += "net.databinder.dispatch" %% "dispatch-core" % "0.9.5"

使用Dispatch文档中的示例,我们可以发出一个HTTP请求,以从http://www.hostip.info/use.html尝试从服务中确定国家/地区

import dispatch._
val svc = url("http://api.hostip.info/country.php")
val country : Promise[String] = Http(svc OK as.String)

println(country())

注意,结果country不是一个,String而是Promise[String]我们apply用来等待结果值。

打印的结果将是一个国家代码,例如GB,或者XX如果不能从您的IP地址确定该国家。

讨论

这个简短的例子预计会有一个200(OK)状态结果,并将结果变成一个String,但这是Dispatch能够实现的一小部分。我们将在本节进一步探讨。

如果请求不返回200怎么办?在这种情况下,使用我们的代码,我们会收到例外:“意外的响应状态:404.” 有几种方法可以改变。

我们可以要求Option

val result : Option[String] = country.option()

正如你所料,这会给一个None或者Some[String]但是,如果您的应用程序中启用了调试级别日志记录,那么您将看到底层Netty库中的请求和响应和错误消息。您可以通过将记录器设置添加到default.logback.xml文件来调整这些消息

<logger name="com.ning.http.client" level="WARN"/>

第二种可能性是either与通常惯例一起使用,这Right是预期的结果,Left意味着失败:

country.either() match {
  case Left(status) => println(status.getMessage)
  case Right(cc) => println(cc)
}

这将打印结果,因为我们强制评估与应用通过either()

Promise[T]农具mapflatMapfilterfold,和所有你希望它让您能够在通常的方法。这意味着你可以用理解的承诺for 

val codeLength = for (cc <- country) yield cc.length

注意codeLength是a Promise[Int]要获得这个价值,你可以评估codeLength(),你会得到一个结果2

除了提取字符串值as.String之外,还有其他选项,包括:

as.Bytes
跟...共事  Promise[Array[Byte]]
as.File
写入一个文件,如同  Http(svc > as.File(new File("/tmp/cc")))
as.Response
允许您提供一个 client.Response => T 功能来使用响应
as.xml.Elem
解析XML响应

例如as.xml.Elem

val svc = url("http://api.hostip.info/?ip=12.215.42.19")
val country  = Http(svc > as.xml.Elem)
println(country.map(_ \\ "description")())

此示例解析对请求的XML响应,该请求返回Promise[scala.xml.Elem]我们通过a选择XML的描述节点map,这将是Promise[NodeSeq]我们强制评估的一个。输出结果如下:

<gml:description
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:gml="http://www.opengis.net/gml">
     这是Hostip Lookup Service
</gml:description>

这个例子假设这个请求要形成良好。除了核心Databinder库之外,还有JSoup和TagSoup的扩展,可帮助解析不一定形成良好的HTML。

例如,要使用JSoup,请包括依赖关系:

libraryDependencies += "net.databinder.dispatch" %% "dispatch-jsoup" % "0.9.5"

然后,您可以使用JSoup的功能,例如使用CSS选择器选择页面的元素:

import org.jsoup.nodes.Document

val svc = url("http://www.example.org").setFollowRedirects(true)
val title = Http(svc > as.jsoup.Document).map(_.select("h1").text).option
println( title() getOrElse "unknown title" )

在这里,我们应用JSoup的select功能来选择<h1>页面上的元素,获取元素的文本,我们将其转换为Promise[Option[String]]结果,除非example.org已经改变,否则将是“示例域”。

作为使用Dispatch的最后一个例子,我们可以将一个请求传递给Lift的JSON库:

import net.liftweb.json._
import com.ning.http.client

object asJson extends (client.Response => JValue) {
  def apply(r: client.Response) = JsonParser.parse(r.getResponseBody)
}

val svc = url("http://api.hostip.info/get_json.php?ip=212.58.241.131")
val json : Promise[JValue] = Http(svc > asJson)

case class HostInfo(country_name: String, country_code: String)
implicit val formats = DefaultFormats

val hostInfo = json.map(_.extract[HostInfo])()

我们正在调用的URL为我们已经通过的IP地址的位置信息返回JSON表示。

通过提供一个Response => JValueDispatch,我们可以将响应体传递给JSON解析器。然后我们可以映射Promise[JValue]以应用我们想要的任何Lift JSON函数。在这种情况下,我们正在提取一个简单的case类。

结果将显示hostInfo为:

HostInfo(UNITED KINGDOM,GB)

也可以看看

Dispatch文档写得很好,并指导您完成Dispatch接近HTTP的方式。花一些时间。

有关Dispatch的问题,最好的地方是Dispatch Google Group

以前的主要版本Dispatch,0.8.x(“Dispatch Classic”)与0.9版本的“重新启动”项目完全不同。因此,您可能会看到使用0.8.x的示例将需要一些转换才能运行0.9.x。Nathan Hamblen的博客描述了这个变化。

要与JSoup合作,请查看JSoup Cookbook

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值