email-client

A gtk4 Email Client
git clone git://archive.git.mtrnord.blog/erooster-mail/email-client.git
Log | Files | Refs | LICENSE

login_view.rs (10396B)


      1 use crate::imap_state::ImapSession;
      2 use crate::imap_state::IMAP_SESSION;
      3 use crate::AppMsg;
      4 use color_eyre::{eyre::bail, Result};
      5 use gtk::prelude::*;
      6 use once_cell::sync::Lazy;
      7 use quick_xml::de::from_str;
      8 use relm4::component::AsyncComponent;
      9 use relm4::component::AsyncComponentParts;
     10 use relm4::loading_widgets::LoadingWidgets;
     11 use relm4::prelude::*;
     12 use relm4::AsyncComponentSender;
     13 use relm4::{view, Worker, WorkerController};
     14 use reqwest::StatusCode;
     15 use serde::Deserialize;
     16 use std::convert::identity;
     17 use tokio::runtime::Runtime;
     18 
     19 static RUNTIME: Lazy<Runtime> = Lazy::new(|| Runtime::new().unwrap());
     20 
     21 pub struct LoginView {
     22     // stores entered values
     23     email: gtk::EntryBuffer,
     24     password: gtk::PasswordEntryBuffer,
     25     login_worker: WorkerController<AsyncLoginHandler>,
     26 }
     27 
     28 #[derive(Debug)]
     29 pub enum LoginMsg {
     30     Finished,
     31     StartLogin,
     32 }
     33 
     34 #[relm4::component(async, pub)]
     35 impl AsyncComponent for LoginView {
     36     type Init = ();
     37     type Input = LoginMsg;
     38     type Output = AppMsg;
     39     type CommandOutput = LoginMsg;
     40 
     41     view! {
     42         gtk::Box {
     43             set_orientation: gtk::Orientation::Vertical,
     44             set_halign: gtk::Align::Fill,
     45             set_valign: gtk::Align::Fill,
     46             set_hexpand: true,
     47             set_vexpand: true,
     48 
     49             gtk::Box {
     50                 set_orientation: gtk::Orientation::Vertical,
     51                 set_margin_all: 8,
     52                 set_spacing: 8,
     53                 set_halign: gtk::Align::Center,
     54                 set_valign: gtk::Align::Center,
     55 
     56                 gtk::Entry {
     57                     set_tooltip_text: Some("Username"),
     58                     set_buffer: &model.email,
     59                 },
     60                 gtk::Entry {
     61                     set_tooltip_text: Some("Password"),
     62                     set_buffer: &model.password,
     63                     set_visibility: false,
     64                 },
     65                 gtk::Button {
     66                     set_label: "Login",
     67                     connect_clicked => LoginMsg::StartLogin,
     68                 },
     69             }
     70         }
     71     }
     72 
     73     fn init_loading_widgets(root: &mut Self::Root) -> Option<LoadingWidgets> {
     74         view! {
     75             #[local_ref]
     76             root {
     77                 // This will be removed automatically by
     78                 // LoadingWidgets when the full view has loaded
     79                 #[name(spinner)]
     80                 gtk::Spinner {
     81                     start: (),
     82                     set_halign: gtk::Align::Center,
     83                 }
     84             }
     85         }
     86         Some(LoadingWidgets::new(root, spinner))
     87     }
     88 
     89     async fn init(
     90         _: Self::Init,
     91         root: Self::Root,
     92         sender: AsyncComponentSender<Self>,
     93     ) -> AsyncComponentParts<Self> {
     94         // TODO: Check if logged in and exit early if needed
     95         let model = LoginView {
     96             email: gtk::EntryBuffer::new(None::<String>),
     97             password: gtk::PasswordEntryBuffer::new(),
     98             login_worker: AsyncLoginHandler::builder()
     99                 .detach_worker(())
    100                 .forward(sender.input_sender(), identity),
    101         };
    102 
    103         // Insert the code generation of the view! macro here
    104         let widgets = view_output!();
    105 
    106         AsyncComponentParts { model, widgets }
    107     }
    108 
    109     async fn update(
    110         &mut self,
    111         msg: Self::Input,
    112         sender: AsyncComponentSender<Self>,
    113         _root: &Self::Root,
    114     ) {
    115         tracing::info!("Got msg1: {:?}", msg);
    116         match msg {
    117             LoginMsg::StartLogin => {
    118                 tracing::info!("Starting login");
    119                 self.login_worker
    120                     .sender()
    121                     .send(AsyncLoginHandlerMsg::StartLogin(
    122                         self.email.text().to_string(),
    123                         self.password.text().to_string(),
    124                     ))
    125                     .unwrap();
    126             }
    127             LoginMsg::Finished => {
    128                 tracing::info!("Login finished");
    129                 sender.output(AppMsg::ToMainView).unwrap();
    130             }
    131             _ => {}
    132         }
    133     }
    134 }
    135 
    136 #[derive(Debug)]
    137 enum AsyncLoginHandlerMsg {
    138     StartLogin(String, String),
    139 }
    140 
    141 struct AsyncLoginHandler;
    142 
    143 impl Worker for AsyncLoginHandler {
    144     type Init = ();
    145     type Input = AsyncLoginHandlerMsg;
    146     type Output = LoginMsg;
    147 
    148     fn init(_init: Self::Init, _sender: ComponentSender<Self>) -> Self {
    149         Self
    150     }
    151 
    152     fn update(&mut self, msg: AsyncLoginHandlerMsg, sender: ComponentSender<Self>) {
    153         tracing::info!("Got msg: {:?}", msg);
    154         match msg {
    155             AsyncLoginHandlerMsg::StartLogin(email, password) => {
    156                 RUNTIME.block_on(async move {
    157                     tracing::info!("Starting login inner");
    158                     let server_url = email.split('@').last().unwrap().to_string();
    159                     let config = get_autoconfig(server_url, email.clone()).await;
    160                     match config {
    161                         Ok(config) => {
    162                             tracing::info!("Got config: {:?}", config);
    163 
    164                             let incoming_server = config
    165                                 .email_provider
    166                                 .incoming_servers
    167                                 .iter()
    168                                 .find(|d| d.socket_type == "SSL")
    169                                 .unwrap();
    170 
    171                             let client = imap::ClientBuilder::new(
    172                                 incoming_server.hostname.clone(),
    173                                 incoming_server.port.parse::<u16>().unwrap(),
    174                             )
    175                             .rustls()
    176                             .unwrap();
    177 
    178                             // TODO: Use OAUth2 if https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat#OAuth2 is set
    179 
    180                             let auth_data = match incoming_server.username.as_str() {
    181                                 "%EMAILADDRESS%" => PlainAuth { email, password },
    182                                 "%EMAILLOCALPART%" => {
    183                                     let email = email.split('@').next().unwrap().to_string();
    184                                     PlainAuth { email, password }
    185                                 }
    186                                 "%EMAILDOMAIN%" => {
    187                                     let email = email.split('@').last().unwrap().to_string();
    188                                     PlainAuth { email, password }
    189                                 }
    190                                 _ => {
    191                                     tracing::error!("Username type not supported");
    192                                     return;
    193                                 }
    194                             };
    195 
    196                             // the client we have here is unauthenticated.
    197                             // to do anything useful with the e-mails, we need to log in
    198                             // FIXME: Use correct authentication method
    199                             let imap_session = client
    200                                 .authenticate("PLAIN", &auth_data)
    201                                 .map_err(|e| e.0)
    202                                 .unwrap();
    203 
    204                             *IMAP_SESSION.write() = ImapSession::TLS(imap_session);
    205                             sender.output(LoginMsg::Finished).unwrap();
    206                         }
    207                         Err(e) => {
    208                             tracing::warn!("Unable to get config: {:?}", e);
    209                         }
    210                     }
    211                 });
    212             }
    213         };
    214     }
    215 }
    216 
    217 struct PlainAuth {
    218     email: String,
    219     password: String,
    220 }
    221 
    222 impl imap::Authenticator for PlainAuth {
    223     type Response = String;
    224     fn process(&self, _: &[u8]) -> Self::Response {
    225         format!("\0{}\0{}", self.email, self.password)
    226     }
    227 }
    228 
    229 async fn get_autoconfig(server_url: String, email: String) -> Result<AutoconfigXML> {
    230     // First check subdomain with email
    231     // TODO: Parse xml
    232     let resp = reqwest::get(format!(
    233         "https://autoconfig.{server_url}/mail/config-v1.1.xml?emailaddress={email}"
    234     ))
    235     .await;
    236 
    237     match resp {
    238         Ok(resp) => {
    239             if resp.status() != StatusCode::OK {
    240                 tracing::info!("Not found. Trying well-known");
    241                 return get_autoconfig_wellknown(server_url).await;
    242             }
    243 
    244             let xml_text = resp.text().await?;
    245             let parsed_xml: AutoconfigXML = from_str(&xml_text)?;
    246 
    247             Ok(parsed_xml)
    248         }
    249         Err(_) => {
    250             tracing::info!("Not found. Trying well-known");
    251             get_autoconfig_wellknown(server_url).await
    252         }
    253     }
    254 }
    255 
    256 async fn get_autoconfig_wellknown(server_url: String) -> Result<AutoconfigXML> {
    257     let resp = reqwest::get(format!(
    258         "https://{server_url}/.well-known/autoconfig/mail/config-v1.1.xml"
    259     ))
    260     .await?;
    261 
    262     if resp.status() != StatusCode::OK {
    263         // TODO: Check DNS
    264         bail!("Unable to find server autoconfig");
    265     }
    266 
    267     let xml_text = resp.text().await?;
    268     let parsed_xml: AutoconfigXML = from_str(&xml_text)?;
    269     Ok(parsed_xml)
    270 }
    271 
    272 #[derive(Debug, Deserialize, PartialEq)]
    273 #[serde(rename_all = "camelCase")]
    274 struct AutoconfigXML {
    275     #[serde(rename = "@version")]
    276     version: String,
    277     email_provider: EmailProviderXML,
    278 }
    279 
    280 #[derive(Debug, Deserialize, PartialEq)]
    281 #[serde(rename_all = "camelCase")]
    282 struct EmailProviderXML {
    283     #[serde(rename = "@id")]
    284     id: String,
    285     #[serde(rename = "domain", default)]
    286     domains: Vec<String>,
    287     display_name: String,
    288     display_short_name: String,
    289     documentation: Option<DocumentationXML>,
    290     #[serde(rename = "incomingServer", default)]
    291     incoming_servers: Vec<IncomingServerXML>,
    292     #[serde(rename = "outgoingServer", default)]
    293     outgoing_servers: Vec<OutgoingServerXML>,
    294 }
    295 
    296 #[derive(Debug, Deserialize, PartialEq)]
    297 struct DocumentationXML {
    298     url: String,
    299 }
    300 
    301 #[derive(Debug, Deserialize, PartialEq)]
    302 #[serde(rename_all = "camelCase")]
    303 struct IncomingServerXML {
    304     #[serde(rename = "@type", default)]
    305     servertype: String,
    306     hostname: String,
    307     port: String,
    308     socket_type: String,
    309     authentication: String,
    310     username: String,
    311 }
    312 
    313 #[derive(Debug, Deserialize, PartialEq)]
    314 #[serde(rename_all = "camelCase")]
    315 struct OutgoingServerXML {
    316     #[serde(rename = "@type", default)]
    317     servertype: String,
    318     hostname: String,
    319     port: String,
    320     socket_type: String,
    321     authentication: String,
    322     username: String,
    323 }