kovi-plugin-mail 0.1.0

基于 Kovi 框架的新邮件提醒插件
Documentation
mod config;

use std::{collections::HashMap, sync::Arc};

use async_imap::{Session, types::Fetch};
use async_native_tls::TlsStream;
use futures::TryStreamExt;
use kovi::{
    PluginBuilder as plugin, RuntimeBot,
    chrono::{DateTime, FixedOffset, Utc},
    log::{info, warn},
    tokio::{net::TcpStream, sync::RwLock, time},
};

use crate::config::MailConfig;

type MailSession = Session<TlsStream<TcpStream>>;
type MailSessions = HashMap<String, Arc<RwLock<MailSession>>>;

struct State {
    date: DateTime<FixedOffset>,
}

#[derive(Debug)]
struct MailInfo {
    subject: String,
    date: DateTime<FixedOffset>,
}

#[kovi::plugin]
async fn main() {
    let bot = plugin::get_runtime_bot();
    let config = config::init(bot.get_data_path()).await.unwrap();

    let sessions: Arc<RwLock<MailSessions>> = Arc::new(RwLock::new(MailSessions::new()));

    info!("[mail] Connecting to mail servers.");

    for cfg in config.mails {
        let state = State {
            date: Utc::now().fixed_offset(),
        };
        let state = Arc::new(RwLock::new(state));

        info!("[mail] {} initialized.", &cfg.email);

        plugin::cron(&format!("0 0/{} * * * ?", config.interval), {
            let bot = bot.clone();
            let state = state.clone();
            let sessions = sessions.clone();
            move || check_mails(cfg.clone(), bot.clone(), sessions.clone(), state.clone())
        })
        .unwrap();
    }

    plugin::drop({
        let sessions = sessions.clone();
        move || on_drop(sessions.clone())
    });

    info!("[mail] Ready to put eyes on mails!")
}

async fn pull_mails(session: &Arc<RwLock<MailSession>>) -> Result<MailInfo, String> {
    let mut session = session.write().await;
    let mails = session
        .fetch("*", "ALL")
        .await
        .map_err(|e| format!("Error when pulling mails: {}", e))?;
    let messages: Vec<Fetch> = mails
        .try_collect()
        .await
        .map_err(|e| format!("Error when pulling mails: {}", e))?;
    let fetch = messages.last().ok_or("No available mail found!")?;
    Ok(MailInfo {
        subject: {
            let sub_bytes = &fetch
                .envelope()
                .ok_or("No envelop found for the latest mail!")?
                .subject
                .clone()
                .ok_or("No subject found for the latest mail!")?;
            encoded_words::decode(
                str::from_utf8(&sub_bytes)
                    .map_err(|e| format!("Invalid UTF-8 Encoding of Mail Subject: {}", e))?,
            )
            .map_err(|e| format!("Error when decoding subject: {}", e))?
            .decoded
        },
        date: fetch
            .internal_date()
            .ok_or("No date found for the latest mail!")?,
    })
}

async fn check_mails(
    cfg: MailConfig,
    bot: Arc<RuntimeBot>,
    sessions: Arc<RwLock<MailSessions>>,
    state: Arc<RwLock<State>>,
) {
    info!("[mail] Checking mails...");

    let session = match time::timeout(time::Duration::from_secs(10), cfg.build_session()).await {
        Ok(session) => session,
        Err(e) => {
            warn!("[mail] Timeout when connecting to mail server: {e}.");
            return;
        }
    };

    if let Err(e) = session {
        warn!("[mail] Failed to connect to mail server: {e}.");
        return;
    }

    let session = Arc::new(RwLock::new(session.unwrap()));
    sessions
        .write()
        .await
        .insert(cfg.email.clone(), session.clone());

    info!("[mail] Connected to {}.", &cfg.email);

    let mail = pull_mails(&session).await;
    if mail.is_err() {
        warn!("[mail] <{}> {}", cfg.email, mail.unwrap_err());
        return;
    }

    let mail = mail.unwrap();

    let mut state = state.write().await;

    if mail.date > state.date {
        state.date = mail.date;
        info!("[mail] New mail detected!");
        let message = format!("{} 收到新邮件!\n{}", &cfg.email, mail.subject);
        if let Some(users) = &cfg.notify_users {
            for user in users {
                bot.send_private_msg(user.to_owned(), message.clone());
            }
        }
        if let Some(groups) = &cfg.notify_groups {
            for group in groups {
                bot.send_private_msg(group.to_owned(), message.clone());
            }
        }
    } else {
        info!("[mail] No new mail detected.");
    }

    if let Err(e) = session.write().await.logout().await {
        warn!("[mail] Error when logging out: {e}.");
    } else {
        info!("[mail] Logged out from {}.", &cfg.email);
    }
    sessions.write().await.remove(&cfg.email);
}

async fn on_drop(sessions: Arc<RwLock<MailSessions>>) {
    let mut sessions = sessions.write().await;
    for (_, s) in sessions.iter() {
        let mut session = s.write().await;
        session.logout().await.unwrap();
    }
    sessions.clear();
    info!("[mail] Logged out mail sessions");
}