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 }