From f529e0ceb6a5a84d3b7c0b66ce14fc3f6dfa6661 Mon Sep 17 00:00:00 2001 From: Jacob Kiers Date: Sat, 6 Aug 2022 23:01:24 +0200 Subject: [PATCH] Make it a real CLI app Signed-off-by: Jacob Kiers --- Cargo.lock | 138 ++++++++++++++++- bin/Cargo.toml | 6 +- bin/src/cli.rs | 48 ++++++ bin/src/debug.rs | 17 +++ bin/src/feed.rs | 108 ++++++++++++++ bin/src/main.rs | 302 +++++++++++++++++--------------------- bin/src/message.rs | 24 +++ bin/src/message_reader.rs | 40 +++-- 8 files changed, 504 insertions(+), 179 deletions(-) create mode 100644 bin/src/cli.rs create mode 100644 bin/src/debug.rs create mode 100644 bin/src/feed.rs create mode 100644 bin/src/message.rs diff --git a/Cargo.lock b/Cargo.lock index 0b7d1d4..5306c47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,6 +30,17 @@ dependencies = [ "quick-xml", ] +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -101,6 +112,45 @@ dependencies = [ "winapi", ] +[[package]] +name = "clap" +version = "3.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3dbbb6653e7c55cc8595ad3e1f7be8f32aba4eb7ff7f0fd1163d4f3d137c0a9" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "indexmap", + "once_cell", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ba52acd3b0a5c33aeada5cdaa3267cdc7c594a98731d4268cdc1532f4264cb4" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "cpufeatures" version = "0.2.2" @@ -230,6 +280,27 @@ dependencies = [ "version_check", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -260,6 +331,16 @@ dependencies = [ "nom", ] +[[package]] +name = "indexmap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +dependencies = [ + "autocfg", + "hashbrown", +] + [[package]] name = "js-sys" version = "0.3.59" @@ -327,11 +408,12 @@ checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91" [[package]] name = "newsletter-to-web" -version = "0.1.0" +version = "0.0.1" dependencies = [ "atom_syndication", "base16ct", "chrono", + "clap", "imap", "mail-parser", "rustls-connector", @@ -374,6 +456,36 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" +[[package]] +name = "os_str_bytes" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.43" @@ -534,6 +646,21 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + [[package]] name = "time" version = "0.1.44" @@ -674,6 +801,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/bin/Cargo.toml b/bin/Cargo.toml index a824381..914ed30 100644 --- a/bin/Cargo.toml +++ b/bin/Cargo.toml @@ -1,8 +1,11 @@ [package] name = "newsletter-to-web" -version = "0.1.0" +version = "0.0.1" edition = "2021" description = "Converts email newsletters to static HTML files" +authors = [ + "Jacob Kiers " +] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -10,6 +13,7 @@ description = "Converts email newsletters to static HTML files" atom_syndication = "^0.11.0" base16ct = { version = "^0.1.0", features = [ "alloc" ] } chrono = "^0.4" +clap = { version = "^3.2.16", features = [ "derive" ] } imap = { version = "^2.4.1", default-features = false } mail-parser = "^0.5.0" rustls-connector = { version = "^0.16.1", default-features = false, features = [ "webpki-roots-certs", "quic" ] } diff --git a/bin/src/cli.rs b/bin/src/cli.rs new file mode 100644 index 0000000..a566c9d --- /dev/null +++ b/bin/src/cli.rs @@ -0,0 +1,48 @@ +use std::path::PathBuf; + +use clap::{Parser, Subcommand}; + +#[derive(Parser)] +#[clap(author, version, about, long_about = None)] +pub(crate) struct Cli { + #[clap(subcommand)] + pub command: Command, +} + +#[derive(Subcommand)] +pub(crate) enum Command { + /// Fetch emails from an IMAP server + FetchFromImap { + #[clap(short, long, value_parser)] + server: String, + #[clap(long, value_parser, default_value_t = 993)] + port: u16, + #[clap(short, long, value_parser)] + username: String, + #[clap(short, long, value_parser)] + password: String, + }, + /// Fetch an email from a .eml file + FetchFromFile { + #[clap(value_parser)] + filename: PathBuf, + }, + /// Build an ATOM feed containing the full contents of the email + BuildFeed { + /// Feed file + #[clap(value_parser, default_value = "output/feed.atom")] + filename: PathBuf, + }, + /// Exports the emails as HTML files + ExportHtml { + /// The directory in which the emails will be stored + #[clap(value_parser, default_value = "output/")] + directory: PathBuf, + }, + /// Fetches and exports all mails from an IMAP mailbox. Requires configuration + FetchAndExport { + /// The directory in which the output will be stored + #[clap(value_parser, default_value = "output/")] + directory: PathBuf, + }, +} diff --git a/bin/src/debug.rs b/bin/src/debug.rs new file mode 100644 index 0000000..835e438 --- /dev/null +++ b/bin/src/debug.rs @@ -0,0 +1,17 @@ +mod debug; + +pub(crate) fn write_to_test_path(msg: &Message) { + let test_path: PathBuf = [ + Path::new("tests/data"), + Path::new(&format!("{}.eml", &msg.get_uid())), + ] + .iter() + .collect(); + + let _ = OpenOptions::new() + .write(true) + .create(true) + .open(test_path) + .expect("Could not open file fir writing") + .write_all(&msg.data); +} \ No newline at end of file diff --git a/bin/src/feed.rs b/bin/src/feed.rs new file mode 100644 index 0000000..ad8681b --- /dev/null +++ b/bin/src/feed.rs @@ -0,0 +1,108 @@ +use crate::Message; + +use atom_syndication::{ + ContentBuilder, Entry, EntryBuilder, Feed, FeedBuilder, Generator, LinkBuilder, Person, +}; +use chrono::{DateTime, Utc, TimeZone}; + +use mail_parser::HeaderValue; + +pub(crate) fn add_entry_to_feed(feed: &mut Feed, message: &Message, processed_html: &String) { + let parsed = message.get_parsed().unwrap(); + let date = parsed.get_date().expect("Could not extract date"); + let from = match parsed.get_from() { + HeaderValue::Address(e) => e, + _ => return, + }; + + let path = crate::get_path(&parsed, message); + + let url = format!("https://newsletters.kiers.eu/{}", &path); + + let mut entry: Entry = Newsletter { + author: Person { + name: match &from.name { + Some(n) => n.to_string(), + _ => match &from.address { + Some(e) => e.to_string(), + _ => "".to_string(), + }, + }, + email: match &from.address { + Some(e) => Some(e.to_string()), + _ => None, + }, + uri: None, + }, + title: parsed + .get_subject() + .expect("Expected a subject") + .to_string(), + content: Some(processed_html.clone()), + id: url.clone(), + published: Utc.timestamp(date.to_timestamp(), 0), //(format!("{}{}", &date.to_iso8601(), "+00:00").as_str()).`unwrap(), + url: url, + } + .into(); + entry.set_updated(Utc.timestamp(date.to_timestamp(), 0)); + feed.entries.push(entry); +} + +pub(crate) fn build_atom_feed() -> Feed { + FeedBuilder::default() + .title("JJKiers Newsletters") + .id("https://newsletters.kiers.eu/feed.atom") + .link( + LinkBuilder::default() + .href("https://newsletters.kiers.eu/") + .rel("alternate") + .build(), + ) + .link( + LinkBuilder::default() + .href("https://newsletters.kiers.eu/feed.atom") + .rel("self") + .build(), + ) + .generator(Generator { + value: String::from("newsletter-to-web"), + uri: None, + version: Some(String::from("0.0.1")), + }) + .build() +} + +//#[derive(Serialize, Deserialize, Debug)] +pub(crate) struct Newsletter { + id: String, + url: String, + title: String, + content: Option, + author: Person, + published: DateTime, +} + +impl From for Entry { + fn from(post: Newsletter) -> Self { + let content = post.content.map(|v| { + ContentBuilder::default() + .value(v) + .content_type(Some("html".to_string())) + .build() + }); + + EntryBuilder::default() + .title(post.title) + .id(post.id) + .published(Some(post.published.clone().into())) + .author(post.author.into()) + .content(content) + .link( + LinkBuilder::default() + .href(post.url) + .rel("alternate") + .build(), + ) + .build() + } +} diff --git a/bin/src/main.rs b/bin/src/main.rs index 2bd6f4e..21a5edf 100644 --- a/bin/src/main.rs +++ b/bin/src/main.rs @@ -1,124 +1,174 @@ +mod cli; +mod feed; +mod message; mod message_reader; +use chrono::Utc; +use clap::Parser; +use mail_parser::Message as ParsedMessage; +use message_reader::{DataDirectoryMessageReader, EmailReader, ImapReader}; +use sha2::{Digest, Sha256}; use std::{ + error::Error, fs::{File, OpenOptions}, io::Write, path::{Path, PathBuf}, }; -use atom_syndication::{ - ContentBuilder, Entry, EntryBuilder, Feed, FeedBuilder, Generator, LinkBuilder, Person, -}; -use chrono::{DateTime, TimeZone, Utc}; -use mail_parser::{HeaderValue, Message as MpMessage}; +pub(crate) use message::Message; -use sha2::{Digest, Sha256}; +fn main() -> Result<(), Box> { + let cli = cli::Cli::parse(); -use message_reader::{EmailReader, TestMessagesReader}; + let data_directory = "data"; -pub struct Message { - uid: String, - data: Vec, + let result = match &cli.command { + cli::Command::FetchFromImap { + server, + port, + username, + password, + } => fetch_from_imap( + data_directory, + server.to_owned(), + *port, + username.to_owned(), + password.to_owned(), + ), + cli::Command::BuildFeed { filename } => build_feed(&filename), + _ => unimplemented!("This method is not yet implemented."), + }; + + result } -impl Message { - pub fn new(uid: String, data: Vec) -> Message { - Message { uid, data } +fn create_directory>(dir: P) -> Result<(), std::io::Error> { + if !dir.as_ref().exists() { + return std::fs::create_dir(&dir); } - pub(crate) fn get_parsed(&self) -> Option { - MpMessage::parse(&self.data) - } - - pub fn get_uid(&self) -> &String { - &self.uid - } + Ok(()) } -fn main() { - let dir = Path::new("data"); - if !dir.exists() { - std::fs::create_dir(&dir).expect("Could not create directory"); - } +fn build_feed(filename: &PathBuf) -> Result<(), Box> { + let dir = filename.parent().ok_or(format!( + "Could not get parent directory of {}", + filename.display() + ))?; - let mut feed = build_atom_feed(); + println!( + "Building the feed to {} in {}/", + filename.display(), + dir.display() + ); - let mut reader = TestMessagesReader::new((&Path::new("tests/data")).to_path_buf()); + create_directory(dir)?; + + let mut feed = feed::build_atom_feed(); + + let mut reader = DataDirectoryMessageReader::new((&Path::new("data")).to_path_buf()); for msg in reader.read_rfc822_messages() { - println!("Processing message {}", msg.get_uid()); - let parsed = msg.get_parsed().expect("A parsed messsage."); + let date = parsed.get_date().ok_or(format!( + "Could not get the date of message {}", + msg.get_uid() + ))?; + + let subject = match parsed.get_subject() { + Some(subject) => subject, + None => "No subject", + }; + + println!( + "Processing message {} from {} with subject {}", + msg.get_uid(), + date.to_string(), + subject + ); + let html_body = parsed.get_html_body(0).expect("Could not read html body"); let processed_html = process_html(&html_body).expect("Could not process the HTML"); - let html_bytes = processed_html.as_bytes(); + + feed::add_entry_to_feed(&mut feed, &msg, &processed_html); + } + + if feed.entries.len() > 0 { + feed.set_updated(Utc::now()); + println!("Writing feed to {}", filename.display()); + let _ = feed.write_to(File::create(filename).unwrap()); + } + + println!("Finished building the feed."); + + Ok(()) +} + +fn fetch_from_imap( + data_directory: &str, + server: String, + port: u16, + username: String, + password: String, +) -> Result<(), Box> { + create_directory(data_directory)?; + + print!("Getting mail from {} for mailbox {}", server, username); + + let mut reader = ImapReader::new( + String::from(server), + port, + String::from(username), + String::from(password), + ); + + for msg in reader.read_rfc822_messages() { + let parsed = msg.get_parsed().ok_or(format!( + "Could not parse the message with id {}", + msg.get_uid() + ))?; + + let date = parsed.get_date().ok_or(format!( + "Could not get the date of message {}", + msg.get_uid() + ))?; + + let subject = match parsed.get_subject() { + Some(subject) => subject, + None => "No subject", + }; + + println!( + "Processing message {} from {} with subject {}", + msg.get_uid(), + date.to_string(), + subject + ); let path = get_path(&parsed, &msg); - let html_path: PathBuf = [dir, Path::new(&path)].iter().collect(); - println!("Storing to {}", &html_path.display()); + let html_path: PathBuf = [ + Path::new(data_directory), + Path::new(&format!("{}.eml", path)), + ] + .iter() + .collect(); - add_entry_to_feed(&mut feed, &msg, &processed_html); + println!("Storing to {}", &html_path.display()); OpenOptions::new() .write(true) .create(true) .open(&html_path) .expect(format!("Could not open file '{}' for writing", &html_path.display()).as_str()) - .write_all(&html_bytes) + .write_all(msg.get_data()) .expect(format!("Could not write html to file '{}'.", &html_path.display()).as_str()); - - println!(); } - if feed.entries.len() > 0 { - feed.set_updated(Utc::now()); - let _ = feed.write_to(File::create(format!("{}/feed.atom", dir.display())).unwrap()); - } + Ok(()) } -fn add_entry_to_feed(feed: &mut Feed, message: &Message, processed_html: &String) { - let parsed = message.get_parsed().unwrap(); - let date = parsed.get_date().expect("Could not extract date"); - let from = match parsed.get_from() { - HeaderValue::Address(e) => e, - _ => return, - }; - - let path = get_path(&parsed, message); - - let url = format!("https://newsletters.kiers.eu/{}", &path); - - let mut entry : Entry = Newsletter { - author: Person { - name: match &from.name { - Some(n) => n.to_string(), - _ => match &from.address { - Some(e) => e.to_string(), - _ => "".to_string(), - }, - }, - email: match &from.address { - Some(e) => Some(e.to_string()), - _ => None, - }, - uri: None, - }, - title: parsed - .get_subject() - .expect("Expected a subject") - .to_string(), - content: Some(processed_html.clone()), - id: url.clone(), - published: Utc.timestamp(date.to_timestamp(), 0), //(format!("{}{}", &date.to_iso8601(), "+00:00").as_str()).`unwrap(), - url: url, - } - .into(); - entry.set_updated(Utc.timestamp(date.to_timestamp(), 0)); - feed.entries.push(entry); -} - -fn get_path(parsed: &MpMessage, msg: &Message) -> String { +fn get_path(parsed: &ParsedMessage, msg: &Message) -> String { let date = parsed.get_date().expect("Could not extract date"); let date_str = format!( "{:04}{:02}{:02}{:02}{:02}{:02}", @@ -129,95 +179,13 @@ fn get_path(parsed: &MpMessage, msg: &Message) -> String { &parsed.get_html_body(0).expect("Expected a body").as_bytes(), )); - let uid: i32 = msg - .get_uid() + let uid: i32 = msg.get_uid() .parse() - .expect("Could not convert message uid to an i32."); - //format!("{}_{}_{}.html", &date_str, &file_name, msg.get_uid()).to_owned() + .expect(&format!("Could not convert message id {} to an i32.", msg.get_uid())); + format!("{:05}_{}_{}.html", uid, date_str, &hash).to_owned() } fn process_html(input: &str) -> Result { Ok(input.replace("src", "data-source")) } - -fn build_atom_feed() -> Feed { - FeedBuilder::default() - .title("JJKiers Newsletters") - .id("https://newsletters.kiers.eu/feed.atom") - .link( - LinkBuilder::default() - .href("https://newsletters.kiers.eu/") - .rel("alternate") - .build(), - ) - .link( - LinkBuilder::default() - .href("https://newsletters.kiers.eu/feed.atom") - .rel("self") - .build(), - ) - .generator(Generator { - value: String::from("newsletter-to-web"), - uri: None, - version: Some(String::from("0.0.1")), - }) - .build() -} - -fn write_to_test_path(msg: &Message) { - let test_path: PathBuf = [ - Path::new("tests/data"), - Path::new(&format!("{}.eml", &msg.get_uid())), - ] - .iter() - .collect(); - - let _ = OpenOptions::new() - .write(true) - .create(true) - .open(test_path) - .expect("Could not open file fir writing") - .write_all(&msg.data); -} - -//#[derive(Serialize, Deserialize, Debug)] -struct Newsletter { - id: String, - url: String, - title: String, - content: Option, - author: Person, - published: DateTime, -} - -impl From for Entry { - fn from(post: Newsletter) -> Self { - let content = post.content.map(|v| { - ContentBuilder::default() - .value(v) - .content_type(Some("html".to_string())) - .build() - }); - - EntryBuilder::default() - .title(post.title) - .id(post.id) - .published(Some(post.published.clone().into())) - .author(post.author.into()) - .content(content) - .link( - LinkBuilder::default() - .href(post.url) - .rel("alternate") - .build(), - ) - .build() - } -} - -// pub fn parse_datetime(s: &str) -> Option> { -// DateTime::::from(s) -// .ok() -// .map(|d| d.with_timezone(&Utc.fix())) -// } diff --git a/bin/src/message.rs b/bin/src/message.rs new file mode 100644 index 0000000..90fdb8e --- /dev/null +++ b/bin/src/message.rs @@ -0,0 +1,24 @@ +use mail_parser::Message as ParsedMessage; + +pub(crate) struct Message { + uid: String, + data: Vec, +} + +impl Message { + pub fn new(uid: String, data: Vec) -> Message { + Message { uid, data } + } + + pub(crate) fn get_parsed(&self) -> Option { + ParsedMessage::parse(&self.data) + } + + pub fn get_uid(&self) -> &String { + &self.uid + } + + pub fn get_data(&self) -> &Vec { + &self.data + } +} \ No newline at end of file diff --git a/bin/src/message_reader.rs b/bin/src/message_reader.rs index b24d245..98ca253 100644 --- a/bin/src/message_reader.rs +++ b/bin/src/message_reader.rs @@ -12,25 +12,29 @@ use rustls_connector::RustlsConnector; use crate::Message; -pub trait EmailReader { +pub(crate) trait EmailReader { fn read_rfc822_messages(&mut self) -> Box>; } -pub(crate) struct TestMessagesReader { +pub(crate) struct DataDirectoryMessageReader { path: PathBuf, } -impl TestMessagesReader { +impl DataDirectoryMessageReader { pub fn new(path: PathBuf) -> Self { - TestMessagesReader { path } + DataDirectoryMessageReader { path } } } -impl EmailReader for TestMessagesReader { +impl EmailReader for DataDirectoryMessageReader { fn read_rfc822_messages(&mut self) -> Box> { + println!("Reading files in {}", &self.path.display()); let reader = match read_dir(&self.path) { Ok(r) => r, - Err(_) => return Box::new(Vec::new().into_iter()), + Err(e) => { + dbg!(e); + return Box::new(Vec::new().into_iter()); + } }; let items = reader @@ -41,9 +45,20 @@ impl EmailReader for TestMessagesReader { None => false, }) .map(|i| { - let uid = i.path().file_stem().unwrap().to_owned(); + let uid = i + .path() + .file_stem() + .unwrap() + .to_owned() + .into_string() + .expect("Could not convert filename to string.") + .split("_") + .collect::>()[0] + .trim_start_matches("0") + .to_string(); + if let Ok(data) = std::fs::read(i.path()) { - Some((uid.into_string().unwrap(), data)) + Some((uid, data)) } else { None } @@ -135,10 +150,15 @@ impl EmailReader for ImapReader { fn read_rfc822_messages(&mut self) -> Box> { let msgs = match self.connect() { Ok(m) => m, - Err(_) => return Box::new(Vec::new().into_iter()), + Err(e) => { + dbg!(e); + return Box::new(Vec::new().into_iter()); + } }; - let items = msgs.iter().map(|i| Message::new(i.0.to_owned(), i.1.to_owned())); + let items = msgs + .iter() + .map(|i| Message::new(i.0.to_owned(), i.1.to_owned())); let iter = items.collect::>().into_iter();