浏览代码

compose/pgp: rewrite key selection logic

Rewrite key selection logic when opening the key selector widget for
signing or encryption keys:

- Fix encrypt_for_self incorrectly using the To: header email instead of
  From: (ugh!)
- Include all recipient addresses in selection (To:, Cc: Bcc:), also
  From:
- Use all addresses in a header value instead of just the first address
- Show shortcuts to sign/encrypt fields in Compose form

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
Manos Pitsidianakis 7 月之前
父节点
当前提交
3433c5c3
共有 2 个文件被更改,包括 377 次插入172 次删除
  1. 162 80
      meli/src/mail/compose.rs
  2. 215 92
      meli/src/mail/compose/gpg.rs

+ 162 - 80
meli/src/mail/compose.rs

@@ -725,6 +725,12 @@ To: {}
         let attachments_no = self.draft.attachments().len();
         let theme_default = crate::conf::value(context, "theme_default");
         grid.clear_area(area, theme_default);
+        let our_map: ShortcutMap =
+            account_settings!(context[self.account_hash].shortcuts.composing).key_values();
+        let mut shortcuts: ShortcutMaps = Default::default();
+        shortcuts.insert(Shortcuts::COMPOSING, our_map);
+        let toggle_shortcut = Key::Char('\n');
+        let edit_shortcut = &shortcuts[Shortcuts::COMPOSING]["edit"];
         #[cfg(feature = "gpgme")]
         if self
             .gpg_state
