115 lines
3.0 KiB
Rust
115 lines
3.0 KiB
Rust
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<i32, MailError> {
|
|
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()),
|
|
};
|
|
}
|