Compare commits
14 commits
faf3417dff
...
3b446e3b73
Author | SHA1 | Date | |
---|---|---|---|
|
3b446e3b73 | ||
|
13f8888261 | ||
|
76d4e91c00 | ||
|
25f63a2656 | ||
|
343298144d | ||
|
247591bfca | ||
|
06b3471998 | ||
|
101210188c | ||
|
24427f6f65 | ||
|
7f8ba1337b | ||
|
68d56b7a1a | ||
|
001ba18cf5 | ||
|
8cd69f8ee6 | ||
|
34859e8dd8 |
5 changed files with 167 additions and 127 deletions
22
README.md
22
README.md
|
@ -10,26 +10,12 @@ WARNING: This library is a personal project to learn Gleam. It is *extremely* in
|
|||
```sh
|
||||
gleam add mbox
|
||||
```
|
||||
```gleam
|
||||
import gleambox
|
||||
import simplifile
|
||||
|
||||
pub fn main() {
|
||||
let mboxcontents =
|
||||
"/path/to/file"
|
||||
|> simplifile.read
|
||||
|> result.unwrap(or: "")
|
||||
TODOs:
|
||||
|
||||
mboxcontents
|
||||
|> gleambox.get_headers
|
||||
|> dict.to_list
|
||||
|> list.map(io.debug)
|
||||
|
||||
mboxcontents
|
||||
|> gleambox.get_body
|
||||
|> io.println
|
||||
}
|
||||
```
|
||||
- [ ] Add example
|
||||
- [ ] Better error propagation
|
||||
- [ ] Handle multi-part MIME messages
|
||||
|
||||
Further documentation can be found at <https://hexdocs.pm/gleambox>.
|
||||
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
name = "gleambox"
|
||||
version = "0.0.5"
|
||||
version = "0.0.10"
|
||||
|
||||
description = "WIP mbox parser in Gleam"
|
||||
licences = ["LGPL-3.0-only"]
|
||||
repository = { type = "forgejo", host = "git.bhankas.org", user = "payas", repo = "gleambox" }
|
||||
|
||||
[dependencies]
|
||||
gleam_stdlib = "~> 0.36"
|
||||
simplifile = "~> 1.5"
|
||||
gleam_stdlib = ">= 0.34.0 and < 2.0.0"
|
||||
simplifile = ">= 2.0.0"
|
||||
birl = ">= 1.6.1 and < 2.0.0"
|
||||
|
||||
[dev-dependencies]
|
||||
gleeunit = "~> 1.0"
|
||||
gleeunit = ">= 1.0.0 and < 2.0.0"
|
||||
|
|
|
@ -2,12 +2,16 @@
|
|||
# You typically do not need to edit this file
|
||||
|
||||
packages = [
|
||||
{ name = "gleam_stdlib", version = "0.36.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "C0D14D807FEC6F8A08A7C9EF8DFDE6AE5C10E40E21325B2B29365965D82EB3D4" },
|
||||
{ name = "gleeunit", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D364C87AFEB26BDB4FB8A5ABDE67D635DC9FA52D6AB68416044C35B096C6882D" },
|
||||
{ name = "simplifile", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "C44DB387524F90DC42142699C78C850003289D32C7C99C7D32873792A299CDF7" },
|
||||
{ name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" },
|
||||
{ name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" },
|
||||
{ name = "gleam_stdlib", version = "0.39.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "2D7DE885A6EA7F1D5015D1698920C9BAF7241102836CE0C3837A4F160128A9C4" },
|
||||
{ name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" },
|
||||
{ name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" },
|
||||
{ name = "simplifile", version = "2.0.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "5FFEBD0CAB39BDD343C3E1CCA6438B2848847DC170BA2386DF9D7064F34DF000" },
|
||||
]
|
||||
|
||||
[requirements]
|
||||
gleam_stdlib = { version = "~> 0.36" }
|
||||
gleeunit = { version = "~> 1.0" }
|
||||
simplifile = { version = "~> 1.5" }
|
||||
birl = { version = ">= 1.6.1 and < 2.0.0" }
|
||||
gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" }
|
||||
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
|
||||
simplifile = { version = ">= 2.0.0" }
|
||||
|
|
|
@ -1,79 +1,73 @@
|
|||
import gleam/result
|
||||
import birl.{type Time}
|
||||
import gleam/dict.{type Dict}
|
||||
import gleam/iterator.{type Iterator}
|
||||
import gleam/list
|
||||
import gleam/string
|
||||
import gleam/pair
|
||||
import gleam/regex
|
||||
import gleam/result
|
||||
import gleam/string
|
||||
import simplifile
|
||||
|
||||
// import gleam/io
|
||||
|
||||
pub type MBox {
|
||||
MBox(headers: Dict(String, String), body: String)
|
||||
InvalidMBox
|
||||
}
|
||||
|
||||
pub fn parse(mboxcontents: String) -> MBox {
|
||||
MBox(headers: parse_headers(mboxcontents), body: parse_body(mboxcontents))
|
||||
pub type Mail {
|
||||
Mail(
|
||||
from: Result(String, Nil),
|
||||
to: Result(String, Nil),
|
||||
// TODO: convert to List(String)
|
||||
subject: Result(String, Nil),
|
||||
message_id: Result(String, Nil),
|
||||
date: Result(Time, Nil),
|
||||
body: Result(String, Nil),
|
||||
headers: Result(Dict(String, String), Nil),
|
||||
)
|
||||
InvalidMail
|
||||
}
|
||||
|
||||
pub fn get_headers(mbox: MBox) -> Dict(String, String) {
|
||||
mbox.headers
|
||||
pub fn parse_mbox(mboxcontents: String) -> MBox {
|
||||
let headers = parse_headers(mboxcontents)
|
||||
let body = parse_body(mboxcontents)
|
||||
|
||||
case headers, body {
|
||||
Ok(parsed_headers), Ok(parsed_body) ->
|
||||
MBox(headers: parsed_headers, body: parsed_body)
|
||||
_, _ -> InvalidMBox
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_header(mbox: MBox, key: String) -> Result(String, Nil) {
|
||||
mbox.headers
|
||||
|> dict.get(key)
|
||||
fn get_headers(mbox: MBox) -> Result(Dict(String, String), Nil) {
|
||||
case mbox {
|
||||
InvalidMBox -> Error(Nil)
|
||||
MBox(headers, _) -> Ok(headers)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_body(mbox: MBox) -> String {
|
||||
mbox.body
|
||||
}
|
||||
|
||||
pub fn get_from(mbox: MBox) -> Result(String, Nil) {
|
||||
get_header(mbox, "From")
|
||||
}
|
||||
|
||||
pub fn get_to(mbox: MBox) -> Result(String, Nil) {
|
||||
get_header(mbox, "To")
|
||||
}
|
||||
|
||||
pub fn get_date(mbox: MBox) -> Result(String, Nil) {
|
||||
get_header(mbox, "Date")
|
||||
}
|
||||
|
||||
pub fn get_subject(mbox: MBox) -> Result(String, Nil) {
|
||||
get_header(mbox, "Subject")
|
||||
}
|
||||
|
||||
pub fn get_message_id(mbox: MBox) -> Result(String, Nil) {
|
||||
get_header(mbox, "Message-ID")
|
||||
}
|
||||
|
||||
pub fn get_references(mbox: MBox) -> List(String) {
|
||||
get_header(mbox, "References")
|
||||
|> result.unwrap(or: "Error")
|
||||
|> string.split(" ")
|
||||
}
|
||||
|
||||
fn parse_body(mboxcontents: String) -> String {
|
||||
mboxcontents
|
||||
fn parse_body(mboxcontents: String) -> Result(String, Nil) {
|
||||
// split headers from body
|
||||
|> string.split_once("\n\n")
|
||||
|> result.unwrap(or: #("", ""))
|
||||
// get only body
|
||||
|> pair.second
|
||||
case string.split_once(mboxcontents, "\n\n") {
|
||||
Ok(pair) -> pair.second(pair) |> Ok
|
||||
Error(_) -> Error(Nil)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_headers(mboxcontents: String) -> Dict(String, String) {
|
||||
mboxcontents
|
||||
// split headers from body
|
||||
|> string.split_once("\n\n")
|
||||
|> result.unwrap(or: #("", ""))
|
||||
// get only headers
|
||||
|> pair.first
|
||||
// fix multi-line header values
|
||||
|> fix_multiline_values
|
||||
|> string.split("\n")
|
||||
// convert to dict of headers
|
||||
|> list.map(get_header_dict)
|
||||
|> dict.from_list
|
||||
fn parse_headers(mboxcontents: String) -> Result(Dict(String, String), Nil) {
|
||||
case string.split_once(mboxcontents, "\n\n") {
|
||||
Ok(pair) ->
|
||||
pair.first(pair)
|
||||
// fix multi-line header values
|
||||
|> fix_multiline_values
|
||||
|> string.split("\n")
|
||||
// convert to dict of headers
|
||||
|> list.map(get_header_dict)
|
||||
|> dict.from_list
|
||||
|> Ok
|
||||
Error(_) -> Error(Nil)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_header_dict(s: String) -> #(String, String) {
|
||||
|
@ -86,8 +80,8 @@ fn fix_multiline_values(s: String) -> String {
|
|||
let assert Ok(multi_line_value) =
|
||||
regex.compile(": [^\n]+\n\\s+[^\n]+$", regex.Options(True, True))
|
||||
|
||||
s
|
||||
|> regex.scan(multi_line_value, _)
|
||||
multi_line_value
|
||||
|> regex.scan(s)
|
||||
|> list.map(fn(match) { match.content })
|
||||
|> list.scan(s, remove_dead_space)
|
||||
|> list.first
|
||||
|
@ -96,9 +90,64 @@ fn fix_multiline_values(s: String) -> String {
|
|||
|
||||
fn remove_dead_space(acc: String, matched_content: String) -> String {
|
||||
let assert Ok(dead_space) = regex.from_string("\\s+")
|
||||
|
||||
matched_content
|
||||
|> regex.split(dead_space, _)
|
||||
dead_space
|
||||
|> regex.split(matched_content)
|
||||
|> string.join(" ")
|
||||
|> string.replace(acc, matched_content, _)
|
||||
}
|
||||
|
||||
// TODO: better error
|
||||
pub fn maildir_iterate(maildir_path: String) -> Iterator(#(String, String)) {
|
||||
case simplifile.get_files(maildir_path) {
|
||||
Ok(maillist) ->
|
||||
iterator.from_list(maillist)
|
||||
|> iterator.map(fn(path) { #(path, read_file(path)) })
|
||||
Error(_) -> #("", "") |> list.wrap |> iterator.from_list
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn main() {
|
||||
// maildir_iterate("/home/payas/.mail/Gmail/[Gmail]/All Mail/cur")
|
||||
// |> iterator.each(fn(mboxpair) {
|
||||
// case parse(pair.second(mboxpair)) {
|
||||
// InvalidMBox -> io.debug("ERR MBOX: " <> pair.first(mboxpair))
|
||||
// MBox(headers, body) ->
|
||||
// case mbox_to_mail(MBox(headers, body)) {
|
||||
// InvalidMail -> io.debug("ERR MAIL: " <> pair.first(mboxpair))
|
||||
// _ -> io.debug("SUCCESS: " <> pair.first(mboxpair))
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
fn read_file(file_path: String) -> String {
|
||||
file_path
|
||||
|> simplifile.read
|
||||
|> result.unwrap(or: "")
|
||||
}
|
||||
|
||||
pub fn parse_mail(mboxcontents: String) -> Mail {
|
||||
case parse_mbox(mboxcontents) {
|
||||
InvalidMBox -> InvalidMail
|
||||
MBox(headers, body) -> mbox_to_mail(MBox(headers, body))
|
||||
}
|
||||
}
|
||||
|
||||
fn mbox_to_mail(mbox: MBox) -> Mail {
|
||||
case mbox {
|
||||
InvalidMBox -> InvalidMail
|
||||
MBox(headers, body) ->
|
||||
Mail(
|
||||
from: dict.get(headers, "From"),
|
||||
to: dict.get(headers, "To"),
|
||||
message_id: dict.get(headers, "Message-ID"),
|
||||
subject: dict.get(headers, "Subject"),
|
||||
date: case dict.get(headers, "Date") {
|
||||
Ok(date_str) -> birl.parse(date_str)
|
||||
Error(_) -> Error(Nil)
|
||||
},
|
||||
headers: mbox |> get_headers,
|
||||
body: Ok(body),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,42 +10,42 @@ pub fn main() {
|
|||
gleeunit.main()
|
||||
}
|
||||
|
||||
pub fn get_from_test() {
|
||||
"./test/mboxtest"
|
||||
|> simplifile.read
|
||||
|> result.unwrap(or: "")
|
||||
|> gleambox.parse
|
||||
|> gleambox.get_from
|
||||
|> result.unwrap(or: "ERROR")
|
||||
|> should.equal("Anonymous Courage <from@gmail.com>")
|
||||
}
|
||||
// pub fn get_from_test() {
|
||||
// "./test/mboxtest"
|
||||
// |> simplifile.read
|
||||
// |> result.unwrap(or: "")
|
||||
// |> gleambox.parse
|
||||
// |> gleambox.get_from
|
||||
// |> result.unwrap(or: "ERROR")
|
||||
// |> should.equal("Anonymous Courage <from@gmail.com>")
|
||||
// }
|
||||
|
||||
pub fn get_to_test() {
|
||||
"./test/mboxtest"
|
||||
|> simplifile.read
|
||||
|> result.unwrap(or: "")
|
||||
|> gleambox.parse
|
||||
|> gleambox.get_to
|
||||
|> result.unwrap(or: "ERROR")
|
||||
|> should.equal("Anonymous Coward <to@gmail.com>")
|
||||
}
|
||||
// pub fn get_to_test() {
|
||||
// "./test/mboxtest"
|
||||
// |> simplifile.read
|
||||
// |> result.unwrap(or: "")
|
||||
// |> gleambox.parse
|
||||
// |> gleambox.get_to
|
||||
// |> result.unwrap(or: "ERROR")
|
||||
// |> should.equal("Anonymous Coward <to@gmail.com>")
|
||||
// }
|
||||
|
||||
pub fn get_headers_test() {
|
||||
"./test/mboxtest"
|
||||
|> simplifile.read
|
||||
|> result.unwrap(or: "")
|
||||
|> gleambox.parse
|
||||
|> gleambox.get_headers
|
||||
|> dict.size
|
||||
|> should.equal(13)
|
||||
}
|
||||
// pub fn get_headers_test() {
|
||||
// "./test/mboxtest"
|
||||
// |> simplifile.read
|
||||
// |> result.unwrap(or: "")
|
||||
// |> gleambox.parse
|
||||
// |> gleambox.get_headers
|
||||
// |> dict.size
|
||||
// |> should.equal(13)
|
||||
// }
|
||||
|
||||
pub fn get_references_test() {
|
||||
"./test/mboxtest"
|
||||
|> simplifile.read
|
||||
|> result.unwrap(or: "")
|
||||
|> gleambox.parse
|
||||
|> gleambox.get_references
|
||||
|> list.fold(0, fn(count, _) { count + 1 })
|
||||
|> should.equal(2)
|
||||
}
|
||||
// pub fn get_references_test() {
|
||||
// "./test/mboxtest"
|
||||
// |> simplifile.read
|
||||
// |> result.unwrap(or: "")
|
||||
// |> gleambox.parse
|
||||
// |> gleambox.get_references
|
||||
// |> list.fold(0, fn(count, _) { count + 1 })
|
||||
// |> should.equal(2)
|
||||
// }
|
||||
|
|
Loading…
Reference in a new issue