use std::{fs::File, io, io::Read}; use mailparse::{ addrparse_header, dateparse, parse_mail, MailHeaderMap, MailParseError, ParsedMail, }; use sqlx::postgres::PgPool; use thiserror::Error; use tracing::info; #[derive(Error, Debug)] pub enum MailError { #[error("missing from header")] MissingFrom, #[error("missing from header display name")] MissingFromDisplayName, #[error("missing subject header")] MissingSubject, #[error("missing html part")] MissingHtmlPart, #[error("missing message ID")] MissingMessageId, #[error("missing date")] MissingDate, #[error("DB error {0}")] SqlxError(#[from] sqlx::Error), #[error("IO error {0}")] IOError(#[from] std::io::Error), #[error("mail parse error {0}")] MailParseError(#[from] MailParseError), } pub async fn read_mail_to_db(pool: &PgPool, path: &str) -> Result<(), MailError> { let mut file = File::open(path)?; let mut buffer = Vec::new(); file.read_to_end(&mut buffer)?; let m = parse_mail(&buffer)?; let subject = m .headers .get_first_value("subject") .ok_or(MailError::MissingSubject)?; let from = addrparse_header( m.headers .get_first_header("from") .ok_or(MailError::MissingFrom)?, )?; let from = from.extract_single_info().ok_or(MailError::MissingFrom)?; let name = from.display_name.ok_or(MailError::MissingFromDisplayName)?; let slug = name.to_lowercase().replace(' ', "-"); let url = from.addr; let message_id = m .headers .get_first_value("Message-ID") .ok_or(MailError::MissingMessageId)?; let uid = &message_id; let feed_id = find_feed(&pool, &name, &slug, &url).await?; let date = dateparse( &m.headers .get_first_value("Date") .ok_or(MailError::MissingDate)?, )?; println!("Feed: {feed_id} Subject: {}", subject); if let Some(m) = first_html(&m) { let body = m.get_body()?; info!("add email {slug} {subject} {message_id} {date} {uid} {url}"); } else { return Err(MailError::MissingHtmlPart.into()); } Ok(()) } fn first_html<'m>(m: &'m ParsedMail<'m>) -> Option<&'m ParsedMail<'m>> { for ele in m.parts() { if ele.ctype.mimetype == "text/html" { return Some(ele); } } None } async fn find_feed(pool: &PgPool, name: &str, slug: &str, url: &str) -> Result { match sqlx::query!( r#" SELECT id FROM feed WHERE slug = $1 "#, slug ) .fetch_one(pool) .await { Err(sqlx::Error::RowNotFound) => { let rec = sqlx::query!( r#" INSERT INTO feed ( name, slug, url, homepage, selector ) VALUES ( $1, $2, $3, '', '' ) RETURNING id "#, name, slug, url ) .fetch_one(pool) .await?; return Ok(rec.id); } Ok(rec) => return Ok(rec.id), Err(e) => return Err(e.into()), }; }