libimagmailfrontend/
lib.rs

1//
2// imag - the personal information management suite for the commandline
3// Copyright (C) 2015-2020 Matthias Beyer <mail@beyermatthias.de> and contributors
4//
5// This library is free software; you can redistribute it and/or
6// modify it under the terms of the GNU Lesser General Public
7// License as published by the Free Software Foundation; version
8// 2.1 of the License.
9//
10// This library is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13// Lesser General Public License for more details.
14//
15// You should have received a copy of the GNU Lesser General Public
16// License along with this library; if not, write to the Free Software
17// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
18//
19
20#![forbid(unsafe_code)]
21
22#![deny(
23    non_camel_case_types,
24    non_snake_case,
25    path_statements,
26    trivial_numeric_casts,
27    unstable_features,
28    unused_allocation,
29    unused_import_braces,
30    unused_imports,
31    unused_must_use,
32    unused_mut,
33    unused_qualifications,
34    while_true,
35)]
36
37extern crate clap;
38#[macro_use] extern crate log;
39#[macro_use] extern crate failure;
40extern crate toml_query;
41#[macro_use] extern crate indoc;
42extern crate resiter;
43
44extern crate libimagrt;
45extern crate libimagmail;
46extern crate libimagerror;
47extern crate libimagstore;
48extern crate libimagutil;
49extern crate libimagentryref;
50
51use std::io::Write;
52use std::path::PathBuf;
53
54use failure::Fallible as Result;
55use failure::err_msg;
56use failure::Error;
57use toml_query::read::TomlValueReadTypeExt;
58use clap::App;
59use resiter::AndThen;
60use resiter::IterInnerOkOrElse;
61
62use libimagmail::mail::Mail;
63use libimagmail::store::MailStore;
64use libimagmail::util;
65use libimagentryref::reference::{Ref, RefFassade};
66use libimagentryref::util::get_ref_config;
67use libimagrt::runtime::Runtime;
68use libimagrt::application::ImagApplication;
69use libimagutil::info_result::*;
70use libimagstore::store::FileLockEntry;
71use libimagstore::storeid::StoreIdIterator;
72use libimagstore::iter::get::StoreIdGetIteratorExtension;
73
74mod ui;
75
76/// Marker enum for implementing ImagApplication on
77///
78/// This is used by binaries crates to execute business logic
79/// or to build a CLI completion.
80pub enum ImagMail {}
81impl ImagApplication for ImagMail {
82    fn run(rt: Runtime) -> Result<()> {
83        match rt.cli().subcommand_name().ok_or_else(|| err_msg("No subcommand called"))? {
84            "import-mail" => import_mail(&rt),
85            "list"        => list(&rt),
86            "mail-store"  => mail_store(&rt),
87            other               => {
88                debug!("Unknown command");
89                if rt.handle_unknown_subcommand("imag-mail", other, rt.cli())?.success() {
90                    Ok(())
91                } else {
92                    Err(err_msg("Failed to handle unknown subcommand"))
93                }
94            },
95        }
96    }
97
98    fn build_cli<'a>(app: App<'a, 'a>) -> App<'a, 'a> {
99        ui::build_ui(app)
100    }
101
102    fn name() -> &'static str {
103        env!("CARGO_PKG_NAME")
104    }
105
106    fn description() -> &'static str {
107        "Mail collection tool"
108    }
109
110    fn version() -> &'static str {
111        env!("CARGO_PKG_VERSION")
112    }
113}
114
115
116fn import_mail(rt: &Runtime) -> Result<()> {
117    let collection_name = get_ref_collection_name(rt)?;
118    let refconfig       = get_ref_config(rt, "imag-mail")?;
119    let scmd            = rt.cli().subcommand_matches("import-mail").unwrap();
120    let store           = rt.store();
121
122    debug!(r#"Importing mail with
123    collection_name = {}
124    refconfig = {:?}
125    "#, collection_name, refconfig);
126
127    scmd.values_of("path")
128        .unwrap() // enforced by clap
129        .map(PathBuf::from)
130        .map(|path| {
131            if scmd.is_present("ignore-existing-ids") {
132                store.retrieve_mail_from_path(path, &collection_name, &refconfig)
133            } else {
134                store.create_mail_from_path(path, &collection_name, &refconfig)
135            }
136            .map_info_str("Ok")
137        })
138        .and_then_ok(|e| rt.report_touched(e.get_location()).map_err(Error::from))
139        .collect::<Result<Vec<()>>>()
140        .map(|_| ())
141}
142
143fn list(rt: &Runtime) -> Result<()> {
144    let refconfig       = get_ref_config(rt, "imag-mail")?;
145    let scmd            = rt.cli().subcommand_matches("list").unwrap(); // safe via clap
146    let print_content   = scmd.is_present("list-read");
147
148    if print_content {
149        // TODO: Check whether workaround with "{}" is still necessary when updating "indoc"
150        warn!("{}", indoc!(r#"You requested to print the content of the mail as well.
151        We use the 'mailparse' crate underneath, but its implementation is nonoptimal.
152        Thus, the content might be printed as empty (no text in the email)
153        This is not reliable and might be wrong."#));
154
155        // TODO: Fix above.
156    }
157
158    // TODO: Implement lister type in libimagmail for this
159    //
160    // Optimization: Pass refconfig here instead of call get_ref_config() in lister function. This
161    // way we do not call get_ref_config() multiple times.
162    fn list_mail<'a>(rt: &Runtime,
163                     refconfig: &::libimagentryref::reference::Config,
164                     m: &FileLockEntry<'a>,
165                     print_content: bool) -> Result<()> {
166
167        let id = match m.get_message_id(&refconfig)? {
168            Some(f) => f,
169            None => "<no id>".to_owned(),
170        };
171
172        let from = match m.get_from(&refconfig)? {
173            Some(f) => f,
174            None => "<no from>".to_owned(),
175        };
176
177        let to = match m.get_to(&refconfig)? {
178            Some(f) => f,
179            None => "<no to>".to_owned(),
180        };
181
182        let subject = match m.get_subject(&refconfig)? {
183            Some(f) => f,
184            None => "<no subject>".to_owned(),
185        };
186
187        if print_content {
188            use libimagmail::hasher::MailHasher;
189
190            let content = m.as_ref_with_hasher::<MailHasher>()
191                .get_path(&refconfig)
192                .and_then(util::get_mail_text_content)?;
193
194            writeln!(rt.stdout(),
195                     "Mail: {id}\nFrom: {from}\nTo: {to}\n{subj}\n---\n{content}\n---\n",
196                     from    = from,
197                     id      = id,
198                     subj    = subject,
199                     to      = to,
200                     content = content
201            )?;
202        } else {
203            writeln!(rt.stdout(),
204                     "Mail: {id}\nFrom: {from}\nTo: {to}\n{subj}\n",
205                     from = from,
206                     id   = id,
207                     subj = subject,
208                     to   = to
209            )?;
210        }
211
212        rt.report_touched(m.get_location())?;
213        Ok(())
214    }
215
216    if rt.ids_from_stdin() {
217        let iter = rt
218            .ids::<crate::ui::PathProvider>()?
219            .ok_or_else(|| err_msg("No ids supplied"))?
220            .into_iter()
221            .map(Ok);
222
223        StoreIdIterator::new(Box::new(iter))
224    } else {
225        rt.store()
226            .all_mails()?
227            .into_storeid_iter()
228    }
229    .inspect(|id| debug!("Found: {:?}", id))
230    .into_get_iter(rt.store())
231    .map_inner_ok_or_else(|| err_msg("Did not find one entry"))
232    .and_then_ok(|m| list_mail(&rt, &refconfig, &m, print_content))
233    .collect::<Result<Vec<_>>>()
234    .map(|_| ())
235}
236
237fn mail_store(rt: &Runtime) -> Result<()> {
238    let _ = rt.cli().subcommand_matches("mail-store").unwrap();
239    Err(format_err!("This feature is currently not implemented."))
240}
241
242fn get_ref_collection_name(rt: &Runtime) -> Result<String> {
243    let setting_name = "mail.ref_collection_name";
244
245    debug!("Getting configuration: {}", setting_name);
246
247    rt.config()
248        .ok_or_else(|| format_err!("No configuration, cannot find collection name for mail collection"))?
249        .read_string(setting_name)?
250        .ok_or_else(|| format_err!("Setting missing: {}", setting_name))
251}
252