diff --git a/melib/src/email.rs b/melib/src/email.rs index 4c4dcd057..ca69f9284 100644 --- a/melib/src/email.rs +++ b/melib/src/email.rs @@ -35,6 +35,7 @@ mod address; pub mod parser; use crate::parser::BytesExt; pub use address::*; +pub mod signatures; use crate::backends::BackendOp; use crate::error::{MeliError, Result}; diff --git a/melib/src/email/attachments.rs b/melib/src/email/attachments.rs index 06cbf4f4a..bbe9cc38f 100644 --- a/melib/src/email/attachments.rs +++ b/melib/src/email/attachments.rs @@ -411,7 +411,7 @@ impl Attachment { fn get_text_recursive(&self, text: &mut Vec) { match self.content_type { - ContentType::Text { .. } => { + ContentType::Text { .. } | ContentType::PGPSignature => { text.extend(decode(self, None)); } ContentType::Multipart { @@ -536,6 +536,16 @@ impl Attachment { _ => false, } } + + pub fn is_signed(&self) -> bool { + match self.content_type { + ContentType::Multipart { + kind: MultipartType::Signed, + .. + } => true, + _ => false, + } + } } pub fn interpret_format_flowed(_t: &str) -> String { @@ -559,7 +569,7 @@ fn decode_rec_helper<'a>(a: &'a Attachment, filter: &mut Option>) -> .unwrap_or_else(|| a.mime_type()) .to_string() .into_bytes(), - ContentType::PGPSignature => a.content_type.to_string().into_bytes(), + ContentType::PGPSignature => Vec::new(), ContentType::MessageRfc822 => { let temp = decode_rfc822(a.body()); decode_rec(&temp, None) @@ -580,6 +590,14 @@ fn decode_rec_helper<'a>(a: &'a Attachment, filter: &mut Option>) -> } decode_helper(a, filter) } + MultipartType::Signed => { + let mut vec = Vec::new(); + for a in parts { + vec.extend(decode_rec_helper(a, filter)); + } + vec.extend(decode_helper(a, filter)); + vec + } _ => { let mut vec = Vec::new(); for a in parts { diff --git a/melib/src/email/signatures.rs b/melib/src/email/signatures.rs new file mode 100644 index 000000000..cf6ce4ac0 --- /dev/null +++ b/melib/src/email/signatures.rs @@ -0,0 +1,129 @@ +/* + * meli - email module. + * + * 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 crate::email::attachments::{Attachment, ContentType, MultipartType}; +use crate::email::parser::BytesExt; +use crate::{MeliError, Result}; + +/// rfc3156 +/// Upon receipt of a signed message, an application MUST: +/// +/// (1) Convert line endings to the canonical sequence before +/// the signature can be verified. This is necessary since the +/// local MTA may have converted to a local end of line convention. +/// (2) Pass both the signed data and its associated content headers +/// along with the OpenPGP signature to the signature verification +/// service. +/// +pub fn convert_attachment_to_rfc_spec(input: &[u8]) -> Vec { + if input.is_empty() { + return Vec::new(); + } + let mut ret = Vec::with_capacity(input.len()); + + if input[0] == b'\n' { + /* This is an RFC violation but test for it anyway */ + ret.push(b'\r'); + ret.push(b'\n'); + } else { + ret.push(input[0]); + } + + let mut ctr = 1; + + while ctr < input.len() { + if input[ctr] == b'\r' && ctr + 1 < input.len() && input[ctr + 1] == b'\n' { + ret.push(b'\r'); + ret.push(b'\n'); + ctr += 2; + } else if input[ctr] == b'\n' { + ret.push(b'\r'); + ret.push(b'\n'); + ctr += 1; + } else { + ret.push(input[ctr]); + ctr += 1; + } + } + loop { + match ret.iter().last() { + None => { + break; + } + Some(b'\n') | Some(b'\r') => { + ret.pop(); + } + _ => { + break; + } + } + } + ret.push(0x0d); + ret.push(0x0a); + ret +} + +pub fn verify_signature(a: &Attachment) -> Result<(Vec, &[u8])> { + match a.content_type { + ContentType::Multipart { + kind: MultipartType::Signed, + ref parts, + boundary: _, + } => { + if parts.len() != 2 { + return Err(MeliError::new(format!( + "Illegal number of parts in multipart/signed. Expected 2 got {}", + parts.len() + ))); + } + + let part_boundaries = a.part_boundaries(); + + let signed_part: Vec = if let Some(v) = parts + .iter() + .zip(part_boundaries.iter()) + .find(|(p, _)| p.content_type != ContentType::PGPSignature) + .map(|(_, s)| convert_attachment_to_rfc_spec(s.display_bytes(a.body()))) + { + v + } else { + return Err(MeliError::new( + "multipart/signed attachment without a signed part".to_string(), + )); + }; + let signature = if let Some(sig) = parts + .iter() + .find(|s| s.content_type == ContentType::PGPSignature) + .map(|a| a.body()) + { + sig.trim() + } else { + return Err(MeliError::new( + "multipart/signed attachment without a signature part".to_string(), + )); + }; + Ok((signed_part, signature)) + } + _ => { + unreachable!("Should not give non-signed attachments to this function"); + } + } +} diff --git a/ui/src/components/mail/view.rs b/ui/src/components/mail/view.rs index 5385e0356..154bd01a4 100644 --- a/ui/src/components/mail/view.rs +++ b/ui/src/components/mail/view.rs @@ -193,6 +193,45 @@ impl MailView { return; } } + } else if a.is_signed() { + v.clear(); + 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("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( + "Failed to find an application to verify PGP signature" + .to_string(), + ), + String::new(), + Some(NotificationType::ERROR), + )); + return; + } + } + Err(e) => { + context.replies.push_back(UIEvent::Notification( + Some(e.to_string()), + String::new(), + Some(NotificationType::ERROR), + )); + } + } } })), )) @@ -765,7 +804,7 @@ impl Component for MailView { return true; } - ContentType::Text { .. } => { + ContentType::Text { .. } | ContentType::PGPSignature => { self.mode = ViewMode::Attachment(lidx); self.dirty = true; } @@ -823,14 +862,6 @@ impl Component for MailView { )); return true; } - ContentType::PGPSignature => { - context.replies.push_back(UIEvent::StatusEvent( - StatusEvent::DisplayMessage( - "Signatures aren't supported yet".to_string(), - ), - )); - return true; - } } } else { context.replies.push_back(UIEvent::StatusEvent(