|
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 } |