소스 검색

idle imap fix

Yurii Sokolovskyi 3 달 전
부모
커밋
9a92c794e6
6개의 변경된 파일257개의 추가작업 그리고 1068개의 파일을 삭제
  1. 6 3
      Cargo.toml
  2. 218 287
      src/imap.rs
  3. 0 242
      src/imap/imap_client.rs
  4. 33 20
      src/main.rs
  5. 0 502
      src/style.css
  6. 0 14
      src/templates/rss.url

+ 6 - 3
Cargo.toml

@@ -2,7 +2,7 @@
 name = "crabmail"
 version = "0.1.0"
 authors = ["alex wennerberg <alex@alexwennerberg.com>"]
-edition = "2018"
+edition = "2021"
 
 [features]
 default = []
@@ -35,6 +35,7 @@ base64 = "0.22.1"
 lettre = {version = "0.11.7", default-features = false, features = ["builder"]}
 tar = "0.4"
 flate2 = "1.0"
+futures = "0.3.30"
 
 [target.'cfg(target_os = "wasi")'.dependencies]
 tokio_wasi = {version = "1.25.2", features = ["full", "rt", "rt-multi-thread", "macros", "time"] }
@@ -42,12 +43,14 @@ tokio-rustls-wasi = "0.25.0-alpha"
 httpcodec = "0.2.3"
 bytecodec = "0.4.15"
 wasmedge_wasi_socket = "0.5.4"
+async-imap-wasi = {path = "./async-imap-wasi/", default-features = false, features = ["runtime-tokio"]}
 
 [target.'cfg(not(target_os = "wasi"))'.dependencies]
 maildir = "0.6.4"
-tokio = { version = "1", features = ["full"] }
+tokio = { version = "1.39.2", features = ["full"] }
 native-tls = "0.2.11"
-imap = "2.4.1"
+async-imap = {version =  "0.9.7" , default-features = false, features = ["runtime-tokio"]}
 axum = "0.7.5"
 tower-http = {version = "0.5.2", features = ["cors"] }
+tokio-rustls = "0.26.0"
 

+ 218 - 287
src/imap.rs

@@ -1,76 +1,69 @@
 use std::collections::{HashMap, HashSet};
-use std::error::Error;
 use std::{io};
+use std::convert::TryFrom;
 use std::fs::File;
 use std::future::Future;
-use std::net::{TcpStream};
 use std::path::{PathBuf};
 use std::io::{BufReader, BufWriter, ErrorKind, Read, Write};
+use std::sync::Arc;
 use std::time::Duration;
+use anyhow::anyhow;
+use futures::{StreamExt, TryStreamExt};
 use kuchiki::iter::NodeIterator;
 use serde_json::to_writer;
-use tokio::task::JoinHandle;
+use tokio::net::TcpStream;
+use tokio_rustls::client::TlsStream;
+use tokio_rustls::rustls::{ClientConfig, RootCertStore};
+use tokio_rustls::rustls::pki_types::ServerName;
 use crate::config::Config;
-
-#[cfg(not(target_os = "wasi"))]
-use native_tls::{TlsStream};
-#[cfg(not(target_os = "wasi"))]
-use imap::{Session};
 use crate::{add_email};
-#[cfg(target_os = "wasi")]
-use crate::imap::imap_client::{Client, connect_to_imap_server};
-
-pub(crate) mod imap_client;
+use tokio::task;
+use tokio::time::sleep;
 
 #[cfg(not(target_os = "wasi"))]
-pub async fn download_email_from_imap(maildir_path: PathBuf, imap_domain: String, username: String, password: String) -> Result<Vec<(u32, PathBuf)>, Box<dyn Error>>{
-    let tls = native_tls::TlsConnector::builder().build().unwrap();
-    let client = imap::connect((imap_domain.clone(), Config::global().imap_port.clone() as u16), imap_domain.clone(), &tls).unwrap();
-    let mut imap_session = client
-        .login(username, password)
-        .map_err(|e| e.0)?;
-    let mut stored_paths: Vec<(u32, PathBuf)> = vec![];
-
-    match list_imap_folders(&mut imap_session).await {
-        Ok(folders) => {
-            for folder in folders {
-                println!("Start downloading {}", folder.clone());
-                let new_paths = match fetch_and_store_emails(&mut imap_session, maildir_path.clone(), folder.clone()).await {
-                    Ok(paths) => {
-                        println!("Emails fetched and stored successfully for {}.", folder);
-                        paths
-                    },
-                    Err(err) => {
-                        eprintln!("Error during email fetching and storing: {}", err);
-                        vec![]
-                    }
-                };
-                stored_paths.extend(new_paths);
-            }
-        },
-        Err(e) => eprintln!("Error listing folders: {}", e),
-    }
-
-    // Logout from the IMAP session
-    match imap_session.logout() {
-        Ok(_) => println!("Logged out successfully."),
-        Err(err) => eprintln!("Error logging out: {}", err),
-    }
-
-    Ok(stored_paths)
-}
+use async_imap::{Client, Session};
+#[cfg(not(target_os = "wasi"))]
+use async_imap::extensions::idle::IdleResponse::NewData;
 
 #[cfg(target_os = "wasi")]
