diff --git a/meli.1 b/meli.1 index ab50f69d..d9c22865 100644 --- a/meli.1 +++ b/meli.1 @@ -180,6 +180,8 @@ in composer, add as an attachment .It Ic remove-attachment Ar INDEX remove attachment with given index +.It Ic toggle sign +toggle between signing and not signing this message. If the gpg invocation fails then the mail won't be sent. .El .Pp generic commands: diff --git a/melib/src/email/attachment_types.rs b/melib/src/email/attachment_types.rs index f5287df6..8c3b8f49 100644 --- a/melib/src/email/attachment_types.rs +++ b/melib/src/email/attachment_types.rs @@ -74,6 +74,27 @@ impl<'a> From<&'a [u8]> for Charset { } } +impl Display for Charset { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + match self { + Charset::Ascii => write!(f, "us-ascii"), + Charset::UTF8 => write!(f, "utf-8"), + Charset::UTF16 => write!(f, "utf-16"), + Charset::ISO8859_1 => write!(f, "iso-8859-1"), + Charset::ISO8859_2 => write!(f, "iso-8859-2"), + Charset::ISO8859_7 => write!(f, "iso-8859-7"), + Charset::ISO8859_15 => write!(f, "iso-8859-15"), + Charset::Windows1251 => write!(f, "windows-1251"), + Charset::Windows1252 => write!(f, "windows-1252"), + Charset::Windows1253 => write!(f, "windows-1253"), + Charset::GBK => write!(f, "GBK"), + Charset::GB2312 => write!(f, "gb2312"), + Charset::BIG5 => write!(f, "BIG5"), + Charset::ISO2022JP => write!(f, "ISO-2022-JP"), + } + } +} + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum MultipartType { Mixed, @@ -264,7 +285,7 @@ pub enum ContentTransferEncoding { impl Default for ContentTransferEncoding { fn default() -> Self { - ContentTransferEncoding::_7Bit + ContentTransferEncoding::_8Bit } } diff --git a/melib/src/email/attachments.rs b/melib/src/email/attachments.rs index bbe9cc38..953f6f57 100644 --- a/melib/src/email/attachments.rs +++ b/melib/src/email/attachments.rs @@ -273,6 +273,23 @@ impl From for AttachmentBuilder { } } +impl From for Attachment { + fn from(val: AttachmentBuilder) -> Self { + let AttachmentBuilder { + content_type, + content_transfer_encoding, + raw, + body, + } = val; + Attachment { + content_type, + content_transfer_encoding, + raw, + body, + } + } +} + /// Immutable attachment type. #[derive(Clone, Serialize, Deserialize, PartialEq)] pub struct Attachment { @@ -546,6 +563,73 @@ impl Attachment { _ => false, } } + + pub fn into_raw(&self) -> String { + let mut ret = String::with_capacity(2 * self.raw.len()); + fn into_raw_helper(a: &Attachment, ret: &mut String) { + ret.extend( + format!( + "Content-Transfer-Encoding: {}\n", + a.content_transfer_encoding + ) + .chars(), + ); + match &a.content_type { + ContentType::Text { kind: _, charset } => { + ret.extend( + format!("Content-Type: {}; charset={}\n\n", a.content_type, charset) + .chars(), + ); + ret.extend(String::from_utf8_lossy(a.body()).chars()); + } + ContentType::Multipart { + boundary, + kind, + parts, + } => { + let boundary = String::from_utf8_lossy(boundary); + ret.extend(format!("Content-Type: {}; boundary={}", kind, boundary).chars()); + if *kind == MultipartType::Signed { + ret.extend( + "; micalg=pgp-sha512; protocol=\"application/pgp-signature\"".chars(), + ); + } + ret.push('\n'); + + let boundary_start = format!("\n--{}\n", boundary); + for p in parts { + ret.extend(boundary_start.chars()); + into_raw_helper(p, ret); + } + ret.extend(format!("--{}--\n\n", boundary).chars()); + } + ContentType::MessageRfc822 => { + ret.extend(format!("Content-Type: {}\n\n", a.content_type).chars()); + ret.extend(String::from_utf8_lossy(a.body()).chars()); + } + ContentType::PGPSignature => { + ret.extend(format!("Content-Type: {}\n\n", a.content_type).chars()); + ret.extend(String::from_utf8_lossy(a.body()).chars()); + } + ContentType::OctetStream { ref name } => { + if let Some(name) = name { + ret.extend( + format!("Content-Type: {}; name={}\n\n", a.content_type, name).chars(), + ); + } else { + ret.extend(format!("Content-Type: {}\n\n", a.content_type).chars()); + } + ret.push_str(&BASE64_MIME.encode(a.body()).trim()); + } + _ => { + ret.extend(format!("Content-Type: {}\n\n", a.content_type).chars()); + ret.extend(String::from_utf8_lossy(a.body()).chars()); + } + } + } + into_raw_helper(self, &mut ret); + ret + } } pub fn interpret_format_flowed(_t: &str) -> String { diff --git a/melib/src/email/compose.rs b/melib/src/email/compose.rs index 17b6c16a..c78c6ce4 100644 --- a/melib/src/email/compose.rs +++ b/melib/src/email/compose.rs @@ -18,11 +18,11 @@ use fnv::FnvHashMap; #[derive(Debug, PartialEq, Clone)] pub struct Draft { - headers: FnvHashMap, - header_order: Vec, - body: String, + pub headers: FnvHashMap, + pub header_order: Vec, + pub body: String, - attachments: Vec, + pub attachments: Vec, } impl Default for Draft { @@ -259,7 +259,19 @@ impl Draft { } ret.push_str("MIME-Version: 1.0\n"); - if !self.attachments.is_empty() { + if self.attachments.is_empty() { + let content_type: ContentType = Default::default(); + let content_transfer_encoding: ContentTransferEncoding = ContentTransferEncoding::_8Bit; + ret.extend(format!("Content-Type: {}; charset=\"utf-8\"\n", content_type).chars()); + ret.extend( + format!("Content-Transfer-Encoding: {}\n", content_transfer_encoding).chars(), + ); + ret.push('\n'); + ret.push_str(&self.body); + } else if self.attachments.len() == 1 && self.body.is_empty() { + let attachment: Attachment = self.attachments.remove(0).into(); + ret.extend(attachment.into_raw().chars()); + } else { let mut parts = Vec::with_capacity(self.attachments.len() + 1); let attachments = std::mem::replace(&mut self.attachments, Vec::new()); let mut body_attachment = AttachmentBuilder::default(); @@ -267,24 +279,6 @@ impl Draft { parts.push(body_attachment); parts.extend(attachments.into_iter()); build_multipart(&mut ret, MultipartType::Mixed, parts); - } else { - if self.body.is_ascii() { - ret.push('\n'); - ret.push_str(&self.body); - } else { - let content_type: ContentType = Default::default(); - let content_transfer_encoding: ContentTransferEncoding = - ContentTransferEncoding::Base64; - - ret.extend(format!("Content-Type: {}; charset=\"utf-8\"\n", content_type).chars()); - ret.extend( - format!("Content-Transfer-Encoding: {}\n", content_transfer_encoding).chars(), - ); - ret.push('\n'); - - ret.push_str(&BASE64_MIME.encode(&self.body.as_bytes()).trim()); - ret.push('\n'); - } } Ok(ret) diff --git a/ui/src/components/mail.rs b/ui/src/components/mail.rs index a4b215e2..657ae664 100644 --- a/ui/src/components/mail.rs +++ b/ui/src/components/mail.rs @@ -33,6 +33,8 @@ pub use crate::view::*; mod compose; pub use self::compose::*; +pub mod pgp; + mod accounts; pub use self::accounts::*; diff --git a/ui/src/components/mail/compose.rs b/ui/src/components/mail/compose.rs index b2b13972..ca3cbd9c 100644 --- a/ui/src/components/mail/compose.rs +++ b/ui/src/components/mail/compose.rs @@ -44,6 +44,7 @@ pub struct Composer { form: FormWidget, mode: ViewMode, + sign_mail: ToggleFlag, dirty: bool, initialized: bool, id: ComponentId, @@ -62,6 +63,7 @@ impl Default for Composer { form: FormWidget::default(), mode: ViewMode::Edit, + sign_mail: ToggleFlag::Unset, dirty: true, initialized: false, id: ComponentId::new_v4(), @@ -217,11 +219,20 @@ impl Composer { } } - fn draw_attachments(&self, grid: &mut CellBuffer, area: Area, _context: &mut Context) { + fn draw_attachments(&self, grid: &mut CellBuffer, area: Area, context: &Context) { let attachments_no = self.draft.attachments().len(); - if attachments_no == 0 { + if self.sign_mail.is_true() { write_string_to_grid( - "no attachments", + &format!( + "☑ sign with {}", + context + .settings + .pgp + .key + .as_ref() + .map(String::as_str) + .unwrap_or("default key") + ), grid, Color::Default, Color::Default, @@ -231,7 +242,7 @@ impl Composer { ); } else { write_string_to_grid( - &format!("{} attachments ", attachments_no), + "☐ don't sign", grid, Color::Default, Color::Default, @@ -239,6 +250,27 @@ impl Composer { (pos_inc(upper_left!(area), (0, 1)), bottom_right!(area)), false, ); + } + if attachments_no == 0 { + write_string_to_grid( + "no attachments", + grid, + Color::Default, + Color::Default, + Attr::Default, + (pos_inc(upper_left!(area), (0, 2)), bottom_right!(area)), + false, + ); + } else { + write_string_to_grid( + &format!("{} attachments ", attachments_no), + grid, + Color::Default, + Color::Default, + Attr::Default, + (pos_inc(upper_left!(area), (0, 2)), bottom_right!(area)), + false, + ); for (i, a) in self.draft.attachments().iter().enumerate() { if let Some(name) = a.content_type().name() { write_string_to_grid( @@ -253,7 +285,7 @@ impl Composer { Color::Default, Color::Default, Attr::Default, - (pos_inc(upper_left!(area), (0, 2 + i)), bottom_right!(area)), + (pos_inc(upper_left!(area), (0, 3 + i)), bottom_right!(area)), false, ); } else { @@ -263,7 +295,7 @@ impl Composer { Color::Default, Color::Default, Attr::Default, - (pos_inc(upper_left!(area), (0, 2 + i)), bottom_right!(area)), + (pos_inc(upper_left!(area), (0, 3 + i)), bottom_right!(area)), false, ); } @@ -291,6 +323,9 @@ impl Component for Composer { }; if !self.initialized { + if self.sign_mail.is_unset() { + self.sign_mail = ToggleFlag::InternalVal(context.settings.pgp.auto_sign); + } if !self.draft.headers().contains_key("From") || self.draft.headers()["From"].is_empty() { self.draft.headers_mut().insert( @@ -632,7 +667,12 @@ impl Component for Composer { } UIEvent::Input(Key::Char('s')) => { self.update_draft(); - if send_draft(context, self.account_cursor, self.draft.clone()) { + if send_draft( + self.sign_mail, + context, + self.account_cursor, + self.draft.clone(), + ) { context .replies .push_back(UIEvent::Action(Tab(Kill(self.id)))); @@ -743,6 +783,12 @@ impl Component for Composer { self.dirty = true; return true; } + Action::Compose(ComposeAction::ToggleSign) => { + let is_true = self.sign_mail.is_true(); + self.sign_mail = ToggleFlag::from(!is_true); + self.dirty = true; + return true; + } _ => {} } } @@ -815,7 +861,12 @@ impl Component for Composer { } } -pub fn send_draft(context: &mut Context, account_cursor: usize, draft: Draft) -> bool { +pub fn send_draft( + sign_mail: ToggleFlag, + context: &mut Context, + account_cursor: usize, + mut draft: Draft, +) -> bool { use std::io::Write; use std::process::{Command, Stdio}; let mut failure = true; @@ -830,6 +881,55 @@ pub fn send_draft(context: &mut Context, account_cursor: usize, draft: Draft) -> .expect("Failed to start mailer command"); { let stdin = msmtp.stdin.as_mut().expect("failed to open stdin"); + if sign_mail.is_true() { + let mut body: AttachmentBuilder = Attachment::new( + Default::default(), + Default::default(), + std::mem::replace(&mut draft.body, String::new()).into_bytes(), + ) + .into(); + if !draft.attachments.is_empty() { + let mut parts = std::mem::replace(&mut draft.attachments, Vec::new()); + parts.insert(0, body); + let boundary = ContentType::make_boundary(&parts); + body = Attachment::new( + ContentType::Multipart { + boundary: boundary.into_bytes(), + kind: MultipartType::Mixed, + parts: parts.into_iter().map(|a| a.into()).collect::>(), + }, + Default::default(), + Vec::new(), + ) + .into(); + } + let output = crate::components::mail::pgp::sign( + body.into(), + context.settings.pgp.gpg_binary.as_ref().map(String::as_str), + context.settings.pgp.key.as_ref().map(String::as_str), + ); + if let Err(e) = &output { + debug!("{:?} could not sign draft msg", e); + log( + format!( + "Could not sign draft in account `{}`: {}.", + context.accounts[account_cursor].name(), + e.to_string() + ), + ERROR, + ); + context.replies.push_back(UIEvent::Notification( + Some(format!( + "Could not sign draft in account `{}`.", + context.accounts[account_cursor].name() + )), + e.to_string(), + Some(NotificationType::ERROR), + )); + return false; + } + draft.attachments.push(output.unwrap()); + } let draft = draft.finalise().unwrap(); stdin .write_all(draft.as_bytes()) diff --git a/ui/src/components/mail/pgp.rs b/ui/src/components/mail/pgp.rs new file mode 100644 index 00000000..233c633b --- /dev/null +++ b/ui/src/components/mail/pgp.rs @@ -0,0 +1,130 @@ +/* + * meli - ui crate. + * + * Copyright 2019 Manos Pitsidianakis + * + * This file is part of meli. + * + * meli is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * meli is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with meli. If not, see . + */ + +use super::*; +use std::io::Write; +use std::process::{Command, Stdio}; + +pub fn verify_signature(a: &Attachment, context: &mut Context) -> Vec { + match melib::signatures::verify_signature(a) { + Ok((bytes, sig)) => { + let bytes_file = create_temp_file(&bytes, None, None, true); + let signature_file = create_temp_file(sig, None, None, true); + if let Ok(gpg) = Command::new( + context + .settings + .pgp + .gpg_binary + .as_ref() + .map(String::as_str) + .unwrap_or("gpg2"), + ) + .args(&[ + "--output", + "-", + "--verify", + signature_file.path.to_str().unwrap(), + bytes_file.path.to_str().unwrap(), + ]) + .stdin(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + { + return gpg.wait_with_output().unwrap().stderr; + } else { + context.replies.push_back(UIEvent::Notification( + Some(format!( + "Failed to launch {} to verify PGP signature", + context + .settings + .pgp + .gpg_binary + .as_ref() + .map(String::as_str) + .unwrap_or("gpg2"), + )), + "see meli.conf(5) for configuration setting pgp.gpg_binary".to_string(), + Some(NotificationType::ERROR), + )); + } + } + Err(e) => { + context.replies.push_back(UIEvent::Notification( + Some(e.to_string()), + String::new(), + Some(NotificationType::ERROR), + )); + } + } + Vec::new() +} + +/// Returns multipart/signed +pub fn sign( + a: AttachmentBuilder, + gpg_binary: Option<&str>, + pgp_key: Option<&str>, +) -> Result { + let mut command = Command::new(gpg_binary.unwrap_or("gpg2")); + command.args(&[ + "--digest-algo", + "sha512", + "--output", + "-", + "--detach-sig", + "--armor", + ]); + if let Some(key) = pgp_key { + command.args(&["--local-user", key]); + } + let a: Attachment = a.into(); + let mut gpg = command + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn()?; + + let sig_attachment = { + gpg.stdin + .as_mut() + .unwrap() + .write_all(&melib::signatures::convert_attachment_to_rfc_spec( + a.into_raw().as_bytes(), + )) + .unwrap(); + let gpg = gpg.wait_with_output().unwrap(); + Attachment::new(ContentType::PGPSignature, Default::default(), gpg.stdout) + }; + + let a: AttachmentBuilder = a.into(); + let parts = vec![a, sig_attachment.into()]; + let boundary = ContentType::make_boundary(&parts); + Ok(Attachment::new( + ContentType::Multipart { + boundary: boundary.into_bytes(), + kind: MultipartType::Signed, + parts: parts.into_iter().map(|a| a.into()).collect::>(), + }, + Default::default(), + Vec::new(), + ) + .into()) +} diff --git a/ui/src/components/mail/view.rs b/ui/src/components/mail/view.rs index 8a8e7c5f..a10d6cae 100644 --- a/ui/src/components/mail/view.rs +++ b/ui/src/components/mail/view.rs @@ -196,58 +196,7 @@ impl MailView { } else if a.is_signed() { v.clear(); if context.settings.pgp.auto_verify_signatures { - match melib::signatures::verify_signature(a) { - Ok((bytes, sig)) => { - let bytes_file = create_temp_file(&bytes, None, None, true); - let signature_file = create_temp_file(sig, None, None, true); - if let Ok(gpg) = Command::new( - context - .settings - .pgp - .gpg_binary - .as_ref() - .map(String::as_str) - .unwrap_or("gpg2"), - ) - .args(&[ - "--output", - "-", - "--verify", - signature_file.path.to_str().unwrap(), - bytes_file.path.to_str().unwrap(), - ]) - .stdin(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - { - v.extend(gpg.wait_with_output().unwrap().stderr); - } else { - context.replies.push_back(UIEvent::Notification( - Some(format!( - "Failed to launch {} to verify PGP signature", - context - .settings - .pgp - .gpg_binary - .as_ref() - .map(String::as_str) - .unwrap_or("gpg2"), - )), - "see meli.conf(5) for configuration setting pgp.gpg_binary" - .to_string(), - Some(NotificationType::ERROR), - )); - return; - } - } - Err(e) => { - context.replies.push_back(UIEvent::Notification( - Some(e.to_string()), - String::new(), - Some(NotificationType::ERROR), - )); - } - } + v.extend(crate::mail::pgp::verify_signature(a, context).into_iter()); } } })), @@ -1041,6 +990,7 @@ impl Component for MailView { ), ); if super::compose::send_draft( + ToggleFlag::False, /* FIXME: refactor to avoid unsafe. * * actions contains byte slices from the envelope's diff --git a/ui/src/conf.rs b/ui/src/conf.rs index dc6cc169..33af0ced 100644 --- a/ui/src/conf.rs +++ b/ui/src/conf.rs @@ -58,7 +58,7 @@ macro_rules! split_command { }}; } -#[derive(Debug, Clone, PartialEq)] +#[derive(Copy, Debug, Clone, PartialEq)] pub enum ToggleFlag { Unset, InternalVal(bool), @@ -66,6 +66,16 @@ pub enum ToggleFlag { True, } +impl From for ToggleFlag { + fn from(val: bool) -> Self { + if val { + ToggleFlag::True + } else { + ToggleFlag::False + } + } +} + impl Default for ToggleFlag { fn default() -> Self { ToggleFlag::Unset diff --git a/ui/src/execute.rs b/ui/src/execute.rs index 18e3a47e..6393c05f 100644 --- a/ui/src/execute.rs +++ b/ui/src/execute.rs @@ -214,6 +214,17 @@ define_commands!([ ); ) }, + { tags: ["toggle sign "], + desc: "switch between sign/unsign for this draft", + parser:( + named!( toggle_sign, + do_parse!( + ws!(tag!("toggle sign")) + >> (Compose(ToggleSign)) + ) + ); + ) + }, { tags: ["create-folder "], desc: "create-folder ACCOUNT FOLDER_PATH", parser:( @@ -350,7 +361,7 @@ named!( named!( compose_action, - alt_complete!(add_attachment | remove_attachment) + alt_complete!(add_attachment | remove_attachment | toggle_sign) ); named!(pub parse_command, diff --git a/ui/src/execute/actions.rs b/ui/src/execute/actions.rs index 23605402..4357e26a 100644 --- a/ui/src/execute/actions.rs +++ b/ui/src/execute/actions.rs @@ -71,6 +71,7 @@ pub enum PagerAction { pub enum ComposeAction { AddAttachment(String), RemoveAttachment(usize), + ToggleSign, } #[derive(Debug)]