summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorManos Pitsidianakis <el13635@mail.ntua.gr>2019-09-28 10:46:49 +0300
committerManos Pitsidianakis <el13635@mail.ntua.gr>2019-09-28 12:19:22 +0300
commite35a93336a2c0ce081cd05f64986c6f6cdc59077 (patch)
treee36c7e99b24f7826980d5eec590551c68713d734
parent963fdd157503966d4ec16a9b175be6b2caa9fba6 (diff)
Add GPG signing and sig verifying
-rw-r--r--meli.12
-rw-r--r--melib/src/email/attachment_types.rs23
-rw-r--r--melib/src/email/attachments.rs84
-rw-r--r--melib/src/email/compose.rs40
-rw-r--r--ui/src/components/mail.rs2
-rw-r--r--ui/src/components/mail/compose.rs114
-rw-r--r--ui/src/components/mail/pgp.rs130
-rw-r--r--ui/src/components/mail/view.rs54
-rw-r--r--ui/src/conf.rs12
-rw-r--r--ui/src/execute.rs13
-rw-r--r--ui/src/execute/actions.rs1
11 files changed, 390 insertions, 85 deletions
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<Attachment> for AttachmentBuilder {
}
}
+impl From<AttachmentBuilder> 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<String, String>,
- header_order: Vec<String>,
- body: String,
+ pub headers: FnvHashMap<String, String>,
+ pub header_order: Vec<String>,
+ pub body: String,
- attachments: Vec<AttachmentBuilder>,
+ pub attachments: Vec<AttachmentBuilder>,
}
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,8 +219,38 @@ 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 self.sign_mail.is_true() {
+ write_string_to_grid(
+ &format!(
+ "☑ sign with {}",
+ context
+ .settings
+ .pgp
+ .key
+ .as_ref()
+ .map(String::as_str)
+ .unwrap_or("default key")
+ ),
+ grid,
+ Color::Default,
+ Color::Default,
+ Attr::Default,
+ (pos_inc(upper_left!(area), (0, 1)), bottom_right!(area)),
+ false,
+ );
+ } else {
+ write_string_to_grid(
+ "☐ don't sign",
+ grid,
+ Color::Default,
+ Color::Default,
+ Attr::Default,
+ (pos_inc(upper_left!(area), (0, 1)), bottom_right!(area)),
+ false,
+ );
+ }
if attachments_no == 0 {
write_string_to_grid(
"no attachments",
@@ -226,7 +258,7 @@ impl Composer {
Color::Default,
Color::Default,
Attr::Default,
- (pos_inc(upper_left!(area), (0, 1)), bottom_right!(area)),
+ (pos_inc(upper_left!(area), (0, 2)), bottom_right!(area)),
false,
);
} else {
@@ -236,7 +268,7 @@ impl Composer {
Color::Default,
Color::Default,
Attr::Default,
- (pos_inc(upper_left!(area), (0, 1)), bottom_right!(area)),
+ (pos_inc(upper_left!(area), (0, 2)), bottom_right!(area)),
false,
);
for (i, a) in self.draft.attachments().iter().enumerate() {
@@ -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::<Vec<_>>(),
+ },
+ 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 <http://www.gnu.org/licenses/>.
+ */
+
+use super::*;
+use std::io::Write;
+use std::process::{Command, Stdio};
+
+pub fn verify_signature(a: &Attachment, context: &mut Context) -> Vec<u8> {
+ 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<AttachmentBuilder> {
+ 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::<Vec<_>>(),
+ },
+ 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<bool> 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<Action>,
+ 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<Action>,
- alt_complete!(add_attachment | remove_attachment)
+ alt_complete!(add_attachment | remove_attachment | toggle_sign)
);
named!(pub parse_command<Action>,
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)]