diff --git a/Cargo.toml b/Cargo.toml index bd36b4d..08f24b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,14 +13,16 @@ path = "src/lib.rs" async-trait = "0.1" base64 = "0.20" dirs = "4.0" -thiserror = "1.0" futures = "0.3" hyper = { version = "0.14", features = ["full"] } hyper-tls = "0.5" +keyring = "2.0" +oauth2 = "4" roxmltree = "0.16" serde = { version = "1.0", features = ["derive"] } serde_derive = "1.0" serde_json = "1.0" +thiserror = "1.0" tokio = { version = "1", features = ["full"] } [profile.dev] diff --git a/README.md b/README.md index fc1ebd3..8b26577 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,13 @@ ### For i3status-rust -I use i3 wm and want to have a notification for new emails in i3 status line. +I use i3/Sway wm and want to have a notification for new emails in i3 status line. ### Preparation -You need to have a fresh version of **rust** and **cargo** +You need to have a fresh version of **rust** and **cargo**. -Clone this repository and build release version +Clone this repository and build release version. ``` git clone https://github.com/Crandel/rust_gmail_checker.git @@ -18,31 +18,30 @@ cd rust_gmail_checker cargo build --release ``` -After building you could move the binary file inside your **PATH** +After building you could move the binary file inside your **PATH**. ``` mv target/release/rust_gmail ~/.local/bin/rust_gmail ``` -During first run it will fail and create **.email.json** file with this structure +During first run it will fail and create **.email.json** file with this structure. + +```json``` +$ rust_gmail +A:0 +``` -```json [ { "mail_type": "gmail", "account": "account_name", - "short_conky":"A", - "email": "@gmail.com", - "password": "" + "short_alias":"A", + "client_id": "", + "client_secret": "" } ] ``` -Just edit this file and you will get this result - -``` -$ rust_gmail -A:0 -``` - -You could use several gmail accounts to have a personal and work notifications +Just edit this file. +You could use several gmail accounts to have a personal and work notifications. +Unread count is available as `dbus` message. diff --git a/src/accessor.rs b/src/accessor.rs new file mode 100644 index 0000000..38a6e5f --- /dev/null +++ b/src/accessor.rs @@ -0,0 +1,10 @@ +use crate::client::InternalError; + +pub trait TokenAccessor { + /// Return token from the source. + /// + /// # Errors + /// + /// This function will return an error if it is not possible to get the token. + fn get_token(&self, client_id: String, client_secret: String) -> Result; +} diff --git a/src/accounts.rs b/src/accounts.rs index 1a1c752..1d9a162 100644 --- a/src/accounts.rs +++ b/src/accounts.rs @@ -4,9 +4,10 @@ use serde::{Deserialize, Serialize}; pub struct Account { mail_type: EmailType, account: String, - short_conky: String, - email: String, - password: String, + short_alias: String, + client_id: String, + client_secret: Option, + refresh_token: Option, } impl Account { @@ -14,34 +15,61 @@ impl Account { pub fn new( mail_type: EmailType, account: String, - short_conky: String, - email: String, - password: String, - ) -> Account { + short_alias: String, + client_id: String, + ) -> Self { + let secret: Option = None; + let token: Option = None; Account { mail_type, account, - short_conky, - email, - password, + short_alias, + client_id, + client_secret: secret, + refresh_token: token, } } // public getter for email - pub fn get_email(&self) -> &str { - &self.email + pub fn get_client_id(&self) -> &str { + &self.client_id } - // public getter for password - pub fn get_password(&self) -> &str { - &self.password + // public getter for client secret + pub fn get_client_secret(&self) -> Option { + if let Some(secret) = self.client_secret.as_deref() { + return Some(String::from(secret)); + } + return None; + } + + // public setter for client secret + pub fn set_client_secret(&mut self, secret: String) { + if secret != "" { + self.client_secret = Some(secret) + } } - // public getter for short_conky value + // public getter for refresh token + pub fn get_refresh_token(&self) -> Option { + if let Some(token) = self.refresh_token.as_deref() { + return Some(String::from(token)); + } + return None; + } + + // public setter for refresh token + pub fn set_refresh_token(&mut self, token: String) { + if token != "" { + self.refresh_token = Some(token) + } + } + + // public getter for short alias value pub fn get_short(&self) -> &str { - &self.short_conky + &self.short_alias } - // public getter for short_conky value + // public getter for mail type value pub fn get_mail_type(&self) -> EmailType { self.mail_type } @@ -62,20 +90,28 @@ mod tests { fn acc_test() { let mail_type = EmailType::Gmail; let name = "test_name"; - let email = "test_email"; - let password = "test_password"; + let client_id = "test_id"; let short = "test_short"; - let acc = Account::new( + let def_client_secret: Option = None; + let mut acc = Account::new( mail_type, String::from(name), String::from(short), - String::from(email), - String::from(password), + String::from(client_id), ); assert_eq!(mail_type, acc.get_mail_type()); - assert_eq!(email, acc.get_email()); - assert_eq!(password, acc.get_password()); + assert_eq!(client_id, acc.get_client_id()); + assert_eq!(def_client_secret, acc.get_client_secret()); assert_eq!(short, acc.get_short()); + + let client_secret_str = "test secret"; + let client_secret: Option = Some(String::from(client_secret_str)); + let refresh_token_str = "test secret"; + let refresh_token: Option = Some(String::from(refresh_token_str)); + acc.set_client_secret(String::from(client_secret_str)); + acc.set_refresh_token(String::from(refresh_token_str)); + assert_eq!(client_secret, acc.get_client_secret()); + assert_eq!(refresh_token, acc.get_refresh_token()); } } diff --git a/src/client.rs b/src/client.rs index 46f1df3..ad571ff 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2,11 +2,13 @@ use thiserror::Error; /// Custom errors that may happen during calls #[derive(Error, Debug)] -pub enum WebClientError { - #[error("Hyper error: {:?}", _0)] - HyperError(hyper::Error), +pub enum InternalError { + #[error("Client error: {:?}", _0)] + ClientError(hyper::Error), #[error("Parsing error: {:?}", _0)] ParsingError(String), #[error("Connection error: {:?}", _0)] ConnectionError(String), + #[error("Token error: {:?}", _0)] + TokenError(String), } diff --git a/src/config.rs b/src/config.rs index bd44125..25884e4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -18,8 +18,7 @@ pub fn create_example() -> Result<(), ConfigError> { EmailType::Gmail, String::from("username"), String::from("Short"), - String::from("email"), - String::from("password"), + String::from("client_id"), ); let def_vec_acc = vec![acc]; let ex_acc_s: String = match serde_json::to_string(&def_vec_acc) { diff --git a/src/lib.rs b/src/lib.rs index 41fb19f..38ad269 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,7 @@ +pub mod accessor; pub mod accounts; pub mod client; pub mod config; pub mod provider; pub mod providers; +pub mod storage; diff --git a/src/main.rs b/src/main.rs index 48817e0..07c845e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,32 +2,48 @@ use futures::stream::futures_unordered::FuturesUnordered; use futures::stream::StreamExt; use hyper::{client::HttpConnector, Body, Client}; use hyper_tls::HttpsConnector; +use mail_lib::storage; use mail_lib::{ - accounts::Account, client::WebClientError, config, provider::MailProvider, + accounts::Account, client::InternalError, config, provider::MailProvider, providers::gmail::GmailProvider, }; use std::env::args; use std::process::exit; -async fn process_accs( - accs: Vec, - client: &Client, Body>, - provider: T, -) -> Vec> { - accs.iter() - .map(|a| provider.get_mail_metadata(a, client)) - .collect::>() - .collect::>() - .await +// async fn process_accs( +// accs: Vec, +// client: &Client, Body>, +// provider: T, +// ) -> Vec> { +// accs.iter() +// .map(|a| provider.get_mail_metadata(a, client)) +// .collect::>() +// .collect::>() +// .await +// } + +fn set_entry_secret(a: &Account) -> bool { + let id = a.get_client_id(); + let secret_id = format!("{}_{}", id, "secret"); + if let Ok(_) = storage::get_entry(secret_id.clone()) { + return true; + } else { + return storage::set_entry(secret_id, String::from("secret")); + } } fn print_help() { println!( - "-h - help message\n ---help - help message\n ---init - create sample config file\n + r#"-h - help message + + + +--help - help message + +--init - create sample config file + -s - new line separator between accounts -" +"# ); exit(0) } @@ -64,23 +80,26 @@ async fn main() { } }; - let https = HttpsConnector::new(); - let client = Client::builder().build::<_, hyper::Body>(https); - let provider = GmailProvider::new(); + let results: Vec = accs.iter().map(|a| set_entry_secret(a)).collect(); - let resp_vec = process_accs(accs, &client, provider).await; - let (responses, errors) = - resp_vec - .into_iter() - .fold((Vec::new(), Vec::new()), |(mut strs, mut errs), current| { - match current { - Ok(s) => strs.push(s), - Err(e) => errs.push(e), - } - (strs, errs) - }); - for error in errors { - eprintln!("{}", error); - } - println!("{}", responses.join(&separator)); + println!("{:?}", results) + // let https = HttpsConnector::new(); + // let client = Client::builder().build::<_, hyper::Body>(https); + // let provider = GmailProvider::new(); }; + + // let resp_vec = process_accs(accs, &client, provider).await; + // let (responses, errors) = + // resp_vec + // .into_iter() + // .fold((Vec::new(), Vec::new()), |(mut strs, mut errs), current| { + // match current { + // Ok(s) => strs.push(s), + // Err(e) => errs.push(e), + // } + // (strs, errs) + // }); + // for error in errors { + // eprintln!("{}", error); + // } + // println!("{}", responses.join(&separator)); } diff --git a/src/provider.rs b/src/provider.rs index dd86123..d729fe4 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -1,4 +1,4 @@ -use crate::{accounts::Account, client::WebClientError}; +use crate::{accounts::Account, client::InternalError}; use async_trait::async_trait; use hyper::{client::HttpConnector, Body, Client}; use hyper_tls::HttpsConnector; @@ -9,6 +9,6 @@ pub trait MailProvider { &self, acc: &Account, client: &Client, Body>, - ) -> Result; - fn parse_body(body: String) -> Result; + ) -> Result; + fn parse_body(body: String) -> Result; } diff --git a/src/providers/gmail.rs b/src/providers/gmail.rs index 19cfd0a..bcf8a24 100644 --- a/src/providers/gmail.rs +++ b/src/providers/gmail.rs @@ -1,7 +1,8 @@ use crate::{ + accessor::TokenAccessor, accounts::Account, - client::WebClientError, - client::WebClientError::{ConnectionError, ParsingError}, + client::InternalError, + client::InternalError::{ConnectionError, ParsingError}, provider::MailProvider, }; pub(crate) use async_trait::async_trait; @@ -12,11 +13,12 @@ use hyper::{ }; use hyper_tls::HttpsConnector; -use base64; use roxmltree::Document; pub struct GmailProvider { - url: String, + feed_url: String, + auth_url: String, + token_url: String, } impl Default for GmailProvider { fn default() -> Self { @@ -26,20 +28,29 @@ impl Default for GmailProvider { impl GmailProvider { pub fn new() -> GmailProvider { - let url = String::from("https://mail.google.com/mail/feed/atom"); - GmailProvider { url } + let feed_url = String::from("https://mail.google.com/mail/feed/atom"); + let auth_url = String::from("https://mail.google.com/mail/feed/atom"); + let token_url = String::from("https://mail.google.com/mail/feed/atom"); + GmailProvider { + feed_url, + auth_url, + token_url, + } } - fn get_request(&self, acc: &Account) -> Result, WebClientError> { - let user_data: String = format!("{}:{}", acc.get_email(), acc.get_password()); - let b64: String = base64::encode(user_data.as_bytes()); - let auth_str: String = format!("Basic {}", b64); + fn get_request(&self, acc: &Account) -> Result, InternalError> { + let Some(client_secret) = acc.get_client_secret() else { + return Err(InternalError::TokenError(String::from("secret err"))); + }; + let Ok(auth_token) = self.get_token(String::from(acc.get_client_id()), client_secret) else { + return Err(InternalError::TokenError(String::from("token err"))); + }; - let value: HeaderValue = HeaderValue::from_str(&auth_str).unwrap(); + let value: HeaderValue = HeaderValue::from_str(&auth_token).unwrap(); // Await the response... Request::builder() .method(Method::GET) - .uri(self.url.to_string()) + .uri(self.feed_url.to_string()) .header(AUTHORIZATION, value) .body(Body::empty()) .map_err(|e| ConnectionError(e.to_string())) @@ -52,7 +63,7 @@ impl MailProvider for GmailProvider { &self, acc: &Account, client: &Client, Body>, - ) -> Result { + ) -> Result { // Parse an `http::Uri`... let request = self.get_request(acc); @@ -63,13 +74,13 @@ impl MailProvider for GmailProvider { .map_err(|err| ConnectionError(err.to_string())), Err(e) => Err(e), }; - let bytes_res: Result = match resp { + let bytes_res: Result = match resp { Ok(rsp) => hyper::body::to_bytes(rsp.into_body()) .await .map_err(|er| ConnectionError(er.to_string())), Err(e) => Err(e), }; - let body_res: Result = match bytes_res { + let body_res: Result = match bytes_res { Ok(bytes) => std::str::from_utf8(&bytes) .map(|by| by.to_string()) .map_err(|er| ParsingError(er.to_string())), @@ -80,7 +91,7 @@ impl MailProvider for GmailProvider { }) } - fn parse_body(body: String) -> Result { + fn parse_body(body: String) -> Result { match Document::parse(body.as_str()) { Ok(doc) => match doc.descendants().find(|n| n.has_tag_name("fullcount")) { Some(fc) => match fc.text() { @@ -93,3 +104,10 @@ impl MailProvider for GmailProvider { } } } + +impl TokenAccessor for GmailProvider { + fn get_token(&self, client_id: String, client_secret: String) -> Result { + let token = String::from(""); + return Ok(token); + } +} diff --git a/src/storage.rs b/src/storage.rs new file mode 100644 index 0000000..13c4d0b --- /dev/null +++ b/src/storage.rs @@ -0,0 +1,24 @@ +use keyring::{Entry, Result}; + +const SERVICE_NAME: &str = "gmail_checker"; + +pub fn get_entry(key: String) -> Result { + let entry = Entry::new(SERVICE_NAME, key.as_str())?; + let password = entry.get_password(); + + return password; +} + +pub fn set_entry(key: String, data: String) -> bool { + let result_entry = Entry::new(SERVICE_NAME, key.as_str()); + match result_entry { + Ok(entry) => { + if let Ok(password) = entry.set_password(data.as_str()) { + return true; + }; + } + Err(err) => println!("{:?}", err), + }; + + return false; +}