-pub async fn download_email_from_imap(maildir_path: PathBuf, imap_domain: String, username: String, password: String) -> Result<Vec<(u32, PathBuf)>, Box<dyn Error>>{
-    let mut client = connect_to_imap_server(imap_domain.clone(), Config::global().imap_port.clone() as u16).await?;
-    client.login(username.clone(), password.clone()).await?;
-    let mut stored_paths: Vec<(u32, PathBuf)> = vec![];
+use async_imap_wasi::{Client, Session};
+#[cfg(target_os = "wasi")]
+use async_imap_wasi::extensions::idle::IdleResponse::NewData;
 
-    match client.list().await {
+pub async fn connect_to_imap() -> anyhow::Result<Client<TlsStream<TcpStream>>>{
+    let mut root_cert_store = RootCertStore::empty();
+    root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
+    let config = ClientConfig::builder()
+        .with_root_certificates(root_cert_store)
+        .with_no_client_auth();
+    let connector = tokio_rustls::TlsConnector::from(Arc::new(config));
+    let dnsname = ServerName::try_from(Config::global().imap_domain.clone()).unwrap();
+
+    let stream = TcpStream::connect(format!("{}:{}", Config::global().imap_domain.clone(), Config::global().imap_port.clone())).await?;
+    let stream = connector.connect(dnsname, stream).await?;
+    
+    #[cfg(not(target_os = "wasi"))]
+    let client = async_imap::Client::new(stream);
+    #[cfg(target_os = "wasi")]
+    let client = async_imap_wasi::Client::new(stream);
+    
+    Ok(client)
+}
+
+pub async fn download_email_from_imap() -> anyhow::Result<Vec<(u32, PathBuf)>>{
+    let mut client = connect_to_imap().await?;
+    let mut session_result = client.login(Config::global().username.clone(), Config::global().password.clone()).await;
+    let mut session = match session_result {
+        Ok(session) => {session}
+        Err(_) => {return Err(anyhow!("Unable to login to IMAP server"))}
+    };
+    
+    let mut stored_paths: Vec<(u32, PathBuf)> = vec![];
+    match list_imap_folders(&mut session).await { 
         Ok(folders) => {
             for folder in folders {
                 println!("Start downloading {}", folder.clone());
-                let new_paths = match fetch_and_store_emails(&mut client, maildir_path.clone(), folder.clone()).await {
+                let new_paths = match fetch_and_store_emails(&mut session, folder.clone()).await {
                     Ok(paths) => {
                         println!("Emails fetched and stored successfully for {}.", folder);
                         paths
@@ -82,43 +75,47 @@ pub async fn download_email_from_imap(maildir_path: PathBuf, imap_domain: String
                 };
                 stored_paths.extend(new_paths);
             }
-        },
-        Err(e) => eprintln!("Error listing folders: {}", e),
+        } 
+        Err(_) => {return Err(anyhow!("Unable to retrieve folders from IMAP server"))}
     }
     
     // Logout from the IMAP session
-    client.logout().await.expect("Unable to logout");
+    session.logout().await.expect("Unable to logout");
 
     Ok(stored_paths)
 }
 
-#[cfg(not(target_os = "wasi"))]
-pub async fn list_imap_folders(imap_session: &mut Session<TlsStream<TcpStream>>) -> imap::error::Result<Vec<String>> {
-    // The function return all available folders(lists) on the IMAP server
-    // Fetch the list of mailboxes (folders)
-    let mailboxes = imap_session.list(None, Some("*"))?;
-    let folder_names = mailboxes.iter().map(|mailbox| mailbox.name().to_string()).collect();
-
-    Ok(folder_names)
+pub async fn list_imap_folders(session: &mut Session<TlsStream<TcpStream>>) -> anyhow::Result<Vec<String>> {
+    let mut folders: Vec<String> = vec![];
+    let mut folders_stream = session.list(None, Some("*")).await?;
+    while let Some(folder_result) = folders_stream.next().await {
+        match folder_result {
+            Ok(folder) => {
+                folders.push(folder.name().to_string())
+            }
+            Err(_) => {}
+        }
+    }
+    Ok(folders)
 }
 
+pub async fn fetch_and_store_emails(session: &mut Session<TlsStream<TcpStream>>, list: String) -> anyhow::Result<Vec<(u32, PathBuf)>> {
+    session.select(list.clone()).await?;
 
-#[cfg(not(target_os = "wasi"))]
-pub async fn fetch_and_store_emails(imap_session: &mut Session<TlsStream<TcpStream>>, maildir_path: PathBuf, list: String) -> imap::error::Result<Vec<(u32, PathBuf)>> {
-    // the function download all emails from the IMAP in the folder(list)
-    imap_session.select(list.clone())?;
+    // Create directories for maildir
+    std::fs::create_dir_all(Config::global().maildir.clone().join(list.clone()).join("new"))
+        .expect(&*("Unable to create 'new' directory in ".to_owned() + &*list.clone()));
+    std::fs::create_dir_all(Config::global().maildir.clone().join(list.clone()).join("cur"))
+        .expect(&*("Unable to create 'cur' directory in ".to_owned() + &*list.clone()));
+    std::fs::create_dir_all(Config::global().maildir.clone().join(list.clone()).join("tmp"))
+        .expect(&*("Unable to create 'tmp' directory in ".to_owned() + &*list.clone()));
 
-    // creating files for maildir
-    std::fs::create_dir_all(maildir_path.clone().join(list.clone()).join("new")).expect(&*("Unable to create 'new' directory in ".to_owned() + &*list.clone()));
-    std::fs::create_dir_all(maildir_path.clone().join(list.clone()).join("cur")).expect(&*("Unable to create 'new' directory in ".to_owned() + &*list.clone()));
-    std::fs::create_dir_all(maildir_path.clone().join(list.clone()).join("tmp")).expect(&*("Unable to create 'new' directory in ".to_owned() + &*list.clone()));
+    let uids_path = Config::global().maildir.clone().join(".uids.json");
 
-    let uids_path = maildir_path.clone().join(".uids.json");
-
-    let uids_server = imap_session.uid_search("ALL").unwrap();
+    let uids_server = session.uid_search("ALL").await.unwrap();
     let uids_local = read_local_uids(uids_path.clone()).unwrap();
     let mut stored_paths: Vec<(u32, PathBuf)> = vec![];
-    
+
     for uid in uids_server {
         if let Some(uids) = uids_local.get(&list) {
             if uids.contains(&uid.to_string()) {
@@ -126,77 +123,45 @@ pub async fn fetch_and_store_emails(imap_session: &mut Session<TlsStream<TcpStre
             }
         }
 
-        let messages = imap_session.uid_fetch(uid.to_string(), "(UID BODY[])")?;
-        let message = messages.iter().next().ok_or(imap::error::Error::Bad("No messages found".to_string()))?;
-
-        if let Some(body) = message.body() {
-            let mail_file = store(maildir_path.clone().join(list.clone()), uid.clone().to_string(), "new".to_string(), body, "");
-            match mail_file {
-                Ok(file) => {stored_paths.push((uid.to_string().parse().unwrap(), file))},
-                Err(e) => eprintln!("Failed to store email: {}", e),
+        let mut messages_stream = session.uid_fetch(uid.to_string(), "(UID BODY[])").await?;
+        let mut message = None;
+        while let Some(message_result) = messages_stream.next().await {
+            match message_result {
+                Ok(message_ok) => {
+                    message = Some(message_ok);
+                }
+                Err(_) => {
+                    return Err(anyhow!("Unable to retrieve message"));
+                }
             }
+        }
+
+        if let Some(msg) = &message {
+            if let Some(body) = msg.body() {
+                let mail_file = store(Config::global().maildir.clone().join(list.clone()), uid.clone().to_string(), "new".to_string(), body, "");
+                match mail_file {
+                    Ok(file) => stored_paths.push((uid.to_string().parse().unwrap(), file)),
+                    Err(e) => eprintln!("Failed to store email: {}", e),
+                }
+            } else {
+                eprintln!("Failed to retrieve email body");
+            }
+
+            if let Some(uid) = msg.uid {
+                update_local_uids(uid.to_string(), list.clone(), uids_path.clone())
+                    .expect("Cannot write uid to .uids.json");
+            }
+
+            println!("Downloaded message with UID {}", uid.to_string());
         } else {
-            eprintln!("Failed to retrieve email body");
+            return Err(anyhow!("No message found with the given UID"));
         }
-
-        if let Some(uid) = message.uid {
-            update_local_uids(uid.to_string(), list.clone(), uids_path.clone()).expect("Cannot write uid to .uids.json");
-        }
-
-        println!("Downloaded message with UID {}", uid.to_string());
     }
 
     Ok(stored_paths)
 }
 
-#[cfg(target_os = "wasi")]
-pub async fn fetch_and_store_emails(client: &mut Client, maildir_path: PathBuf, list: String) -> Result<Vec<(u32, PathBuf)>, Box<dyn std::error::Error>> {
-    // the function download all emails from the IMAP in the folder(list)
-    client.select(list.clone()).await?;
-    
-    // creating files for maildir
-    std::fs::create_dir_all(maildir_path.clone().join(list.clone()).join("new")).expect(&*("Unable to create 'new' directory in ".to_owned() + &*list.clone()));
-    std::fs::create_dir_all(maildir_path.clone().join(list.clone()).join("cur")).expect(&*("Unable to create 'new' directory in ".to_owned() + &*list.clone()));
-    std::fs::create_dir_all(maildir_path.clone().join(list.clone()).join("tmp")).expect(&*("Unable to create 'new' directory in ".to_owned() + &*list.clone()));
-    
-    let uids_path = maildir_path.clone().join(".uids.json");
-    
-    let uids_server = client.uid_search("ALL".to_string()).await.unwrap();
-    let uids_local = read_local_uids(uids_path.clone()).unwrap();
-    let mut stored_paths: Vec<(u32, PathBuf)> = vec![];
-    
-    for uid in uids_server {
-        if let Some(uids) = uids_local.get(&list) {
-            if uids.contains(&uid.to_string()) {
-                continue;
-            }
-        }
-    
-        let message = client.uid_fetch(uid.clone().to_string(), "(UID BODY[])".to_string()).await?;
-        let mut body = message.clone();
-        
-        if let Some(index) = message.find('\n') {
-            body = (&message[index+1..]).to_string();
-        } else {}
-
-
-        let mail_file = store(maildir_path.clone().join(list.clone()), uid.clone().to_string(), "new".to_string(), body.as_bytes(), "");
-        match mail_file {
-            Ok(file) => {stored_paths.push((uid.to_string().parse().unwrap(), file))},
-            Err(e) => eprintln!("Failed to store email: {}", e),
-        }
-
-        update_local_uids(uid.to_string(), list.clone(), uids_path.clone()).expect("Cannot write uid to .uids.txt");
-        
-    
-        println!("Downloaded message with UID {}", uid.to_string());
-    }
-
-    Ok(stored_paths)
-}
-
-
-fn read_local_uids(uids_path: PathBuf) -> io::Result<HashMap<String, HashSet<String>>> {
+fn read_local_uids(uids_path: PathBuf) -> anyhow::Result<HashMap<String, HashSet<String>>> {
     let file = match File::open(&uids_path) {
         Ok(file) => file,
         Err(_) => return Ok(HashMap::new()), // Propagate other errors
@@ -214,7 +179,7 @@ fn read_local_uids(uids_path: PathBuf) -> io::Result<HashMap<String, HashSet<Str
     // Deserialize the JSON into a temporary structure
     let data: HashMap<String, Vec<String>> = match serde_json::from_str(&content) {
         Ok(data) => data,
-        Err(e) => return Err(io::Error::new(io::ErrorKind::InvalidData, e)), // Handle JSON parsing errors
+        Err(e) => return Err(anyhow!("Error deserializing local uids")), // Handle JSON parsing errors
     };
 
     // Convert Vec<String> to HashSet<String>
@@ -226,7 +191,7 @@ fn read_local_uids(uids_path: PathBuf) -> io::Result<HashMap<String, HashSet<Str
     Ok(result)
 }
 
-fn update_local_uids(uid: String, list: String, uids_path: PathBuf) -> io::Result<()> {
+fn update_local_uids(uid: String, list: String, uids_path: PathBuf) -> anyhow::Result<()> {
     let file = std::fs::OpenOptions::new()
         .write(true)
         .create(true)
@@ -252,178 +217,144 @@ fn update_local_uids(uid: String, list: String, uids_path: PathBuf) -> io::Resul
     Ok(())
 }
 
-#[cfg(target_os = "wasi")]
 pub async fn delete_email_by_uid(list: String, uid: u32) -> anyhow::Result<()>{
-    let mut client = connect_to_imap_server(Config::global().imap_domain.clone(), Config::global().imap_port.clone() as u16).await?;
-    client.login(Config::global().username.clone(), Config::global().password.clone()).await?;
-    client.select(list.clone()).await?;
-    client.mark_deleted(uid).await?;
-    client.expunge().await?;
-    client.logout().await?;
-    Ok(())
-}
-
-#[cfg(not(target_os = "wasi"))]
-pub async fn delete_email_by_uid(list: String, uid: u32) -> anyhow::Result<()>{
-    let tls = native_tls::TlsConnector::builder().build().unwrap();
-    let client = imap::connect((Config::global().imap_domain.clone(), Config::global().imap_port.clone() as u16), Config::global().imap_domain.clone(), &tls).unwrap();
-    let mut imap_session = client
-        .login(Config::global().username.clone(), Config::global().password.clone())
-        .map_err(|e| e.0)?;
+    let mut client = connect_to_imap().await?;
+    let mut session_result = client.login(Config::global().username.clone(), Config::global().password.clone()).await;
+    let mut session = match session_result {
+        Ok(session) => {session}
+        Err(_) => {return Err(anyhow!("Unable to login to IMAP server"))}
+    };
     
-    imap_session.select(list)?;
-    imap_session.uid_store(uid.to_string(), "+FLAGS (\\Deleted)")?;
-    imap_session.expunge()?;
-    imap_session.logout()?;
-
+    session.select(list.clone()).await?;
+    // session.mark_deleted(uid).await?;
+    let updates_stream = session.store(format!("{}", uid), "+FLAGS (\\Deleted)").await?;
+    let _updates: Vec<_> = updates_stream.try_collect().await?;
+    let _stream = session.expunge().await?;
+    drop(_stream);
+    session.logout().await?;
     Ok(())
 }
 
-#[cfg(not(target_os = "wasi"))]
 pub async fn create_folder(name: String) -> anyhow::Result<()>{
-    let tls = native_tls::TlsConnector::builder().build().unwrap();
-    let client = imap::connect((Config::global().imap_domain.clone(), Config::global().imap_port.clone() as u16), Config::global().imap_domain.clone(), &tls).unwrap();
-    let mut imap_session = client
-        .login(Config::global().username.clone(), Config::global().password.clone())
-        .map_err(|e| e.0)?;
-
-    imap_session.create(name)?;
-    imap_session.logout()?;
-
+    let mut client = connect_to_imap().await?;
+    let mut session_result = client.login(Config::global().username.clone(), Config::global().password.clone()).await;
+    let mut session = match session_result {
+        Ok(session) => {session}
+        Err(_) => {return Err(anyhow!("Unable to login to IMAP server"))}
+    };
+    session.create(name.clone()).await?;
+    session.logout().await?;
     Ok(())
 }
 
-#[cfg(target_os = "wasi")]
-pub async fn create_folder(name: String) -> anyhow::Result<()>{
-    let mut client = connect_to_imap_server(Config::global().imap_domain.clone(), Config::global().imap_port.clone() as u16).await?;
-    client.login(Config::global().username.clone(), Config::global().password.clone()).await?;
-    client.create_folder(name.clone()).await?;
-    client.logout().await?;
-    Ok(())
-}
-
-#[cfg(not(target_os = "wasi"))]
 pub async fn rename_folder(name: String, new_name: String) -> anyhow::Result<()>{
-    let tls = native_tls::TlsConnector::builder().build().unwrap();
-    let client = imap::connect((Config::global().imap_domain.clone(), Config::global().imap_port.clone() as u16), Config::global().imap_domain.clone(), &tls).unwrap();
-    let mut imap_session = client
-        .login(Config::global().username.clone(), Config::global().password.clone())
-        .map_err(|e| e.0)?;
-
-    imap_session.rename(name, new_name)?;
-    imap_session.logout()?;
-
+    let mut client = connect_to_imap().await?;
+    let mut session_result = client.login(Config::global().username.clone(), Config::global().password.clone()).await;
+    let mut session = match session_result {
+        Ok(session) => {session}
+        Err(_) => {return Err(anyhow!("Unable to login to IMAP server"))}
+    };
+    session.rename(name.clone(), new_name.clone()).await?;
+    session.logout().await?;
     Ok(())
 }
 
-#[cfg(target_os = "wasi")]
-pub async fn rename_folder(name: String, new_name: String) -> anyhow::Result<()>{
-    let mut client = connect_to_imap_server(Config::global().imap_domain.clone(), Config::global().imap_port.clone() as u16).await?;
-    client.login(Config::global().username.clone(), Config::global().password.clone()).await?;
-    client.rename_folder(name.clone(), new_name.clone()).await?;
-    client.logout().await?;
-    Ok(())
-}
-
-#[cfg(not(target_os = "wasi"))]
 pub async fn delete_folder(name: String) -> anyhow::Result<()>{
-    let tls = native_tls::TlsConnector::builder().build().unwrap();
-    let client = imap::connect((Config::global().imap_domain.clone(), Config::global().imap_port.clone() as u16), Config::global().imap_domain.clone(), &tls).unwrap();
-    let mut imap_session = client
-        .login(Config::global().username.clone(), Config::global().password.clone())
-        .map_err(|e| e.0)?;
-
-    imap_session.delete(name)?;
-    imap_session.logout()?;
-
+    let mut client = connect_to_imap().await?;
+    let mut session_result = client.login(Config::global().username.clone(), Config::global().password.clone()).await;
+    let mut session = match session_result {
+        Ok(session) => {session}
+        Err(_) => {return Err(anyhow!("Unable to login to IMAP server"))}
+    };
+    session.delete(name.clone()).await?;
+    session.logout().await?;
     Ok(())
 }
 
-#[cfg(target_os = "wasi")]
-pub async fn delete_folder(name: String) -> anyhow::Result<()>{
-    let mut client = connect_to_imap_server(Config::global().imap_domain.clone(), Config::global().imap_port.clone() as u16).await?;
-    client.login(Config::global().username.clone(), Config::global().password.clone()).await?;
-    client.delete_folder(name.clone()).await?;
-    client.logout().await?;
-    Ok(())
-}
+// #[cfg(target_os = "wasi")]
+// pub async fn check_for_updates(mailbox: String) -> anyhow::Result<()>{
+//     let mut client = connect_to_imap_server(Config::global().imap_domain.clone(), Config::global().imap_port.clone() as u16).await?;
+//     client.login(Config::global().username.clone(), Config::global().password.clone()).await?;
+// 
+//     client.select(mailbox).await?;
+//     let mut id = client.idle().await?;
+// 
+//     println!("Start looking for updates");
+//     loop {
+//         println!("UPDATE loop");
+//         client.read_response_by_keyword("EXISTS".to_string()).await?;
+// 
+//         // TODO do not update all emails (IMAP returns * {number} RECENT) and do it only for one mailbox
+//         let new_paths = download_email_from_imap().await.expect("Cannot download new emails");
+// 
+//         for (uid, path) in new_paths.clone() {
+//             match add_email(path.clone(), uid.clone()){
+//                 Ok(_) => {}
+//                 Err(_) => {println!("Error adding email from {:?}", path.clone())}
+//             };
+//         }
+//     }
+// 
+//     client.idle_done().await;
+//     client.logout().await;
+//     Ok(()) 
+// }
+// 
 
-#[cfg(target_os = "wasi")]
-pub async fn check_for_updates(mailbox: String) -> anyhow::Result<()>{
-    let mut client = connect_to_imap_server(Config::global().imap_domain.clone(), Config::global().imap_port.clone() as u16).await?;
-    client.login(Config::global().username.clone(), Config::global().password.clone()).await?;
+pub async fn check_for_updates(mailbox: String) -> anyhow::Result<()> {
+    task::spawn(async move {
+        let mut client = match connect_to_imap().await {
+            Ok(client) => client,
+            Err(e) => {
+                return Err::<(), anyhow::Error>(anyhow!("Failed to connect to IMAP"));
+            }
+        };
 
-    client.select(mailbox).await?;
-    let mut id = client.idle().await?;
+        let session_result = client
+            .login(Config::global().username.clone(), Config::global().password.clone())
+            .await;
 
-    println!("Start looking for updates");
-    loop {
-        println!("UPDATE loop");
-        client.read_response_by_keyword("EXISTS".to_string()).await?;
+        let mut session = match session_result {
+            Ok(session) => session,
+            Err(_) => return Err(anyhow!("Unable to login to IMAP server")),
+        };
 
-        // TODO do not update all emails (IMAP returns * {number} RECENT) and do it only for one mailbox
-        let new_paths = download_email_from_imap(
-            Config::global().maildir.clone(),
-            Config::global().imap_domain.clone(),
-            Config::global().username.clone(),
-            Config::global().password.clone()
-        ).await.expect("Cannot download new emails");
+        session.select(mailbox.clone()).await?;
 
-        for (uid, path) in new_paths.clone() {
-            match add_email(path.clone(), uid.clone()){
-                Ok(_) => {}
-                Err(_) => {println!("Error adding email from {:?}", path.clone())}
-            };
+        loop {
+            let mut idle = session.idle();
+            idle.init().await.unwrap();
+            let (idle_wait, interrupt) = idle.wait();
+            let idle_handle = task::spawn(async move {
+                println!("IDLE: waiting for 30s");
+                sleep(Duration::from_secs(30)).await;
+                println!("IDLE: waited 30 secs, now interrupting idle");
+                drop(interrupt);
+            });
+
+            match idle_wait.await.unwrap() {
+                NewData(data) => {
+                    let s = String::from_utf8(data.borrow_owner().to_vec()).unwrap();
+                    println!("IDLE data:\n{}", s);
+                    // Process the data as needed here
+                }
+                reason => {
+                    println!("IDLE failed {:?}", reason);
+                }
+            }
+
+            // Ensure the idle handle is dropped before the next loop iteration
+            idle_handle.await.unwrap();
+
+            // Reassign session to prevent ownership issues in the next loop iteration
+            session = idle.done().await.unwrap();
         }
-    }
+    });
 
-    client.idle_done().await;
-    client.logout().await;
-    Ok(()) 
-}
-
-#[cfg(not(target_os = "wasi"))]
-pub async fn check_for_updates(mailbox: String) -> anyhow::Result<()>{
-    let tls = native_tls::TlsConnector::builder().build().unwrap();
-    let client = imap::connect((Config::global().imap_domain.clone(), Config::global().imap_port.clone() as u16), Config::global().imap_domain.clone(), &tls).unwrap();
-    let mut imap_session = client
-        .login(Config::global().username.clone(), Config::global().password.clone())
-        .map_err(|e| e.0)?;
-
-    imap_session.select(mailbox)?;
-
-    println!("Start looking for updates");
-    loop {
-        let idle = imap_session.idle()?;
-        idle.wait()?;
-
-        // TODO do not update all emails (IMAP returns * {number} RECENT) and do it only for one mailbox
-        let new_paths = download_email_from_imap(
-            Config::global().maildir.clone(),
-            Config::global().imap_domain.clone(),
-            Config::global().username.clone(),
-            Config::global().password.clone()
-        ).await.expect("Cannot download new emails");
-
-        for (uid, path) in new_paths.clone() {
-            match add_email(path.clone(), uid.clone()){
-                Ok(_) => {}
-                Err(_) => {println!("Error adding email from {:?}", path.clone())}
-            };
-        }
-    }
-
-    imap_session.logout()?;
     Ok(())
 }
 
-fn store(
-    path: PathBuf,
-    uid: String,
-    subfolder: String,
-    data: &[u8],
-    info: &str,
-) -> io::Result<PathBuf> {
+fn store(path: PathBuf, uid: String, subfolder: String, data: &[u8], info: &str) -> io::Result<PathBuf> {
     // loop when conflicting filenames occur, as described at
     // http://www.courier-mta.org/maildir.html
     // this assumes that pid and hostname don't change.

+ 0 - 242
src/imap/imap_client.rs

@@ -1,242 +0,0 @@
-#![cfg(target_os = "wasi")]
-use std::sync::{Arc, Mutex};
-use regex::Regex;
-use rustls_pki_types::ServerName;
-use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, ReadHalf, WriteHalf};
-use tokio::net::TcpStream;
-use tokio_rustls::rustls::{ClientConfig, RootCertStore};
-use std::convert::TryFrom;
-use std::time::Duration;
-use anyhow::anyhow;
-use imap_proto::{self, MailboxDatum};
-use imap_proto::parser::parse_response;
-use tokio::time::{sleep, timeout};
-
-pub struct Client{
-    reader: Arc<tokio::sync::Mutex<BufReader<ReadHalf<tokio_rustls::client::TlsStream<TcpStream>>>>>,
-    writer: Arc<tokio::sync::Mutex<WriteHalf<tokio_rustls::client::TlsStream<TcpStream>>>>
-}
-
-impl Client {
-    pub fn new(stream: tokio_rustls::client::TlsStream<tokio::net::TcpStream>) -> Client {
-        let (reader, writer) = tokio::io::split(stream);
-        let reader = Arc::new(tokio::sync::Mutex::new(BufReader::new(reader)));
-        let writer = Arc::new(tokio::sync::Mutex::new(writer));
-        
-        Client {
-            reader,
-            writer,
-        }
-    }
-
-    async fn run_command(&mut self, command: String) -> anyhow::Result<()>{
-        let writer_clone = Arc::clone(&self.writer);
-        let mut writer = writer_clone.lock().await;
-        
-        (*writer).write_all((command + "\r\n").as_bytes()).await?;
-        (*writer).flush().await?;
-        drop(writer);
-
-        Ok(())
-    }
-
-    pub async fn read_response_by_id(&mut self, id: String) -> anyhow::Result<String>{
-        let mut response = String::new();
-        let mut temp = String::new();
-        let reader_clone = Arc::clone(&self.reader);
-        let mut reader = reader_clone.lock().await;
-
-        while let Ok(bytes) = (*reader).read_line(&mut temp).await {
-            println!("Temp: {:?}", temp);
-            if bytes == 0 || temp.contains(&*id.to_string()) { // if bytes == 0 || temp.contains(&*format!("{} OK", id))
-                break;
-            }
-            response += &*temp;
-            temp.clear();
-        }
-        drop(reader);
-
-        Ok(response)
-    }
-
-    pub async fn read_response_by_keyword(&self, key: String) -> anyhow::Result<String> {
-        // Clone the Arc to move into the async task
-        let reader_clone = Arc::clone(&self.reader);
-
-        let handler = tokio::spawn(async move {
-            let mut response = String::new();
-            let mut temp = String::new();
-            loop {
-                // Lock the reader for access
-                let mut reader = reader_clone.lock().await;
-
-                // Use tokio::select! for concurrent operations
-                let result = timeout(Duration::from_secs(2), reader.read_line(&mut temp)).await;
-                match result {
-                    Ok(Ok(bytes)) => {
-                        println!("Temp: {:?}", temp);
-                        if bytes == 0 || temp.contains(&*key) {
-                            return Ok(response)
-                        }
-                        response += &*temp;
-                        temp.clear();
-                    }
-                    Ok(Err(e)) => {
-                        eprintln!("Error reading line: {:?}", e);
-                        return Err(anyhow!("Error reading line from IMAP server"));
-                    }
-                    _ => {
-                        sleep(Duration::from_secs(1)).await;
-                        tokio::task::yield_now().await;
-                    }
-                }
-                
-                drop(reader);
-            }
-        });
-
-        // Wait for the handler to complete and return the result
-        handler.await.unwrap_or_else(|e| Err(anyhow!("Task panicked: {:?}", e)))
-    }
-
-    pub async fn login(&mut self, username: String, password: String) -> anyhow::Result<String>{
-        let id = "aLogin".to_string();
-        self.run_command(format!("{} LOGIN {} {}", id, username, password)).await?;
-
-        Ok(self.read_response_by_id(id).await?)
-    }
-
-    pub async fn logout(&mut self) -> anyhow::Result<()>{
-        self.run_command("LOGOUT".to_string()).await.expect("Unable to run a command");
-
-        Ok(())
-    }
-
-    pub async fn list(&mut self) -> anyhow::Result<Vec<String>>{
-        let list_command = "aList LIST \"\" \"*\"";
-        self.run_command(list_command.to_string()).await.expect("Unable to run a command");
-        let lists = self.read_response_by_id("aList".to_string()).await?;
-        let mut lines = lists.as_bytes();
-        
-        let mut things = Vec::new();
-        loop {
-            if lines.is_empty() {
-                break;
-            }
-
-            match parse_response(lines) {
-                Ok((rest, resp)) => {
-                    lines = rest;
-                    things.push(resp)
-                }
-                _ => {
-                    break;
-                }
-            }
-        }
-        
-        Ok(things.into_iter()
-            .filter_map(|datum| {
-                if let imap_proto::Response::MailboxData(MailboxDatum::List { name, .. }) = datum {
-                    Some(name.to_string())
-                } else {
-                    None
-                }
-            })
-            .collect::<Vec<String>>())
-    }
-
-    pub async fn select(&mut self, list: String) -> anyhow::Result<String>{
-        let id = "aSelect".to_string();
-        self.run_command(format!("{} SELECT {}", id, list)).await.expect("Unable to run a command");
-
-        self.read_response_by_id(id).await
-    }
-
-
-    pub async fn uid_search(&mut self, query: String) -> anyhow::Result<Vec<String>> {
-        let id = "aUidSearch".to_string(); // Tag for the command
-        self.run_command(format!("{} UID SEARCH {}", id, query)).await.expect("Unable to run a command");
-
-        Ok(Self::uids_parse(self.read_response_by_id(id).await?).unwrap())
-    }
-
-    fn uids_parse(uids: String) -> anyhow::Result<Vec<String>>{
-        let re = Regex::new(r"\b\d+\b")?;
-        let uids = re.find_iter(&*uids)
-            .filter_map(|mat| mat.as_str().parse::<String>().ok())
-            .collect::<Vec<String>>();
-
-        Ok(uids)
-    }
-
-    pub async fn uid_fetch(&mut self, uid: String, query: String) -> anyhow::Result<String>{
-        let id = "aUidFetch".to_string();
-        self.run_command(format!("{} UID FETCH {} {}", id, uid, query)).await.expect("Unable to run a command");
-
-        Ok(self.read_response_by_id(id).await?)
-    }
-    
-    pub async fn mark_deleted(&mut self, uid: u32) -> anyhow::Result<()>{
-        let id = "aMarkDeleted".to_string();
-        self.run_command(format!("{} UID STORE {} +FLAGS (\\Deleted)", id, uid)).await.unwrap();
-        Ok(())
-    }
-    
-    pub async fn expunge(&mut self) -> anyhow::Result<()>{
-        let id = "aExpunge".to_string();
-        self.run_command(format!("{} EXPUNGE", id)).await.unwrap();
-        Ok(())
-    }
-    
-    pub async fn create_folder(&mut self, name: String) -> anyhow::Result<()>{
-        if name.is_empty() { return Err(anyhow!("Folder name is empty")) }
-        let id = "aCreateFolder".to_string();
-        self.run_command(format!("{} CREATE \"{}\"", id, name)).await.unwrap();
-        Ok(())
-    }
-
-    pub async fn delete_folder(&mut self, name: String) -> anyhow::Result<()>{
-        if name.is_empty() { return Err(anyhow!("Folder name is empty")) }
-
-        let id = "aDeleteFolder".to_string();
-        self.run_command(format!("{} DELETE \"{}\"", id, name)).await.unwrap();
-        Ok(())
-    }
-
-    pub async fn rename_folder(&mut self, name: String, new_name: String) -> anyhow::Result<()>{
-        if name.is_empty() { return Err(anyhow!("Folder name is empty")) }
-
-        let id = "aRenameFolder".to_string();
-        self.run_command(format!("{} RENAME \"{}\" \"{}\"", id, name, new_name)).await.unwrap();
-        Ok(())
-    }
-    
-    pub async fn idle(&mut self) -> anyhow::Result<String>{
-        let id = "aIdle".to_string();
-        self.run_command(format!("{} IDLE", id)).await.unwrap();
-        Ok(id)
-    }
-
-    pub async fn idle_done(&mut self) -> anyhow::Result<String>{
-        let id = "aIdleDone".to_string();
-        self.run_command(format!("{} DONE", id)).await.unwrap();
-        Ok(id)
-    }
-}
-
-pub async fn connect_to_imap_server(domain: String, port: u16) -> anyhow::Result<Client> {
-    let mut root_cert_store = RootCertStore::empty();
-    root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
-    let config = ClientConfig::builder()
-        .with_root_certificates(root_cert_store)
-        .with_no_client_auth();
-    let connector = tokio_rustls::TlsConnector::from(Arc::new(config));
-    let dnsname = ServerName::try_from(domain.clone()).unwrap();
-
-    // Connect to the IMAP server over SSL
-    let stream = tokio::net::TcpStream::connect(&(domain, port)).await?;
-    let stream = connector.connect(dnsname, stream).await?;
-
-    Ok(Client::new(stream))
-}

+ 33 - 20
src/main.rs

@@ -9,7 +9,7 @@ use std::fs::{create_dir_all, File, OpenOptions};
 use std::future::Future;
 use std::io::{BufReader, Write};
 use std::net::SocketAddr;
-use std::time::Duration;
+use std::time::{Duration, Instant};
 use anyhow::anyhow;
 use serde::{Deserialize, Serialize};
 use crate::indexes::{Indexes, SerializableMessage, SerializableThread};
@@ -19,8 +19,9 @@ use mailparse::parse_mail;
 use regex::Regex;
 use crate::templates::util::parse_email;
 use crate::util::{compress_and_save_file, read_and_decompress_file};
-use crate::imap::{delete_folder, rename_folder, create_folder};
+use crate::imap::{delete_folder, rename_folder, create_folder, check_for_updates};
 use crate::js::email_scripts;
+use tokio::runtime::Runtime;
 
 #[cfg(not(target_os = "wasi"))]
 use axum::extract::Query;
@@ -41,7 +42,8 @@ use bytecodec::DecodeExt;
 use httpcodec::{HttpVersion, ReasonPhrase, Request, RequestDecoder, Response, StatusCode, Header};
 #[cfg(target_os = "wasi")]
 use lettre::message::{Mailbox, Message as LettreMessage, MultiPart, SinglePart};
-use tokio::runtime::Runtime;
+use tokio::task;
+use tokio::time::sleep;
 #[cfg(target_os = "wasi")]
 use wasmedge_wasi_socket::{Shutdown, TcpListener, TcpStream};
 #[cfg(target_os = "wasi")]
@@ -326,12 +328,7 @@ async fn run(){
     parse_args();
 
     // downloading new emails
-    let new_paths = imap::download_email_from_imap(
-        Config::global().maildir.clone(), // tODO remove all args
-        Config::global().imap_domain.clone(),
-        Config::global().username.clone(),
-        Config::global().password.clone()
-    ).await.expect("Cannot download new emails");
+    let new_paths = imap::download_email_from_imap().await.expect("Cannot download new emails");
 
     
     let mut lists: Vec<String> = Vec::new();
@@ -374,6 +371,8 @@ async fn run(){
 #[cfg(target_os = "wasi")]
 #[tokio::main(flavor = "current_thread")]
 async fn main() -> anyhow::Result<()> {
+    let start = Instant::now();
+    
     run().await;
 
 
@@ -404,17 +403,23 @@ async fn main() -> anyhow::Result<()> {
     
     
     // looking for updates
-    // let one = tokio::spawn(async {
-    //     let imap_update_handler = imap::check_for_updates("INBOX".to_string()).await;
-    // });
+    let imap_update_handler = imap::check_for_updates("INBOX".to_string()).await;
+    
+    
+    let duration = start.elapsed();
+    println!("Duration {:?}", duration);
+    
     
     // API
-    
     println!("Server is running on {}", Config::global().api_port);
     let listener = TcpListener::bind(format!("{}:{}", Config::global().api_addr, Config::global().api_port), false)?;
     loop {
         let (stream, _) = listener.accept(false)?;
-        let _ = handle_client(stream).await;
+        task::spawn(async move {
+            if let Err(e) = handle_client(stream).await {
+                eprintln!("Error handling client: {:?}", e);
+            }
+        });
     }
     
     Ok(())
@@ -423,6 +428,8 @@ async fn main() -> anyhow::Result<()> {
 #[cfg(not(target_os = "wasi"))]
 #[tokio::main]
 async fn main() {
+    let start = Instant::now();
+    
     run().await;
     
     
@@ -450,14 +457,20 @@ async fn main() {
 
     // looking for updates
     // let imap_update_handle = tokio::spawn(async move {
-    //     imap::check_for_updates("INBOX".to_string()).await.expect("panic message");
+    //     if let Err(e) = check_for_updates("INBOX".to_string()).await {
+    //         eprintln!("Failed to monitor mailbox: {:?}", e);
+    //     }
     // });
-    
+    if let Err(e) = check_for_updates("INBOX".to_string()).await {
+        eprintln!("Failed to monitor mailbox: {:?}", e);
+    }
 
     // imap::create_folder("testFolder".to_string()).await.unwrap();
     // imap::rename_folder("testFolder".to_string(), "newTestFolder".to_string()).await.unwrap();
     // imap::delete_folder("newTestFolder".to_string()).await.unwrap();
-    
+
+    let duration = start.elapsed();
+    println!("Duration {:?}", duration);
     
     // API
     // Define the CORS layer
@@ -466,7 +479,7 @@ async fn main() {
             .allow_origin(Any)
             .allow_methods(vec![Method::GET, Method::POST])
             .allow_headers(Any);
-
+    
         let app = Router::new()
             .route("/folders", get(get_folders_handle))
             .route("/sorted_threads_by_date", get(sorted_threads_by_date_handle))
@@ -476,7 +489,7 @@ async fn main() {
             .route("/rename_folder", post(rename_folder_handle))
             .route("/delete_folder", post(delete_folder_handle))
             .layer(cors);
-
+    
         
         
         let rt = Runtime::new().unwrap();
@@ -487,7 +500,7 @@ async fn main() {
             axum::serve(listener, app).await.unwrap();
         });
     });
-
+    
     handle.join().unwrap();
 }
 

+ 0 - 502
src/style.css

@@ -1,502 +0,0 @@
-:root {
-    --link: #00F;
-    --main-text: black;
-    --light-text: dimgrey;
-    --light-hr:grey;
-    --emph-background: #EEE;
-}
-
-body {
-    padding: 2ch;
-    padding-left: 4ch;
-    padding-right: 4ch;
-    margin: 0 auto; !important;
-    font-family: "Roboto", Helvetica, Arial, Sans-serif;
-    word-wrap: break-word;
-    line-height: 1.5;
-    max-width: 80ch;
-    padding: 1ch;
-    color: var(--main-text);
-}
-
-#content .email-body{
-    display: none;
-}
-
-body > hr { margin-left: -1ch; }
-
-table { border-spacing: 0.5em 0.1em; }
-
-.message-sum {
-  margin-top: 6px;
-  margin-bottom: 12px;
-}
-
-.message-meta {
-  background-color: var(--emph-background);
-  padding: 7px;
-}
-
-.monospace {
-  font-family: monospace;
-  font-size: 1rem;
-}
-.right {
-  text-align: right;
-}
-
-.bold {
-  font-weight: bold;
-}
-
-.italic {
-  font-style: italic;
-}
-
-a {
-  color: var(--link);
-}
-
-.bigger {
-  font-size: 1.1rem;
-}
-
-.light {
-  color: var(--light-text);
-}
-
-.email-body {
-  white-space:pre-line;
-  font-size: 1rem;
-  font-family: monospace;
-}
-
-.footer {
-    text-align: center;
-    font-style: italic;
-    font-size: .9rem;
-}
-
-hr {
-    border: 0;
-    height: 0;
-    color: var(--light-hr);
-    border-top: 1px solid;
-}
-
-h1, h2, h3 {
-     margin: 0;
-}
-
-a:visited {
-}
-
-a:hover{
-  text-decoration: none;
-}
-
-@media only screen and (max-width: 600px) {
-    body {
-        font-size: 16px;
-    }
-}
-/*************************************************************
-
-  PURPLE NUMBERS
-
-*************************************************************/
-
-/* TODO - reconcile with .purplenumber+ in content.css that correctly places the purple numbers
-        - move all this to content.css???                */
-
-.purplenumber  {
-    color: #C8A8FF;
-    font-size: 0.9em;
-    font-weight: bold;
-    font-style: normal;
-    text-transform: lowercase;
-    text-decoration: none;
-    display: inline;
-    float: right;
-    position: relative;
-    bottom: -1em;
-}
-
-
-/* purple number a:psuedo classes cannot use inherit for text decoration or display values*/
-
-
-.purplenumber a:link, .purplenumber a:hover, .purplenumber a:active, .purplenumber a:visited  {
-    color:inherit;text-decoration:none;
-}
-
-
-.purplenumberH0  {
-    color: #C8A8FF;
-    font-size: 0.9em;
-    font-weight: bold;
-    font-style: normal;
-    text-transform: lowercase;
-    float: right;
-    position:relative;
-    top: -1.25em;
-}
-
-
-.purplenumberH0 a:link, .purplenumberH0 a:hover, .purplenumberH0 a:active, .purplenumberH0 a:visited  {
-    color:inherit;text-decoration:none;
-}
-
-
-.purplenumberH1  {
-    color: #C8A8FF;
-    font-size: 0.9em;
-    font-weight: bold;
-    font-style: normal;
-    text-transform: lowercase;
-    display: inline;
-    float: right;
-    margin-bottom: -0.5em;
-}
-
-
-.purplenumberH1 a:link, .purplenumberH1 a:hover, .purplenumberH1 a:active, .purplenumberH1 a:visited  {
-    color:inherit;text-decoration:none;
-}
-
-
-.purplenumberH2  {
-    color: #C8A8FF;
-    font-size: 0.9em;
-    font-weight: bold;
-    font-style: normal;
-    text-transform: lowercase;
-    display: inline;
-    float: right;
-    position: relative;
-    bottom: -0.5px   ;
-}
-
-
-.purplenumberH2 a:link, .purplenumberH2 a:hover, .purplenumberH2 a:active, .purplenumberH2 a:visited  {
-    color:inherit;text-decoration:none;
-}
-
-
-.purplenumberH3  {
-    color: #C8A8FF;
-    font-size: 0.9em;
-    font-weight: bold;
-    font-style: normal;
-    text-transform: lowercase;
-    display: inline;
-    float: right;
-    position: relative;
-    bottom: -0.5px   ;
-}
-
-
-.purplenumberH3 a:link, .purplenumberH3 a:hover, .purplenumberH3 a:active, .purplenumberH3 a:visited  {
-    color:inherit;text-decoration:none;
-}
-
-
-.purplenumberH4  {
-    color: #C8A8FF;
-    font-size: 0.9em;
-    font-weight: bold;
-    font-style: normal;
-    text-transform: lowercase;
-    display: inline;
-    float: right;
-    position: relative;
-    bottom: -0.5px   ;
-}
-
-
-.purplenumberH4 a:link, .purplenumberH4 a:hover, .purplenumberH4 a:active, .purplenumberH4 a:visited  {
-    color:inherit;text-decoration:none;
-}
-
-
-.purplenumberH5  {
-    color: #C8A8FF;
-    font-size: 0.9em;
-    font-weight: bold;
-    font-style: normal;
-    text-transform: lowercase;
-    display: inline;
-    float: right;
-    position: relative;
-    bottom: -0.5px   ;
-}
-
-
-.purplenumberH5 a:link, .purplenumberH5 a:hover, .purplenumberH5 a:active, .purplenumberH5 a:visited  {
-    color:inherit;text-decoration:none;
-}
-
-
-.purplenumberH6  {
-    color: #C8A8FF;
-    font-size: 0.9em;
-    font-weight: bold;
-    font-style: normal;
-    text-transform: lowercase;
-    display: inline;
-    float: right;
-    position: relative;
-    bottom: -0.5px   ;
-}
-
-
-.purplenumberH6 a:link, .purplenumberH6 a:hover, .purplenumberH6 a:active, .purplenumberH6 a:visited  {
-    color:inherit;text-decoration:none;
-}
-
-
-
-
-/***************** END PURPLE NUMBERS STYLING *****************/
-
-
-
-/*************************************************************
-
-     TABLE OF CONTENTS STYLING
-
-*************************************************************/
-
-#floatdiv  {
-    width: 118px;
-    position: absolute;
-    left: -138px;
-    top: 141px; 			 /* was 175px - overridden by targetY in /includes/js/floatingmenu.js */
-    padding: 3px 3px 3px 0px;
-    background: #FFFFFF;
-    border: 2px solid #e0eaf4;
-}
-
-
-#toc-listContainer  {
-    margin:0px 0px 0px 0px;
-    padding-left:1px;
-}
-
-
-.toc-title  { 		   /* preset label "TABLE OF CONTENTS" unknown origin  */
-    visibility: hidden;
-    position: relative;
-    color: #666666;
-    font-size:  0.85em;
-    font-weight:  bold;
-    text-transform: uppercase;
-    text-align: center;
-    margin-bottom:  -26px;
-    background: #e2ebf4;    /* try to get this background behind CONTENTS */
-    width: 100%;
-}
-
-.toc-title::before  {    /* override preset now "CONTENTS"  */
-    visibility: visible;
-    position: absolute;
-    margin-left: -0.5em;
-    content: "CONTENTS" ;
-
-}
-
-
-
-/* For all links COLOR and TEXT-DECORATION*/
-
-
-.toc-top:link,.toc-mainheadingtitle:link, .toc-mainheading a:link, .toc-h0 a:link, .toc-h1 a:link, .toc-h2 a:link, .toc-h3 a:link, .toc-h4 a:link  {
-    text-decoration: none;
-    color:#666666;
-}
-
-
-.toc-top:hover, .toc-mainheadingtitle:hover, .toc-mainheading a:hover, .toc-h1 a:hover, .toc-h2 a:hover, .toc-h3 a:hover, .toc-h4 a:hover  {
-    text-decoration:underline;
-    color:#6699cc;
-}
-
-
-.toc-mainheadingtitle:visited, .toc-mainheading a:visited, .toc-h1 a:visited, .toc-h2 a:visited, .toc-h3 a:visited, .toc-h4 a:visited  {
-    text-decoration:none;
-    color:#666666;
-}
-
-
-.toc-mainheadingtitle, .toc-mainheadingtitle a {
-    font-family:"Helvetica Narrow", Arial, sans-serif;
-    color:#446898;
-    font-size: 0.9em;
-    font-weight: 500;
-    line-height: 1em;
-    margin-bottom: 7px;
-    text-decoration: none;
-    text-align: center;
-    padding-left: 1px;
-}
-
-.toc-mainheadingtitle:hover, .toc-mainheading a:hover, .toc-h0 a:hover  {
-    color:#6699cc;
-}
-
-.toc-top, .toc-top a, .toc-top a:hover, .toc-top a:link   {
-    color: #666666;
-    font-size:  0.675em;
-    float:  right;
-    margin-bottom:  -10px;
-    text-decoration:  none;
-    /*reduce whitespace*/
-}
-
-
-.toc-h0, .toc-h1, .toc-h2, .toc-h3, .toc-h4  {
-    font-size: 0.725em;
-    color: #666666;
-    line-height: 1.1em;
-    margin-bottom: 5px;
-}
-
-
-/*For level indending */
-
-.toc-h2  { padding-left: 7px; }
-
-.toc-h3  { padding-left: 14px; }
-
-.toc-h4  { padding-left: 21px; }
-
-
-.toc-purplenumber, .toc-purplenumberH1, .toc-purplenumberH2, .toc-purplenumberH3, .toc-purplenumberH4, .toc-purplenumberH5  {
-    color: #C8A8FF;
-    font-size: 0.85em;
-    font-weight: bold;
-    font-style: normal;
-    text-transform: lowercase;
-    text-decoration: none !important;
-    display: inline;
-    float: right;
-    position: relative;
-    bottom: -0.5px   ;
-}
-
-/********************* END TOC STYLING *********************/
-
-
-/******* DEFINE left panel, main content area, right panel (if any) *******/
-
-.contentItemTable  {
-    border:0px;
-}
-
-.contentItemTableLeft  {
-    vertical-align: top;
-    padding: 0px 10px 0px 0px;
-}
-
-.contentItemTableRight  {
-    width: 180px;
-    vertical-align: top;
-}
-
-.contentItemTableRight div  {
-    border:3px outset #007297;
-    padding:10px;
-    text-align: center;
-}
-
-.contentItemTableRight div p  {
-    font-size: 0.9em;
-}
-
-#mailList   {
-    padding: 18px 12px;
-}
-
-script {
-    display: none;
-}
-
-.after-element:after {
-    content: " [id ]";
-    /* Additional styles for the :after element */
-}
-/* Dropdown container */
-.dropdown {
-    position: relative;
-    display: inline-block;
-}
-
-/* Dropdown content (hidden by default) */
-.dropdown-content {
-    display: none;
-    position: absolute;
-    background-color: #f9f9f9;
-    min-width: 110px;
-    box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
-    z-index: 1;
-}
-
-/* Links inside the dropdown */
-.dropdown-content a {
-    color: black;
-    text-decoration: none;
-    display: block;
-}
-
-/* Change color of dropdown links on hover */
-.dropdown-content a:hover {
-    background-color: #f1f1f1;
-}
-
-/* Show the dropdown menu on button click */
-.dropdown:hover .dropdown-content {
-    display: block;
-}
-
-/* Style for the Jump Item text field */
-#textfield {
-    width: 50px; /* Adjust the width as needed */
-}
-
-/* Style for the pop-up */
-.popup {
-    display: none;
-    position: fixed;
-    left: 50%;
-    top: 50%;
-    transform: translate(-50%, -50%);
-    border: 1px solid #000;
-    background-color: #fff;
-    box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
-    z-index: 10;
-    padding: 20px;
-    width: 300px;
-}
-
-.popup-header {
-    font-weight: bold;
-    margin-bottom: 10px;
-}
-
-.popup-content {
-    margin-bottom: 10px;
-}
-
-.popup-content input {
-    margin-right: 10px;
-}
-
-.popup-close {
-    cursor: pointer;
-    color: red;
-    font-weight: bold;
-}
-
-.transparent {
-    color: transparent;
-}

+ 0 - 14
src/templates/rss.url

@@ -1,14 +0,0 @@
-data:image/svg+xml,<?xml version="1.0" encoding="UTF-8"?>
-<svg xmlns="http://www.w3.org/2000/svg"
-     id="RSSicon"
-     viewBox="0 0 8 8" width="16" height="16">
-  <title>RSS feed icon</title>
-  <style type="text/css">
-    .button {stroke: none; fill: orange;}
-    .symbol {stroke: none; fill: white;}
-  </style>
-  <rect   class="button" width="8" height="8" rx="1.5" />
-  <circle class="symbol" cx="2" cy="6" r="1" />
-  <path   class="symbol" d="m 1,4 a 3,3 0 0 1 3,3 h 1 a 4,4 0 0 0 -4,-4 z" />
-  <path   class="symbol" d="m 1,2 a 5,5 0 0 1 5,5 h 1 a 6,6 0 0 0 -6,-6 z" />
-</svg>