@@ -742,12 +748,14 @@ To: {}
 
             grid.write_string(
                 &format!(
-                    "☑ sign with {}",
+                    "☑ sign with [toggle: {}, edit: {}] {}",
+                    toggle_shortcut,
+                    edit_shortcut,
                     if self.gpg_state.sign_keys.is_empty() {
                         "default key"
                     } else {
                         key_list.as_str()
-                    }
+                    },
                 ),
                 theme_default.fg,
                 if self.cursor == Cursor::Sign {
@@ -762,7 +770,10 @@ To: {}
             );
         } else {
             grid.write_string(
-                "☐ don't sign",
+                &format!(
+                    "☐ don't sign [toggle: {}, edit: {}]",
+                    toggle_shortcut, edit_shortcut,
+                ),
                 theme_default.fg,
                 if self.cursor == Cursor::Sign {
                     crate::conf::value(context, "highlight").bg
@@ -792,12 +803,22 @@ To: {}
 
             grid.write_string(
                 &format!(
-                    "{}{}",
+                    "{}{}{}",
                     if self.gpg_state.encrypt_keys.is_empty() {
-                        "☐ no keys to encrypt with!"
+                        "☐ no keys selected to encrypt with"
                     } else {
-                        "☑ encrypt with "
+                        "☑ encrypt with"
                     },
+                    &format!(
+                        " [toggle: {}, edit: {}]{}",
+                        toggle_shortcut,
+                        edit_shortcut,
+                        if self.gpg_state.encrypt_keys.is_empty() {
+                            ""
+                        } else {
+                            " "
+                        }
+                    ),
                     if self.gpg_state.encrypt_keys.is_empty() {
                         ""
                     } else {
@@ -817,7 +838,10 @@ To: {}
             );
         } else {
             grid.write_string(
-                "☐ don't encrypt",
+                &format!(
+                    "☐ don't encrypt [toggle: {}, edit: {}]",
+                    toggle_shortcut, edit_shortcut,
+                ),
                 theme_default.fg,
                 if self.cursor == Cursor::Encrypt {
                     crate::conf::value(context, "highlight").bg
@@ -832,7 +856,7 @@ To: {}
         }
         if attachments_no == 0 {
             grid.write_string(
-                "no attachments",
+                &format!("no attachments [edit: {}]", edit_shortcut),
                 theme_default.fg,
                 if self.cursor == Cursor::Attachments {
                     crate::conf::value(context, "highlight").bg
@@ -846,7 +870,7 @@ To: {}
             );
         } else {
             grid.write_string(
-                &format!("{} attachments ", attachments_no),
+                &format!("{} attachments [edit: {}]", attachments_no, edit_shortcut),
                 theme_default.fg,
                 if self.cursor == Cursor::Attachments {
                     crate::conf::value(context, "highlight").bg
@@ -912,6 +936,36 @@ To: {}
             }
         }
     }
+
+    #[cfg(feature = "gpgme")]
+    fn create_key_selection_widget(
+        &self,
+        secret: bool,
+        header: &HeaderName,
+        context: &Context,
+    ) -> Result<gpg::KeySelectionLoading> {
+        let (_, mut list) = melib::email::parser::address::rfc2822address_list(
+            self.form.values()[header.as_str()].as_str().as_bytes(),
+        )
+        .map_err(|_err| -> Error { format!("No valid address in `{header}:`").into() })?;
+        if list.is_empty() {
+            return Err(format!("No valid address in `{header}:`").into());
+        }
+        let first = list.remove(0);
+        let patterns = (
+            first.get_email(),
+            list.into_iter()
+                .map(|addr| addr.get_email())
+                .collect::<Vec<String>>(),
+        );
+        gpg::KeySelectionLoading::new(
+            secret,
+            account_settings!(context[self.account_hash].pgp.allow_remote_lookup).is_true(),
+            patterns,
+            *account_settings!(context[self.account_hash].pgp.allow_remote_lookup),
+            context,
+        )
+    }
 }
 
 impl Component for Composer {
@@ -978,12 +1032,27 @@ impl Component for Composer {
 
         if self.dirty {
             grid.clear_area(area.nth_row(0), crate::conf::value(context, "highlight"));
+            let our_map: ShortcutMap =
+                account_settings!(context[self.account_hash].shortcuts.composing).key_values();
+            let mut shortcuts: ShortcutMaps = Default::default();
+            shortcuts.insert(Shortcuts::COMPOSING, our_map);
+            let scroll_down_shortcut = &shortcuts[Shortcuts::COMPOSING]["scroll_down"];
+            let scroll_up_shortcut = &shortcuts[Shortcuts::COMPOSING]["scroll_up"];
+            let field_shortcut = Key::Char('\n');
+            let edit_shortcut = &shortcuts[Shortcuts::COMPOSING]["edit"];
             grid.write_string(
-                if self.reply_context.is_some() {
-                    "COMPOSING REPLY"
-                } else {
-                    "COMPOSING MESSAGE"
-                },
+                &format!(
+                    "COMPOSING {} [scroll down: {}, scroll up: {}, edit fields: {}, edit body: {}]",
+                    if self.reply_context.is_some() {
+                        "REPLY"
+                    } else {
+                        "MESSAGE"
+                    },
+                    scroll_down_shortcut,
+                    scroll_up_shortcut,
+                    field_shortcut,
+                    edit_shortcut
+                ),
                 crate::conf::value(context, "highlight").fg,
                 crate::conf::value(context, "highlight").bg,
                 crate::conf::value(context, "highlight").attrs,
@@ -1049,11 +1118,11 @@ impl Component for Composer {
                     let stopped_message: String =
                         format!("Process with PID {} has stopped.", guard.child_pid);
                     let stopped_message_2: String = format!(
-                        "-press '{}' (edit shortcut) to re-activate.",
+                        "- re-activate '{}' (edit shortcut)",
                         shortcuts[Shortcuts::COMPOSING]["edit"]
                     );
                     const STOPPED_MESSAGE_3: &str =
-                        "-press Ctrl-C to forcefully kill it and return to editor.";
+                        "- press Ctrl-C to forcefully kill it and return to editor";
                     let max_len = std::cmp::max(
                         stopped_message.len(),
                         std::cmp::max(stopped_message_2.len(), STOPPED_MESSAGE_3.len()),
@@ -1473,13 +1542,13 @@ impl Component for Composer {
                 ViewMode::SelectKey(is_encrypt, ref mut selector),
                 UIEvent::FinishedUIDialog(id, result),
             ) if *id == selector.id() => {
-                if let Some(Some(key)) = result.downcast_mut::<Option<melib::gpgme::Key>>() {
+                if let Some(Some(keys)) = result.downcast_mut::<Option<Vec<melib::gpgme::Key>>>() {
                     if *is_encrypt {
                         self.gpg_state.encrypt_keys.clear();
-                        self.gpg_state.encrypt_keys.push(key.clone());
+                        self.gpg_state.encrypt_keys = std::mem::take(keys);
                     } else {
                         self.gpg_state.sign_keys.clear();
-                        self.gpg_state.sign_keys.push(key.clone());
+                        self.gpg_state.sign_keys = std::mem::take(keys);
                     }
                 }
                 self.mode = ViewMode::Edit;
@@ -1756,39 +1825,28 @@ impl Component for Composer {
                     && shortcut!(key == shortcuts[Shortcuts::COMPOSING]["edit"]) =>
             {
                 #[cfg(feature = "gpgme")]
-                match melib::email::parser::address::rfc2822address_list(
-                    self.form.values()["From"].as_str().as_bytes(),
-                )
-                .map_err(|_err| -> Error { "No valid sender address in `From:`".into() })
-                .and_then(|(_, list)| {
-                    list.first()
-                        .cloned()
-                        .ok_or_else(|| "No valid sender address in `From:`".into())
-                })
-                .and_then(|addr| {
-                    gpg::KeySelection::new(
-                        false,
-                        account_settings!(context[self.account_hash].pgp.allow_remote_lookup)
-                            .is_true(),
-                        addr.get_email(),
-                        *account_settings!(context[self.account_hash].pgp.allow_remote_lookup),
-                        context,
-                    )
-                }) {
-                    Ok(widget) => {
-                        self.gpg_state.sign_mail = Some(ActionFlag::from(true));
-                        self.mode = ViewMode::SelectKey(false, widget);
-                    }
-                    Err(err) => {
-                        context.replies.push_back(UIEvent::Notification {
-                            title: Some("Could not list keys.".into()),
-                            source: None,
-                            body: format!("libgpgme error: {err}").into(),
-                            kind: Some(NotificationType::Error(melib::error::ErrorKind::External)),
-                        });
+                {
+                    match self
+                        .create_key_selection_widget(false, &HeaderName::FROM, context)
+                        .map(Into::into)
+                    {
+                        Ok(widget) => {
+                            self.gpg_state.sign_mail = Some(ActionFlag::from(true));
+                            self.mode = ViewMode::SelectKey(false, widget);
+                        }
+                        Err(err) => {
+                            context.replies.push_back(UIEvent::Notification {
+                                title: Some("Could not list keys.".into()),
+                                source: None,
+                                body: format!("libgpgme error: {err}").into(),
+                                kind: Some(NotificationType::Error(
+                                    melib::error::ErrorKind::External,
+                                )),
+                            });
+                        }
                     }
+                    self.set_dirty(true);
                 }
-                self.set_dirty(true);
                 return true;
             }
             UIEvent::Input(ref key)
@@ -1797,39 +1855,63 @@ impl Component for Composer {
                     && shortcut!(key == shortcuts[Shortcuts::COMPOSING]["edit"]) =>
             {
                 #[cfg(feature = "gpgme")]
-                match melib::email::parser::address::rfc2822address_list(
-                    self.form.values()["To"].as_str().as_bytes(),
-                )
-                .map_err(|_err| -> Error { "No valid recipient addresses in `To:`".into() })
-                .and_then(|(_, list)| {
-                    list.first()
-                        .cloned()
-                        .ok_or_else(|| "No valid recipient addresses in `To:`".into())
-                })
-                .and_then(|addr| {
-                    gpg::KeySelection::new(
-                        false,
-                        account_settings!(context[self.account_hash].pgp.allow_remote_lookup)
-                            .is_true(),
-                        addr.get_email(),
-                        *account_settings!(context[self.account_hash].pgp.allow_remote_lookup),
-                        context,
-                    )
-                }) {
-                    Ok(widget) => {
-                        self.gpg_state.encrypt_mail = Some(ActionFlag::from(true));
-                        self.mode = ViewMode::SelectKey(true, widget);
+                {
+                    let mut result =
+                        self.create_key_selection_widget(false, &HeaderName::TO, context);
+                    if !self.form.values()[HeaderName::CC.as_str()]
+                        .as_str()
+                        .is_empty()
+                    {
+                        result = result.and_then(|mut to_result| {
+                            let cc_result =
+                                self.create_key_selection_widget(false, &HeaderName::CC, context)?;
+                            to_result.merge(cc_result);
+                            Ok(to_result)
+                        });
                     }
-                    Err(err) => {
-                        context.replies.push_back(UIEvent::Notification {
-                            title: Some("Could not list keys.".into()),
-                            source: None,
-                            body: format!("libgpgme error: {err}").into(),
-                            kind: Some(NotificationType::Error(melib::error::ErrorKind::External)),
+                    if !self.form.values()[HeaderName::BCC.as_str()]
+                        .as_str()
+                        .is_empty()
+                    {
+                        result = result.and_then(|mut to_result| {
+                            let bcc_result =
+                                self.create_key_selection_widget(false, &HeaderName::BCC, context)?;
+                            to_result.merge(bcc_result);
+                            Ok(to_result)
+                        });
+                    }
+                    if !self.form.values()[HeaderName::FROM.as_str()]
+                        .as_str()
+                        .is_empty()
+                    {
+                        result = result.and_then(|mut to_result| {
+                            let from_result = self.create_key_selection_widget(
+                                false,
+                                &HeaderName::FROM,
+                                context,
+                            )?;
+                            to_result.merge(from_result);
+                            Ok(to_result)
                         });
                     }
+                    match result.map(Into::into) {
+                        Ok(widget) => {
+                            self.gpg_state.encrypt_mail = Some(ActionFlag::from(true));
+                            self.mode = ViewMode::SelectKey(true, widget);
+                        }
+                        Err(err) => {
+                            context.replies.push_back(UIEvent::Notification {
+                                title: Some("Could not list keys.".into()),
+                                source: None,
+                                body: format!("libgpgme error: {err}").into(),
+                                kind: Some(NotificationType::Error(
+                                    melib::error::ErrorKind::External,
+                                )),
+                            });
+                        }
+                    }
+                    self.set_dirty(true);
                 }
-                self.set_dirty(true);
                 return true;
             }
             UIEvent::Input(ref key)
@@ -2658,7 +2740,7 @@ pub fn send_draft_async(
             gpg_state.encrypt_for_self.then_some(()).map_or_else(
                 || Ok(None),
                 |()| {
-                    draft.headers().get(HeaderName::TO).map_or_else(
+                    draft.headers().get(HeaderName::FROM).map_or_else(
                         || Ok(None),
                         |s| Some(melib::Address::try_from(s)).transpose(),
                     )

+ 215 - 92
meli/src/mail/compose/gpg.rs

@@ -21,37 +21,23 @@
 
 use super::*;
 
-#[derive(Debug)]
-pub enum KeySelection {
-    LoadingKeys {
-        handle: JoinHandle<Result<Vec<melib::gpgme::Key>>>,
-        progress_spinner: ProgressSpinner,
-        secret: bool,
-        local: bool,
-        pattern: String,
-        allow_remote_lookup: ActionFlag,
-    },
-    Error {
-        id: ComponentId,
-        err: Error,
-    },
-    Loaded {
-        widget: Box<UIDialog<melib::gpgme::Key>>,
-        keys: Vec<melib::gpgme::Key>,
-    },
-}
+type KeylistJoinHandle = JoinHandle<Result<Vec<melib::gpgme::Key>>>;
 
-impl std::fmt::Display for KeySelection {
-    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
-        write!(f, "select pgp keys")
-    }
+#[derive(Debug)]
+pub struct KeySelectionLoading {
+    handles: (KeylistJoinHandle, Vec<KeylistJoinHandle>),
+    progress_spinner: ProgressSpinner,
+    secret: bool,
+    local: bool,
+    patterns: (String, Vec<String>),
+    allow_remote_lookup: ActionFlag,
 }
 
-impl KeySelection {
+impl KeySelectionLoading {
     pub fn new(
         secret: bool,
         local: bool,
-        pattern: String,
+        patterns: (String, Vec<String>),
         allow_remote_lookup: ActionFlag,
         context: &Context,
     ) -> Result<Self> {
@@ -62,31 +48,103 @@ impl KeySelection {
         } else {
             ctx.set_auto_key_locate(LocateKey::WKD | LocateKey::LOCAL)?;
         }
-        let job = ctx.keylist(secret, Some(pattern.clone()))?;
-        let handle = context.main_loop_handler.job_executor.spawn(
+        let (pattern, other_patterns) = patterns;
+        let main_job = ctx.keylist(secret, Some(pattern.clone()))?;
+        let main_handle = context.main_loop_handler.job_executor.spawn(
             "gpg::keylist".into(),
-            job,
+            main_job,
             IsAsync::Blocking,
         );
+        let other_handles = other_patterns
+            .iter()
+            .map(|pattern| {
+                let job = ctx.keylist(secret, Some(pattern.clone()))?;
+                Ok(context.main_loop_handler.job_executor.spawn(
+                    "gpg::keylist".into(),
+                    job,
+                    IsAsync::Blocking,
+                ))
+            })
+            .collect::<Result<Vec<KeylistJoinHandle>>>()?;
         let mut progress_spinner = ProgressSpinner::new(8, context);
         progress_spinner.start();
-        Ok(Self::LoadingKeys {
-            handle,
+        Ok(Self {
+            handles: (main_handle, other_handles),
             secret,
             local,
-            pattern,
+            patterns: (pattern, other_patterns),
             allow_remote_lookup,
             progress_spinner,
         })
     }
+
+    pub fn merge(&mut self, rhs: Self) {
+        let Self {
+            handles: (_, ref mut other_handles),
+            secret: _,
+            local: _,
+            patterns: (_, ref mut other_patterns),
+            allow_remote_lookup: _,
+            progress_spinner: _,
+        } = self;
+        let Self {
+            handles: (rhs_handle, rhs_other_handles),
+            patterns: (rhs_pattern, rhs_other_patterns),
+            secret: _,
+            local: _,
+            allow_remote_lookup: _,
+            progress_spinner: _,
+        } = rhs;
+        other_handles.push(rhs_handle);
+        other_handles.extend(rhs_other_handles);
+        other_patterns.push(rhs_pattern);
+        other_patterns.extend(rhs_other_patterns);
+    }
+}
+
+#[derive(Debug)]
+pub enum KeySelection {
+    Loading {
+        inner: KeySelectionLoading,
+        /// Accumulate results from intermediate results (i.e. not the main
+        /// pattern)
+        keys_accumulator: Vec<melib::gpgme::Key>,
+    },
+    Error {
+        id: ComponentId,
+        err: Error,
+    },
+    Loaded {
+        widget: Box<UIDialog<melib::gpgme::Key>>,
+        keys: Vec<melib::gpgme::Key>,
+    },
+}
+
+impl From<KeySelectionLoading> for KeySelection {
+    fn from(inner: KeySelectionLoading) -> Self {
+        Self::Loading {
+            inner,
+            keys_accumulator: vec![],
+        }
+    }
+}
+
+impl std::fmt::Display for KeySelection {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        write!(f, "select pgp keys")
+    }
 }
 
 impl Component for KeySelection {
     fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
         match self {
-            Self::LoadingKeys {
-                ref mut progress_spinner,
-                ..
+            Self::Loading {
+                inner:
+                    KeySelectionLoading {
+                        ref mut progress_spinner,
+                        ..
+                    },
+                keys_accumulator: _,
             } => progress_spinner.draw(grid, area.center_inside((2, 2)), context),
             Self::Error { ref err, .. } => {
                 let theme_default = crate::conf::value(context, "theme_default");
@@ -106,16 +164,30 @@ impl Component for KeySelection {
 
     fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
         match self {
-            Self::LoadingKeys {
-                ref mut progress_spinner,
-                ref mut handle,
-                secret,
-                local,
-                ref mut pattern,
-                allow_remote_lookup,
-                ..
+            Self::Loading {
+                inner:
+                    KeySelectionLoading {
+                        ref mut progress_spinner,
+                        handles: (ref mut main_handle, ref mut other_handles),
+                        secret,
+                        local,
+                        patterns: (ref mut pattern, ref mut other_patterns),
+                        allow_remote_lookup,
+                        ..
+                    },
+                ref mut keys_accumulator,
             } => match event {
-                UIEvent::StatusEvent(StatusEvent::JobFinished(ref id)) if *id == handle.job_id => {
+                UIEvent::StatusEvent(StatusEvent::JobFinished(ref id))
+                    if *id == main_handle.job_id
+                        || other_handles.iter().any(|h| h.job_id == *id) =>
+                {
+                    let mut main_handle_ref = &mut (*main_handle);
+                    let is_main = *id == main_handle_ref.job_id;
+                    let handle = if is_main {
+                        &mut main_handle_ref
+                    } else {
+                        &mut other_handles.iter_mut().find(|h| h.job_id == *id).unwrap()
+                    };
                     match handle.chan.try_recv() {
                         Err(_) => { /* Job was canceled */ }
                         Ok(None) => { /* something happened, perhaps a worker thread panicked */ }
@@ -123,15 +195,19 @@ impl Component for KeySelection {
                             if keys.is_empty() {
                                 let id = progress_spinner.id();
                                 if allow_remote_lookup.is_true() {
-                                    match Self::new(
+                                    match KeySelectionLoading::new(
                                         *secret,
                                         *local,
-                                        std::mem::take(pattern),
+                                        (std::mem::take(pattern), std::mem::take(other_patterns)),
                                         *allow_remote_lookup,
                                         context,
                                     ) {
-                                        Ok(w) => {
-                                            *self = w;
+                                        Ok(inner) => {
+                                            let keys_accumulator = std::mem::take(keys_accumulator);
+                                            *self = Self::Loading {
+                                                inner,
+                                                keys_accumulator,
+                                            };
                                         }
                                         Err(err) => *self = Self::Error { err, id },
                                     }
@@ -157,42 +233,74 @@ impl Component for KeySelection {
                                     context.replies.push_back(UIEvent::StatusEvent(
                                         StatusEvent::DisplayMessage(err.to_string()),
                                     ));
-                                    let res: Option<melib::gpgme::Key> = None;
+                                    // Even in case of error, we should send a FinishedUIDialog
+                                    // event so that the component parent knows we're done.
+                                    let res: Option<Vec<melib::gpgme::Key>> = None;
                                     context
                                         .replies
                                         .push_back(UIEvent::FinishedUIDialog(id, Box::new(res)));
                                 }
                                 return false;
                             }
-                            let mut widget = Box::new(UIDialog::new(
-                                "select key",
-                                keys.iter()
-                                    .map(|k| {
-                                        (
-                                            k.clone(),
-                                            if let Some(primary_uid) = k.primary_uid() {
-                                                format!("{} {}", k.fingerprint(), primary_uid)
-                                            } else {
-                                                k.fingerprint().to_string()
-                                            },
-                                        )
-                                    })
-                                    .collect::<Vec<(melib::gpgme::Key, String)>>(),
-                                true,
-                                Some(Box::new(
-                                    move |id: ComponentId, results: &[melib::gpgme::Key]| {
-                                        Some(UIEvent::FinishedUIDialog(
-                                            id,
-                                            Box::new(results.first().cloned()),
-                                        ))
-                                    },
-                                )),
-                                context,
-                            ));
-                            widget.set_dirty(true);
-                            *self = Self::Loaded { widget, keys };
+                            keys_accumulator.extend(keys);
+                            if !is_main {
+                                other_handles.retain(|h| h.job_id != *id);
+                                return false;
+                            }
+                            if other_handles.is_empty() {
+                                // We are done with all Futures, so finally transition into the
+                                // "show the user the list of keys to select" state.
+                                let mut widget = Box::new(UIDialog::new(
+                                    "select key",
+                                    keys_accumulator
+                                        .iter()
+                                        .map(|k| {
+                                            (
+                                                k.clone(),
+                                                if let Some(primary_uid) = k.primary_uid() {
+                                                    format!("{} {}", k.fingerprint(), primary_uid)
+                                                } else {
+                                                    k.fingerprint().to_string()
+                                                },
+                                            )
+                                        })
+                                        .collect::<Vec<(melib::gpgme::Key, String)>>(),
+                                    false,
+                                    Some(Box::new(
+                                        move |id: ComponentId, results: &[melib::gpgme::Key]| {
+                                            Some(UIEvent::FinishedUIDialog(
+                                                id,
+                                                Box::new(if results.is_empty() {
+                                                    None
+                                                } else {
+                                                    Some(results.to_vec())
+                                                }),
+                                            ))
+                                        },
+                                    )),
+                                    context,
+                                ));
+                                widget.set_dirty(true);
+                                *self = Self::Loaded {
+                                    widget,
+                                    keys: std::mem::take(keys_accumulator),
+                                };
+                            } else {
+                                // Main handle has finished, replace it with some other one from
+                                // other_handles.
+                                *main_handle = other_handles.remove(0);
+                            }
                         }
                         Ok(Some(Err(err))) => {
+                            context.replies.push_back(UIEvent::StatusEvent(
+                                StatusEvent::DisplayMessage(err.to_string()),
+                            ));
+                            // Even in case of error, we should send a FinishedUIDialog
+                            // event so that the component parent knows we're done.
+                            let res: Option<Vec<melib::gpgme::Key>> = None;
+                            context
+                                .replies
+                                .push_back(UIEvent::FinishedUIDialog(self.id(), Box::new(res)));
                             *self = Self::Error {
                                 err,
                                 id: ComponentId::default(),
@@ -210,9 +318,13 @@ impl Component for KeySelection {
 
     fn is_dirty(&self) -> bool {
         match self {
-            Self::LoadingKeys {
-                ref progress_spinner,
-                ..
+            Self::Loading {
+                inner:
+                    KeySelectionLoading {
+                        ref progress_spinner,
+                        ..
+                    },
+                keys_accumulator: _,
             } => progress_spinner.is_dirty(),
             Self::Error { .. } => true,
             Self::Loaded { ref widget, .. } => widget.is_dirty(),
@@ -221,9 +333,13 @@ impl Component for KeySelection {
 
     fn set_dirty(&mut self, value: bool) {
         match self {
-            Self::LoadingKeys {
-                ref mut progress_spinner,
-                ..
+            Self::Loading {
+                inner:
+                    KeySelectionLoading {
+                        ref mut progress_spinner,
+                        ..
+                    },
+                keys_accumulator: _,
             } => progress_spinner.set_dirty(value),
             Self::Error { .. } => {}
             Self::Loaded { ref mut widget, .. } => widget.set_dirty(value),
@@ -234,16 +350,20 @@ impl Component for KeySelection {
 
     fn shortcuts(&self, context: &Context) -> ShortcutMaps {
         match self {
-            Self::LoadingKeys { .. } | Self::Error { .. } => ShortcutMaps::default(),
+            Self::Loading { .. } | Self::Error { .. } => ShortcutMaps::default(),
             Self::Loaded { ref widget, .. } => widget.shortcuts(context),
         }
     }
 
     fn id(&self) -> ComponentId {
         match self {
-            Self::LoadingKeys {
-                ref progress_spinner,
-                ..
+            Self::Loading {
+                inner:
+                    KeySelectionLoading {
+                        ref progress_spinner,
+                        ..
+                    },
+                keys_accumulator: _,
             } => progress_spinner.id(),
             Self::Error { ref id, .. } => *id,
             Self::Loaded { ref widget, .. } => widget.id(),
@@ -303,13 +423,16 @@ mod tests {
             );
             let mut progress_spinner = ProgressSpinner::new(8, context);
             progress_spinner.start();
-            Ok(Self::LoadingKeys {
-                handle,
-                secret,
-                local,
-                pattern,
-                allow_remote_lookup,
-                progress_spinner,
+            Ok(Self::Loading {
+                inner: KeySelectionLoading {
+                    handles: (handle, vec![]),
+                    secret,
+                    local,
+                    patterns: (pattern, vec![]),
+                    allow_remote_lookup,
+                    progress_spinner,
+                },
+                keys_accumulator: vec![],
             })
         }
     }