scala/mail-parser/mail-parser.sc
changeset 53 09b1d3c0aa20
equal deleted inserted replaced
52:c0d94e64d89a 53:09b1d3c0aa20
       
     1 #!/usr/bin/env amm
       
     2 
       
     3 import java.io.{ByteArrayInputStream, File, InputStream}
       
     4 import java.nio.file.{Files, StandardCopyOption}
       
     5 import java.text.SimpleDateFormat
       
     6 import java.util.Date
       
     7 
       
     8 import ammonite.main.Router.{doc, main}
       
     9 import ammonite.ops._
       
    10 import upickle.default
       
    11 
       
    12 //import $ivy.`javax.mail:javax.mail-api:1.6.1`
       
    13 //import $ivy.`com.sun.mail:mailapi:1.6.1`
       
    14 import $ivy.`javax.mail:javax.mail-api:1.6.1`
       
    15 import $ivy.`com.sun.mail:mailapi:1.6.1`
       
    16 import scala.language.postfixOps
       
    17 import javax.mail.internet._
       
    18 import javax.mail._
       
    19 import javax.mail.{Address => mAddress}
       
    20 import javax.mail.internet.{ContentType => mContentType}
       
    21 
       
    22 import scala.collection.JavaConverters._
       
    23 
       
    24 case class Address(email: String, name: Option[String], `type`: String)
       
    25 
       
    26 object Address {
       
    27   def apply(a: mAddress): Option[Address] = a match {
       
    28     case ia:InternetAddress =>
       
    29       Some(Address(ia.getAddress, Option(ia.getPersonal), a.getType))
       
    30     case _ =>
       
    31       None
       
    32   }
       
    33 }
       
    34 
       
    35 sealed abstract class SystemFlag(code: String)
       
    36 object SystemFlag {
       
    37   case object ANSWERED extends SystemFlag("Answered")
       
    38   case object DELETED extends SystemFlag("Deleted")
       
    39   case object DRAFT extends SystemFlag("Draft")
       
    40   case object FLAGGED extends SystemFlag("Flagged")
       
    41   case object RECENT extends SystemFlag("Recent")
       
    42   case object SEEN extends SystemFlag("Seen")
       
    43   case object USER extends SystemFlag("User")
       
    44 
       
    45   def apply(fl: Flags.Flag): Option[SystemFlag] = fl match {
       
    46     case Flags.Flag.ANSWERED => Some(ANSWERED)
       
    47     case Flags.Flag.DELETED => Some(DELETED)
       
    48     case Flags.Flag.DRAFT => Some(DRAFT)
       
    49     case Flags.Flag.FLAGGED => Some(FLAGGED)
       
    50     case Flags.Flag.RECENT => Some(RECENT)
       
    51     case Flags.Flag.SEEN => Some(SEEN)
       
    52     case Flags.Flag.USER => Some(USER)
       
    53     case _ => None
       
    54   }
       
    55 }
       
    56 
       
    57 case class Header(name: String, value: String)
       
    58 
       
    59 case class Envelope(
       
    60   from: List[Address],
       
    61   replyTo: List[Address],
       
    62   to: List[Address],
       
    63   cc: List[Address],
       
    64   subject: Option[String],
       
    65   received: Option[Date],
       
    66   sent: Option[Date],
       
    67   systemFlags: List[SystemFlag],
       
    68   userFlags: List[String],
       
    69   headers: List[Header],
       
    70   encoding: Option[String],
       
    71   messageId: Option[String]
       
    72 )
       
    73 
       
    74 object Envelope {
       
    75   private def safe(v: Array[mAddress]): List[mAddress] =
       
    76     if (v==null) Nil else v toList
       
    77 
       
    78   def apply(m: Message): Envelope = {
       
    79     val mm = m match {
       
    80       case mm: MimeMessage => Some(mm)
       
    81       case _ => None
       
    82     }
       
    83     Envelope(safe(m.getFrom) flatMap(Address(_)),
       
    84       safe(m.getReplyTo) flatMap(Address(_)),
       
    85       safe(m.getRecipients(Message.RecipientType.TO)) flatMap(Address(_)),
       
    86       safe(m.getRecipients(Message.RecipientType.CC)) flatMap(Address(_)),
       
    87       Option(MimeUtility.decodeText(m.getSubject)),
       
    88       Option(m.getReceivedDate),
       
    89       Option(m.getSentDate),
       
    90       Option(m.getFlags).toList flatMap(_.getSystemFlags) flatMap(SystemFlag(_)),
       
    91       Option(m.getFlags).toList flatMap(_.getUserFlags),
       
    92       m.getAllHeaders.asScala map(h => Header(h.getName, h.getValue)) toList,
       
    93       mm flatMap(v => Option(v.getEncoding)),
       
    94       mm flatMap(v => Option(v.getMessageID))
       
    95     )
       
    96   }
       
    97 }
       
    98 
       
    99 case class ContentType(
       
   100   primaryType: String,
       
   101   subType: String,
       
   102   charset: Option[String],
       
   103   parameters: List[ContentType.Parameter]
       
   104 )
       
   105 
       
   106 object ContentType {
       
   107   case class Parameter(name: String, value: String)
       
   108 
       
   109   def apply(s: String): ContentType = {
       
   110     val ct = new mContentType(s)
       
   111     val pl = ct.getParameterList
       
   112     ContentType(ct.getPrimaryType, ct.getSubType,
       
   113       Option(pl.get("charset")),
       
   114       pl.getNames.asScala map(n => Parameter(n, pl.get(n))) toList)
       
   115   }
       
   116 }
       
   117 
       
   118 case class Descriptor(
       
   119   contentType: ContentType,
       
   120   size: Option[Int],
       
   121   lineCount: Option[Int],
       
   122   disposition: Option[String],
       
   123   description: Option[String],
       
   124   fileName: Option[String],
       
   125   headers: List[Header],
       
   126   encoding: Option[String]
       
   127 )
       
   128 
       
   129 object Descriptor {
       
   130   def apply(p: Part): Descriptor = Descriptor(
       
   131     ContentType(p.getContentType),
       
   132     Option(p.getSize) filterNot(_ == -1),
       
   133     Option(p.getLineCount) filterNot(_ == -1),
       
   134     Option(p.getDisposition),
       
   135     Option(p.getDescription),
       
   136     Option(p.getFileName),
       
   137     Option(p.getAllHeaders).toList flatMap(_.asScala)
       
   138       map(h => Header(h.getName, h.getValue)),
       
   139     p match {
       
   140       case mp: MimePart => Option(mp.getEncoding)
       
   141       case _ => None
       
   142     }
       
   143   )
       
   144 }
       
   145 
       
   146 sealed abstract class Content { def descriptor: Descriptor }
       
   147 object Content {
       
   148 
       
   149   case class Plain(text: String, descriptor: Descriptor) extends Content
       
   150   case class Html(text: String, descriptor: Descriptor) extends Content
       
   151   case class Text(text: String, descriptor: Descriptor) extends Content
       
   152   case class Multi(parts: List[Content], descriptor: Descriptor) extends Content
       
   153   case class Nested(message: Content, descriptor: Descriptor) extends Content
       
   154   case class Stream(bytes: Array[Byte], descriptor: Descriptor) extends Content
       
   155   case class Attachment(file: String, descriptor: Descriptor) extends Content
       
   156   case class Unknown(clz: String, descriptor: Descriptor) extends Content
       
   157 
       
   158   def apply(m: Part)(implicit attDir: File): Content = {
       
   159     val d = Descriptor(m)
       
   160     m.getContent match {
       
   161       case t: String if m.isMimeType("text/plain") => Plain(t, d)
       
   162       case t: String if m.isMimeType("text/html") => Html(t, d)
       
   163       case t: String => Text(t, d)
       
   164       case mp: Multipart if m.isMimeType("multipart/*") =>
       
   165         Multi(0 until mp.getCount map(i => Content(mp.getBodyPart(i))) toList,
       
   166           d)
       
   167       case p: Part if m.isMimeType("message/rfc822") =>
       
   168         Nested(Content(p), d)
       
   169       case p: MimeBodyPart =>
       
   170         val f = File.createTempFile("mail-", ".att", attDir)
       
   171         p.saveFile(f)
       
   172         Attachment(f.getAbsolutePath, d)
       
   173       case is: InputStream =>
       
   174         val f = File.createTempFile("mail-", ".att", attDir)
       
   175         Files.copy(is, f.toPath, StandardCopyOption.REPLACE_EXISTING)
       
   176         Attachment(f.getAbsolutePath, d)
       
   177       case x => Unknown(x.getClass.getName, d)
       
   178     }
       
   179   }
       
   180 }
       
   181 
       
   182 case class EmailMessage(
       
   183   envelope: Envelope,
       
   184   content: Content
       
   185 )
       
   186 
       
   187 object EmailMessage {
       
   188   def apply(m: Message)(implicit attDir: File): EmailMessage = {
       
   189     EmailMessage(Envelope(m), Content(m))
       
   190   }
       
   191 }
       
   192 
       
   193 @main
       
   194 def main(
       
   195   @doc("Email message file") mail: Path,
       
   196   @doc("Output directory") outDir: Path): Unit = {
       
   197 
       
   198   val session = Session.getInstance(System.getProperties)
       
   199   val msg = new MimeMessage(session, new ByteArrayInputStream(read.bytes(mail)))
       
   200   import upickle.default._
       
   201   import upickle.Js
       
   202   val df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
       
   203   implicit val w: default.Writer[Date] = Writer[Date](d => Js.Str(df.format(d)))
       
   204   outDir.toIO.mkdirs()
       
   205   val m = EmailMessage(msg)(outDir.toIO)
       
   206   ammonite.ops.write.over(outDir / 'mail, write(m))
       
   207   println(s"Written to $outDir")
       
   208 }